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'
59require 'puppet/pops/evaluator/deferred_resolver'
610require_relative '../../../puppet_x/puppetlabs/dsc_lite/powershell_hash_formatter'
711
@@ -41,9 +45,13 @@ def self.vendored_modules_path
4145 File . expand_path ( Pathname . new ( __FILE__ ) . dirname + '../../../' + 'puppet_x/dsc_resources' )
4246 end
4347
48+ # ---------- Resolution helpers (Option 2) ----------
49+
50+ # 1) Catalog-wide resolve: replace all Deferreds/futures in the catalog
4451 def force_resolve_catalog_deferred!
4552 cat = resource &.catalog
4653 return unless cat
54+
4755 facts = Puppet . lookup ( :facts ) { nil }
4856 env = if cat . respond_to? ( :environment_instance )
4957 cat . environment_instance
@@ -58,6 +66,59 @@ def force_resolve_catalog_deferred!
5866 end
5967 end
6068
69+ # Build a compiler on the agent for local resolution (ScriptCompiler on Puppet 8)
70+ def build_agent_compiler ( env )
71+ node_name = Puppet [ :node_name_value ]
72+ node = Puppet ::Node . new ( node_name , environment : env )
73+
74+ # If you can attach facts, do so—it can influence function behavior during resolve
75+ begin
76+ facts = Puppet . lookup ( :facts ) { nil }
77+ node . add_facts ( facts ) if facts
78+ rescue => e
79+ Puppet . debug ( "DSC_lite: could not attach facts to node for local resolve: #{ e . class } : #{ e . message } " )
80+ end
81+
82+ if defined? ( Puppet ::Parser ::ScriptCompiler )
83+ Puppet ::Parser ::ScriptCompiler . new ( node , env )
84+ else
85+ Puppet ::Parser ::Compiler . new ( node )
86+ end
87+ end
88+
89+ # 2) Targeted resolve: explicitly resolve the :properties value using a resolver that
90+ # can handle both Deferred and evaluator futures, then write it back to the resource.
91+ def force_resolve_properties!
92+ return unless resource . parameters . key? ( :properties )
93+
94+ cat = resource &.catalog
95+ env = if cat &.respond_to? ( :environment_instance )
96+ cat . environment_instance
97+ else
98+ Puppet . lookup ( :current_environment ) { nil }
99+ end
100+
101+ begin
102+ compiler = build_agent_compiler ( env ) if env
103+ return unless compiler # without a compiler, local resolve can't proceed
104+
105+ facts = Puppet . lookup ( :facts ) { nil }
106+
107+ resolver = Puppet ::Pops ::Evaluator ::DeferredResolver . new ( compiler , true )
108+ resolver . set_facts_variable ( facts ) if facts
109+
110+ props = resource . parameters [ :properties ] . value
111+ resolved = resolver . resolve ( props ) # handles nested Deferred and futures
112+ resource . parameters [ :properties ] . value = resolved
113+
114+ Puppet . debug ( 'DSC_lite: explicitly resolved resource[:properties] on agent' )
115+ rescue => e
116+ Puppet . debug ( "DSC_lite: explicit properties resolve failed: #{ e . class } : #{ e . message } " )
117+ end
118+ end
119+
120+ # ---------- Existing provider helpers ----------
121+
61122 def dsc_parameters
62123 resource . parameters_with_value . select do |p |
63124 p . name . to_s . include? 'dsc_'
@@ -83,20 +144,31 @@ def ps_manager
83144 Pwsh ::Manager . instance ( command ( :powershell ) , Pwsh ::Manager . powershell_args , debug : debug_output )
84145 end
85146
147+ # If your ERBs call provider.format_for_ps(...), keep this minimal helper
148+ def format_for_ps ( value )
149+ self . class . format_dsc_lite ( value )
150+ end
151+
152+ # ---------- Provider operations ----------
153+
86154 def exists?
87155 Puppet . notice ( "DSC PROVIDER SENTINEL → #{ __FILE__ } " )
156+
157+ # Two-stage resolve before we render ERB
88158 force_resolve_catalog_deferred!
159+ force_resolve_properties!
160+
89161 timeout = set_timeout
90162 Puppet . debug "Dsc Timeout: #{ timeout } milliseconds"
91163 version = Facter . value ( :powershell_version )
92164 Puppet . debug "PowerShell Version: #{ version } "
165+
93166 script_content = ps_script_content ( 'test' )
94167 Puppet . debug "\n " + self . class . redact_content ( script_content )
95168
96169 if Pwsh ::Manager . windows_powershell_supported?
97170 output = ps_manager . execute ( script_content , timeout )
98171 raise Puppet ::Error , output [ :errormessage ] if output [ :errormessage ] &.match? ( %r{PowerShell module timeout \( \d + ms\) exceeded while executing} )
99-
100172 output = output [ :stdout ]
101173 else
102174 self . class . upgrade_message
@@ -116,16 +188,20 @@ def exists?
116188
117189 def create
118190 Puppet . notice ( "DSC PROVIDER SENTINEL → #{ __FILE__ } " )
191+
192+ # Two-stage resolve before we render ERB
119193 force_resolve_catalog_deferred!
194+ force_resolve_properties!
195+
120196 timeout = set_timeout
121197 Puppet . debug "Dsc Timeout: #{ timeout } milliseconds"
198+
122199 script_content = ps_script_content ( 'set' )
123200 Puppet . debug "\n " + self . class . redact_content ( script_content )
124201
125202 if Pwsh ::Manager . windows_powershell_supported?
126203 output = ps_manager . execute ( script_content , timeout )
127204 raise Puppet ::Error , output [ :errormessage ] if output [ :errormessage ] &.match? ( %r{PowerShell module timeout \( \d + ms\) exceeded while executing} )
128-
129205 output = output [ :stdout ]
130206 else
131207 self . class . upgrade_message
@@ -136,7 +212,6 @@ def create
136212 data = JSON . parse ( output )
137213
138214 raise ( data [ 'errormessage' ] ) unless data [ 'errormessage' ] . empty?
139-
140215 notify_reboot_pending if data [ 'rebootrequired' ] == true
141216
142217 data
@@ -169,12 +244,7 @@ def self.escape_quotes(text)
169244 end
170245
171246 def self . redact_content ( content )
172- # Note that here we match after an equals to ensure we redact the value being passed, but not the key.
173- # This means a redaction of a string not including '= ' before the string value will not redact.
174- # Every secret unwrapped in this module will unwrap as "'secret' # PuppetSensitive" and, currently,
175- # always inside a hash table to be passed along. This means we can (currently) expect the value to
176- # always come after an equals sign.
177- # Note that the line may include a semi-colon and/or a newline character after the sensitive unwrap.
247+ # NOTE: match after '=' so we redact the value being passed, but not the key
178248 content . gsub ( %r{= '.+' # PuppetSensitive;?(\\ n)?$} , "= '[REDACTED]'" )
179249 end
180250
@@ -187,6 +257,10 @@ def self.ps_script_content(mode, resource, provider)
187257 @param_hash = resource
188258 template_name = resource . generic_dsc ? '/invoke_generic_dsc_resource.ps1.erb' : '/invoke_dsc_resource.ps1.erb'
189259 file = File . new ( template_path + template_name , encoding : Encoding ::UTF_8 )
260+
261+ # Make vendored_modules_path visible in ERB if the template uses it
262+ vendored_modules_path = self . vendored_modules_path
263+
190264 template = ERB . new ( file . read , trim_mode : '-' )
191265 template . result ( binding )
192266 end
0 commit comments