diff --git a/lib/puppet/functions/defined.rb b/lib/puppet/functions/defined.rb index abcebfb66..f0d213be9 100644 --- a/lib/puppet/functions/defined.rb +++ b/lib/puppet/functions/defined.rb @@ -1,98 +1,97 @@ # Determines whether # a given class or resource type is defined. This function can also determine whether a # specific resource has been declared, or whether a variable has been assigned a value # (including undef...as opposed to never having been assigned anything). Returns true # or false. Accepts class names, type names, resource references, and variable # reference strings of the form '$name'. When more than one argument is # supplied, defined() returns true if any are defined. # # The `defined` function checks both native and defined types, including types # provided as plugins via modules. Types and classes are both checked using their names: # # defined("file") # defined("customtype") # defined("foo") # defined("foo::bar") # defined('$name') # # Resource declarations are checked using resource references, e.g. # `defined( File['/tmp/myfile'] )`. Checking whether a given resource # has been declared is, unfortunately, dependent on the evaluation order of # the configuration, and the following code will not work: # # if defined(File['/tmp/foo']) { # notify { "This configuration includes the /tmp/foo file.":} # } # file { "/tmp/foo": # ensure => present, # } # # However, this order requirement refers to evaluation order only, and ordering of # resources in the configuration graph (e.g. with `before` or `require`) does not # affect the behavior of `defined`. # # If the future parser is in effect, you may also search using types: # # defined(Resource['file','/some/file']) # defined(File['/some/file']) # defined(Class['foo']) # # When used with the future parser (4.x), the `defined` function does not answer if data # types (e.g. `Integer`) are defined, and the rules for asking for undef, empty strings, and # the main class are different: # # defined('') # 3.x => true, 4.x => false # defined(undef) # 3.x => true, 4.x => error # defined('main') # 3.x => false, 4.x => true # # @since 2.7.0 # @since 3.6.0 variable reference and future parser types") # Puppet::Functions.create_function(:'defined', Puppet::Functions::InternalFunction) do ARG_TYPE = 'Variant[String,Type[CatalogEntry]]' dispatch :is_defined do scope_param param ARG_TYPE, 'first_arg' repeated_param ARG_TYPE, 'additional_args' end def is_defined(scope, *vals) vals.any? do |val| case val when String if m = /^\$(.+)$/.match(val) scope.exist?(m[1]) else - val = case val + case val when '' next nil when 'main' - '' + scope.compiler.findresource(:class, '') # scope.find_hostclass('') else - val + scope.find_resource_type(val) || scope.find_definition(val) || scope.compiler.findresource(:class, val) end - scope.find_resource_type(val) || scope.find_definition(val) || scope.find_hostclass(val) end when Puppet::Resource scope.compiler.findresource(val.type, val.title) when Puppet::Pops::Types::PResourceType raise ArgumentError, "The given resource type is a reference to all kind of types" if val.type_name.nil? if val.title.nil? scope.find_builtin_resource_type(val.type_name) || scope.find_definition(val.type_name) else scope.compiler.findresource(val.type_name, val.title) end when Puppet::Pops::Types::PHostClassType raise ArgumentError, "The given class type is a reference to all classes" if val.class_name.nil? scope.compiler.findresource(:class, val.class_name) else raise ArgumentError, "Invalid argument of type '#{val.class}' to 'defined'" end end end end diff --git a/spec/unit/functions/defined_spec.rb b/spec/unit/functions/defined_spec.rb index 94f76b83d..1330ddb07 100755 --- a/spec/unit/functions/defined_spec.rb +++ b/spec/unit/functions/defined_spec.rb @@ -1,154 +1,158 @@ #! /usr/bin/env ruby require 'spec_helper' require 'puppet/pops' require 'puppet/loaders' describe "the 'defined' function" do after(:all) { Puppet::Pops::Loaders.clear } # This loads the function once and makes it easy to call it # It does not matter that it is not bound to the env used later since the function # looks up everything via the scope that is given to it. # The individual tests needs to have a fresh env/catalog set up # let(:loaders) { Puppet::Pops::Loaders.new(Puppet::Node::Environment.create(:testing, [])) } let(:func) { loaders.puppet_system_loader.load(:function, 'defined') } before :each do # This is only for the 4.x version of the defined function Puppet[:parser] = 'future' # A fresh environment is needed for each test since tests creates types and resources environment = Puppet::Node::Environment.create(:testing, []) @node = Puppet::Node.new("yaynode", :environment => environment) @known_resource_types = environment.known_resource_types @compiler = Puppet::Parser::Compiler.new(@node) @scope = Puppet::Parser::Scope.new(@compiler) end def newclass(name) @known_resource_types.add Puppet::Resource::Type.new(:hostclass, name) end def newdefine(name) @known_resource_types.add Puppet::Resource::Type.new(:definition, name) end def newresource(type, title) resource = Puppet::Resource.new(type, title) @compiler.add_resource(@scope, resource) resource end it "is true when the name is defined as a class" do newclass 'yayness' + newresource(:class, 'yayness') expect(func.call(@scope, "yayness")).to be_true end it "is true when the name is defined as a definition" do newdefine "yayness" expect(func.call(@scope, "yayness")).to be_true end it "is true when the name is defined as a builtin type" do expect(func.call(@scope, "file")).to be_true end it "is true when any of the provided names are defined" do newdefine "yayness" expect(func.call(@scope, "meh", "yayness", "booness")).to be_true end it "is false when a single given name is not defined" do expect(func.call(@scope, "meh")).to be_false end it "is false when none of the names are defined" do expect(func.call(@scope, "meh", "yayness", "booness")).to be_false end it "is true when a resource reference is provided and the resource is in the catalog" do resource = newresource("file", "/my/file") expect(func.call(@scope, resource)).to be_true end context "with string variable references" do it "is true when variable exists in scope" do @scope['x'] = 'something' expect(func.call(@scope, '$x')).to be_true end it "is true when ::variable exists in scope" do @compiler.topscope['x'] = 'something' expect(func.call(@scope, '$::x')).to be_true end it "is true when at least one variable exists in scope" do @scope['x'] = 'something' expect(func.call(@scope, '$y', '$x', '$z')).to be_true end it "is false when variable does not exist in scope" do expect(func.call(@scope, '$x')).to be_false end end it "is true when a future resource type reference is provided, and the resource is in the catalog" do resource = newresource("file", "/my/file") resource_type = Puppet::Pops::Types::TypeFactory.resource('file', '/my/file') expect(func.call(@scope, resource_type)).to be_true end it "raises an argument error if you ask if Resource is defined" do resource_type = Puppet::Pops::Types::TypeFactory.resource expect { func.call(@scope, resource_type)}.to raise_error(ArgumentError, /reference to all.*type/) end it "is true if referencing a built in type" do resource_type = Puppet::Pops::Types::TypeFactory.resource('file') expect(func.call(@scope, resource_type)).to be_true end it "is true if referencing a defined type" do @scope.known_resource_types.add Puppet::Resource::Type.new(:definition, "yayness") resource_type = Puppet::Pops::Types::TypeFactory.resource('yayness') expect(func.call(@scope, resource_type)).to be_true end it "is false if referencing an undefined type" do resource_type = Puppet::Pops::Types::TypeFactory.resource('barbershops') expect(func.call(@scope, resource_type)).to be_false end it "is true when a future class reference type is provided (and class is included)" do name = "cowabunga" newclass name newresource(:class, name) class_type = Puppet::Pops::Types::TypeFactory.host_class(name) expect(func.call(@scope, class_type)).to be_true end it "is false when a future class reference type is provided (and class is not included)" do name = "cowabunga" newclass name class_type = Puppet::Pops::Types::TypeFactory.host_class(name) expect(func.call(@scope, class_type)).to be_false end it "raises an argument error if you ask if Class is defined" do class_type = Puppet::Pops::Types::TypeFactory.host_class expect { func.call(@scope, class_type) }.to raise_error(ArgumentError, /reference to all.*class/) end it "raises error if referencing undef" do expect{func.call(@scope, nil)}.to raise_error(ArgumentError, /mis-matched arguments/) end it "is false if referencing empty string" do expect(func.call(@scope, '')).to be_false end it "is true if referencing 'main'" do + # mimic what compiler does with "main" in intial import + newclass '' + newresource :class, '' expect(func.call(@scope, 'main')).to be_true end end