diff --git a/lib/puppet/functions/defined.rb b/lib/puppet/functions/defined.rb new file mode 100644 index 000000000..06764abc3 --- /dev/null +++ b/lib/puppet/functions/defined.rb @@ -0,0 +1,98 @@ +# 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 + when '' + next nil + when 'main' + '' + else + 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.find_hostclass(val.class_name) + + 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 4ec2018a4..c579912bb 100644 --- a/lib/puppet/parser/functions/defined.rb +++ b/lib/puppet/parser/functions/defined.rb @@ -1,73 +1,81 @@ # 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 + - Since 2.7.0 - Since 3.6.0 variable reference and future parser types") 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 new file mode 100755 index 000000000..b02c6b392 --- /dev/null +++ b/spec/unit/functions/defined_spec.rb @@ -0,0 +1,146 @@ +#! /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' + 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" do + @scope.known_resource_types.add Puppet::Resource::Type.new(:hostclass, "cowabunga") + + class_type = Puppet::Pops::Types::TypeFactory.host_class("cowabunga") + expect(func.call(@scope, class_type)).to be_true + 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 "is false if referencing nil" 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 + expect(func.call(@scope, 'main')).to be_true + end + +end