Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Parse deferred templates non deferred first #1425

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 34 additions & 8 deletions REFERENCE.md
Original file line number Diff line number Diff line change
Expand Up @@ -3173,18 +3173,38 @@ Type: Puppet Language
This function returns either a rendered template or a deferred function to render at runtime.
If any of the values in the variables hash are deferred, then the template will be deferred.

Note: this function requires all parameters to be explicitly passed in. It cannot expect to
use facts, class variables, and other variables in scope. This is because when deferred, we
have to explicitly pass the entire scope to the client.

#### `stdlib::deferrable_epp(String $template, Hash $variables)`
Note: In the case where at least some of the values are deferred and preparse is `true` the template
is parsed twice:
The first parse will evalute any parameters in the template that do not have deferred values.
The second parse will run deferred and evaluate only the remaining deferred parameters. Consequently
any parameters to be deferred must accept a String[1] in original template so as to accept the value
"<%= $variable_with_deferred_value %>" on the first parse.

@param template template location - identical to epp function template location.
@param variables parameters to pass into the template - some of which may have deferred values.
@param preparse
If `true` the epp template will be parsed twice, once normally and then a second time deferred.
It may be nescessary to set `preparse` `false` when deferred values are somethig other than
a string

#### `stdlib::deferrable_epp(String $template, Hash $variables, Boolean $preparse = true)`

This function returns either a rendered template or a deferred function to render at runtime.
If any of the values in the variables hash are deferred, then the template will be deferred.

Note: this function requires all parameters to be explicitly passed in. It cannot expect to
use facts, class variables, and other variables in scope. This is because when deferred, we
have to explicitly pass the entire scope to the client.
Note: In the case where at least some of the values are deferred and preparse is `true` the template
is parsed twice:
The first parse will evalute any parameters in the template that do not have deferred values.
The second parse will run deferred and evaluate only the remaining deferred parameters. Consequently
any parameters to be deferred must accept a String[1] in original template so as to accept the value
"<%= $variable_with_deferred_value %>" on the first parse.

@param template template location - identical to epp function template location.
@param variables parameters to pass into the template - some of which may have deferred values.
@param preparse
If `true` the epp template will be parsed twice, once normally and then a second time deferred.
It may be nescessary to set `preparse` `false` when deferred values are somethig other than
a string

Returns: `Variant[String, Sensitive[String], Deferred]`

Expand All @@ -3200,6 +3220,12 @@ Data type: `Hash`



##### `preparse`

Data type: `Boolean`



### <a name="stdlib--end_with"></a>`stdlib::end_with`

Type: Ruby 4.x API
Expand Down
34 changes: 29 additions & 5 deletions functions/deferrable_epp.pp
Original file line number Diff line number Diff line change
@@ -1,15 +1,39 @@
# This function returns either a rendered template or a deferred function to render at runtime.
# If any of the values in the variables hash are deferred, then the template will be deferred.
#
# Note: this function requires all parameters to be explicitly passed in. It cannot expect to
# use facts, class variables, and other variables in scope. This is because when deferred, we
# have to explicitly pass the entire scope to the client.
# Note: In the case where at least some of the values are deferred and preparse is `true` the template
# is parsed twice:
# The first parse will evalute any parameters in the template that do not have deferred values.
# The second parse will run deferred and evaluate only the remaining deferred parameters. Consequently
# any parameters to be deferred must accept a String[1] in original template so as to accept the value
# "<%= $variable_with_deferred_value %>" on the first parse.
#
function stdlib::deferrable_epp(String $template, Hash $variables) >> Variant[String, Sensitive[String], Deferred] {
# @param template template location - identical to epp function template location.
# @param variables parameters to pass into the template - some of which may have deferred values.
# @param preparse
# If `true` the epp template will be parsed twice, once normally and then a second time deferred.
# It may be nescessary to set `preparse` `false` when deferred values are somethig other than
# a string
#
function stdlib::deferrable_epp(String $template, Hash $variables, Boolean $preparse = true) >> Variant[String, Sensitive[String], Deferred] {
if $variables.stdlib::nested_values.any |$value| { $value.is_a(Deferred) } {
if $preparse {
$_variables_escaped = $variables.map | $_var , $_value | {
Comment on lines 19 to +21
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Like voxpupuli/puppet-redis#515 (comment) this has the problem that nested_values does recurse into hashes while $variables.map doesn't. I think the inner part is essentially a Puppet native version of Ruby's Hash#transform_values. Perhaps what's needed is a deep_transform_values.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So:

stdlib::deferrable_epp(template, { 'data' => { 'a' => Deferred(....) } })

Used to be supported and now it would not be with this patch as it is.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes. It would be good to somehow cover this in a test case.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Even assuming deep_transform_values can be made available (there is one in rails I see) exactly how you then handle the above case in the the intermediate template somewhat makes my head hurt.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Which is I'd think that Puppet should have a native way. Isn't there a .resolve function like there is .unwrap for Sensitive?

if $_value.is_a(Deferred) {
{ $_var => "<%= \$${_var} %>" }
} else {
{ $_var => $_value }
}
}.reduce | $_memo, $_kv | { $_memo + $_kv }

$_template = inline_epp(find_template($template).file,$_variables_escaped)
} else {
$_template = find_template($template).file
}

Deferred(
'inline_epp',
[find_template($template).file, $variables],
[$_template, $variables],
)
}
else {
Expand Down
94 changes: 94 additions & 0 deletions spec/acceptance/stdlib_deferrable_epp_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
# frozen_string_literal: true

require 'spec_helper_acceptance'

describe 'stdlib::deferable_epp function' do
let(:testfile) { (os[:family] == 'windows') ? 'C:\\test.epp' : '/tmp/test.epp' }

before(:all) do
apply_manifest(<<-MANIFEST)
$_epp = @(EPP)
<%- |
Stdlib::Port $port,
String[1] $password,
| -%>
port=<%= $port %>
password=<%= $password %>"
| EPP
$_testfile = $facts['os']['family'] ? {
'windows' => 'C:\\test.epp',
default => '/tmp/test.epp',
}

file{ $_testfile:
ensure => file,
content => $_epp,
}
MANIFEST
end

before(:each) do
rm_testfile = <<-MANIFEST
$_testfile = $facts['os']['family'] ? {
'windows' => 'C:\\test.epp',
default => '/tmp/test.epp',
}
file { "${_testfile}.rendered":
ensure => absent,
}
MANIFEST
apply_manifest(rm_testfile)
end

context 'with no deferred values' do
let(:pp) do
<<-MANIFEST
$_testfile = $facts['os']['family'] ? {
'windows' => 'C:\\test.epp',
default => '/tmp/test.epp',
}

file{ "${_testfile}.rendered":
ensure => file,
content => stdlib::deferrable_epp(
$_testfile,
{'port' => 1234, 'password' => 'top_secret'}
),
}
MANIFEST
end

it 'applies manifest, generates file' do
idempotent_apply(pp)
expect(file("#{testfile}.rendered")).to be_file
expect(file("#{testfile}.rendered").content).to match(%r{port=1234})
expect(file("#{testfile}.rendered").content).to match(%r{password=top_secret})
end
end

context 'with deferred values' do
let(:pp) do
<<-MANIFEST
$_testfile = $facts['os']['family'] ? {
'windows' => 'C:\\test.epp',
default => '/tmp/test.epp',
}

file{ "${_testfile}.rendered":
ensure => file,
content => stdlib::deferrable_epp(
$_testfile,
{'port' => 1234, 'password' => Deferred('inline_epp',['<%= $secret_password %>',{'secret_password' => 'so_secret'}])},
),
}
MANIFEST
end

it 'applies manifest, generates file' do
idempotent_apply(pp)
expect(file("#{testfile}.rendered")).to be_file
expect(file("#{testfile}.rendered").content).to match(%r{port=1234})
expect(file("#{testfile}.rendered").content).to match(%r{password=so_secret})
end
end
end
33 changes: 31 additions & 2 deletions spec/functions/stdlib_deferrable_epp_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,37 @@

it {
foo = Puppet::Pops::Types::TypeFactory.deferred.create('join', [1, 2, 3])
# This kind_of matcher requires https://github.com/puppetlabs/rspec-puppet/pull/24
expect(subject).to run.with_params('mymod/template.epp', { 'foo' => foo }) # .and_return(kind_of Puppet::Pops::Types::PuppetObject)
expect(subject).to run.with_params('mymod/template.epp', { 'foo' => foo }).and_return(kind_of(Puppet::Pops::Types::PuppetObject))
}
end

context 'defers rendering with mixed deferred and undeferred input' do
let(:pre_condition) do
<<~END
function epp($str, $data) { fail("should not have invoked epp()") }
function find_template($str) { return "path" }
function file($path) { return "foo: <%= foo %>, bar: <%= bar %>" }
END
end

it {
foo = Puppet::Pops::Types::TypeFactory.deferred.create('join', [1, 2, 3])
expect(subject).to run.with_params('mymod/template.epp', { 'foo' => foo, 'bar' => 'xyz' }).and_return(kind_of(Puppet::Pops::Types::PuppetObject))
}
end

context 'defers rendering with mixed deferred and undeferred input and preparse false' do
let(:pre_condition) do
<<~END
function epp($str, $data) { fail("should not have invoked epp()") }
function find_template($str) { return "path" }
function file($path) { return "foo: <%= foo %>, bar: <%= bar %>" }
END
end

it {
foo = Puppet::Pops::Types::TypeFactory.deferred.create('join', [1, 2, 3])
expect(subject).to run.with_params('mymod/template.epp', { 'foo' => foo, 'bar' => 'xyz' }, false).and_return(kind_of(Puppet::Pops::Types::PuppetObject))
}
end
end
Loading