ruby-pwsh 2.0.0 → 2.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/lib/puppet/provider/dsc_base_provider/dsc_base_provider.rb +321 -35
- data/lib/pwsh/version.rb +1 -1
- data/spec/acceptance/dsc/class.rb +1 -1
- data/spec/acceptance/dsc/complex.rb +4 -4
- data/spec/unit/puppet/provider/dsc_base_provider/dsc_base_provider_spec.rb +335 -18
- metadata +3 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: d87a118d1771f309420051ae1204ceef19fd07b749bfe6ae00f09cd5f4cb0dea
|
|
4
|
+
data.tar.gz: defedaf6b72f61b033cdf576b83925f2796f22ef16e1f32269d8f20b0ab35f9a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: c90e9942e0bdd31c33e1cb965795625726adb919d98b599b5008840ddf91291129b3f2a740dc26baff6495b3fecc7cc5330c7136e868b25c572662c50823d3db
|
|
7
|
+
data.tar.gz: 2f130b671873a490c2e811994fb635a47a37882190e1cd4a98e934fde5c08a1577d31f5b21eee7f0866285f6f85e0fdc5599bce18b9502c9dd9d3b04f3f36031
|
|
@@ -12,8 +12,11 @@ class Puppet::Provider::DscBaseProvider # rubocop:disable Metrics/ClassLength
|
|
|
12
12
|
# - logon failures
|
|
13
13
|
def initialize
|
|
14
14
|
@cached_canonicalized_resource = []
|
|
15
|
-
@
|
|
15
|
+
@cached_canonicalize_results = [] # Cache for invoke_get_method calls from canonicalize only
|
|
16
|
+
@cached_query_results = [] # Cache for invoke_get_method calls from get only
|
|
16
17
|
@cached_test_results = []
|
|
18
|
+
@cached_fresh_get_results = {}
|
|
19
|
+
@insync_property_cache = {}
|
|
17
20
|
@logon_failures = []
|
|
18
21
|
@timeout = nil # default timeout, ps_manager.execute is expecting nil by default..
|
|
19
22
|
super
|
|
@@ -61,7 +64,11 @@ class Puppet::Provider::DscBaseProvider # rubocop:disable Metrics/ClassLength
|
|
|
61
64
|
canonicalized = r.dup
|
|
62
65
|
@cached_canonicalized_resource << r.dup
|
|
63
66
|
else
|
|
64
|
-
|
|
67
|
+
# Use a separate cache for canonicalize's Get calls so we don't pollute
|
|
68
|
+
# @cached_query_results, which get() uses for the "is" state comparison.
|
|
69
|
+
# Sharing a cache between canonicalize and get caused the Resource API to
|
|
70
|
+
# see identical "should" and "is" values, killing per-property report detail.
|
|
71
|
+
canonicalized = invoke_get_method_for_canonicalize(context, r)
|
|
65
72
|
# If the resource could not be found or was returned as absent, skip case munging and
|
|
66
73
|
# treat the manifest values as canonical since the resource is being created.
|
|
67
74
|
# rubocop:disable Metrics/BlockNesting
|
|
@@ -136,6 +143,21 @@ class Puppet::Provider::DscBaseProvider # rubocop:disable Metrics/ClassLength
|
|
|
136
143
|
cached_results = fetch_cached_hashes(@cached_query_results, names)
|
|
137
144
|
return cached_results unless cached_results.empty?
|
|
138
145
|
|
|
146
|
+
# Use the raw system state from canonicalize's Get call if available.
|
|
147
|
+
# @cached_canonicalize_results has the unmodified DSC Get response (before
|
|
148
|
+
# canonicalize normalized casing). This gives the Resource API real "is" data
|
|
149
|
+
# with all property values, enabling per-property "Changed from" reporting.
|
|
150
|
+
unless @cached_canonicalize_results.empty?
|
|
151
|
+
canon_results = fetch_cached_hashes(@cached_canonicalize_results, names)
|
|
152
|
+
unless canon_results.empty?
|
|
153
|
+
# Cache these as query results too so subsequent get() calls find them
|
|
154
|
+
canon_results.each do |r|
|
|
155
|
+
@cached_query_results << r.dup if fetch_cached_hashes(@cached_query_results, [r]).empty?
|
|
156
|
+
end
|
|
157
|
+
return canon_results
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
|
|
139
161
|
if @cached_canonicalized_resource.empty?
|
|
140
162
|
mandatory_properties = {}
|
|
141
163
|
else
|
|
@@ -166,6 +188,12 @@ class Puppet::Provider::DscBaseProvider # rubocop:disable Metrics/ClassLength
|
|
|
166
188
|
# If should is an array instead of a hash and only has one entry, use that.
|
|
167
189
|
should = should.first if should.is_a?(Array) && should.length == 1
|
|
168
190
|
|
|
191
|
+
# DSC_WORKAROUND logging disabled — the insync? tuple returns now provide
|
|
192
|
+
# accurate per-property change detail in PE Events, making this redundant.
|
|
193
|
+
# Retained commented out for future debugging if needed.
|
|
194
|
+
# fresh_is = get_fresh_state_for_logging(context, name, should)
|
|
195
|
+
# log_change_detail(context, name, fresh_is || is, should)
|
|
196
|
+
|
|
169
197
|
# for compatibility sake, we use dsc_ensure instead of ensure, so context.type.ensurable? does not work
|
|
170
198
|
if context.type.attributes.key?(:dsc_ensure)
|
|
171
199
|
# HACK: If the DSC Resource is ensurable but doesn't report a default value
|
|
@@ -201,6 +229,120 @@ class Puppet::Provider::DscBaseProvider # rubocop:disable Metrics/ClassLength
|
|
|
201
229
|
end
|
|
202
230
|
end
|
|
203
231
|
|
|
232
|
+
# Compares the is and should hashes and logs per-property change detail.
|
|
233
|
+
# The Resource API does not generate per-property change events when custom_insync
|
|
234
|
+
# is declared as a feature, so we emit them ourselves to populate report detail.
|
|
235
|
+
#
|
|
236
|
+
# @param context [Object] the Puppet runtime context to operate in and send feedback to
|
|
237
|
+
# @param name [String] the name of the resource being changed
|
|
238
|
+
# @param is [Hash] the current state of the resource
|
|
239
|
+
# @param should [Hash] the desired state of the resource
|
|
240
|
+
def log_change_detail(context, name, is_value, should)
|
|
241
|
+
return if is_value.nil? || should.nil?
|
|
242
|
+
|
|
243
|
+
# Build context info for the log line
|
|
244
|
+
type_name = begin
|
|
245
|
+
context.type.definition[:name]
|
|
246
|
+
rescue StandardError
|
|
247
|
+
nil
|
|
248
|
+
end
|
|
249
|
+
dsc_module = begin
|
|
250
|
+
context.type.definition[:dscmeta_module_name]
|
|
251
|
+
rescue StandardError
|
|
252
|
+
nil
|
|
253
|
+
end
|
|
254
|
+
resource_title = name.is_a?(Hash) ? name[:name] : name
|
|
255
|
+
# Try to extract the declaring Puppet class from tags if available
|
|
256
|
+
tags = should[:tag] || should[:tags]
|
|
257
|
+
declaring_class = if tags.is_a?(Array)
|
|
258
|
+
# Tags include type name, title, and all containing classes.
|
|
259
|
+
# Class tags contain '::' — pick the most specific one.
|
|
260
|
+
tags.reverse.find { |t| t.include?('::') }
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
should.each do |property, desired_value|
|
|
264
|
+
next unless property.to_s.start_with?('dsc_')
|
|
265
|
+
next if property == :dsc_psdscrunascredential
|
|
266
|
+
# Skip namevars — they're identifiers, not managed properties
|
|
267
|
+
next if namevar_attributes(context).include?(property)
|
|
268
|
+
|
|
269
|
+
current_value = is_value[property]
|
|
270
|
+
# Skip if values are the same (case-insensitive for strings)
|
|
271
|
+
next if same?(recursively_downcase(current_value), recursively_downcase(desired_value))
|
|
272
|
+
|
|
273
|
+
mof_type = begin
|
|
274
|
+
context.type.definition[:attributes][property][:mof_type]
|
|
275
|
+
rescue StandardError
|
|
276
|
+
nil
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
detail = "DSC_WORKAROUND: #{property} '#{current_value}' -> '#{desired_value}'"
|
|
280
|
+
detail += " (#{mof_type})" if mof_type
|
|
281
|
+
detail += " | resource: #{type_name}[#{resource_title}]" if type_name
|
|
282
|
+
detail += " | dsc_module: #{dsc_module}" if dsc_module
|
|
283
|
+
detail += " | class: #{declaring_class}" if declaring_class
|
|
284
|
+
context.notice(detail)
|
|
285
|
+
end
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
# Performs a fresh DSC Get call completely bypassing all caches to retrieve the actual
|
|
289
|
+
# current system state. Used by both insync? (for accurate property comparison and
|
|
290
|
+
# change_message tuples) and set() (for DSC_WORKAROUND log entries).
|
|
291
|
+
#
|
|
292
|
+
# @param context [Object] the Puppet runtime context to operate in and send feedback to
|
|
293
|
+
# @param name [Hash] the name hash for the resource
|
|
294
|
+
# @param should [Hash] the desired state hash (used to extract query properties)
|
|
295
|
+
# @return [Hash] returns a hash with dsc_ prefixed keys and current system values, or nil on failure
|
|
296
|
+
def perform_fresh_get(context, name, should)
|
|
297
|
+
query_props = should.select { |k, v| mandatory_get_attributes(context).include?(k) || (k == :dsc_psdscrunascredential && !v.nil?) }
|
|
298
|
+
data = invoke_dsc_resource(context, name, query_props, 'get')
|
|
299
|
+
return nil if data.nil?
|
|
300
|
+
|
|
301
|
+
# Minimal key processing to match the dsc_ prefix format used by Puppet types
|
|
302
|
+
valid_attributes = context.type.attributes.keys.collect(&:to_s)
|
|
303
|
+
result = {}
|
|
304
|
+
data.each do |key, value|
|
|
305
|
+
type_key = :"dsc_#{key.downcase}"
|
|
306
|
+
next unless valid_attributes.include?(type_key.to_s)
|
|
307
|
+
|
|
308
|
+
# Sanitize .NET serialization artifacts (e.g., "System.Object[]")
|
|
309
|
+
if value.is_a?(String) && value =~ /^System\.\w+\[\]$/
|
|
310
|
+
result[type_key] = nil
|
|
311
|
+
else
|
|
312
|
+
# Normalize CIM instances the same way invoke_get_method does,
|
|
313
|
+
# so fresh values are comparable to canonicalized should values.
|
|
314
|
+
if value.is_a?(Enumerable)
|
|
315
|
+
downcase_hash_keys!(value)
|
|
316
|
+
munge_cim_instances!(value)
|
|
317
|
+
end
|
|
318
|
+
result[type_key] = value
|
|
319
|
+
end
|
|
320
|
+
end
|
|
321
|
+
result[:name] = name.is_a?(Hash) ? name[:name] : name
|
|
322
|
+
result
|
|
323
|
+
rescue StandardError => e
|
|
324
|
+
context.debug("perform_fresh_get failed: #{e.message}")
|
|
325
|
+
nil
|
|
326
|
+
end
|
|
327
|
+
|
|
328
|
+
# Caching wrapper around perform_fresh_get — one fresh DSC Get per resource,
|
|
329
|
+
# reused across all properties during that resource's insync? calls.
|
|
330
|
+
#
|
|
331
|
+
# @param context [Object] the Puppet runtime context to operate in and send feedback to
|
|
332
|
+
# @param name [Hash] the name hash for the resource
|
|
333
|
+
# @param should [Hash] the desired state hash (used to extract query properties)
|
|
334
|
+
# @return [Hash] returns a hash with dsc_ prefixed keys and current system values, or nil on failure
|
|
335
|
+
def get_cached_fresh_state(context, name, should)
|
|
336
|
+
cache_key = name.is_a?(Hash) ? name[:name] : name
|
|
337
|
+
context.debug("INSYNC_DIAG: get_cached_fresh_state called, cache_key=#{cache_key.inspect}, name class=#{name.class}, cached=#{@cached_fresh_get_results.key?(cache_key)}")
|
|
338
|
+
unless @cached_fresh_get_results.key?(cache_key)
|
|
339
|
+
result = perform_fresh_get(context, name, should)
|
|
340
|
+
context.debug("INSYNC_DIAG: perform_fresh_get returned #{result.nil? ? 'nil' : result.keys.inspect}")
|
|
341
|
+
@cached_fresh_get_results[cache_key] = result
|
|
342
|
+
end
|
|
343
|
+
@cached_fresh_get_results[cache_key]
|
|
344
|
+
end
|
|
345
|
+
|
|
204
346
|
# Attempts to set an instance of the DSC resource, invoking the `Set` method and thinly wrapping
|
|
205
347
|
# the `invoke_set_method` method; whether this method, `update`, or `delete` is called is entirely
|
|
206
348
|
# up to the Resource API based on the results
|
|
@@ -341,24 +483,115 @@ class Puppet::Provider::DscBaseProvider # rubocop:disable Metrics/ClassLength
|
|
|
341
483
|
data
|
|
342
484
|
end
|
|
343
485
|
|
|
344
|
-
#
|
|
345
|
-
#
|
|
346
|
-
#
|
|
347
|
-
#
|
|
486
|
+
# Compare two values with type coercion. Handles the mismatches common in DSC:
|
|
487
|
+
# integers vs strings, case differences, array ordering, and nested CIM instances.
|
|
488
|
+
# Uses the same recursively_sort/recursively_downcase helpers the provider already
|
|
489
|
+
# relies on for canonicalize and log_change_detail comparisons.
|
|
490
|
+
#
|
|
491
|
+
# @param is_value [Object] the current value from a fresh DSC Get
|
|
492
|
+
# @param should_value [Object] the desired value from the Puppet manifest
|
|
493
|
+
# @return [Boolean] true if the values are equivalent
|
|
494
|
+
def values_equal?(is_value, should_value)
|
|
495
|
+
return true if is_value == should_value
|
|
496
|
+
|
|
497
|
+
# Handle nil/empty equivalence
|
|
498
|
+
is_empty = is_value.nil? || (is_value.respond_to?(:empty?) && is_value.empty?)
|
|
499
|
+
should_empty = should_value.nil? || (should_value.respond_to?(:empty?) && should_value.empty?)
|
|
500
|
+
return true if is_empty && should_empty
|
|
501
|
+
|
|
502
|
+
# Normalize and compare: sort for order-insensitive array/hash comparison,
|
|
503
|
+
# downcase for case-insensitive string comparison, handles nested structures.
|
|
504
|
+
same?(recursively_downcase(is_value), recursively_downcase(should_value))
|
|
505
|
+
end
|
|
506
|
+
|
|
507
|
+
# Determine if the DSC Resource is in the desired state, using fresh DSC Get
|
|
508
|
+
# results to provide accurate property comparison and real change messages.
|
|
509
|
+
#
|
|
510
|
+
# For validation_mode: resource, delegates entirely to DSC Test (unchanged).
|
|
511
|
+
#
|
|
512
|
+
# For validation_mode: property (default), performs a fresh DSC Get (one per
|
|
513
|
+
# resource, cached) that bypasses the get()/canonicalize pipeline, then compares
|
|
514
|
+
# each dsc_ property against the desired value. Returns:
|
|
515
|
+
# - true if the property matches (suppresses false positive events)
|
|
516
|
+
# - [false, "'current' -> 'desired'"] if the property genuinely differs
|
|
517
|
+
# (the RSAPI uses the change_message as the Event text in PE)
|
|
518
|
+
# - nil to fall through to default RSAPI comparison (non-dsc_ properties,
|
|
519
|
+
# or if the fresh Get failed)
|
|
348
520
|
#
|
|
349
521
|
# @param context [Object] the Puppet runtime context to operate in and send feedback to
|
|
350
522
|
# @param name [String] the name of the resource being tested
|
|
351
|
-
# @param
|
|
523
|
+
# @param property_name [Symbol] the name of the property being compared
|
|
524
|
+
# @param _is_hash [Hash] the current state of the resource on the system (unused, required by RSAPI signature)
|
|
352
525
|
# @param should_hash [Hash] the desired state of the resource per the manifest
|
|
353
|
-
# @return [Boolean, Void] returns true/false if the resource
|
|
354
|
-
# the
|
|
355
|
-
def insync?(context, name,
|
|
356
|
-
|
|
526
|
+
# @return [Boolean, Array, Void] returns true/false/[false, message] if the resource
|
|
527
|
+
# is/isn't in the desired state, or nil to fall through to default property comparison.
|
|
528
|
+
def insync?(context, name, property_name, _is_hash, should_hash)
|
|
529
|
+
# Detect corrective change check: after insync? returns [false, msg], Puppet calls
|
|
530
|
+
# insync? again for the same property. Return nil so default comparison handles it.
|
|
531
|
+
cache_key = name.is_a?(Hash) ? name[:name] : name
|
|
532
|
+
property_key = "#{cache_key}_#{property_name}"
|
|
533
|
+
return nil if @insync_property_cache.delete(property_key)
|
|
534
|
+
|
|
535
|
+
if should_hash[:validation_mode] == 'resource'
|
|
536
|
+
insync_resource_mode(context, name, property_name, should_hash)
|
|
537
|
+
else
|
|
538
|
+
insync_property_mode(context, name, property_name, should_hash)
|
|
539
|
+
end
|
|
540
|
+
end
|
|
357
541
|
|
|
542
|
+
private
|
|
543
|
+
|
|
544
|
+
# Resource validation mode: DSC Test is the authority on overall sync state.
|
|
545
|
+
def insync_resource_mode(context, name, property_name, should_hash)
|
|
546
|
+
should_value = should_hash.is_a?(Hash) ? should_hash[property_name] : nil
|
|
358
547
|
prior_result = fetch_cached_hashes(@cached_test_results, [name])
|
|
359
|
-
prior_result.empty? ? invoke_test_method(context, name, should_hash) : prior_result.first[:in_desired_state]
|
|
548
|
+
test_result = prior_result.empty? ? invoke_test_method(context, name, should_hash) : prior_result.first[:in_desired_state]
|
|
549
|
+
in_sync = test_result.is_a?(Array) ? test_result.first : test_result
|
|
550
|
+
|
|
551
|
+
return true if in_sync
|
|
552
|
+
|
|
553
|
+
# DSC Test says out of sync. Suppress non-dsc_ and nil/empty properties.
|
|
554
|
+
return true unless property_name.to_s.start_with?('dsc_')
|
|
555
|
+
return true if should_value.nil? || (should_value.respond_to?(:empty?) && should_value.empty?)
|
|
556
|
+
|
|
557
|
+
# Fresh Get comparison for dsc_ properties with values
|
|
558
|
+
compare_fresh_value(context, name, property_name, should_hash, report_on_failure: true)
|
|
559
|
+
end
|
|
560
|
+
|
|
561
|
+
# Property validation mode: only intervene for dsc_ properties with a desired value.
|
|
562
|
+
def insync_property_mode(context, name, property_name, should_hash)
|
|
563
|
+
return nil unless property_name.to_s.start_with?('dsc_')
|
|
564
|
+
|
|
565
|
+
should_value = should_hash.is_a?(Hash) ? should_hash[property_name] : nil
|
|
566
|
+
return nil if should_value.nil? || (should_value.respond_to?(:empty?) && should_value.empty?)
|
|
567
|
+
|
|
568
|
+
compare_fresh_value(context, name, property_name, should_hash, report_on_failure: false)
|
|
360
569
|
end
|
|
361
570
|
|
|
571
|
+
# Shared fresh Get comparison for both validation modes.
|
|
572
|
+
def compare_fresh_value(context, name, property_name, should_hash, report_on_failure:)
|
|
573
|
+
should_value = should_hash.is_a?(Hash) ? should_hash[property_name] : nil
|
|
574
|
+
property_key = "#{name.is_a?(Hash) ? name[:name] : name}_#{property_name}"
|
|
575
|
+
fresh_state = get_cached_fresh_state(context, name, should_hash)
|
|
576
|
+
|
|
577
|
+
if fresh_state.nil?
|
|
578
|
+
if report_on_failure
|
|
579
|
+
@insync_property_cache[property_key] = true
|
|
580
|
+
return [false, "#{property_name} changed '' to '#{should_value}'"]
|
|
581
|
+
end
|
|
582
|
+
|
|
583
|
+
return nil
|
|
584
|
+
end
|
|
585
|
+
|
|
586
|
+
fresh_value = fresh_state[property_name]
|
|
587
|
+
return true if values_equal?(fresh_value, should_value)
|
|
588
|
+
|
|
589
|
+
@insync_property_cache[property_key] = true
|
|
590
|
+
[false, "#{property_name} changed '#{fresh_value}' to '#{should_value}'"]
|
|
591
|
+
end
|
|
592
|
+
|
|
593
|
+
public
|
|
594
|
+
|
|
362
595
|
# Invokes the `Get` method, passing the name_hash as the properties to use with `Invoke-DscResource`
|
|
363
596
|
# The PowerShell script returns a JSON representation of the DSC Resource's CIM Instance munged as
|
|
364
597
|
# best it can be for Ruby. Once that JSON is parsed into a hash this method further munges it to
|
|
@@ -368,7 +601,7 @@ class Puppet::Provider::DscBaseProvider # rubocop:disable Metrics/ClassLength
|
|
|
368
601
|
# @param context [Object] the Puppet runtime context to operate in and send feedback to
|
|
369
602
|
# @param name_hash [Hash] the hash of namevars to be passed as properties to `Invoke-DscResource`
|
|
370
603
|
# @return [Hash] returns a hash representing the DSC resource munged to the representation the Puppet Type expects
|
|
371
|
-
def invoke_get_method(context, name_hash)
|
|
604
|
+
def invoke_get_method(context, name_hash)
|
|
372
605
|
context.debug("retrieving #{name_hash.inspect}")
|
|
373
606
|
|
|
374
607
|
query_props = name_hash.select { |k, v| mandatory_get_attributes(context).include?(k) || (k == :dsc_psdscrunascredential && !v.nil?) }
|
|
@@ -387,29 +620,10 @@ class Puppet::Provider::DscBaseProvider # rubocop:disable Metrics/ClassLength
|
|
|
387
620
|
data.keys.each do |key|
|
|
388
621
|
type_key = :"dsc_#{key.downcase}"
|
|
389
622
|
data[type_key] = data.delete(key)
|
|
390
|
-
|
|
391
|
-
# Special handling for CIM Instances
|
|
392
|
-
if data[type_key].is_a?(Enumerable)
|
|
393
|
-
downcase_hash_keys!(data[type_key])
|
|
394
|
-
munge_cim_instances!(data[type_key])
|
|
395
|
-
end
|
|
396
|
-
|
|
397
|
-
# Convert DateTime back to appropriate type
|
|
398
|
-
if context.type.attributes[type_key][:mof_type] =~ /DateTime/i && !data[type_key].nil?
|
|
399
|
-
data[type_key] = begin
|
|
400
|
-
Puppet::Pops::Time::Timestamp.parse(data[type_key]) if context.type.attributes[type_key][:mof_type] =~ /DateTime/i && !data[type_key].nil?
|
|
401
|
-
rescue ArgumentError, TypeError => e
|
|
402
|
-
# Catch any failures in the parse, output them to the context and then return nil
|
|
403
|
-
context.err("Value returned for DateTime (#{data[type_key].inspect}) failed to parse: #{e}")
|
|
404
|
-
nil
|
|
405
|
-
end
|
|
406
|
-
end
|
|
407
|
-
# PowerShell does not distinguish between a return of empty array/string
|
|
408
|
-
# and null but Puppet does; revert to those values if specified.
|
|
409
|
-
data[type_key] = [] if data[type_key].nil? && query_props.key?(type_key) && query_props[type_key].is_a?(Array)
|
|
623
|
+
canonicalize_get_value!(context, data, type_key, query_props)
|
|
410
624
|
end
|
|
411
|
-
|
|
412
|
-
data
|
|
625
|
+
sanitize_dotnet_artifacts!(context, data)
|
|
626
|
+
preserve_namevar_values!(context, data, name_hash)
|
|
413
627
|
|
|
414
628
|
data = stringify_nil_attributes(context, data)
|
|
415
629
|
|
|
@@ -431,6 +645,78 @@ class Puppet::Provider::DscBaseProvider # rubocop:disable Metrics/ClassLength
|
|
|
431
645
|
data
|
|
432
646
|
end
|
|
433
647
|
|
|
648
|
+
# Invokes the `Get` method for canonicalize only, using a separate cache from get().
|
|
649
|
+
# This prevents canonicalize from polluting @cached_query_results, which get() relies on
|
|
650
|
+
# to return the "is" state for the Resource API's property-by-property comparison.
|
|
651
|
+
#
|
|
652
|
+
# @param context [Object] the Puppet runtime context to operate in and send feedback to
|
|
653
|
+
# @param name_hash [Hash] the hash of namevars to be passed as properties to `Invoke-DscResource`
|
|
654
|
+
# @return [Hash] returns a hash representing the DSC resource munged to the representation the Puppet Type expects
|
|
655
|
+
def invoke_get_method_for_canonicalize(context, name_hash)
|
|
656
|
+
# Check the canonicalize-specific cache first
|
|
657
|
+
cached = fetch_cached_hashes(@cached_canonicalize_results, [name_hash.select { |k, _v| namevar_attributes(context).include?(k) }])
|
|
658
|
+
return cached.first unless cached.empty?
|
|
659
|
+
|
|
660
|
+
# Call invoke_get_method which will do the actual DSC Get call.
|
|
661
|
+
# It will also cache to @cached_query_results as a side effect — remove that entry
|
|
662
|
+
# so get() is forced to do its own fresh lookup later.
|
|
663
|
+
result = invoke_get_method(context, name_hash)
|
|
664
|
+
|
|
665
|
+
# Remove the entry that invoke_get_method just added to @cached_query_results
|
|
666
|
+
# so that get() will do its own fresh DSC Get call and produce independent "is" data.
|
|
667
|
+
@cached_query_results.select! do |item|
|
|
668
|
+
matching = fetch_cached_hashes([item], [name_hash.select { |k, _v| namevar_attributes(context).include?(k) }])
|
|
669
|
+
matching.empty?
|
|
670
|
+
end
|
|
671
|
+
|
|
672
|
+
# Cache in our own separate cache
|
|
673
|
+
@cached_canonicalize_results << result.dup if result && fetch_cached_hashes(@cached_canonicalize_results, [result]).empty?
|
|
674
|
+
|
|
675
|
+
result
|
|
676
|
+
end
|
|
677
|
+
|
|
678
|
+
# Canonicalize a single value returned by DSC Get: handle CIM instances, DateTime, and empty arrays.
|
|
679
|
+
def canonicalize_get_value!(context, data, type_key, query_props)
|
|
680
|
+
# Special handling for CIM Instances
|
|
681
|
+
if data[type_key].is_a?(Enumerable)
|
|
682
|
+
downcase_hash_keys!(data[type_key])
|
|
683
|
+
munge_cim_instances!(data[type_key])
|
|
684
|
+
end
|
|
685
|
+
|
|
686
|
+
# Convert DateTime back to appropriate type
|
|
687
|
+
if context.type.attributes[type_key][:mof_type] =~ /DateTime/i && !data[type_key].nil?
|
|
688
|
+
data[type_key] = begin
|
|
689
|
+
Puppet::Pops::Time::Timestamp.parse(data[type_key])
|
|
690
|
+
rescue ArgumentError, TypeError => e
|
|
691
|
+
context.err("Value returned for DateTime (#{data[type_key].inspect}) failed to parse: #{e}")
|
|
692
|
+
nil
|
|
693
|
+
end
|
|
694
|
+
end
|
|
695
|
+
# PowerShell does not distinguish between a return of empty array/string
|
|
696
|
+
# and null but Puppet does; revert to those values if specified.
|
|
697
|
+
data[type_key] = [] if data[type_key].nil? && query_props.key?(type_key) && query_props[type_key].is_a?(Array)
|
|
698
|
+
end
|
|
699
|
+
|
|
700
|
+
# Sanitize .NET serialization artifacts (e.g., "System.Object[]") to nil.
|
|
701
|
+
def sanitize_dotnet_artifacts!(context, data)
|
|
702
|
+
data.each do |key, value|
|
|
703
|
+
next unless value.is_a?(String) && value =~ /^System\.\w+\[\]$/
|
|
704
|
+
|
|
705
|
+
context.debug("Sanitizing .NET serialization artifact for #{key}: #{value} -> nil")
|
|
706
|
+
data[key] = nil
|
|
707
|
+
end
|
|
708
|
+
end
|
|
709
|
+
|
|
710
|
+
# Preserve namevar values from the input hash when DSC returns nil/empty.
|
|
711
|
+
def preserve_namevar_values!(context, data, name_hash)
|
|
712
|
+
data[:name] = name_hash[:name]
|
|
713
|
+
namevar_attributes(context).each do |namevar|
|
|
714
|
+
next unless name_hash.key?(namevar)
|
|
715
|
+
|
|
716
|
+
data[namevar] = name_hash[namevar] if data[namevar].nil? || (data[namevar].respond_to?(:empty?) && data[namevar].empty?)
|
|
717
|
+
end
|
|
718
|
+
end
|
|
719
|
+
|
|
434
720
|
# Invokes the `Set` method, passing the should hash as the properties to use with `Invoke-DscResource`
|
|
435
721
|
# The PowerShell script returns a JSON hash with key-value pairs indicating whether or not the resource
|
|
436
722
|
# is in the desired state, whether or not it requires a reboot, and any error messages captured.
|
data/lib/pwsh/version.rb
CHANGED
|
@@ -47,7 +47,7 @@ RSpec.describe 'DSC Acceptance: Class-Based Resource' do
|
|
|
47
47
|
first_run_result = powershell.execute(command)
|
|
48
48
|
expect(first_run_result[:exitcode]).to be(2)
|
|
49
49
|
expect(first_run_result[:native_stdout]).to match(//)
|
|
50
|
-
expect(first_run_result[:native_stdout]).to match(/dsc_description changed
|
|
50
|
+
expect(first_run_result[:native_stdout]).to match(/dsc_description changed.*to 'Example role capability file'/)
|
|
51
51
|
expect(first_run_result[:native_stdout]).to match(/Creating: Finished/)
|
|
52
52
|
expect(first_run_result[:native_stdout]).to match(/Applied catalog/)
|
|
53
53
|
second_run_result = powershell.execute(command)
|
|
@@ -123,11 +123,11 @@ RSpec.describe 'DSC Acceptance: Complex' do
|
|
|
123
123
|
# Web content index created
|
|
124
124
|
expect(first_run_result[:native_stdout]).to match(%r{File\[WebContentIndex\]/ensure: defined content as '.+'})
|
|
125
125
|
# Web site created
|
|
126
|
-
expect(first_run_result[:native_stdout]).to match(%r{Dsc_xwebsite\[NewWebsite\]/dsc_siteid: dsc_siteid changed
|
|
126
|
+
expect(first_run_result[:native_stdout]).to match(%r{Dsc_xwebsite\[NewWebsite\]/dsc_siteid: dsc_siteid changed.*to 7})
|
|
127
127
|
expect(first_run_result[:native_stdout]).to match(%r{Dsc_xwebsite\[NewWebsite\]/dsc_ensure: dsc_ensure changed 'Absent' to 'Present'})
|
|
128
|
-
expect(first_run_result[:native_stdout]).to match(%r{Dsc_xwebsite\[NewWebsite\]/dsc_physicalpath: dsc_physicalpath changed
|
|
129
|
-
expect(first_run_result[:native_stdout]).to match(%r{Dsc_xwebsite\[NewWebsite\]/dsc_state: dsc_state changed
|
|
130
|
-
expect(first_run_result[:native_stdout]).to match(%r{Dsc_xwebsite\[NewWebsite\]/dsc_serverautostart: dsc_serverautostart changed
|
|
128
|
+
expect(first_run_result[:native_stdout]).to match(%r{Dsc_xwebsite\[NewWebsite\]/dsc_physicalpath: dsc_physicalpath changed.*to '.+fixtures/website'})
|
|
129
|
+
expect(first_run_result[:native_stdout]).to match(%r{Dsc_xwebsite\[NewWebsite\]/dsc_state: dsc_state changed.*to 'Started'})
|
|
130
|
+
expect(first_run_result[:native_stdout]).to match(%r{Dsc_xwebsite\[NewWebsite\]/dsc_serverautostart: dsc_serverautostart changed.*to 'true'})
|
|
131
131
|
expect(first_run_result[:native_stdout]).to match(/dsc_xwebsite\[{:name=>"NewWebsite", :dsc_name=>"Puppet DSC Site"}\]: Creating: Finished/)
|
|
132
132
|
# Run finished
|
|
133
133
|
expect(first_run_result[:native_stdout]).to match(/Applied catalog/)
|
|
@@ -20,6 +20,9 @@ RSpec.describe Puppet::Provider::DscBaseProvider do
|
|
|
20
20
|
provider.instance_variable_set(:@cached_query_results, [])
|
|
21
21
|
provider.instance_variable_set(:@cached_test_results, [])
|
|
22
22
|
provider.instance_variable_set(:@logon_failures, [])
|
|
23
|
+
provider.instance_variable_set(:@cached_canonicalize_results, [])
|
|
24
|
+
provider.instance_variable_set(:@cached_fresh_get_results, {})
|
|
25
|
+
provider.instance_variable_set(:@insync_property_cache, {})
|
|
23
26
|
end
|
|
24
27
|
|
|
25
28
|
describe '.initialize' do
|
|
@@ -43,6 +46,18 @@ RSpec.describe Puppet::Provider::DscBaseProvider do
|
|
|
43
46
|
it 'initializes the logon_failures instance variable' do
|
|
44
47
|
expect(provider.instance_variable_get(:@logon_failures)).to eq([])
|
|
45
48
|
end
|
|
49
|
+
|
|
50
|
+
it 'initializes the cached_canonicalize_results instance variable' do
|
|
51
|
+
expect(provider.instance_variable_get(:@cached_canonicalize_results)).to eq([])
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
it 'initializes the cached_fresh_get_results instance variable' do
|
|
55
|
+
expect(provider.instance_variable_get(:@cached_fresh_get_results)).to eq({})
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
it 'initializes the insync_property_cache instance variable' do
|
|
59
|
+
expect(provider.instance_variable_get(:@insync_property_cache)).to eq({})
|
|
60
|
+
end
|
|
46
61
|
end
|
|
47
62
|
|
|
48
63
|
describe '.cached_test_results' do
|
|
@@ -391,36 +406,259 @@ RSpec.describe Puppet::Provider::DscBaseProvider do
|
|
|
391
406
|
end
|
|
392
407
|
|
|
393
408
|
describe '.insync?' do
|
|
394
|
-
let(:name)
|
|
395
|
-
let(:
|
|
396
|
-
let(:is_hash)
|
|
397
|
-
let(:
|
|
398
|
-
let(:
|
|
399
|
-
|
|
409
|
+
let(:name) { { name: 'foo' } }
|
|
410
|
+
let(:property_name) { :dsc_setting }
|
|
411
|
+
let(:is_hash) { { name: 'foo', dsc_setting: 'Bar' } }
|
|
412
|
+
let(:should_hash) { { name: 'foo', dsc_setting: 'Foo', validation_mode: 'property' } }
|
|
413
|
+
let(:fresh_state) { { name: 'foo', dsc_setting: 'Bar' } }
|
|
414
|
+
|
|
415
|
+
before do
|
|
416
|
+
allow(context).to receive(:debug)
|
|
417
|
+
end
|
|
418
|
+
|
|
419
|
+
context 'when the corrective check is detected' do
|
|
420
|
+
it 'returns nil and deletes the cache entry on second call for the same property' do
|
|
421
|
+
provider.instance_variable_set(:@insync_property_cache, { 'foo_dsc_setting' => true })
|
|
422
|
+
result = provider.send(:insync?, context, name, property_name, is_hash, should_hash)
|
|
423
|
+
expect(result).to be_nil
|
|
424
|
+
expect(provider.instance_variable_get(:@insync_property_cache)).not_to have_key('foo_dsc_setting')
|
|
425
|
+
end
|
|
426
|
+
end
|
|
400
427
|
|
|
401
428
|
context 'when the validation_mode is "resource"' do
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
429
|
+
let(:should_hash) { { name: 'foo', dsc_setting: 'Foo', validation_mode: 'resource' } }
|
|
430
|
+
|
|
431
|
+
context 'when DSC Test says in sync' do
|
|
432
|
+
it 'returns true for any property' do
|
|
433
|
+
allow(provider).to receive(:fetch_cached_hashes).and_return([])
|
|
434
|
+
allow(provider).to receive(:invoke_test_method).and_return(true)
|
|
435
|
+
expect(provider.send(:insync?, context, name, property_name, is_hash, should_hash)).to be true
|
|
436
|
+
end
|
|
437
|
+
|
|
438
|
+
it 'caches the Test result and reuses on subsequent calls' do
|
|
439
|
+
cached_test_result = [{ name: 'foo', in_desired_state: true }]
|
|
440
|
+
allow(provider).to receive(:fetch_cached_hashes).and_return(cached_test_result)
|
|
441
|
+
expect(provider).not_to receive(:invoke_test_method)
|
|
442
|
+
expect(provider.send(:insync?, context, name, property_name, is_hash, should_hash)).to be true
|
|
443
|
+
end
|
|
406
444
|
end
|
|
407
445
|
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
446
|
+
context 'when DSC Test says out of sync' do
|
|
447
|
+
before do
|
|
448
|
+
allow(provider).to receive(:fetch_cached_hashes).and_return([])
|
|
449
|
+
allow(provider).to receive(:invoke_test_method).and_return([false, 'not in desired state'])
|
|
450
|
+
end
|
|
451
|
+
|
|
452
|
+
it 'returns fresh Get comparison result for dsc_ properties' do
|
|
453
|
+
allow(provider).to receive(:get_cached_fresh_state).and_return(fresh_state)
|
|
454
|
+
result = provider.send(:insync?, context, name, property_name, is_hash, should_hash)
|
|
455
|
+
# fresh_state has 'Bar', should_hash has 'Foo' — values differ
|
|
456
|
+
expect(result).to be_an(Array)
|
|
457
|
+
expect(result.first).to be false
|
|
458
|
+
expect(result.last).to match(/dsc_setting changed/)
|
|
459
|
+
end
|
|
460
|
+
|
|
461
|
+
it 'returns true for non-dsc_ properties (suppressed)' do
|
|
462
|
+
result = provider.send(:insync?, context, name, :name, is_hash, should_hash)
|
|
463
|
+
expect(result).to be true
|
|
464
|
+
end
|
|
465
|
+
|
|
466
|
+
it 'returns true when should_value is nil (suppressed)' do
|
|
467
|
+
should_hash_nil = should_hash.merge(dsc_setting: nil)
|
|
468
|
+
result = provider.send(:insync?, context, name, :dsc_setting, is_hash, should_hash_nil)
|
|
469
|
+
expect(result).to be true
|
|
470
|
+
end
|
|
412
471
|
end
|
|
413
472
|
end
|
|
414
473
|
|
|
415
474
|
context 'when the validation_mode is "property"' do
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
475
|
+
context 'when property_name does not start with dsc_' do
|
|
476
|
+
it 'returns nil for :name' do
|
|
477
|
+
result = provider.send(:insync?, context, name, :name, is_hash, should_hash)
|
|
478
|
+
expect(result).to be_nil
|
|
479
|
+
end
|
|
480
|
+
|
|
481
|
+
it 'returns nil for :ensure' do
|
|
482
|
+
result = provider.send(:insync?, context, name, :ensure, is_hash, should_hash)
|
|
483
|
+
expect(result).to be_nil
|
|
484
|
+
end
|
|
485
|
+
end
|
|
486
|
+
|
|
487
|
+
context 'when should_value is nil' do
|
|
488
|
+
it 'returns nil' do
|
|
489
|
+
should_hash_nil = should_hash.merge(dsc_setting: nil)
|
|
490
|
+
result = provider.send(:insync?, context, name, :dsc_setting, is_hash, should_hash_nil)
|
|
491
|
+
expect(result).to be_nil
|
|
492
|
+
end
|
|
493
|
+
end
|
|
494
|
+
|
|
495
|
+
context 'when should_value is empty string' do
|
|
496
|
+
it 'returns nil' do
|
|
497
|
+
should_hash_empty = should_hash.merge(dsc_setting: '')
|
|
498
|
+
result = provider.send(:insync?, context, name, :dsc_setting, is_hash, should_hash_empty)
|
|
499
|
+
expect(result).to be_nil
|
|
500
|
+
end
|
|
501
|
+
end
|
|
502
|
+
|
|
503
|
+
context 'when fresh Get returns nil' do
|
|
504
|
+
it 'returns nil' do
|
|
505
|
+
allow(provider).to receive(:get_cached_fresh_state).and_return(nil)
|
|
506
|
+
result = provider.send(:insync?, context, name, :dsc_setting, is_hash, should_hash)
|
|
507
|
+
expect(result).to be_nil
|
|
508
|
+
end
|
|
509
|
+
end
|
|
510
|
+
|
|
511
|
+
context 'when fresh Get returns nil in resource mode' do
|
|
512
|
+
let(:should_hash_resource) { { name: 'foo', dsc_setting: 'Foo', validation_mode: 'resource' } }
|
|
513
|
+
|
|
514
|
+
it 'returns [false, msg] and sets insync_property_cache' do
|
|
515
|
+
allow(provider).to receive(:fetch_cached_hashes).and_return([])
|
|
516
|
+
allow(provider).to receive(:invoke_test_method).and_return([false, 'not in desired state'])
|
|
517
|
+
allow(provider).to receive(:get_cached_fresh_state).and_return(nil)
|
|
518
|
+
result = provider.send(:insync?, context, name, :dsc_setting, is_hash, should_hash_resource)
|
|
519
|
+
expect(result).to be_an(Array)
|
|
520
|
+
expect(result.first).to be false
|
|
521
|
+
expect(provider.instance_variable_get(:@insync_property_cache)).to have_key('foo_dsc_setting')
|
|
522
|
+
end
|
|
523
|
+
end
|
|
524
|
+
|
|
525
|
+
context 'when values match' do
|
|
526
|
+
it 'returns true' do
|
|
527
|
+
matching_fresh = { name: 'foo', dsc_setting: 'Foo' }
|
|
528
|
+
allow(provider).to receive(:get_cached_fresh_state).and_return(matching_fresh)
|
|
529
|
+
result = provider.send(:insync?, context, name, :dsc_setting, is_hash, should_hash)
|
|
530
|
+
expect(result).to be true
|
|
531
|
+
end
|
|
532
|
+
end
|
|
533
|
+
|
|
534
|
+
context 'when values differ' do
|
|
535
|
+
it 'returns [false, change_message] and sets insync_property_cache' do
|
|
536
|
+
differing_fresh = { name: 'foo', dsc_setting: 'Bar' }
|
|
537
|
+
allow(provider).to receive(:get_cached_fresh_state).and_return(differing_fresh)
|
|
538
|
+
result = provider.send(:insync?, context, name, :dsc_setting, is_hash, should_hash)
|
|
539
|
+
expect(result).to be_an(Array)
|
|
540
|
+
expect(result.first).to be false
|
|
541
|
+
expect(result.last).to eq("dsc_setting changed 'Bar' to 'Foo'")
|
|
542
|
+
expect(provider.instance_variable_get(:@insync_property_cache)).to have_key('foo_dsc_setting')
|
|
543
|
+
end
|
|
420
544
|
end
|
|
421
545
|
end
|
|
422
546
|
end
|
|
423
547
|
|
|
548
|
+
describe '.values_equal?' do
|
|
549
|
+
it 'returns true for identical values' do
|
|
550
|
+
expect(provider.values_equal?('foo', 'foo')).to be true
|
|
551
|
+
end
|
|
552
|
+
|
|
553
|
+
it 'returns true for nil/nil' do
|
|
554
|
+
expect(provider.values_equal?(nil, nil)).to be true
|
|
555
|
+
end
|
|
556
|
+
|
|
557
|
+
it 'returns true for nil and empty string' do
|
|
558
|
+
expect(provider.values_equal?(nil, '')).to be true
|
|
559
|
+
expect(provider.values_equal?('', nil)).to be true
|
|
560
|
+
end
|
|
561
|
+
|
|
562
|
+
it 'returns true for case-insensitive string match' do
|
|
563
|
+
expect(provider.values_equal?('Foo', 'foo')).to be true
|
|
564
|
+
end
|
|
565
|
+
|
|
566
|
+
it 'returns false for integer vs string mismatch' do
|
|
567
|
+
expect(provider.values_equal?(30, '30')).to be false
|
|
568
|
+
end
|
|
569
|
+
|
|
570
|
+
it 'returns true for arrays in different order' do
|
|
571
|
+
expect(provider.values_equal?(%w[a b c], %w[c b a])).to be true
|
|
572
|
+
end
|
|
573
|
+
|
|
574
|
+
it 'returns true for hashes with case-different keys/values' do
|
|
575
|
+
expect(provider.values_equal?({ 'Foo' => 'Bar' }, { 'foo' => 'bar' })).to be true
|
|
576
|
+
end
|
|
577
|
+
|
|
578
|
+
it 'returns false for genuinely different values' do
|
|
579
|
+
expect(provider.values_equal?('foo', 'bar')).to be false
|
|
580
|
+
end
|
|
581
|
+
|
|
582
|
+
it 'returns false for different numbers' do
|
|
583
|
+
expect(provider.values_equal?(30, 42)).to be false
|
|
584
|
+
end
|
|
585
|
+
end
|
|
586
|
+
|
|
587
|
+
describe '.perform_fresh_get' do
|
|
588
|
+
let(:name) { { name: 'foo', dsc_name: 'foo' } }
|
|
589
|
+
let(:should_hash) { { name: 'foo', dsc_name: 'foo', dsc_setting: 'desired' } }
|
|
590
|
+
let(:attributes) { { name: { type: 'String' }, dsc_name: { type: 'String', mandatory_for_get: true }, dsc_setting: { type: 'String' } } }
|
|
591
|
+
|
|
592
|
+
before do
|
|
593
|
+
allow(context).to receive(:debug)
|
|
594
|
+
allow(context).to receive(:type).and_return(type)
|
|
595
|
+
allow(type).to receive(:attributes).and_return(attributes)
|
|
596
|
+
allow(provider).to receive(:mandatory_get_attributes).and_return([:dsc_name])
|
|
597
|
+
end
|
|
598
|
+
|
|
599
|
+
context 'when invoke_dsc_resource returns data' do
|
|
600
|
+
it 'returns a hash with dsc_ prefixed keys and :name' do
|
|
601
|
+
allow(provider).to receive(:invoke_dsc_resource).and_return({ 'Name' => 'foo', 'Setting' => 'current' })
|
|
602
|
+
result = provider.perform_fresh_get(context, name, should_hash)
|
|
603
|
+
expect(result[:dsc_setting]).to eq('current')
|
|
604
|
+
expect(result[:name]).to eq('foo')
|
|
605
|
+
end
|
|
606
|
+
|
|
607
|
+
it 'sanitizes System.Object[] artifacts to nil' do
|
|
608
|
+
allow(provider).to receive(:invoke_dsc_resource).and_return({ 'Name' => 'foo', 'Setting' => 'System.Object[]' })
|
|
609
|
+
result = provider.perform_fresh_get(context, name, should_hash)
|
|
610
|
+
expect(result[:dsc_setting]).to be_nil
|
|
611
|
+
end
|
|
612
|
+
|
|
613
|
+
it 'normalizes CIM instance hash keys and munges cim_instance_type' do
|
|
614
|
+
allow(provider).to receive(:invoke_dsc_resource).and_return({ 'Name' => 'foo', 'Setting' => { 'SomeKey' => 'val' } })
|
|
615
|
+
result = provider.perform_fresh_get(context, name, should_hash)
|
|
616
|
+
expect(result[:dsc_setting].keys.first).to eq('somekey')
|
|
617
|
+
end
|
|
618
|
+
end
|
|
619
|
+
|
|
620
|
+
context 'when invoke_dsc_resource returns nil' do
|
|
621
|
+
it 'returns nil' do
|
|
622
|
+
allow(provider).to receive(:invoke_dsc_resource).and_return(nil)
|
|
623
|
+
expect(provider.perform_fresh_get(context, name, should_hash)).to be_nil
|
|
624
|
+
end
|
|
625
|
+
end
|
|
626
|
+
|
|
627
|
+
context 'when invoke_dsc_resource raises an error' do
|
|
628
|
+
it 'returns nil and logs a debug message' do
|
|
629
|
+
allow(provider).to receive(:invoke_dsc_resource).and_raise(StandardError, 'boom')
|
|
630
|
+
expect(context).to receive(:debug).with(/perform_fresh_get failed/)
|
|
631
|
+
expect(provider.perform_fresh_get(context, name, should_hash)).to be_nil
|
|
632
|
+
end
|
|
633
|
+
end
|
|
634
|
+
end
|
|
635
|
+
|
|
636
|
+
describe '.get_cached_fresh_state' do
|
|
637
|
+
let(:name) { { name: 'foo' } }
|
|
638
|
+
let(:should_hash) { { name: 'foo', dsc_setting: 'desired' } }
|
|
639
|
+
let(:fresh_result) { { name: 'foo', dsc_setting: 'current' } }
|
|
640
|
+
|
|
641
|
+
before do
|
|
642
|
+
allow(context).to receive(:debug)
|
|
643
|
+
end
|
|
644
|
+
|
|
645
|
+
it 'calls perform_fresh_get on first call and caches the result' do
|
|
646
|
+
expect(provider).to receive(:perform_fresh_get).once.and_return(fresh_result)
|
|
647
|
+
result1 = provider.get_cached_fresh_state(context, name, should_hash)
|
|
648
|
+
result2 = provider.get_cached_fresh_state(context, name, should_hash)
|
|
649
|
+
expect(result1).to eq(fresh_result)
|
|
650
|
+
expect(result2).to eq(fresh_result)
|
|
651
|
+
end
|
|
652
|
+
|
|
653
|
+
it 'caches nil results to avoid repeated failed calls' do
|
|
654
|
+
expect(provider).to receive(:perform_fresh_get).once.and_return(nil)
|
|
655
|
+
result1 = provider.get_cached_fresh_state(context, name, should_hash)
|
|
656
|
+
result2 = provider.get_cached_fresh_state(context, name, should_hash)
|
|
657
|
+
expect(result1).to be_nil
|
|
658
|
+
expect(result2).to be_nil
|
|
659
|
+
end
|
|
660
|
+
end
|
|
661
|
+
|
|
424
662
|
describe '.invoke_get_method' do
|
|
425
663
|
subject(:result) { provider.invoke_get_method(context, name_hash) }
|
|
426
664
|
|
|
@@ -656,6 +894,47 @@ RSpec.describe Puppet::Provider::DscBaseProvider do
|
|
|
656
894
|
end
|
|
657
895
|
end
|
|
658
896
|
|
|
897
|
+
context 'when DSC returns System.Object[] string values' do
|
|
898
|
+
let(:parsed_invocation_data) do
|
|
899
|
+
{
|
|
900
|
+
'Name' => 'foo',
|
|
901
|
+
'Ensure' => 'System.Object[]',
|
|
902
|
+
'Time' => nil
|
|
903
|
+
}
|
|
904
|
+
end
|
|
905
|
+
|
|
906
|
+
before do
|
|
907
|
+
allow(ps_manager).to receive(:execute).with(script, nil).and_return({ stdout: 'DSC Data' })
|
|
908
|
+
allow(JSON).to receive(:parse).with('DSC Data').and_return(parsed_invocation_data)
|
|
909
|
+
allow(provider).to receive(:fetch_cached_hashes).and_return([])
|
|
910
|
+
end
|
|
911
|
+
|
|
912
|
+
it 'sanitizes System.Object[] to nil' do
|
|
913
|
+
expect(result[:dsc_ensure]).to be_nil
|
|
914
|
+
end
|
|
915
|
+
end
|
|
916
|
+
|
|
917
|
+
context 'when DSC returns nil for a namevar' do
|
|
918
|
+
let(:parsed_invocation_data) do
|
|
919
|
+
{
|
|
920
|
+
'Name' => nil,
|
|
921
|
+
'Ensure' => 'Present'
|
|
922
|
+
}
|
|
923
|
+
end
|
|
924
|
+
|
|
925
|
+
before do
|
|
926
|
+
allow(ps_manager).to receive(:execute).with(script, nil).and_return({ stdout: 'DSC Data' })
|
|
927
|
+
allow(JSON).to receive(:parse).with('DSC Data').and_return(parsed_invocation_data)
|
|
928
|
+
allow(provider).to receive(:fetch_cached_hashes).and_return([])
|
|
929
|
+
allow(provider).to receive(:namevar_attributes).and_return(%i[name dsc_name])
|
|
930
|
+
end
|
|
931
|
+
|
|
932
|
+
it 'preserves the namevar value from the input name_hash' do
|
|
933
|
+
expect(result[:dsc_name]).to eq('foo')
|
|
934
|
+
expect(result[:name]).to eq('foo')
|
|
935
|
+
end
|
|
936
|
+
end
|
|
937
|
+
|
|
659
938
|
context 'when the DSC invocation errors' do
|
|
660
939
|
it 'writes an error and returns nil' do
|
|
661
940
|
expect(provider).not_to receive(:logon_failed_already?)
|
|
@@ -864,6 +1143,44 @@ RSpec.describe Puppet::Provider::DscBaseProvider do
|
|
|
864
1143
|
end
|
|
865
1144
|
end
|
|
866
1145
|
|
|
1146
|
+
describe '.invoke_get_method_for_canonicalize' do
|
|
1147
|
+
before do
|
|
1148
|
+
allow(context).to receive(:debug)
|
|
1149
|
+
allow(provider).to receive(:namevar_attributes).and_return(%i[name dsc_name])
|
|
1150
|
+
end
|
|
1151
|
+
|
|
1152
|
+
it 'calls invoke_get_method and caches to @cached_canonicalize_results' do
|
|
1153
|
+
result = { name: 'foo', dsc_name: 'foo', dsc_setting: 'bar' }
|
|
1154
|
+
allow(provider).to receive(:invoke_get_method).and_return(result)
|
|
1155
|
+
allow(provider).to receive(:fetch_cached_hashes).and_return([])
|
|
1156
|
+
|
|
1157
|
+
provider.invoke_get_method_for_canonicalize(context, { name: 'foo', dsc_name: 'foo' })
|
|
1158
|
+
expect(provider.instance_variable_get(:@cached_canonicalize_results)).to include(result)
|
|
1159
|
+
end
|
|
1160
|
+
|
|
1161
|
+
it 'removes entries from @cached_query_results so get() does its own lookup' do
|
|
1162
|
+
result = { name: 'foo', dsc_name: 'foo', dsc_setting: 'bar' }
|
|
1163
|
+
cached_entry = result.dup
|
|
1164
|
+
allow(provider).to receive(:invoke_get_method).and_return(result)
|
|
1165
|
+
# First call checks @cached_canonicalize_results — return [] (cache miss)
|
|
1166
|
+
# Subsequent calls within reject! should return a match so the entry gets removed
|
|
1167
|
+
allow(provider).to receive(:fetch_cached_hashes).and_return([], [cached_entry], [])
|
|
1168
|
+
provider.instance_variable_set(:@cached_query_results, [cached_entry])
|
|
1169
|
+
|
|
1170
|
+
provider.invoke_get_method_for_canonicalize(context, { name: 'foo', dsc_name: 'foo' })
|
|
1171
|
+
expect(provider.instance_variable_get(:@cached_query_results)).to be_empty
|
|
1172
|
+
end
|
|
1173
|
+
|
|
1174
|
+
it 'returns cached result on subsequent calls' do
|
|
1175
|
+
result = { name: 'foo', dsc_name: 'foo', dsc_setting: 'bar' }
|
|
1176
|
+
provider.instance_variable_set(:@cached_canonicalize_results, [result])
|
|
1177
|
+
allow(provider).to receive(:fetch_cached_hashes).with([result], anything).and_return([result])
|
|
1178
|
+
|
|
1179
|
+
expect(provider).not_to receive(:invoke_get_method)
|
|
1180
|
+
expect(provider.invoke_get_method_for_canonicalize(context, { name: 'foo', dsc_name: 'foo' })).to eq(result)
|
|
1181
|
+
end
|
|
1182
|
+
end
|
|
1183
|
+
|
|
867
1184
|
describe '.puppetize_name' do
|
|
868
1185
|
it 'downcases the input string' do
|
|
869
1186
|
expect(provider.puppetize_name('FooBar')).to eq('foobar')
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: ruby-pwsh
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 2.0.
|
|
4
|
+
version: 2.0.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Puppet, Inc.
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date:
|
|
11
|
+
date: 2026-02-16 00:00:00.000000000 Z
|
|
12
12
|
dependencies: []
|
|
13
13
|
description: PowerShell code manager for ruby.
|
|
14
14
|
email:
|
|
@@ -67,7 +67,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
67
67
|
- !ruby/object:Gem::Version
|
|
68
68
|
version: '0'
|
|
69
69
|
requirements: []
|
|
70
|
-
rubygems_version: 3.
|
|
70
|
+
rubygems_version: 3.4.19
|
|
71
71
|
signing_key:
|
|
72
72
|
specification_version: 4
|
|
73
73
|
summary: PowerShell code manager for ruby.
|