diff --git a/lib/puppet/parser/ast/resource.rb b/lib/puppet/parser/ast/resource.rb index ce3c499c5..b0b4d2652 100644 --- a/lib/puppet/parser/ast/resource.rb +++ b/lib/puppet/parser/ast/resource.rb @@ -1,67 +1,74 @@ require 'puppet/parser/ast/resource_reference' # Any normal puppet resource declaration. Can point to a definition or a # builtin type. class Puppet::Parser::AST class Resource < AST::Branch associates_doc attr_accessor :type, :instances, :exported, :virtual # Does not actually return an object; instead sets an object # in the current scope. def evaluate(scope) # We want virtual to be true if exported is true. We can't # just set :virtual => self.virtual in the initialization, # because sometimes the :virtual attribute is set *after* # :exported, in which case it clobbers :exported if :exported # is true. Argh, this was a very tough one to track down. virt = self.virtual || self.exported # First level of implicit iteration: build a resource for each # instance. This handles things like: # file { '/foo': owner => blah; '/bar': owner => blah } @instances.collect { |instance| # Evaluate all of the specified params. paramobjects = instance.parameters.collect { |param| param.safeevaluate(scope) } resource_titles = instance.title.safeevaluate(scope) # it's easier to always use an array, even for only one name - resource_titles = [resource_titles] unless resource_titles.is_a?(Array) + resource_titles = [resource_titles].flatten.collect do |title| + case title + when ::String, ::Numeric, ::Symbol, true, false + title.to_s + else + raise Puppet::ParseError, "Resource title must be a String, not #{title.class}" + end + end fully_qualified_type, resource_titles = scope.resolve_type_and_titles(type, resource_titles) # Second level of implicit iteration; build a resource for each # title. This handles things like: # file { ['/foo', '/bar']: owner => blah } - resource_titles.flatten.collect { |resource_title| + resource_titles.collect { |resource_title| exceptwrap :type => Puppet::ParseError do resource = Puppet::Parser::Resource.new( fully_qualified_type, resource_title, :parameters => paramobjects, :file => self.file, :line => self.line, :exported => self.exported, :virtual => virt, :source => scope.source, :scope => scope, :strict => true ) if resource.resource_type.is_a? Puppet::Resource::Type resource.resource_type.instantiate_resource(scope, resource) end scope.compiler.add_resource(scope, resource) scope.compiler.evaluate_classes([resource_title],scope,false) if fully_qualified_type == 'class' resource end } }.flatten.reject { |resource| resource.nil? } end end end diff --git a/spec/unit/parser/ast/resource_spec.rb b/spec/unit/parser/ast/resource_spec.rb index 68ad9c229..609bc73f9 100755 --- a/spec/unit/parser/ast/resource_spec.rb +++ b/spec/unit/parser/ast/resource_spec.rb @@ -1,183 +1,210 @@ #!/usr/bin/env rspec require 'spec_helper' describe Puppet::Parser::AST::Resource do ast = Puppet::Parser::AST describe "for builtin types" do before :each do @title = Puppet::Parser::AST::String.new(:value => "mytitle") @compiler = Puppet::Parser::Compiler.new(Puppet::Node.new("mynode")) @scope = Puppet::Parser::Scope.new(:compiler => @compiler) @scope.stubs(:resource).returns(stub_everything) @instance = ast::ResourceInstance.new(:title => @title, :parameters => ast::ASTArray.new(:children => [])) @resource = ast::Resource.new(:type => "file", :instances => ast::ASTArray.new(:children => [@instance])) @resource.stubs(:qualified_type).returns("Resource") end it "should evaluate all its parameters" do param = stub 'param' param.expects(:safeevaluate).with(@scope).returns Puppet::Parser::Resource::Param.new(:name => "myparam", :value => "myvalue", :source => stub("source")) @instance.stubs(:parameters).returns [param] @resource.evaluate(@scope) end it "should evaluate its title" do @resource.evaluate(@scope)[0].title.should == "mytitle" end it "should flatten the titles array" do titles = [] %w{one two}.each do |title| titles << Puppet::Parser::AST::String.new(:value => title) end array = Puppet::Parser::AST::ASTArray.new(:children => titles) @instance.title = array result = @resource.evaluate(@scope).collect { |r| r.title } result.should be_include("one") result.should be_include("two") end + + [5, 5.0, true, false, :three, "foo"].each do |title| + it "should stringify the title if it is a #{title.class}" do + @instance.title.stubs(:safeevaluate).returns title + + result = @resource.evaluate(@scope).map(&:title) + result.first.should == title.to_s + end + end + + [ + {:a => 1}, + /noway/i, + Puppet::Resource.new(:notify, "something"), + Pathname.new("foo"), + (1..50), + nil, + ].each do |title| + it "should fail if the title is a #{title.class}" do + @instance.title.stubs(:safeevaluate).returns title + + expect do + @resource.evaluate(@scope) + end.to raise_error(Puppet::ParseError, /Resource title must be a String, not #{title.class}/) + end + end + it "should create and return one resource objects per title" do titles = [] %w{one two}.each do |title| titles << Puppet::Parser::AST::String.new(:value => title) end array = Puppet::Parser::AST::ASTArray.new(:children => titles) @instance.title = array result = @resource.evaluate(@scope).collect { |r| r.title } result.should be_include("one") result.should be_include("two") end it "should implicitly iterate over instances" do new_title = Puppet::Parser::AST::String.new(:value => "other_title") new_instance = ast::ResourceInstance.new(:title => new_title, :parameters => ast::ASTArray.new(:children => [])) @resource.instances.push(new_instance) @resource.evaluate(@scope).collect { |r| r.title }.should == ["mytitle", "other_title"] end it "should handover resources to the compiler" do titles = [] %w{one two}.each do |title| titles << Puppet::Parser::AST::String.new(:value => title) end array = Puppet::Parser::AST::ASTArray.new(:children => titles) @instance.title = array result = @resource.evaluate(@scope) result.each do |res| @compiler.catalog.resource(res.ref).should be_instance_of(Puppet::Parser::Resource) end end it "should generate virtual resources if it is virtual" do @resource.virtual = true result = @resource.evaluate(@scope) result[0].should be_virtual end it "should generate virtual and exported resources if it is exported" do @resource.exported = true result = @resource.evaluate(@scope) result[0].should be_virtual result[0].should be_exported end # Related to #806, make sure resources always look up the full path to the resource. describe "when generating qualified resources" do before do @scope = Puppet::Parser::Scope.new :compiler => Puppet::Parser::Compiler.new(Puppet::Node.new("mynode")) @parser = Puppet::Parser::Parser.new(Puppet::Node::Environment.new) ["one", "one::two", "three"].each do |name| @parser.environment.known_resource_types.add(Puppet::Resource::Type.new(:definition, name, {})) end @twoscope = @scope.newscope(:namespace => "one") @twoscope.resource = @scope.resource end def resource(type, params = nil) params ||= Puppet::Parser::AST::ASTArray.new(:children => []) instance = Puppet::Parser::AST::ResourceInstance.new( :title => Puppet::Parser::AST::String.new(:value => "myresource"), :parameters => params) Puppet::Parser::AST::Resource.new(:type => type, :instances => Puppet::Parser::AST::ASTArray.new(:children => [instance])) end it "should be able to generate resources with fully qualified type information" do resource("two").evaluate(@twoscope)[0].type.should == "One::Two" end it "should be able to generate resources with unqualified type information" do resource("one").evaluate(@twoscope)[0].type.should == "One" end it "should correctly generate resources that can look up builtin types" do resource("file").evaluate(@twoscope)[0].type.should == "File" end it "should correctly generate resources that can look up defined classes by title" do @scope.known_resource_types.add_hostclass Puppet::Resource::Type.new(:hostclass, "Myresource", {}) @scope.compiler.stubs(:evaluate_classes) res = resource("class").evaluate(@twoscope)[0] res.type.should == "Class" res.title.should == "Myresource" end it "should evaluate parameterized classes when they are instantiated" do @scope.known_resource_types.add_hostclass Puppet::Resource::Type.new(:hostclass, "Myresource", {}) @scope.compiler.expects(:evaluate_classes).with(['myresource'],@twoscope,false) resource("class").evaluate(@twoscope)[0] end it "should fail for resource types that do not exist" do lambda { resource("nosuchtype").evaluate(@twoscope) }.should raise_error(Puppet::ParseError) end end end describe "for class resources" do before do @title = Puppet::Parser::AST::String.new(:value => "classname") @compiler = Puppet::Parser::Compiler.new(Puppet::Node.new("mynode")) @scope = Puppet::Parser::Scope.new(:compiler => @compiler) @scope.stubs(:resource).returns(stub_everything) @instance = ast::ResourceInstance.new(:title => @title, :parameters => ast::ASTArray.new(:children => [])) @resource = ast::Resource.new(:type => "Class", :instances => ast::ASTArray.new(:children => [@instance])) @resource.stubs(:qualified_type).returns("Resource") @type = Puppet::Resource::Type.new(:hostclass, "classname") @compiler.known_resource_types.add(@type) end it "should instantiate the class" do @compiler.stubs(:evaluate_classes) result = @resource.evaluate(@scope) result.length.should == 1 result.first.ref.should == "Class[Classname]" @compiler.catalog.resource("Class[Classname]").should equal(result.first) end it "should cause its parent to be evaluated" do parent_type = Puppet::Resource::Type.new(:hostclass, "parentname") @compiler.stubs(:evaluate_classes) @compiler.known_resource_types.add(parent_type) @type.parent = "parentname" result = @resource.evaluate(@scope) result.length.should == 1 result.first.ref.should == "Class[Classname]" @compiler.catalog.resource("Class[Classname]").should equal(result.first) @compiler.catalog.resource("Class[Parentname]").should be_instance_of(Puppet::Parser::Resource) end end end