diff --git a/lib/puppet/parser/functions/defined_with_params.rb b/lib/puppet/parser/functions/defined_with_params.rb index 4277584..daac567 100644 --- a/lib/puppet/parser/functions/defined_with_params.rb +++ b/lib/puppet/parser/functions/defined_with_params.rb @@ -1,72 +1,79 @@ # Test whether a given class or definition is defined require 'puppet/parser/functions' Puppet::Parser::Functions.newfunction(:defined_with_params, :type => :rvalue, :doc => <<-DOC @summary Takes a resource reference and an optional hash of attributes. Returns `true` if a resource with the specified attributes has already been added to the catalog, and `false` otherwise. ``` user { 'dan': ensure => present, } if ! defined_with_params(User[dan], {'ensure' => 'present' }) { user { 'dan': ensure => present, } } ``` @return [Boolean] returns `true` or `false` DOC ) do |vals| reference, params = vals raise(ArgumentError, 'Must specify a reference') unless reference if !params || params == '' params = {} end ret = false if Puppet::Util::Package.versioncmp(Puppet.version, '4.6.0') >= 0 # Workaround for PE-20308 if reference.is_a?(String) type_name, title = Puppet::Resource.type_and_title(reference, nil) type = Puppet::Pops::Evaluator::Runtime3ResourceSupport.find_resource_type_or_class(find_global_scope, type_name.downcase) elsif reference.is_a?(Puppet::Resource) type = reference.type title = reference.title else raise(ArgumentError, "Reference is not understood: '#{reference.class}'") end # end workaround else type = reference.to_s title = nil end resources = if title.empty? catalog.resources.select { |r| r.type == type } else [findresource(type, title)] end - resources.compact.each do |resource| + resources.compact.each do |res| + # If you call this from within a defined type, it will find itself + next if res.to_s == resource.to_s + matches = params.map do |key, value| # eql? avoids bugs caused by monkeypatching in puppet - resource_is_undef = resource[key].eql?(:undef) || resource[key].nil? + res_is_undef = res[key].eql?(:undef) || res[key].nil? value_is_undef = value.eql?(:undef) || value.nil? - (resource_is_undef && value_is_undef) || (resource[key] == value) + found_match = (res_is_undef && value_is_undef) || (res[key] == value) + + Puppet.debug("Matching resource is #{res}") if found_match + + found_match end ret = params.empty? || !matches.include?(false) break if ret end Puppet.debug("Resource #{reference} was not determined to be defined") unless ret ret end diff --git a/spec/functions/defined_with_params_spec.rb b/spec/functions/defined_with_params_spec.rb index cc191a4..36a627d 100644 --- a/spec/functions/defined_with_params_spec.rb +++ b/spec/functions/defined_with_params_spec.rb @@ -1,109 +1,129 @@ require 'spec_helper' describe 'defined_with_params' do describe 'when no resource is specified' do it { is_expected.to run.with_params.and_raise_error(ArgumentError) } end describe 'when compared against a resource with no attributes' do let :pre_condition do 'user { "dan": }' end it { is_expected.to run.with_params('User[dan]', {}).and_return(true) } it { is_expected.to run.with_params('User[bob]', {}).and_return(false) } it { is_expected.to run.with_params('User[dan]', 'foo' => 'bar').and_return(false) } context 'with UTF8 and double byte characters' do it { is_expected.to run.with_params('User[ĵĭмოү]', {}).and_return(false) } it { is_expected.to run.with_params('User[ポーラ]', {}).and_return(false) } end end describe 'when compared against a resource with attributes' do let :pre_condition do 'user { "dan": ensure => present, shell => "/bin/csh", managehome => false}' end it { is_expected.to run.with_params('User[dan]', {}).and_return(true) } it { is_expected.to run.with_params('User[dan]', '').and_return(true) } it { is_expected.to run.with_params('User[dan]', 'ensure' => 'present').and_return(true) } it { is_expected.to run.with_params('User[dan]', 'ensure' => 'present', 'managehome' => false).and_return(true) } it { is_expected.to run.with_params('User[dan]', 'ensure' => 'absent', 'managehome' => false).and_return(false) } end describe 'when passing undef values' do let :pre_condition do 'file { "/tmp/a": ensure => present }' end let(:is_puppet_6_or_greater) { Puppet::Util::Package.versioncmp(Puppet.version, '6.0.0') >= 0 } let(:undef_value) { is_puppet_6_or_greater ? nil : :undef } # even if :undef would work on 6.0.1, :undef should not be used it { is_expected.to run.with_params('File[/tmp/a]', {}).and_return(true) } it { is_expected.to run.with_params('File[/tmp/a]', 'ensure' => 'present', 'owner' => undef_value).and_return(true) } end describe 'when the reference is a' do let :pre_condition do 'user { "dan": }' end context 'with reference' do it { is_expected.to run.with_params(Puppet::Resource.new('User[dan]'), {}).and_return(true) } end if Puppet::Util::Package.versioncmp(Puppet.version, '4.6.0') >= 0 context 'with array' do it 'fails' do expect { subject.execute(['User[dan]'], {}) }.to raise_error(ArgumentError, %r{not understood: 'Array'}) end end end end describe 'when passed a defined type' do let :pre_condition do <<-PRECOND define test::deftype( Optional $port = undef ) { } test::deftype { "foo": } test::deftype { "baz": port => 100 } test::deftype { "adv": port => 200 } test::deftype { "adv2": port => 200 } # Unsure how to stub this out below properly if defined_with_params(Test::Deftype, { 'port' => 200 }) { notify { 'Duplicate found somewhere': } } if defined_with_params(Test::Deftype, { 'port' => 'nope' }) { notify { 'Should not find me': } } PRECOND end it { is_expected.to run.with_params('Test::Deftype[foo]', {}).and_return(true) } it { is_expected.to run.with_params('Test::Deftype[bar]', {}).and_return(false) } it { is_expected.to run.with_params(Puppet::Resource.new('Test::Deftype[foo]'), {}).and_return(true) } it { is_expected.to run.with_params(Puppet::Resource.new('Test::Deftype[bar]'), {}).and_return(false) expect(catalogue.resource('Notify[Duplicate found somewhere]')).not_to be_nil expect(catalogue.resource('Notify[Should not find me]')).to be_nil } end + describe 'when called from within a defined type looking for a defined type of the same type' do + let :pre_condition do + <<-PRECOND + define test::deftype( + Optional $port = undef + ) { + if defined_with_params(Test::Deftype, { 'port' => $port }) { + fail('Ruh Roh Shaggy') + } + } + + test::deftype { 'foo': } + test::deftype { 'bar': port => 200 } + PRECOND + end + + # Testing to make sure that the internal logic handles this case via the pre_condition + it { is_expected.to run.with_params('NoOp[noop]', {}).and_return(false) } + end + describe 'when passed a class' do let :pre_condition do 'class test () { } class { "test": }' end it { is_expected.to run.with_params('Class[test]', {}).and_return(true) } it { is_expected.to run.with_params('Class["bar"]', {}).and_return(false) } it { is_expected.to run.with_params('Class[bar]', {}).and_return(false) } it { is_expected.to run.with_params(Puppet::Resource.new('class', 'test'), {}).and_return(true) } it { is_expected.to run.with_params(Puppet::Resource.new('Class["bar"]'), {}).and_return(false) } it { is_expected.to run.with_params(Puppet::Resource.new('Class[bar]'), {}).and_return(false) } end end