bolt 3.13.0 → 3.16.1

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 (55) 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/bolt-modules/boltlib/lib/puppet/functions/background.rb +2 -1
  5. data/bolt-modules/boltlib/lib/puppet/functions/parallelize.rb +5 -1
  6. data/bolt-modules/boltlib/lib/puppet/functions/run_plan.rb +13 -0
  7. data/bolt-modules/boltlib/lib/puppet/functions/wait.rb +47 -7
  8. data/bolt-modules/out/lib/puppet/functions/out/message.rb +4 -2
  9. data/bolt-modules/out/lib/puppet/functions/out/verbose.rb +4 -2
  10. data/guides/{debugging.txt → debugging.yaml} +5 -6
  11. data/guides/{inventory.txt → inventory.yaml} +6 -7
  12. data/guides/{links.txt → links.yaml} +3 -4
  13. data/guides/{logging.txt → logging.yaml} +5 -6
  14. data/guides/{module.txt → module.yaml} +5 -6
  15. data/guides/{modulepath.txt → modulepath.yaml} +5 -6
  16. data/guides/{project.txt → project.yaml} +6 -7
  17. data/guides/{targets.txt → targets.yaml} +5 -6
  18. data/guides/{transports.txt → transports.yaml} +6 -7
  19. data/lib/bolt/analytics.rb +3 -20
  20. data/lib/bolt/application.rb +620 -0
  21. data/lib/bolt/bolt_option_parser.rb +17 -5
  22. data/lib/bolt/cli.rb +592 -772
  23. data/lib/bolt/config/transport/options.rb +12 -0
  24. data/lib/bolt/config/transport/ssh.rb +7 -0
  25. data/lib/bolt/executor.rb +12 -4
  26. data/lib/bolt/fiber_executor.rb +63 -14
  27. data/lib/bolt/module_installer/puppetfile.rb +24 -10
  28. data/lib/bolt/outputter/human.rb +199 -43
  29. data/lib/bolt/outputter/json.rb +66 -43
  30. data/lib/bolt/outputter/logger.rb +1 -1
  31. data/lib/bolt/pal.rb +67 -14
  32. data/lib/bolt/pal/yaml_plan/step.rb +2 -0
  33. data/lib/bolt/pal/yaml_plan/step/message.rb +0 -8
  34. data/lib/bolt/pal/yaml_plan/step/verbose.rb +31 -0
  35. data/lib/bolt/pal/yaml_plan/transpiler.rb +1 -1
  36. data/lib/bolt/plan_creator.rb +2 -20
  37. data/lib/bolt/plan_future.rb +23 -3
  38. data/lib/bolt/plan_result.rb +1 -1
  39. data/lib/bolt/plugin/task.rb +1 -1
  40. data/lib/bolt/project.rb +0 -7
  41. data/lib/bolt/result_set.rb +2 -1
  42. data/lib/bolt/transport/local/connection.rb +17 -1
  43. data/lib/bolt/transport/orch/connection.rb +13 -1
  44. data/lib/bolt/transport/ssh/exec_connection.rb +3 -1
  45. data/lib/bolt/version.rb +1 -1
  46. data/lib/bolt_server/file_cache.rb +12 -0
  47. data/lib/bolt_server/schemas/action-apply.json +32 -0
  48. data/lib/bolt_server/schemas/action-apply_prep.json +19 -0
  49. data/lib/bolt_server/schemas/partials/target-ssh.json +4 -0
  50. data/lib/bolt_server/schemas/partials/target-winrm.json +4 -0
  51. data/lib/bolt_server/transport_app.rb +180 -60
  52. data/lib/bolt_spec/plans/mock_executor.rb +16 -6
  53. metadata +23 -15
  54. data/guides/guide.txt +0 -17
  55. data/lib/bolt/secret.rb +0 -37
@@ -507,11 +507,14 @@ module Bolt
507
507
  show
508
508
 
509
509
  #{colorize(:cyan, 'Usage')}
510
- bolt module show [options]
510
+ bolt module show [module name] [options]
511
511
 
512
512
  #{colorize(:cyan, 'Description')}
513
513
  List modules available to the Bolt project.
514
514
 
515
+ Providing the name of a module will display detailed documentation for
516
+ the module.
517
+
515
518
  #{colorize(:cyan, 'Documentation')}
516
519
  To learn more about Bolt modules, run 'bolt guide module'.
517
520
  HELP
@@ -894,7 +897,7 @@ module Bolt
894
897
  end
895
898
  define('--params PARAMETERS',
896
899
  "Parameters to a task or plan as json, a json file '@<file>', or on stdin '-'.") do |params|
897
- @options[:task_options] = parse_params(params)
900
+ @options[:params] = parse_params(params)
898
901
  end
899
902
  define('-e', '--execute CODE',
900
903
  "Puppet manifest code to apply to the targets.") do |code|
@@ -1083,8 +1086,7 @@ module Bolt
1083
1086
  @options[:help] = true
1084
1087
  end
1085
1088
  define('--version', 'Display the version.') do |_|
1086
- puts Bolt::VERSION
1087
- raise Bolt::CLIExit
1089
+ @options[:version] = true
1088
1090
  end
1089
1091
  define('--log-level LEVEL',
1090
1092
  "Set the log level for the console. Available options are",
@@ -1092,7 +1094,7 @@ module Bolt
1092
1094
  @options[:log] = { 'console' => { 'level' => level } }
1093
1095
  end
1094
1096
  define('--clear-cache',
1095
- "Clear plugin cache before executing.") do |_|
1097
+ "Clear plugin, plan, and task caches before executing.") do |_|
1096
1098
  @options[:clear_cache] = true
1097
1099
  end
1098
1100
  define('--plugin PLUGIN', 'Select the plugin to use.') do |plug|
@@ -1127,5 +1129,15 @@ module Bolt
1127
1129
  rescue JSON::ParserError => e
1128
1130
  raise Bolt::CLIError, "Unable to parse --params value as JSON: #{e}"
1129
1131
  end
1132
+
1133
+ def permute(args)
1134
+ super(args)
1135
+ rescue OptionParser::MissingArgument => e
1136
+ raise Bolt::CLIError, "Option '#{e.args.first}' needs a parameter"
1137
+ rescue OptionParser::InvalidArgument => e
1138
+ raise Bolt::CLIError, "Invalid parameter specified for option '#{e.args.first}': #{e.args[1]}"
1139
+ rescue OptionParser::InvalidOption, OptionParser::AmbiguousOption => e
1140
+ raise Bolt::CLIError, "Unknown argument '#{e.args.first}'"
1141
+ end
1130
1142
  end
1131
1143
  end
data/lib/bolt/cli.rb CHANGED
@@ -10,6 +10,7 @@ require 'io/console'
10
10
  require 'logging'
11
11
  require 'optparse'
12
12
  require 'bolt/analytics'
13
+ require 'bolt/application'
13
14
  require 'bolt/bolt_option_parser'
14
15
  require 'bolt/config'
15
16
  require 'bolt/error'
@@ -24,7 +25,6 @@ require 'bolt/plugin'
24
25
  require 'bolt/project_manager'
25
26
  require 'bolt/puppetdb'
26
27
  require 'bolt/rerun'
27
- require 'bolt/secret'
28
28
  require 'bolt/target'
29
29
  require 'bolt/version'
30
30
 
@@ -32,6 +32,8 @@ module Bolt
32
32
  class CLIExit < StandardError; end
33
33
 
34
34
  class CLI
35
+ attr_reader :outputter, :rerun
36
+
35
37
  COMMANDS = {
36
38
  'apply' => %w[],
37
39
  'command' => %w[run],
@@ -51,22 +53,24 @@ module Bolt
51
53
 
52
54
  TARGETING_OPTIONS = %i[query rerun targets].freeze
53
55
 
54
- attr_reader :config, :options
56
+ SUCCESS = 0
57
+ ERROR = 1
58
+ FAILURE = 2
55
59
 
56
60
  def initialize(argv)
57
61
  Bolt::Logger.initialize_logging
58
62
  @logger = Bolt::Logger.logger(self)
59
- @argv = argv
60
- @options = {}
61
- end
62
-
63
- # Only call after @config has been initialized.
64
- def inventory
65
- @inventory ||= Bolt::Inventory.from_config(config, plugins)
63
+ @argv = argv
66
64
  end
67
- private :inventory
68
65
 
69
- def help?(remaining)
66
+ # TODO: Move this to the parser.
67
+ #
68
+ # Query whether the help text needs to be displayed.
69
+ #
70
+ # @param remaining [Array] Remaining arguments after parsing the command.
71
+ # @param options [Hash] The CLI options.
72
+ #
73
+ private def help?(options, remaining)
70
74
  # Set the subcommand
71
75
  options[:subcommand] = remaining.shift
72
76
 
@@ -84,24 +88,90 @@ module Bolt
84
88
 
85
89
  options[:help]
86
90
  end
87
- private :help?
88
91
 
89
- # Wrapper method that is called by the Bolt executable. Parses the command and
90
- # then loads the project and config. Once config is loaded, it completes the
91
- # setup process by configuring Bolt and logging messages.
92
+ # TODO: Move most of this to the parser.
93
+ #
94
+ # Parse the command and validate options. All errors that are raised here
95
+ # are not handled by the outputter, as it relies on config being loaded.
92
96
  #
93
- # This separation is needed since the Bolt::Outputter class that normally handles
94
- # printing errors relies on config being loaded. All setup that happens before
95
- # config is loaded will have errors printed directly to stdout, while all errors
96
- # raised after config is loaded are handled by the outputter.
97
97
  def parse
98
- parse_command
99
- load_config
100
- finalize_setup
98
+ with_error_handling do
99
+ options = {}
100
+ parser = BoltOptionParser.new(options)
101
+
102
+ # This part aims to handle both `bolt <mode> --help` and `bolt help <mode>`.
103
+ remaining = parser.permute(@argv) unless @argv.empty?
104
+
105
+ if @argv.empty? || help?(options, remaining)
106
+ # If the subcommand is not enabled, display the default
107
+ # help text
108
+ options[:subcommand] = nil unless COMMANDS.include?(options[:subcommand])
109
+
110
+ if Bolt::Util.first_run?
111
+ FileUtils.touch(Bolt::Util.first_runs_free)
112
+
113
+ if options[:subcommand].nil? && $stdout.isatty
114
+ welcome_message
115
+ raise Bolt::CLIExit
116
+ end
117
+ end
118
+
119
+ # Update the parser for the subcommand (or lack thereof)
120
+ parser.update
121
+ puts parser.help
122
+ raise Bolt::CLIExit
123
+ end
124
+
125
+ if options[:version]
126
+ puts Bolt::VERSION
127
+ raise Bolt::CLIExit
128
+ end
129
+
130
+ options[:object] = remaining.shift
131
+
132
+ # Handle reading a command from a file
133
+ if options[:subcommand] == 'command' && options[:object]
134
+ options[:object] = Bolt::Util.get_arg_input(options[:object])
135
+ end
136
+
137
+ # Only parse params for task or plan
138
+ if %w[task plan].include?(options[:subcommand])
139
+ params, remaining = remaining.partition { |s| s =~ /.+=/ }
140
+ if options[:params]
141
+ unless params.empty?
142
+ raise Bolt::CLIError,
143
+ "Parameters must be specified through either the --params " \
144
+ "option or param=value pairs, not both"
145
+ end
146
+ options[:params_parsed] = true
147
+ elsif params.any?
148
+ options[:params_parsed] = false
149
+ options[:params] = Hash[params.map { |a| a.split('=', 2) }]
150
+ else
151
+ options[:params_parsed] = true
152
+ options[:params] = {}
153
+ end
154
+ end
155
+ options[:leftovers] = remaining
156
+
157
+ # Default to verbose for everything except plans
158
+ unless options.key?(:verbose)
159
+ options[:verbose] = options[:subcommand] != 'plan'
160
+ end
161
+
162
+ validate(options)
163
+ validate_ps_version
164
+
165
+ options
166
+ end
101
167
  end
102
168
 
103
- # Prints a welcome message when users first install Bolt and run `bolt`, `bolt help` or `bolt --help`
104
- def welcome_message
169
+ # TODO: Move this to the parser.
170
+ #
171
+ # Print a welcome message when users first install Bolt and run `bolt`,
172
+ # `bolt help` or `bolt --help`.
173
+ #
174
+ private def welcome_message
105
175
  bolt = <<~BOLT
106
176
  `.::-`
107
177
  `.-:///////-.`
@@ -142,149 +212,15 @@ module Bolt
142
212
  $stdout.print message
143
213
  end
144
214
 
145
- # Parses the command and validates options. All errors that are raised here
146
- # are not handled by the outputter, as it relies on config being loaded.
147
- def parse_command
148
- parser = BoltOptionParser.new(options)
149
- # This part aims to handle both `bolt <mode> --help` and `bolt help <mode>`.
150
- remaining = handle_parser_errors { parser.permute(@argv) } unless @argv.empty?
151
- if @argv.empty? || help?(remaining)
152
- # If the subcommand is not enabled, display the default
153
- # help text
154
- options[:subcommand] = nil unless COMMANDS.include?(options[:subcommand])
155
-
156
- if Bolt::Util.first_run?
157
- FileUtils.touch(Bolt::Util.first_runs_free)
158
-
159
- if options[:subcommand].nil? && $stdout.isatty
160
- welcome_message
161
- raise Bolt::CLIExit
162
- end
163
- end
164
-
165
- # Update the parser for the subcommand (or lack thereof)
166
- parser.update
167
- puts parser.help
168
- raise Bolt::CLIExit
169
- end
170
-
171
- options[:object] = remaining.shift
172
-
173
- # Handle reading a command from a file
174
- if options[:subcommand] == 'command' && options[:object]
175
- options[:object] = Bolt::Util.get_arg_input(options[:object])
176
- end
177
-
178
- # Only parse task_options for task or plan
179
- if %w[task plan].include?(options[:subcommand])
180
- task_options, remaining = remaining.partition { |s| s =~ /.+=/ }
181
- if options[:task_options]
182
- unless task_options.empty?
183
- raise Bolt::CLIError,
184
- "Parameters must be specified through either the --params " \
185
- "option or param=value pairs, not both"
186
- end
187
- options[:params_parsed] = true
188
- elsif task_options.any?
189
- options[:params_parsed] = false
190
- options[:task_options] = Hash[task_options.map { |a| a.split('=', 2) }]
191
- else
192
- options[:params_parsed] = true
193
- options[:task_options] = {}
194
- end
195
- end
196
- options[:leftovers] = remaining
197
-
198
- # Default to verbose for everything except plans
199
- unless options.key?(:verbose)
200
- options[:verbose] = options[:subcommand] != 'plan'
201
- end
202
-
203
- validate(options)
204
- rescue Bolt::Error => e
205
- fatal_error(e)
206
- raise e
207
- end
208
-
209
- # Loads the project and configuration. All errors that are raised here are not
210
- # handled by the outputter, as it relies on config being loaded.
211
- def load_config
212
- project = if ENV['BOLT_PROJECT']
213
- Bolt::Project.create_project(ENV['BOLT_PROJECT'], 'environment')
214
- elsif options[:project]
215
- dir = Pathname.new(options[:project])
216
- if (dir + Bolt::Project::BOLTDIR_NAME).directory?
217
- Bolt::Project.create_project(dir + Bolt::Project::BOLTDIR_NAME)
218
- else
219
- Bolt::Project.create_project(dir)
220
- end
221
- else
222
- Bolt::Project.find_boltdir(Dir.pwd)
223
- end
224
- @config = Bolt::Config.from_project(project, options)
225
- rescue Bolt::Error => e
226
- fatal_error(e)
227
- raise e
228
- end
229
-
230
- # Completes the setup process by configuring Bolt and log messages
231
- def finalize_setup
232
- Bolt::Logger.configure(config.log, config.color, config.disable_warnings)
233
- Bolt::Logger.stream = config.stream
234
- Bolt::Logger.analytics = analytics
235
- Bolt::Logger.flush_queue
236
-
237
- # Logger must be configured before checking path case and project file, otherwise logs will not display
238
- config.check_path_case('modulepath', config.modulepath)
239
- config.project.check_deprecated_file
240
-
241
- if options[:clear_cache] && File.exist?(config.project.plugin_cache_file)
242
- FileUtils.rm(config.project.plugin_cache_file)
243
- end
244
-
245
- warn_inventory_overrides_cli(options)
246
- validate_ps_version
247
-
248
- options
249
- rescue Bolt::Error => e
250
- outputter.fatal_error(e)
251
- raise e
252
- end
253
-
254
- private def validate_ps_version
255
- if Bolt::Util.powershell?
256
- command = "powershell.exe -NoProfile -NonInteractive -NoLogo -ExecutionPolicy "\
257
- "Bypass -Command $PSVersionTable.PSVersion.Major"
258
- stdout, _stderr, _status = Open3.capture3(command)
259
-
260
- return unless !stdout.empty? && stdout.to_i < 3
261
-
262
- msg = "Detected PowerShell 2 on controller. PowerShell 2 is unsupported."
263
- Bolt::Logger.deprecation_warning("powershell_2_controller", msg)
264
- end
265
- end
266
-
267
- def update_targets(options)
268
- target_opts = options.keys.select { |opt| TARGETING_OPTIONS.include?(opt) }
269
- target_string = "'--targets', '--rerun', or '--query'"
270
- if target_opts.length > 1
271
- raise Bolt::CLIError, "Only one targeting option #{target_string} can be specified"
272
- elsif target_opts.empty? && options[:subcommand] != 'plan'
273
- raise Bolt::CLIError, "Command requires a targeting option: #{target_string}"
274
- end
275
-
276
- targets = if options[:query]
277
- query_puppetdb_nodes(options[:query])
278
- elsif options[:rerun]
279
- rerun.get_targets(options[:rerun])
280
- else
281
- options[:targets] || []
282
- end
283
- options[:target_args] = targets
284
- options[:targets] = inventory.get_targets(targets)
285
- end
286
-
287
- def validate(options)
215
+ # TODO: Move this to the parser.
216
+ #
217
+ # Validate the command. Ensure that the subcommand and action are
218
+ # recognized, all required arguments are specified, and only supported
219
+ # command-line options are used.
220
+ #
221
+ # @param options [Hash] The CLI options.
222
+ #
223
+ private def validate(options)
288
224
  unless COMMANDS.include?(options[:subcommand])
289
225
  command = Bolt::Util.powershell? ? 'Get-Command -Module PuppetBolt' : 'bolt help'
290
226
  raise Bolt::CLIError,
@@ -349,12 +285,34 @@ module Bolt
349
285
  raise Bolt::CLIError, "Must specify a module name."
350
286
  end
351
287
 
288
+ if options[:action] == 'convert' && !options[:object]
289
+ raise Bolt::CLIError, "Must specify a plan."
290
+ end
291
+
352
292
  if options[:subcommand] == 'module' && options[:action] == 'install' && options[:object]
353
293
  command = Bolt::Util.powershell? ? 'Add-BoltModule -Module' : 'bolt module add'
354
294
  raise Bolt::CLIError, "Invalid argument '#{options[:object]}'. To add a new module to "\
355
295
  "the project, run '#{command} #{options[:object]}'."
356
296
  end
357
297
 
298
+ if %w[download upload].include?(options[:action])
299
+ raise Bolt::CLIError, "Must specify a source" unless options[:object]
300
+
301
+ if options[:leftovers].empty?
302
+ raise Bolt::CLIError, "Must specify a destination"
303
+ elsif options[:leftovers].size > 1
304
+ raise Bolt::CLIError, "Unknown arguments #{options[:leftovers].drop(1).join(', ')}"
305
+ end
306
+ end
307
+
308
+ if options[:subcommand] == 'group' && options[:object]
309
+ raise Bolt::CLIError, "Unknown argument #{options[:object]}"
310
+ end
311
+
312
+ if options[:action] == 'generate-types' && options[:object]
313
+ raise Bolt::CLIError, "Unknown argument #{options[:object]}"
314
+ end
315
+
358
316
  if !%w[file script lookup].include?(options[:subcommand]) &&
359
317
  !options[:leftovers].empty?
360
318
  raise Bolt::CLIError,
@@ -379,710 +337,572 @@ module Bolt
379
337
  "Option '--env-var' can only be specified when running a command or script"
380
338
  end
381
339
  end
382
- end
383
-
384
- def handle_parser_errors
385
- yield
386
- rescue OptionParser::MissingArgument => e
387
- raise Bolt::CLIError, "Option '#{e.args.first}' needs a parameter"
388
- rescue OptionParser::InvalidArgument => e
389
- raise Bolt::CLIError, "Invalid parameter specified for option '#{e.args.first}': #{e.args[1]}"
390
- rescue OptionParser::InvalidOption, OptionParser::AmbiguousOption => e
391
- raise Bolt::CLIError, "Unknown argument '#{e.args.first}'"
392
- end
393
-
394
- def puppetdb_client
395
- plugins.puppetdb_client
396
- end
397
340
 
398
- def plugins
399
- @plugins ||= Bolt::Plugin.setup(config, pal, analytics)
341
+ validate_targeting_options(options)
400
342
  end
401
343
 
402
- def query_puppetdb_nodes(query)
403
- puppetdb_client.query_certnames(query)
404
- end
405
-
406
- def warn_inventory_overrides_cli(opts)
407
- inventory_source = if ENV[Bolt::Inventory::ENVIRONMENT_VAR]
408
- Bolt::Inventory::ENVIRONMENT_VAR
409
- elsif config.inventoryfile
410
- config.inventoryfile
411
- elsif File.exist?(config.default_inventoryfile)
412
- config.default_inventoryfile
413
- end
344
+ # Validates that only one targeting option is provided and that commands
345
+ # requiring a targeting option received one.
346
+ #
347
+ # @param options [Hash] The CLI options.
348
+ #
349
+ private def validate_targeting_options(options)
350
+ target_opts = options.slice(*TARGETING_OPTIONS)
351
+ target_string = "'--targets', '--rerun', or '--query'"
414
352
 
415
- inventory_cli_opts = %i[authentication escalation transports].each_with_object([]) do |key, acc|
416
- acc.concat(Bolt::BoltOptionParser::OPTIONS[key])
353
+ if target_opts.length > 1
354
+ raise Bolt::CLIError, "Only one targeting option can be specified: #{target_string}"
417
355
  end
418
356
 
419
- inventory_cli_opts.concat(%w[no-host-key-check no-ssl no-ssl-verify no-tty])
357
+ return if %w[guide module plan project secret].include?(options[:subcommand]) ||
358
+ %w[convert new show].include?(options[:action]) ||
359
+ options[:plan_hierarchy]
420
360
 
421
- conflicting_options = Set.new(opts.keys.map(&:to_s)).intersection(inventory_cli_opts)
422
-
423
- if inventory_source && conflicting_options.any?
424
- Bolt::Logger.warn(
425
- "cli_overrides",
426
- "CLI arguments #{conflicting_options.to_a} might be overridden by Inventory: #{inventory_source}"
427
- )
361
+ if target_opts.empty?
362
+ raise Bolt::CLIError, "Command requires a targeting option: #{target_string}"
428
363
  end
429
364
  end
430
365
 
366
+ # Execute a Bolt command. The +options+ hash includes the subcommand and
367
+ # action to be run, as well as any additional arguments and options for the
368
+ # command.
369
+ #
370
+ # @param options [Hash] The CLI options.
371
+ #
431
372
  def execute(options)
432
- message = nil
433
-
434
- handler = Signal.trap :INT do |signo|
435
- @logger.info(
436
- "Exiting after receiving SIG#{Signal.signame(signo)} signal.#{message ? ' ' + message : ''}"
437
- )
438
- exit!
439
- end
440
-
441
- # Initialize inventory and targets. Errors here are better to catch early.
442
- # options[:target_args] will contain a string/array version of the targeting options this is passed to plans
443
- # options[:targets] will contain a resolved set of Target objects
444
- unless %w[guide module project secret].include?(options[:subcommand]) ||
445
- %w[convert new show].include?(options[:action]) ||
446
- options[:plan_hierarchy]
447
- update_targets(options)
448
- end
373
+ with_signal_handling do
374
+ with_error_handling do
375
+ # TODO: Separate from options hash and pass as own args.
376
+ command = options[:subcommand]
377
+ action = options[:action]
378
+
379
+ #
380
+ # INITIALIZE CORE CLASSES
381
+ #
382
+
383
+ project = if ENV['BOLT_PROJECT']
384
+ Bolt::Project.create_project(ENV['BOLT_PROJECT'], 'environment')
385
+ elsif options[:project]
386
+ dir = Pathname.new(options[:project])
387
+ if (dir + Bolt::Project::BOLTDIR_NAME).directory?
388
+ Bolt::Project.create_project(dir + Bolt::Project::BOLTDIR_NAME)
389
+ else
390
+ Bolt::Project.create_project(dir)
391
+ end
392
+ else
393
+ Bolt::Project.find_boltdir(Dir.pwd)
394
+ end
449
395
 
450
- screen = "#{options[:subcommand]}_#{options[:action]}"
451
- # submit a different screen for `bolt task show` and `bolt task show foo`
452
- if options[:action] == 'show' && options[:object]
453
- screen += '_object'
454
- end
396
+ config = Bolt::Config.from_project(project, options)
455
397
 
456
- screen_view_fields = {
457
- output_format: config.format,
458
- # For continuity
459
- boltdir_type: config.project.type
460
- }.merge!(analytics.plan_counts(config.project.plans_path))
398
+ @outputter = Bolt::Outputter.for_format(
399
+ config.format,
400
+ config.color,
401
+ options[:verbose],
402
+ config.trace,
403
+ config.spinner
404
+ )
461
405
 
462
- # Only include target and inventory info for commands that take a targets
463
- # list. This avoids loading inventory for commands that don't need it.
464
- if options.key?(:targets)
465
- screen_view_fields.merge!(target_nodes: options[:targets].count,
466
- inventory_nodes: inventory.node_names.count,
467
- inventory_groups: inventory.group_names.count,
468
- inventory_version: inventory.version)
469
- end
406
+ @rerun = Bolt::Rerun.new(config.rerunfile, config.save_rerun)
470
407
 
471
- analytics.screen_view(screen, **screen_view_fields)
408
+ # TODO: Subscribe this to the executor.
409
+ analytics = begin
410
+ client = Bolt::Analytics.build_client(config.analytics)
411
+ client.bundled_content = bundled_content(options)
412
+ client
413
+ end
472
414
 
473
- case options[:action]
474
- when 'show'
475
- case options[:subcommand]
476
- when 'task'
477
- if options[:object]
478
- show_task(options[:object])
479
- else
480
- list_tasks
415
+ Bolt::Logger.configure(config.log, config.color, config.disable_warnings)
416
+ Bolt::Logger.stream = config.stream
417
+ Bolt::Logger.analytics = analytics
418
+ Bolt::Logger.flush_queue
419
+
420
+ executor = Bolt::Executor.new(
421
+ config.concurrency,
422
+ analytics,
423
+ options[:noop],
424
+ config.modified_concurrency,
425
+ config.future
426
+ )
427
+
428
+ pal = Bolt::PAL.new(
429
+ Bolt::Config::Modulepath.new(config.modulepath),
430
+ config.hiera_config,
431
+ config.project.resource_types,
432
+ config.compile_concurrency,
433
+ config.trusted_external,
434
+ config.apply_settings,
435
+ config.project
436
+ )
437
+
438
+ plugins = Bolt::Plugin.setup(config, pal, analytics)
439
+
440
+ inventory = Bolt::Inventory.from_config(config, plugins)
441
+
442
+ log_outputter = Bolt::Outputter::Logger.new(options[:verbose], config.trace)
443
+
444
+ #
445
+ # FINALIZING SETUP
446
+ #
447
+
448
+ check_gem_install
449
+ warn_inventory_overrides_cli(config, options)
450
+ submit_screen_view(analytics, config, inventory, options)
451
+ options[:targets] = process_target_list(plugins.puppetdb_client, @rerun, options)
452
+
453
+ # TODO: Fix casing issue in Windows.
454
+ config.check_path_case('modulepath', config.modulepath)
455
+
456
+ if options[:clear_cache]
457
+ FileUtils.rm(config.project.plugin_cache_file) if File.exist?(config.project.plugin_cache_file)
458
+ FileUtils.rm(config.project.task_cache_file) if File.exist?(config.project.task_cache_file)
459
+ FileUtils.rm(config.project.plan_cache_file) if File.exist?(config.project.plan_cache_file)
481
460
  end
482
- when 'plan'
483
- if options[:object]
484
- show_plan(options[:object])
461
+
462
+ case command
463
+ when 'apply', 'lookup'
464
+ if %w[human rainbow].include?(config.format)
465
+ executor.subscribe(outputter)
466
+ end
467
+ when 'plan'
468
+ if %w[human rainbow].include?(config.format)
469
+ executor.subscribe(outputter)
470
+ else
471
+ executor.subscribe(outputter, %i[message verbose])
472
+ end
485
473
  else
486
- list_plans
474
+ executor.subscribe(outputter)
487
475
  end
488
- when 'inventory'
489
- if options[:detail]
490
- show_targets
491
- else
492
- list_targets
476
+
477
+ executor.subscribe(log_outputter)
478
+
479
+ # TODO: Figure out where this should really go. It doesn't seem to
480
+ # make sense in the application, since the params should already
481
+ # be data when they reach that point.
482
+ if %w[plan task].include?(command) && action == 'run'
483
+ options[:params] = parse_params(
484
+ command,
485
+ options[:object],
486
+ pal,
487
+ **options.slice(:params, :params_parsed)
488
+ )
493
489
  end
494
- when 'group'
495
- list_groups
496
- when 'module'
497
- list_modules
498
- when 'plugin'
499
- list_plugins
490
+
491
+ application = Bolt::Application.new(
492
+ analytics: analytics,
493
+ config: config,
494
+ executor: executor,
495
+ inventory: inventory,
496
+ pal: pal,
497
+ plugins: plugins
498
+ )
499
+
500
+ process_command(application, command, action, options)
501
+ ensure
502
+ analytics&.finish
500
503
  end
501
- return 0
502
- when 'convert'
503
- pal.convert_plan(options[:object])
504
- return 0
505
504
  end
505
+ end
506
+
507
+ # Process the command.
508
+ #
509
+ # @param app [Bolt::Application] The application.
510
+ # @param command [String] The command.
511
+ # @param action [String, NilClass] The action.
512
+ # @param options [Hash] The CLI options.
513
+ #
514
+ private def process_command(app, command, action, options)
515
+ case command
516
+ when 'apply'
517
+ results = outputter.spin do
518
+ app.apply(options[:object], options[:targets], **options.slice(:code, :noop))
519
+ end
520
+ rerun.update(results)
521
+ app.shutdown
522
+ outputter.print_apply_result(results)
523
+ results.ok? ? SUCCESS : FAILURE
506
524
 
507
- message = 'There might be processes left executing on some nodes.'
525
+ when 'command'
526
+ outputter.print_head
527
+ results = app.run_command(options[:object], options[:targets], **options.slice(:env_vars))
528
+ rerun.update(results)
529
+ app.shutdown
530
+ outputter.print_summary(results, results.elapsed_time)
531
+ results.ok? ? SUCCESS : FAILURE
532
+
533
+ when 'file'
534
+ case action
535
+ when 'download'
536
+ outputter.print_head
537
+ results = app.download_file(options[:object], options[:leftovers].first, options[:targets])
538
+ rerun.update(results)
539
+ app.shutdown
540
+ outputter.print_summary(results, results.elapsed_time)
541
+ results.ok? ? SUCCESS : FAILURE
542
+ when 'upload'
543
+ outputter.print_head
544
+ results = app.upload_file(options[:object], options[:leftovers].first, options[:targets])
545
+ rerun.update(results)
546
+ app.shutdown
547
+ outputter.print_summary(results, results.elapsed_time)
548
+ results.ok? ? SUCCESS : FAILURE
549
+ end
508
550
 
509
- if %w[task plan].include?(options[:subcommand]) && options[:task_options] && !options[:params_parsed] && pal
510
- options[:task_options] = pal.parse_params(options[:subcommand], options[:object], options[:task_options])
511
- end
551
+ when 'group'
552
+ outputter.print_groups(**app.list_groups)
553
+ SUCCESS
512
554
 
513
- case options[:subcommand]
514
555
  when 'guide'
515
- code = if options[:object]
516
- show_guide(options[:object])
517
- else
518
- list_topics
519
- end
520
- when 'project'
521
- case options[:action]
522
- when 'init'
523
- code = Bolt::ProjectManager.new(config, outputter, pal)
524
- .create(Dir.pwd, options[:object], options[:modules])
525
- when 'migrate'
526
- code = Bolt::ProjectManager.new(config, outputter, pal).migrate
556
+ if options[:object]
557
+ outputter.print_guide(**app.show_guide(options[:object]))
558
+ else
559
+ outputter.print_topics(**app.list_guides)
527
560
  end
528
- when 'lookup'
529
- plan_vars = Hash[options[:leftovers].map { |a| a.split('=', 2) }]
530
- # Validate functions verifies one of these was passed
531
- if options[:targets]
532
- code = lookup(options[:object], options[:targets], plan_vars: plan_vars)
533
- elsif options[:plan_hierarchy]
534
- code = plan_lookup(options[:object], plan_vars: plan_vars)
561
+ SUCCESS
562
+
563
+ when 'inventory'
564
+ targets = app.show_inventory(options[:targets])
565
+ .merge(flag: !options[:targets].nil?)
566
+ if options[:detail]
567
+ outputter.print_target_info(**targets)
568
+ else
569
+ outputter.print_targets(**targets)
535
570
  end
536
- when 'plan'
537
- case options[:action]
538
- when 'new'
539
- plan_name = options[:object]
540
-
541
- # If this passes validation, it will return the path to the plan to create
542
- Bolt::PlanCreator.validate_input(config.project, plan_name)
543
- code = Bolt::PlanCreator.create_plan(config.project.plans_path,
544
- plan_name,
545
- outputter,
546
- options[:puppet])
547
- when 'run'
548
- code = run_plan(options[:object], options[:task_options], options[:target_args], options)
571
+ SUCCESS
572
+
573
+ when 'lookup'
574
+ options[:vars] = parse_vars(options[:leftovers])
575
+ if options[:plan_hierarchy]
576
+ outputter.print_plan_lookup(app.plan_lookup(options[:object], **options.slice(:vars)))
577
+ SUCCESS
578
+ else
579
+ results = outputter.spin do
580
+ app.lookup(options[:object], options[:targets], **options.slice(:vars))
581
+ end
582
+ rerun.update(results)
583
+ app.shutdown
584
+ outputter.print_result_set(results)
585
+ results.ok? ? SUCCESS : FAILURE
549
586
  end
587
+
550
588
  when 'module'
551
- case options[:action]
589
+ case action
552
590
  when 'add'
553
- code = add_project_module(options[:object], config.project, config.module_install)
554
- when 'install'
555
- code = install_project_modules(config.project, config.module_install, options[:force], options[:resolve])
591
+ ok = outputter.spin { app.add_module(options[:object], outputter) }
592
+ ok ? SUCCESS : FAILURE
556
593
  when 'generate-types'
557
- code = generate_types
558
- end
559
- when 'secret'
560
- code = Bolt::Secret.execute(plugins, outputter, options)
561
- when 'apply'
562
- if options[:object]
563
- validate_file('manifest', options[:object])
564
- options[:code] = File.read(File.expand_path(options[:object]))
594
+ app.generate_types
595
+ SUCCESS
596
+ when 'install'
597
+ ok = outputter.spin { app.install_modules(outputter, **options.slice(:force, :resolve)) }
598
+ ok ? SUCCESS : FAILURE
599
+ when 'show'
600
+ if options[:object]
601
+ outputter.print_module_info(**app.show_module(options[:object]))
602
+ else
603
+ outputter.print_module_list(app.list_modules)
604
+ end
605
+ SUCCESS
565
606
  end
566
- code = apply_manifest(options[:code], options[:targets], options[:object], options[:noop])
567
- else
568
- executor = Bolt::Executor.new(config.concurrency,
569
- analytics,
570
- options[:noop],
571
- config.modified_concurrency,
572
- config.future)
573
- targets = options[:targets]
574
-
575
- results = nil
576
- outputter.print_head
577
607
 
578
- elapsed_time = Benchmark.realtime do
579
- executor_opts = {}
580
- executor_opts[:env_vars] = options[:env_vars] if options.key?(:env_vars)
581
- executor.subscribe(outputter)
582
- executor.subscribe(log_outputter)
583
- results =
584
- case options[:subcommand]
585
- when 'command'
586
- executor.run_command(targets, options[:object], executor_opts)
587
- when 'script'
588
- script_path = find_file(options[:object], executor.future&.fetch('file_paths', false))
589
- validate_file('script', script_path)
590
- executor.run_script(targets, script_path, options[:leftovers], executor_opts)
591
- when 'task'
592
- pal.run_task(options[:object],
593
- targets,
594
- options[:task_options],
595
- executor,
596
- inventory)
597
- when 'file'
598
- src = options[:object]
599
- dest = options[:leftovers].first
600
-
601
- if src.nil?
602
- raise Bolt::CLIError, "A source path must be specified"
603
- end
604
-
605
- if dest.nil?
606
- raise Bolt::CLIError, "A destination path must be specified"
607
- end
608
-
609
- case options[:action]
610
- when 'download'
611
- dest = File.expand_path(dest, Dir.pwd)
612
- executor.download_file(targets, src, dest, executor_opts)
613
- when 'upload'
614
- src_path = find_file(src, executor.future&.fetch('file_paths', false))
615
- validate_file('source file', src_path, true)
616
- executor.upload_file(targets, src_path, dest, executor_opts)
617
- end
618
- end
608
+ when 'plan'
609
+ case action
610
+ when 'convert'
611
+ app.convert_plan(options[:object])
612
+ SUCCESS
613
+ when 'new'
614
+ result = app.new_plan(options[:object], **options.slice(:puppet))
615
+ outputter.print_new_plan(**result)
616
+ SUCCESS
617
+ when 'run'
618
+ result = app.run_plan(options[:object], options[:targets], **options.slice(:params))
619
+ rerun.update(result)
620
+ app.shutdown
621
+ outputter.print_plan_result(result)
622
+ result.ok? ? SUCCESS : FAILURE
623
+ when 'show'
624
+ if options[:object]
625
+ outputter.print_plan_info(app.show_plan(options[:object]))
626
+ else
627
+ outputter.print_plans(**app.list_plans(**options.slice(:filter)))
628
+ end
629
+ SUCCESS
619
630
  end
620
631
 
621
- executor.shutdown
622
- rerun.update(results)
623
-
624
- outputter.print_summary(results, elapsed_time)
625
- code = results.ok ? 0 : 2
626
- end
627
- code
628
- rescue Bolt::Error => e
629
- outputter.fatal_error(e)
630
- raise e
631
- ensure
632
- # restore original signal handler
633
- Signal.trap :INT, handler if handler
634
- analytics&.finish
635
- end
636
-
637
- def show_task(task_name)
638
- outputter.print_task_info(pal.get_task(task_name))
639
- end
640
-
641
- # Filters a list of content by matching substring.
642
- #
643
- private def filter_content(content, filter)
644
- return content unless content && filter
645
- content.select { |name,| name.include?(filter) }
646
- end
647
-
648
- def list_tasks
649
- tasks = filter_content(pal.list_tasks_with_cache(filter_content: true), options[:filter])
650
- outputter.print_tasks(tasks, pal.user_modulepath)
651
- end
632
+ when 'plugin'
633
+ outputter.print_plugin_list(**app.list_plugins)
634
+ SUCCESS
652
635
 
653
- def show_plan(plan_name)
654
- outputter.print_plan_info(pal.get_plan_info(plan_name))
655
- end
656
-
657
- def list_plans
658
- plans = filter_content(pal.list_plans_with_cache(filter_content: true), options[:filter])
659
- outputter.print_plans(plans, pal.user_modulepath)
660
- end
636
+ when 'project'
637
+ case action
638
+ when 'init'
639
+ app.create_project(options[:object], outputter, **options.slice(:modules))
640
+ SUCCESS
641
+ when 'migrate'
642
+ app.migrate_project(outputter)
643
+ SUCCESS
644
+ end
661
645
 
662
- def list_targets
663
- if options.keys.any? { |key| TARGETING_OPTIONS.include?(key) }
664
- target_flag = true
665
- else
666
- options[:targets] = 'all'
667
- end
646
+ when 'script'
647
+ outputter.print_head
648
+ opts = options.slice(:env_vars).merge(arguments: options[:leftovers])
649
+ results = app.run_script(options[:object], options[:targets], **opts)
650
+ rerun.update(results)
651
+ app.shutdown
652
+ outputter.print_summary(results, results.elapsed_time)
653
+ results.ok? ? SUCCESS : FAILURE
668
654
 
669
- outputter.print_targets(
670
- group_targets_by_source,
671
- inventory.source,
672
- config.default_inventoryfile,
673
- target_flag
674
- )
675
- end
655
+ when 'secret'
656
+ case action
657
+ when 'createkeys'
658
+ result = app.create_secret_keys(**options.slice(:force, :plugin))
659
+ outputter.print_message(result)
660
+ SUCCESS
661
+ when 'decrypt'
662
+ result = app.decrypt_secret(options[:object], **options.slice(:plugin))
663
+ outputter.print_message(result)
664
+ SUCCESS
665
+ when 'encrypt'
666
+ result = app.encrypt_secret(options[:object], **options.slice(:plugin))
667
+ outputter.print_message(result)
668
+ SUCCESS
669
+ end
676
670
 
677
- def show_targets
678
- if options.keys.any? { |key| TARGETING_OPTIONS.include?(key) }
679
- target_flag = true
680
- else
681
- options[:targets] = 'all'
671
+ when 'task'
672
+ case action
673
+ when 'run'
674
+ outputter.print_head
675
+ results = app.run_task(options[:object], options[:targets], **options.slice(:params))
676
+ rerun.update(results)
677
+ app.shutdown
678
+ outputter.print_summary(results, results.elapsed_time)
679
+ results.ok? ? SUCCESS : FAILURE
680
+ when 'show'
681
+ if options[:object]
682
+ outputter.print_task_info(**app.show_task(options[:object]))
683
+ else
684
+ outputter.print_tasks(**app.list_tasks(**options.slice(:filter)))
685
+ end
686
+ SUCCESS
687
+ end
682
688
  end
683
-
684
- outputter.print_target_info(
685
- group_targets_by_source,
686
- inventory.source,
687
- config.default_inventoryfile,
688
- target_flag
689
- )
690
689
  end
691
690
 
692
- # Returns a hash of targets sorted by those that are found in the
693
- # inventory and those that are provided on the command line.
691
+ # Process the target list by turning a PuppetDB query or rerun mode into a
692
+ # list of target names.
694
693
  #
695
- private def group_targets_by_source
696
- # Retrieve the known group and target names. This needs to be done before
697
- # updating targets, as that will add adhoc targets to the inventory.
698
- known_names = inventory.target_names
699
-
700
- update_targets(options)
701
-
702
- inventory_targets, adhoc_targets = options[:targets].partition do |target|
703
- known_names.include?(target.name)
694
+ # @param pdb_client [Bolt::PuppetDB::Client] The PuppetDB client.
695
+ # @param rerun [Bolt::Rerun] The Rerun instance.
696
+ # @param options [Hash] The CLI options.
697
+ # @return [Hash] The target list.
698
+ #
699
+ private def process_target_list(pdb_client, rerun, options)
700
+ if options[:query]
701
+ pdb_client.query_certnames(options[:query])
702
+ elsif options[:rerun]
703
+ rerun.get_targets(options[:rerun])
704
+ elsif options[:targets]
705
+ options[:targets]
704
706
  end
705
-
706
- { inventory: inventory_targets, adhoc: adhoc_targets }
707
- end
708
-
709
- def list_groups
710
- outputter.print_groups(inventory.group_names.sort, inventory.source, config.default_inventoryfile)
711
707
  end
712
708
 
713
- # Looks up a value with Hiera as if in a plan outside an apply block, using
714
- # provided variable values for interpolations
709
+ # List content that ships with Bolt.
715
710
  #
716
- def plan_lookup(key, plan_vars: {})
717
- result = pal.plan_hierarchy_lookup(key, plan_vars: plan_vars)
718
- outputter.print_plan_lookup(result)
719
- 0
720
- end
721
-
722
- # Looks up a value with Hiera, using targets as the contexts to perform the
723
- # look ups in. This should return the same value as a lookup in an apply block.
711
+ # @param options [Hash] The CLI options.
724
712
  #
725
- def lookup(key, targets, plan_vars: {})
726
- executor = Bolt::Executor.new(
727
- config.concurrency,
728
- analytics,
729
- options[:noop],
730
- config.modified_concurrency,
731
- config.future
732
- )
733
-
734
- executor.subscribe(outputter) if config.format == 'human'
735
- executor.subscribe(log_outputter)
736
- executor.publish_event(type: :plan_start, plan: nil)
737
-
738
- results = outputter.spin do
739
- pal.lookup(
740
- key,
741
- targets,
742
- inventory,
743
- executor,
744
- plan_vars: plan_vars
745
- )
746
- end
747
-
748
- executor.shutdown
749
- outputter.print_result_set(results)
750
-
751
- results.ok ? 0 : 1
752
- end
753
-
754
- def run_plan(plan_name, plan_arguments, nodes, options)
755
- unless nodes.empty?
756
- if plan_arguments['nodes'] || plan_arguments['targets']
757
- key = plan_arguments.include?('nodes') ? 'nodes' : 'targets'
758
- raise Bolt::CLIError,
759
- "A plan's '#{key}' parameter can be specified using the --#{key} option, but in that " \
760
- "case it must not be specified as a separate #{key}=<value> parameter nor included " \
761
- "in the JSON data passed in the --params option"
762
- end
763
-
764
- plan_params = pal.get_plan_info(plan_name)['parameters']
765
- target_param = plan_params.dig('targets', 'type') =~ /TargetSpec/
766
- node_param = plan_params.include?('nodes')
767
-
768
- if node_param && target_param
769
- msg = "Plan parameters include both 'nodes' and 'targets' with type 'TargetSpec', " \
770
- "neither will populated with the value for --nodes or --targets."
771
- Bolt::Logger.warn("nodes_targets_parameters", msg)
772
- elsif node_param
773
- plan_arguments['nodes'] = nodes.join(',')
774
- elsif target_param
775
- plan_arguments['targets'] = nodes.join(',')
776
- end
777
- end
778
-
779
- plan_context = { plan_name: plan_name,
780
- params: plan_arguments }
781
-
782
- executor = Bolt::Executor.new(config.concurrency,
783
- analytics,
784
- options[:noop],
785
- config.modified_concurrency,
786
- config.future)
787
- if %w[human rainbow].include?(options.fetch(:format, 'human'))
788
- executor.subscribe(outputter)
789
- else
790
- # Only subscribe to out module events for JSON outputter
791
- executor.subscribe(outputter, %i[message verbose])
792
- end
793
-
794
- executor.subscribe(log_outputter)
795
- executor.start_plan(plan_context)
796
- result = pal.run_plan(plan_name, plan_arguments, executor, inventory, puppetdb_client)
797
-
798
- # If a non-bolt exception bubbles up the plan won't get finished
799
- executor.finish_plan(result)
800
- executor.shutdown
801
- rerun.update(result)
802
-
803
- outputter.print_plan_result(result)
804
- result.ok? ? 0 : 1
805
- end
806
-
807
- def apply_manifest(code, targets, filename = nil, noop = false)
808
- Puppet[:tasks] = false
809
- ast = pal.parse_manifest(code, filename)
810
-
811
- if defined?(ast.body) &&
812
- (ast.body.is_a?(Puppet::Pops::Model::HostClassDefinition) ||
813
- ast.body.is_a?(Puppet::Pops::Model::ResourceTypeDefinition))
814
- message = "Manifest only contains definitions and will result in no changes on the targets. "\
815
- "Definitions must be declared for their resources to be applied. You can read more "\
816
- "about defining and declaring classes and types in the Puppet documentation at "\
817
- "https://puppet.com/docs/puppet/latest/lang_classes.html and "\
818
- "https://puppet.com/docs/puppet/latest/lang_defined_types.html"
819
- Bolt::Logger.warn("empty_manifest", message)
820
- end
821
-
822
- executor = Bolt::Executor.new(config.concurrency,
823
- analytics,
824
- noop,
825
- config.modified_concurrency,
826
- config.future)
827
- executor.subscribe(outputter) if options.fetch(:format, 'human') == 'human'
828
- executor.subscribe(log_outputter)
829
- # apply logging looks like plan logging, so tell the outputter we're in a
830
- # plan even though we're not
831
- executor.publish_event(type: :plan_start, plan: nil)
832
-
833
- results = nil
834
- elapsed_time = Benchmark.realtime do
835
- pal.in_plan_compiler(executor, inventory, puppetdb_client) do |compiler|
836
- compiler.call_function('apply_prep', targets)
713
+ private def bundled_content(options)
714
+ # We only need to enumerate bundled content when running a task or plan
715
+ content = { 'Plan' => [],
716
+ 'Task' => [],
717
+ 'Plugin' => Bolt::Plugin::BUILTIN_PLUGINS }
718
+ if %w[plan task].include?(options[:subcommand]) && options[:action] == 'run'
719
+ default_content = Bolt::PAL.new(Bolt::Config::Modulepath.new([]), nil, nil)
720
+ content['Plan'] = default_content.list_plans.each_with_object([]) do |iter, col|
721
+ col << iter&.first
837
722
  end
838
-
839
- results = pal.with_bolt_executor(executor, inventory, puppetdb_client) do
840
- Puppet.lookup(:apply_executor).apply_ast(ast, targets, catch_errors: true, noop: noop)
723
+ content['Task'] = default_content.list_tasks.each_with_object([]) do |iter, col|
724
+ col << iter&.first
841
725
  end
842
726
  end
843
727
 
844
- executor.shutdown
845
- outputter.print_apply_result(results, elapsed_time)
846
- rerun.update(results)
847
-
848
- results.ok ? 0 : 1
849
- end
850
-
851
- def list_modules
852
- outputter.print_module_list(pal.list_modules)
853
- end
854
-
855
- def list_plugins
856
- outputter.print_plugin_list(plugins.list_plugins, pal.user_modulepath)
857
- end
858
-
859
- def generate_types
860
- # generate_types will surface a nice error with helpful message if it fails
861
- pal.generate_types(cache: true)
862
- 0
728
+ content
863
729
  end
864
730
 
865
- # Installs modules declared in the project configuration file.
731
+ # Check and warn if Bolt is installed as a gem.
866
732
  #
867
- def install_project_modules(project, config, force, resolve)
868
- assert_project_file(project)
869
-
870
- if project.modules.empty? && resolve != false
871
- outputter.print_message(
872
- "Project configuration file #{project.project_file} does not "\
873
- "specify any module dependencies. Nothing to do."
874
- )
875
- return 0
876
- end
733
+ private def check_gem_install
734
+ if ENV['BOLT_GEM'].nil? && incomplete_install?
735
+ msg = <<~MSG.chomp
736
+ Bolt might be installed as a gem. To use Bolt reliably and with all of its
737
+ dependencies, uninstall the 'bolt' gem and install Bolt as a package:
738
+ https://puppet.com/docs/bolt/latest/bolt_installing.html
877
739
 
878
- installer = Bolt::ModuleInstaller.new(outputter, pal)
740
+ If you meant to install Bolt as a gem and want to disable this warning,
741
+ set the BOLT_GEM environment variable.
742
+ MSG
879
743
 
880
- ok = outputter.spin do
881
- installer.install(project.modules,
882
- project.puppetfile,
883
- project.managed_moduledir,
884
- config,
885
- force: force,
886
- resolve: resolve)
744
+ Bolt::Logger.warn("gem_install", msg)
887
745
  end
888
-
889
- ok ? 0 : 1
890
746
  end
891
747
 
892
- # Adds a single module to the project.
748
+ # Print a fatal error. Print using the outputter if it's configured.
749
+ # Otherwise, mock the output by printing directly to stdout.
750
+ #
751
+ # @param error [StandardError] The error to print.
893
752
  #
894
- def add_project_module(name, project, config)
895
- assert_project_file(project)
896
-
897
- installer = Bolt::ModuleInstaller.new(outputter, pal)
898
-
899
- ok = outputter.spin do
900
- installer.add(name,
901
- project.modules,
902
- project.puppetfile,
903
- project.managed_moduledir,
904
- project.project_file,
905
- config)
753
+ private def fatal_error(error)
754
+ if @outputter
755
+ @outputter.fatal_error(error)
756
+ elsif $stdout.isatty
757
+ $stdout.puts("\033[31m#{error.message}\033[0m")
758
+ else
759
+ $stdout.puts(error.message)
906
760
  end
907
-
908
- ok ? 0 : 1
909
761
  end
910
762
 
911
- # Asserts that there is a project configuration file.
763
+ # Query whether Bolt is installed as a gem or package by checking if all
764
+ # built-in modules are installed.
912
765
  #
913
- def assert_project_file(project)
914
- unless project.project_file?
915
- command = Bolt::Util.powershell? ? 'New-BoltProject' : 'bolt project init'
916
-
917
- msg = "Could not find project configuration file #{project.project_file}, unable "\
918
- "to install modules. To create a Bolt project, run '#{command}'."
919
-
920
- raise Bolt::Error.new(msg, 'bolt/missing-project-config-error')
921
- end
766
+ private def incomplete_install?
767
+ builtin_module_list = %w[aggregate canary puppetdb_fact secure_env_vars puppet_connect]
768
+ (Dir.children(Bolt::Config::Modulepath::MODULES_PATH) - builtin_module_list).empty?
922
769
  end
923
770
 
924
- # Loads a Puppetfile and installs its modules.
771
+ # Parse parameters for tasks and plans.
772
+ #
773
+ # @param options [Hash] Options from the calling method.
925
774
  #
926
- def install_puppetfile(puppetfile_config, puppetfile, moduledir)
927
- outputter.print_message("Installing modules from Puppetfile")
928
- installer = Bolt::ModuleInstaller.new(outputter, pal)
929
- ok = outputter.spin do
930
- installer.install_puppetfile(puppetfile, moduledir, puppetfile_config)
775
+ private def parse_params(command, object, pal, params: nil, params_parsed: nil)
776
+ if params
777
+ params_parsed ? params : pal.parse_params(command, object, params)
778
+ else
779
+ {}
931
780
  end
932
-
933
- ok ? 0 : 1
934
781
  end
935
782
 
936
- def pal
937
- @pal ||= Bolt::PAL.new(Bolt::Config::Modulepath.new(config.modulepath),
938
- config.hiera_config,
939
- config.project.resource_types,
940
- config.compile_concurrency,
941
- config.trusted_external,
942
- config.apply_settings,
943
- config.project)
783
+ # Parse variables for lookups.
784
+ #
785
+ # @param vars [Array, NilClass] Unparsed variables.
786
+ #
787
+ private def parse_vars(vars)
788
+ return unless vars
789
+ Hash[vars.map { |a| a.split('=', 2) }]
944
790
  end
945
791
 
946
- # Collects the list of Bolt guides and maps them to their topics.
947
- def guides
948
- @guides ||= begin
949
- root_path = File.expand_path(File.join(__dir__, '..', '..', 'guides'))
950
- files = Dir.children(root_path).sort
792
+ # TODO: See if this can be moved to Bolt::Analytics.
793
+ #
794
+ # Submit a screen view to the analytics client.
795
+ #
796
+ # @param analytics [Bolt::Analytics] The analytics client.
797
+ # @param config [Bolt::Config] The config.
798
+ # @param inventory [Bolt::Inventory] The inventory.
799
+ # @param options [Hash] The CLI options.
800
+ #
801
+ private def submit_screen_view(analytics, config, inventory, options)
802
+ screen = "#{options[:subcommand]}_#{options[:action]}"
951
803
 
952
- files.each_with_object({}) do |file, guides|
953
- next if file !~ /\.txt\z/
954
- topic = File.basename(file, '.txt')
955
- guides[topic] = File.join(root_path, file)
956
- end
957
- rescue SystemCallError => e
958
- raise Bolt::FileError.new("#{e.message}: unable to load guides directory", root_path)
804
+ if options[:action] == 'show' && options[:object]
805
+ screen += '_object'
959
806
  end
960
- end
961
-
962
- # Display the list of available Bolt guides.
963
- def list_topics
964
- outputter.print_topics(guides.keys - ['guide'])
965
- 0
966
- end
967
807
 
968
- # Display a specific Bolt guide.
969
- def show_guide(topic)
970
- if guides[topic]
971
- analytics.event('Guide', 'known_topic', label: topic)
808
+ pp_count, yaml_count = if File.exist?(config.project.plans_path)
809
+ %w[pp yaml].map do |extension|
810
+ Find.find(config.project.plans_path.to_s)
811
+ .grep(/.*\.#{extension}/)
812
+ .length
813
+ end
814
+ else
815
+ [0, 0]
816
+ end
972
817
 
973
- begin
974
- guide = File.read(guides[topic])
975
- rescue SystemCallError => e
976
- raise Bolt::FileError("#{e.message}: unable to load guide page", filepath)
977
- end
978
-
979
- outputter.print_guide(guide, topic)
980
- else
981
- analytics.event('Guide', 'unknown_topic', label: topic)
982
- outputter.print_message("Did not find guide for topic '#{topic}'.\n\n")
983
- list_topics
984
- end
985
- 0
986
- end
818
+ screen_view_fields = {
819
+ output_format: config.format,
820
+ boltdir_type: config.project.type,
821
+ puppet_plan_count: pp_count,
822
+ yaml_plan_count: yaml_count
823
+ }
987
824
 
988
- def validate_file(type, path, allow_dir = false)
989
- if path.nil?
990
- raise Bolt::CLIError, "A #{type} must be specified"
825
+ if options.key?(:targets)
826
+ screen_view_fields.merge!(
827
+ target_nodes: options[:targets].count,
828
+ inventory_nodes: inventory.node_names.count,
829
+ inventory_groups: inventory.group_names.count,
830
+ inventory_version: inventory.version
831
+ )
991
832
  end
992
833
 
993
- Bolt::Util.validate_file(type, path, allow_dir)
834
+ analytics.screen_view(screen, **screen_view_fields)
994
835
  end
995
836
 
996
- # Returns the path to a file. If the path is an absolute or relative to
997
- # a file, and the file exists, returns the path as-is. Otherwise, checks if
998
- # the path is a Puppet file path and looks for the file in a module's files
999
- # directory.
837
+ # Issue a deprecation warning if the user is running an unsupported version
838
+ # of PowerShell on the controller.
1000
839
  #
1001
- def find_file(path, future_file_paths)
1002
- return path if File.exist?(path) || Pathname.new(path).absolute?
1003
- modulepath = Bolt::Config::Modulepath.new(config.modulepath)
1004
- modules = Bolt::Module.discover(modulepath.full_modulepath, config.project)
1005
- mod, file = path.split(File::SEPARATOR, 2)
1006
-
1007
- if modules[mod]
1008
- @logger.debug("Did not find file at #{File.expand_path(path)}, checking in module '#{mod}'")
1009
- found = Bolt::Util.find_file_in_module(modules[mod].path, file || "", future_file_paths)
1010
- path = found.nil? ? File.join(modules[mod].path, 'files', file) : found
1011
- end
1012
- path
1013
- end
840
+ private def validate_ps_version
841
+ if Bolt::Util.powershell?
842
+ command = "powershell.exe -NoProfile -NonInteractive -NoLogo -ExecutionPolicy "\
843
+ "Bypass -Command $PSVersionTable.PSVersion.Major"
844
+ stdout, _stderr, _status = Open3.capture3(command)
1014
845
 
1015
- def rerun
1016
- @rerun ||= Bolt::Rerun.new(config.rerunfile, config.save_rerun)
1017
- end
846
+ return unless !stdout.empty? && stdout.to_i < 3
1018
847
 
1019
- def outputter
1020
- @outputter ||= Bolt::Outputter.for_format(config.format,
1021
- config.color,
1022
- options[:verbose],
1023
- config.trace,
1024
- config.spinner)
848
+ msg = "Detected PowerShell 2 on controller. PowerShell 2 is unsupported."
849
+ Bolt::Logger.deprecation_warning("powershell_2_controller", msg)
850
+ end
1025
851
  end
1026
852
 
1027
- def log_outputter
1028
- @log_outputter ||= Bolt::Outputter::Logger.new(options[:verbose], config.trace)
1029
- end
853
+ # Warn the user that transport configuration options set from the command
854
+ # line may be overridden by transport configuration set in the inventory.
855
+ #
856
+ # @param opts [Hash] The CLI options.
857
+ #
858
+ private def warn_inventory_overrides_cli(config, opts)
859
+ inventory_source = if ENV[Bolt::Inventory::ENVIRONMENT_VAR]
860
+ Bolt::Inventory::ENVIRONMENT_VAR
861
+ elsif config.inventoryfile
862
+ config.inventoryfile
863
+ elsif File.exist?(config.default_inventoryfile)
864
+ config.default_inventoryfile
865
+ end
1030
866
 
1031
- def analytics
1032
- @analytics ||= begin
1033
- client = Bolt::Analytics.build_client(config.analytics)
1034
- client.bundled_content = bundled_content
1035
- client
867
+ inventory_cli_opts = %i[authentication escalation transports].each_with_object([]) do |key, acc|
868
+ acc.concat(Bolt::BoltOptionParser::OPTIONS[key])
1036
869
  end
1037
- end
1038
870
 
1039
- def bundled_content
1040
- # If the bundled content directory is empty, Bolt is likely installed as a gem.
1041
- if ENV['BOLT_GEM'].nil? && incomplete_install?
1042
- msg = <<~MSG.chomp
1043
- Bolt might be installed as a gem. To use Bolt reliably and with all of its
1044
- dependencies, uninstall the 'bolt' gem and install Bolt as a package:
1045
- https://puppet.com/docs/bolt/latest/bolt_installing.html
1046
-
1047
- If you meant to install Bolt as a gem and want to disable this warning,
1048
- set the BOLT_GEM environment variable.
1049
- MSG
871
+ inventory_cli_opts.concat(%w[no-host-key-check no-ssl no-ssl-verify no-tty])
1050
872
 
1051
- Bolt::Logger.warn("gem_install", msg)
1052
- end
873
+ conflicting_options = Set.new(opts.keys.map(&:to_s)).intersection(inventory_cli_opts)
1053
874
 
1054
- # We only need to enumerate bundled content when running a task or plan
1055
- content = { 'Plan' => [],
1056
- 'Task' => [],
1057
- 'Plugin' => Bolt::Plugin::BUILTIN_PLUGINS }
1058
- if %w[plan task].include?(options[:subcommand]) && options[:action] == 'run'
1059
- default_content = Bolt::PAL.new(Bolt::Config::Modulepath.new([]), nil, nil)
1060
- content['Plan'] = default_content.list_plans.each_with_object([]) do |iter, col|
1061
- col << iter&.first
1062
- end
1063
- content['Task'] = default_content.list_tasks.each_with_object([]) do |iter, col|
1064
- col << iter&.first
1065
- end
875
+ if inventory_source && conflicting_options.any?
876
+ Bolt::Logger.warn(
877
+ "cli_overrides",
878
+ "CLI arguments #{conflicting_options.to_a} might be overridden by Inventory: #{inventory_source}"
879
+ )
1066
880
  end
1067
-
1068
- content
1069
881
  end
1070
882
 
1071
- # Gem installs include the aggregate, canary, and puppetdb_fact modules, while
1072
- # package installs include modules listed in the Bolt repo Puppetfile
1073
- def incomplete_install?
1074
- builtin_module_list = %w[aggregate canary puppetdb_fact secure_env_vars puppet_connect]
1075
- (Dir.children(Bolt::Config::Modulepath::MODULES_PATH) - builtin_module_list).empty?
883
+ # Handle and print errors.
884
+ #
885
+ private def with_error_handling
886
+ yield
887
+ rescue Bolt::Error => e
888
+ fatal_error(e)
889
+ raise e
1076
890
  end
1077
891
 
1078
- # Mimicks the output from Outputter::Human#fatal_error. This should be used to print
1079
- # errors prior to config being loaded, as the outputter relies on config being loaded.
1080
- def fatal_error(error)
1081
- if $stdout.isatty
1082
- $stdout.puts("\033[31m#{error.message}\033[0m")
1083
- else
1084
- $stdout.puts(error.message)
892
+ # Handle signals.
893
+ #
894
+ private def with_signal_handling
895
+ handler = Signal.trap :INT do |signo|
896
+ Bolt::Logger.logger(self).info(
897
+ "Exiting after receiving SIG#{Signal.signame(signo)} signal. "\
898
+ "There might be processes left executing on some targets."
899
+ )
900
+ exit!
1085
901
  end
902
+
903
+ yield
904
+ ensure
905
+ Signal.trap :INT, handler if handler
1086
906
  end
1087
907
  end
1088
908
  end