22
33require 'pathname'
44require 'json'
5+ require 'erb' # ensure ERB is available
56require_relative '../../../puppet_x/puppetlabs/dsc_lite/powershell_hash_formatter'
67
78Puppet ::Type . type ( :base_dsc_lite ) . provide ( :powershell ) do
3132 Puppet (including 3.x), or to a Puppet version newer than 3.x.
3233 UPGRADE
3334
35+ # ---------------------------
36+ # Class-level helpers
37+ # ---------------------------
38+
3439 def self . upgrade_message
3540 Puppet . warning DSC_LITE_MODULE_PUPPET_UPGRADE_MSG unless @upgrade_warning_issued
3641 @upgrade_warning_issued = true
@@ -40,22 +45,48 @@ def self.vendored_modules_path
4045 File . expand_path ( Pathname . new ( __FILE__ ) . dirname + '../../../' + 'puppet_x/dsc_resources' )
4146 end
4247
48+ def self . template_path
49+ File . expand_path ( Pathname . new ( __FILE__ ) . dirname )
50+ end
51+
52+ def self . format_dsc_lite ( dsc_value )
53+ # Keep this PURE to avoid breaking unit tests:
54+ # it should not try to resolve Deferred values.
55+ PuppetX ::PuppetLabs ::DscLite ::PowerShellHashFormatter . format ( dsc_value )
56+ end
57+
58+ def self . escape_quotes ( text )
59+ text . gsub ( "'" , "''" )
60+ end
61+
62+ def self . redact_content ( content )
63+ # Note that here we match after an equals to ensure we redact the value being passed, but not the key.
64+ # This means a redaction of a string not including '= ' before the string value will not redact.
65+ # Every secret unwrapped in this module will unwrap as "'secret' # PuppetSensitive" and, currently,
66+ # always inside a hash table to be passed along. This means we can (currently) expect the value to
67+ # always come after an equals sign.
68+ # Note that the line may include a semi-colon and/or a newline character after the sensitive unwrap.
69+ content . gsub ( %r{= '.+' # PuppetSensitive;?(\\ n)?$} , "= '[REDACTED]'" )
70+ end
71+
72+ # ---------------------------
73+ # Instance-level helpers
74+ # ---------------------------
75+
76+ # Return only dsc_* parameters that have a value
4377 def dsc_parameters
4478 resource . parameters_with_value . select do |p |
4579 p . name . to_s . include? 'dsc_'
4680 end
4781 end
4882
83+ # Return the :properties parameter (if provided)
4984 def dsc_property_param
5085 resource . parameters_with_value . select { |pr | pr . name == :properties } . each do |p |
5186 p . name . to_s . include? 'dsc_'
5287 end
5388 end
5489
55- def self . template_path
56- File . expand_path ( Pathname . new ( __FILE__ ) . dirname )
57- end
58-
5990 def set_timeout
6091 resource [ :dsc_timeout ] ? resource [ :dsc_timeout ] * 1000 : 1_200_000
6192 end
@@ -65,11 +96,83 @@ def ps_manager
6596 Pwsh ::Manager . instance ( command ( :powershell ) , Pwsh ::Manager . powershell_args , debug : debug_output )
6697 end
6798
99+ # --- NEW: deep scan for Deferred ---
100+ # Recursively detect any object whose class name suggests it's a Puppet Deferred
101+ def deep_contains_deferred? ( obj )
102+ case obj
103+ when Hash
104+ obj . any? { |k , v | deep_contains_deferred? ( k ) || deep_contains_deferred? ( v ) }
105+ when Array
106+ obj . any? { |v | deep_contains_deferred? ( v ) }
107+ else
108+ obj && obj . class && obj . class . name . to_s . include? ( 'Deferred' )
109+ end
110+ end
111+
112+ # --- NEW: best-effort resolution hook ---
113+ # Attempt to resolve any remaining Deferreds before rendering ERB.
114+ # This is intentionally defensive: it tries available agent-side options
115+ # and degrades to no-op if none are present.
116+ def ensure_deferreds_resolved!
117+ # 1) Catalog-wide resolve (if the catalog supports it)
118+ begin
119+ cat = resource &.catalog
120+ if cat && cat . respond_to? ( :resolve_and_replace )
121+ cat . resolve_and_replace
122+ Puppet . debug ( 'DSC_lite: called catalog.resolve_and_replace for last-chance Deferred resolution' )
123+ end
124+ rescue StandardError => e
125+ Puppet . debug ( "DSC_lite: resolve_and_replace raised #{ e . class } : #{ e . message } " )
126+ end
127+
128+ # 2) Compiler lookup (often nil in agent/provider context)
129+ begin
130+ compiler = Puppet . respond_to? ( :lookup ) ? Puppet . lookup ( :compiler ) { nil } : nil
131+ Puppet . debug ( 'DSC_lite: compiler available for resolution' ) if compiler
132+ rescue StandardError => e
133+ Puppet . debug ( "DSC_lite: compiler lookup failed: #{ e . class } : #{ e . message } " )
134+ end
135+
136+ # 3) Nothing else to do safely here—actual evaluation requires scope,
137+ # which agent providers typically don't have. We rely on (1), and if
138+ # it still leaks we'll detect/log just before formatting.
139+ nil
140+ end
141+
142+ # --- NEW: provider helper used by ERB ---
143+ # Route every formatted value through here so we can:
144+ # - perform a last-chance Deferred resolution
145+ # - then delegate to the pure formatter
146+ def format_for_ps ( value )
147+ if deep_contains_deferred? ( value )
148+ Puppet . debug ( 'DSC_lite: Deferred detected in ERB-bound value; attempting last-chance resolution' )
149+ ensure_deferreds_resolved!
150+ if deep_contains_deferred? ( value )
151+ Puppet . debug ( 'DSC_lite: value still contains Deferred after last-chance resolution' )
152+ # Optional: uncomment to fail fast with a clearer message instead of formatter type error
153+ # raise Puppet::Error, 'DSC_lite: Deferred value reached ERB formatting; '\
154+ # 'agent invoked provider before deferrals completed (Puppet 8 timing).'
155+ end
156+ end
157+ self . class . format_dsc_lite ( value )
158+ end
159+ # ---------------------------
160+ # Provider operations
161+ # ---------------------------
162+
68163 def exists?
69164 timeout = set_timeout
70165 Puppet . debug "Dsc Timeout: #{ timeout } milliseconds"
71166 version = Facter . value ( :powershell_version )
72167 Puppet . debug "PowerShell Version: #{ version } "
168+
169+ # Last-chance resolve + pinpoint diagnostic BEFORE rendering
170+ ensure_deferreds_resolved!
171+ leaks = resource . parameters_with_value
172+ . select { |p | deep_contains_deferred? ( p . value ) }
173+ . map { |p | "#{ p . name } =#{ p . value . class } " }
174+ Puppet . debug ( "DSC_lite: still deferred after resolution? #{ leaks . join ( ', ' ) } " )
175+
73176 script_content = ps_script_content ( 'test' )
74177 Puppet . debug "\n " + self . class . redact_content ( script_content )
75178
@@ -97,6 +200,14 @@ def exists?
97200 def create
98201 timeout = set_timeout
99202 Puppet . debug "Dsc Timeout: #{ timeout } milliseconds"
203+
204+ # Last-chance resolve + pinpoint diagnostic BEFORE rendering
205+ ensure_deferreds_resolved!
206+ leaks = resource . parameters_with_value
207+ . select { |p | deep_contains_deferred? ( p . value ) }
208+ . map { |p | "#{ p . name } =#{ p . value . class } " }
209+ Puppet . debug ( "DSC_lite: still deferred after resolution? #{ leaks . join ( ', ' ) } " )
210+
100211 script_content = ps_script_content ( 'set' )
101212 Puppet . debug "\n " + self . class . redact_content ( script_content )
102213
@@ -116,7 +227,6 @@ def create
116227 raise ( data [ 'errormessage' ] ) unless data [ 'errormessage' ] . empty?
117228
118229 notify_reboot_pending if data [ 'rebootrequired' ] == true
119-
120230 data
121231 end
122232
@@ -138,34 +248,20 @@ def notify_reboot_pending
138248 end
139249 end
140250
141- def self . format_dsc_lite ( dsc_value )
142- PuppetX ::PuppetLabs ::DscLite ::PowerShellHashFormatter . format ( dsc_value )
143- end
144-
145- def self . escape_quotes ( text )
146- text . gsub ( "'" , "''" )
147- end
148-
149- def self . redact_content ( content )
150- # Note that here we match after an equals to ensure we redact the value being passed, but not the key.
151- # This means a redaction of a string not including '= ' before the string value will not redact.
152- # Every secret unwrapped in this module will unwrap as "'secret' # PuppetSensitive" and, currently,
153- # always inside a hash table to be passed along. This means we can (currently) expect the value to
154- # always come after an equals sign.
155- # Note that the line may include a semi-colon and/or a newline character after the sensitive unwrap.
156- content . gsub ( %r{= '.+' # PuppetSensitive;?(\\ n)?$} , "= '[REDACTED]'" )
157- end
158-
159251 def ps_script_content ( mode )
160252 self . class . ps_script_content ( mode , resource , self )
161253 end
162-
254+
163255 def self . ps_script_content ( mode , resource , provider )
164256 dsc_invoke_method = mode
165257 @param_hash = resource
166258 template_name = resource . generic_dsc ? '/invoke_generic_dsc_resource.ps1.erb' : '/invoke_dsc_resource.ps1.erb'
167259 file = File . new ( template_path + template_name , encoding : Encoding ::UTF_8 )
260+
261+ # Provide explicit local for ERB if templates refer to it
262+ vendored_modules_path = self . vendored_modules_path
263+
168264 template = ERB . new ( file . read , trim_mode : '-' )
169- template . result ( binding )
265+ template . result ( binding ) # binding includes: resource, provider, dsc_invoke_method, vendored_modules_path
170266 end
171267end
0 commit comments