diff --git a/lib/puppet/indirector/resource_type/parser.rb b/lib/puppet/indirector/resource_type/parser.rb index 4bcaf3f47..a167a7dca 100644 --- a/lib/puppet/indirector/resource_type/parser.rb +++ b/lib/puppet/indirector/resource_type/parser.rb @@ -1,43 +1,72 @@ require 'puppet/resource/type' require 'puppet/indirector/code' require 'puppet/indirector/resource_type' class Puppet::Indirector::ResourceType::Parser < Puppet::Indirector::Code desc "Return the data-form of a resource type." def find(request) krt = request.environment.known_resource_types # This is a bit ugly. [:hostclass, :definition, :node].each do |type| # We have to us 'find_' here because it will # load any missing types from disk, whereas the plain # '' method only returns from memory. if r = krt.send("find_#{type}", [""], request.key) return r end end nil end + # This is the "search" indirection method for resource types. It searches + # through a specified environment for all custom declared classes + # (a.k.a 'hostclasses'), defined types (a.k.a. 'definitions'), and nodes. + # + # @param [Puppet::Indirector::Request] request + # Important properties of the request parameter: + # 1. request.environment : The environment in which to look for types. + # 2. request.key : A String that will be treated as a regular expression to + # be matched against the names of the available types. You may also + # pass a "*", which will match all available types. + # 3. request.options[:kind] : a String that can be used to filter the output + # to only return the desired kinds. The current supported values are + # 'class', 'defined_type', and 'node'. def search(request) krt = request.environment.known_resource_types # Make sure we've got all of the types loaded. krt.loader.import_all - result = [krt.hostclasses.values, krt.definitions.values, krt.nodes.values].flatten.reject { |t| t.name == "" } + + result_candidates = case request.options[:kind] + when "class" + krt.hostclasses.values + when "defined_type" + krt.definitions.values + when "node" + krt.nodes.values + when nil + result_candidates = [krt.hostclasses.values, krt.definitions.values, krt.nodes.values] + else + raise ArgumentError, "Unrecognized kind filter: " + + "'#{request.options[:kind]}', expected one " + + " of 'class', 'defined_type', or 'node'." + end + + result = result_candidates.flatten.reject { |t| t.name == "" } return nil if result.empty? return result if request.key == "*" # Strip the regex of any wrapping slashes that might exist key = request.key.sub(/^\//, '').sub(/\/$/, '') begin regex = Regexp.new(key) rescue => detail raise ArgumentError, "Invalid regex '#{request.key}': #{detail}" end result.reject! { |t| t.name.to_s !~ regex } return nil if result.empty? result end end diff --git a/lib/puppet/network/http/api/v1.rb b/lib/puppet/network/http/api/v1.rb index ef19fe487..29146ff9b 100644 --- a/lib/puppet/network/http/api/v1.rb +++ b/lib/puppet/network/http/api/v1.rb @@ -1,83 +1,83 @@ require 'puppet/network/http/api' module Puppet::Network::HTTP::API::V1 # How we map http methods and the indirection name in the URI # to an indirection method. METHOD_MAP = { "GET" => { :plural => :search, :singular => :find }, "POST" => { :singular => :find, }, "PUT" => { :singular => :save }, "DELETE" => { :singular => :destroy }, "HEAD" => { :singular => :head } } def uri2indirection(http_method, uri, params) environment, indirection, key = uri.split("/", 4)[1..-1] # the first field is always nil because of the leading slash raise ArgumentError, "The environment must be purely alphanumeric, not '#{environment}'" unless environment =~ /^\w+$/ raise ArgumentError, "The indirection name must be purely alphanumeric, not '#{indirection}'" unless indirection =~ /^\w+$/ method = indirection_method(http_method, indirection) params[:environment] = Puppet::Node::Environment.new(environment) params.delete(:bucket_path) raise ArgumentError, "No request key specified in #{uri}" if key == "" or key.nil? key = URI.unescape(key) [indirection, method, key, params] end def indirection2uri(request) indirection = request.method == :search ? pluralize(request.indirection_name.to_s) : request.indirection_name.to_s "/#{request.environment.to_s}/#{indirection}/#{request.escaped_key}#{request.query_string}" end def request_to_uri_and_body(request) indirection = request.method == :search ? pluralize(request.indirection_name.to_s) : request.indirection_name.to_s ["/#{request.environment.to_s}/#{indirection}/#{request.escaped_key}", request.query_string.sub(/^\?/,'')] end def indirection_method(http_method, indirection) raise ArgumentError, "No support for http method #{http_method}" unless METHOD_MAP[http_method] unless method = METHOD_MAP[http_method][plurality(indirection)] - raise ArgumentError, "No support for plural #{http_method} operations" + raise ArgumentError, "No support for plurality #{plurality(indirection)} for #{http_method} operations" end method end def pluralize(indirection) return(indirection == "status" ? "statuses" : indirection + "s") end def plurality(indirection) # NOTE This specific hook for facts is ridiculous, but it's a *many*-line # fix to not need this, and our goal is to move away from the complication # that leads to the fix being too long. return :singular if indirection == "facts" return :singular if indirection == "status" return :singular if indirection == "certificate_status" return :plural if indirection == "inventory" result = (indirection =~ /s$|_search$/) ? :plural : :singular indirection.sub!(/s$|_search$/, '') indirection.sub!(/statuse$/, 'status') result end end diff --git a/lib/puppet/resource/type.rb b/lib/puppet/resource/type.rb index 72dbf22fd..be8f2fec6 100644 --- a/lib/puppet/resource/type.rb +++ b/lib/puppet/resource/type.rb @@ -1,344 +1,404 @@ require 'puppet/parser/parser' require 'puppet/util/warnings' require 'puppet/util/errors' require 'puppet/util/inline_docs' require 'puppet/parser/ast/leaf' require 'puppet/dsl' class Puppet::Resource::Type Puppet::ResourceType = self include Puppet::Util::InlineDocs include Puppet::Util::Warnings include Puppet::Util::Errors - RESOURCE_SUPERTYPES = [:hostclass, :node, :definition] + RESOURCE_KINDS = [:hostclass, :node, :definition] + + # We have reached a point where we've established some naming conventions + # in our documentation that don't entirely match up with our internal names + # for things. Ideally we'd change the internal representation to match the + # conventions expressed in our docs, but that would be a fairly far-reaching + # and risky change. For the time being, we're settling for mapping the + # internal names to the external ones (and vice-versa) during serialization + # and deserialization. These two hashes is here to help with that mapping. + RESOURCE_KINDS_TO_EXTERNAL_NAMES = { + :hostclass => "class", + :node => "node", + :definition => "defined_type", + } + RESOURCE_EXTERNAL_NAMES_TO_KINDS = RESOURCE_KINDS_TO_EXTERNAL_NAMES.invert attr_accessor :file, :line, :doc, :code, :ruby_code, :parent, :resource_type_collection - attr_reader :type, :namespace, :arguments, :behaves_like, :module_name + attr_reader :namespace, :arguments, :behaves_like, :module_name - RESOURCE_SUPERTYPES.each do |t| + # This should probably be renamed to 'kind' eventually, in accordance with the changes + # made for serialization and API usability (#14137). At the moment that seems like + # it would touch a whole lot of places in the code, though. --cprice 2012-04-23 + attr_reader :type + + RESOURCE_KINDS.each do |t| define_method("#{t}?") { self.type == t } end require 'puppet/indirector' extend Puppet::Indirector indirects :resource_type, :terminus_class => :parser def self.from_pson(data) name = data.delete('name') or raise ArgumentError, "Resource Type names must be specified" - type = data.delete('type') || "definition" + kind = data.delete('kind') || "definition" + + unless type = RESOURCE_EXTERNAL_NAMES_TO_KINDS[kind] + raise ArgumentError, "Unsupported resource kind '#{kind}'" + end data = data.inject({}) { |result, ary| result[ary[0].intern] = ary[1]; result } + # This is a bit of a hack; when we serialize, we use the term "parameters" because that + # is the terminology that we use in our documentation. However, internally to this + # class we use the term "arguments". Ideally we'd change the implementation to be consistent + # with the documentation, but that would be challenging right now because it could potentially + # touch a lot of places in the code, not to mention that we already have another meaning for + # "parameters" internally. So, for now, we will simply transform the internal "arguments" + # value to "parameters" when serializing, and the opposite when deserializing. + # --cprice 2012-04-23 + data[:arguments] = data.delete(:parameters) + new(type, name, data) end + # This method doesn't seem like it has anything to do with PSON in particular, and it shouldn't. + # It's just transforming to a simple object that can be serialized and de-serialized via + # any transport format. Should probably be renamed if we get a chance to clean up our + # serialization / deserialization, and there are probably many other similar methods in + # other classes. + # --cprice 2012-04-23 + def to_pson_data_hash data = [:doc, :line, :file, :parent].inject({}) do |hash, param| next hash unless (value = self.send(param)) and (value != "") hash[param.to_s] = value hash end - data['arguments'] = arguments.dup unless arguments.empty? + # This is a bit of a hack; when we serialize, we use the term "parameters" because that + # is the terminology that we use in our documentation. However, internally to this + # class we use the term "arguments". Ideally we'd change the implementation to be consistent + # with the documentation, but that would be challenging right now because it could potentially + # touch a lot of places in the code, not to mention that we already have another meaning for + # "parameters" internally. So, for now, we will simply transform the internal "arguments" + # value to "parameters" when serializing, and the opposite when deserializing. + # --cprice 2012-04-23 + data['parameters'] = arguments.dup unless arguments.empty? data['name'] = name - data['type'] = type + unless RESOURCE_KINDS_TO_EXTERNAL_NAMES.has_key?(type) + raise ArgumentError, "Unsupported resource kind '#{type}'" + end + data['kind'] = RESOURCE_KINDS_TO_EXTERNAL_NAMES[type] data end + # It seems wrong that we have a 'to_pson' method on this class, but not a 'to_yaml'. + # As a result, if you use the REST API to retrieve one or more objects of this type, + # you will receive different data if you use 'Accept: yaml' vs 'Accept: pson'. That + # seems really, really wrong. The "Accept" header should never affect what data is + # being returned--only the format of the data. If the data itself is going to differ, + # then there should be a different request URL. Documenting the REST API becomes + # a much more complex problem when the "Accept" header can change the semantics + # of the response. --cprice 2012-04-23 + def to_pson(*args) to_pson_data_hash.to_pson(*args) end # Are we a child of the passed class? Do a recursive search up our # parentage tree to figure it out. def child_of?(klass) return false unless parent return(klass == parent_type ? true : parent_type.child_of?(klass)) end # Now evaluate the code associated with this class or definition. def evaluate_code(resource) static_parent = evaluate_parent_type(resource) scope = static_parent || resource.scope scope = scope.newscope(:namespace => namespace, :source => self, :resource => resource) unless resource.title == :main scope.compiler.add_class(name) unless definition? set_resource_parameters(resource, scope) resource.add_edge_to_stage code.safeevaluate(scope) if code evaluate_ruby_code(resource, scope) if ruby_code end def initialize(type, name, options = {}) @type = type.to_s.downcase.to_sym - raise ArgumentError, "Invalid resource supertype '#{type}'" unless RESOURCE_SUPERTYPES.include?(@type) + raise ArgumentError, "Invalid resource supertype '#{type}'" unless RESOURCE_KINDS.include?(@type) name = convert_from_ast(name) if name.is_a?(Puppet::Parser::AST::HostName) set_name_and_namespace(name) [:code, :doc, :line, :file, :parent].each do |param| next unless value = options[param] send(param.to_s + "=", value) end set_arguments(options[:arguments]) @module_name = options[:module_name] end # This is only used for node names, and really only when the node name # is a regexp. def match(string) return string.to_s.downcase == name unless name_is_regex? @name =~ string end # Add code from a new instance to our code. def merge(other) fail "#{name} is not a class; cannot add code to it" unless type == :hostclass fail "#{other.name} is not a class; cannot add code from it" unless other.type == :hostclass fail "Cannot have code outside of a class/node/define because 'freeze_main' is enabled" if name == "" and Puppet.settings[:freeze_main] if parent and other.parent and parent != other.parent fail "Cannot merge classes with different parent classes (#{name} => #{parent} vs. #{other.name} => #{other.parent})" end # We know they're either equal or only one is set, so keep whichever parent is specified. self.parent ||= other.parent if other.doc self.doc ||= "" self.doc += other.doc end # This might just be an empty, stub class. return unless other.code unless self.code self.code = other.code return end array_class = Puppet::Parser::AST::ASTArray self.code = array_class.new(:children => [self.code]) unless self.code.is_a?(array_class) if other.code.is_a?(array_class) code.children += other.code.children else code.children << other.code end end # Make an instance of the resource type, and place it in the catalog # if it isn't in the catalog already. This is only possible for # classes and nodes. No parameters are be supplied--if this is a # parameterized class, then all parameters take on their default # values. def ensure_in_catalog(scope, parameters=nil) type == :definition and raise ArgumentError, "Cannot create resources for defined resource types" resource_type = type == :hostclass ? :class : :node # Do nothing if the resource already exists; this makes sure we don't # get multiple copies of the class resource, which helps provide the # singleton nature of classes. # we should not do this for classes with parameters # if parameters are passed, we should still try to create the resource # even if it exists so that we can fail # this prevents us from being able to combine param classes with include if resource = scope.catalog.resource(resource_type, name) and !parameters return resource end resource = Puppet::Parser::Resource.new(resource_type, name, :scope => scope, :source => self) assign_parameter_values(parameters, resource) instantiate_resource(scope, resource) scope.compiler.add_resource(scope, resource) resource end def instantiate_resource(scope, resource) # Make sure our parent class has been evaluated, if we have one. if parent && !scope.catalog.resource(resource.type, parent) parent_type(scope).ensure_in_catalog(scope) end if ['Class', 'Node'].include? resource.type scope.catalog.tag(*resource.tags) end end def name return @name unless @name.is_a?(Regexp) @name.source.downcase.gsub(/[^-\w:.]/,'').sub(/^\.+/,'') end def name_is_regex? @name.is_a?(Regexp) end def assign_parameter_values(parameters, resource) return unless parameters scope = resource.scope || {} # It'd be nice to assign default parameter values here, # but we can't because they often rely on local variables # created during set_resource_parameters. parameters.each do |name, value| resource.set_parameter name, value end end # MQR TODO: # # The change(s) introduced by the fix for #4270 are mostly silly & should be # removed, though we didn't realize it at the time. If it can be established/ # ensured that nodes never call parent_type and that resource_types are always # (as they should be) members of exactly one resource_type_collection the # following method could / should be replaced with: # # def parent_type # @parent_type ||= parent && ( # resource_type_collection.find_or_load([name],parent,type.to_sym) || # fail Puppet::ParseError, "Could not find parent resource type '#{parent}' of type #{type} in #{resource_type_collection.environment}" # ) # end # # ...and then the rest of the changes around passing in scope reverted. # def parent_type(scope = nil) return nil unless parent unless @parent_type raise "Must pass scope to parent_type when called first time" unless scope unless @parent_type = scope.environment.known_resource_types.send("find_#{type}", [name], parent) fail Puppet::ParseError, "Could not find parent resource type '#{parent}' of type #{type} in #{scope.environment}" end end @parent_type end # Set any arguments passed by the resource as variables in the scope. def set_resource_parameters(resource, scope) set = {} resource.to_hash.each do |param, value| param = param.to_sym fail Puppet::ParseError, "#{resource.ref} does not accept attribute #{param}" unless valid_parameter?(param) exceptwrap { scope[param.to_s] = value } set[param] = true end if @type == :hostclass scope["title"] = resource.title.to_s.downcase unless set.include? :title scope["name"] = resource.name.to_s.downcase unless set.include? :name else scope["title"] = resource.title unless set.include? :title scope["name"] = resource.name unless set.include? :name end scope["module_name"] = module_name if module_name and ! set.include? :module_name if caller_name = scope.parent_module_name and ! set.include?(:caller_module_name) scope["caller_module_name"] = caller_name end scope.class_set(self.name,scope) if hostclass? or node? # Evaluate the default parameters, now that all other variables are set default_params = resource.set_default_parameters(scope) default_params.each { |param| scope[param.to_s] = resource[param] } # This has to come after the above parameters so that default values # can use their values resource.validate_complete end # Check whether a given argument is valid. def valid_parameter?(param) param = param.to_s return true if param == "name" return true if Puppet::Type.metaparam?(param) return false unless defined?(@arguments) return(arguments.include?(param) ? true : false) end def set_arguments(arguments) @arguments = {} return if arguments.nil? arguments.each do |arg, default| arg = arg.to_s warn_if_metaparam(arg, default) @arguments[arg] = default end end private def convert_from_ast(name) value = name.value if value.is_a?(Puppet::Parser::AST::Regex) name = value.value else name = value end end def evaluate_parent_type(resource) return unless klass = parent_type(resource.scope) and parent_resource = resource.scope.compiler.catalog.resource(:class, klass.name) || resource.scope.compiler.catalog.resource(:node, klass.name) parent_resource.evaluate unless parent_resource.evaluated? parent_scope(resource.scope, klass) end def evaluate_ruby_code(resource, scope) Puppet::DSL::ResourceAPI.new(resource, scope, ruby_code).evaluate end # Split an fq name into a namespace and name def namesplit(fullname) ary = fullname.split("::") n = ary.pop || "" ns = ary.join("::") return ns, n end def parent_scope(scope, klass) scope.class_scope(klass) || raise(Puppet::DevError, "Could not find scope for #{klass.name}") end def set_name_and_namespace(name) if name.is_a?(Regexp) @name = name @namespace = "" else @name = name.to_s.downcase # Note we're doing something somewhat weird here -- we're setting # the class's namespace to its fully qualified name. This means # anything inside that class starts looking in that namespace first. @namespace, ignored_shortname = @type == :hostclass ? [@name, ''] : namesplit(@name) end end def warn_if_metaparam(param, default) return unless Puppet::Type.metaparamclass(param) if default warnonce "#{param} is a metaparam; this value will inherit to all contained resources" else raise Puppet::ParseError, "#{param} is a metaparameter; please choose another parameter name in the #{self.name} definition" end end end diff --git a/spec/unit/indirector/resource_type/parser_spec.rb b/spec/unit/indirector/resource_type/parser_spec.rb index a0362496e..2c0db235a 100755 --- a/spec/unit/indirector/resource_type/parser_spec.rb +++ b/spec/unit/indirector/resource_type/parser_spec.rb @@ -1,149 +1,249 @@ #!/usr/bin/env rspec require 'spec_helper' require 'puppet/indirector/resource_type/parser' require 'puppet_spec/files' describe Puppet::Indirector::ResourceType::Parser do include PuppetSpec::Files before do @terminus = Puppet::Indirector::ResourceType::Parser.new @request = Puppet::Indirector::Request.new(:resource_type, :find, "foo", nil) @krt = @request.environment.known_resource_types end it "should be registered with the resource_type indirection" do Puppet::Indirector::Terminus.terminus_class(:resource_type, :parser).should equal(Puppet::Indirector::ResourceType::Parser) end describe "when finding" do it "should return any found type from the request's environment" do type = Puppet::Resource::Type.new(:hostclass, "foo") @request.environment.known_resource_types.add(type) @terminus.find(@request).should == type end it "should attempt to load the type if none is found in memory" do dir = tmpdir("find_a_type") FileUtils.mkdir_p(dir) Puppet[:modulepath] = dir # Make a new request, since we've reset the env @request = Puppet::Indirector::Request.new(:resource_type, :find, "foo::bar", nil) manifest_path = File.join(dir, "foo", "manifests") FileUtils.mkdir_p(manifest_path) File.open(File.join(manifest_path, "bar.pp"), "w") { |f| f.puts "class foo::bar {}" } result = @terminus.find(@request) result.should be_instance_of(Puppet::Resource::Type) result.name.should == "foo::bar" end it "should return nil if no type can be found" do @terminus.find(@request).should be_nil end it "should prefer definitions to nodes" do type = @krt.add(Puppet::Resource::Type.new(:hostclass, "foo")) node = @krt.add(Puppet::Resource::Type.new(:node, "foo")) @terminus.find(@request).should == type end end describe "when searching" do - before do - @request.key = "*" + describe "when the search key is a wildcard" do + before do + @request.key = "*" + end + + it "should use the request's environment's list of known resource types" do + @request.environment.known_resource_types.expects(:hostclasses).returns({}) + + @terminus.search(@request) + end + + it "should return all results if '*' is provided as the search string" do + type = @krt.add(Puppet::Resource::Type.new(:hostclass, "foo")) + node = @krt.add(Puppet::Resource::Type.new(:node, "bar")) + define = @krt.add(Puppet::Resource::Type.new(:definition, "baz")) + + result = @terminus.search(@request) + result.should be_include(type) + result.should be_include(node) + result.should be_include(define) + end + + it "should return all known types" do + type = @krt.add(Puppet::Resource::Type.new(:hostclass, "foo")) + node = @krt.add(Puppet::Resource::Type.new(:node, "bar")) + define = @krt.add(Puppet::Resource::Type.new(:definition, "baz")) + + result = @terminus.search(@request) + result.should be_include(type) + result.should be_include(node) + result.should be_include(define) + end + + it "should not return the 'main' class" do + main = @krt.add(Puppet::Resource::Type.new(:hostclass, "")) + + # So there is a return value + foo = @krt.add(Puppet::Resource::Type.new(:hostclass, "foo")) + + @terminus.search(@request).should_not be_include(main) + end + + it "should return nil if no types can be found" do + @terminus.search(@request).should be_nil + end + + it "should load all resource types from all search paths" do + dir = tmpdir("searching_in_all") + first = File.join(dir, "first") + second = File.join(dir, "second") + FileUtils.mkdir_p(first) + FileUtils.mkdir_p(second) + Puppet[:modulepath] = "#{first}#{File::PATH_SEPARATOR}#{second}" + + # Make a new request, since we've reset the env + @request = Puppet::Indirector::Request.new(:resource_type, :search, "*", nil) + + onepath = File.join(first, "one", "manifests") + FileUtils.mkdir_p(onepath) + twopath = File.join(first, "two", "manifests") + FileUtils.mkdir_p(twopath) + + File.open(File.join(onepath, "oneklass.pp"), "w") { |f| f.puts "class one::oneklass {}" } + File.open(File.join(twopath, "twoklass.pp"), "w") { |f| f.puts "class two::twoklass {}" } + + result = @terminus.search(@request) + result.find { |t| t.name == "one::oneklass" }.should be_instance_of(Puppet::Resource::Type) + result.find { |t| t.name == "two::twoklass" }.should be_instance_of(Puppet::Resource::Type) + end + + context "when specifying a 'kind' parameter" do + before :each do + @klass = @krt.add(Puppet::Resource::Type.new(:hostclass, "foo")) + @node = @krt.add(Puppet::Resource::Type.new(:node, "bar")) + @define = @krt.add(Puppet::Resource::Type.new(:definition, "baz")) + end + + it "should raise an error if you pass an invalid kind filter" do + @request.options[:kind] = "i bet you don't have a kind called this" + expect { + @terminus.search(@request) + }.to raise_error(ArgumentError, /Unrecognized kind filter/) + + end + + it "should support filtering for only hostclass results" do + @request.options[:kind] = "class" + + result = @terminus.search(@request) + result.should be_include(@klass) + result.should_not be_include(@node) + result.should_not be_include(@define) + end + + it "should support filtering for only node results" do + @request.options[:kind] = "node" + + result = @terminus.search(@request) + result.should_not be_include(@klass) + result.should be_include(@node) + result.should_not be_include(@define) + end + + it "should support filtering for only definition results" do + @request.options[:kind] = "defined_type" + + result = @terminus.search(@request) + result.should_not be_include(@klass) + result.should_not be_include(@node) + result.should be_include(@define) + end + end end - it "should use the request's environment's list of known resource types" do - @request.environment.known_resource_types.expects(:hostclasses).returns({}) - - @terminus.search(@request) - end - - it "should return all results if '*' is provided as the search string" do - @request.key = "*" - type = @krt.add(Puppet::Resource::Type.new(:hostclass, "foo")) - node = @krt.add(Puppet::Resource::Type.new(:node, "bar")) - define = @krt.add(Puppet::Resource::Type.new(:definition, "baz")) - - result = @terminus.search(@request) - result.should be_include(type) - result.should be_include(node) - result.should be_include(define) - end - - it "should treat any search string not '*' as a regex" do - @request.key = "a" - foo = @krt.add(Puppet::Resource::Type.new(:hostclass, "foo")) - bar = @krt.add(Puppet::Resource::Type.new(:hostclass, "bar")) - baz = @krt.add(Puppet::Resource::Type.new(:hostclass, "baz")) - - result = @terminus.search(@request) - result.should be_include(bar) - result.should be_include(baz) - result.should_not be_include(foo) - end - - it "should fail if a provided search string is not '*' and is not a valid regex" do - @request.key = "*foo*" - - # Add one instance so we don't just get an empty array" - @krt.add(Puppet::Resource::Type.new(:hostclass, "foo")) - lambda { @terminus.search(@request) }.should raise_error(ArgumentError) - end - - it "should return all known types" do - type = @krt.add(Puppet::Resource::Type.new(:hostclass, "foo")) - node = @krt.add(Puppet::Resource::Type.new(:node, "bar")) - define = @krt.add(Puppet::Resource::Type.new(:definition, "baz")) - - result = @terminus.search(@request) - result.should be_include(type) - result.should be_include(node) - result.should be_include(define) + context "when the search string is not a wildcard" do + + it "should treat any search string as a regex" do + @request.key = "a" + foo = @krt.add(Puppet::Resource::Type.new(:hostclass, "foo")) + bar = @krt.add(Puppet::Resource::Type.new(:hostclass, "bar")) + baz = @krt.add(Puppet::Resource::Type.new(:hostclass, "baz")) + + result = @terminus.search(@request) + result.should be_include(bar) + result.should be_include(baz) + result.should_not be_include(foo) + end + + it "should support kind filtering with a regex" do + @request.key = "foo" + @request.options[:kind] = "class" + + foobar = @krt.add(Puppet::Resource::Type.new(:hostclass, "foobar")) + foobaz = @krt.add(Puppet::Resource::Type.new(:hostclass, "foobaz")) + foobam = @krt.add(Puppet::Resource::Type.new(:definition, "foobam")) + fooball = @krt.add(Puppet::Resource::Type.new(:node, "fooball")) + + result = @terminus.search(@request) + result.should be_include(foobar) + result.should be_include(foobaz) + result.should_not be_include(foobam) + result.should_not be_include(fooball) + end + + it "should fail if a provided search string is not a valid regex" do + @request.key = "*foo*" + + # Add one instance so we don't just get an empty array" + @krt.add(Puppet::Resource::Type.new(:hostclass, "foo")) + lambda { @terminus.search(@request) }.should raise_error(ArgumentError) + end end it "should not return the 'main' class" do main = @krt.add(Puppet::Resource::Type.new(:hostclass, "")) # So there is a return value foo = @krt.add(Puppet::Resource::Type.new(:hostclass, "foo")) @terminus.search(@request).should_not be_include(main) end it "should return nil if no types can be found" do @terminus.search(@request).should be_nil end it "should load all resource types from all search paths" do dir = tmpdir("searching_in_all") first = File.join(dir, "first") second = File.join(dir, "second") FileUtils.mkdir_p(first) FileUtils.mkdir_p(second) Puppet[:modulepath] = "#{first}#{File::PATH_SEPARATOR}#{second}" # Make a new request, since we've reset the env @request = Puppet::Indirector::Request.new(:resource_type, :search, "*", nil) onepath = File.join(first, "one", "manifests") FileUtils.mkdir_p(onepath) twopath = File.join(first, "two", "manifests") FileUtils.mkdir_p(twopath) File.open(File.join(onepath, "oneklass.pp"), "w") { |f| f.puts "class one::oneklass {}" } File.open(File.join(twopath, "twoklass.pp"), "w") { |f| f.puts "class two::twoklass {}" } result = @terminus.search(@request) result.find { |t| t.name == "one::oneklass" }.should be_instance_of(Puppet::Resource::Type) result.find { |t| t.name == "two::twoklass" }.should be_instance_of(Puppet::Resource::Type) end end end