Skip to content

Commit 7709c9e

Browse files
authored
Merge pull request #1381 from nicklewis/GH-1350-resolve-references-in-config
(GH-1350) Configure plugins using other plugins
2 parents 9516158 + c8f7f79 commit 7709c9e

21 files changed

+199
-67
lines changed

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,18 @@
1717
Bolt now accepts the `_run_as` metaparameter for puppet_library hooks. `_run_as` specifies which user the library install task will be executed as.
1818

1919
* **Added `--password-prompt` and `--sudo-password-prompt` to CLI flags** ([#1269](https://github.com/puppetlabs/bolt/issues/1269))
20+
2021
Two new flags have been added to support users who would like to set a `password` or `sudo-password` from a prompt without using a plugin. A deprecation message will appear when a value is not supplied for `--password` or `--sudo-password`.
2122

2223
* **Subcommand `project migrate` new to the CLI** ([#1377](https://github.com/puppetlabs/bolt/issues/1377))
2324

2425
The CLI now provides the subcommand `project migrate` which migrates Bolt projects to the latest version. When migrating a project the [inventory file](https://puppet.com/docs/bolt/latest/inventory_file.html) will be changed from `v1` to `v2`. Changes are made in place and will not preserve comments or formatting.
2526

27+
* **Plugin support in `bolt.yml`** ([#1381](https://github.com/puppetlabs/bolt/pull/1381))
28+
29+
Plugin configuration can now be set by looking up data from other plugins. For example, the password for one plugin can be queried from another plugin.
30+
31+
2632
## Bug fixes
2733

2834
* **Bolt issued an error for unset environment variables with `system::env`** ([#1414](https://github.com/puppetlabs/bolt/issues/1414))

bolt-modules/boltlib/spec/functions/apply_prep_spec.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
include PuppetlabsSpec::Fixtures
1515
let(:applicator) { mock('Bolt::Applicator') }
1616
let(:executor) { Bolt::Executor.new(1, Bolt::Analytics::NoopClient.new) }
17-
let(:plugins) { Bolt::Plugin.new(nil, nil, Bolt::Analytics::NoopClient.new) }
17+
let(:plugins) { Bolt::Plugin.setup(Bolt::Config.default, nil, nil, Bolt::Analytics::NoopClient.new) }
1818
let(:plugin_result) { {} }
1919
let(:task_hook) { proc { |_opts, target, _fun| proc { Bolt::Result.new(target, value: plugin_result) } } }
2020
let(:inventory) { Bolt::Inventory.create_version({}, nil, plugins) }
@@ -171,7 +171,7 @@
171171

172172
let(:config) { Bolt::Config.new(Bolt::Boltdir.new('.'), {}) }
173173
let(:pal) { nil }
174-
let(:plugins) { Bolt::Plugin.new(config, pal, Bolt::Analytics::NoopClient.new) }
174+
let(:plugins) { Bolt::Plugin.setup(config, pal, nil, Bolt::Analytics::NoopClient.new) }
175175
let(:inventory) { Bolt::Inventory.create_version(data, config, plugins) }
176176
let(:target) { inventory.get_target(hostname) }
177177
let(:targets) { inventory.get_targets(hostname) }

bolt-modules/boltlib/spec/functions/run_plan_spec.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,7 @@
116116
context 'with inventory v2' do
117117
let(:config) { Bolt::Config.new(Bolt::Boltdir.new('.'), {}) }
118118
let(:pal) { nil }
119-
let(:plugins) { Bolt::Plugin.new(config, pal, Bolt::Analytics::NoopClient.new) }
119+
let(:plugins) { Bolt::Plugin.setup(config, pal, nil, Bolt::Analytics::NoopClient.new) }
120120
let(:inventory) { Bolt::Inventory.create_version({ 'version' => 2 }, config, plugins) }
121121

122122
it 'parameters with type TargetSpec are added to inventory' do

documentation/bolt_configuration_options.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -244,3 +244,14 @@ plugin_hooks:
244244
master: 'puppet.example.com'
245245
cacert_content: <CERT>
246246
```
247+
248+
You can also configure `plugin_hooks` using `_plugin` references:
249+
250+
```yaml
251+
plugin_hooks:
252+
puppet_library:
253+
plugin: puppet_agent
254+
version:
255+
_plugin: prompt
256+
message: "Which version of Puppet do you want to install?"
257+
```

documentation/using_plugins.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,26 @@ plugins:
9494
private-key: ~/bolt_private_key.pem
9595
```
9696
97+
Plugin configuration can be derived from other plugins using `_plugin` references. For example, you can encrypt the credentials used to configure the `vault` plugin.
98+
99+
```
100+
plugins:
101+
vault:
102+
auth:
103+
token:
104+
_plugin: pkcs7
105+
encrypted_value: |
106+
ENC[PKCS7,MIIBiQYJKoZIhvcNAQcDoIIBejCCAXYCAQAxggEhMIIBHQIBADAFMAACAQEw
107+
DQYJKoZIhvcNAQEBBQAEggEARQNZqnN8ByTelBjokvkgOemMxyjmblWga8g6
108+
y0nYfmA5Hdqj1nC/wIJTZafbmfzCEtUQZ+Hf70YPV04OYy7PU1WtYp0u/B0t
109+
YCX7GgWHoXUSrEV+YtGyIpoa/pStvzzP12CBIaXwGh62TP6ZSbRnr/q/pnfk
110+
mOx6HghUoNXfKBLW+sq8KgyNN1DJDTl0KubHVLnJvTc1jjHX7YK+qxV4eb3B
111+
yklwuaDziPd+pipQOcUfjMnVW45THRUzE06iI8Q+DqVGA7/RsTEdG0HGtj5h
112+
P7i5wLUdZ2AhYBkP1sacW7yiUjqwPjwMwx0T/xn/DqVW02QOjFgqsaSwi1CD
113+
MOA3pDBMBgkqhkiG9w0BBwEwHQYJYIZIAWUDBAEqBBC87Iy6lvqGicslM6si
114+
994ogCDRAeJgS/0HTaFdhjdxC8CmMCADl7qVgxKDf1ztpXznyg==]
115+
```
116+
97117
## Bundled plugins
98118
99119
Bolt ships with a few plugins out of the box: task, puppetdb, terraform, azure_inventory, aws ec2, prompt, pkcs7, and vault.

lib/bolt/cli.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -526,6 +526,7 @@ def run_plan(plan_name, plan_arguments, nodes, options)
526526
end
527527

528528
def apply_manifest(code, targets, filename = nil, noop = false)
529+
Puppet[:tasks] = false
529530
ast = pal.parse_manifest(code, filename)
530531

531532
executor = Bolt::Executor.new(config.concurrency, analytics, noop)

lib/bolt/config.rb

Lines changed: 24 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ def initialize(boltdir, config_data, overrides = {})
7171
@save_rerun = true
7272
@puppetfile_config = {}
7373
@plugins = {}
74-
@plugin_hooks = { 'puppet_library' => { 'plugin' => 'puppet_agent', 'stop_service' => true } }
74+
@plugin_hooks = {}
7575

7676
# add an entry for the default console logger
7777
@log = { 'console' => {} }
@@ -166,30 +166,14 @@ def update_from_file(data)
166166

167167
@save_rerun = data['save-rerun'] if data.key?('save-rerun')
168168

169-
# Plugins are only settable from config not inventory so we can overwrite
170169
@plugins = data['plugins'] if data.key?('plugins')
171-
@plugin_hooks.merge!(data['plugin_hooks']) if data.key?('plugin_hooks')
170+
@plugin_hooks = data['plugin_hooks'] if data.key?('plugin_hooks')
172171

173-
%w[concurrency format puppetdb color transport].each do |key|
172+
%w[concurrency format puppetdb color].each do |key|
174173
send("#{key}=", data[key]) if data.key?(key)
175174
end
176175

177-
TRANSPORTS.each do |key, impl|
178-
if data[key.to_s]
179-
selected = impl.filter_options(data[key.to_s])
180-
if @future
181-
to_expand = %w[private-key cacert token-file] & selected.keys
182-
to_expand.each do |opt|
183-
selected[opt] = File.expand_path(selected[opt], @boltdir.path) if opt.is_a?(String)
184-
end
185-
end
186-
187-
@transports[key] = Bolt::Util.deep_merge(@transports[key], selected)
188-
end
189-
if @transports[key]['interpreters']
190-
@transports[key]['interpreters'] = normalize_interpreters(@transports[key]['interpreters'])
191-
end
192-
end
176+
update_transports(data)
193177
end
194178
private :update_from_file
195179

@@ -229,11 +213,28 @@ def apply_overrides(options)
229213
end
230214

231215
def update_from_inventory(data)
232-
update_from_file(data)
216+
update_transports(data)
217+
end
233218

234-
if data['transport']
235-
@transport = data['transport']
219+
def update_transports(data)
220+
TRANSPORTS.each do |key, impl|
221+
if data[key.to_s]
222+
selected = impl.filter_options(data[key.to_s])
223+
if @future
224+
to_expand = %w[private-key cacert token-file] & selected.keys
225+
to_expand.each do |opt|
226+
selected[opt] = File.expand_path(selected[opt], @boltdir.path) if opt.is_a?(String)
227+
end
228+
end
229+
230+
@transports[key] = Bolt::Util.deep_merge(@transports[key], selected)
231+
end
232+
if @transports[key]['interpreters']
233+
@transports[key]['interpreters'] = normalize_interpreters(@transports[key]['interpreters'])
234+
end
236235
end
236+
237+
@transport = data['transport'] if data.key?('transport')
237238
end
238239

239240
def transport_conf

lib/bolt/inventory.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -234,7 +234,7 @@ def update_target(target)
234234
set_facts(target.name, data['facts']) unless @target_facts[target.name]
235235
data['features']&.each { |feature| set_feature(target, feature) } unless @target_features[target.name]
236236
unless @target_plugin_hooks[target.name]
237-
set_plugin_hooks(target.name, @config.plugin_hooks.merge(data['plugin_hooks'] || {}))
237+
set_plugin_hooks(target.name, (@plugins&.plugin_hooks || {}).merge(data['plugin_hooks'] || {}))
238238
end
239239

240240
# Use Config object to ensure config section is treated consistently with config file

lib/bolt/inventory/target.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ def set_feature(feature, value = true)
7878
def plugin_hooks
7979
# Merge plugin_hooks from the config file with any defined by the group
8080
# or assigned dynamically to the target
81-
@inventory.config.plugin_hooks.merge(group_cache['plugin_hooks']).merge(@plugin_hooks)
81+
@inventory.plugins.plugin_hooks.merge(group_cache['plugin_hooks']).merge(@plugin_hooks)
8282
end
8383

8484
def set_config(key_or_key_path, value)

lib/bolt/plugin.rb

Lines changed: 47 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -39,9 +39,10 @@ def initialize(plugin_name, hook)
3939
end
4040

4141
class PluginContext
42-
def initialize(config, pal)
42+
def initialize(config, pal, plugins)
4343
@pal = pal
4444
@config = config
45+
@plugins = plugins
4546
end
4647

4748
def serial_executor
@@ -50,7 +51,7 @@ def serial_executor
5051
private :serial_executor
5152

5253
def empty_inventory
53-
@empty_inventory ||= Bolt::Inventory.new({}, @config)
54+
@empty_inventory ||= Bolt::Inventory::Inventory2.new({}, @config, plugins: @plugins)
5455
end
5556
private :empty_inventory
5657

@@ -120,29 +121,46 @@ def boltdir
120121

121122
def self.setup(config, pal, pdb_client, analytics)
122123
plugins = new(config, pal, analytics)
123-
# PDB is special do we want to expose the default client to the context?
124+
125+
# PDB is special because it needs the PDB client. Since it has no config,
126+
# we can just add it first.
124127
plugins.add_plugin(Bolt::Plugin::Puppetdb.new(pdb_client))
125128

126-
plugins.add_ruby_plugin('Bolt::Plugin::InstallAgent')
127-
plugins.add_ruby_plugin('Bolt::Plugin::Task')
128-
plugins.add_ruby_plugin('Bolt::Plugin::Pkcs7')
129-
plugins.add_ruby_plugin('Bolt::Plugin::Prompt')
129+
# Initialize any plugins referenced in config. This will also indirectly
130+
# initialize any plugins they depend on.
131+
if plugins.reference?(config.plugins)
132+
msg = "The 'plugins' setting cannot be set by a plugin reference"
133+
raise PluginError.new(msg, 'bolt/plugin-error')
134+
end
135+
136+
config.plugins.keys.each do |plugin|
137+
plugins.by_name(plugin)
138+
end
139+
140+
plugins.plugin_hooks.merge!(plugins.resolve_references(config.plugin_hooks))
130141

131142
plugins
132143
end
133144

134-
BUILTIN_PLUGINS = %w[task terraform pkcs7 prompt vault aws_inventory
135-
puppetdb azure_inventory yaml].freeze
145+
RUBY_PLUGINS = %w[install_agent task pkcs7 prompt].freeze
146+
BUILTIN_PLUGINS = %w[task terraform pkcs7 prompt vault aws_inventory puppetdb azure_inventory yaml].freeze
147+
DEFAULT_PLUGIN_HOOKS = { 'puppet_library' => { 'plugin' => 'puppet_agent', 'stop_service' => true } }.freeze
136148

137149
attr_reader :pal, :plugin_context
150+
attr_accessor :plugin_hooks
151+
152+
private_class_method :new
138153

139154
def initialize(config, pal, analytics)
140155
@config = config
141156
@analytics = analytics
142-
@plugin_context = PluginContext.new(config, pal)
157+
@plugin_context = PluginContext.new(config, pal, self)
143158
@plugins = {}
144159
@pal = pal
145160
@unknown = Set.new
161+
@resolution_stack = []
162+
@unresolved_plugin_configs = config.plugins.dup
163+
@plugin_hooks = DEFAULT_PLUGIN_HOOKS.dup
146164
end
147165

148166
def modules
@@ -154,11 +172,11 @@ def add_plugin(plugin)
154172
@plugins[plugin.name] = plugin
155173
end
156174

157-
def add_ruby_plugin(cls_name)
158-
snake_name = Bolt::Util.class_name_to_file_name(cls_name)
159-
require snake_name
160-
cls = Kernel.const_get(cls_name)
161-
plugin_name = snake_name.split('/').last
175+
def add_ruby_plugin(plugin_name)
176+
cls_name = Bolt::Util.snake_name_to_class_name(plugin_name)
177+
filename = "bolt/plugin/#{plugin_name}"
178+
require filename
179+
cls = Kernel.const_get("Bolt::Plugin::#{cls_name}")
162180
opts = {
163181
context: @plugin_context,
164182
config: config_for_plugin(plugin_name)
@@ -178,14 +196,18 @@ def add_module_plugin(plugin_name)
178196
add_plugin(plugin)
179197
end
180198

181-
def add_from_config
182-
@config.plugins.keys.each do |plugin_name|
183-
by_name(plugin_name)
184-
end
185-
end
186-
187199
def config_for_plugin(plugin_name)
188-
@config.plugins[plugin_name] || {}
200+
return {} unless @unresolved_plugin_configs.include?(plugin_name)
201+
if @resolution_stack.include?(plugin_name)
202+
msg = "Configuration for plugin '#{plugin_name}' depends on the plugin itself"
203+
raise PluginError.new(msg, 'bolt/plugin-error')
204+
else
205+
@resolution_stack.push(plugin_name)
206+
config = resolve_references(@unresolved_plugin_configs[plugin_name])
207+
@unresolved_plugin_configs.delete(plugin_name)
208+
@resolution_stack.pop
209+
config
210+
end
189211
end
190212

191213
def get_hook(plugin_name, hook)
@@ -201,7 +223,9 @@ def get_hook(plugin_name, hook)
201223
def by_name(plugin_name)
202224
return @plugins[plugin_name] if @plugins.include?(plugin_name)
203225
begin
204-
unless @unknown.include?(plugin_name)
226+
if RUBY_PLUGINS.include?(plugin_name)
227+
add_ruby_plugin(plugin_name)
228+
elsif !@unknown.include?(plugin_name)
205229
add_module_plugin(plugin_name)
206230
end
207231
rescue PluginError::Unknown

lib/bolt/plugin/prompt.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ def validate_resolve_reference(opts)
2020

2121
def resolve_reference(opts)
2222
# rubocop:disable Style/GlobalVars
23-
$future ? STDERR.print("#{opts['message']}:") : STDOUT.print("#{opts['message']}:")
23+
$future ? STDERR.print("#{opts['message']}: ") : STDOUT.print("#{opts['message']}: ")
2424
value = STDIN.noecho(&:gets).chomp
2525
$future ? STDERR.puts : STDOUT.puts
2626
# rubocop:enable Style/GlobalVars

lib/bolt/util.rb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -204,6 +204,10 @@ def file_stat(path)
204204
File.stat(File.expand_path(path))
205205
end
206206

207+
def snake_name_to_class_name(snake_name)
208+
snake_name.split('_').map(&:capitalize).join
209+
end
210+
207211
def class_name_to_file_name(cls_name)
208212
# Note this turns Bolt::CLI -> 'bolt/cli' not 'bolt/c_l_i'
209213
# this won't handle Bolt::Inventory2Foo

spec/bolt/cli_spec.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1818,7 +1818,7 @@ def stub_config(file_content = {})
18181818
anything,
18191819
true).and_return(executor)
18201820

1821-
plugins = Bolt::Plugin.new(nil, nil, nil)
1821+
plugins = Bolt::Plugin.setup(Bolt::Config.default, nil, nil, nil)
18221822
allow(cli).to receive(:plugins).and_return(plugins)
18231823

18241824
outputter = Bolt::Outputter::JSON.new(false, false, false, output)

spec/bolt/inventory/group2_spec.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313

1414
let(:data) { { 'name' => 'all' } }
1515
let(:pal) { nil } # Not used
16-
let(:plugins) { Bolt::Plugin.new(config, nil, Bolt::Analytics::NoopClient.new) }
16+
let(:plugins) { Bolt::Plugin.setup(config, nil, nil, Bolt::Analytics::NoopClient.new) }
1717
let(:group) {
1818
# Inventory always resolves unknown labels to names or aliases from the top-down when constructed,
1919
# passing the collection of all aliases in it. Do that manually here to ensure plain target strings
@@ -764,7 +764,7 @@
764764
let(:pal) { Bolt::PAL.new(modulepath, nil, nil) }
765765

766766
let(:plugins) do
767-
plugins = Bolt::Plugin.new(config, pal, Bolt::Analytics::NoopClient.new)
767+
plugins = Bolt::Plugin.setup(config, pal, nil, Bolt::Analytics::NoopClient.new)
768768
plugins.add_plugin(BoltSpec::Plugins::Constant.new)
769769
plugins.add_plugin(BoltSpec::Plugins::Error.new)
770770
plugins.add_plugin(BoltSpec::Plugins::TestLookup.new(lookup_data))

spec/bolt/inventory/inventory2_spec.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ def get_target(inventory, name, alia = nil)
1616
end
1717

1818
let(:pal) { nil } # Not used
19-
let(:plugins) { Bolt::Plugin.new(config, pal, Bolt::Analytics::NoopClient.new) }
19+
let(:plugins) { Bolt::Plugin.setup(config, pal, nil, Bolt::Analytics::NoopClient.new) }
2020
let(:target_name) { "example.com" }
2121
let(:target_entry) { target_name }
2222
let(:targets) { [target_entry] }
@@ -1250,7 +1250,7 @@ def common_data(transport)
12501250
}
12511251

12521252
let(:plugins) do
1253-
plugins = Bolt::Plugin.new(nil, pal, Bolt::Analytics::NoopClient.new)
1253+
plugins = Bolt::Plugin.setup(config, pal, nil, Bolt::Analytics::NoopClient.new)
12541254
plugin = double('plugin')
12551255
allow(plugin).to receive(:name).and_return('test_plugin')
12561256
allow(plugin).to receive(:hooks).and_return([:resolve_reference])

0 commit comments

Comments
 (0)