33require 'pathname'
44require 'json'
55require 'erb' # ensure ERB is available
6+ require 'puppet'
7+ require 'puppet/node'
8+ require 'puppet/parser/script_compiler'
69require 'puppet/pops/evaluator/deferred_resolver'
710require_relative '../../../puppet_x/puppetlabs/dsc_lite/powershell_hash_formatter'
811
@@ -84,22 +87,56 @@ def ps_manager
8487 Pwsh ::Manager . instance ( command ( :powershell ) , Pwsh ::Manager . powershell_args , debug : debug_output )
8588 end
8689
87- # Minimal provider-side formatter for ERB (keeps patched templates working)
90+ # Minimal provider-side formatter used by ERB
8891 def format_for_ps ( value )
8992 self . class . format_dsc_lite ( value )
9093 end
9194
92- # ---- Option 1: force-resolve all Deferreds in the catalog before rendering ----
95+ # --------- Two-stage resolver ---------
96+
97+ # Build an environment instance suitable for compilation on the agent
98+ def current_environment_instance
99+ # Prefer the catalog's environment_instance if present
100+ env = resource &.catalog &.respond_to? ( :environment_instance ) ? resource . catalog . environment_instance : nil
101+ return env if env
102+
103+ # Fallback to the current environment from the context
104+ begin
105+ Puppet . lookup ( :current_environment ) { nil }
106+ rescue StandardError
107+ nil
108+ end
109+ end
110+
111+ # Build Facts for the resolver. It accepts Puppet::Node::Facts or nil.
112+ def build_facts_for_resolver ( env )
113+ # Best effort: use the looked-up facts if present
114+ facts = begin
115+ Puppet . lookup ( :facts ) { nil }
116+ rescue
117+ nil
118+ end
119+ return facts if facts
120+
121+ # Fallback: construct from Facter.to_hash for this node
122+ begin
123+ node_name = Puppet [ :node_name_value ]
124+ fact_hash = Facter . to_hash
125+ Puppet ::Node ::Facts . new ( node_name , fact_hash , environment : env &.name )
126+ rescue StandardError => e
127+ Puppet . debug ( "DSC_lite: unable to construct facts for resolver: #{ e . class } : #{ e . message } " )
128+ nil
129+ end
130+ end
131+
132+ # Stage 1: Resolve all deferreds in the entire catalog (direct and nested)
133+ # using Puppet's public API (used by Bolt/pxp-agent). See docs. [1](https://www.rubydoc.info/gems/puppet/8.4.0/Puppet/Pops/Evaluator/DeferredResolver)
93134 def force_resolve_catalog_deferred!
94135 cat = resource &.catalog
95136 return unless cat
96137
97- facts = Puppet . lookup ( :facts ) { nil }
98- env = if cat . respond_to? ( :environment_instance )
99- cat . environment_instance
100- else
101- Puppet . lookup ( :current_environment ) { nil }
102- end
138+ env = current_environment_instance
139+ facts = build_facts_for_resolver ( env )
103140
104141 begin
105142 Puppet ::Pops ::Evaluator ::DeferredResolver . resolve_and_replace ( facts , cat , env , true )
@@ -109,6 +146,51 @@ def force_resolve_catalog_deferred!
109146 end
110147 end
111148
149+ # Stage 2: If :properties still contains a Deferred (nested), resolve that
150+ # hash locally by spinning up a temporary ScriptCompiler and replace it back.
151+ # Uses DeferredResolver.resolve(value, compiler). [1](https://www.rubydoc.info/gems/puppet/8.4.0/Puppet/Pops/Evaluator/DeferredResolver)
152+ def resolve_properties_locally_if_needed!
153+ return unless resource . parameters . key? ( :properties )
154+
155+ props = resource [ :properties ]
156+ return unless contains_deferred? ( props )
157+
158+ begin
159+ env = current_environment_instance || Puppet ::Node ::Environment . create ( 'production' , [ ] )
160+ node_name = Puppet [ :node_name_value ]
161+
162+ node = Puppet ::Node . new ( node_name , environment : env )
163+
164+ # Attach facts if available (improves function behavior during resolution)
165+ begin
166+ facts = build_facts_for_resolver ( env )
167+ node . add_facts ( facts ) if facts
168+ rescue StandardError => e
169+ Puppet . debug ( "DSC_lite: failed to attach facts to node: #{ e . class } : #{ e . message } " )
170+ end
171+
172+ compiler = Puppet ::Parser ::ScriptCompiler . new ( node )
173+
174+ resolved = Puppet ::Pops ::Evaluator ::DeferredResolver . resolve ( props , compiler )
175+ resource [ :properties ] = resolved # write resolved value back into the resource
176+ Puppet . debug ( 'DSC_lite: locally resolved :properties via ScriptCompiler' )
177+ rescue StandardError => e
178+ Puppet . debug ( "DSC_lite: local properties resolution failed: #{ e . class } : #{ e . message } " )
179+ end
180+ end
181+
182+ # Lightweight check for nested Deferred presence
183+ def contains_deferred? ( obj )
184+ case obj
185+ when Hash
186+ obj . any? { |k , v | contains_deferred? ( k ) || contains_deferred? ( v ) }
187+ when Array
188+ obj . any? { |v | contains_deferred? ( v ) }
189+ else
190+ obj && obj . class && obj . class . name . to_s . include? ( 'Deferred' )
191+ end
192+ end
193+
112194 # ---------- provider operations ----------
113195
114196 def exists?
@@ -117,8 +199,9 @@ def exists?
117199 version = Facter . value ( :powershell_version )
118200 Puppet . debug "PowerShell Version: #{ version } "
119201
120- # Ensure all Deferreds (including nested) are resolved prior to ERB rendering
202+ # Two-stage resolution before building the PowerShell script
121203 force_resolve_catalog_deferred!
204+ resolve_properties_locally_if_needed!
122205
123206 script_content = ps_script_content ( 'test' )
124207 Puppet . debug "\n " + self . class . redact_content ( script_content )
@@ -148,8 +231,9 @@ def create
148231 timeout = set_timeout
149232 Puppet . debug "Dsc Timeout: #{ timeout } milliseconds"
150233
151- # Ensure all Deferreds (including nested) are resolved prior to ERB rendering
234+ # Two-stage resolution before building the PowerShell script
152235 force_resolve_catalog_deferred!
236+ resolve_properties_locally_if_needed!
153237
154238 script_content = ps_script_content ( 'set' )
155239 Puppet . debug "\n " + self . class . redact_content ( script_content )
0 commit comments