Skip to content

Commit

Permalink
(puppetlabsGH-1365) Move resolve_references functions to Bolt::Plugin
Browse files Browse the repository at this point in the history
This commit moves the resolve_references functions from
Bolt::Inventory::Group2 to Bolt::Plugin.
  • Loading branch information
beechtom committed Nov 5, 2019
1 parent 2aa7a19 commit 91a64e7
Show file tree
Hide file tree
Showing 5 changed files with 157 additions and 94 deletions.
12 changes: 6 additions & 6 deletions bolt-modules/boltlib/lib/puppet/functions/resolve_references.rb
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,13 @@
def resolve_references(references)
unless Puppet[:tasks]
raise Puppet::ParseErrorWithIssue
.from_issue_and_stack(Bolt::PAL::Issues::PLAN_OPERATION_NOT_SUPPORTED_WHEN_COMPILING, action: 'resolve_references')
.from_issue_and_stack(
Bolt::PAL::Issues::PLAN_OPERATION_NOT_SUPPORTED_WHEN_COMPILING,
action: 'resolve_references'
)
end

references = references.merge('name' => 'all') unless references['name']
inventory = Puppet.lookup(:bolt_inventory)
group = Bolt::Inventory::Group2.new(references, inventory.plugins)

group.resolve_references(references)
plugins = Puppet.lookup(:bolt_inventory).plugins
plugins.resolve_references(references)
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
#!/usr/bin/env ruby
# frozen_string_literal: true

require 'json'

values = {
"value" => {
"name" => "127.0.0.1"
}
}

puts values.to_json
56 changes: 54 additions & 2 deletions bolt-modules/boltlib/spec/functions/resolve_references_spec.rb
Original file line number Diff line number Diff line change
@@ -1,9 +1,61 @@
# frozen_string_literal: true

require 'spec_helper'
require 'bolt/plugin'

describe 'resolve_references' do
it 'resolves data' do
# TODO
include PuppetlabsSpec::Fixtures
let(:boltdir) { Bolt::Boltdir.new('./spec/fixtures') }
let(:config) { Bolt::Config.new(boltdir, {}) }
let(:pal) { Bolt::PAL.new(config.modulepath, config.hiera_config, config.boltdir.resource_types) }
let(:pdb_client) { Bolt::PuppetDB::Client.new({}) }
let(:analytics) { Bolt::Analytics::NoopClient.new }
let(:plugins) { Bolt::Plugin.setup(config, pal, pdb_client, analytics) }
let(:inventory) { Bolt::Inventory.new({}, plugins: plugins) }
let(:tasks_enabled) { true }

let(:references) do
{
"targets" => {
"_plugin" => "task",
"task" => "test::references"
}
}
end

let(:resolved) do
{
"targets" => {
"name" => "127.0.0.1"
}
}
end

around(:each) do |example|
Puppet[:tasks] = tasks_enabled
Puppet.override(bolt_inventory: inventory) do
example.run
end
end

context 'calls resolve_references' do
it 'resolves all plugin references' do
is_expected.to run.with_params(references).and_return(resolved)
end

it 'errors when called with incorrect plugin data' do
references['targets']['_plugin'] = 'fake_plugin'
is_expected.to run.with_params(references).and_raise_error(/Unknown plugin/)
end
end

context 'without tasks enabled' do
let(:tasks_enabled) { false }

it 'fails and reports that resolve_references is not available' do
is_expected.to run
.with_params(references)
.and_raise_error(/Plan language function 'resolve_references' cannot be used/)
end
end
end
101 changes: 15 additions & 86 deletions lib/bolt/inventory/group2.rb
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,11 @@ def initialize(input, plugins)
@logger = Logging.logger[self]
@plugins = plugins

input = resolve_top_level_references(input) if reference?(input)
input = plugins.resolve_top_level_references(input) if plugins.reference?(input)

raise ValidationError.new("Group does not have a name", nil) unless input.key?('name')

@name = resolve_references(input['name'])
@name = plugins.resolve_references(input['name'])

raise ValidationError.new("Group name must be a String, not #{@name.inspect}", nil) unless @name.is_a?(String)
raise ValidationError.new("Invalid group name #{@name}", @name) unless @name =~ NAME_REGEX
Expand All @@ -37,7 +37,7 @@ def initialize(input, plugins)

validate_data_keys(@input)

targets = resolve_top_level_references(input.fetch('targets', []))
targets = plugins.resolve_top_level_references(input.fetch('targets', []))

@unresolved_targets = {}
@resolved_targets = {}
Expand Down Expand Up @@ -67,82 +67,11 @@ def initialize(input, plugins)
# them until we have a value. We don't just use resolve_references
# though, since that will resolve any nested references and we want to
# leave it to the group to do that lazily.
groups = resolve_top_level_references(groups)
groups = plugins.resolve_top_level_references(groups)

@groups = Array(groups).map { |g| Group2.new(g, plugins) }
end

# Evaluate all _plugin references in a data structure. Leaves are
# evaluated and then their parents are evaluated with references replaced
# by their values. If the result of a reference contains more references,
# they are resolved again before continuing to ascend the tree. The final
# result will not contain any references.
def resolve_references(data)
Bolt::Util.postwalk_vals(data) do |value|
reference?(value) ? resolve_references(resolve_single_reference(value)) : value
end
end

# Iteratively resolves "top-level" references until the result no longer
# has top-level references. A top-level reference is one which is not
# contained within another hash. It may be either the actual top-level
# result or arbitrarily nested within arrays. If parameters of the
# reference are themselves references, they will be looked. Any remaining
# references nested inside the result will *not* be evaluated once the
# top-level result is not a reference. This is used to resolve the
# `targets` and `groups` keys which are allowed to be references or
# arrays of references, but which may return data with nested references
# that should be resolved lazily. The end result will either be a single
# hash or a flat array of hashes.
def resolve_top_level_references(data)
if data.is_a?(Array)
data.flat_map { |elem| resolve_top_level_references(elem) }
elsif reference?(data)
partially_resolved = data.map do |k, v|
[k, resolve_references(v)]
end.to_h
fully_resolved = resolve_single_reference(partially_resolved)
# The top-level reference may have returned more references, so repeat the process
resolve_top_level_references(fully_resolved)
else
data
end
end
private :resolve_top_level_references

# Evaluates a single reference. The value returned may be another
# reference.
def resolve_single_reference(reference)
plugin_name = reference['_plugin']
begin
hook = @plugins.get_hook(plugin_name, :resolve_reference)
rescue Bolt::Plugin::PluginError => e
raise ValidationError.new(e.message, @name)
end

begin
validate_proc = @plugins.get_hook(plugin_name, :validate_resolve_reference)
rescue Bolt::Plugin::PluginError
validate_proc = proc { |*args| }
end

validate_proc.call(reference)

begin
# Evaluate the plugin and then recursively evaluate any plugin returned by it.
hook.call(reference)
rescue StandardError => e
loc = "resolve_reference in #{@name}"
raise Bolt::Plugin::PluginError::ExecutionError.new(e.message, plugin_name, loc)
end
end
private :resolve_single_reference

# Checks whether a given value is a _plugin reference
def reference?(input)
input.is_a?(Hash) && input.key?('_plugin')
end

def target_data(target_name)
if @unresolved_targets.key?(target_name)
target = @unresolved_targets.delete(target_name)
Expand All @@ -166,9 +95,9 @@ def add_target_definition(target)
raise ValidationError.new("Node entry must be a Hash, not #{target.class}", @name)
end

target['name'] = resolve_references(target['name']) if target.key?('name')
target['uri'] = resolve_references(target['uri']) if target.key?('uri')
target['alias'] = resolve_references(target['alias']) if target.key?('alias')
target['name'] = @plugins.resolve_references(target['name']) if target.key?('name')
target['uri'] = @plugins.resolve_references(target['uri']) if target.key?('uri')
target['alias'] = @plugins.resolve_references(target['alias']) if target.key?('alias')

t_name = target['name'] || target['uri']

Expand Down Expand Up @@ -294,7 +223,7 @@ def validate_group_input(input)
end

Bolt::Util.walk_keys(input) do |key|
if reference?(key)
if @plugins.reference?(key)
raise ValidationError.new("Group keys cannot be specified as _plugin references", @name)
else
key
Expand Down Expand Up @@ -357,11 +286,11 @@ def validate(used_group_names = Set.new, used_target_names = Set.new, used_alias

def resolve_data_keys(data, target = nil)
result = {
'config' => resolve_references(data.fetch('config', {})),
'vars' => resolve_references(data.fetch('vars', {})),
'facts' => resolve_references(data.fetch('facts', {})),
'features' => resolve_references(data.fetch('features', [])),
'plugin_hooks' => resolve_references(data.fetch('plugin_hooks', {}))
'config' => @plugins.resolve_references(data.fetch('config', {})),
'vars' => @plugins.resolve_references(data.fetch('vars', {})),
'facts' => @plugins.resolve_references(data.fetch('facts', {})),
'features' => @plugins.resolve_references(data.fetch('features', [])),
'plugin_hooks' => @plugins.resolve_references(data.fetch('plugin_hooks', {}))
}
validate_data_keys(result, target)
result['features'] = Set.new(result['features'].flatten)
Expand All @@ -376,13 +305,13 @@ def validate_data_keys(data, target = nil)
'features' => Array,
'plugin_hooks' => Hash
}.each do |key, expected_type|
next if !data.key?(key) || data[key].is_a?(expected_type) || reference?(data[key])
next if !data.key?(key) || data[key].is_a?(expected_type) || @plugins.reference?(data[key])

msg = +"Expected #{key} to be of type #{expected_type}, not #{data[key].class}"
msg << " for target #{target}" if target
raise ValidationError.new(msg, @name)
end
unless reference?(data['config'])
unless @plugins.reference?(data['config'])
unexpected_keys = data.fetch('config', {}).keys - CONFIG_KEYS
if unexpected_keys.any?
msg = +"Found unexpected key(s) #{unexpected_keys.join(', ')} in config for"
Expand Down
70 changes: 70 additions & 0 deletions lib/bolt/plugin.rb
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,76 @@ def by_name(plugin_name)
nil
end
end

# Evaluate all _plugin references in a data structure. Leaves are
# evaluated and then their parents are evaluated with references replaced
# by their values. If the result of a reference contains more references,
# they are resolved again before continuing to ascend the tree. The final
# result will not contain any references.
def resolve_references(data)
Bolt::Util.postwalk_vals(data) do |value|
reference?(value) ? resolve_references(resolve_single_reference(value)) : value
end
end

# Iteratively resolves "top-level" references until the result no longer
# has top-level references. A top-level reference is one which is not
# contained within another hash. It may be either the actual top-level
# result or arbitrarily nested within arrays. If parameters of the
# reference are themselves references, they will be looked. Any remaining
# references nested inside the result will *not* be evaluated once the
# top-level result is not a reference. This is used to resolve the
# `targets` and `groups` keys which are allowed to be references or
# arrays of references, but which may return data with nested references
# that should be resolved lazily. The end result will either be a single
# hash or a flat array of hashes.
def resolve_top_level_references(data)
if data.is_a?(Array)
data.flat_map { |elem| resolve_top_level_references(elem) }
elsif reference?(data)
partially_resolved = data.map do |k, v|
[k, resolve_references(v)]
end.to_h
fully_resolved = resolve_single_reference(partially_resolved)
# The top-level reference may have returned more references, so repeat the process
resolve_top_level_references(fully_resolved)
else
data
end
end

# Evaluates a single reference. The value returned may be another
# reference.
def resolve_single_reference(reference)
plugin_name = reference['_plugin']
begin
hook = get_hook(plugin_name, :resolve_reference)
rescue Bolt::Plugin::PluginError => e
raise ValidationError.new(e.message)
end

begin
validate_proc = get_hook(plugin_name, :validate_resolve_reference)
rescue Bolt::Plugin::PluginError
validate_proc = proc { |*args| }
end

validate_proc.call(reference)

begin
# Evaluate the plugin and then recursively evaluate any plugin returned by it.
hook.call(reference)
rescue StandardError => e
loc = "resolve_reference in #{plugin_name}"
raise Bolt::Plugin::PluginError::ExecutionError.new(e.message, plugin_name, loc)
end
end
private :resolve_single_reference

# Checks whether a given value is a _plugin reference
def reference?(input)
input.is_a?(Hash) && input.key?('_plugin')
end
end
end

Expand Down

0 comments on commit 91a64e7

Please sign in to comment.