Skip to content

Commit a62937a

Browse files
committed
patch
1 parent 2aca2e9 commit a62937a

File tree

3 files changed

+133
-40
lines changed

3 files changed

+133
-40
lines changed

lib/puppet/provider/base_dsc_lite/invoke_dsc_resource.ps1.erb

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,11 +36,13 @@ $invokeParams = @{
3636
if name == 'ensure' && dsc_invoke_method == 'test'
3737
value = "\'#{resource.parameters[:ensure].default.to_s}\'"
3838
elsif p.mof_type == 'MSFT_Credential'
39-
value = "[PSCustomObject]#{format_dsc_value(p.value)} | new-pscredential"
39+
# Route via provider helper so last-chance Deferred resolution can run on agent
40+
value = "[PSCustomObject]#{provider.format_for_ps(p.value)} | new-pscredential"
4041
elsif p.mof_is_embedded? && p.mof_type != 'MSFT_KeyValuePair'
4142
vals = p.value.is_a?(Hash) ? [p.value] : p.value
4243
vals = vals.collect do |v|
43-
"(New-CimInstance -ClassName '#{p.mof_type.gsub('[]','')}' -ClientOnly -Property #{format_dsc_value(v)})"
44+
# Route embedded instance Property hashtable via provider formatter (Deferred-safe)
45+
"(New-CimInstance -ClassName '#{p.mof_type.gsub('[]','')}' -ClientOnly -Property #{provider.format_for_ps(v)})"
4446
end
4547
# Ensure that we pass a single CimInstance or array correctly based on MOF schema definition
4648
if p.value.is_a?(Hash)
@@ -49,7 +51,8 @@ $invokeParams = @{
4951
value = "[CimInstance[]]@(#{vals.join(',')})"
5052
end
5153
else
52-
value = format_dsc_value(p.value)
54+
# Default path: format via provider helper (Deferred-safe)
55+
value = provider.format_for_ps(p.value)
5356
end
5457
-%>
5558
<%= name %> = <%= value %>

lib/puppet/provider/base_dsc_lite/invoke_generic_dsc_resource.ps1.erb

Lines changed: 6 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -22,18 +22,12 @@ $response = @{
2222
}
2323

2424
$invokeParams = @{
25-
Name = '<%= resource.parameters[:resource_name].value %>'
26-
ModuleName = <% if resource.parameters[:module].value.is_a?(Hash) -%>
27-
@{
28-
modulename = '<%= resource.parameters[:module].value['name'] %>'
29-
moduleversion = '<%= resource.parameters[:module].value['version'] %>'
30-
}
31-
<% else -%>
32-
'<%= resource.parameters[:module].value %>'
33-
<% end -%>
34-
Method = '<%= dsc_invoke_method %>'
35-
Property = <% provider.dsc_property_param.each do |p| -%>
36-
<%= format_dsc_lite(p.value) %>
25+
# Route values through provider helper so last-chance Deferred resolution can run on agent
26+
Name = <%= provider.format_for_ps(resource.parameters[:resource_name].value) %>
27+
ModuleName = <%= provider.format_for_ps(resource.parameters[:module].value) %>
28+
Method = <%= provider.format_for_ps(dsc_invoke_method) %>
29+
Property = <% provider.dsc_property_param.each do |p| -%>
30+
<%= provider.format_for_ps(p.value) %>
3731
<% end -%>
3832
}
3933

lib/puppet/provider/base_dsc_lite/powershell.rb

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

33
require 'pathname'
44
require 'json'
5+
require 'erb' # ensure ERB is available
56
require_relative '../../../puppet_x/puppetlabs/dsc_lite/powershell_hash_formatter'
67

78
Puppet::Type.type(:base_dsc_lite).provide(:powershell) do
@@ -31,6 +32,10 @@
3132
Puppet (including 3.x), or to a Puppet version newer than 3.x.
3233
UPGRADE
3334

35+
# ---------------------------
36+
# Class-level helpers
37+
# ---------------------------
38+
3439
def self.upgrade_message
3540
Puppet.warning DSC_LITE_MODULE_PUPPET_UPGRADE_MSG unless @upgrade_warning_issued
3641
@upgrade_warning_issued = true
@@ -40,22 +45,48 @@ def self.vendored_modules_path
4045
File.expand_path(Pathname.new(__FILE__).dirname + '../../../' + 'puppet_x/dsc_resources')
4146
end
4247

48+
def self.template_path
49+
File.expand_path(Pathname.new(__FILE__).dirname)
50+
end
51+
52+
def self.format_dsc_lite(dsc_value)
53+
# Keep this PURE to avoid breaking unit tests:
54+
# it should not try to resolve Deferred values.
55+
PuppetX::PuppetLabs::DscLite::PowerShellHashFormatter.format(dsc_value)
56+
end
57+
58+
def self.escape_quotes(text)
59+
text.gsub("'", "''")
60+
end
61+
62+
def self.redact_content(content)
63+
# Note that here we match after an equals to ensure we redact the value being passed, but not the key.
64+
# This means a redaction of a string not including '= ' before the string value will not redact.
65+
# Every secret unwrapped in this module will unwrap as "'secret' # PuppetSensitive" and, currently,
66+
# always inside a hash table to be passed along. This means we can (currently) expect the value to
67+
# always come after an equals sign.
68+
# Note that the line may include a semi-colon and/or a newline character after the sensitive unwrap.
69+
content.gsub(%r{= '.+' # PuppetSensitive;?(\\n)?$}, "= '[REDACTED]'")
70+
end
71+
72+
# ---------------------------
73+
# Instance-level helpers
74+
# ---------------------------
75+
76+
# Return only dsc_* parameters that have a value
4377
def dsc_parameters
4478
resource.parameters_with_value.select do |p|
4579
p.name.to_s.include? 'dsc_'
4680
end
4781
end
4882

83+
# Return the :properties parameter (if provided)
4984
def dsc_property_param
5085
resource.parameters_with_value.select { |pr| pr.name == :properties }.each do |p|
5186
p.name.to_s.include? 'dsc_'
5287
end
5388
end
5489

55-
def self.template_path
56-
File.expand_path(Pathname.new(__FILE__).dirname)
57-
end
58-
5990
def set_timeout
6091
resource[:dsc_timeout] ? resource[:dsc_timeout] * 1000 : 1_200_000
6192
end
@@ -65,11 +96,83 @@ def ps_manager
6596
Pwsh::Manager.instance(command(:powershell), Pwsh::Manager.powershell_args, debug: debug_output)
6697
end
6798

99+
# --- NEW: deep scan for Deferred ---
100+
# Recursively detect any object whose class name suggests it's a Puppet Deferred
101+
def deep_contains_deferred?(obj)
102+
case obj
103+
when Hash
104+
obj.any? { |k, v| deep_contains_deferred?(k) || deep_contains_deferred?(v) }
105+
when Array
106+
obj.any? { |v| deep_contains_deferred?(v) }
107+
else
108+
obj && obj.class && obj.class.name.to_s.include?('Deferred')
109+
end
110+
end
111+
112+
# --- NEW: best-effort resolution hook ---
113+
# Attempt to resolve any remaining Deferreds before rendering ERB.
114+
# This is intentionally defensive: it tries available agent-side options
115+
# and degrades to no-op if none are present.
116+
def ensure_deferreds_resolved!
117+
# 1) Catalog-wide resolve (if the catalog supports it)
118+
begin
119+
cat = resource&.catalog
120+
if cat && cat.respond_to?(:resolve_and_replace)
121+
cat.resolve_and_replace
122+
Puppet.debug('DSC_lite: called catalog.resolve_and_replace for last-chance Deferred resolution')
123+
end
124+
rescue StandardError => e
125+
Puppet.debug("DSC_lite: resolve_and_replace raised #{e.class}: #{e.message}")
126+
end
127+
128+
# 2) Compiler lookup (often nil in agent/provider context)
129+
begin
130+
compiler = Puppet.respond_to?(:lookup) ? Puppet.lookup(:compiler) { nil } : nil
131+
Puppet.debug('DSC_lite: compiler available for resolution') if compiler
132+
rescue StandardError => e
133+
Puppet.debug("DSC_lite: compiler lookup failed: #{e.class}: #{e.message}")
134+
end
135+
136+
# 3) Nothing else to do safely here—actual evaluation requires scope,
137+
# which agent providers typically don't have. We rely on (1), and if
138+
# it still leaks we'll detect/log just before formatting.
139+
nil
140+
end
141+
142+
# --- NEW: provider helper used by ERB ---
143+
# Route every formatted value through here so we can:
144+
# - perform a last-chance Deferred resolution
145+
# - then delegate to the pure formatter
146+
def format_for_ps(value)
147+
if deep_contains_deferred?(value)
148+
Puppet.debug('DSC_lite: Deferred detected in ERB-bound value; attempting last-chance resolution')
149+
ensure_deferreds_resolved!
150+
if deep_contains_deferred?(value)
151+
Puppet.debug('DSC_lite: value still contains Deferred after last-chance resolution')
152+
# Optional: uncomment to fail fast with a clearer message instead of formatter type error
153+
# raise Puppet::Error, 'DSC_lite: Deferred value reached ERB formatting; '\
154+
# 'agent invoked provider before deferrals completed (Puppet 8 timing).'
155+
end
156+
end
157+
self.class.format_dsc_lite(value)
158+
end
159+
# ---------------------------
160+
# Provider operations
161+
# ---------------------------
162+
68163
def exists?
69164
timeout = set_timeout
70165
Puppet.debug "Dsc Timeout: #{timeout} milliseconds"
71166
version = Facter.value(:powershell_version)
72167
Puppet.debug "PowerShell Version: #{version}"
168+
169+
# Last-chance resolve + pinpoint diagnostic BEFORE rendering
170+
ensure_deferreds_resolved!
171+
leaks = resource.parameters_with_value
172+
.select { |p| deep_contains_deferred?(p.value) }
173+
.map { |p| "#{p.name}=#{p.value.class}" }
174+
Puppet.debug("DSC_lite: still deferred after resolution? #{leaks.join(', ')}")
175+
73176
script_content = ps_script_content('test')
74177
Puppet.debug "\n" + self.class.redact_content(script_content)
75178

@@ -97,6 +200,14 @@ def exists?
97200
def create
98201
timeout = set_timeout
99202
Puppet.debug "Dsc Timeout: #{timeout} milliseconds"
203+
204+
# Last-chance resolve + pinpoint diagnostic BEFORE rendering
205+
ensure_deferreds_resolved!
206+
leaks = resource.parameters_with_value
207+
.select { |p| deep_contains_deferred?(p.value) }
208+
.map { |p| "#{p.name}=#{p.value.class}" }
209+
Puppet.debug("DSC_lite: still deferred after resolution? #{leaks.join(', ')}")
210+
100211
script_content = ps_script_content('set')
101212
Puppet.debug "\n" + self.class.redact_content(script_content)
102213

@@ -116,7 +227,6 @@ def create
116227
raise(data['errormessage']) unless data['errormessage'].empty?
117228

118229
notify_reboot_pending if data['rebootrequired'] == true
119-
120230
data
121231
end
122232

@@ -138,34 +248,20 @@ def notify_reboot_pending
138248
end
139249
end
140250

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-
159251
def ps_script_content(mode)
160252
self.class.ps_script_content(mode, resource, self)
161253
end
162-
254+
163255
def self.ps_script_content(mode, resource, provider)
164256
dsc_invoke_method = mode
165257
@param_hash = resource
166258
template_name = resource.generic_dsc ? '/invoke_generic_dsc_resource.ps1.erb' : '/invoke_dsc_resource.ps1.erb'
167259
file = File.new(template_path + template_name, encoding: Encoding::UTF_8)
260+
261+
# Provide explicit local for ERB if templates refer to it
262+
vendored_modules_path = self.vendored_modules_path
263+
168264
template = ERB.new(file.read, trim_mode: '-')
169-
template.result(binding)
265+
template.result(binding) # binding includes: resource, provider, dsc_invoke_method, vendored_modules_path
170266
end
171267
end

0 commit comments

Comments
 (0)