Skip to content

Commit a76f498

Browse files
committed
(PUP-9323) Resolve deferred values at resource evaluation time
Allow deferred values to be resolved lazily as each resource is evaluated. This also means validation, munging and unmunging are delayed until the resolved value is set back on the parameter or property. The deferred evaluation is similar to how Puppet::Type#eval_generate works, but since we're not modifying the graph, we don't need to worry about containment edges, tags, noop, etc.
1 parent 983f652 commit a76f498

File tree

4 files changed

+93
-6
lines changed

4 files changed

+93
-6
lines changed

lib/puppet/parameter.rb

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -415,6 +415,8 @@ 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+
418420
unsafe_unmunge(value)
419421
end
420422

@@ -435,6 +437,8 @@ def unsafe_unmunge(value)
435437
# @return [Object] the munged (internal) value
436438
#
437439
def munge(value)
440+
return value if value.is_a?(Puppet::Pops::Evaluator::DeferredValue)
441+
438442
begin
439443
ret = unsafe_munge(value)
440444
rescue Puppet::Error => detail
@@ -468,6 +472,8 @@ def unsafe_validate(value)
468472
# @api public
469473
#
470474
def validate(value)
475+
return if value.is_a?(Puppet::Pops::Evaluator::DeferredValue)
476+
471477
begin
472478
unsafe_validate(value)
473479
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: 15 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,20 @@ 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+
resource.eachparameter do |param|
472+
if param.value.instance_of?(Puppet::Pops::Evaluator::DeferredValue)
473+
# Puppet::Parameter#value= triggers validation and munging. Puppet::Property#value=
474+
# overrides the method, but also triggers validation and munging, since we're
475+
# setting the desired/should value.
476+
resolved = param.value.resolve
477+
# resource.notice("Resolved deferred value to #{resolved}")
478+
param.value = resolved
479+
end
480+
end
481+
end
467482
end
468483

469484
require_relative 'transaction/report'

spec/unit/pops/evaluator/deferred_resolver_spec.rb

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,4 +17,30 @@
1717

1818
expect(catalog.resource(:notify, 'deferred')[:message]).to eq('1:2:3')
1919
end
20+
21+
it 'lazily resolves deferred values in a catalog' do
22+
catalog = compile_to_catalog(<<~END)
23+
notify { "deferred":
24+
message => Deferred("join", [[1,2,3], ":"])
25+
}
26+
END
27+
described_class.resolve_and_replace(facts, catalog, environment, false)
28+
29+
deferred = catalog.resource(:notify, 'deferred')[:message]
30+
expect(deferred.resolve).to eq('1:2:3')
31+
end
32+
33+
it 'lazily resolves nested deferred values in a catalog' do
34+
catalog = compile_to_catalog(<<~END)
35+
$args = Deferred("inline_epp", ["<%= 'a,b,c' %>"])
36+
notify { "deferred":
37+
message => Deferred("split", [$args, ","])
38+
}
39+
END
40+
described_class.resolve_and_replace(facts, catalog, environment, false)
41+
42+
deferred = catalog.resource(:notify, 'deferred')[:message]
43+
expect(deferred.resolve).to eq(["a", "b", "c"])
44+
end
45+
2046
end

0 commit comments

Comments
 (0)