diff --git a/lib/puppet/functions/defined.rb b/lib/puppet/functions/defined.rb index f0d213be9..e95db4930 100644 --- a/lib/puppet/functions/defined.rb +++ b/lib/puppet/functions/defined.rb @@ -1,97 +1,131 @@ # 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 +# `defined( File['/tmp/myfile'] )`, or `defined( Class[myclass] )`. +# 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: +# 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: +# The `defined` function does not answer if data types (e.g. `Integer`) are defined. +# +# The rules for asking for undef, empty strings, and the main class are different from 3.x +# (non future parser) and 4.x (with future parser or in Puppet 4.0.0 and later): # # defined('') # 3.x => true, 4.x => false # defined(undef) # 3.x => true, 4.x => error # defined('main') # 3.x => false, 4.x => true # +# With the future parser, it is also possible to ask specifically if a name is +# a resource type (built in or defined), or a class, by giving its type: +# +# defined(Type[Class['foo']]) +# defined(Type[Resource['foo']]) +# +# Which is different from asking: +# +# defined('foo') +# +# Since the later returns true if 'foo' is either a class, a built-in resource type, or a user defined +# resource type, and a specific request like `Type[Class['foo']]` only returns true if `'foo'` is a class. +# # @since 2.7.0 # @since 3.6.0 variable reference and future parser types") +# @since 3.8.1 type specific requests with future parser # Puppet::Functions.create_function(:'defined', Puppet::Functions::InternalFunction) do - ARG_TYPE = 'Variant[String,Type[CatalogEntry]]' + ARG_TYPE = 'Variant[String,Type[CatalogEntry], Type[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 case val when '' next nil when 'main' - scope.compiler.findresource(:class, '') # scope.find_hostclass('') + # Find the main class (known as ''), it does not have to be in the catalog + scope.find_hostclass('') # scope.find_hostclass('') else - scope.find_resource_type(val) || scope.find_definition(val) || scope.compiler.findresource(:class, val) + # Find a resource type, definition or class definition + scope.find_resource_type(val) || scope.find_definition(val) || scope.find_hostclass(val) + #scope.compiler.findresource(:class, val) end end when Puppet::Resource + # Find instance of given resource type and title that is in the catalog 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) + when Puppet::Pops::Types::PType + case val.type + when Puppet::Pops::Types::PResourceType + # It is most reasonable to take Type[File] and Type[File[foo]] to mean the same as if not wrapped in a Type + # Since the difference between File and File[foo] already captures the distinction of type vs instance. + is_defined(scope, val.type) + + when Puppet::Pops::Types::PHostClassType + # Interpreted as asking if a class (and nothing else) is defined without having to be included in the catalog + # (this is the same as asking for just the class' name, but with the added certainty that it cannot be a defined type. + # + raise ArgumentError, "The given class type is a reference to all classes" if val.type.class_name.nil? + scope.find_hostclass(val.type.class_name) + end else raise ArgumentError, "Invalid argument of type '#{val.class}' to 'defined'" end end end end diff --git a/lib/puppet/parser/functions/defined.rb b/lib/puppet/parser/functions/defined.rb index c579912bb..20adccdf0 100644 --- a/lib/puppet/parser/functions/defined.rb +++ b/lib/puppet/parser/functions/defined.rb @@ -1,81 +1,95 @@ # Test whether a given class or definition is defined Puppet::Parser::Functions::newfunction(:defined, :type => :rvalue, :arity => -2, :doc => "Determine 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 parse 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 parse 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 + With the future parser, it is also possible to ask specifically if a name is + a resource type (built in or defined), or a class, by giving its type: + + defined(Type[Class['foo']]) + defined(Type[Resource['foo']]) + + Which is different from asking: + + defined('foo') + + Since the later returns true if 'foo' is either a class, a built-in resource type, or a user defined + resource type, and a specific request like `Type[Class['foo']]` only returns true if `'foo'` is a class. + - Since 2.7.0 - - Since 3.6.0 variable reference and future parser types") do |vals| + - Since 3.6.0 variable reference and future parser types + - Since 3.8.1 type specific requests with future parser") do |vals| vals = [vals] unless vals.is_a?(Array) vals.any? do |val| case val when String if m = /^\$(.+)$/.match(val) exist?(m[1]) else find_resource_type(val) or find_definition(val) or find_hostclass(val) end when Puppet::Resource compiler.findresource(val.type, val.title) else if Puppet.future_parser? case val 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? find_builtin_resource_type(val.type_name) || find_definition(val.type_name) else 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? find_hostclass(val.class_name) end 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 1330ddb07..caf356168 100755 --- a/spec/unit/functions/defined_spec.rb +++ b/spec/unit/functions/defined_spec.rb @@ -1,158 +1,291 @@ #! /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 + #--- CLASS + # + context "can determine if a class" do + context "is defined" do - it "is true when the name is defined as a definition" do - newdefine "yayness" - expect(func.call(@scope, "yayness")).to be_true - end + it "by using the class name in string form" do + newclass '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 "by using a Type[Class[name]] type reference" do + name = 'yayness' + newclass name + class_type = Puppet::Pops::Types::TypeFactory.host_class(name) + type_type = Puppet::Pops::Types::TypeFactory.type_type(class_type) + expect(func.call(@scope, type_type)).to be_true + end + 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 + context "is not defined" do + it "by using the class name in string form" do + expect(func.call(@scope, "yayness")).to be_false + end - it "is false when a single given name is not defined" do - expect(func.call(@scope, "meh")).to be_false - end + it "even if there is a define, by using a Type[Class[name]] type reference" do + name = 'yayness' + newdefine name + class_type = Puppet::Pops::Types::TypeFactory.host_class(name) + type_type = Puppet::Pops::Types::TypeFactory.type_type(class_type) + expect(func.call(@scope, type_type)).to be_false + end + end - it "is false when none of the names are defined" do - expect(func.call(@scope, "meh", "yayness", "booness")).to be_false - end + context "is defined and realized" do + it "by using a Class[name] reference" 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 + end + + context "is not realized" do + it "(although defined) by using a Class[name] reference" 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 "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 + it "(and not defined) by using a Class[name] reference" do + name = "cowabunga" + class_type = Puppet::Pops::Types::TypeFactory.host_class(name) + expect(func.call(@scope, class_type)).to be_false + end + end 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 + #---RESOURCE TYPE + # + context "can determine if a resource type" do + context "is defined" do + + it "by using the type name (of a built in type) in string form" do + expect(func.call(@scope, "file")).to be_true + end + + it "by using the type name (of a resource type) in string form" do + newdefine 'yayness' + expect(func.call(@scope, "yayness")).to be_true + end + + it "by using a File type reference (built in type)" do + resource_type = Puppet::Pops::Types::TypeFactory.resource('file') + type_type = Puppet::Pops::Types::TypeFactory.type_type(resource_type) + expect(func.call(@scope, type_type)).to be_true + end + + it "by using a Type[File] type reference" do + resource_type = Puppet::Pops::Types::TypeFactory.resource('file') + type_type = Puppet::Pops::Types::TypeFactory.type_type(resource_type) + expect(func.call(@scope, type_type)).to be_true + end + + it "by using a Resource[T] type reference (defined type)" do + name = 'yayness' + newdefine name + resource_type = Puppet::Pops::Types::TypeFactory.resource(name) + expect(func.call(@scope, resource_type)).to be_true + end + + it "by using a Type[Resource[T]] type reference (defined type)" do + name = 'yayness' + newdefine name + resource_type = Puppet::Pops::Types::TypeFactory.resource(name) + type_type = Puppet::Pops::Types::TypeFactory.type_type(resource_type) + expect(func.call(@scope, type_type)).to be_true + end end - it "is true when ::variable exists in scope" do - @compiler.topscope['x'] = 'something' - expect(func.call(@scope, '$::x')).to be_true + context "is not defined" do + it "by using the resource name in string form" do + expect(func.call(@scope, "notatype")).to be_false + end + + it "even if there is a class with the same name, by using a Type[Resource[T]] type reference" do + name = 'yayness' + newclass name + resource_type = Puppet::Pops::Types::TypeFactory.resource(name) + type_type = Puppet::Pops::Types::TypeFactory.type_type(resource_type) + expect(func.call(@scope, type_type)).to be_false + end 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 + context "is defined and instance realized" do + it "by using a Resource[T, title] reference for a built in type" do + type_name = 'file' + title = '/tmp/myfile' + newdefine type_name + newresource(type_name, title) + class_type = Puppet::Pops::Types::TypeFactory.resource(type_name, title) + expect(func.call(@scope, class_type)).to be_true + end + + it "by using a Resource[T, title] reference for a defined type" do + type_name = 'meme' + title = 'cowabunga' + newdefine type_name + newresource(type_name, title) + class_type = Puppet::Pops::Types::TypeFactory.resource(type_name, title) + expect(func.call(@scope, class_type)).to be_true + end end - it "is false when variable does not exist in scope" do - expect(func.call(@scope, '$x')).to be_false + context "is not realized" do + it "(although defined) by using a Resource[T, title] reference or Type[Resource[T, title]] reference" do + type_name = 'meme' + title = "cowabunga" + newdefine type_name + resource_type = Puppet::Pops::Types::TypeFactory.resource(type_name, title) + expect(func.call(@scope, resource_type)).to be_false + + type_type = Puppet::Pops::Types::TypeFactory.type_type(resource_type) + expect(func.call(@scope, type_type)).to be_false + end + + it "(and not defined) by using a Resource[T, title] reference or Type[Resource[T, title]] reference" do + type_name = 'meme' + title = "cowabunga" + resource_type = Puppet::Pops::Types::TypeFactory.resource(type_name, title) + expect(func.call(@scope, resource_type)).to be_false + + type_type = Puppet::Pops::Types::TypeFactory.type_type(resource_type) + expect(func.call(@scope, type_type)).to be_false + end 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 + #---VARIABLES + # + context "can determine if a variable" do + context "is defined" do + it "by giving the variable in string form" do + @scope['x'] = 'something' + expect(func.call(@scope, '$x')).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 "by giving a :: prefixed variable in string form" do + @compiler.topscope['x'] = 'something' + expect(func.call(@scope, '$::x')).to be_true + 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 "by giving a numeric variable in string form (when there is a match scope)" do + # with no match scope, there are no numeric variables defined + expect(func.call(@scope, '$0')).to be_false + expect(func.call(@scope, '$42')).to be_false + pattern = Regexp.new(".*") + @scope.new_match_scope(pattern.match("anything")) - 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 + # with a match scope, all numeric variables are set (the match defines if they have a value or not, but they are defined) + # even if their value is undef. + expect(func.call(@scope, '$0')).to be_true + expect(func.call(@scope, '$42')).to be_true + end + 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 + context "is undefined" do + it "by giving a :: prefixed or regular variable in string form" do + expect(func.call(@scope, '$x')).to be_false + expect(func.call(@scope, '$::x')).to be_false + end + end 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 + context "has any? semantics when given multiple arguments" do + it "and one of the names is a defined user defined type" do + newdefine "yayness" + expect(func.call(@scope, "meh", "yayness", "booness")).to be_true + end + + it "and one of the names is a built type" do + expect(func.call(@scope, "meh", "file", "booness")).to be_true + end + + it "and one of the names is a defined class" do + newclass "yayness" + expect(func.call(@scope, "meh", "yayness", "booness")).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 none of the names are defined" do + expect(func.call(@scope, "meh", "yayness", "booness")).to be_false + end 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 + it "raises an argument error when asking if Resource type 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 "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/) + expect{func.call(@scope, nil)}.to raise_error(ArgumentError, /mis-matched arguments/) + end + + it "raises error if referencing a number" do + expect{func.call(@scope, 42)}.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