diff --git a/api/docs/http_file_content.md b/api/docs/http_file_content.md index 8b13417c7..694895c9d 100644 --- a/api/docs/http_file_content.md +++ b/api/docs/http_file_content.md @@ -1,70 +1,70 @@ File Content ============= The `file_content` endpoint returns the contents of the specified file. Find ---- Get a file. GET /:environment/file_content/:mount_point/:name `:mount_point` is one of mounts configured in the `fileserver.conf`. See [the puppet file server guide](http://docs.puppetlabs.com/guides/file_serving.html) for more information about how mount points work. `:name` is the path to the file within the `:mount_point` that is requested. ### Supported HTTP Methods GET ### Supported Response Formats raw (the raw binary content) ### Parameters None ### Notes ### Responses #### File found GET /env/file_content/modules/example/my_file Accept: raw HTTP/1.1 200 OK - Content-Type: application/x-raw + Content-Type: application/octet-stream Content-Length: 16 this is my file #### File not found GET /env/file_content/modules/example/not_found Accept: raw HTTP/1.1 404 Not Found Content-Type: text/plain Not Found: Could not find file_content modules/example/not_found #### No file name given GET /env/file_content/ HTTP/1.1 400 Bad Request Content-Type: text/plain No request key specified in /env/file_content/ Schema ------ A `file_content` response body is not structured data according to any standard scheme such as json/pson/yaml, so no schema is applicable. diff --git a/lib/puppet/file_serving/content.rb b/lib/puppet/file_serving/content.rb index 6b48cc640..fc63f4484 100644 --- a/lib/puppet/file_serving/content.rb +++ b/lib/puppet/file_serving/content.rb @@ -1,43 +1,43 @@ require 'puppet/indirector' require 'puppet/file_serving' require 'puppet/file_serving/base' # A class that handles retrieving file contents. # It only reads the file when its content is specifically # asked for. class Puppet::FileServing::Content < Puppet::FileServing::Base extend Puppet::Indirector indirects :file_content, :terminus_class => :selector attr_writer :content def self.supported_formats - [:raw] + [:binary] end - def self.from_raw(content) + def self.from_binary(content) instance = new("/this/is/a/fake/path") instance.content = content instance end # This is no longer used, but is still called by the file server implementations when interacting # with their model abstraction. def collect(source_permissions = nil) end # Read the content of our file in. def content unless @content # This stat can raise an exception, too. raise(ArgumentError, "Cannot read the contents of links unless following links") if stat.ftype == "symlink" @content = Puppet::FileSystem.binread(full_path) end @content end - def to_raw + def to_binary File.new(full_path, "rb") end end diff --git a/lib/puppet/network/formats.rb b/lib/puppet/network/formats.rb index f482834ab..552f77369 100644 --- a/lib/puppet/network/formats.rb +++ b/lib/puppet/network/formats.rb @@ -1,163 +1,160 @@ require 'puppet/network/format_handler' Puppet::Network::FormatHandler.create_serialized_formats(:msgpack, :weight => 20, :mime => "application/x-msgpack", :required_methods => [:render_method, :intern_method], :intern_method => :from_data_hash) do confine :feature => :msgpack def intern(klass, text) data = MessagePack.unpack(text) return data if data.is_a?(klass) klass.from_data_hash(data) end def intern_multiple(klass, text) MessagePack.unpack(text).collect do |data| klass.from_data_hash(data) end end def render_multiple(instances) instances.to_msgpack end end Puppet::Network::FormatHandler.create_serialized_formats(:yaml) do def intern(klass, text) data = YAML.load(text) data_to_instance(klass, data) end def intern_multiple(klass, text) data = YAML.load(text) unless data.respond_to?(:collect) raise Puppet::Network::FormatHandler::FormatError, "Serialized YAML did not contain a collection of instances when calling intern_multiple" end data.collect do |datum| data_to_instance(klass, datum) end end def data_to_instance(klass, data) return data if data.is_a?(klass) unless data.is_a? Hash raise Puppet::Network::FormatHandler::FormatError, "Serialized YAML did not contain a valid instance of #{klass}" end klass.from_data_hash(data) end def render(instance) instance.to_yaml end # Yaml monkey-patches Array, so this works. def render_multiple(instances) instances.to_yaml end def supported?(klass) true end end Puppet::Network::FormatHandler.create(:s, :mime => "text/plain", :extension => "txt") -Puppet::Network::FormatHandler.create(:binary, :mime => "application/octet-stream") - -# A very low-weight format so it'll never get chosen automatically. -Puppet::Network::FormatHandler.create(:raw, :mime => "application/x-raw", :weight => 1) do +Puppet::Network::FormatHandler.create(:binary, :mime => "application/octet-stream", :weight => 1) do def intern_multiple(klass, text) raise NotImplementedError end def render_multiple(instances) raise NotImplementedError end # LAK:NOTE The format system isn't currently flexible enough to handle # what I need to support raw formats just for individual instances (rather # than both individual and collections), but we don't yet have enough data # to make a "correct" design. # So, we hack it so it works for singular but fail if someone tries it # on plurals. def supported?(klass) true end end Puppet::Network::FormatHandler.create_serialized_formats(:pson, :weight => 10, :required_methods => [:render_method, :intern_method], :intern_method => :from_data_hash) do def intern(klass, text) data_to_instance(klass, PSON.parse(text)) end def intern_multiple(klass, text) PSON.parse(text).collect do |data| data_to_instance(klass, data) end end # PSON monkey-patches Array, so this works. def render_multiple(instances) instances.to_pson end # If they pass class information, we want to ignore it. # This is required for compatibility with Puppet 3.x def data_to_instance(klass, data) if data.is_a?(Hash) and d = data['data'] data = d end return data if data.is_a?(klass) klass.from_data_hash(data) end end # This is really only ever going to be used for Catalogs. Puppet::Network::FormatHandler.create_serialized_formats(:dot, :required_methods => [:render_method]) Puppet::Network::FormatHandler.create(:console, :mime => 'text/x-console-text', :weight => 0) do def json @json ||= Puppet::Network::FormatHandler.format(:pson) end def render(datum) # String to String return datum if datum.is_a? String return datum if datum.is_a? Numeric # Simple hash to table if datum.is_a? Hash and datum.keys.all? { |x| x.is_a? String or x.is_a? Numeric } output = '' column_a = datum.empty? ? 2 : datum.map{ |k,v| k.to_s.length }.max + 2 datum.sort_by { |k,v| k.to_s } .each do |key, value| output << key.to_s.ljust(column_a) output << json.render(value). chomp.gsub(/\n */) { |x| x + (' ' * column_a) } output << "\n" end return output end # Print one item per line for arrays if datum.is_a? Array output = '' datum.each do |item| output << item.to_s output << "\n" end return output end # ...or pretty-print the inspect outcome. return json.render(datum) end def render_multiple(data) data.collect(&:render).join("\n") end end diff --git a/lib/puppet/type/file/content.rb b/lib/puppet/type/file/content.rb index 77bda913d..5e4dcdf25 100644 --- a/lib/puppet/type/file/content.rb +++ b/lib/puppet/type/file/content.rb @@ -1,242 +1,242 @@ require 'net/http' require 'uri' require 'tempfile' require 'puppet/util/checksums' require 'puppet/network/http' require 'puppet/network/http/api/indirected_routes' require 'puppet/network/http/compression' module Puppet Puppet::Type.type(:file).newproperty(:content) do include Puppet::Util::Diff include Puppet::Util::Checksums include Puppet::Network::HTTP::Compression.module attr_reader :actual_content desc <<-'EOT' The desired contents of a file, as a string. This attribute is mutually exclusive with `source` and `target`. Newlines and tabs can be specified in double-quoted strings using standard escaped syntax --- \n for a newline, and \t for a tab. With very small files, you can construct content strings directly in the manifest... define resolve(nameserver1, nameserver2, domain, search) { $str = "search $search domain $domain nameserver $nameserver1 nameserver $nameserver2 " file { "/etc/resolv.conf": content => "$str", } } ...but for larger files, this attribute is more useful when combined with the [template](http://docs.puppetlabs.com/references/latest/function.html#template) or [file](http://docs.puppetlabs.com/references/latest/function.html#file) function. EOT # Store a checksum as the value, rather than the actual content. # Simplifies everything. munge do |value| if value == :absent value elsif checksum?(value) # XXX This is potentially dangerous because it means users can't write a file whose # entire contents are a plain checksum value else @actual_content = value resource.parameter(:checksum).sum(value) end end # Checksums need to invert how changes are printed. def change_to_s(currentvalue, newvalue) # Our "new" checksum value is provided by the source. if source = resource.parameter(:source) and tmp = source.checksum newvalue = tmp end if currentvalue == :absent return "defined content as '#{newvalue}'" elsif newvalue == :absent return "undefined content from '#{currentvalue}'" else return "content changed '#{currentvalue}' to '#{newvalue}'" end end def checksum_type if source = resource.parameter(:source) result = source.checksum else result = resource[:checksum] end if result =~ /^\{(\w+)\}.+/ return $1.to_sym else return result end end def length (actual_content and actual_content.length) || 0 end def content self.should end # Override this method to provide diffs if asked for. # Also, fix #872: when content is used, and replace is true, the file # should be insync when it exists def insync?(is) if resource.should_be_file? return false if is == :absent else if resource[:ensure] == :present and resource[:content] and s = resource.stat resource.warning "Ensure set to :present but file type is #{s.ftype} so no content will be synced" end return true end return true if ! @resource.replace? result = super if ! result and Puppet[:show_diff] and resource.show_diff? write_temporarily do |path| send @resource[:loglevel], "\n" + diff(@resource[:path], path) end end result end def retrieve return :absent unless stat = @resource.stat ftype = stat.ftype # Don't even try to manage the content on directories or links return nil if ["directory","link"].include?(ftype) begin resource.parameter(:checksum).sum_file(resource[:path]) rescue => detail raise Puppet::Error, "Could not read #{ftype} #{@resource.title}: #{detail}", detail.backtrace end end # Make sure we're also managing the checksum property. def should=(value) # treat the value as a bytestring, in Ruby versions that support it, regardless of the encoding # in which it has been supplied value = value.dup.force_encoding(Encoding::ASCII_8BIT) if value.respond_to?(:force_encoding) @resource.newattr(:checksum) unless @resource.parameter(:checksum) super end # Just write our content out to disk. def sync return_event = @resource.stat ? :file_changed : :file_created # We're safe not testing for the 'source' if there's no 'should' # because we wouldn't have gotten this far if there weren't at least # one valid value somewhere. @resource.write(:content) return_event end def write_temporarily tempfile = Tempfile.new("puppet-file") tempfile.open write(tempfile) tempfile.close yield tempfile.path tempfile.delete end def write(file) resource.parameter(:checksum).sum_stream { |sum| each_chunk_from(actual_content || resource.parameter(:source)) { |chunk| sum << chunk file.print chunk } } end # the content is munged so if it's a checksum source_or_content is nil # unless the checksum indirectly comes from source def each_chunk_from(source_or_content) if source_or_content.is_a?(String) yield source_or_content elsif content_is_really_a_checksum? && source_or_content.nil? yield read_file_from_filebucket elsif source_or_content.nil? yield '' elsif Puppet[:default_file_terminus] == :file_server yield source_or_content.content elsif source_or_content.local? chunk_file_from_disk(source_or_content) { |chunk| yield chunk } else chunk_file_from_source(source_or_content) { |chunk| yield chunk } end end private def content_is_really_a_checksum? checksum?(should) end def chunk_file_from_disk(source_or_content) File.open(source_or_content.full_path, "rb") do |src| while chunk = src.read(8192) yield chunk end end end def get_from_source(source_or_content, &block) source = source_or_content.metadata.source request = Puppet::Indirector::Request.new(:file_content, :find, source, nil, :environment => resource.catalog.environment_instance) request.do_request(:fileserver) do |req| connection = Puppet::Network::HttpPool.http_instance(req.server, req.port) - connection.request_get(Puppet::Network::HTTP::API::IndirectedRoutes.request_to_uri(req), add_accept_encoding({"Accept" => "raw"}), &block) + connection.request_get(Puppet::Network::HTTP::API::IndirectedRoutes.request_to_uri(req), add_accept_encoding({"Accept" => "binary"}), &block) end end def chunk_file_from_source(source_or_content) get_from_source(source_or_content) do |response| case response.code when /^2/; uncompress(response) { |uncompressor| response.read_body { |chunk| yield uncompressor.uncompress(chunk) } } else # Raise the http error if we didn't get a 'success' of some kind. message = "Error #{response.code} on SERVER: #{(response.body||'').empty? ? response.message : uncompress_body(response)}" raise Net::HTTPError.new(message, response) end end end def read_file_from_filebucket raise "Could not get filebucket from file" unless dipper = resource.bucket sum = should.sub(/\{\w+\}/, '') dipper.getfile(sum) rescue => detail self.fail Puppet::Error, "Could not retrieve content for #{should} from filebucket: #{detail}", detail end end end diff --git a/spec/lib/puppet/indirector_testing.rb b/spec/lib/puppet/indirector_testing.rb index 9f9b2207a..0298c646b 100644 --- a/spec/lib/puppet/indirector_testing.rb +++ b/spec/lib/puppet/indirector_testing.rb @@ -1,27 +1,27 @@ require 'puppet/indirector' class Puppet::IndirectorTesting extend Puppet::Indirector indirects :indirector_testing # We should have some way to identify if we got a valid object back with the # current values, no? attr_accessor :value alias_method :name, :value alias_method :name=, :value= def initialize(value) self.value = value end - def self.from_raw(raw) + def self.from_binary(raw) new(raw) end def self.from_data_hash(data) new(data['value']) end def to_data_hash { 'value' => value } end end diff --git a/spec/unit/file_bucket/file_spec.rb b/spec/unit/file_bucket/file_spec.rb index 13aafd050..441b02dbe 100755 --- a/spec/unit/file_bucket/file_spec.rb +++ b/spec/unit/file_bucket/file_spec.rb @@ -1,60 +1,60 @@ #! /usr/bin/env ruby require 'spec_helper' require 'puppet/file_bucket/file' describe Puppet::FileBucket::File, :uses_checksums => true do include PuppetSpec::Files # this is the default from spec_helper, but it keeps getting reset at odd times let(:bucketdir) { Puppet[:bucketdir] = tmpdir('bucket') } it "defaults to serializing to `:binary`" do expect(Puppet::FileBucket::File.default_format).to eq(:binary) end - it "accepts s" do + it "accepts binary" do expect(Puppet::FileBucket::File.supported_formats).to include(:binary) end describe "making round trips through network formats" do with_digest_algorithms do - it "can make a round trip through `s`" do + it "can make a round trip through `binary`" do file = Puppet::FileBucket::File.new(plaintext) tripped = Puppet::FileBucket::File.convert_from(:binary, file.render) expect(tripped.contents).to eq(plaintext) end end end it "should require contents to be a string" do expect { Puppet::FileBucket::File.new(5) }.to raise_error(ArgumentError, /contents must be a String or Pathname, got a Fixnum$/) end it "should complain about options other than :bucket_path" do expect { Puppet::FileBucket::File.new('5', :crazy_option => 'should not be passed') }.to raise_error(ArgumentError, /Unknown option\(s\): crazy_option/) end with_digest_algorithms do it "it uses #{metadata[:digest_algorithm]} as the configured digest algorithm" do file = Puppet::FileBucket::File.new(plaintext) expect(file.contents).to eq(plaintext) expect(file.checksum_type).to eq(digest_algorithm) expect(file.checksum).to eq("{#{digest_algorithm}}#{checksum}") expect(file.name).to eq("#{digest_algorithm}/#{checksum}") end end describe "when using back-ends" do it "should redirect using Puppet::Indirector" do expect(Puppet::Indirector::Indirection.instance(:file_bucket_file).model).to equal(Puppet::FileBucket::File) end it "should have a :save instance method" do expect(Puppet::FileBucket::File.indirection).to respond_to(:save) end end end diff --git a/spec/unit/file_serving/content_spec.rb b/spec/unit/file_serving/content_spec.rb index a1e8f6585..ec446cd27 100755 --- a/spec/unit/file_serving/content_spec.rb +++ b/spec/unit/file_serving/content_spec.rb @@ -1,100 +1,100 @@ #! /usr/bin/env ruby require 'spec_helper' require 'puppet/file_serving/content' describe Puppet::FileServing::Content do let(:path) { File.expand_path('/path') } it "should be a subclass of Base" do expect(Puppet::FileServing::Content.superclass).to equal(Puppet::FileServing::Base) end it "should indirect file_content" do expect(Puppet::FileServing::Content.indirection.name).to eq(:file_content) end - it "should only support the raw format" do - expect(Puppet::FileServing::Content.supported_formats).to eq([:raw]) + it "should only support the binary format" do + expect(Puppet::FileServing::Content.supported_formats).to eq([:binary]) end it "should have a method for collecting its attributes" do expect(Puppet::FileServing::Content.new(path)).to respond_to(:collect) end it "should not retrieve and store its contents when its attributes are collected" do content = Puppet::FileServing::Content.new(path) result = "foo" File.expects(:read).with(path).never content.collect expect(content.instance_variable_get("@content")).to be_nil end it "should have a method for setting its content" do content = Puppet::FileServing::Content.new(path) expect(content).to respond_to(:content=) end it "should make content available when set externally" do content = Puppet::FileServing::Content.new(path) content.content = "foo/bar" expect(content.content).to eq("foo/bar") end - it "should be able to create a content instance from raw file contents" do - expect(Puppet::FileServing::Content).to respond_to(:from_raw) + it "should be able to create a content instance from binary file contents" do + expect(Puppet::FileServing::Content).to respond_to(:from_binary) end - it "should create an instance with a fake file name and correct content when converting from raw" do + it "should create an instance with a fake file name and correct content when converting from binary" do instance = mock 'instance' Puppet::FileServing::Content.expects(:new).with("/this/is/a/fake/path").returns instance instance.expects(:content=).with "foo/bar" - expect(Puppet::FileServing::Content.from_raw("foo/bar")).to equal(instance) + expect(Puppet::FileServing::Content.from_binary("foo/bar")).to equal(instance) end - it "should return an opened File when converted to raw" do + it "should return an opened File when converted to binary" do content = Puppet::FileServing::Content.new(path) File.expects(:new).with(path, "rb").returns :file - expect(content.to_raw).to eq(:file) + expect(content.to_binary).to eq(:file) end end describe Puppet::FileServing::Content, "when returning the contents" do let(:path) { File.expand_path('/my/path') } let(:content) { Puppet::FileServing::Content.new(path, :links => :follow) } it "should fail if the file is a symlink and links are set to :manage" do content.links = :manage Puppet::FileSystem.expects(:lstat).with(path).returns stub("stat", :ftype => "symlink") expect { content.content }.to raise_error(ArgumentError) end it "should fail if a path is not set" do expect { content.content }.to raise_error(Errno::ENOENT) end it "should raise Errno::ENOENT if the file is absent" do content.path = File.expand_path("/there/is/absolutely/no/chance/that/this/path/exists") expect { content.content }.to raise_error(Errno::ENOENT) end it "should return the contents of the path if the file exists" do Puppet::FileSystem.expects(:stat).with(path).returns(stub('stat', :ftype => 'file')) Puppet::FileSystem.expects(:binread).with(path).returns(:mycontent) expect(content.content).to eq(:mycontent) end it "should cache the returned contents" do Puppet::FileSystem.expects(:stat).with(path).returns(stub('stat', :ftype => 'file')) Puppet::FileSystem.expects(:binread).with(path).returns(:mycontent) content.content # The second run would throw a failure if the content weren't being cached. content.content end end diff --git a/spec/unit/network/formats_spec.rb b/spec/unit/network/formats_spec.rb index b7cb7af86..269fd84ac 100755 --- a/spec/unit/network/formats_spec.rb +++ b/spec/unit/network/formats_spec.rb @@ -1,340 +1,330 @@ #! /usr/bin/env ruby require 'spec_helper' require 'puppet/network/formats' class PsonTest attr_accessor :string def ==(other) string == other.string end def self.from_data_hash(data) new(data) end def initialize(string) @string = string end def to_pson(*args) { 'type' => self.class.name, 'data' => @string }.to_pson(*args) end end describe "Puppet Network Format" do it "should include a msgpack format", :if => Puppet.features.msgpack? do expect(Puppet::Network::FormatHandler.format(:msgpack)).not_to be_nil end describe "msgpack", :if => Puppet.features.msgpack? do before do @msgpack = Puppet::Network::FormatHandler.format(:msgpack) end it "should have its mime type set to application/x-msgpack" do expect(@msgpack.mime).to eq("application/x-msgpack") end it "should have a weight of 20" do expect(@msgpack.weight).to eq(20) end it "should fail when one element does not have a from_data_hash" do expect do @msgpack.intern_multiple(Hash, MessagePack.pack(["foo"])) end.to raise_error(NoMethodError) end it "should be able to serialize a catalog" do cat = Puppet::Resource::Catalog.new('foo', Puppet::Node::Environment.create(:testing, [])) cat.add_resource(Puppet::Resource.new(:file, 'my_file')) catunpack = MessagePack.unpack(cat.to_msgpack) expect(catunpack).to include( "tags"=>[], "name"=>"foo", "version"=>nil, "environment"=>"testing", "edges"=>[], "classes"=>[] ) expect(catunpack["resources"][0]).to include( "type"=>"File", "title"=>"my_file", "exported"=>false ) expect(catunpack["resources"][0]["tags"]).to include( "file", "my_file" ) end end describe "yaml" do before do @yaml = Puppet::Network::FormatHandler.format(:yaml) end it "should have its mime type set to text/yaml" do expect(@yaml.mime).to eq("text/yaml") end it "should be supported on Strings" do expect(@yaml).to be_supported(String) end it "should render by calling 'to_yaml' on the instance" do instance = mock 'instance' instance.expects(:to_yaml).returns "foo" expect(@yaml.render(instance)).to eq("foo") end it "should render multiple instances by calling 'to_yaml' on the array" do instances = [mock('instance')] instances.expects(:to_yaml).returns "foo" expect(@yaml.render_multiple(instances)).to eq("foo") end it "should deserialize YAML" do expect(@yaml.intern(String, YAML.dump("foo"))).to eq("foo") end it "should deserialize symbols as strings" do expect { @yaml.intern(String, YAML.dump(:foo))}.to raise_error(Puppet::Network::FormatHandler::FormatError) end it "should load from yaml when deserializing an array" do text = YAML.dump(["foo"]) expect(@yaml.intern_multiple(String, text)).to eq(["foo"]) end it "fails intelligibly instead of calling to_pson with something other than a hash" do expect do @yaml.intern(Puppet::Node, '') end.to raise_error(Puppet::Network::FormatHandler::FormatError, /did not contain a valid instance/) end it "fails intelligibly when intern_multiple is called and yaml doesn't decode to an array" do expect do @yaml.intern_multiple(Puppet::Node, '') end.to raise_error(Puppet::Network::FormatHandler::FormatError, /did not contain a collection/) end it "fails intelligibly instead of calling to_pson with something other than a hash when interning multiple" do expect do @yaml.intern_multiple(Puppet::Node, YAML.dump(["hello"])) end.to raise_error(Puppet::Network::FormatHandler::FormatError, /did not contain a valid instance/) end end - describe "binary" do - before do - @binary = Puppet::Network::FormatHandler.format(:binary) - end - - it "should have its mimetype set to application/octet-stream" do - @binary.mime.should == "application/octet-stream" - end - end - describe "plaintext" do before do @text = Puppet::Network::FormatHandler.format(:s) end it "should have its mimetype set to text/plain" do expect(@text.mime).to eq("text/plain") end it "should use 'txt' as its extension" do expect(@text.extension).to eq("txt") end end describe "dot" do before do @dot = Puppet::Network::FormatHandler.format(:dot) end it "should have its mimetype set to text/dot" do expect(@dot.mime).to eq("text/dot") end end - describe Puppet::Network::FormatHandler.format(:raw) do + describe Puppet::Network::FormatHandler.format(:binary) do before do - @format = Puppet::Network::FormatHandler.format(:raw) + @format = Puppet::Network::FormatHandler.format(:binary) end it "should exist" do expect(@format).not_to be_nil end - it "should have its mimetype set to application/x-raw" do - expect(@format.mime).to eq("application/x-raw") + it "should have its mimetype set to application/octet-stream" do + expect(@format.mime).to eq("application/octet-stream") end it "should always be supported" do expect(@format).to be_supported(String) end it "should fail if its multiple_render method is used" do expect { @format.render_multiple("foo") }.to raise_error(NotImplementedError) end it "should fail if its multiple_intern method is used" do expect { @format.intern_multiple(String, "foo") }.to raise_error(NotImplementedError) end it "should have a weight of 1" do expect(@format.weight).to eq(1) end end it "should include a pson format" do expect(Puppet::Network::FormatHandler.format(:pson)).not_to be_nil end describe "pson" do before do @pson = Puppet::Network::FormatHandler.format(:pson) end it "should have its mime type set to text/pson" do expect(Puppet::Network::FormatHandler.format(:pson).mime).to eq("text/pson") end it "should require the :render_method" do expect(Puppet::Network::FormatHandler.format(:pson).required_methods).to be_include(:render_method) end it "should require the :intern_method" do expect(Puppet::Network::FormatHandler.format(:pson).required_methods).to be_include(:intern_method) end it "should have a weight of 10" do expect(@pson.weight).to eq(10) end describe "when supported" do it "should render by calling 'to_pson' on the instance" do instance = PsonTest.new("foo") instance.expects(:to_pson).returns "foo" expect(@pson.render(instance)).to eq("foo") end it "should render multiple instances by calling 'to_pson' on the array" do instances = [mock('instance')] instances.expects(:to_pson).returns "foo" expect(@pson.render_multiple(instances)).to eq("foo") end it "should intern by calling 'PSON.parse' on the text and then using from_data_hash to convert the data into an instance" do text = "foo" PSON.expects(:parse).with("foo").returns("type" => "PsonTest", "data" => "foo") PsonTest.expects(:from_data_hash).with("foo").returns "parsed_pson" expect(@pson.intern(PsonTest, text)).to eq("parsed_pson") end it "should not render twice if 'PSON.parse' creates the appropriate instance" do text = "foo" instance = PsonTest.new("foo") PSON.expects(:parse).with("foo").returns(instance) PsonTest.expects(:from_data_hash).never expect(@pson.intern(PsonTest, text)).to equal(instance) end it "should intern by calling 'PSON.parse' on the text and then using from_data_hash to convert the actual into an instance if the pson has no class/data separation" do text = "foo" PSON.expects(:parse).with("foo").returns("foo") PsonTest.expects(:from_data_hash).with("foo").returns "parsed_pson" expect(@pson.intern(PsonTest, text)).to eq("parsed_pson") end it "should intern multiples by parsing the text and using 'class.intern' on each resulting data structure" do text = "foo" PSON.expects(:parse).with("foo").returns ["bar", "baz"] PsonTest.expects(:from_data_hash).with("bar").returns "BAR" PsonTest.expects(:from_data_hash).with("baz").returns "BAZ" expect(@pson.intern_multiple(PsonTest, text)).to eq(%w{BAR BAZ}) end it "fails intelligibly when given invalid data" do expect do @pson.intern(Puppet::Node, '') end.to raise_error(PSON::ParserError, /source did not contain any PSON/) end end end describe ":console format" do subject { Puppet::Network::FormatHandler.format(:console) } it { is_expected.to be_an_instance_of Puppet::Network::Format } let :json do Puppet::Network::FormatHandler.format(:pson) end [:intern, :intern_multiple].each do |method| it "should not implement #{method}" do expect { subject.send(method, String, 'blah') }.to raise_error NotImplementedError end end ["hello", 1, 1.0].each do |input| it "should just return a #{input.inspect}" do expect(subject.render(input)).to eq(input) end end [[1, 2], ["one"], [{ 1 => 1 }]].each do |input| it "should render #{input.inspect} as one item per line" do expect(subject.render(input)).to eq(input.collect { |item| item.to_s + "\n" }.join('')) end end it "should render empty hashes as empty strings" do expect(subject.render({})).to eq('') end it "should render a non-trivially-keyed Hash as JSON" do hash = { [1,2] => 3, [2,3] => 5, [3,4] => 7 } expect(subject.render(hash)).to eq(json.render(hash).chomp) end it "should render a {String,Numeric}-keyed Hash into a table" do object = Object.new hash = { "one" => 1, "two" => [], "three" => {}, "four" => object, 5 => 5, 6.0 => 6 } # Gotta love ASCII-betical sort order. Hope your objects are better # structured for display than my test one is. --daniel 2011-04-18 expect(subject.render(hash)).to eq < { "1" => '1' * 40, "2" => '2' * 40, '3' => '3' * 40 }, "text" => { "a" => 'a' * 40, 'b' => 'b' * 40, 'c' => 'c' * 40 } } expect(subject.render(hash)).to eq <"1111111111111111111111111111111111111111", "2"=>"2222222222222222222222222222222222222222", "3"=>"3333333333333333333333333333333333333333"} text {"a"=>"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", "b"=>"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", "c"=>"cccccccccccccccccccccccccccccccccccccccc"} EOT end end end diff --git a/spec/unit/network/http/api/indirected_routes_spec.rb b/spec/unit/network/http/api/indirected_routes_spec.rb index 97b5de61b..278406765 100644 --- a/spec/unit/network/http/api/indirected_routes_spec.rb +++ b/spec/unit/network/http/api/indirected_routes_spec.rb @@ -1,516 +1,516 @@ #! /usr/bin/env ruby require 'spec_helper' require 'puppet/network/http' require 'puppet/network/http/api/indirected_routes' require 'puppet/indirector_testing' describe Puppet::Network::HTTP::API::IndirectedRoutes do let(:not_found_code) { Puppet::Network::HTTP::Error::HTTPNotFoundError::CODE } let(:not_acceptable_code) { Puppet::Network::HTTP::Error::HTTPNotAcceptableError::CODE } let(:bad_request_code) { Puppet::Network::HTTP::Error::HTTPBadRequestError::CODE } let(:not_authorized_code) { Puppet::Network::HTTP::Error::HTTPNotAuthorizedError::CODE } let(:indirection) { Puppet::IndirectorTesting.indirection } let(:handler) { Puppet::Network::HTTP::API::IndirectedRoutes.new } let(:response) { Puppet::Network::HTTP::MemoryResponse.new } let(:params) { { :environment => "production" } } let(:master_url_prefix) { "#{Puppet::Network::HTTP::MASTER_URL_PREFIX}/v3"} let(:ca_url_prefix) { "#{Puppet::Network::HTTP::CA_URL_PREFIX}/v1"} def a_request_that_heads(data, request = {}) Puppet::Network::HTTP::Request.from_hash({ :headers => { 'accept' => request[:accept_header], 'content-type' => "text/pson", }, :method => "HEAD", :path => "#{master_url_prefix}/#{indirection.name}/#{data.value}", :params => params, }) end def a_request_that_submits(data, request = {}) Puppet::Network::HTTP::Request.from_hash({ :headers => { 'accept' => request[:accept_header], 'content-type' => request[:content_type_header] || "text/pson", }, :method => "PUT", :path => "#{master_url_prefix}/#{indirection.name}/#{data.value}", :params => params, :body => request[:body].nil? ? data.render("pson") : request[:body] }) end def a_request_that_destroys(data, request = {}) Puppet::Network::HTTP::Request.from_hash({ :headers => { 'accept' => request[:accept_header], 'content-type' => "text/pson", }, :method => "DELETE", :path => "#{master_url_prefix}/#{indirection.name}/#{data.value}", :params => params, :body => '' }) end def a_request_that_finds(data, request = {}) Puppet::Network::HTTP::Request.from_hash({ :headers => { 'accept' => request[:accept_header], 'content-type' => "text/pson", }, :method => "GET", :path => "#{master_url_prefix}/#{indirection.name}/#{data.value}", :params => params, :body => '' }) end def a_request_that_searches(key, request = {}) Puppet::Network::HTTP::Request.from_hash({ :headers => { 'accept' => request[:accept_header], 'content-type' => "text/pson", }, :method => "GET", :path => "#{master_url_prefix}/#{indirection.name}s/#{key}", :params => params, :body => '' }) end before do Puppet::IndirectorTesting.indirection.terminus_class = :memory Puppet::IndirectorTesting.indirection.terminus.clear handler.stubs(:check_authorization) handler.stubs(:warn_if_near_expiration) end describe "when converting a URI into a request" do let(:environment) { Puppet::Node::Environment.create(:env, []) } let(:env_loaders) { Puppet::Environments::Static.new(environment) } let(:params) { { :environment => "env" } } before do handler.stubs(:handler).returns "foo" end around do |example| Puppet.override(:environments => env_loaders) do example.run end end it "should get the environment from a query parameter" do expect(handler.uri2indirection("GET", "#{master_url_prefix}/node/bar", params)[3][:environment].to_s).to eq("env") end it "should fail if there is no environment specified" do expect(lambda { handler.uri2indirection("GET", "#{master_url_prefix}/node/bar", {}) }).to raise_error(ArgumentError) end it "should fail if the environment is not alphanumeric" do expect(lambda { handler.uri2indirection("GET", "#{master_url_prefix}/node/bar", {:environment => "env ness"}) }).to raise_error(ArgumentError) end it "should fail if the indirection does not match the prefix" do expect(lambda { handler.uri2indirection("GET", "#{master_url_prefix}/certificate/foo", params) }).to raise_error(ArgumentError) end it "should fail if the indirection does not have the correct version" do expect(lambda { handler.uri2indirection("GET", "#{Puppet::Network::HTTP::CA_URL_PREFIX}/v3/certificate/foo", params) }).to raise_error(ArgumentError) end it "should not pass a buck_path parameter through (See Bugs #13553, #13518, #13511)" do expect(handler.uri2indirection("GET", "#{master_url_prefix}/node/bar", { :environment => "env", :bucket_path => "/malicious/path" })[3]).not_to include({ :bucket_path => "/malicious/path" }) end it "should pass allowed parameters through" do expect(handler.uri2indirection("GET", "#{master_url_prefix}/node/bar", { :environment => "env", :allowed_param => "value" })[3]).to include({ :allowed_param => "value" }) end it "should return the environment as a Puppet::Node::Environment" do expect(handler.uri2indirection("GET", "#{master_url_prefix}/node/bar", params)[3][:environment]).to be_a(Puppet::Node::Environment) end it "should use the first field of the URI as the indirection name" do expect(handler.uri2indirection("GET", "#{master_url_prefix}/node/bar", params)[0].name).to eq(:node) end it "should fail if the indirection name is not alphanumeric" do expect(lambda { handler.uri2indirection("GET", "#{master_url_prefix}/foo ness/bar", params) }).to raise_error(ArgumentError) end it "should use the remainder of the URI as the indirection key" do expect(handler.uri2indirection("GET", "#{master_url_prefix}/node/bar", params)[2]).to eq("bar") end it "should support the indirection key being a /-separated file path" do expect(handler.uri2indirection("GET", "#{master_url_prefix}/node/bee/baz/bomb", params)[2]).to eq("bee/baz/bomb") end it "should fail if no indirection key is specified" do expect(lambda { handler.uri2indirection("GET", "#{master_url_prefix}/node", params) }).to raise_error(ArgumentError) end it "should choose 'find' as the indirection method if the http method is a GET and the indirection name is singular" do expect(handler.uri2indirection("GET", "#{master_url_prefix}/node/bar", params)[1]).to eq(:find) end it "should choose 'find' as the indirection method if the http method is a POST and the indirection name is singular" do expect(handler.uri2indirection("POST", "#{master_url_prefix}/node/bar", params)[1]).to eq(:find) end it "should choose 'head' as the indirection method if the http method is a HEAD and the indirection name is singular" do expect(handler.uri2indirection("HEAD", "#{master_url_prefix}/node/bar", params)[1]).to eq(:head) end it "should choose 'search' as the indirection method if the http method is a GET and the indirection name is plural" do expect(handler.uri2indirection("GET", "#{master_url_prefix}/nodes/bar", params)[1]).to eq(:search) end it "should change indirection name to 'status' if the http method is a GET and the indirection name is statuses" do expect(handler.uri2indirection("GET", "#{master_url_prefix}/statuses/bar", params)[0].name).to eq(:status) end it "should change indirection name to 'node' if the http method is a GET and the indirection name is nodes" do expect(handler.uri2indirection("GET", "#{master_url_prefix}/nodes/bar", params)[0].name).to eq(:node) end it "should choose 'delete' as the indirection method if the http method is a DELETE and the indirection name is singular" do expect(handler.uri2indirection("DELETE", "#{master_url_prefix}/node/bar", params)[1]).to eq(:destroy) end it "should choose 'save' as the indirection method if the http method is a PUT and the indirection name is singular" do expect(handler.uri2indirection("PUT", "#{master_url_prefix}/node/bar", params)[1]).to eq(:save) end it "should fail if an indirection method cannot be picked" do expect(lambda { handler.uri2indirection("UPDATE", "#{master_url_prefix}/node/bar", params) }).to raise_error(ArgumentError) end it "should URI unescape the indirection key" do escaped = URI.escape("foo bar") indirection, method, key, final_params = handler.uri2indirection("GET", "#{master_url_prefix}/node/#{escaped}", params) expect(key).to eq("foo bar") end end describe "when converting a request into a URI" do let(:environment) { Puppet::Node::Environment.create(:myenv, []) } let(:request) { Puppet::Indirector::Request.new(:foo, :find, "with spaces", nil, :foo => :bar, :environment => environment) } before do handler.stubs(:handler).returns "foo" end it "should include the environment in the query string of the URI" do expect(handler.class.request_to_uri(request)).to eq("#{master_url_prefix}/foo/with%20spaces?environment=myenv&foo=bar") end it "should include the correct url prefix if it is a ca request" do request.stubs(:indirection_name).returns("certificate") expect(handler.class.request_to_uri(request)).to eq("#{ca_url_prefix}/certificate/with%20spaces?environment=myenv&foo=bar") end it "should pluralize the indirection name if the method is 'search'" do request.stubs(:method).returns :search expect(handler.class.request_to_uri(request).split("/")[3]).to eq("foos") end it "should add the query string to the URI" do request.expects(:query_string).returns "query" expect(handler.class.request_to_uri(request)).to match(/\&query$/) end end describe "when converting a request into a URI with body" do let(:environment) { Puppet::Node::Environment.create(:myenv, []) } let(:request) { Puppet::Indirector::Request.new(:foo, :find, "with spaces", nil, :foo => :bar, :environment => environment) } it "should use the indirection as the first field of the URI" do expect(handler.class.request_to_uri_and_body(request).first.split("/")[3]).to eq("foo") end it "should use the escaped key as the remainder of the URI" do escaped = URI.escape("with spaces") expect(handler.class.request_to_uri_and_body(request).first.split("/")[4].sub(/\?.+/, '')).to eq(escaped) end it "should include the correct url prefix if it is a master request" do expect(handler.class.request_to_uri_and_body(request).first).to eq("#{master_url_prefix}/foo/with%20spaces") end it "should include the correct url prefix if it is a ca request" do request.stubs(:indirection_name).returns("certificate") expect(handler.class.request_to_uri_and_body(request).first).to eq("#{ca_url_prefix}/certificate/with%20spaces") end it "should return the URI and body separately" do expect(handler.class.request_to_uri_and_body(request)).to eq(["#{master_url_prefix}/foo/with%20spaces", "environment=myenv&foo=bar"]) end end describe "when processing a request" do it "should return not_authorized_code if the request is not authorized" do request = a_request_that_heads(Puppet::IndirectorTesting.new("my data")) handler.expects(:check_authorization).raises(Puppet::Network::AuthorizationError.new("forbidden")) handler.call(request, response) expect(response.code).to eq(not_authorized_code) end it "should return 'not found' if the indirection does not support remote requests" do request = a_request_that_heads(Puppet::IndirectorTesting.new("my data")) indirection.expects(:allow_remote_requests?).returns(false) handler.call(request, response) expect(response.code).to eq(not_found_code) end it "should return 'bad request' if the environment does not exist" do Puppet.override(:environments => Puppet::Environments::Static.new()) do request = a_request_that_heads(Puppet::IndirectorTesting.new("my data")) handler.call(request, response) expect(response.code).to eq(bad_request_code) end end it "should serialize a controller exception when an exception is thrown while finding the model instance" do request = a_request_that_finds(Puppet::IndirectorTesting.new("key")) handler.expects(:do_find).raises(ArgumentError, "The exception") handler.call(request, response) expect(response.code).to eq(bad_request_code) expect(response.body).to eq("The exception") expect(response.type).to eq("text/plain") end end describe "when finding a model instance" do it "uses the first supported format for the response" do data = Puppet::IndirectorTesting.new("my data") indirection.save(data, "my data") request = a_request_that_finds(data, :accept_header => "unknown, pson") handler.call(request, response) expect(response.body).to eq(data.render(:pson)) expect(response.type).to eq(Puppet::Network::FormatHandler.format(:pson)) end it "responds with a not_acceptable_code error when no accept header is provided" do data = Puppet::IndirectorTesting.new("my data") indirection.save(data, "my data") request = a_request_that_finds(data, :accept_header => nil) handler.call(request, response) expect(response.code).to eq(not_acceptable_code) end it "raises an error when no accepted formats are known" do data = Puppet::IndirectorTesting.new("my data") indirection.save(data, "my data") request = a_request_that_finds(data, :accept_header => "unknown, also/unknown") handler.call(request, response) expect(response.code).to eq(not_acceptable_code) end it "should pass the result through without rendering it if the result is a string" do data = Puppet::IndirectorTesting.new("my data") data_string = "my data string" request = a_request_that_finds(data, :accept_header => "text/pson") indirection.expects(:find).returns(data_string) handler.call(request, response) expect(response.body).to eq(data_string) expect(response.type).to eq(Puppet::Network::FormatHandler.format(:pson)) end it "should return a not_found_code when no model instance can be found" do data = Puppet::IndirectorTesting.new("my data") request = a_request_that_finds(data, :accept_header => "unknown, text/pson") handler.call(request, response) expect(response.code).to eq(not_found_code) end end describe "when searching for model instances" do it "uses the first supported format for the response" do data = Puppet::IndirectorTesting.new("my data") indirection.save(data, "my data") request = a_request_that_searches("my", :accept_header => "unknown, text/pson") handler.call(request, response) expect(response.type).to eq(Puppet::Network::FormatHandler.format(:pson)) expect(response.body).to eq(Puppet::IndirectorTesting.render_multiple(:pson, [data])) end it "should return [] when searching returns an empty array" do request = a_request_that_searches("nothing", :accept_header => "unknown, text/pson") handler.call(request, response) expect(response.body).to eq("[]") expect(response.type).to eq(Puppet::Network::FormatHandler.format(:pson)) end it "should return a not_found_code when searching returns nil" do request = a_request_that_searches("nothing", :accept_header => "unknown, text/pson") indirection.expects(:search).returns(nil) handler.call(request, response) expect(response.code).to eq(not_found_code) end end describe "when destroying a model instance" do it "destroys the data indicated in the request" do data = Puppet::IndirectorTesting.new("my data") indirection.save(data, "my data") request = a_request_that_destroys(data) handler.call(request, response) expect(Puppet::IndirectorTesting.indirection.find("my data")).to be_nil end it "responds with pson when no Accept header is given" do data = Puppet::IndirectorTesting.new("my data") indirection.save(data, "my data") request = a_request_that_destroys(data, :accept_header => nil) handler.call(request, response) expect(response.body).to eq(data.render(:pson)) expect(response.type).to eq(Puppet::Network::FormatHandler.format(:pson)) end it "uses the first supported format for the response" do data = Puppet::IndirectorTesting.new("my data") indirection.save(data, "my data") request = a_request_that_destroys(data, :accept_header => "unknown, text/pson") handler.call(request, response) expect(response.body).to eq(data.render(:pson)) expect(response.type).to eq(Puppet::Network::FormatHandler.format(:pson)) end it "raises an error and does not destroy when no accepted formats are known" do data = Puppet::IndirectorTesting.new("my data") indirection.save(data, "my data") request = a_request_that_destroys(data, :accept_header => "unknown, also/unknown") handler.call(request, response) expect(response.code).to eq(not_acceptable_code) expect(Puppet::IndirectorTesting.indirection.find("my data")).not_to be_nil end end describe "when saving a model instance" do it "allows an empty body when the format supports it" do class Puppet::IndirectorTesting::Nonvalidatingmemory < Puppet::IndirectorTesting::Memory def validate_key(_) # nothing end end indirection.terminus_class = :nonvalidatingmemory data = Puppet::IndirectorTesting.new("test") request = a_request_that_submits(data, - :content_type_header => "application/x-raw", + :content_type_header => "application/octet-stream", :body => '') handler.call(request, response) # PUP-3272 this test fails when yaml is removed and pson is used. Instead of returning an # empty string, the a string '""' is returned - Don't know what the expecation is, if this is # corrent or not. # (helindbe) # expect(Puppet::IndirectorTesting.indirection.find("test").name).to eq('') end it "saves the data sent in the request" do data = Puppet::IndirectorTesting.new("my data") request = a_request_that_submits(data) handler.call(request, response) saved = Puppet::IndirectorTesting.indirection.find("my data") expect(saved.name).to eq(data.name) end it "responds with pson when no Accept header is given" do data = Puppet::IndirectorTesting.new("my data") request = a_request_that_submits(data, :accept_header => nil) handler.call(request, response) expect(response.body).to eq(data.render(:pson)) expect(response.type).to eq(Puppet::Network::FormatHandler.format(:pson)) end it "uses the first supported format for the response" do data = Puppet::IndirectorTesting.new("my data") request = a_request_that_submits(data, :accept_header => "unknown, text/pson") handler.call(request, response) expect(response.body).to eq(data.render(:pson)) expect(response.type).to eq(Puppet::Network::FormatHandler.format(:pson)) end it "raises an error and does not save when no accepted formats are known" do data = Puppet::IndirectorTesting.new("my data") request = a_request_that_submits(data, :accept_header => "unknown, also/unknown") handler.call(request, response) expect(Puppet::IndirectorTesting.indirection.find("my data")).to be_nil expect(response.code).to eq(not_acceptable_code) end end describe "when performing head operation" do it "should not generate a response when a model head call succeeds" do data = Puppet::IndirectorTesting.new("my data") indirection.save(data, "my data") request = a_request_that_heads(data) handler.call(request, response) expect(response.code).to eq(nil) end it "should return a not_found_code when the model head call returns false" do data = Puppet::IndirectorTesting.new("my data") request = a_request_that_heads(data) handler.call(request, response) expect(response.code).to eq(not_found_code) expect(response.type).to eq("text/plain") expect(response.body).to eq("Not Found: Could not find indirector_testing my data") end end end