bolt 3.0.1 → 3.6.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 (82) hide show
  1. checksums.yaml +4 -4
  2. data/Puppetfile +13 -11
  3. data/bolt-modules/boltlib/lib/puppet/datatypes/containerresult.rb +24 -0
  4. data/bolt-modules/boltlib/lib/puppet/functions/add_facts.rb +1 -1
  5. data/bolt-modules/boltlib/lib/puppet/functions/run_command.rb +20 -2
  6. data/bolt-modules/boltlib/lib/puppet/functions/run_container.rb +162 -0
  7. data/bolt-modules/boltlib/lib/puppet/functions/run_plan.rb +2 -2
  8. data/bolt-modules/boltlib/lib/puppet/functions/run_script.rb +44 -5
  9. data/bolt-modules/boltlib/lib/puppet/functions/upload_file.rb +1 -1
  10. data/bolt-modules/boltlib/types/planresult.pp +1 -0
  11. data/bolt-modules/file/lib/puppet/functions/file/read.rb +3 -2
  12. data/bolt-modules/prompt/lib/puppet/functions/prompt.rb +20 -2
  13. data/bolt-modules/prompt/lib/puppet/functions/prompt/menu.rb +103 -0
  14. data/lib/bolt/analytics.rb +4 -8
  15. data/lib/bolt/apply_result.rb +1 -1
  16. data/lib/bolt/bolt_option_parser.rb +6 -3
  17. data/lib/bolt/cli.rb +123 -37
  18. data/lib/bolt/config.rb +15 -7
  19. data/lib/bolt/config/options.rb +62 -12
  20. data/lib/bolt/config/transport/lxd.rb +23 -0
  21. data/lib/bolt/config/transport/options.rb +8 -1
  22. data/lib/bolt/config/transport/podman.rb +33 -0
  23. data/lib/bolt/container_result.rb +105 -0
  24. data/lib/bolt/error.rb +15 -0
  25. data/lib/bolt/executor.rb +37 -18
  26. data/lib/bolt/inventory/options.rb +9 -0
  27. data/lib/bolt/inventory/target.rb +16 -0
  28. data/lib/bolt/logger.rb +8 -0
  29. data/lib/bolt/module_installer.rb +2 -2
  30. data/lib/bolt/module_installer/puppetfile.rb +2 -2
  31. data/lib/bolt/module_installer/specs/forge_spec.rb +2 -2
  32. data/lib/bolt/module_installer/specs/git_spec.rb +2 -2
  33. data/lib/bolt/node/output.rb +14 -4
  34. data/lib/bolt/outputter/human.rb +259 -90
  35. data/lib/bolt/outputter/json.rb +3 -1
  36. data/lib/bolt/outputter/logger.rb +17 -0
  37. data/lib/bolt/pal.rb +25 -4
  38. data/lib/bolt/pal/yaml_plan.rb +1 -2
  39. data/lib/bolt/pal/yaml_plan/evaluator.rb +5 -141
  40. data/lib/bolt/pal/yaml_plan/step.rb +91 -31
  41. data/lib/bolt/pal/yaml_plan/step/command.rb +21 -13
  42. data/lib/bolt/pal/yaml_plan/step/download.rb +15 -16
  43. data/lib/bolt/pal/yaml_plan/step/eval.rb +11 -11
  44. data/lib/bolt/pal/yaml_plan/step/message.rb +13 -4
  45. data/lib/bolt/pal/yaml_plan/step/plan.rb +19 -15
  46. data/lib/bolt/pal/yaml_plan/step/resources.rb +82 -21
  47. data/lib/bolt/pal/yaml_plan/step/script.rb +36 -17
  48. data/lib/bolt/pal/yaml_plan/step/task.rb +19 -16
  49. data/lib/bolt/pal/yaml_plan/step/upload.rb +16 -17
  50. data/lib/bolt/pal/yaml_plan/transpiler.rb +3 -3
  51. data/lib/bolt/plan_creator.rb +1 -1
  52. data/lib/bolt/plugin.rb +13 -11
  53. data/lib/bolt/project_manager.rb +1 -1
  54. data/lib/bolt/project_manager/module_migrator.rb +1 -1
  55. data/lib/bolt/result.rb +11 -15
  56. data/lib/bolt/shell.rb +16 -0
  57. data/lib/bolt/shell/bash.rb +61 -31
  58. data/lib/bolt/shell/bash/tmpdir.rb +2 -2
  59. data/lib/bolt/shell/powershell.rb +34 -12
  60. data/lib/bolt/shell/powershell/snippets.rb +30 -3
  61. data/lib/bolt/task.rb +1 -1
  62. data/lib/bolt/transport/base.rb +0 -9
  63. data/lib/bolt/transport/docker.rb +2 -126
  64. data/lib/bolt/transport/docker/connection.rb +81 -167
  65. data/lib/bolt/transport/lxd.rb +26 -0
  66. data/lib/bolt/transport/lxd/connection.rb +99 -0
  67. data/lib/bolt/transport/orch.rb +13 -5
  68. data/lib/bolt/transport/podman.rb +19 -0
  69. data/lib/bolt/transport/podman/connection.rb +98 -0
  70. data/lib/bolt/transport/ssh/connection.rb +1 -1
  71. data/lib/bolt/transport/winrm/connection.rb +1 -1
  72. data/lib/bolt/util.rb +42 -0
  73. data/lib/bolt/version.rb +1 -1
  74. data/lib/bolt_server/transport_app.rb +64 -33
  75. data/lib/bolt_spec/bolt_context.rb +9 -4
  76. data/lib/bolt_spec/plans.rb +1 -109
  77. data/lib/bolt_spec/plans/action_stubs.rb +1 -1
  78. data/lib/bolt_spec/plans/action_stubs/command_stub.rb +8 -1
  79. data/lib/bolt_spec/plans/action_stubs/script_stub.rb +8 -1
  80. data/lib/bolt_spec/plans/mock_executor.rb +91 -7
  81. data/modules/puppet_connect/plans/test_input_data.pp +65 -7
  82. metadata +12 -2
@@ -11,12 +11,24 @@ Puppet::Functions.create_function(:prompt) do
11
11
  # @option options [Boolean] sensitive Disable echo back and mark the response as sensitive.
12
12
  # The returned value will be wrapped by the `Sensitive` data type. To access the raw
13
13
  # value, use the `unwrap` function (i.e. `$sensitive_value.unwrap`).
14
+ # @option options [String] default The default value to return if the user does not provide
15
+ # input or if stdin is not a tty.
14
16
  # @return The response to the prompt.
15
17
  # @example Prompt the user if plan execution should continue
16
18
  # $response = prompt('Continue executing plan? [Y\N]')
17
19
  # @example Prompt the user for sensitive information
18
20
  # $password = prompt('Enter your password', 'sensitive' => true)
19
21
  # out::message("Password is: ${password.unwrap}")
22
+ # @example Prompt the user and provide a default value
23
+ # $user = prompt('Enter your login username', 'default' => 'root')
24
+ # @example Prompt the user for sensitive information, returning a sensitive default if one is not provided
25
+ # $token = prompt('Enter token', 'default' => lookup('default_token'), 'sensitive' => true)
26
+ # out::message("Token is: ${token.unwrap}")
27
+ # @example Prompt the user and fail with a custom message if no input was provided
28
+ # $response = prompt('Enter your name', 'default' => '')
29
+ # if $response.empty {
30
+ # fail_plan('Must provide your name')
31
+ # }
20
32
  dispatch :prompt do
21
33
  param 'String', :prompt
22
34
  optional_param 'Hash[String[1], Any]', :options
@@ -30,14 +42,20 @@ Puppet::Functions.create_function(:prompt) do
30
42
  action: 'prompt')
31
43
  end
32
44
 
33
- options = options.transform_keys(&:to_sym)
34
-
45
+ options = options.transform_keys(&:to_sym)
35
46
  executor = Puppet.lookup(:bolt_executor)
47
+
36
48
  # Send analytics report
37
49
  executor.report_function_call(self.class.name)
38
50
 
51
+ # Require default to be a string value
52
+ if options.key?(:default) && !options[:default].is_a?(String)
53
+ raise Bolt::ValidationError, "Option 'default' must be a string"
54
+ end
55
+
39
56
  response = executor.prompt(prompt, options)
40
57
 
58
+ # If sensitive, wrap it
41
59
  if options[:sensitive]
42
60
  Puppet::Pops::Types::PSensitiveType::Sensitive.new(response)
43
61
  else
@@ -0,0 +1,103 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bolt/error'
4
+
5
+ # Display a menu prompt and wait for a response. Continues to prompt
6
+ # until an option from the menu is selected.
7
+ #
8
+ # > **Note:** Not available in apply block
9
+ Puppet::Functions.create_function(:'prompt::menu') do
10
+ # Select from a list of options.
11
+ # @param prompt The prompt to display.
12
+ # @param menu A list of options to choose from.
13
+ # @param options A hash of additional options.
14
+ # @option options [String] default The default option to return if the user does not provide
15
+ # input or if standard in (stdin) is not a tty. Must be an option present in the menu.
16
+ # @return The selected option.
17
+ # @example Prompt the user to select from a list of options
18
+ # $selection = prompt::menu('Select a fruit', ['apple', 'banana', 'carrot'])
19
+ # @example Prompt the user to select from a list of options with a default value
20
+ # $selection = prompt::menu('Select environment', ['development', 'production'], 'default' => 'development')
21
+ dispatch :prompt_menu_array do
22
+ param 'String', :prompt
23
+ param 'Array[Variant[Target, Data]]', :menu
24
+ optional_param 'Hash[String[1], Variant[Target, Data]]', :options
25
+ return_type 'Variant[Target, Data]'
26
+ end
27
+
28
+ # Select from a list of options with custom inputs.
29
+ # @param prompt The prompt to display.
30
+ # @param menu A hash of options to choose from, where keys are the input used to select a value.
31
+ # @param options A hash of additional options.
32
+ # @option options [String] default The default option to return if the user does not provide
33
+ # input or if standard in (stdin) is not a tty. Must be an option present in the menu.
34
+ # @return The selected option.
35
+ # @example Prompt the user to select from a list of options with custom inputs
36
+ # $menu = { 'y' => 'yes', 'n' => 'no' }
37
+ # $selection = prompt::menu('Install Puppet?', $menu)
38
+ dispatch :prompt_menu do
39
+ param 'String', :prompt
40
+ param 'Hash[String[1], Variant[Target, Data]]', :menu
41
+ optional_param 'Hash[String[1], Variant[Target, Data]]', :options
42
+ return_type 'Variant[TargetSpec, Data]'
43
+ end
44
+
45
+ def prompt_menu_array(prompt, menu, options = {})
46
+ menu_hash = menu.map.with_index { |value, index| [(index + 1).to_s, value] }.to_h
47
+ prompt_menu(prompt, menu_hash, options)
48
+ end
49
+
50
+ def prompt_menu(prompt, menu, options = {})
51
+ unless Puppet[:tasks]
52
+ raise Puppet::ParseErrorWithIssue
53
+ .from_issue_and_stack(Bolt::PAL::Issues::PLAN_OPERATION_NOT_SUPPORTED_WHEN_COMPILING,
54
+ action: 'prompt::menu')
55
+ end
56
+
57
+ options = options.transform_keys(&:to_sym)
58
+ executor = Puppet.lookup(:bolt_executor)
59
+
60
+ # Send analytics report
61
+ executor.report_function_call(self.class.name)
62
+
63
+ # Error if there are no options
64
+ if menu.empty?
65
+ raise Bolt::ValidationError, "Menu cannot be empty"
66
+ end
67
+
68
+ # Error if the default value is not on the menu
69
+ if options.key?(:default) && !menu.value?(options[:default])
70
+ raise Bolt::ValidationError, "Default value '#{options[:default]}' is not one of the provided menu options"
71
+ end
72
+
73
+ # The first prompt should include the menu
74
+ to_prompt = format_menu(menu) + prompt
75
+
76
+ # Request input from the user until they provide a valid option
77
+ loop do
78
+ selection = executor.prompt(to_prompt, options)
79
+
80
+ return menu[selection] if menu.key?(selection)
81
+ return selection if options.key?(:default) && options[:default] == selection
82
+
83
+ # Only reprint the prompt, not the menu
84
+ to_prompt = "Invalid option, try again. #{prompt}"
85
+ end
86
+ end
87
+
88
+ # Builds the menu string. Aligns all the values by padding input keys.
89
+ #
90
+ private def format_menu(menu)
91
+ # Find the longest input and add 2 for wrapping parenthesis
92
+ key_length = menu.keys.max_by(&:length).length + 2
93
+
94
+ menu_string = +''
95
+
96
+ menu.each do |key, value|
97
+ key = "(#{key})".ljust(key_length)
98
+ menu_string << "#{key} #{value}\n"
99
+ end
100
+
101
+ menu_string
102
+ end
103
+ end
@@ -29,7 +29,7 @@ module Bolt
29
29
  yaml_plan_count: :cd13
30
30
  }.freeze
31
31
 
32
- def self.build_client
32
+ def self.build_client(enabled = true)
33
33
  logger = Bolt::Logger.logger(self)
34
34
  begin
35
35
  config_file = config_path
@@ -38,7 +38,7 @@ module Bolt
38
38
  config = { 'disabled' => true }
39
39
  end
40
40
 
41
- if config['disabled'] || ENV['BOLT_DISABLE_ANALYTICS']
41
+ if !enabled || config['disabled'] || ENV['BOLT_DISABLE_ANALYTICS']
42
42
  logger.debug "Analytics opt-out is set, analytics will be disabled"
43
43
  NoopClient.new
44
44
  else
@@ -80,12 +80,8 @@ module Bolt
80
80
  unless ENV['BOLT_DISABLE_ANALYTICS']
81
81
  msg = <<~ANALYTICS
82
82
  Bolt collects data about how you use it. You can opt out of providing this data.
83
-
84
- To disable analytics data collection, add this line to ~/.puppetlabs/etc/bolt/analytics.yaml :
85
- disabled: true
86
-
87
- Read more about what data Bolt collects and why here:
88
- https://puppet.com/docs/bolt/latest/bolt_installing.html#analytics-data-collection
83
+ To learn how to disable data collection, or see what data Bolt collects and why,
84
+ see http://pup.pt/bolt-analytics
89
85
  ANALYTICS
90
86
  Bolt::Logger.warn_once('analytics_opt_out', msg)
91
87
  end
@@ -54,7 +54,7 @@ module Bolt
54
54
  unless missing_keys.empty?
55
55
  if result['_output']
56
56
  # rubocop:disable Layout/LineLength
57
- msg = "Report result contains an '_output' key. Catalog application may have printed extraneous output to stdout: #{result['_output']}"
57
+ msg = "Report result contains an '_output' key. Catalog application might have printed extraneous output to stdout: #{result['_output']}"
58
58
  # rubocop:enable Layout/LineLength
59
59
  else
60
60
  msg = "Report did not contain all expected keys missing: #{missing_keys.join(', ')}"
@@ -13,7 +13,7 @@ module Bolt
13
13
  run_context: %w[concurrency inventoryfile save-rerun cleanup],
14
14
  global_config_setters: PROJECT_PATHS + %w[modulepath],
15
15
  transports: %w[transport connect-timeout tty native-ssh ssh-command copy-command],
16
- display: %w[format color verbose trace],
16
+ display: %w[format color verbose trace stream],
17
17
  global: %w[help version log-level clear-cache] }.freeze
18
18
 
19
19
  ACTION_OPTS = OPTIONS.values.flatten.freeze
@@ -454,7 +454,7 @@ module Bolt
454
454
  DESCRIPTION
455
455
  Convert a YAML plan to a Puppet language plan and print the converted plan to stdout.
456
456
 
457
- Converting a YAML plan may result in a plan that is syntactically
457
+ Converting a YAML plan might result in a plan that is syntactically
458
458
  correct but has different behavior. Always verify a converted plan's
459
459
  functionality. Note that the converted plan is not written to a file.
460
460
 
@@ -707,7 +707,7 @@ module Bolt
707
707
  "Or read a target list from an input file '@<file>' or stdin '-'.",
708
708
  'Example: --targets localhost,target_group,ssh://nix.com:23,winrm://windows.puppet.com',
709
709
  'URI format is [protocol://]host[:port]',
710
- "SSH is the default protocol; may be #{TRANSPORTS.keys.join(', ')}",
710
+ "SSH is the default protocol; can be #{TRANSPORTS.keys.join(', ')}",
711
711
  'For Windows targets, specify the winrm:// protocol if it has not be configured',
712
712
  'For SSH, port defaults to `22`',
713
713
  'For WinRM, port defaults to `5985` or `5986` based on the --[no-]ssl setting') do |targets|
@@ -888,6 +888,9 @@ module Bolt
888
888
  define('-v', '--[no-]verbose', 'Display verbose logging') do |value|
889
889
  @options[:verbose] = value
890
890
  end
891
+ define('--stream', 'Stream output from scripts and commands to the console') do |_|
892
+ @options[:stream] = true
893
+ end
891
894
  define('--trace', 'Display error stack traces') do |_|
892
895
  @options[:trace] = true
893
896
  end
data/lib/bolt/cli.rb CHANGED
@@ -96,6 +96,48 @@ module Bolt
96
96
  finalize_setup
97
97
  end
98
98
 
99
+ # Prints a welcome message when users first install Bolt and run `bolt`, `bolt help` or `bolt --help`
100
+ def welcome_message
101
+ bolt = <<~BOLT
102
+ `.::-`
103
+ `.-:///////-.`
104
+ `-:////:. `-:///:- /ooo. .ooo/
105
+ `.-:///::///:-` `-//: ymmm- :mmmy .---.
106
+ :///:-. `.:////. -//: ymmm- :mmmy +mmm+
107
+ ://. ///. -//: ymmm--/++/- `-/++/:` :mmmy-:smmms::-
108
+ ://. ://. .://: ymmmdmmmmmmdo` .smmmmmmmmh: :mmmysmmmmmmmms
109
+ ://. ://:///:-. ymmmh/--/hmmmy -mmmd/-.:hmmm+:mmmy.-smmms--.
110
+ ://:.` .-////:-` ymmm- ymmm:hmmm- `dmmm/mmmy +mmm+
111
+ `-:///:-..:///:-.` ymmm- ommm/dmmm` hmmm+mmmy +mmm+
112
+ `.-:////:-` ymmm+ /mmmm.ommms` /mmmh:mmmy +mmmo
113
+ `-.` ymmmmmhhmmmmd: ommmmhydmmmy`:mmmy -mmmmdhd
114
+ oyyy+shddhs/` .+shddhy+- -yyyo .ohddhs
115
+
116
+
117
+ BOLT
118
+ example_cmd = if Bolt::Util.windows?
119
+ "Invoke-BoltCommand -Command 'hostname' -Targets localhost"
120
+ else
121
+ "bolt command run 'hostname' --target localhost"
122
+ end
123
+ prev_cmd = String.new("bolt")
124
+ prev_cmd << " #{@argv[0]}" unless @argv.empty?
125
+
126
+ message = <<~MSG
127
+ 🎉 Welcome to Bolt #{VERSION}
128
+ 😌 We're here to help bring order to the chaos
129
+ 📖 Find our documentation at https://bolt.guide
130
+ 🙋 Ask a question in #bolt on https://slack.puppet.com/
131
+ 🔩 Contribute at https://github.com/puppetlabs/bolt/
132
+ 💡 Not sure where to start? Try "#{example_cmd}"
133
+
134
+ We only print this message once. Run "#{prev_cmd}" again for help text.
135
+ MSG
136
+
137
+ $stdout.print "\033[36m#{bolt}\033[0m"
138
+ $stdout.print message
139
+ end
140
+
99
141
  # Parses the command and validates options. All errors that are raised here
100
142
  # are not handled by the outputter, as it relies on config being loaded.
101
143
  def parse_command
@@ -107,6 +149,16 @@ module Bolt
107
149
  # help text
108
150
  options[:subcommand] = nil unless COMMANDS.include?(options[:subcommand])
109
151
 
152
+ if Bolt::Util.first_run?
153
+ FileUtils.mkdir_p(Bolt::Util.first_runs_free.dirname)
154
+ FileUtils.touch(Bolt::Util.first_runs_free)
155
+
156
+ if options[:subcommand].nil? && $stdout.isatty
157
+ welcome_message
158
+ raise Bolt::CLIExit
159
+ end
160
+ end
161
+
110
162
  # Update the parser for the subcommand (or lack thereof)
111
163
  parser.update
112
164
  puts parser.help
@@ -175,6 +227,7 @@ module Bolt
175
227
  # Completes the setup process by configuring Bolt and log messages
176
228
  def finalize_setup
177
229
  Bolt::Logger.configure(config.log, config.color, config.disable_warnings)
230
+ Bolt::Logger.stream = config.stream
178
231
  Bolt::Logger.analytics = analytics
179
232
  Bolt::Logger.flush_queue
180
233
 
@@ -212,7 +265,7 @@ module Bolt
212
265
  target_opts = options.keys.select { |opt| %i[query rerun targets].include?(opt) }
213
266
  target_string = "'--targets', '--rerun', or '--query'"
214
267
  if target_opts.length > 1
215
- raise Bolt::CLIError, "Only one targeting option #{target_string} may be specified"
268
+ raise Bolt::CLIError, "Only one targeting option #{target_string} can be specified"
216
269
  elsif target_opts.empty? && options[:subcommand] != 'plan'
217
270
  raise Bolt::CLIError, "Command requires a targeting option: #{target_string}"
218
271
  end
@@ -249,11 +302,14 @@ module Bolt
249
302
  end
250
303
  end
251
304
 
252
- if %w[task plan].include?(options[:subcommand]) && options[:action] == 'run'
305
+ if %w[task plan script].include?(options[:subcommand]) && options[:action] == 'run'
253
306
  if options[:object].nil?
254
307
  raise Bolt::CLIError, "Must specify a #{options[:subcommand]} to run"
255
308
  end
256
- # This may mean that we parsed a parameter as the object
309
+ end
310
+
311
+ # This may mean that we parsed a parameter as the object
312
+ if %w[task plan].include?(options[:subcommand]) && options[:action] == 'run'
257
313
  unless options[:object] =~ /\A([a-z][a-z0-9_]*)?(::[a-z][a-z0-9_]*)*\Z/
258
314
  raise Bolt::CLIError,
259
315
  "Invalid #{options[:subcommand]} '#{options[:object]}'"
@@ -301,13 +357,13 @@ module Bolt
301
357
  if options[:noop] &&
302
358
  !(options[:subcommand] == 'task' && options[:action] == 'run') && options[:subcommand] != 'apply'
303
359
  raise Bolt::CLIError,
304
- "Option '--noop' may only be specified when running a task or applying manifest code"
360
+ "Option '--noop' can only be specified when running a task or applying manifest code"
305
361
  end
306
362
 
307
363
  if options[:env_vars]
308
364
  unless %w[command script].include?(options[:subcommand]) && options[:action] == 'run'
309
365
  raise Bolt::CLIError,
310
- "Option '--env-var' may only be specified when running a command or script"
366
+ "Option '--env-var' can only be specified when running a command or script"
311
367
  end
312
368
  end
313
369
  end
@@ -354,7 +410,7 @@ module Bolt
354
410
  if inventory_source && conflicting_options.any?
355
411
  Bolt::Logger.warn(
356
412
  "cli_overrides",
357
- "CLI arguments #{conflicting_options.to_a} may be overridden by Inventory: #{inventory_source}"
413
+ "CLI arguments #{conflicting_options.to_a} might be overridden by Inventory: #{inventory_source}"
358
414
  )
359
415
  end
360
416
  end
@@ -432,7 +488,7 @@ module Bolt
432
488
  return 0
433
489
  end
434
490
 
435
- message = 'There may be processes left executing on some nodes.'
491
+ message = 'There might be processes left executing on some nodes.'
436
492
 
437
493
  if %w[task plan].include?(options[:subcommand]) && options[:task_options] && !options[:params_parsed] && pal
438
494
  options[:task_options] = pal.parse_params(options[:subcommand], options[:object], options[:task_options])
@@ -485,7 +541,11 @@ module Bolt
485
541
  end
486
542
  code = apply_manifest(options[:code], options[:targets], options[:object], options[:noop])
487
543
  else
488
- executor = Bolt::Executor.new(config.concurrency, analytics, options[:noop], config.modified_concurrency)
544
+ executor = Bolt::Executor.new(config.concurrency,
545
+ analytics,
546
+ options[:noop],
547
+ config.modified_concurrency,
548
+ config.future)
489
549
  targets = options[:targets]
490
550
 
491
551
  results = nil
@@ -501,9 +561,8 @@ module Bolt
501
561
  when 'command'
502
562
  executor.run_command(targets, options[:object], executor_opts)
503
563
  when 'script'
504
- script = options[:object]
505
- validate_file('script', script)
506
- executor.run_script(targets, script, options[:leftovers], executor_opts)
564
+ script_path = find_file(options[:object])
565
+ executor.run_script(targets, script_path, options[:leftovers], executor_opts)
507
566
  when 'task'
508
567
  pal.run_task(options[:object],
509
568
  targets,
@@ -576,7 +635,18 @@ module Bolt
576
635
 
577
636
  def list_targets
578
637
  inventoryfile = config.inventoryfile || config.default_inventoryfile
638
+ outputter.print_targets(group_targets_by_source, inventoryfile)
639
+ end
640
+
641
+ def show_targets
642
+ inventoryfile = config.inventoryfile || config.default_inventoryfile
643
+ outputter.print_target_info(group_targets_by_source, inventoryfile)
644
+ end
579
645
 
646
+ # Returns a hash of targets sorted by those that are found in the
647
+ # inventory and those that are provided on the command line.
648
+ #
649
+ private def group_targets_by_source
580
650
  # Retrieve the known group and target names. This needs to be done before
581
651
  # updating targets, as that will add adhoc targets to the inventory.
582
652
  known_names = inventory.target_names
@@ -587,17 +657,7 @@ module Bolt
587
657
  known_names.include?(target.name)
588
658
  end
589
659
 
590
- target_list = {
591
- inventory: inventory_targets,
592
- adhoc: adhoc_targets
593
- }
594
-
595
- outputter.print_targets(target_list, inventoryfile)
596
- end
597
-
598
- def show_targets
599
- update_targets(options)
600
- outputter.print_target_info(options[:targets])
660
+ { inventory: inventory_targets, adhoc: adhoc_targets }
601
661
  end
602
662
 
603
663
  def list_groups
@@ -610,7 +670,7 @@ module Bolt
610
670
  if plan_arguments['nodes'] || plan_arguments['targets']
611
671
  key = plan_arguments.include?('nodes') ? 'nodes' : 'targets'
612
672
  raise Bolt::CLIError,
613
- "A plan's '#{key}' parameter may be specified using the --#{key} option, but in that " \
673
+ "A plan's '#{key}' parameter can be specified using the --#{key} option, but in that " \
614
674
  "case it must not be specified as a separate #{key}=<value> parameter nor included " \
615
675
  "in the JSON data passed in the --params option"
616
676
  end
@@ -633,7 +693,11 @@ module Bolt
633
693
  plan_context = { plan_name: plan_name,
634
694
  params: plan_arguments }
635
695
 
636
- executor = Bolt::Executor.new(config.concurrency, analytics, options[:noop], config.modified_concurrency)
696
+ executor = Bolt::Executor.new(config.concurrency,
697
+ analytics,
698
+ options[:noop],
699
+ config.modified_concurrency,
700
+ config.future)
637
701
  if %w[human rainbow].include?(options.fetch(:format, 'human'))
638
702
  executor.subscribe(outputter)
639
703
  else
@@ -669,7 +733,11 @@ module Bolt
669
733
  Bolt::Logger.warn("empty_manifest", message)
670
734
  end
671
735
 
672
- executor = Bolt::Executor.new(config.concurrency, analytics, noop, config.modified_concurrency)
736
+ executor = Bolt::Executor.new(config.concurrency,
737
+ analytics,
738
+ noop,
739
+ config.modified_concurrency,
740
+ config.future)
673
741
  executor.subscribe(outputter) if options.fetch(:format, 'human') == 'human'
674
742
  executor.subscribe(log_outputter)
675
743
  # apply logging looks like plan logging, so tell the outputter we're in a
@@ -754,15 +822,10 @@ module Bolt
754
822
  #
755
823
  def assert_project_file(project)
756
824
  unless project.project_file?
757
- msg = if project.config_file.exist?
758
- command = Bolt::Util.powershell? ? 'Update-BoltProject' : 'bolt project migrate'
759
- "Detected Bolt configuration file #{project.config_file}, unable to install "\
760
- "modules. To update to a project configuration file, run '#{command}'."
761
- else
762
- command = Bolt::Util.powershell? ? 'New-BoltProject' : 'bolt project init'
763
- "Could not find project configuration file #{project.project_file}, unable "\
764
- "to install modules. To create a Bolt project, run '#{command}'."
765
- end
825
+ command = Bolt::Util.powershell? ? 'New-BoltProject' : 'bolt project init'
826
+
827
+ msg = "Could not find project configuration file #{project.project_file}, unable "\
828
+ "to install modules. To create a Bolt project, run '#{command}'."
766
829
 
767
830
  raise Bolt::Error.new(msg, 'bolt/missing-project-config-error')
768
831
  end
@@ -840,6 +903,28 @@ module Bolt
840
903
  Bolt::Util.validate_file(type, path, allow_dir)
841
904
  end
842
905
 
906
+ # Returns the path to a file. If the path is an absolute or relative to
907
+ # a file, and the file exists, returns the path as-is. Otherwise, checks if
908
+ # the path is a Puppet file path and looks for the file in a module's files
909
+ # directory.
910
+ #
911
+ def find_file(path)
912
+ unless File.exist?(path) || Pathname.new(path).absolute?
913
+ modulepath = Bolt::Config::Modulepath.new(config.modulepath)
914
+ modules = Bolt::Module.discover(modulepath.full_modulepath, config.project)
915
+ mod, file = path.split(File::SEPARATOR, 2)
916
+
917
+ if modules[mod]
918
+ @logger.debug("Did not find file at #{File.expand_path(path)}, checking in module '#{mod}'")
919
+ path = File.join(modules[mod].path, 'files', file)
920
+ end
921
+ end
922
+
923
+ Bolt::Util.validate_file('script', path)
924
+
925
+ path
926
+ end
927
+
843
928
  def rerun
844
929
  @rerun ||= Bolt::Rerun.new(config.rerunfile, config.save_rerun)
845
930
  end
@@ -858,7 +943,7 @@ module Bolt
858
943
 
859
944
  def analytics
860
945
  @analytics ||= begin
861
- client = Bolt::Analytics.build_client
946
+ client = Bolt::Analytics.build_client(config.analytics)
862
947
  client.bundled_content = bundled_content
863
948
  client
864
949
  end
@@ -868,7 +953,7 @@ module Bolt
868
953
  # If the bundled content directory is empty, Bolt is likely installed as a gem.
869
954
  if ENV['BOLT_GEM'].nil? && incomplete_install?
870
955
  msg = <<~MSG.chomp
871
- Bolt may be installed as a gem. To use Bolt reliably and with all of its
956
+ Bolt might be installed as a gem. To use Bolt reliably and with all of its
872
957
  dependencies, uninstall the 'bolt' gem and install Bolt as a package:
873
958
  https://puppet.com/docs/bolt/latest/bolt_installing.html
874
959
 
@@ -899,7 +984,8 @@ module Bolt
899
984
  # Gem installs include the aggregate, canary, and puppetdb_fact modules, while
900
985
  # package installs include modules listed in the Bolt repo Puppetfile
901
986
  def incomplete_install?
902
- (Dir.children(Bolt::Config::Modulepath::MODULES_PATH) - %w[aggregate canary puppetdb_fact secure_env_vars]).empty?
987
+ builtin_module_list = %w[aggregate canary puppetdb_fact secure_env_vars puppet_connect]
988
+ (Dir.children(Bolt::Config::Modulepath::MODULES_PATH) - builtin_module_list).empty?
903
989
  end
904
990
 
905
991
  # Mimicks the output from Outputter::Human#fatal_error. This should be used to print