Skip to content

Commit 8b4db04

Browse files
committed
adjust type to account for deferred
1 parent 506e29e commit 8b4db04

File tree

3 files changed

+236
-26
lines changed

3 files changed

+236
-26
lines changed

lib/puppet/provider/base_dsc_lite/powershell.rb

Lines changed: 228 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,11 @@
22

33
require 'pathname'
44
require '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'
510
require_relative '../../../puppet_x/puppetlabs/dsc_lite/powershell_hash_formatter'
611

712
Puppet::Type.type(:base_dsc_lite).provide(:powershell) do
@@ -31,6 +36,10 @@
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

lib/puppet/type/dsc.rb

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# frozen_string_literal: true
22

33
require 'pathname'
4+
require 'puppet/pops/evaluator/deferred_resolver' # ← add: resolver API
45

56
Puppet::Type.newtype(:dsc) do
67
desc <<-DOC
@@ -17,6 +18,7 @@ module => 'PSDesiredStateConfiguration',
1718
}
1819
}
1920
DOC
21+
2022
require Pathname.new(__FILE__).dirname + '../../' + 'puppet/type/base_dsc_lite'
2123
require Pathname.new(__FILE__).dirname + '../../puppet_x/puppetlabs/dsc_lite/dsc_type_helpers'
2224

@@ -105,6 +107,11 @@ def change_to_s(currentvalue, newvalue)
105107
end
106108

107109
munge do |value|
110+
# Don't try to resolve deferred values during munge - they should be resolved
111+
# on the agent during catalog application, not during catalog compilation.
112+
# Just pass them through to the provider for proper resolution timing.
113+
Puppet.notice("DSC TYPE SENTINEL → #{__FILE__} - letting deferred values pass through")
114+
108115
PuppetX::DscLite::TypeHelpers.munge_sensitive_hash(value)
109116
end
110117
end

spec/acceptance/deferred_spec.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ def read_win_file_if_exists(path)
7272
# NEW 02c: inline Deferred on the DSC property (no variable intermediary)
7373
it '02c: passing a Deferred inline while calling an epp' do
7474
apply = apply_manifest(dsc_deferred_epp_inline)
75-
out = read_win_file_if_exists('C:/Temp/from_dsc_inline.txt')
75+
out = read_win_file_if_exists('C:/Temp/from_dsc.txt')
7676
content = out[:content].strip
7777
if out[:exists] && content == 'hello-dsc-epp'
7878
expect(true).to be(true)

0 commit comments

Comments
 (0)