@@ -40,6 +40,119 @@ def self.vendored_modules_path
4040 File . expand_path ( Pathname . new ( __FILE__ ) . dirname + '../../../' + 'puppet_x/dsc_resources' )
4141 end
4242
43+ def self . template_path
44+ File . expand_path ( Pathname . new ( __FILE__ ) . dirname )
45+ end
46+
47+ def self . format_dsc_lite ( dsc_value )
48+ PuppetX ::PuppetLabs ::DscLite ::PowerShellHashFormatter . format ( dsc_value )
49+ end
50+
51+ def self . escape_quotes ( text )
52+ text . gsub ( "'" , "''" )
53+ end
54+
55+ def self . redact_content ( content )
56+ # Note that here we match after an equals to ensure we redact the value being passed, but not the key.
57+ # This means a redaction of a string not including '= ' before the string value will not redact.
58+ # Every secret unwrapped in this module will unwrap as "'secret' # PuppetSensitive" and, currently,
59+ # always inside a hash table to be passed along. This means we can (currently) expect the value to
60+ # always come after an equals sign.
61+ # Note that the line may include a semi-colon and/or a newline character after the sensitive unwrap.
62+ content . gsub ( %r{= '.+' # PuppetSensitive;?(\\ n)?$} , "= '[REDACTED]'" )
63+ end
64+
65+ # ---------------------------
66+ # Deferred value resolution - NEW APPROACH
67+ # ---------------------------
68+
69+ # Resolve deferred values in properties right before PowerShell script generation
70+ # This is the correct timing - after catalog application starts but before template rendering
71+ def resolve_deferred_values!
72+ return unless resource . parameters . key? ( :properties )
73+
74+ current_properties = resource . parameters [ :properties ] . value
75+ return unless contains_deferred_values? ( current_properties )
76+
77+ Puppet . notice ( 'DSC PROVIDER → Resolving deferred values in properties' )
78+
79+ begin
80+ # Resolve deferred values directly using the properties hash
81+ resolved_properties = manually_resolve_deferred_values ( current_properties )
82+
83+ # Update the resource with resolved properties
84+ resource . parameters [ :properties ] . value = resolved_properties
85+
86+ # Verify resolution worked
87+ if contains_deferred_values? ( resolved_properties )
88+ Puppet . warning ( 'DSC PROVIDER → Some deferred values could not be resolved' )
89+ else
90+ Puppet . notice ( 'DSC PROVIDER → All deferred values resolved successfully' )
91+ end
92+ rescue => e
93+ Puppet . warning ( "DSC PROVIDER → Error resolving deferred values: #{ e . class } : #{ e . message } " )
94+ Puppet . debug ( "DSC PROVIDER → Error backtrace: #{ e . backtrace . join ( "\n " ) } " )
95+ # Continue with unresolved values - they will be stringified but at least won't crash
96+ end
97+ end
98+
99+ # Recursively resolve deferred values in a data structure
100+ def manually_resolve_deferred_values ( value )
101+ case value
102+ when Hash
103+ resolved_hash = { }
104+ value . each do |k , v |
105+ resolved_key = manually_resolve_deferred_values ( k )
106+ resolved_value = manually_resolve_deferred_values ( v )
107+ resolved_hash [ resolved_key ] = resolved_value
108+ end
109+ resolved_hash
110+ when Array
111+ value . map { |v | manually_resolve_deferred_values ( v ) }
112+ else
113+ # Handle different types of deferred objects
114+ if value . is_a? ( Puppet ::Pops ::Evaluator ::DeferredValue )
115+ # DeferredValue objects have a @proc instance variable we can call
116+ proc = value . instance_variable_get ( :@proc )
117+ return proc . call if proc && proc . respond_to? ( :call )
118+
119+ Puppet . debug ( 'DSC PROVIDER → DeferredValue has no callable proc' )
120+ return value . to_s
121+
122+ elsif value && value . class . name . include? ( 'Deferred' )
123+ # For other Deferred types, try standard resolution
124+ if value . respond_to? ( :name )
125+ begin
126+ return Puppet ::Pops ::Evaluator ::DeferredResolver . resolve ( value . name , nil , { } )
127+ rescue => e
128+ Puppet . debug ( "DSC PROVIDER → Failed to resolve Deferred object: #{ e . message } " )
129+ return value . to_s
130+ end
131+ else
132+ Puppet . debug ( 'DSC PROVIDER → Deferred object has no name method' )
133+ return value . to_s
134+ end
135+ end
136+
137+ # Return the value unchanged if it's not deferred
138+ value
139+ end
140+ end
141+
142+ # Check if a value contains any deferred values (recursively)
143+ def contains_deferred_values? ( value )
144+ case value
145+ when Hash
146+ value . any? { |k , v | contains_deferred_values? ( k ) || contains_deferred_values? ( v ) }
147+ when Array
148+ value . any? { |v | contains_deferred_values? ( v ) }
149+ else
150+ # Check if this is a Deferred object or DeferredValue
151+ value && ( value . class . name . include? ( 'Deferred' ) ||
152+ value . is_a? ( Puppet ::Pops ::Evaluator ::DeferredValue ) )
153+ end
154+ end
155+
43156 def dsc_parameters
44157 resource . parameters_with_value . select do |p |
45158 p . name . to_s . include? 'dsc_'
@@ -52,10 +165,6 @@ def dsc_property_param
52165 end
53166 end
54167
55- def self . template_path
56- File . expand_path ( Pathname . new ( __FILE__ ) . dirname )
57- end
58-
59168 def set_timeout
60169 resource [ :dsc_timeout ] ? resource [ :dsc_timeout ] * 1000 : 1_200_000
61170 end
@@ -65,18 +174,26 @@ def ps_manager
65174 Pwsh ::Manager . instance ( command ( :powershell ) , Pwsh ::Manager . powershell_args , debug : debug_output )
66175 end
67176
177+ # Keep for ERBs that call provider.format_for_ps(...)
178+ def format_for_ps ( value )
179+ self . class . format_dsc_lite ( value )
180+ end
181+
68182 def exists?
183+ # Resolve deferred values right before we start processing
184+ resolve_deferred_values!
185+
69186 timeout = set_timeout
70187 Puppet . debug "Dsc Timeout: #{ timeout } milliseconds"
71188 version = Facter . value ( :powershell_version )
72189 Puppet . debug "PowerShell Version: #{ version } "
190+
73191 script_content = ps_script_content ( 'test' )
74192 Puppet . debug "\n " + self . class . redact_content ( script_content )
75193
76194 if Pwsh ::Manager . windows_powershell_supported?
77195 output = ps_manager . execute ( script_content , timeout )
78196 raise Puppet ::Error , output [ :errormessage ] if output [ :errormessage ] &.match? ( %r{PowerShell module timeout \( \d + ms\) exceeded while executing} )
79-
80197 output = output [ :stdout ]
81198 else
82199 self . class . upgrade_message
@@ -95,15 +212,18 @@ def exists?
95212 end
96213
97214 def create
215+ # Resolve deferred values right before we start processing
216+ resolve_deferred_values!
217+
98218 timeout = set_timeout
99219 Puppet . debug "Dsc Timeout: #{ timeout } milliseconds"
220+
100221 script_content = ps_script_content ( 'set' )
101222 Puppet . debug "\n " + self . class . redact_content ( script_content )
102223
103224 if Pwsh ::Manager . windows_powershell_supported?
104225 output = ps_manager . execute ( script_content , timeout )
105226 raise Puppet ::Error , output [ :errormessage ] if output [ :errormessage ] &.match? ( %r{PowerShell module timeout \( \d + ms\) exceeded while executing} )
106-
107227 output = output [ :stdout ]
108228 else
109229 self . class . upgrade_message
@@ -114,7 +234,6 @@ def create
114234 data = JSON . parse ( output )
115235
116236 raise ( data [ 'errormessage' ] ) unless data [ 'errormessage' ] . empty?
117-
118237 notify_reboot_pending if data [ 'rebootrequired' ] == true
119238
120239 data
@@ -138,24 +257,6 @@ def notify_reboot_pending
138257 end
139258 end
140259
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-
159260 def ps_script_content ( mode )
160261 self . class . ps_script_content ( mode , resource , self )
161262 end
@@ -165,6 +266,10 @@ def self.ps_script_content(mode, resource, provider)
165266 @param_hash = resource
166267 template_name = resource . generic_dsc ? '/invoke_generic_dsc_resource.ps1.erb' : '/invoke_dsc_resource.ps1.erb'
167268 file = File . new ( template_path + template_name , encoding : Encoding ::UTF_8 )
269+
270+ # Make vendored_modules_path visible in ERB if the template uses it
271+ vendored_modules_path = self . vendored_modules_path
272+
168273 template = ERB . new ( file . read , trim_mode : '-' )
169274 template . result ( binding )
170275 end
0 commit comments