Skip to content

Commit

Permalink
(puppetlabsGH-2815) Support plan-hierarchy lookups on the CLI
Browse files Browse the repository at this point in the history
This adds a new flag `--plan-hierarchy` to the `bolt lookup` CLI
command, which will perform a lookup as if in a plan outside an apply
block rather than in an apply block. The command optionally accepts
variable definitions for plan variable interpolation in hiera config.
The flag is mutually exclusive with targetting options, and either a
targetting option or `--plan-hierarchy` are required. The command
returns the bare value of the lookup, rather than a Result object.

!feature

* **Lookup hiera plan_hierarchy values from the CLI** ([puppetlabs#2815](puppetlabs#2815))

  The `bolt lookup` command now has a `--plan-hierarchy` flag that will
  lookup values from Hiera's `plan_hierarchy`.
  • Loading branch information
lucywyman committed Jun 1, 2021
1 parent 339083b commit 26cea34
Show file tree
Hide file tree
Showing 13 changed files with 243 additions and 95 deletions.
33 changes: 27 additions & 6 deletions documentation/hiera.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,13 +46,11 @@ data set by a user overrides the default data set by the module’s author.
## Look up data from the command line

You can use the `bolt lookup` command and `Invoke-BoltLookup` PowerShell cmdlet
to look up data from the command line. The `lookup` command looks up data in the
context of a target, allowing you to interpolate target facts and variables in
your hierarchy.
to look up Hiera data from the command line.

> **Note:** The `bolt lookup` and `Invoke-BoltLookup` commands only look up data
> using the `hierarchy` key in the Hiera configuration file. `plan_hierarchy`
> is not supported from the command line.
### Lookup up data from hierarchy key
Without additional options, the `lookup` command looks up data in the context of a target, allowing
you to interpolate target facts and variables in your hierarchy.

When you run the `bolt lookup` and `Invoke-BoltLookup` commands, Bolt first
runs an `apply_prep` on each of the targets specified. This installs the
Expand Down Expand Up @@ -119,6 +117,29 @@ Successful on 2 targets: windows_target, ubuntu_target
Ran on 2 targets
```

### Look up data from plan_hierarchy key

When passed `--plan-hierarchy`, the `lookup` command will look up data from Hiera's
[plan_hierarchy](#outside-apply-blocks) key. This mimics performing a lookup outside an apply block
in a Bolt plan.

Because `plan_hierarchy` values are not specific to individual targets this command will perform
just one lookup and return the bare value. The `--plan-hierarchy` option is cannot be passed in
addition to targetting options like `--targets`.

If your `plan_hierarchy` contains [interpolations from plan variables](#todo) you can pass values to
interpolate to `lookup` like so:

_\*nix shell command_
```
bolt lookup key --plan-hierarchy plan_var=interpolate_me
```
_PowerShell cmdlet_
```
Invoke-BoltLookup -Key key -PlanHierarchy plan_var=interpolation_me
```
## Look up data in plans
You can use the [Puppet `lookup()`
Expand Down
10 changes: 8 additions & 2 deletions lib/bolt/bolt_option_parser.rb
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ def get_help_text(subcommand, action = nil)
{ flags: OPTIONS[:global] + %w[format],
banner: GUIDE_HELP }
when 'lookup'
{ flags: ACTION_OPTS + %w[hiera-config],
{ flags: ACTION_OPTS + %w[hiera-config plan-hierarchy],
banner: LOOKUP_HELP }
when 'module'
case action
Expand Down Expand Up @@ -407,7 +407,7 @@ module Manage Bolt project modules
lookup
#{colorize(:cyan, 'Usage')}
bolt lookup <key> {--targets TARGETS | --query QUERY | --rerun FILTER}
bolt lookup <key> {--targets TARGETS | --query QUERY | --rerun FILTER | --plan-hierarchy}
[options]
#{colorize(:cyan, 'Description')}
Expand All @@ -418,6 +418,7 @@ module Manage Bolt project modules
#{colorize(:cyan, 'Examples')}
bolt lookup password --targets servers
bolt lookup password --plan-hierarchy variable=value
HELP

MODULE_HELP = <<~HELP
Expand Down Expand Up @@ -988,6 +989,11 @@ def initialize(options)
@options[:resolve] = resolve
end

separator "\n#{self.class.colorize(:cyan, 'Lookup options')}"
define('--plan-hierarchy', 'Look up a value with Hiera in the context of a specific plan.') do |_|
@options[:plan_hierarchy] = true
end

separator "\n#{self.class.colorize(:cyan, 'Plan options')}"
define('--pp', 'Create a new Puppet language plan.') do |_|
@options[:puppet] = true
Expand Down
32 changes: 27 additions & 5 deletions lib/bolt/cli.rb
Original file line number Diff line number Diff line change
Expand Up @@ -354,12 +354,18 @@ def validate(options)
"the project, run '#{command} #{options[:object]}'."
end

if options[:subcommand] != 'file' && options[:subcommand] != 'script' &&
if !%w[file script lookup].include?(options[:subcommand]) &&
!options[:leftovers].empty?
raise Bolt::CLIError,
"Unknown argument(s) #{options[:leftovers].join(', ')}"
end

target_opts = options.keys.select { |opt| TARGETING_OPTIONS.include?(opt) }
if options[:subcommand] == 'lookup' &&
target_opts.any? && options[:plan_hierarchy]
raise Bolt::CLIError, "The 'lookup' command accepts either targetting option OR --plan-hierarchy."
end

if options[:noop] &&
!(options[:subcommand] == 'task' && options[:action] == 'run') && options[:subcommand] != 'apply'
raise Bolt::CLIError,
Expand Down Expand Up @@ -435,7 +441,8 @@ def execute(options)
# options[:target_args] will contain a string/array version of the targetting options this is passed to plans
# options[:targets] will contain a resolved set of Target objects
unless %w[guide module project secret].include?(options[:subcommand]) ||
%w[convert new show].include?(options[:action])
%w[convert new show].include?(options[:action]) ||
options[:plan_hierarchy] # Literally just assuming the CLI refactor will make this not matter
update_targets(options)
end

Expand Down Expand Up @@ -516,7 +523,13 @@ def execute(options)
code = Bolt::ProjectManager.new(config, outputter, pal).migrate
end
when 'lookup'
code = lookup(options[:object], options[:targets])
# Validate functions verifies one of these was passed
if options[:targets]
code = lookup(options[:object], options[:targets])
elsif options[:plan_hierarchy]
vars = Hash[options[:leftovers].map { |a| a.split('=', 2) }]
code = plan_lookup(options[:object], vars)
end
when 'plan'
case options[:action]
when 'new'
Expand Down Expand Up @@ -694,8 +707,17 @@ def list_groups
outputter.print_groups(inventory.group_names.sort, inventory.source, config.default_inventoryfile)
end

# Looks up a value with Hiera as if in a plan outside an apply block, using
# provided variable values for interpolations
#
def plan_lookup(key, vars = {})
result = pal.plan_hierarchy_lookup(key, vars)
outputter.print_plan_lookup(result)
0
end

# Looks up a value with Hiera, using targets as the contexts to perform the
# look ups in.
# look ups in. This should return the same value as a lookup in an apply block.
#
def lookup(key, targets)
executor = Bolt::Executor.new(
Expand All @@ -706,7 +728,7 @@ def lookup(key, targets)
config.future
)

executor.subscribe(outputter) if options.fetch(:format, 'human') == 'human'
executor.subscribe(outputter) if config.format == 'human'
executor.subscribe(log_outputter)
executor.publish_event(type: :plan_start, plan: nil)

Expand Down
4 changes: 4 additions & 0 deletions lib/bolt/outputter/human.rb
Original file line number Diff line number Diff line change
Expand Up @@ -460,6 +460,10 @@ def print_guide(guide, _topic)
@stream.puts(guide)
end

def print_plan_lookup(value)
@stream.puts(value)
end

def print_module_list(module_list)
module_list.each do |path, modules|
if (mod = modules.find { |m| m[:internal_module_group] })
Expand Down
4 changes: 4 additions & 0 deletions lib/bolt/outputter/json.rb
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,10 @@ def print_guide(guide, topic)
}.to_json)
end

def print_plan_lookup(value)
@stream.puts(value.to_json)
end

def print_puppetfile_result(success, puppetfile, moduledir)
@stream.puts({ success: success,
puppetfile: puppetfile,
Expand Down
15 changes: 13 additions & 2 deletions lib/bolt/pal.rb
Original file line number Diff line number Diff line change
Expand Up @@ -163,9 +163,10 @@ def detect_project_conflict(project, environment)
# Runs a block in a PAL script compiler configured for Bolt. Catches
# exceptions thrown by the block and re-raises them ensuring they are
# Bolt::Errors since the script compiler block will squash all exceptions.
def in_bolt_compiler
def in_bolt_compiler(compiler_params: {})
# TODO: If we always call this inside a bolt_executor we can remove this here
setup
compiler_params.merge!(set_local_facts: false)
r = Puppet::Pal.in_tmp_environment('bolt', modulepath: full_modulepath, facts: {}) do |pal|
# Only load the project if it a) exists, b) has a name it can be loaded with
Puppet.override(bolt_project: @project,
Expand All @@ -174,7 +175,7 @@ def in_bolt_compiler
# of modules, it must happen *after* we have overridden
# bolt_project or the project will be ignored
detect_project_conflict(@project, Puppet.lookup(:environments).get('bolt'))
pal.with_script_compiler(set_local_facts: false) do |compiler|
pal.with_script_compiler(**compiler_params) do |compiler|
alias_types(compiler)
register_resource_types(Puppet.lookup(:loaders)) if @resource_types
begin
Expand Down Expand Up @@ -632,6 +633,16 @@ def run_plan(plan_name, params, executor, inventory = nil, pdb_client = nil, app
Bolt::PlanResult.new(e, 'failure')
end

def plan_hierarchy_lookup(key, vars = {})
# Do a lookup with a script compiler, which uses the 'plan_hierarchy' key in
# Hiera config.
with_puppet_settings do
in_bolt_compiler(compiler_params: { variables: vars }) do |compiler|
compiler.call_function('lookup', key)
end
end
end

def lookup(key, targets, inventory, executor, _concurrency)
# Install the puppet-agent package and collect facts. Facts are
# automatically added to the targets.
Expand Down
5 changes: 5 additions & 0 deletions pwsh_module/command.tests.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -311,5 +311,10 @@ Describe "test all bolt command examples" {
$results = Invoke-BoltLookup -key 'key' -targets 'target1,target2'
$results | Should -Be "bolt lookup key --targets target1,target2"
}

It "bolt lookup key --plan-hierarchy" {
$results = Invoke-BoltLookup -key 'key' -PlanHierarchy
$results | Should -Be "bolt lookup key --plan-hierarchy"
}
}
}
36 changes: 29 additions & 7 deletions spec/bolt/cli_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,8 @@ def stub_config(file_content = {})
end

context 'lookup' do
let(:pal) { double('pal', lookup: results) }
let(:pal) { double('pal', lookup: results, plan_hierarchy_lookup: value) }
let(:value) { 'value' }
let(:results) { Bolt::ResultSet.new([]) }

it 'errors without a key' do
Expand All @@ -180,7 +181,7 @@ def stub_config(file_content = {})
)
end

it 'errors without a targeting option' do
it 'errors without a targetting option or plan-hierarchy' do
cli = Bolt::CLI.new(%w[lookup key])

expect { cli.execute(cli.parse) }.to raise_error(
Expand All @@ -189,12 +190,33 @@ def stub_config(file_content = {})
)
end

it 'calls Bolt::PAL#lookup' do
allow(Bolt::PAL).to receive(:new).and_return(pal)
expect(pal).to receive(:lookup)
it 'errors with both a targetting option and plan-hierarchy' do
cli = Bolt::CLI.new(%w[lookup key --plan-hierarchy --rerun all])

cli = Bolt::CLI.new(%w[lookup key --targets foo])
cli.execute(cli.parse)
expect { cli.execute(cli.parse) }.to raise_error(
Bolt::CLIError,
/accepts either targetting option OR --plan-hierarchy/
)
end

context 'without plan-hierarchy' do
it 'calls Bolt::PAL#lookup' do
allow(Bolt::PAL).to receive(:new).and_return(pal)
expect(pal).to receive(:lookup)

cli = Bolt::CLI.new(%w[lookup key --targets foo])
cli.execute(cli.parse)
end
end

context 'with plan-hierarchy' do
it 'calls Bolt::PAL#plan_hierarchy_lookup' do
allow(Bolt::PAL).to receive(:new).and_return(pal)
expect(pal).to receive(:plan_hierarchy_lookup)

cli = Bolt::CLI.new(%w[lookup key --plan-hierarchy])
cli.execute(cli.parse)
end
end
end

Expand Down
6 changes: 6 additions & 0 deletions spec/bolt/outputter/human_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -374,6 +374,12 @@
expect(output.string).to eq(guide)
end

it 'prints a plan-hierarchy lookup result' do
value = 'peanut butter'
outputter.print_plan_lookup(value)
expect(output.string).to eq(value)
end

it 'does not spin when spinner is set to false' do
outputter.start_spin
sleep(0.3)
Expand Down
7 changes: 7 additions & 0 deletions spec/bolt/outputter/json_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,13 @@
expect(parsed['guide']).to eq(guide)
end

it 'prints a plan-hierarchy lookup value' do
value = 'peanut butter'
outputter.print_plan_lookup(value)
expect { JSON.parse(output.string) }.not_to raise_error
expect(output.string).to eq("\"#{value}\"\n")
end

context '#print_targets' do
let(:inventoryfile) { '/path/to/inventory' }

Expand Down
File renamed without changes.
5 changes: 5 additions & 0 deletions spec/fixtures/hiera/plan_hiera_var.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
version: 5

plan_hierarchy:
- name: Common
path: "%{interpolate}.yaml"
Loading

0 comments on commit 26cea34

Please sign in to comment.