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'
510require_relative '../../../puppet_x/puppetlabs/dsc_lite/powershell_hash_formatter'
611
712Puppet ::Type . type ( :base_dsc_lite ) . provide ( :powershell ) do
3136 Puppet (including 3.x), or to a Puppet version newer than 3.x.
3237 UPGRADE
3338
39+ # ---------------------------
40+ # Class helpers
41+ # ---------------------------
42+
3443 def self . upgrade_message
3544 Puppet . warning DSC_LITE_MODULE_PUPPET_UPGRADE_MSG unless @upgrade_warning_issued
3645 @upgrade_warning_issued = true
@@ -40,6 +49,200 @@ def self.vendored_modules_path
4049 File . expand_path ( Pathname . new ( __FILE__ ) . dirname + '../../../' + 'puppet_x/dsc_resources' )
4150 end
4251
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+
73+ # Resolve deferred values in properties right before PowerShell script generation
74+ # This is the correct timing - after catalog application starts but before template rendering
75+ def resolve_deferred_values!
76+ return unless resource . parameters . key? ( :properties )
77+
78+ current_properties = resource . parameters [ :properties ] . value
79+ return unless contains_deferred_values? ( current_properties )
80+
81+ Puppet . notice ( 'DSC PROVIDER → Resolving deferred values in properties' )
82+
83+ begin
84+ # Resolve deferred values directly using the properties hash
85+ resolved_properties = manually_resolve_deferred_values ( current_properties )
86+
87+ # Update the resource with resolved properties
88+ 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
96+ rescue => e
97+ Puppet . warning ( "DSC PROVIDER → Error resolving deferred values: #{ e . class } : #{ e . message } " )
98+ Puppet . debug ( "DSC PROVIDER → Error backtrace: #{ e . backtrace . join ( "\n " ) } " )
99+ # Continue with unresolved values - they will be stringified but at least won't crash
100+ end
101+ end
102+
103+ # Recursively resolve deferred values in a data structure
104+ def manually_resolve_deferred_values ( value )
105+ case value
106+ when Hash
107+ resolved_hash = { }
108+ value . each do |k , v |
109+ resolved_key = manually_resolve_deferred_values ( k )
110+ resolved_value = manually_resolve_deferred_values ( v )
111+ resolved_hash [ resolved_key ] = resolved_value
112+ end
113+ resolved_hash
114+ when Array
115+ value . map { |v | manually_resolve_deferred_values ( v ) }
116+ else
117+ # Handle different types of deferred objects
118+ if value . is_a? ( Puppet ::Pops ::Evaluator ::DeferredValue )
119+ # DeferredValue objects have a @proc instance variable we can call
120+ proc = value . instance_variable_get ( :@proc )
121+ return proc . call if proc && proc . respond_to? ( :call )
122+
123+ Puppet . debug ( 'DSC PROVIDER → DeferredValue has no callable proc' )
124+ return value . to_s
125+
126+ elsif value && value . class . name . include? ( 'Deferred' )
127+ # 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' )
137+ return value . to_s
138+ end
139+ end
140+
141+ # Return the value unchanged if it's not deferred
142+ value
143+ end
144+ end
145+
146+ # Check if a value contains any deferred values (recursively)
147+ def contains_deferred_values? ( value )
148+ case value
149+ when Hash
150+ value . any? { |k , v | contains_deferred_values? ( k ) || contains_deferred_values? ( v ) }
151+ when Array
152+ value . any? { |v | contains_deferred_values? ( v ) }
153+ else
154+ # Check if this is a Deferred object or DeferredValue
155+ value && ( value . class . name . include? ( 'Deferred' ) ||
156+ value . is_a? ( Puppet ::Pops ::Evaluator ::DeferredValue ) )
157+ end
158+ end
159+
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+
43246 def dsc_parameters
44247 resource . parameters_with_value . select do |p |
45248 p . name . to_s . include? 'dsc_'
@@ -52,10 +255,6 @@ def dsc_property_param
52255 end
53256 end
54257
55- def self . template_path
56- File . expand_path ( Pathname . new ( __FILE__ ) . dirname )
57- end
58-
59258 def set_timeout
60259 resource [ :dsc_timeout ] ? resource [ :dsc_timeout ] * 1000 : 1_200_000
61260 end
@@ -65,18 +264,32 @@ def ps_manager
65264 Pwsh ::Manager . instance ( command ( :powershell ) , Pwsh ::Manager . powershell_args , debug : debug_output )
66265 end
67266
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+
68276 def exists?
277+ Puppet . notice ( "DSC PROVIDER SENTINEL → #{ __FILE__ } " )
278+
279+ # Resolve deferred values right before we start processing
280+ resolve_deferred_values!
281+
69282 timeout = set_timeout
70283 Puppet . debug "Dsc Timeout: #{ timeout } milliseconds"
71284 version = Facter . value ( :powershell_version )
72285 Puppet . debug "PowerShell Version: #{ version } "
286+
73287 script_content = ps_script_content ( 'test' )
74288 Puppet . debug "\n " + self . class . redact_content ( script_content )
75289
76290 if Pwsh ::Manager . windows_powershell_supported?
77291 output = ps_manager . execute ( script_content , timeout )
78292 raise Puppet ::Error , output [ :errormessage ] if output [ :errormessage ] &.match? ( %r{PowerShell module timeout \( \d + ms\) exceeded while executing} )
79-
80293 output = output [ :stdout ]
81294 else
82295 self . class . upgrade_message
@@ -95,15 +308,20 @@ def exists?
95308 end
96309
97310 def create
311+ Puppet . notice ( "DSC PROVIDER SENTINEL → #{ __FILE__ } " )
312+
313+ # Resolve deferred values right before we start processing
314+ resolve_deferred_values!
315+
98316 timeout = set_timeout
99317 Puppet . debug "Dsc Timeout: #{ timeout } milliseconds"
318+
100319 script_content = ps_script_content ( 'set' )
101320 Puppet . debug "\n " + self . class . redact_content ( script_content )
102321
103322 if Pwsh ::Manager . windows_powershell_supported?
104323 output = ps_manager . execute ( script_content , timeout )
105324 raise Puppet ::Error , output [ :errormessage ] if output [ :errormessage ] &.match? ( %r{PowerShell module timeout \( \d + ms\) exceeded while executing} )
106-
107325 output = output [ :stdout ]
108326 else
109327 self . class . upgrade_message
@@ -114,7 +332,6 @@ def create
114332 data = JSON . parse ( output )
115333
116334 raise ( data [ 'errormessage' ] ) unless data [ 'errormessage' ] . empty?
117-
118335 notify_reboot_pending if data [ 'rebootrequired' ] == true
119336
120337 data
@@ -138,24 +355,6 @@ def notify_reboot_pending
138355 end
139356 end
140357
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-
159358 def ps_script_content ( mode )
160359 self . class . ps_script_content ( mode , resource , self )
161360 end
@@ -165,6 +364,10 @@ def self.ps_script_content(mode, resource, provider)
165364 @param_hash = resource
166365 template_name = resource . generic_dsc ? '/invoke_generic_dsc_resource.ps1.erb' : '/invoke_dsc_resource.ps1.erb'
167366 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+
168371 template = ERB . new ( file . read , trim_mode : '-' )
169372 template . result ( binding )
170373 end
0 commit comments