Skip to content

Commit f65fd99

Browse files
authored
Merge pull request #8902 from joshcooper/deferred
(PUP-9323) Allow deferred functions to adhere to puppet relationships and ordering
2 parents 10eeeb7 + 815c9c7 commit f65fd99

File tree

10 files changed

+269
-16
lines changed

10 files changed

+269
-16
lines changed

lib/puppet/application/apply.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -241,7 +241,7 @@ def main
241241
end
242242

243243
# Resolve all deferred values and replace them / mutate the catalog
244-
Puppet::Pops::Evaluator::DeferredResolver.resolve_and_replace(node.facts, catalog, apply_environment)
244+
Puppet::Pops::Evaluator::DeferredResolver.resolve_and_replace(node.facts, catalog, apply_environment, Puppet[:preprocess_deferred])
245245

246246
# Translate it to a RAL catalog
247247
catalog = catalog.to_ral
@@ -350,7 +350,7 @@ def read_catalog(text)
350350
raise Puppet::Error, _("Could not deserialize catalog from %{format}: %{detail}") % { format: format, detail: detail }, detail.backtrace
351351
end
352352
# Resolve all deferred values and replace them / mutate the catalog
353-
Puppet::Pops::Evaluator::DeferredResolver.resolve_and_replace(node.facts, catalog, configured_environment)
353+
Puppet::Pops::Evaluator::DeferredResolver.resolve_and_replace(node.facts, catalog, configured_environment, Puppet[:preprocess_deferred])
354354

355355
catalog.to_ral
356356
end

lib/puppet/configurer.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,7 @@ def convert_catalog(result, duration, facts, options = {})
112112
catalog_conversion_time = thinmark do
113113
# Will mutate the result and replace all Deferred values with resolved values
114114
if facts
115-
Puppet::Pops::Evaluator::DeferredResolver.resolve_and_replace(facts, result, Puppet.lookup(:current_environment))
115+
Puppet::Pops::Evaluator::DeferredResolver.resolve_and_replace(facts, result, Puppet.lookup(:current_environment), Puppet[:preprocess_deferred])
116116
end
117117

118118
catalog = result.to_ral

lib/puppet/defaults.rb

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2021,6 +2021,16 @@ def self.initialize_default_settings!(settings)
20212021
being evaluated. This allows you to interactively see exactly
20222022
what is being done.",
20232023
},
2024+
:preprocess_deferred => {
2025+
:default => true,
2026+
:type => :boolean,
2027+
:desc => "Whether puppet should call deferred functions before applying
2028+
the catalog. If set to `true`, then all prerequisites needed for the
2029+
deferred function must be satified prior to puppet running. If set to
2030+
`false`, then deferred functions will follow puppet relationships and
2031+
ordering. This allows puppet to install prerequisites needed for a
2032+
deferred function and call the deferred function in the same run."
2033+
},
20242034
:summarize => {
20252035
:default => false,
20262036
:type => :boolean,

lib/puppet/parameter.rb

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -177,15 +177,15 @@ def munge(&block)
177177
end
178178

179179
# @overload unmunge {|| ... }
180-
# Defines an optional method used to convert the parameter value to DSL/string form from an internal form.
180+
# Defines an optional method used to convert the parameter value from internal form to DSL/string form.
181181
# If an `unmunge` method is not defined, the internal form is used.
182182
# @see munge
183-
# @note This adds a method with the name `unmunge` in the created parameter class.
183+
# @note This adds a method with the name `unsafe_unmunge` in the created parameter class.
184184
# @dsl type
185185
# @api public
186186
#
187187
def unmunge(&block)
188-
define_method(:unmunge, &block)
188+
define_method(:unsafe_unmunge, &block)
189189
end
190190

191191
# Sets a marker indicating that this parameter is the _namevar_ (unique identifier) of the type
@@ -415,17 +415,30 @@ def unsafe_munge(value)
415415
# @return [Object] the unmunged value
416416
#
417417
def unmunge(value)
418+
return value if value.is_a?(Puppet::Pops::Evaluator::DeferredValue)
419+
420+
unsafe_unmunge(value)
421+
end
422+
423+
# This is the default implementation of `unmunge` that simply produces the value (if it is valid).
424+
# The DSL method {unmunge} should be used to define an overriding method if unmunging is required.
425+
#
426+
# @api private
427+
#
428+
def unsafe_unmunge(value)
418429
value
419430
end
420431

421-
# Munges the value to internal form.
432+
# Munges the value from DSL form to internal form.
422433
# This implementation of `munge` provides exception handling around the specified munging of this parameter.
423434
# @note This method should not be overridden. Use the DSL method {munge} to define a munging method
424435
# if required.
425436
# @param value [Object] the DSL value to munge
426437
# @return [Object] the munged (internal) value
427438
#
428439
def munge(value)
440+
return value if value.is_a?(Puppet::Pops::Evaluator::DeferredValue)
441+
429442
begin
430443
ret = unsafe_munge(value)
431444
rescue Puppet::Error => detail
@@ -459,6 +472,8 @@ def unsafe_validate(value)
459472
# @api public
460473
#
461474
def validate(value)
475+
return if value.is_a?(Puppet::Pops::Evaluator::DeferredValue)
476+
462477
begin
463478
unsafe_validate(value)
464479
rescue ArgumentError => detail

lib/puppet/pops/evaluator/deferred_resolver.rb

Lines changed: 46 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,16 @@
33
module Puppet::Pops
44
module Evaluator
55

6+
class DeferredValue
7+
def initialize(proc)
8+
@proc = proc
9+
end
10+
11+
def resolve
12+
@proc.call
13+
end
14+
end
15+
616
# Utility class to help resolve instances of Puppet::Pops::Types::PDeferredType::Deferred
717
#
818
class DeferredResolver
@@ -20,9 +30,9 @@ class DeferredResolver
2030
# are to be mixed into the scope
2131
# @return [nil] does not return anything - the catalog is modified as a side effect
2232
#
23-
def self.resolve_and_replace(facts, catalog, environment = catalog.environment_instance)
24-
compiler = Puppet::Parser::ScriptCompiler.new(environment, catalog.name, true)
25-
resolver = new(compiler)
33+
def self.resolve_and_replace(facts, catalog, environment = catalog.environment_instance, preprocess_deferred = true)
34+
compiler = Puppet::Parser::ScriptCompiler.new(environment, catalog.name, preprocess_deferred)
35+
resolver = new(compiler, preprocess_deferred)
2636
resolver.set_facts_variable(facts)
2737
# TODO:
2838
# # When scripting the trusted data are always local, but set them anyway
@@ -53,11 +63,12 @@ def self.resolve(value, compiler)
5363
resolver.resolve(value)
5464
end
5565

56-
def initialize(compiler)
66+
def initialize(compiler, preprocess_deferred = true)
5767
@compiler = compiler
5868
# Always resolve in top scope
5969
@scope = @compiler.topscope
6070
@deferred_class = Puppet::Pops::Types::TypeFactory.deferred.implementation_class
71+
@preprocess_deferred = preprocess_deferred
6172
end
6273

6374
# @param facts [Puppet::Node::Facts] the facts to set in $facts in the compiler's topscope
@@ -106,6 +117,24 @@ def resolve(x)
106117
end
107118
end
108119

120+
def resolve_lazy_args(x)
121+
if x.is_a?(DeferredValue)
122+
x.resolve
123+
elsif x.is_a?(Array)
124+
x.map {|v| resolve_lazy_args(v) }
125+
elsif x.is_a?(Hash)
126+
result = {}
127+
x.each_pair {|k,v| result[k] = resolve_lazy_args(v) }
128+
result
129+
elsif x.is_a?(Puppet::Pops::Types::PSensitiveType::Sensitive)
130+
# rewrap in a new Sensitive after resolving any nested deferred values
131+
Puppet::Pops::Types::PSensitiveType::Sensitive.new(resolve_lazy_args(x.unwrap))
132+
else
133+
x
134+
end
135+
end
136+
private :resolve_lazy_args
137+
109138
def resolve_future(f)
110139
# If any of the arguments to a future is a future it needs to be resolved first
111140
func_name = f.name
@@ -117,8 +146,19 @@ def resolve_future(f)
117146
mapped_arguments.insert(0, @scope[var_name])
118147
end
119148

120-
# call the function (name in deferred, or 'dig' for a variable)
121-
@scope.call_function(func_name, mapped_arguments)
149+
if @preprocess_deferred
150+
# call the function (name in deferred, or 'dig' for a variable)
151+
@scope.call_function(func_name, mapped_arguments)
152+
else
153+
# call the function later
154+
DeferredValue.new(
155+
Proc.new {
156+
# deferred functions can have nested deferred arguments
157+
resolved_arguments = mapped_arguments.map { |arg| resolve_lazy_args(arg) }
158+
@scope.call_function(func_name, resolved_arguments)
159+
}
160+
)
161+
end
122162
end
123163

124164
def map_arguments(args)

lib/puppet/transaction.rb

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -276,6 +276,7 @@ def apply(resource, ancestor = nil)
276276

277277
# Evaluate a single resource.
278278
def eval_resource(resource, ancestor = nil)
279+
resolve_resource(resource)
279280
propagate_failure(resource)
280281
if skip?(resource)
281282
resource_status(resource).skipped = true
@@ -464,6 +465,27 @@ def split_qualified_tags?
464465
public :skip?
465466
public :missing_tags?
466467

468+
def resolve_resource(resource)
469+
return unless catalog.host_config?
470+
471+
deferred_validate = false
472+
473+
resource.eachparameter do |param|
474+
if param.value.instance_of?(Puppet::Pops::Evaluator::DeferredValue)
475+
# Puppet::Parameter#value= triggers validation and munging. Puppet::Property#value=
476+
# overrides the method, but also triggers validation and munging, since we're
477+
# setting the desired/should value.
478+
resolved = param.value.resolve
479+
# resource.notice("Resolved deferred value to #{resolved}")
480+
param.value = resolved
481+
deferred_validate = true
482+
end
483+
end
484+
485+
if deferred_validate
486+
resource.validate_resource
487+
end
488+
end
467489
end
468490

469491
require_relative 'transaction/report'

lib/puppet/type.rb

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2282,7 +2282,13 @@ def self.to_s
22822282
# @api public
22832283
#
22842284
def self.validate(&block)
2285-
define_method(:validate, &block)
2285+
define_method(:unsafe_validate, &block)
2286+
2287+
define_method(:validate) do
2288+
return if enum_for(:eachparameter).any? { |p| p.value.instance_of?(Puppet::Pops::Evaluator::DeferredValue) }
2289+
2290+
unsafe_validate
2291+
end
22862292
end
22872293

22882294
# @return [String] The file from which this type originates from
@@ -2372,15 +2378,26 @@ def initialize(resource)
23722378

23732379
set_parameters(@original_parameters)
23742380

2381+
validate_resource
2382+
2383+
set_sensitive_parameters(resource.sensitive_parameters)
2384+
end
2385+
2386+
# Optionally validate the resource. This method is a noop if the type has not defined
2387+
# a `validate` method using the puppet DSL. If validation fails, then an exception will
2388+
# be raised with this resources as the context.
2389+
#
2390+
# @api public
2391+
#
2392+
# @return [void]
2393+
def validate_resource
23752394
begin
23762395
self.validate if self.respond_to?(:validate)
23772396
rescue Puppet::Error, ArgumentError => detail
23782397
error = Puppet::ResourceError.new("Validation of #{ref} failed: #{detail}")
23792398
adderrorcontext(error, detail)
23802399
raise error
23812400
end
2382-
2383-
set_sensitive_parameters(resource.sensitive_parameters)
23842401
end
23852402

23862403
protected

spec/integration/application/agent_spec.rb

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,18 @@
9898
end
9999

100100
context 'rich data' do
101+
let(:deferred_file) { tmpfile('deferred') }
102+
let(:deferred_manifest) do <<~END
103+
file { '#{deferred_file}':
104+
ensure => file,
105+
content => '123',
106+
} ->
107+
notify { 'deferred':
108+
message => Deferred('binary_file', ['#{deferred_file}'])
109+
}
110+
END
111+
end
112+
101113
it "calls a deferred 4x function" do
102114
catalog_handler = -> (req, res) {
103115
catalog = compile_to_catalog(<<-MANIFEST, node)
@@ -142,6 +154,43 @@
142154
end
143155
end
144156

157+
it "fails to apply a deferred function with an unsatified prerequisite" do
158+
catalog_handler = -> (req, res) {
159+
catalog = compile_to_catalog(deferred_manifest, node)
160+
res.body = formatter.render(catalog)
161+
res['Content-Type'] = formatter.mime
162+
}
163+
164+
server.start_server(mounts: {catalog: catalog_handler}) do |port|
165+
Puppet[:serverport] = port
166+
expect {
167+
agent.command_line.args << '--test'
168+
agent.run
169+
}.to exit_with(1)
170+
.and output(%r{Using environment}).to_stdout
171+
.and output(%r{The given file '#{deferred_file}' does not exist}).to_stderr
172+
end
173+
end
174+
175+
it "applies a deferred function and its prerequisite in the same run" do
176+
Puppet[:preprocess_deferred] = false
177+
178+
catalog_handler = -> (req, res) {
179+
catalog = compile_to_catalog(deferred_manifest, node)
180+
res.body = formatter.render(catalog)
181+
res['Content-Type'] = formatter.mime
182+
}
183+
184+
server.start_server(mounts: {catalog: catalog_handler}) do |port|
185+
Puppet[:serverport] = port
186+
expect {
187+
agent.command_line.args << '--test'
188+
agent.run
189+
}.to exit_with(2)
190+
.and output(%r{defined 'message' as Binary\("MTIz"\)}).to_stdout
191+
end
192+
end
193+
145194
it "re-evaluates a deferred function in a cached catalog" do
146195
Puppet[:report] = false
147196
Puppet[:use_cached_catalog] = true

0 commit comments

Comments
 (0)