diff --git a/lib/puppet/parser/ast.rb b/lib/puppet/parser/ast.rb index b7e324687..ed2c66bba 100644 --- a/lib/puppet/parser/ast.rb +++ b/lib/puppet/parser/ast.rb @@ -1,130 +1,129 @@ # the parent class for all of our syntactical objects require 'puppet' require 'puppet/util/autoload' # The base class for all of the objects that make up the parse trees. # Handles things like file name, line #, and also does the initialization # for all of the parameters of all of the child objects. class Puppet::Parser::AST # Do this so I don't have to type the full path in all of the subclasses AST = Puppet::Parser::AST include Puppet::Util::Errors include Puppet::Util::MethodHelper include Puppet::Util::Docs attr_accessor :parent, :scope, :file, :line, :pos def inspect "( #{self.class} #{self.to_s} #{@children.inspect} )" end # don't fetch lexer comment by default def use_docs self.class.use_docs end # allow our subclass to specify they want documentation class << self attr_accessor :use_docs def associates_doc self.use_docs = true end end # Evaluate the current object. Just a stub method, since the subclass # should override this method. def evaluate(*options) end # Throw a parse error. def parsefail(message) self.fail(Puppet::ParseError, message) end # Wrap a statemp in a reusable way so we always throw a parse error. def parsewrap exceptwrap :type => Puppet::ParseError do yield end end # The version of the evaluate method that should be called, because it # correctly handles errors. It is critical to use this method because # it can enable you to catch the error where it happens, rather than # much higher up the stack. def safeevaluate(*options) # We duplicate code here, rather than using exceptwrap, because this # is called so many times during parsing. begin return self.evaluate(*options) rescue Puppet::Error => detail raise adderrorcontext(detail) rescue => detail error = Puppet::ParseError.new(detail.to_s, nil, nil, detail) # We can't use self.fail here because it always expects strings, # not exceptions. raise adderrorcontext(error, detail) end end # Initialize the object. Requires a hash as the argument, and # takes each of the parameters of the hash and calls the settor # method for them. This is probably pretty inefficient and should # likely be changed at some point. def initialize(args) set_options(args) end # evaluate ourselves, and match def evaluate_match(value, scope) obj = self.safeevaluate(scope) obj = obj.downcase if obj.respond_to?(:downcase) value = value.downcase if value.respond_to?(:downcase) obj = Puppet::Parser::Scope.number?(obj) || obj value = Puppet::Parser::Scope.number?(value) || value # "" == undef for case/selector/if obj == value or (obj == "" and value == :undef) or (obj == :undef and value == "") end end # And include all of the AST subclasses. require 'puppet/parser/ast/arithmetic_operator' require 'puppet/parser/ast/astarray' require 'puppet/parser/ast/asthash' require 'puppet/parser/ast/boolean_operator' require 'puppet/parser/ast/branch' require 'puppet/parser/ast/caseopt' require 'puppet/parser/ast/casestatement' require 'puppet/parser/ast/collection' require 'puppet/parser/ast/collexpr' require 'puppet/parser/ast/comparison_operator' require 'puppet/parser/ast/definition' require 'puppet/parser/ast/else' require 'puppet/parser/ast/function' require 'puppet/parser/ast/hostclass' require 'puppet/parser/ast/ifstatement' require 'puppet/parser/ast/in_operator' require 'puppet/parser/ast/lambda' require 'puppet/parser/ast/leaf' require 'puppet/parser/ast/match_operator' require 'puppet/parser/ast/method_call' require 'puppet/parser/ast/minus' require 'puppet/parser/ast/node' require 'puppet/parser/ast/nop' require 'puppet/parser/ast/not' require 'puppet/parser/ast/relationship' require 'puppet/parser/ast/resource' require 'puppet/parser/ast/resource_defaults' require 'puppet/parser/ast/resource_instance' require 'puppet/parser/ast/resource_override' require 'puppet/parser/ast/resource_reference' require 'puppet/parser/ast/resourceparam' require 'puppet/parser/ast/selector' -require 'puppet/parser/ast/tag' require 'puppet/parser/ast/vardef' require 'puppet/parser/code_merger' diff --git a/lib/puppet/parser/ast/collexpr.rb b/lib/puppet/parser/ast/collexpr.rb index 9f266ed47..481c14b20 100644 --- a/lib/puppet/parser/ast/collexpr.rb +++ b/lib/puppet/parser/ast/collexpr.rb @@ -1,109 +1,109 @@ require 'puppet' require 'puppet/parser/ast/branch' require 'puppet/parser/collector' # An object that collects stored objects from the central cache and returns # them to the current host, yo. class Puppet::Parser::AST class CollExpr < AST::Branch attr_accessor :test1, :test2, :oper, :form, :type, :parens def evaluate(scope) if Puppet[:parser] == 'future' evaluate4x(scope) else evaluate3x(scope) end end # We return an object that does a late-binding evaluation. def evaluate3x(scope) # Make sure our contained expressions have all the info they need. [@test1, @test2].each do |t| if t.is_a?(self.class) t.form ||= self.form t.type ||= self.type end end # The code is only used for virtual lookups match1, code1 = @test1.safeevaluate scope match2, code2 = @test2.safeevaluate scope # First build up the virtual code. # If we're a conjunction operator, then we're calling code. I did # some speed comparisons, and it's at least twice as fast doing these # case statements as doing an eval here. code = proc do |resource| case @oper when "and"; code1.call(resource) and code2.call(resource) when "or"; code1.call(resource) or code2.call(resource) when "==" if match1 == "tag" resource.tagged?(match2) else if resource[match1].is_a?(Array) resource[match1].include?(match2) else resource[match1] == match2 end end when "!="; resource[match1] != match2 end end match = [match1, @oper, match2] return match, code end - # Late binding evaluation of a collect expression (as done in 3x), but with proper Puppet Langauge + # Late binding evaluation of a collect expression (as done in 3x), but with proper Puppet Language # semantics for equals and include # def evaluate4x(scope) # Make sure our contained expressions have all the info they need. [@test1, @test2].each do |t| if t.is_a?(self.class) t.form ||= self.form t.type ||= self.type end end # The code is only used for virtual lookups match1, code1 = @test1.safeevaluate scope match2, code2 = @test2.safeevaluate scope # First build up the virtual code. # If we're a conjunction operator, then we're calling code. I did # some speed comparisons, and it's at least twice as fast doing these # case statements as doing an eval here. code = proc do |resource| case @oper when "and"; code1.call(resource) and code2.call(resource) when "or"; code1.call(resource) or code2.call(resource) when "==" if match1 == "tag" resource.tagged?(match2) else if resource[match1].is_a?(Array) @@compare_operator.include?(resource[match1], match2) else @@compare_operator.equals(resource[match1], match2) end end when "!="; ! @@compare_operator.equals(resource[match1], match2) end end match = [match1, @oper, match2] return match, code end def initialize(hash = {}) super if Puppet[:parser] == "future" @@compare_operator ||= Puppet::Pops::Evaluator::CompareOperator.new end raise ArgumentError, "Invalid operator #{@oper}" unless %w{== != and or}.include?(@oper) end end end diff --git a/lib/puppet/parser/ast/tag.rb b/lib/puppet/parser/ast/tag.rb deleted file mode 100644 index 6f906a1c6..000000000 --- a/lib/puppet/parser/ast/tag.rb +++ /dev/null @@ -1,24 +0,0 @@ -require 'puppet/parser/ast/branch' - -class Puppet::Parser::AST - # The code associated with a class. This is different from components - # in that each class is a singleton -- only one will exist for a given - # node. - class Tag < AST::Branch - @name = :class - attr_accessor :type - - def evaluate(scope) - types = @type.safeevaluate(scope) - - types = [types] unless types.is_a? Array - - types.each do |type| - # Now set our class. We don't have to worry about checking - # whether we've been evaluated because we're not evaluating - # any code. - scope.setclass(self.object_id, type) - end - end - end -end diff --git a/lib/puppet/parser/resource.rb b/lib/puppet/parser/resource.rb index e17479ba6..506f8a27e 100644 --- a/lib/puppet/parser/resource.rb +++ b/lib/puppet/parser/resource.rb @@ -1,267 +1,278 @@ require 'puppet/resource' # The primary difference between this class and its # parent is that this class has rules on who can set # parameters class Puppet::Parser::Resource < Puppet::Resource require 'puppet/parser/resource/param' require 'puppet/util/tagging' require 'puppet/parser/yaml_trimmer' require 'puppet/resource/type_collection_helper' include Puppet::Resource::TypeCollectionHelper include Puppet::Util include Puppet::Util::MethodHelper include Puppet::Util::Errors include Puppet::Util::Logging include Puppet::Parser::YamlTrimmer attr_accessor :source, :scope, :collector_id attr_accessor :virtual, :override, :translated, :catalog, :evaluated attr_accessor :file, :line attr_reader :exported, :parameters # Determine whether the provided parameter name is a relationship parameter. def self.relationship_parameter?(name) @relationship_names ||= Puppet::Type.relationship_params.collect { |p| p.name } @relationship_names.include?(name) end # Set up some boolean test methods def translated?; !!@translated; end def override?; !!@override; end def evaluated?; !!@evaluated; end def [](param) param = param.intern if param == :title return self.title end if @parameters.has_key?(param) @parameters[param].value else nil end end def eachparam @parameters.each do |name, param| yield param end end def environment scope.environment end # Process the stage metaparameter for a class. A containment edge # is drawn from the class to the stage. The stage for containment # defaults to main, if none is specified. def add_edge_to_stage return unless self.class? unless stage = catalog.resource(:stage, self[:stage] || (scope && scope.resource && scope.resource[:stage]) || :main) raise ArgumentError, "Could not find stage #{self[:stage] || :main} specified by #{self}" end self[:stage] ||= stage.title unless stage.title == :main catalog.add_edge(stage, self) end # Retrieve the associated definition and evaluate it. def evaluate return if evaluated? @evaluated = true if klass = resource_type and ! builtin_type? finish evaluated_code = klass.evaluate_code(self) return evaluated_code elsif builtin? devfail "Cannot evaluate a builtin type (#{type})" else self.fail "Cannot find definition #{type}" end end # Mark this resource as both exported and virtual, # or remove the exported mark. def exported=(value) if value @virtual = true @exported = value else @exported = value end end # Do any finishing work on this object, called before evaluation or # before storage/translation. def finish return if finished? @finished = true add_defaults add_scope_tags validate end # Has this resource already been finished? def finished? @finished end def initialize(*args) raise ArgumentError, "Resources require a hash as last argument" unless args.last.is_a? Hash raise ArgumentError, "Resources require a scope" unless args.last[:scope] super @source ||= scope.source end # Is this resource modeling an isomorphic resource type? def isomorphic? if builtin_type? return resource_type.isomorphic? else return true end end # Merge an override resource in. This will throw exceptions if # any overrides aren't allowed. def merge(resource) # Test the resource scope, to make sure the resource is even allowed # to override. unless self.source.object_id == resource.source.object_id || resource.source.child_of?(self.source) raise Puppet::ParseError.new("Only subclasses can override parameters", resource.line, resource.file) end # Some of these might fail, but they'll fail in the way we want. resource.parameters.each do |name, param| override_parameter(param) end end # This only mattered for clients < 0.25, which we don't support any longer. # ...but, since this hasn't been deprecated, and at least some functions # used it, deprecate now rather than just eliminate. --daniel 2012-07-15 def metaparam_compatibility_mode? Puppet.deprecation_warning "metaparam_compatibility_mode? is obsolete since < 0.25 clients are really, really not supported any more" false end def name self[:name] || self.title end # A temporary occasion, until I get paths in the scopes figured out. alias path to_s # Define a parameter in our resource. # if we ever receive a parameter named 'tag', set # the resource tags with its value. def set_parameter(param, value = nil) if ! value.nil? param = Puppet::Parser::Resource::Param.new( :name => param, :value => value, :source => self.source ) elsif ! param.is_a?(Puppet::Parser::Resource::Param) raise ArgumentError, "Received incomplete information - no value provided for parameter #{param}" end tag(*param.value) if param.name == :tag # And store it in our parameter hash. @parameters[param.name] = param end alias []= set_parameter def to_hash @parameters.inject({}) do |hash, ary| param = ary[1] # Skip "undef" values. hash[param.name] = param.value if param.value != :undef hash end end # Convert this resource to a RAL resource. def to_ral copy_as_resource.to_ral end + # Is the receiver tagged with the given tags? + # This match takes into account the tags that a resource will inherit from its container + # but have not been set yet. + # It does *not* take tags set via resource defaults as these will *never* be set on + # the resource itself since all resources always have tags that are automatically + # assigned. + # + def tagged?(*tags) + super || ((scope_resource = scope.resource) && scope_resource != self && scope_resource.tagged?(tags)) + end + private # Add default values from our definition. def add_defaults scope.lookupdefaults(self.type).each do |name, param| unless @parameters.include?(name) self.debug "Adding default for #{name}" @parameters[name] = param.dup end end end def add_scope_tags if scope_resource = scope.resource tag(*scope_resource.tags) end end # Accept a parameter from an override. def override_parameter(param) # This can happen if the override is defining a new parameter, rather # than replacing an existing one. (set_parameter(param) and return) unless current = @parameters[param.name] # The parameter is already set. Fail if they're not allowed to override it. unless param.source.child_of?(current.source) msg = "Parameter '#{param.name}' is already set on #{self}" msg += " by #{current.source}" if current.source.to_s != "" if current.file or current.line fields = [] fields << current.file if current.file fields << current.line.to_s if current.line msg += " at #{fields.join(":")}" end msg += "; cannot redefine" Puppet.log_exception(ArgumentError.new(), msg) raise Puppet::ParseError.new(msg, param.line, param.file) end # If we've gotten this far, we're allowed to override. # Merge with previous value, if the parameter was generated with the +> # syntax. It's important that we use a copy of the new param instance # here, not the old one, and not the original new one, so that the source # is registered correctly for later overrides but the values aren't # implcitly shared when multiple resources are overrriden at once (see # ticket #3556). if param.add param = param.dup param.value = [current.value, param.value].flatten end set_parameter(param) end # Make sure the resource's parameters are all valid for the type. def validate @parameters.each do |name, param| validate_parameter(name) end rescue => detail self.fail Puppet::ParseError, detail.to_s + " on #{self}", detail end def extract_parameters(params) params.each do |param| # Don't set the same parameter twice self.fail Puppet::ParseError, "Duplicate parameter '#{param.name}' for on #{self}" if @parameters[param.name] set_parameter(param) end end end diff --git a/lib/puppet/util/tagging.rb b/lib/puppet/util/tagging.rb index 031b27f93..266029a1f 100644 --- a/lib/puppet/util/tagging.rb +++ b/lib/puppet/util/tagging.rb @@ -1,55 +1,56 @@ require 'puppet/util/tag_set' module Puppet::Util::Tagging ValidTagRegex = /^\w[-\w:.]*$/ - # Add a tag to our current list. These tags will be added to all - # of the objects contained in this scope. + # Add a tag to the current tag set. + # When a tag set is used for a scope, these tags will be added to all of + # the objects contained in this scope when the objects are finished. + # def tag(*ary) @tags ||= new_tags - ary.each do |tag| + ary.flatten.each do |tag| name = tag.to_s.downcase if name =~ ValidTagRegex @tags << name name.split("::").each do |section| @tags << section end else - fail(Puppet::ParseError, "Invalid tag #{name}") + fail(Puppet::ParseError, "Invalid tag '#{name}'") end end end - # Are we tagged with the provided tag? + # Is the receiver tagged with the given tags? def tagged?(*tags) not ( self.tags & tags.flatten.collect { |t| t.to_s } ).empty? end # Return a copy of the tag list, so someone can't ask for our tags # and then modify them. def tags @tags ||= new_tags @tags.dup end def tags=(tags) @tags = new_tags return if tags.nil? or tags == "" tags = tags.strip.split(/\s*,\s*/) if tags.is_a?(String) - tags.each {|t| tag(t) } end private def valid_tag?(tag) tag.is_a?(String) and tag =~ ValidTagRegex end def new_tags Puppet::Util::TagSet.new end end diff --git a/spec/integration/parser/compiler_spec.rb b/spec/integration/parser/compiler_spec.rb index f10ce1adc..6ce0b7d67 100755 --- a/spec/integration/parser/compiler_spec.rb +++ b/spec/integration/parser/compiler_spec.rb @@ -1,513 +1,541 @@ #! /usr/bin/env ruby require 'spec_helper' require 'puppet/parser/parser_factory' require 'puppet_spec/compiler' require 'matchers/resource' describe "Puppet::Parser::Compiler" do include PuppetSpec::Compiler include Matchers::Resource before :each do @node = Puppet::Node.new "testnode" @scope_resource = stub 'scope_resource', :builtin? => true, :finish => nil, :ref => 'Class[main]' @scope = stub 'scope', :resource => @scope_resource, :source => mock("source") end after do Puppet.settings.clear end # shared because tests are invoked both for classic and future parser # shared_examples_for "the compiler" do it "should be able to determine the configuration version from a local version control repository" do pending("Bug #14071 about semantics of Puppet::Util::Execute on Windows", :if => Puppet.features.microsoft_windows?) do # This should always work, because we should always be # in the puppet repo when we run this. version = %x{git rev-parse HEAD}.chomp Puppet.settings[:config_version] = 'git rev-parse HEAD' @parser = Puppet::Parser::ParserFactory.parser "development" @compiler = Puppet::Parser::Compiler.new(@node) @compiler.catalog.version.should == version end end it "should not create duplicate resources when a class is referenced both directly and indirectly by the node classifier (4792)" do Puppet[:code] = <<-PP class foo { notify { foo_notify: } include bar } class bar { notify { bar_notify: } } PP @node.stubs(:classes).returns(['foo', 'bar']) catalog = Puppet::Parser::Compiler.compile(@node) catalog.resource("Notify[foo_notify]").should_not be_nil catalog.resource("Notify[bar_notify]").should_not be_nil end describe "when resolving class references" do it "should favor local scope, even if there's an included class in topscope" do Puppet[:code] = <<-PP class experiment { class baz { } notify {"x" : require => Class[Baz] } } class baz { } include baz include experiment include experiment::baz PP catalog = Puppet::Parser::Compiler.compile(Puppet::Node.new("mynode")) notify_resource = catalog.resource( "Notify[x]" ) notify_resource[:require].title.should == "Experiment::Baz" end it "should favor local scope, even if there's an unincluded class in topscope" do Puppet[:code] = <<-PP class experiment { class baz { } notify {"x" : require => Class[Baz] } } class baz { } include experiment include experiment::baz PP catalog = Puppet::Parser::Compiler.compile(Puppet::Node.new("mynode")) notify_resource = catalog.resource( "Notify[x]" ) notify_resource[:require].title.should == "Experiment::Baz" end end describe "(ticket #13349) when explicitly specifying top scope" do ["class {'::bar::baz':}", "include ::bar::baz"].each do |include| describe "with #{include}" do it "should find the top level class" do Puppet[:code] = <<-MANIFEST class { 'foo::test': } class foo::test { #{include} } class bar::baz { notify { 'good!': } } class foo::bar::baz { notify { 'bad!': } } MANIFEST catalog = Puppet::Parser::Compiler.compile(Puppet::Node.new("mynode")) catalog.resource("Class[Bar::Baz]").should_not be_nil catalog.resource("Notify[good!]").should_not be_nil catalog.resource("Class[Foo::Bar::Baz]").should be_nil catalog.resource("Notify[bad!]").should be_nil end end end end it "should recompute the version after input files are re-parsed" do Puppet[:code] = 'class foo { }' Time.stubs(:now).returns(1) node = Puppet::Node.new('mynode') Puppet::Parser::Compiler.compile(node).version.should == 1 Time.stubs(:now).returns(2) Puppet::Parser::Compiler.compile(node).version.should == 1 # no change because files didn't change Puppet::Resource::TypeCollection.any_instance.stubs(:stale?).returns(true).then.returns(false) # pretend change Puppet::Parser::Compiler.compile(node).version.should == 2 end ['class', 'define', 'node'].each do |thing| it "should not allow '#{thing}' inside evaluated conditional constructs" do Puppet[:code] = <<-PP if true { #{thing} foo { } notify { decoy: } } PP begin Puppet::Parser::Compiler.compile(Puppet::Node.new("mynode")) raise "compilation should have raised Puppet::Error" rescue Puppet::Error => e e.message.should =~ /at line 2/ end end end it "should not allow classes inside unevaluated conditional constructs" do Puppet[:code] = <<-PP if false { class foo { } } PP lambda { Puppet::Parser::Compiler.compile(Puppet::Node.new("mynode")) }.should raise_error(Puppet::Error) end describe "when defining relationships" do def extract_name(ref) ref.sub(/File\[(\w+)\]/, '\1') end let(:node) { Puppet::Node.new('mynode') } let(:code) do <<-MANIFEST file { [a,b,c]: mode => 0644, } file { [d,e]: mode => 0755, } MANIFEST end let(:expected_relationships) { [] } let(:expected_subscriptions) { [] } before :each do Puppet[:code] = code end after :each do catalog = Puppet::Parser::Compiler.compile(node) resources = catalog.resources.select { |res| res.type == 'File' } actual_relationships, actual_subscriptions = [:before, :notify].map do |relation| resources.map do |res| dependents = Array(res[relation]) dependents.map { |ref| [res.title, extract_name(ref)] } end.inject(&:concat) end actual_relationships.should =~ expected_relationships actual_subscriptions.should =~ expected_subscriptions end it "should create a relationship" do code << "File[a] -> File[b]" expected_relationships << ['a','b'] end it "should create a subscription" do code << "File[a] ~> File[b]" expected_subscriptions << ['a', 'b'] end it "should create relationships using title arrays" do code << "File[a,b] -> File[c,d]" expected_relationships.concat [ ['a', 'c'], ['b', 'c'], ['a', 'd'], ['b', 'd'], ] end it "should create relationships using collection expressions" do code << "File <| mode == 0644 |> -> File <| mode == 0755 |>" expected_relationships.concat [ ['a', 'd'], ['b', 'd'], ['c', 'd'], ['a', 'e'], ['b', 'e'], ['c', 'e'], ] end it "should create relationships using resource names" do code << "'File[a]' -> 'File[b]'" expected_relationships << ['a', 'b'] end it "should create relationships using variables" do code << <<-MANIFEST $var = File[a] $var -> File[b] MANIFEST expected_relationships << ['a', 'b'] end it "should create relationships using case statements" do code << <<-MANIFEST $var = 10 case $var { 10: { file { s1: } } 12: { file { s2: } } } -> case $var + 2 { 10: { file { t1: } } 12: { file { t2: } } } MANIFEST expected_relationships << ['s1', 't2'] end it "should create relationships using array members" do code << <<-MANIFEST $var = [ [ [ File[a], File[b] ] ] ] $var[0][0][0] -> $var[0][0][1] MANIFEST expected_relationships << ['a', 'b'] end it "should create relationships using hash members" do code << <<-MANIFEST $var = {'foo' => {'bar' => {'source' => File[a], 'target' => File[b]}}} $var[foo][bar][source] -> $var[foo][bar][target] MANIFEST expected_relationships << ['a', 'b'] end it "should create relationships using resource declarations" do code << "file { l: } -> file { r: }" expected_relationships << ['l', 'r'] end it "should chain relationships" do code << "File[a] -> File[b] ~> File[c] <- File[d] <~ File[e]" expected_relationships << ['a', 'b'] << ['d', 'c'] expected_subscriptions << ['b', 'c'] << ['e', 'd'] end end context 'when working with immutable node data' do context 'and have opted in to immutable_node_data' do before :each do Puppet[:immutable_node_data] = true end def node_with_facts(facts) Puppet[:facts_terminus] = :memory Puppet::Node::Facts.indirection.save(Puppet::Node::Facts.new("testing", facts)) node = Puppet::Node.new("testing") node.fact_merge node end matcher :fail_compile_with do |node, message_regex| match do |manifest| @error = nil begin compile_to_catalog(manifest, node) false rescue Puppet::Error => e @error = e message_regex.match(e.message) end end failure_message_for_should do if @error "failed with #{@error}\n#{@error.backtrace}" else "did not fail" end end end it 'should make $facts available' do node = node_with_facts('the_facts' => 'straight') catalog = compile_to_catalog(<<-MANIFEST, node) notify { 'test': message => $facts[the_facts] } MANIFEST catalog.resource("Notify[test]")[:message].should == "straight" end it 'should make $facts reserved' do node = node_with_facts('the_facts' => 'straight') expect('$facts = {}').to fail_compile_with(node, /assign to a reserved variable name: 'facts'/) expect('class a { $facts = {} } include a').to fail_compile_with(node, /assign to a reserved variable name: 'facts'/) end it 'should make $facts immutable' do node = node_with_facts('string' => 'value', 'array' => ['string'], 'hash' => { 'a' => 'string' }, 'number' => 1, 'boolean' => true) expect('$i=inline_template("<% @facts[%q{new}] = 2 %>")').to fail_compile_with(node, /frozen Hash/i) expect('$i=inline_template("<% @facts[%q{string}].chop! %>")').to fail_compile_with(node, /frozen String/i) expect('$i=inline_template("<% @facts[%q{array}][0].chop! %>")').to fail_compile_with(node, /frozen String/i) expect('$i=inline_template("<% @facts[%q{array}][1] = 2 %>")').to fail_compile_with(node, /frozen Array/i) expect('$i=inline_template("<% @facts[%q{hash}][%q{a}].chop! %>")').to fail_compile_with(node, /frozen String/i) expect('$i=inline_template("<% @facts[%q{hash}][%q{b}] = 2 %>")').to fail_compile_with(node, /frozen Hash/i) end it 'should make $facts available even if there are no facts' do Puppet[:facts_terminus] = :memory node = Puppet::Node.new("testing2") node.fact_merge catalog = compile_to_catalog(<<-MANIFEST, node) notify { 'test': message => $facts } MANIFEST expect(catalog).to have_resource("Notify[test]").with_parameter(:message, {}) end end context 'and have not opted in to immutable_node_data' do before :each do Puppet[:immutable_node_data] = false end it 'should not make $facts available' do Puppet[:facts_terminus] = :memory facts = Puppet::Node::Facts.new("testing", 'the_facts' => 'straight') Puppet::Node::Facts.indirection.save(facts) node = Puppet::Node.new("testing") node.fact_merge catalog = compile_to_catalog(<<-MANIFEST, node) notify { 'test': message => "An $facts space" } MANIFEST catalog.resource("Notify[test]")[:message].should == "An space" end end end context 'when working with the trusted data hash' do context 'and have opted in to trusted_node_data' do before :each do Puppet[:trusted_node_data] = true end it 'should make $trusted available' do node = Puppet::Node.new("testing") node.trusted_data = { "data" => "value" } catalog = compile_to_catalog(<<-MANIFEST, node) notify { 'test': message => $trusted[data] } MANIFEST catalog.resource("Notify[test]")[:message].should == "value" end it 'should not allow assignment to $trusted' do node = Puppet::Node.new("testing") node.trusted_data = { "data" => "value" } expect do catalog = compile_to_catalog(<<-MANIFEST, node) $trusted = 'changed' notify { 'test': message => $trusted == 'changed' } MANIFEST catalog.resource("Notify[test]")[:message].should == true end.to raise_error(Puppet::Error, /Attempt to assign to a reserved variable name: 'trusted'/) end it 'should not allow addition to $trusted hash' do node = Puppet::Node.new("testing") node.trusted_data = { "data" => "value" } expect do catalog = compile_to_catalog(<<-MANIFEST, node) $trusted['extra'] = 'added' notify { 'test': message => $trusted['extra'] == 'added' } MANIFEST catalog.resource("Notify[test]")[:message].should == true # different errors depending on regular or future parser end.to raise_error(Puppet::Error, /(can't modify frozen [hH]ash)|(Illegal attempt to assign)/) end it 'should not allow addition to $trusted hash via Ruby inline template' do node = Puppet::Node.new("testing") node.trusted_data = { "data" => "value" } expect do catalog = compile_to_catalog(<<-MANIFEST, node) $dummy = inline_template("<% @trusted['extra'] = 'added' %> lol") notify { 'test': message => $trusted['extra'] == 'added' } MANIFEST catalog.resource("Notify[test]")[:message].should == true end.to raise_error(Puppet::Error, /can't modify frozen [hH]ash/) end end context 'and have not opted in to trusted_node_data' do before :each do Puppet[:trusted_node_data] = false end it 'should not make $trusted available' do node = Puppet::Node.new("testing") node.trusted_data = { "data" => "value" } catalog = compile_to_catalog(<<-MANIFEST, node) notify { 'test': message => $trusted == undef } MANIFEST catalog.resource("Notify[test]")[:message].should == true end it 'should allow assignment to $trusted' do node = Puppet::Node.new("testing") catalog = compile_to_catalog(<<-MANIFEST, node) $trusted = 'changed' notify { 'test': message => $trusted == 'changed' } MANIFEST catalog.resource("Notify[test]")[:message].should == true end end end + + context 'when evaluating collection' do + it 'matches on container inherited tags' do + Puppet[:code] = <<-MANIFEST + class xport_test { + tag 'foo_bar' + @notify { 'nbr1': + message => 'explicitly tagged', + tag => 'foo_bar' + } + + @notify { 'nbr2': + message => 'implicitly tagged' + } + + Notify <| tag == 'foo_bar' |> { + message => 'overridden' + } + } + include xport_test + MANIFEST + + catalog = Puppet::Parser::Compiler.compile(Puppet::Node.new("mynode")) + + expect(catalog).to have_resource("Notify[nbr1]").with_parameter(:message, 'overridden') + expect(catalog).to have_resource("Notify[nbr2]").with_parameter(:message, 'overridden') + end + end end describe 'using classic parser' do before :each do Puppet[:parser] = 'current' end it_behaves_like 'the compiler' do end end end diff --git a/spec/integration/parser/future_compiler_spec.rb b/spec/integration/parser/future_compiler_spec.rb index 9d4d98776..7fcd7aaaa 100644 --- a/spec/integration/parser/future_compiler_spec.rb +++ b/spec/integration/parser/future_compiler_spec.rb @@ -1,370 +1,399 @@ require 'spec_helper' require 'puppet/pops' require 'puppet/parser/parser_factory' require 'puppet_spec/compiler' require 'puppet_spec/pops' require 'puppet_spec/scope' require 'matchers/resource' require 'rgen/metamodel_builder' # Test compilation using the future evaluator describe "Puppet::Parser::Compiler" do include PuppetSpec::Compiler include Matchers::Resource before :each do Puppet[:parser] = 'future' end describe "the compiler when using future parser and evaluator" do it "should be able to determine the configuration version from a local version control repository" do pending("Bug #14071 about semantics of Puppet::Util::Execute on Windows", :if => Puppet.features.microsoft_windows?) do # This should always work, because we should always be # in the puppet repo when we run this. version = %x{git rev-parse HEAD}.chomp Puppet.settings[:config_version] = 'git rev-parse HEAD' compiler = Puppet::Parser::Compiler.new(Puppet::Node.new("testnode")) compiler.catalog.version.should == version end end it "should not create duplicate resources when a class is referenced both directly and indirectly by the node classifier (4792)" do node = Puppet::Node.new("testnodex") node.classes = ['foo', 'bar'] catalog = compile_to_catalog(<<-PP, node) class foo { notify { foo_notify: } include bar } class bar { notify { bar_notify: } } PP catalog = Puppet::Parser::Compiler.compile(node) expect(catalog).to have_resource("Notify[foo_notify]") expect(catalog).to have_resource("Notify[bar_notify]") end it 'applies defaults for defines with qualified names (PUP-2302)' do catalog = compile_to_catalog(<<-CODE) define my::thing($msg = 'foo') { notify {'check_me': message => $msg } } My::Thing { msg => 'evoe' } my::thing { 'name': } CODE expect(catalog).to have_resource("Notify[check_me]").with_parameter(:message, "evoe") end describe "when resolving class references" do it "should favor local scope, even if there's an included class in topscope" do catalog = compile_to_catalog(<<-PP) class experiment { class baz { } notify {"x" : require => Class[Baz] } } class baz { } include baz include experiment include experiment::baz PP expect(catalog).to have_resource("Notify[x]").with_parameter(:require, be_resource("Class[Experiment::Baz]")) end it "should favor local scope, even if there's an unincluded class in topscope" do catalog = compile_to_catalog(<<-PP) class experiment { class baz { } notify {"x" : require => Class[Baz] } } class baz { } include experiment include experiment::baz PP expect(catalog).to have_resource("Notify[x]").with_parameter(:require, be_resource("Class[Experiment::Baz]")) end end describe "(ticket #13349) when explicitly specifying top scope" do ["class {'::bar::baz':}", "include ::bar::baz"].each do |include| describe "with #{include}" do it "should find the top level class" do catalog = compile_to_catalog(<<-MANIFEST) class { 'foo::test': } class foo::test { #{include} } class bar::baz { notify { 'good!': } } class foo::bar::baz { notify { 'bad!': } } MANIFEST expect(catalog).to have_resource("Class[Bar::Baz]") expect(catalog).to have_resource("Notify[good!]") expect(catalog).to_not have_resource("Class[Foo::Bar::Baz]") expect(catalog).to_not have_resource("Notify[bad!]") end end end end it "should recompute the version after input files are re-parsed" do Puppet[:code] = 'class foo { }' Time.stubs(:now).returns(1) node = Puppet::Node.new('mynode') Puppet::Parser::Compiler.compile(node).version.should == 1 Time.stubs(:now).returns(2) Puppet::Parser::Compiler.compile(node).version.should == 1 # no change because files didn't change Puppet::Resource::TypeCollection.any_instance.stubs(:stale?).returns(true).then.returns(false) # pretend change Puppet::Parser::Compiler.compile(node).version.should == 2 end ['define', 'class', 'node'].each do |thing| it "'#{thing}' is not allowed inside evaluated conditional constructs" do expect do compile_to_catalog(<<-PP) if true { #{thing} foo { } notify { decoy: } } PP end.to raise_error(Puppet::Error, /Classes, definitions, and nodes may only appear at toplevel/) end it "'#{thing}' is not allowed inside un-evaluated conditional constructs" do expect do compile_to_catalog(<<-PP) if false { #{thing} foo { } notify { decoy: } } PP end.to raise_error(Puppet::Error, /Classes, definitions, and nodes may only appear at toplevel/) end end describe "relationships can be formed" do def extract_name(ref) ref.sub(/File\[(\w+)\]/, '\1') end def assert_creates_relationships(relationship_code, expectations) base_manifest = <<-MANIFEST file { [a,b,c]: mode => 0644, } file { [d,e]: mode => 0755, } MANIFEST catalog = compile_to_catalog(base_manifest + relationship_code) resources = catalog.resources.select { |res| res.type == 'File' } actual_relationships, actual_subscriptions = [:before, :notify].map do |relation| resources.map do |res| dependents = Array(res[relation]) dependents.map { |ref| [res.title, extract_name(ref)] } end.inject(&:concat) end actual_relationships.should =~ (expectations[:relationships] || []) actual_subscriptions.should =~ (expectations[:subscriptions] || []) end it "of regular type" do assert_creates_relationships("File[a] -> File[b]", :relationships => [['a','b']]) end it "of subscription type" do assert_creates_relationships("File[a] ~> File[b]", :subscriptions => [['a', 'b']]) end it "between multiple resources expressed as resource with multiple titles" do assert_creates_relationships("File[a,b] -> File[c,d]", :relationships => [['a', 'c'], ['b', 'c'], ['a', 'd'], ['b', 'd']]) end it "between collection expressions" do assert_creates_relationships("File <| mode == 0644 |> -> File <| mode == 0755 |>", :relationships => [['a', 'd'], ['b', 'd'], ['c', 'd'], ['a', 'e'], ['b', 'e'], ['c', 'e']]) end it "between resources expressed as Strings" do assert_creates_relationships("'File[a]' -> 'File[b]'", :relationships => [['a', 'b']]) end it "between resources expressed as variables" do assert_creates_relationships(<<-MANIFEST, :relationships => [['a', 'b']]) $var = File[a] $var -> File[b] MANIFEST end it "between resources expressed as case statements" do assert_creates_relationships(<<-MANIFEST, :relationships => [['s1', 't2']]) $var = 10 case $var { 10: { file { s1: } } 12: { file { s2: } } } -> case $var + 2 { 10: { file { t1: } } 12: { file { t2: } } } MANIFEST end it "using deep access in array" do assert_creates_relationships(<<-MANIFEST, :relationships => [['a', 'b']]) $var = [ [ [ File[a], File[b] ] ] ] $var[0][0][0] -> $var[0][0][1] MANIFEST end it "using deep access in hash" do assert_creates_relationships(<<-MANIFEST, :relationships => [['a', 'b']]) $var = {'foo' => {'bar' => {'source' => File[a], 'target' => File[b]}}} $var[foo][bar][source] -> $var[foo][bar][target] MANIFEST end it "using resource declarations" do assert_creates_relationships("file { l: } -> file { r: }", :relationships => [['l', 'r']]) end it "between entries in a chain of relationships" do assert_creates_relationships("File[a] -> File[b] ~> File[c] <- File[d] <~ File[e]", :relationships => [['a', 'b'], ['d', 'c']], :subscriptions => [['b', 'c'], ['e', 'd']]) end end context "when dealing with variable references" do it 'an initial underscore in a variable name is ok' do catalog = compile_to_catalog(<<-MANIFEST) class a { $_a = 10} include a notify { 'test': message => $a::_a } MANIFEST expect(catalog).to have_resource("Notify[test]").with_parameter(:message, 10) end it 'an initial underscore in not ok if elsewhere than last segment' do expect do catalog = compile_to_catalog(<<-MANIFEST) class a { $_a = 10} include a notify { 'test': message => $_a::_a } MANIFEST end.to raise_error(/Illegal variable name/) end it 'a missing variable as default value becomes undef' do catalog = compile_to_catalog(<<-MANIFEST) class a ($b=$x) { notify {$b: message=>'meh'} } include a MANIFEST expect(catalog).to have_resource("Notify[undef]").with_parameter(:message, "meh") end end context 'when working with the trusted data hash' do context 'and have opted in to hashed_node_data' do before :each do Puppet[:trusted_node_data] = true end it 'should make $trusted available' do node = Puppet::Node.new("testing") node.trusted_data = { "data" => "value" } catalog = compile_to_catalog(<<-MANIFEST, node) notify { 'test': message => $trusted[data] } MANIFEST expect(catalog).to have_resource("Notify[test]").with_parameter(:message, "value") end it 'should not allow assignment to $trusted' do node = Puppet::Node.new("testing") node.trusted_data = { "data" => "value" } expect do compile_to_catalog(<<-MANIFEST, node) $trusted = 'changed' notify { 'test': message => $trusted == 'changed' } MANIFEST end.to raise_error(Puppet::Error, /Attempt to assign to a reserved variable name: 'trusted'/) end end context 'and have not opted in to hashed_node_data' do before :each do Puppet[:trusted_node_data] = false end it 'should not make $trusted available' do node = Puppet::Node.new("testing") node.trusted_data = { "data" => "value" } catalog = compile_to_catalog(<<-MANIFEST, node) notify { 'test': message => ($trusted == undef) } MANIFEST expect(catalog).to have_resource("Notify[test]").with_parameter(:message, true) end it 'should allow assignment to $trusted' do catalog = compile_to_catalog(<<-MANIFEST) $trusted = 'changed' notify { 'test': message => $trusted == 'changed' } MANIFEST expect(catalog).to have_resource("Notify[test]").with_parameter(:message, true) end end end end + + context 'when evaluating collection' do + it 'matches on container inherited tags' do + Puppet[:code] = <<-MANIFEST + class xport_test { + tag('foo_bar') + @notify { 'nbr1': + message => 'explicitly tagged', + tag => 'foo_bar' + } + + @notify { 'nbr2': + message => 'implicitly tagged' + } + + Notify <| tag == 'foo_bar' |> { + message => 'overridden' + } + } + include xport_test + MANIFEST + + catalog = Puppet::Parser::Compiler.compile(Puppet::Node.new("mynode")) + + expect(catalog).to have_resource("Notify[nbr1]").with_parameter(:message, 'overridden') + expect(catalog).to have_resource("Notify[nbr2]").with_parameter(:message, 'overridden') + end + end + end diff --git a/spec/unit/util/tagging_spec.rb b/spec/unit/util/tagging_spec.rb index 248e915e9..c2ebeaab3 100755 --- a/spec/unit/util/tagging_spec.rb +++ b/spec/unit/util/tagging_spec.rb @@ -1,131 +1,162 @@ #! /usr/bin/env ruby require 'spec_helper' require 'puppet/util/tagging' describe Puppet::Util::Tagging do let(:tagger) { Object.new.extend(Puppet::Util::Tagging) } it "should add tags to the returned tag list" do tagger.tag("one") expect(tagger.tags).to include("one") end it "should return a duplicate of the tag list, rather than the original" do tagger.tag("one") tags = tagger.tags tags << "two" expect(tagger.tags).to_not include("two") end it "should add all provided tags to the tag list" do tagger.tag("one", "two") expect(tagger.tags).to include("one") expect(tagger.tags).to include("two") end it "should fail on tags containing '*' characters" do expect { tagger.tag("bad*tag") }.to raise_error(Puppet::ParseError) end it "should fail on tags starting with '-' characters" do expect { tagger.tag("-badtag") }.to raise_error(Puppet::ParseError) end it "should fail on tags containing ' ' characters" do expect { tagger.tag("bad tag") }.to raise_error(Puppet::ParseError) end it "should allow alpha tags" do expect { tagger.tag("good_tag") }.not_to raise_error end it "should allow tags containing '.' characters" do expect { tagger.tag("good.tag") }.to_not raise_error(Puppet::ParseError) end it "should add qualified classes as tags" do tagger.tag("one::two") expect(tagger.tags).to include("one::two") end it "should add each part of qualified classes as tags" do tagger.tag("one::two::three") expect(tagger.tags).to include('one') expect(tagger.tags).to include("two") expect(tagger.tags).to include("three") end it "should indicate when the object is tagged with a provided tag" do tagger.tag("one") expect(tagger).to be_tagged("one") end it "should indicate when the object is not tagged with a provided tag" do expect(tagger).to_not be_tagged("one") end it "should indicate when the object is tagged with any tag in an array" do tagger.tag("one") expect(tagger).to be_tagged("one","two","three") end it "should indicate when the object is not tagged with any tag in an array" do tagger.tag("one") expect(tagger).to_not be_tagged("two","three") end context "when tagging" do it "converts symbols to strings" do tagger.tag(:hello) expect(tagger.tags).to include('hello') end it "downcases tags" do tagger.tag(:HEllO) tagger.tag("GooDByE") expect(tagger).to be_tagged("hello") expect(tagger).to be_tagged("goodbye") end it "accepts hyphenated tags" do tagger.tag("my-tag") expect(tagger).to be_tagged("my-tag") end end context "when querying if tagged" do it "responds true if queried on the entire set" do tagger.tag("one", "two") expect(tagger).to be_tagged("one", "two") end it "responds true if queried on a subset" do tagger.tag("one", "two", "three") expect(tagger).to be_tagged("two", "one") end it "responds true if queried on an overlapping but not fully contained set" do tagger.tag("one", "two") expect(tagger).to be_tagged("zero", "one") end it "responds false if queried on a disjoint set" do tagger.tag("one", "two", "three") expect(tagger).to_not be_tagged("five") end it "responds false if queried on the empty set" do expect(tagger).to_not be_tagged end end context "when assigning tags" do it "splits a string on ','" do tagger.tags = "one, two, three" expect(tagger).to be_tagged("one") expect(tagger).to be_tagged("two") expect(tagger).to be_tagged("three") end + + it "protects against empty tags" do + expect { tagger.tags = "one,,two"}.to raise_error(/Invalid tag ''/) + end + + it "takes an array of tags" do + tagger.tags = ["one", "two"] + + expect(tagger).to be_tagged("one") + expect(tagger).to be_tagged("two") + end + + it "removes any existing tags when reassigning" do + tagger.tags = "one, two" + + tagger.tags = "three, four" + + expect(tagger).to_not be_tagged("one") + expect(tagger).to_not be_tagged("two") + expect(tagger).to be_tagged("three") + expect(tagger).to be_tagged("four") + end + + it "allows empty tags that are generated from :: separated tags" do + tagger.tags = "one::::two::three" + + expect(tagger).to be_tagged("one") + expect(tagger).to be_tagged("") + expect(tagger).to be_tagged("two") + expect(tagger).to be_tagged("three") + end end end