22
33require 'pathname'
44require 'json'
5- require 'erb' # ensure ERB is available
6- require 'puppet'
7- require 'puppet/node'
8- require 'puppet/parser/script_compiler'
9- require 'puppet/pops/evaluator/deferred_resolver'
105require_relative '../../../puppet_x/puppetlabs/dsc_lite/powershell_hash_formatter'
116
127Puppet ::Type . type ( :base_dsc_lite ) . provide ( :powershell ) do
3631 Puppet (including 3.x), or to a Puppet version newer than 3.x.
3732 UPGRADE
3833
39- # ---------------------------
40- # Class helpers
41- # ---------------------------
42-
4334 def self . upgrade_message
4435 Puppet . warning DSC_LITE_MODULE_PUPPET_UPGRADE_MSG unless @upgrade_warning_issued
4536 @upgrade_warning_issued = true
@@ -49,53 +40,20 @@ def self.vendored_modules_path
4940 File . expand_path ( Pathname . new ( __FILE__ ) . dirname + '../../../' + 'puppet_x/dsc_resources' )
5041 end
5142
52- def self . template_path
53- File . expand_path ( Pathname . new ( __FILE__ ) . dirname )
54- end
55-
56- def self . format_dsc_lite ( dsc_value )
57- PuppetX ::PuppetLabs ::DscLite ::PowerShellHashFormatter . format ( dsc_value )
58- end
59-
60- def self . escape_quotes ( text )
61- text . gsub ( "'" , "''" )
62- end
63-
64- def self . redact_content ( content )
65- # Redact Sensitive unwraps that appear as "'secret' # PuppetSensitive"
66- content . gsub ( %r{= '.+' # PuppetSensitive;?(\\ n)?$} , "= '[REDACTED]'" )
67- end
68-
69- # ---------------------------
70- # Deferred value resolution - NEW APPROACH
71- # ---------------------------
72-
7343 # Resolve deferred values in properties right before PowerShell script generation
74- # This is the correct timing - after catalog application starts but before template rendering
7544 def resolve_deferred_values!
7645 return unless resource . parameters . key? ( :properties )
7746
7847 current_properties = resource . parameters [ :properties ] . value
7948 return unless contains_deferred_values? ( current_properties )
8049
81- Puppet . notice ( 'DSC PROVIDER → Resolving deferred values in properties' )
82-
8350 begin
8451 # Resolve deferred values directly using the properties hash
8552 resolved_properties = manually_resolve_deferred_values ( current_properties )
86-
8753 # Update the resource with resolved properties
8854 resource . parameters [ :properties ] . value = resolved_properties
89-
90- # Verify resolution worked
91- if contains_deferred_values? ( resolved_properties )
92- Puppet . warning ( 'DSC PROVIDER → Some deferred values could not be resolved' )
93- else
94- Puppet . notice ( 'DSC PROVIDER → All deferred values resolved successfully' )
95- end
9655 rescue => e
9756 Puppet . warning ( "DSC PROVIDER → Error resolving deferred values: #{ e . class } : #{ e . message } " )
98- Puppet . debug ( "DSC PROVIDER → Error backtrace: #{ e . backtrace . join ( "\n " ) } " )
9957 # Continue with unresolved values - they will be stringified but at least won't crash
10058 end
10159 end
@@ -119,25 +77,17 @@ def manually_resolve_deferred_values(value)
11977 # DeferredValue objects have a @proc instance variable we can call
12078 proc = value . instance_variable_get ( :@proc )
12179 return proc . call if proc && proc . respond_to? ( :call )
122-
123- Puppet . debug ( 'DSC PROVIDER → DeferredValue has no callable proc' )
12480 return value . to_s
125-
12681 elsif value && value . class . name . include? ( 'Deferred' )
12782 # For other Deferred types, try standard resolution
128- if value . respond_to? ( :name )
129- begin
130- return Puppet ::Pops ::Evaluator ::DeferredResolver . resolve ( value . name , nil , { } )
131- rescue => e
132- Puppet . debug ( "DSC PROVIDER → Failed to resolve Deferred object: #{ e . message } " )
133- return value . to_s
134- end
135- else
136- Puppet . debug ( 'DSC PROVIDER → Deferred object has no name method' )
83+ return value . to_s unless value . respond_to? ( :name )
84+ begin
85+ return Puppet ::Pops ::Evaluator ::DeferredResolver . resolve ( value . name , nil , { } )
86+ rescue
13787 return value . to_s
13888 end
139- end
14089
90+ end
14191 # Return the value unchanged if it's not deferred
14292 value
14393 end
@@ -157,92 +107,6 @@ def contains_deferred_values?(value)
157107 end
158108 end
159109
160- # ---------------------------
161- # Legacy deferred resolution methods (keeping for reference but not using)
162- # ---------------------------
163-
164- # 1) Catalog-wide resolve: replace all Deferreds/futures in the catalog
165- def force_resolve_catalog_deferred!
166- cat = resource &.catalog
167- return unless cat
168-
169- facts = Puppet . lookup ( :facts ) { nil }
170- env = if cat . respond_to? ( :environment_instance )
171- cat . environment_instance
172- else
173- Puppet . lookup ( :current_environment ) { nil }
174- end
175-
176- begin
177- Puppet ::Pops ::Evaluator ::DeferredResolver . resolve_and_replace ( facts , cat , env , true )
178- Puppet . notice ( 'DSC PROVIDER SENTINEL → resolve_and_replace invoked' )
179- rescue => e
180- Puppet . notice ( "DSC PROVIDER resolve_and_replace error: #{ e . class } : #{ e . message } " )
181- end
182- end
183-
184- # Build a compiler on the agent for local resolution
185- def build_agent_compiler ( env )
186- node_name = Puppet [ :node_name_value ]
187- node = Puppet ::Node . new ( node_name , environment : env )
188-
189- # Attaching facts improves function behavior during resolve (best-effort)
190- begin
191- facts = Puppet . lookup ( :facts ) { nil }
192- node . add_facts ( facts ) if facts
193- rescue => e
194- Puppet . debug ( "DSC_lite: could not attach facts to node for local resolve: #{ e . class } : #{ e . message } " )
195- end
196-
197- if defined? ( Puppet ::Parser ::ScriptCompiler )
198- Puppet ::Parser ::ScriptCompiler . new ( node , env )
199- else
200- Puppet ::Parser ::Compiler . new ( node )
201- end
202- end
203-
204- # 2) Targeted resolve: explicitly resolve the :properties value using a resolver that
205- # handles both Deferred and evaluator futures, then write it back to the resource.
206- def force_resolve_properties!
207- return unless resource . parameters . key? ( :properties )
208-
209- cat = resource &.catalog
210- env = if cat &.respond_to? ( :environment_instance )
211- cat . environment_instance
212- else
213- Puppet . lookup ( :current_environment ) { nil }
214- end
215-
216- begin
217- compiler = build_agent_compiler ( env ) if env
218- return unless compiler # without a compiler, local resolve can't proceed
219-
220- facts = Puppet . lookup ( :facts ) { nil }
221-
222- resolver = Puppet ::Pops ::Evaluator ::DeferredResolver . new ( compiler , true )
223- resolver . set_facts_variable ( facts ) if facts
224-
225- # Read current value, resolve deeply, then write back into the parameter
226- current = resource . parameters [ :properties ] . value
227- resolved = resolver . resolve ( current )
228- resource . parameters [ :properties ] . value = resolved
229-
230- # Post-check: what class is properties['Contents'] now?
231- cls = if resolved . is_a? ( Hash ) && resolved . key? ( 'Contents' )
232- resolved [ 'Contents' ] . class
233- else
234- resolved . class
235- end
236- Puppet . notice ( "DSC PROVIDER SENTINEL → post-resolve properties.Contents: #{ cls } " )
237- rescue => e
238- Puppet . debug ( "DSC_lite: explicit properties resolve failed: #{ e . class } : #{ e . message } " )
239- end
240- end
241-
242- # ---------------------------
243- # Existing provider helpers
244- # ---------------------------
245-
246110 def dsc_parameters
247111 resource . parameters_with_value . select do |p |
248112 p . name . to_s . include? 'dsc_'
@@ -255,6 +119,10 @@ def dsc_property_param
255119 end
256120 end
257121
122+ def self . template_path
123+ File . expand_path ( Pathname . new ( __FILE__ ) . dirname )
124+ end
125+
258126 def set_timeout
259127 resource [ :dsc_timeout ] ? resource [ :dsc_timeout ] * 1000 : 1_200_000
260128 end
@@ -264,32 +132,21 @@ def ps_manager
264132 Pwsh ::Manager . instance ( command ( :powershell ) , Pwsh ::Manager . powershell_args , debug : debug_output )
265133 end
266134
267- # Keep for ERBs that call provider.format_for_ps(...)
268- def format_for_ps ( value )
269- self . class . format_dsc_lite ( value )
270- end
271-
272- # ---------------------------
273- # Provider operations
274- # ---------------------------
275-
276135 def exists?
277- Puppet . notice ( "DSC PROVIDER SENTINEL → #{ __FILE__ } " )
278-
279136 # Resolve deferred values right before we start processing
280137 resolve_deferred_values!
281138
282139 timeout = set_timeout
283140 Puppet . debug "Dsc Timeout: #{ timeout } milliseconds"
284141 version = Facter . value ( :powershell_version )
285142 Puppet . debug "PowerShell Version: #{ version } "
286-
287143 script_content = ps_script_content ( 'test' )
288144 Puppet . debug "\n " + self . class . redact_content ( script_content )
289145
290146 if Pwsh ::Manager . windows_powershell_supported?
291147 output = ps_manager . execute ( script_content , timeout )
292148 raise Puppet ::Error , output [ :errormessage ] if output [ :errormessage ] &.match? ( %r{PowerShell module timeout \( \d + ms\) exceeded while executing} )
149+
293150 output = output [ :stdout ]
294151 else
295152 self . class . upgrade_message
@@ -308,20 +165,18 @@ def exists?
308165 end
309166
310167 def create
311- Puppet . notice ( "DSC PROVIDER SENTINEL → #{ __FILE__ } " )
312-
313168 # Resolve deferred values right before we start processing
314169 resolve_deferred_values!
315170
316171 timeout = set_timeout
317172 Puppet . debug "Dsc Timeout: #{ timeout } milliseconds"
318-
319173 script_content = ps_script_content ( 'set' )
320174 Puppet . debug "\n " + self . class . redact_content ( script_content )
321175
322176 if Pwsh ::Manager . windows_powershell_supported?
323177 output = ps_manager . execute ( script_content , timeout )
324178 raise Puppet ::Error , output [ :errormessage ] if output [ :errormessage ] &.match? ( %r{PowerShell module timeout \( \d + ms\) exceeded while executing} )
179+
325180 output = output [ :stdout ]
326181 else
327182 self . class . upgrade_message
@@ -332,6 +187,7 @@ def create
332187 data = JSON . parse ( output )
333188
334189 raise ( data [ 'errormessage' ] ) unless data [ 'errormessage' ] . empty?
190+
335191 notify_reboot_pending if data [ 'rebootrequired' ] == true
336192
337193 data
@@ -355,19 +211,32 @@ def notify_reboot_pending
355211 end
356212 end
357213
214+ def self . format_dsc_lite ( dsc_value )
215+ PuppetX ::PuppetLabs ::DscLite ::PowerShellHashFormatter . format ( dsc_value )
216+ end
217+
218+ def self . escape_quotes ( text )
219+ text . gsub ( "'" , "''" )
220+ end
221+
222+ def self . redact_content ( content )
223+ # Note that here we match after an equals to ensure we redact the value being passed, but not the key.
224+ # This means a redaction of a string not including '= ' before the string value will not redact.
225+ # Every secret unwrapped in this module will unwrap as "'secret' # PuppetSensitive" and, currently,
226+ # always inside a hash table to be passed along. This means we can (currently) expect the value to
227+ # always come after an equals sign.
228+ # Note that the line may include a semi-colon and/or a newline character after the sensitive unwrap.
229+ content . gsub ( %r{= '.+' # PuppetSensitive;?(\\ n)?$} , "= '[REDACTED]'" )
230+ end
231+
358232 def ps_script_content ( mode )
359233 self . class . ps_script_content ( mode , resource , self )
360234 end
361235
362236 def self . ps_script_content ( mode , resource , provider )
363- dsc_invoke_method = mode
364237 @param_hash = resource
365238 template_name = resource . generic_dsc ? '/invoke_generic_dsc_resource.ps1.erb' : '/invoke_dsc_resource.ps1.erb'
366239 file = File . new ( template_path + template_name , encoding : Encoding ::UTF_8 )
367-
368- # Make vendored_modules_path visible in ERB if the template uses it
369- vendored_modules_path = self . vendored_modules_path
370-
371240 template = ERB . new ( file . read , trim_mode : '-' )
372241 template . result ( binding )
373242 end
0 commit comments