bolt 3.14.1 → 3.17.0

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of bolt might be problematic. Click here for more details.

Files changed (42) hide show
  1. checksums.yaml +4 -4
  2. data/Puppetfile +1 -1
  3. data/bolt-modules/boltlib/lib/puppet/functions/apply_prep.rb +137 -104
  4. data/guides/debugging.yaml +27 -0
  5. data/guides/inventory.yaml +23 -0
  6. data/guides/links.yaml +12 -0
  7. data/guides/logging.yaml +17 -0
  8. data/guides/module.yaml +18 -0
  9. data/guides/modulepath.yaml +24 -0
  10. data/guides/project.yaml +21 -0
  11. data/guides/targets.yaml +28 -0
  12. data/guides/transports.yaml +22 -0
  13. data/lib/bolt/analytics.rb +2 -19
  14. data/lib/bolt/application.rb +634 -0
  15. data/lib/bolt/bolt_option_parser.rb +28 -4
  16. data/lib/bolt/cli.rb +592 -788
  17. data/lib/bolt/fiber_executor.rb +7 -3
  18. data/lib/bolt/inventory/inventory.rb +68 -39
  19. data/lib/bolt/inventory.rb +2 -9
  20. data/lib/bolt/module_installer/puppetfile.rb +24 -10
  21. data/lib/bolt/outputter/human.rb +83 -32
  22. data/lib/bolt/outputter/json.rb +63 -38
  23. data/lib/bolt/pal/yaml_plan/transpiler.rb +1 -1
  24. data/lib/bolt/pal.rb +31 -11
  25. data/lib/bolt/plan_creator.rb +84 -25
  26. data/lib/bolt/plan_future.rb +11 -6
  27. data/lib/bolt/plan_result.rb +1 -1
  28. data/lib/bolt/plugin/task.rb +1 -1
  29. data/lib/bolt/plugin.rb +11 -17
  30. data/lib/bolt/project.rb +0 -7
  31. data/lib/bolt/result_set.rb +2 -1
  32. data/lib/bolt/transport/local/connection.rb +17 -1
  33. data/lib/bolt/transport/orch/connection.rb +13 -1
  34. data/lib/bolt/version.rb +1 -1
  35. data/lib/bolt_server/file_cache.rb +12 -0
  36. data/lib/bolt_server/schemas/action-apply.json +32 -0
  37. data/lib/bolt_server/schemas/action-apply_prep.json +19 -0
  38. data/lib/bolt_server/transport_app.rb +113 -24
  39. data/lib/bolt_spec/bolt_context.rb +1 -1
  40. data/lib/bolt_spec/run.rb +1 -1
  41. metadata +14 -3
  42. data/lib/bolt/secret.rb +0 -37
@@ -0,0 +1,28 @@
1
+ ---
2
+ topic: targets
3
+ guide: |
4
+ A target is a device that Bolt connects to and runs actions on. Targets can
5
+ be physical, such as servers, or virtual, such as containers or virtual
6
+ machines.
7
+
8
+ Several of Bolt's commands connect to targets and run actions on them.
9
+ These commands require a target or targets to run on. You can specify
10
+ targets to a command using one of the following command-line options:
11
+
12
+ *nix options Powershell options
13
+ -t, --targets TARGETS -T, -Targets TARGETS
14
+ -q, --query QUERY -Q, -Query QUERY
15
+ --rerun FILTER -Rerun FILTER
16
+
17
+ The 'targets' option accepts a comma-separated list of target URIs or group
18
+ names, or can read a target list from an input file '@<file>' or stdin '-'.
19
+ URIs can be specified with the format [protocol://][user@]host[:port]. To
20
+ learn more about available protocols and their defaults, run 'bolt guide
21
+ transports'.
22
+
23
+ Typically, targets and their configuration and data are listed in a
24
+ project's inventory file. For more information about inventory files,
25
+ see 'bolt guide inventory'.
26
+
27
+ documentation:
28
+ - https://pup.pt/bolt-commands
@@ -0,0 +1,22 @@
1
+ ---
2
+ topic: transports
3
+ guide: |
4
+ Bolt uses transports (also known as protocols) to establish a connection
5
+ with a target in order to run actions on the target. The default transport is
6
+ SSH, and you can see available transports along with their configuration
7
+ options and defaults at http://pup.pt/bolt-reference.
8
+
9
+ You can specify a transport for a target by prepending '<transport>://' to
10
+ the target's URI. For example, to connect to a target with hostname
11
+ 'example.com' as user 'Administrator' using the WinRM transport, you would
12
+ pass the following to the target flag:
13
+ winrm://Administrator@example.com
14
+
15
+ You can also specify a default transport for all targets by passing the
16
+ '--transport' flag on *nix systems and the '-Transport' flag in Powershell.
17
+ Finally, you can set the transport for a target in the inventory. For more
18
+ information about the Bolt inventory, run 'bolt guide inventory'.
19
+
20
+ documentation:
21
+ - https://pup.pt/bolt-commands#specify-a-transport
22
+ - http://pup.pt/bolt-inventory#transport-configuration
@@ -30,7 +30,6 @@ module Bolt
30
30
  }.freeze
31
31
 
32
32
  def self.build_client(enabled = true)
33
- logger = Bolt::Logger.logger(self)
34
33
  begin
35
34
  config_file = config_path
36
35
  config = enabled ? load_config(config_file) : {}
@@ -39,7 +38,7 @@ module Bolt
39
38
  end
40
39
 
41
40
  if !enabled || config['disabled'] || ENV['BOLT_DISABLE_ANALYTICS']
42
- logger.debug "Analytics opt-out is set, analytics will be disabled"
41
+ Bolt::Logger.debug "Analytics opt-out is set, analytics will be disabled"
43
42
  NoopClient.new
44
43
  else
45
44
  unless config.key?('user-id')
@@ -50,7 +49,7 @@ module Bolt
50
49
  Client.new(config['user-id'])
51
50
  end
52
51
  rescue StandardError => e
53
- logger.debug "Failed to initialize analytics client, analytics will be disabled: #{e}"
52
+ Bolt::Logger.debug "Failed to initialize analytics client, analytics will be disabled: #{e}"
54
53
  NoopClient.new
55
54
  end
56
55
 
@@ -139,18 +138,6 @@ module Bolt
139
138
  end
140
139
  end
141
140
 
142
- def plan_counts(plans_path)
143
- pp_count, yaml_count = if File.exist?(plans_path)
144
- %w[pp yaml].map do |extension|
145
- Find.find(plans_path.to_s).grep(/.*\.#{extension}/).length
146
- end
147
- else
148
- [0, 0]
149
- end
150
-
151
- { puppet_plan_count: pp_count, yaml_plan_count: yaml_count }
152
- end
153
-
154
141
  def event(category, action, label: nil, value: nil, **kwargs)
155
142
  custom_dimensions = Bolt::Util.walk_keys(kwargs) do |k|
156
143
  CUSTOM_DIMENSIONS[k] || raise("Unknown analytics key '#{k}'")
@@ -236,10 +223,6 @@ module Bolt
236
223
 
237
224
  def report_bundled_content(mode, name); end
238
225
 
239
- def plan_counts(_)
240
- {}
241
- end
242
-
243
226
  def event(category, action, **_kwargs)
244
227
  @logger.trace "Skipping submission of '#{category} #{action}' event because analytics is disabled"
245
228
  end
@@ -0,0 +1,634 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'benchmark'
4
+
5
+ require 'bolt/plan_creator'
6
+ require 'bolt/util'
7
+
8
+ module Bolt
9
+ class Application
10
+ attr_reader :analytics, :config, :executor, :inventory, :logger, :pal, :plugins
11
+ private :analytics, :config, :executor, :inventory, :logger, :pal, :plugins
12
+
13
+ def initialize(
14
+ analytics:,
15
+ config:,
16
+ executor:,
17
+ inventory:,
18
+ pal:,
19
+ plugins:
20
+ )
21
+ @analytics = analytics
22
+ @config = config
23
+ @executor = executor
24
+ @inventory = inventory
25
+ @logger = Bolt::Logger.logger(self)
26
+ @pal = pal
27
+ @plugins = plugins
28
+ end
29
+
30
+ # Shuts down the application.
31
+ #
32
+ def shutdown
33
+ executor.shutdown
34
+ end
35
+
36
+ # Apply Puppet manifest code to a list of targets.
37
+ #
38
+ # @param manifest [String, NilClass] The path to a Puppet manifest file.
39
+ # @param targets [Array[String]] The targets to run on.
40
+ # @param code [String] Puppet manifest code to apply.
41
+ # @param noop [Boolean] Whether to apply in no-operation mode.
42
+ # @return [Bolt::ResultSet]
43
+ #
44
+ def apply(manifest, targets, code: '', noop: false)
45
+ manifest_code = if manifest
46
+ Bolt::Util.validate_file('manifest', manifest)
47
+ File.read(File.expand_path(manifest))
48
+ else
49
+ code
50
+ end
51
+
52
+ targets = inventory.get_targets(targets)
53
+
54
+ Puppet[:tasks] = false
55
+ ast = pal.parse_manifest(manifest_code, manifest)
56
+
57
+ if defined?(ast.body) &&
58
+ (ast.body.is_a?(Puppet::Pops::Model::HostClassDefinition) ||
59
+ ast.body.is_a?(Puppet::Pops::Model::ResourceTypeDefinition))
60
+ message = "Manifest only contains definitions and will result in no changes on the targets. "\
61
+ "Definitions must be declared for their resources to be applied. You can read more "\
62
+ "about defining and declaring classes and types in the Puppet documentation at "\
63
+ "https://puppet.com/docs/puppet/latest/lang_classes.html and "\
64
+ "https://puppet.com/docs/puppet/latest/lang_defined_types.html"
65
+ Bolt::Logger.warn("empty_manifest", message)
66
+ end
67
+
68
+ # Apply logging looks like plan logging
69
+ executor.publish_event(type: :plan_start, plan: nil)
70
+
71
+ with_benchmark do
72
+ apply_prep_results = pal.in_plan_compiler(executor, inventory, plugins.puppetdb_client) do |compiler|
73
+ compiler.call_function('apply_prep', targets, '_catch_errors' => true)
74
+ end
75
+
76
+ apply_results = pal.with_bolt_executor(executor, inventory, plugins.puppetdb_client) do
77
+ Puppet.lookup(:apply_executor)
78
+ .apply_ast(ast, apply_prep_results.ok_set.targets, catch_errors: true, noop: noop)
79
+ end
80
+
81
+ Bolt::ResultSet.new(apply_prep_results.error_set.results + apply_results.results)
82
+ end
83
+ end
84
+
85
+ # Run a command on a list of targets.
86
+ #
87
+ # @param command [String] The command.
88
+ # @param targets [Array[String]] The targets to run on.
89
+ # @param env_vars [Hash] Environment variables to set on the target.
90
+ # @return [Bolt::ResultSet]
91
+ #
92
+ def run_command(command, targets, env_vars: nil)
93
+ targets = inventory.get_targets(targets)
94
+
95
+ with_benchmark do
96
+ executor.run_command(targets, command, env_vars: env_vars)
97
+ end
98
+ end
99
+
100
+ # Download a file from a list of targets to a directory on the controller.
101
+ #
102
+ # @param source [String] The path to the file on the targets.
103
+ # @param destination [String] The path to the directory on the controller.
104
+ # @param targets [Array[String]] The targets to run on.
105
+ # @return [Bolt::ResultSet]
106
+ #
107
+ def download_file(source, destination, targets)
108
+ destination = File.expand_path(destination, Dir.pwd)
109
+ targets = inventory.get_targets(targets)
110
+
111
+ with_benchmark do
112
+ executor.download_file(targets, source, destination)
113
+ end
114
+ end
115
+
116
+ # Upload a file from the controller to a list of targets.
117
+ #
118
+ # @param source [String] The path to the file on the controller.
119
+ # @param destination [String] The destination path on the targets.
120
+ # @param targets [Array[String]] The targets to run on.
121
+ # @return [Bolt::ResultSet]
122
+ #
123
+ def upload_file(source, destination, targets)
124
+ source = find_file(source)
125
+ targets = inventory.get_targets(targets)
126
+
127
+ Bolt::Util.validate_file('source file', source, true)
128
+
129
+ with_benchmark do
130
+ executor.upload_file(targets, source, destination)
131
+ end
132
+ end
133
+
134
+ # Show groups in the inventory.
135
+ #
136
+ # @return [Hash]
137
+ #
138
+ def list_groups
139
+ {
140
+ count: inventory.group_names.count,
141
+ groups: inventory.group_names.sort,
142
+ inventory: {
143
+ default: config.default_inventoryfile.to_s,
144
+ source: inventory.source
145
+ }
146
+ }
147
+ end
148
+
149
+ # Show available guides.
150
+ #
151
+ # @param guides [Hash] A map of topics to paths to guides.
152
+ # @param outputter [Bolt::Outputter] An outputter instance.
153
+ # @return [Boolean]
154
+ #
155
+ def list_guides
156
+ { topics: load_guides.keys }
157
+ end
158
+
159
+ # Show a guide.
160
+ #
161
+ # @param topic [String] The topic to show.
162
+ # @param guides [Hash] A map of topics to paths to guides.
163
+ # @param outputter [Bolt::Outputter] An outputter instance.
164
+ # @return [Boolean]
165
+ #
166
+ def show_guide(topic)
167
+ if (path = load_guides[topic])
168
+ analytics.event('Guide', 'known_topic', label: topic)
169
+
170
+ begin
171
+ guide = Bolt::Util.read_yaml_hash(path, 'guide')
172
+ rescue SystemCallError => e
173
+ raise Bolt::FileError("#{e.message}: unable to load guide page", filepath)
174
+ end
175
+
176
+ # Make sure both topic and guide keys are defined
177
+ unless (%w[topic guide] - guide.keys).empty?
178
+ msg = "Guide file #{path} must have a 'topic' key and 'guide' key, but has #{guide.keys} keys."
179
+ raise Bolt::Error.new(msg, 'bolt/invalid-guide')
180
+ end
181
+
182
+ Bolt::Util.symbolize_top_level_keys(guide)
183
+ else
184
+ analytics.event('Guide', 'unknown_topic', label: topic)
185
+ raise Bolt::Error.new(
186
+ "Unknown topic '#{topic}'. For a list of available topics, run 'bolt guide'.",
187
+ 'bolt/unknown-topic'
188
+ )
189
+ end
190
+ end
191
+
192
+ # Show inventory information.
193
+ #
194
+ # @param targets [Array[String]] The targets to show.
195
+ # @return [Hash]
196
+ #
197
+ def show_inventory(targets = nil)
198
+ targets = group_targets_by_source(targets || ['all'])
199
+
200
+ {
201
+ adhoc: {
202
+ count: targets[:adhoc].count,
203
+ targets: targets[:adhoc].map(&:detail)
204
+ },
205
+ inventory: {
206
+ count: targets[:inventory].count,
207
+ targets: targets[:inventory].map(&:detail),
208
+ file: (inventory.source || config.default_inventoryfile).to_s,
209
+ default: config.default_inventoryfile.to_s
210
+ },
211
+ targets: targets.values.flatten.map(&:detail),
212
+ count: targets.values.flatten.count
213
+ }
214
+ end
215
+
216
+ # Lookup a value with Hiera.
217
+ #
218
+ # @param key [String] The key to look up in the hierarchy.
219
+ # @param targets [Array[String]] The targets to use as context.
220
+ # @param vars [Hash] Variables to set in the scope.
221
+ # @return [Bolt::ResultSet, String] The result of the lookup.
222
+ #
223
+ def lookup(key, targets, vars: {})
224
+ executor.publish_event(type: :plan_start, plan: nil)
225
+
226
+ with_benchmark do
227
+ pal.lookup(key,
228
+ inventory.get_targets(targets),
229
+ inventory,
230
+ executor,
231
+ plan_vars: vars)
232
+ end
233
+ end
234
+
235
+ # Lookup a value with Hiera using plan_hierarchy.
236
+ #
237
+ # @param key [String] The key to lookup up in the plan_hierarchy.
238
+ # @param vars [Hash] Variables to set in the scope.
239
+ # @return [String] The result of the lookup.
240
+ #
241
+ def plan_lookup(key, vars: {})
242
+ pal.plan_hierarchy_lookup(key, plan_vars: vars)
243
+ end
244
+
245
+ # Add a new module to the project.
246
+ #
247
+ # @param name [String] The name of the module to add.
248
+ # @param outputter [Bolt::Outputter] An outputter instance.
249
+ # @return [Boolean]
250
+ #
251
+ def add_module(name, outputter)
252
+ assert_project_file(config.project)
253
+
254
+ installer = Bolt::ModuleInstaller.new(outputter, pal)
255
+
256
+ installer.add(name,
257
+ config.project.modules,
258
+ config.project.puppetfile,
259
+ config.project.managed_moduledir,
260
+ config.project.project_file,
261
+ config.module_install)
262
+ end
263
+
264
+ # Generate Puppet data types from project modules.
265
+ #
266
+ # @return [Boolean]
267
+ #
268
+ def generate_types
269
+ pal.generate_types(cache: true)
270
+ end
271
+
272
+ # Install the project's modules.
273
+ #
274
+ # @param outputter [Bolt::Outputter] An outputter instance.
275
+ # @param force [Boolean] Forcibly install modules.
276
+ # @param resolve [Boolean] Resolve module dependencies.
277
+ # @return [Boolean]
278
+ #
279
+ def install_modules(outputter, force: false, resolve: true)
280
+ assert_project_file(config.project)
281
+
282
+ if config.project.modules.empty? && resolve
283
+ outputter.print_message(
284
+ "Project configuration file #{config.project.project_file} does not "\
285
+ "specify any module dependencies. Nothing to do."
286
+ )
287
+ return true
288
+ end
289
+
290
+ installer = Bolt::ModuleInstaller.new(outputter, pal)
291
+
292
+ installer.install(config.project.modules,
293
+ config.project.puppetfile,
294
+ config.project.managed_moduledir,
295
+ config.module_install,
296
+ force: force,
297
+ resolve: resolve)
298
+ end
299
+
300
+ # Show modules available to the project.
301
+ #
302
+ # @return [Hash] A map of module directories to module definitions.
303
+ #
304
+ def list_modules
305
+ pal.list_modules
306
+ end
307
+
308
+ # Show module information.
309
+ #
310
+ # @param name [String] The name of the module.
311
+ # @return [Hash] The module information.
312
+ #
313
+ def show_module(name)
314
+ pal.show_module(name)
315
+ end
316
+
317
+ # Convert a YAML plan to a Puppet language plan.
318
+ #
319
+ # @param plan [String] The plan to convert. Can be a plan name or a path.
320
+ # @return [String] The converted plan.
321
+ #
322
+ def convert_plan(plan)
323
+ pal.convert_plan(plan)
324
+ end
325
+
326
+ # Create a new project-level plan.
327
+ #
328
+ # @param name [String] The name of the new plan.
329
+ # @param puppet [Boolean] Create a Puppet language plan.
330
+ # @param plan_script [String] Reference to the script to run in the new plan.
331
+ # @return [Boolean]
332
+ #
333
+ def new_plan(name, puppet: false, plan_script: nil)
334
+ Bolt::PlanCreator.validate_plan_name(config.project, name)
335
+
336
+ if plan_script && !config.future&.fetch('file_paths', false)
337
+ raise Bolt::CLIError,
338
+ "The --script flag can only be used if future.file_paths is " \
339
+ "configured in bolt-project.yaml."
340
+ end
341
+
342
+ if plan_script
343
+ Bolt::Util.validate_file('script', find_file(plan_script))
344
+ end
345
+
346
+ Bolt::PlanCreator.create_plan(config.project.plans_path,
347
+ name,
348
+ is_puppet: puppet,
349
+ script: plan_script)
350
+ end
351
+
352
+ # Run a plan.
353
+ #
354
+ # @param plan [String] The plan to run.
355
+ # @param targets [Array[String], NilClass] The targets to pass to the plan.
356
+ # @param params [Hash] Parameters to pass to the plan.
357
+ # @return [Bolt::PlanResult]
358
+ #
359
+ def run_plan(plan, targets, params: {})
360
+ if targets && targets.any?
361
+ if params['nodes'] || params['targets']
362
+ key = params.include?('nodes') ? 'nodes' : 'targets'
363
+ raise Bolt::CLIError,
364
+ "A plan's '#{key}' parameter can be specified using the --#{key} option, but in that " \
365
+ "case it must not be specified as a separate #{key}=<value> parameter nor included " \
366
+ "in the JSON data passed in the --params option"
367
+ end
368
+
369
+ plan_params = pal.get_plan_info(plan)['parameters']
370
+ target_param = plan_params.dig('targets', 'type') =~ /TargetSpec/
371
+ node_param = plan_params.include?('nodes')
372
+
373
+ if node_param && target_param
374
+ msg = "Plan parameters include both 'nodes' and 'targets' with type 'TargetSpec', " \
375
+ "neither will populated with the value for --nodes or --targets."
376
+ Bolt::Logger.warn("nodes_targets_parameters", msg)
377
+ elsif node_param
378
+ params['nodes'] = targets.join(',')
379
+ elsif target_param
380
+ params['targets'] = targets.join(',')
381
+ end
382
+ end
383
+
384
+ plan_context = { plan_name: plan, params: params }
385
+
386
+ executor.start_plan(plan_context)
387
+ result = pal.run_plan(plan, params, executor, inventory, plugins.puppetdb_client)
388
+ executor.finish_plan(result)
389
+
390
+ result
391
+ end
392
+
393
+ # Show plan information.
394
+ #
395
+ # @param plan [String] The name of the plan to show.
396
+ # @return [Hash]
397
+ #
398
+ def show_plan(plan)
399
+ pal.get_plan_info(plan)
400
+ end
401
+
402
+ # List plans available to the project.
403
+ #
404
+ # @param filter [String] A substring to filter plans by.
405
+ # @return [Hash]
406
+ #
407
+ def list_plans(filter: nil)
408
+ {
409
+ plans: filter_content(pal.list_plans_with_cache(filter_content: true), filter),
410
+ modulepath: pal.user_modulepath
411
+ }
412
+ end
413
+
414
+ # Show available plugins.
415
+ #
416
+ # @return [Hash]
417
+ #
418
+ def list_plugins
419
+ { plugins: plugins.list_plugins, modulepath: pal.user_modulepath }
420
+ end
421
+
422
+ # Initialize the current directory as a Bolt project.
423
+ #
424
+ # @param name [String] The name of the project.
425
+ # @param [Bolt::Outputter] An outputter instance.
426
+ # @param modules [Array[String], NilClass] Modules to install.
427
+ # @return [Boolean]
428
+ #
429
+ def create_project(name, outputter, modules: nil)
430
+ Bolt::ProjectManager.new(config, outputter, pal)
431
+ .create(Dir.pwd, name, modules)
432
+ end
433
+
434
+ # Migrate a project to current best practices.
435
+ #
436
+ # @param [Bolt::Outputter] An outputter instance.
437
+ # @return [Boolean]
438
+ #
439
+ def migrate_project(outputter)
440
+ Bolt::ProjectManager.new(config, outputter, pal).migrate
441
+ end
442
+
443
+ # Run a script on a list of targets.
444
+ #
445
+ # @param script [String] The path to the script to run.
446
+ # @param targets [Array[String]] The targets to run on.
447
+ # @param arguments [Array[String], NilClass] Arguments to pass to the script.
448
+ # @param env_vars [Hash] Environment variables to set on the target.
449
+ # @return [Bolt::ResultSet]
450
+ #
451
+ def run_script(script, targets, arguments: [], env_vars: nil)
452
+ script = find_file(script)
453
+
454
+ Bolt::Util.validate_file('script', script)
455
+
456
+ with_benchmark do
457
+ executor.run_script(inventory.get_targets(targets), script, arguments, env_vars: env_vars)
458
+ end
459
+ end
460
+
461
+ # Generate a keypair using the configured secret plugin.
462
+ #
463
+ # @param force [Boolean] Forcibly create a keypair.
464
+ # @param plugin [String] The secret plugin to use.
465
+ # @return [Boolean]
466
+ #
467
+ def create_secret_keys(force: false, plugin: 'pkcs7')
468
+ unless plugins.by_name(plugin)
469
+ raise Bolt::Plugin::PluginError::Unknown, plugin
470
+ end
471
+
472
+ plugins.get_hook(plugin, :secret_createkeys)
473
+ .call('force' => force)
474
+ end
475
+
476
+ # Decrypt ciphertext using the configured secret plugin.
477
+ #
478
+ # @param ciphertext [String] The ciphertext to decrypt.
479
+ # @param plugin [String] The secret plugin to use.
480
+ # @return [Boolean]
481
+ #
482
+ def decrypt_secret(ciphertext, plugin: 'pkcs7')
483
+ unless plugins.by_name(plugin)
484
+ raise Bolt::Plugin::PluginError::Unknown, plugin
485
+ end
486
+
487
+ plugins.get_hook(plugin, :secret_decrypt)
488
+ .call('encrypted_value' => ciphertext)
489
+ end
490
+
491
+ # Encrypt plaintext using the configured secret plugin.
492
+ #
493
+ # @param plaintext [String] The plaintext to encrypt.
494
+ # @param plugin [String] The secret plugin to use.
495
+ # @return [Boolean]
496
+ #
497
+ def encrypt_secret(plaintext, plugin: 'pkcs7')
498
+ unless plugins.by_name(plugin)
499
+ raise Bolt::Plugin::PluginError::Unknown, plugin
500
+ end
501
+
502
+ plugins.get_hook(plugin, :secret_encrypt)
503
+ .call('plaintext_value' => plaintext)
504
+ end
505
+
506
+ # Run a task on a list of targets.
507
+ #
508
+ # @param task [String] The name of the task.
509
+ # @param options [Hash] Additional options.
510
+ # @return [Bolt::ResultSet]
511
+ #
512
+ def run_task(task, targets, params: {})
513
+ targets = inventory.get_targets(targets)
514
+
515
+ with_benchmark do
516
+ pal.run_task(task, targets, params, executor, inventory)
517
+ end
518
+ end
519
+
520
+ # Show task information.
521
+ #
522
+ # @param task [String] The name of the task to show.
523
+ # @return [Hash]
524
+ #
525
+ def show_task(task)
526
+ { task: pal.get_task(task) }
527
+ end
528
+
529
+ # List available tasks.
530
+ #
531
+ # @param filter [String] A substring to filter tasks by.
532
+ # @return [Hash]
533
+ #
534
+ def list_tasks(filter: nil)
535
+ {
536
+ tasks: filter_content(pal.list_tasks_with_cache(filter_content: true), filter),
537
+ modulepath: pal.user_modulepath
538
+ }
539
+ end
540
+
541
+ # Assert that there is a project configuration file.
542
+ #
543
+ # @param project [Bolt::Project] The Bolt project.
544
+ #
545
+ private def assert_project_file(project)
546
+ unless project.project_file?
547
+ command = Bolt::Util.powershell? ? 'New-BoltProject' : 'bolt project init'
548
+
549
+ msg = "Could not find project configuration file #{project.project_file}, unable "\
550
+ "to install modules. To create a Bolt project, run '#{command}'."
551
+
552
+ raise Bolt::Error.new(msg, 'bolt/missing-project-config-error')
553
+ end
554
+ end
555
+
556
+ # Filter a list of content by matching substring.
557
+ #
558
+ # @param content [Hash] The content to filter.
559
+ # @param filter [String] The substring to filter content by.
560
+ #
561
+ private def filter_content(content, filter)
562
+ return content unless content && filter
563
+ content.select { |name,| name.include?(filter) }
564
+ end
565
+
566
+ # Return the path to a file. If the path is an absolute or relative path to
567
+ # a file, and the file exists, return the path as-is. Otherwise, check if
568
+ # the path is a Puppet file path and look for the file in a module's files
569
+ # directory.
570
+ #
571
+ # @param path [String] The path to the file.
572
+ #
573
+ private def find_file(path)
574
+ return path if File.exist?(path) || Pathname.new(path).absolute?
575
+ modulepath = Bolt::Config::Modulepath.new(config.modulepath)
576
+ modules = Bolt::Module.discover(modulepath.full_modulepath, config.project)
577
+ mod, file = path.split(File::SEPARATOR, 2)
578
+ future = executor.future&.fetch('file_paths', false)
579
+
580
+ if modules[mod]
581
+ logger.debug("Did not find file at #{File.expand_path(path)}, checking in module '#{mod}'")
582
+ found = Bolt::Util.find_file_in_module(modules[mod].path, file || "", future)
583
+ path = found.nil? ? File.join(modules[mod].path, 'files', file) : found
584
+ end
585
+
586
+ path
587
+ end
588
+
589
+ # Get a list of Bolt guides.
590
+ #
591
+ private def load_guides
592
+ root_path = File.expand_path(File.join(__dir__, '..', '..', 'guides'))
593
+ files = Dir.children(root_path).sort
594
+
595
+ files.each_with_object({}) do |file, guides|
596
+ next if file !~ /\.(yaml|yml)\z/
597
+ topic = File.basename(file, ".*")
598
+ guides[topic] = File.join(root_path, file)
599
+ end
600
+ rescue SystemCallError => e
601
+ raise Bolt::FileError.new("#{e.message}: unable to load guides directory", root_path)
602
+ end
603
+
604
+ # Return a hash of targets sorted by those that are found in the inventory
605
+ # and those that are provided on the command line.
606
+ #
607
+ # @param targets [Array[String]] The targets to group.
608
+ #
609
+ private def group_targets_by_source(targets)
610
+ # Retrieve the known group and target names. This needs to be done before
611
+ # updating targets, as that will add adhoc targets to the inventory.
612
+ known_names = inventory.target_names
613
+ targets = inventory.get_targets(targets)
614
+
615
+ inventory_targets, adhoc_targets = targets.partition do |target|
616
+ known_names.include?(target.name)
617
+ end
618
+
619
+ { inventory: inventory_targets, adhoc: adhoc_targets }
620
+ end
621
+
622
+ # Benchmark the action and set the elapsed time on the result.
623
+ #
624
+ private def with_benchmark
625
+ result = nil
626
+
627
+ elapsed_time = Benchmark.realtime do
628
+ result = yield
629
+ end
630
+
631
+ result.tap { |r| r.elapsed_time = elapsed_time if r.is_a?(Bolt::ResultSet) }
632
+ end
633
+ end
634
+ end