Skip to content

Commit

Permalink
(puppetlabsGH-2475) Allow entire inventory to be a plugin reference
Browse files Browse the repository at this point in the history
This change allows an entire inventory to be a plugin reference. For
exmaple, the following inventory was previously invalid:

```yaml
---
_plugin: yaml
filepath: /path/to/inventory_partial.yaml
```

A top-level plugin like this used to be invalid as Bolt automatically
added `'name' => 'all'` to the top of the loaded inventory, which would
cause a plugin to receive a `name` parameter and (likely) error. Now,
Bolt will resolve a top-level plugin reference, validate that the
resolved data does not include a `name` key, and then merge in `'name'
=> 'all'`.

!bug

* **Allow entire inventory to be specified with a plugin**
  ([puppetlabs#2475](puppetlabs#2475))

  Inventory files can now be specified with a plugin. For example, the
  following inventory file is now valid:

  ```yaml
  ---
  _plugin: yaml
  filepath: /path/to/inventory_partial.yaml
  ```
  • Loading branch information
beechtom committed Dec 18, 2020
1 parent cdbf1f1 commit 608c608
Show file tree
Hide file tree
Showing 9 changed files with 171 additions and 29 deletions.
3 changes: 2 additions & 1 deletion lib/bolt/inventory.rb
Expand Up @@ -55,7 +55,8 @@ def self.schema
schema = {
type: Hash,
properties: OPTIONS.map { |opt| [opt, _ref: opt] }.to_h,
definitions: DEFINITIONS
definitions: DEFINITIONS,
_plugin: true
}

schema[:definitions]['config'][:properties] = Bolt::Config.transport_definitions
Expand Down
8 changes: 7 additions & 1 deletion lib/bolt/inventory/group.rb
Expand Up @@ -18,12 +18,18 @@ class Group
GROUP_KEYS = DATA_KEYS + %w[name groups targets]
CONFIG_KEYS = Bolt::Config::INVENTORY_OPTIONS.keys

def initialize(input, plugins)
def initialize(input, plugins, all_group: false)
@logger = Bolt::Logger.logger(self)
@plugins = plugins

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

if all_group && input.key?('name')
raise ValidationError.new("Top-level group 'all' cannot specify name '#{input['name']}'", nil)
elsif all_group
input = input.merge('name' => 'all')
end

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

@name = @plugins.resolve_references(input['name'])
Expand Down
2 changes: 1 addition & 1 deletion lib/bolt/inventory/inventory.rb
Expand Up @@ -21,7 +21,7 @@ def initialize(data, transport, transports, plugins)
@transport = transport
@config = transports
@plugins = plugins
@groups = Group.new(@data.merge('name' => 'all'), plugins)
@groups = Group.new(@data, plugins, all_group: true)
@group_lookup = {}
@targets = {}

Expand Down
2 changes: 1 addition & 1 deletion rakelib/schemas.rake
Expand Up @@ -211,7 +211,7 @@ end

def add_plugin_reference(definition)
definition, data = definition.partition do |k, _|
k == :description
%i[definitions description].include?(k)
end.map(&:to_h)

definition[:oneOf] = [
Expand Down
87 changes: 68 additions & 19 deletions schemas/bolt-inventory.schema.json
Expand Up @@ -1353,25 +1353,74 @@
]
}
},
"type": "object",
"properties": {
"config": {
"$ref": "#/definitions/config"
},
"facts": {
"$ref": "#/definitions/facts"
},
"features": {
"$ref": "#/definitions/features"
},
"groups": {
"$ref": "#/definitions/groups"
},
"targets": {
"$ref": "#/definitions/targets"
"oneOf": [
{
"type": "object",
"properties": {
"config": {
"oneOf": [
{
"$ref": "#/definitions/config"
},
{
"$ref": "#/definitions/_plugin"
}
]
},
"facts": {
"oneOf": [
{
"$ref": "#/definitions/facts"
},
{
"$ref": "#/definitions/_plugin"
}
]
},
"features": {
"oneOf": [
{
"$ref": "#/definitions/features"
},
{
"$ref": "#/definitions/_plugin"
}
]
},
"groups": {
"oneOf": [
{
"$ref": "#/definitions/groups"
},
{
"$ref": "#/definitions/_plugin"
}
]
},
"targets": {
"oneOf": [
{
"$ref": "#/definitions/targets"
},
{
"$ref": "#/definitions/_plugin"
}
]
},
"vars": {
"oneOf": [
{
"$ref": "#/definitions/vars"
},
{
"$ref": "#/definitions/_plugin"
}
]
}
}
},
"vars": {
"$ref": "#/definitions/vars"
{
"$ref": "#/definitions/_plugin"
}
}
]
}
2 changes: 0 additions & 2 deletions spec/bolt/inventory/inventory_spec.rb
Expand Up @@ -366,7 +366,6 @@ def get_target(inventory, name, alia = nil)
context 'with targets at the top level' do
let(:data) {
{
'name' => 'group1',
'targets' => [
'target1',
{ 'uri' => 'target2' },
Expand Down Expand Up @@ -791,7 +790,6 @@ def common_data(transport)
context 'with targets at the top level' do
let(:data) {
{
'name' => 'group1',
'targets' => [
'target1',
{ 'uri' => 'target2' },
Expand Down
6 changes: 2 additions & 4 deletions spec/bolt/transport/local_spec.rb
Expand Up @@ -44,8 +44,7 @@ def get_target(inventory, name, alia = nil)

context 'with group-level config' do
let(:data) {
{ 'name' => 'locomoco',
'targets' => [uri],
{ 'targets' => [uri],
'config' => {
'transport' => 'ssh',
'local' => {
Expand Down Expand Up @@ -100,8 +99,7 @@ def get_target(inventory, name, alia = nil)

context 'with group-level config' do
let(:data) {
{ 'name' => 'locomoco',
'targets' => [uri],
{ 'targets' => [uri],
'config' => {
'local' => {
'bundled-ruby' => true,
Expand Down
73 changes: 73 additions & 0 deletions spec/integration/inventory_spec.rb
Expand Up @@ -2,14 +2,18 @@

require 'spec_helper'
require 'bolt_spec/conn'
require 'bolt_spec/env_var'
require 'bolt_spec/files'
require 'bolt_spec/integration'
require 'bolt_spec/project'
require 'bolt_spec/puppetdb'

describe 'running with an inventory file', reset_puppet_settings: true do
include BoltSpec::Conn
include BoltSpec::EnvVar
include BoltSpec::Files
include BoltSpec::Integration
include BoltSpec::Project
include BoltSpec::PuppetDB

let(:conn) { conn_info('ssh') }
Expand Down Expand Up @@ -665,4 +669,73 @@ def fact_plan(name = 'facts_test')
.not_to raise_error
end
end

context 'top-level plugin' do
let(:command) { %W[inventory show --targets all --project #{@project.path}] }
let(:env_var) { 'BOLT_INVENTORY_PARTIAL' }
let(:partial_path) { 'partial.yaml' }

let(:inventory) do
{
'_plugin' => 'yaml',
'filepath' => partial_path
}
end

let(:partial) do
{
'targets' => %w[foo bar baz]
}
end

around(:each) do |example|
with_env_vars(env_var => partial_path) do
with_project(inventory: inventory) do |project|
@project = project
File.write(project.path + partial_path, partial.to_yaml)
example.run
end
end
end

context 'with valid resolved data' do
it 'does not error' do
result = run_cli_json(command)
expect(result['targets']).to match_array(partial['targets'])
end
end

context 'with resolved data with a name' do
let(:partial) do
{
'name' => 'badname',
'targets' => %w[foo bar baz]
}
end

it 'errors' do
expect { run_cli_json(command) }.to raise_error(
Bolt::Inventory::ValidationError,
/Top-level group 'all' cannot specify name 'badname'/
)
end
end

context 'with nested plugins' do
let(:inventory) do
{
'_plugin' => 'yaml',
'filepath' => {
'_plugin' => 'env_var',
'var' => env_var
}
}
end

it 'resolves and does not error' do
result = run_cli_json(command)
expect(result['targets']).to match_array(partial['targets'])
end
end
end
end
17 changes: 17 additions & 0 deletions spec/lib/bolt_spec/env_var.rb
@@ -0,0 +1,17 @@
# frozen_string_literal: true

module BoltSpec
module EnvVar
def with_env_vars(new_vars)
new_vars.transform_keys!(&:to_s)

begin
old_vars = new_vars.keys.collect { |var| [var, ENV[var]] }.to_h
ENV.update(new_vars)
yield
ensure
ENV.update(old_vars) if old_vars
end
end
end
end

0 comments on commit 608c608

Please sign in to comment.