Skip to content

Commit 7f24c8c

Browse files
seanmilDavidS
authored andcommitted
Add title consistency checks for multi-namevar providers
This ensures results returned from a multi-namevar provider meet the following consistency checks: * There must be a title attribute returned * The title value must match one of the title patterns * The values parsed from the title pattern match must match the values returned from the provider for the respective namevars.
1 parent 7a139b1 commit 7f24c8c

File tree

3 files changed

+180
-8
lines changed

3 files changed

+180
-8
lines changed

lib/puppet/resource_api.rb

Lines changed: 52 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -261,7 +261,7 @@ def refresh_current_state
261261

262262
if @rsapi_current_state
263263
type_definition.check_schema(@rsapi_current_state)
264-
strict_check(@rsapi_current_state) if type_definition.feature?('canonicalize')
264+
strict_check(@rsapi_current_state)
265265
else
266266
@rsapi_current_state = if rsapi_title.is_a? Hash
267267
rsapi_title.dup
@@ -275,7 +275,7 @@ def refresh_current_state
275275
# Use this to set the current state from the `instances` method
276276
def cache_current_state(resource_hash)
277277
@rsapi_current_state = resource_hash
278-
strict_check(@rsapi_current_state) if type_definition.feature?('canonicalize')
278+
strict_check(@rsapi_current_state)
279279
end
280280

281281
def retrieve
@@ -354,6 +354,22 @@ def raise_missing_params
354354
def strict_check(current_state)
355355
return if Puppet.settings[:strict] == :off
356356

357+
strict_check_canonicalize(current_state) if type_definition.feature?('canonicalize')
358+
strict_check_title_parameter(current_state) if type_definition.namevars.size > 1
359+
360+
nil
361+
end
362+
363+
def strict_message(message)
364+
case Puppet.settings[:strict]
365+
when :warning
366+
Puppet.warning(message)
367+
when :error
368+
raise Puppet::DevError, message
369+
end
370+
end
371+
372+
def strict_check_canonicalize(current_state)
357373
# if strict checking is on we must notify if the values are changed by canonicalize
358374
# make a deep copy to perform the operation on and to compare against later
359375
state_clone = Marshal.load(Marshal.dump(current_state))
@@ -370,15 +386,43 @@ def strict_check(current_state)
370386
Canonicalized values: #{state_clone.inspect}
371387
MESSAGE
372388
#:nocov:
389+
strict_message(message)
390+
end
373391

374-
case Puppet.settings[:strict]
375-
when :warning
376-
Puppet.warning(message)
377-
when :error
378-
raise Puppet::DevError, message
392+
def strict_check_title_parameter(current_state)
393+
unless current_state.key?(:title)
394+
strict_message("#{type_definition.name}[#{@title}]#get has not provided a title attribute.")
395+
return
379396
end
380397

381-
nil
398+
# Logic borrowed from Puppet::Resource.parse_title
399+
title_hash = {}
400+
self.class.title_patterns.each do |regexp, symbols|
401+
captures = regexp.match(current_state[:title])
402+
next if captures.nil?
403+
symbols.zip(captures[1..-1]).each do |symbol_and_lambda, capture|
404+
# The Resource API does not support passing procs in title_patterns
405+
# so, unlike Puppet::Resource, we do not need to handle that here.
406+
symbol = symbol_and_lambda[0]
407+
title_hash[symbol] = capture
408+
end
409+
break
410+
end
411+
412+
return if title_hash == rsapi_title
413+
414+
namevars = type_definition.namevars.reject { |namevar| title_hash[namevar] == rsapi_title[namevar] }
415+
416+
#:nocov:
417+
# codecov fails to register this multiline as covered, even though simplecov does.
418+
message = <<MESSAGE.strip
419+
#{type_definition.name}[#{@title}]#get has provided a title attribute which does not match all namevars.
420+
Namevars which do not match: #{namevars.inspect}
421+
Returned parsed title hash: #{title_hash.inspect}
422+
Expected hash: #{rsapi_title.inspect}
423+
MESSAGE
424+
#:nocov:
425+
strict_message(message)
382426
end
383427

384428
define_singleton_method(:context) do

spec/fixtures/test_module/lib/puppet/type/multiple_namevar.rb

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,16 @@
55
docs: <<-EOS,
66
This type provides Puppet with the capabilities to manage ...
77
EOS
8+
title_patterns: [
9+
{
10+
pattern: %r{^(?<package>.*[^-])-(?<manager>.*)$},
11+
desc: 'Package and manager with a hyphen seperator',
12+
},
13+
{
14+
pattern: %r{^(?<package>.*)$},
15+
desc: 'Package',
16+
},
17+
],
818
attributes: {
919
ensure: {
1020
type: 'Enum[present, absent]',

spec/puppet/resource_api_spec.rb

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1026,6 +1026,7 @@ def set(_context, changes) end
10261026
let(:definition) do
10271027
{
10281028
name: 'composite',
1029+
desc: 'some desc',
10291030
title_patterns: [
10301031
{
10311032
pattern: %r{^(?<package>.*[^/])/(?<manager>.*)$},
@@ -1039,13 +1040,16 @@ def set(_context, changes) end
10391040
attributes: {
10401041
ensure: {
10411042
type: 'Enum[present, absent]',
1043+
desc: '',
10421044
},
10431045
package: {
10441046
type: 'String',
1047+
desc: '',
10451048
behavior: :namevar,
10461049
},
10471050
manager: {
10481051
type: 'String',
1052+
desc: '',
10491053
behavior: :namevar,
10501054
},
10511055
},
@@ -1123,6 +1127,120 @@ def set(_context, _changes); end
11231127
type_class.new(title: 'php/yum', ensure: :absent).flush
11241128
end
11251129
end
1130+
1131+
context 'when no title is returned by get' do
1132+
let(:provider_class) do
1133+
Class.new do
1134+
def get(_context)
1135+
[{ package: 'php', manager: 'yum', ensure: 'present' }]
1136+
end
1137+
1138+
def set(_context, _changes); end
1139+
end
1140+
end
1141+
let(:type) { type_class.new(title: 'mytitle', package: 'php', manager: 'yum') }
1142+
1143+
context 'when Puppet strict setting is :off' do
1144+
let(:strict_level) { :off }
1145+
1146+
it 'instances will not log a warning' do
1147+
expect(Puppet).not_to receive(:warning)
1148+
type_class.instances
1149+
end
1150+
1151+
it 'refresh_current_state will not log a warning' do
1152+
expect(Puppet).not_to receive(:warning)
1153+
type.refresh_current_state
1154+
end
1155+
end
1156+
1157+
context 'when Puppet strict setting is :error' do
1158+
let(:strict_level) { :error }
1159+
1160+
it 'instances will throw an exception' do
1161+
expect {
1162+
type_class.instances
1163+
}.to raise_error(Puppet::DevError, %r{has not provided a title attribute})
1164+
end
1165+
1166+
it 'refresh_current_state will throw an exception' do
1167+
expect {
1168+
type.refresh_current_state
1169+
}.to raise_error(Puppet::DevError, %r{has not provided a title attribute})
1170+
end
1171+
end
1172+
1173+
context 'when Puppet strict setting is :warning' do
1174+
let(:strict_level) { :warning }
1175+
1176+
it 'instances will not log a warning' do
1177+
expect(Puppet).to receive(:warning).with(%r{has not provided a title attribute})
1178+
type_class.instances
1179+
end
1180+
1181+
it 'refresh_current_state will not log a warning' do
1182+
expect(Puppet).to receive(:warning).with(%r{has not provided a title attribute})
1183+
type.refresh_current_state
1184+
end
1185+
end
1186+
end
1187+
1188+
context 'when the title does not match a title pattern' do
1189+
let(:provider_class) do
1190+
Class.new do
1191+
def get(_context)
1192+
[{ title: 'Nomatch', package: 'php', manager: 'yum', ensure: 'present' }]
1193+
end
1194+
1195+
def set(_context, _changes); end
1196+
end
1197+
end
1198+
let(:type) { type_class.new(title: 'mytitle', package: 'php', manager: 'yum') }
1199+
1200+
context 'when Puppet strict setting is :off' do
1201+
let(:strict_level) { :off }
1202+
1203+
it 'instances will not log a warning' do
1204+
expect(Puppet).not_to receive(:warning)
1205+
type_class.instances
1206+
end
1207+
1208+
it 'refresh_current_state will not log a warning' do
1209+
expect(Puppet).not_to receive(:warning)
1210+
type.refresh_current_state
1211+
end
1212+
end
1213+
1214+
context 'when Puppet strict setting is :error' do
1215+
let(:strict_level) { :error }
1216+
1217+
it 'instances will throw an exception' do
1218+
expect {
1219+
type_class.instances
1220+
}.to raise_error(Puppet::DevError, %r{has provided a title attribute which does not match})
1221+
end
1222+
1223+
it 'refresh_current_state will throw an exception' do
1224+
expect {
1225+
type.refresh_current_state
1226+
}.to raise_error(Puppet::DevError, %r{has provided a title attribute which does not match})
1227+
end
1228+
end
1229+
1230+
context 'when Puppet strict setting is :warning' do
1231+
let(:strict_level) { :warning }
1232+
1233+
it 'instances will not log a warning' do
1234+
expect(Puppet).to receive(:warning).with(%r{has provided a title attribute which does not match})
1235+
type_class.instances
1236+
end
1237+
1238+
it 'refresh_current_state will not log a warning' do
1239+
expect(Puppet).to receive(:warning).with(%r{has provided a title attribute which does not match})
1240+
type.refresh_current_state
1241+
end
1242+
end
1243+
end
11261244
end
11271245
end
11281246

0 commit comments

Comments
 (0)