bolt 3.15.0 → 3.16.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.

@@ -897,7 +897,7 @@ module Bolt
897
897
  end
898
898
  define('--params PARAMETERS',
899
899
  "Parameters to a task or plan as json, a json file '@<file>', or on stdin '-'.") do |params|
900
- @options[:task_options] = parse_params(params)
900
+ @options[:params] = parse_params(params)
901
901
  end
902
902
  define('-e', '--execute CODE',
903
903
  "Puppet manifest code to apply to the targets.") do |code|
@@ -1086,8 +1086,7 @@ module Bolt
1086
1086
  @options[:help] = true
1087
1087
  end
1088
1088
  define('--version', 'Display the version.') do |_|
1089
- puts Bolt::VERSION
1090
- raise Bolt::CLIExit
1089
+ @options[:version] = true
1091
1090
  end
1092
1091
  define('--log-level LEVEL',
1093
1092
  "Set the log level for the console. Available options are",
@@ -1130,5 +1129,15 @@ module Bolt
1130
1129
  rescue JSON::ParserError => e
1131
1130
  raise Bolt::CLIError, "Unable to parse --params value as JSON: #{e}"
1132
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
1133
1142
  end
1134
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,151 +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]
242
- FileUtils.rm(config.project.plugin_cache_file) if File.exist?(config.project.plugin_cache_file)
243
- FileUtils.rm(config.project.task_cache_file) if File.exist?(config.project.task_cache_file)
244
- FileUtils.rm(config.project.plan_cache_file) if File.exist?(config.project.plan_cache_file)
245
- end
246
-
247
- warn_inventory_overrides_cli(options)
248
- validate_ps_version
249
-
250
- options
251
- rescue Bolt::Error => e
252
- outputter.fatal_error(e)
253
- raise e
254
- end
255
-
256
- private def validate_ps_version
257
- if Bolt::Util.powershell?
258
- command = "powershell.exe -NoProfile -NonInteractive -NoLogo -ExecutionPolicy "\
259
- "Bypass -Command $PSVersionTable.PSVersion.Major"
260
- stdout, _stderr, _status = Open3.capture3(command)
261
-
262
- return unless !stdout.empty? && stdout.to_i < 3
263
-
264
- msg = "Detected PowerShell 2 on controller. PowerShell 2 is unsupported."
265
- Bolt::Logger.deprecation_warning("powershell_2_controller", msg)
266
- end
267
- end
268
-
269
- def update_targets(options)
270
- target_opts = options.keys.select { |opt| TARGETING_OPTIONS.include?(opt) }
271
- target_string = "'--targets', '--rerun', or '--query'"
272
- if target_opts.length > 1
273
- raise Bolt::CLIError, "Only one targeting option #{target_string} can be specified"
274
- elsif target_opts.empty? && options[:subcommand] != 'plan'
275
- raise Bolt::CLIError, "Command requires a targeting option: #{target_string}"
276
- end
277
-
278
- targets = if options[:query]
279
- query_puppetdb_nodes(options[:query])
280
- elsif options[:rerun]
281
- rerun.get_targets(options[:rerun])
282
- else
283
- options[:targets] || []
284
- end
285
- options[:target_args] = targets
286
- options[:targets] = inventory.get_targets(targets)
287
- end
288
-
289
- 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)
290
224
  unless COMMANDS.include?(options[:subcommand])
291
225
  command = Bolt::Util.powershell? ? 'Get-Command -Module PuppetBolt' : 'bolt help'
292
226
  raise Bolt::CLIError,
@@ -351,12 +285,34 @@ module Bolt
351
285
  raise Bolt::CLIError, "Must specify a module name."
352
286
  end
353
287
 
288
+ if options[:action] == 'convert' && !options[:object]
289
+ raise Bolt::CLIError, "Must specify a plan."
290
+ end
291
+
354
292
  if options[:subcommand] == 'module' && options[:action] == 'install' && options[:object]
355
293
  command = Bolt::Util.powershell? ? 'Add-BoltModule -Module' : 'bolt module add'
356
294
  raise Bolt::CLIError, "Invalid argument '#{options[:object]}'. To add a new module to "\
357
295
  "the project, run '#{command} #{options[:object]}'."
358
296
  end
359
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
+
360
316
  if !%w[file script lookup].include?(options[:subcommand]) &&
361
317
  !options[:leftovers].empty?
362
318
  raise Bolt::CLIError,
@@ -381,728 +337,572 @@ module Bolt
381
337
  "Option '--env-var' can only be specified when running a command or script"
382
338
  end
383
339
  end
384
- end
385
-
386
- def handle_parser_errors
387
- yield
388
- rescue OptionParser::MissingArgument => e
389
- raise Bolt::CLIError, "Option '#{e.args.first}' needs a parameter"
390
- rescue OptionParser::InvalidArgument => e
391
- raise Bolt::CLIError, "Invalid parameter specified for option '#{e.args.first}': #{e.args[1]}"
392
- rescue OptionParser::InvalidOption, OptionParser::AmbiguousOption => e
393
- raise Bolt::CLIError, "Unknown argument '#{e.args.first}'"
394
- end
395
-
396
- def puppetdb_client
397
- plugins.puppetdb_client
398
- end
399
340
 
400
- def plugins
401
- @plugins ||= Bolt::Plugin.setup(config, pal, analytics)
341
+ validate_targeting_options(options)
402
342
  end
403
343
 
404
- def query_puppetdb_nodes(query)
405
- puppetdb_client.query_certnames(query)
406
- end
407
-
408
- def warn_inventory_overrides_cli(opts)
409
- inventory_source = if ENV[Bolt::Inventory::ENVIRONMENT_VAR]
410
- Bolt::Inventory::ENVIRONMENT_VAR
411
- elsif config.inventoryfile
412
- config.inventoryfile
413
- elsif File.exist?(config.default_inventoryfile)
414
- config.default_inventoryfile
415
- 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'"
416
352
 
417
- inventory_cli_opts = %i[authentication escalation transports].each_with_object([]) do |key, acc|
418
- 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}"
419
355
  end
420
356
 
421
- inventory_cli_opts.concat(%w[no-host-key-check no-ssl no-ssl-verify no-tty])
422
-
423
- conflicting_options = Set.new(opts.keys.map(&:to_s)).intersection(inventory_cli_opts)
357
+ return if %w[guide module plan project secret].include?(options[:subcommand]) ||
358
+ %w[convert new show].include?(options[:action]) ||
359
+ options[:plan_hierarchy]
424
360
 
425
- if inventory_source && conflicting_options.any?
426
- Bolt::Logger.warn(
427
- "cli_overrides",
428
- "CLI arguments #{conflicting_options.to_a} might be overridden by Inventory: #{inventory_source}"
429
- )
361
+ if target_opts.empty?
362
+ raise Bolt::CLIError, "Command requires a targeting option: #{target_string}"
430
363
  end
431
364
  end
432
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
+ #
433
372
  def execute(options)
434
- message = nil
435
-
436
- handler = Signal.trap :INT do |signo|
437
- @logger.info(
438
- "Exiting after receiving SIG#{Signal.signame(signo)} signal.#{message ? ' ' + message : ''}"
439
- )
440
- exit!
441
- end
442
-
443
- # Initialize inventory and targets. Errors here are better to catch early.
444
- # options[:target_args] will contain a string/array version of the targeting options this is passed to plans
445
- # options[:targets] will contain a resolved set of Target objects
446
- unless %w[guide module project secret].include?(options[:subcommand]) ||
447
- %w[convert new show].include?(options[:action]) ||
448
- options[:plan_hierarchy]
449
- update_targets(options)
450
- end
451
-
452
- screen = "#{options[:subcommand]}_#{options[:action]}"
453
- # submit a different screen for `bolt task show` and `bolt task show foo`
454
- if options[:action] == 'show' && options[:object]
455
- screen += '_object'
456
- 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
457
395
 
458
- screen_view_fields = {
459
- output_format: config.format,
460
- # For continuity
461
- boltdir_type: config.project.type
462
- }.merge!(analytics.plan_counts(config.project.plans_path))
396
+ config = Bolt::Config.from_project(project, options)
463
397
 
464
- # Only include target and inventory info for commands that take a targets
465
- # list. This avoids loading inventory for commands that don't need it.
466
- if options.key?(:targets)
467
- screen_view_fields.merge!(target_nodes: options[:targets].count,
468
- inventory_nodes: inventory.node_names.count,
469
- inventory_groups: inventory.group_names.count,
470
- inventory_version: inventory.version)
471
- end
398
+ @outputter = Bolt::Outputter.for_format(
399
+ config.format,
400
+ config.color,
401
+ options[:verbose],
402
+ config.trace,
403
+ config.spinner
404
+ )
472
405
 
473
- analytics.screen_view(screen, **screen_view_fields)
406
+ @rerun = Bolt::Rerun.new(config.rerunfile, config.save_rerun)
474
407
 
475
- case options[:action]
476
- when 'show'
477
- case options[:subcommand]
478
- when 'task'
479
- if options[:object]
480
- show_task(options[:object])
481
- else
482
- list_tasks
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
483
413
  end
484
- when 'plan'
485
- if options[:object]
486
- show_plan(options[:object])
487
- else
488
- list_plans
414
+
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)
489
460
  end
490
- when 'inventory'
491
- if options[:detail]
492
- show_targets
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
493
473
  else
494
- list_targets
474
+ executor.subscribe(outputter)
495
475
  end
496
- when 'group'
497
- list_groups
498
- when 'module'
499
- if options[:object]
500
- show_module(options[:object])
501
- else
502
- list_modules
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
+ )
503
489
  end
504
- when 'plugin'
505
- 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
506
503
  end
507
- return 0
508
- when 'convert'
509
- pal.convert_plan(options[:object])
510
- return 0
511
504
  end
505
+ end
512
506
 
513
- message = 'There might be processes left executing on some nodes.'
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
514
524
 
515
- if %w[task plan].include?(options[:subcommand]) && options[:task_options] && !options[:params_parsed] && pal
516
- options[:task_options] = pal.parse_params(options[:subcommand], options[:object], options[:task_options])
517
- end
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
550
+
551
+ when 'group'
552
+ outputter.print_groups(**app.list_groups)
553
+ SUCCESS
518
554
 
519
- case options[:subcommand]
520
555
  when 'guide'
521
- code = if options[:object]
522
- show_guide(options[:object])
523
- else
524
- list_topics
525
- end
526
- when 'project'
527
- case options[:action]
528
- when 'init'
529
- code = Bolt::ProjectManager.new(config, outputter, pal)
530
- .create(Dir.pwd, options[:object], options[:modules])
531
- when 'migrate'
532
- 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)
533
560
  end
534
- when 'lookup'
535
- plan_vars = Hash[options[:leftovers].map { |a| a.split('=', 2) }]
536
- # Validate functions verifies one of these was passed
537
- if options[:targets]
538
- code = lookup(options[:object], options[:targets], plan_vars: plan_vars)
539
- elsif options[:plan_hierarchy]
540
- 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)
541
570
  end
542
- when 'plan'
543
- case options[:action]
544
- when 'new'
545
- plan_name = options[:object]
546
-
547
- # If this passes validation, it will return the path to the plan to create
548
- Bolt::PlanCreator.validate_input(config.project, plan_name)
549
- code = Bolt::PlanCreator.create_plan(config.project.plans_path,
550
- plan_name,
551
- outputter,
552
- options[:puppet])
553
- when 'run'
554
- 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
555
586
  end
587
+
556
588
  when 'module'
557
- case options[:action]
589
+ case action
558
590
  when 'add'
559
- code = add_project_module(options[:object], config.project, config.module_install)
560
- when 'install'
561
- 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
562
593
  when 'generate-types'
563
- code = generate_types
564
- end
565
- when 'secret'
566
- code = Bolt::Secret.execute(plugins, outputter, options)
567
- when 'apply'
568
- if options[:object]
569
- validate_file('manifest', options[:object])
570
- 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
571
606
  end
572
- code = apply_manifest(options[:code], options[:targets], options[:object], options[:noop])
573
- else
574
- executor = Bolt::Executor.new(config.concurrency,
575
- analytics,
576
- options[:noop],
577
- config.modified_concurrency,
578
- config.future)
579
- targets = options[:targets]
580
-
581
- results = nil
582
- outputter.print_head
583
607
 
584
- elapsed_time = Benchmark.realtime do
585
- executor_opts = {}
586
- executor_opts[:env_vars] = options[:env_vars] if options.key?(:env_vars)
587
- executor.subscribe(outputter)
588
- executor.subscribe(log_outputter)
589
- results =
590
- case options[:subcommand]
591
- when 'command'
592
- executor.run_command(targets, options[:object], executor_opts)
593
- when 'script'
594
- script_path = find_file(options[:object], executor.future&.fetch('file_paths', false))
595
- validate_file('script', script_path)
596
- executor.run_script(targets, script_path, options[:leftovers], executor_opts)
597
- when 'task'
598
- pal.run_task(options[:object],
599
- targets,
600
- options[:task_options],
601
- executor,
602
- inventory)
603
- when 'file'
604
- src = options[:object]
605
- dest = options[:leftovers].first
606
-
607
- if src.nil?
608
- raise Bolt::CLIError, "A source path must be specified"
609
- end
610
-
611
- if dest.nil?
612
- raise Bolt::CLIError, "A destination path must be specified"
613
- end
614
-
615
- case options[:action]
616
- when 'download'
617
- dest = File.expand_path(dest, Dir.pwd)
618
- executor.download_file(targets, src, dest, executor_opts)
619
- when 'upload'
620
- src_path = find_file(src, executor.future&.fetch('file_paths', false))
621
- validate_file('source file', src_path, true)
622
- executor.upload_file(targets, src_path, dest, executor_opts)
623
- end
624
- 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
625
630
  end
626
631
 
627
- executor.shutdown
628
- rerun.update(results)
632
+ when 'plugin'
633
+ outputter.print_plugin_list(**app.list_plugins)
634
+ SUCCESS
629
635
 
630
- outputter.print_summary(results, elapsed_time)
631
- code = results.ok ? 0 : 2
632
- end
633
- code
634
- rescue Bolt::Error => e
635
- outputter.fatal_error(e)
636
- raise e
637
- ensure
638
- # restore original signal handler
639
- Signal.trap :INT, handler if handler
640
- analytics&.finish
641
- end
642
-
643
- def show_task(task_name)
644
- outputter.print_task_info(pal.get_task(task_name))
645
- end
646
-
647
- # Filters a list of content by matching substring.
648
- #
649
- private def filter_content(content, filter)
650
- return content unless content && filter
651
- content.select { |name,| name.include?(filter) }
652
- end
653
-
654
- def list_tasks
655
- tasks = filter_content(pal.list_tasks_with_cache(filter_content: true), options[:filter])
656
- outputter.print_tasks(tasks, pal.user_modulepath)
657
- end
658
-
659
- def show_plan(plan_name)
660
- outputter.print_plan_info(pal.get_plan_info(plan_name))
661
- end
662
-
663
- def list_plans
664
- plans = filter_content(pal.list_plans_with_cache(filter_content: true), options[:filter])
665
- outputter.print_plans(plans, pal.user_modulepath)
666
- 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
667
645
 
668
- def list_targets
669
- if options.keys.any? { |key| TARGETING_OPTIONS.include?(key) }
670
- target_flag = true
671
- else
672
- options[:targets] = 'all'
673
- 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
674
654
 
675
- outputter.print_targets(
676
- group_targets_by_source,
677
- inventory.source,
678
- config.default_inventoryfile,
679
- target_flag
680
- )
681
- 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
682
670
 
683
- def show_targets
684
- if options.keys.any? { |key| TARGETING_OPTIONS.include?(key) }
685
- target_flag = true
686
- else
687
- 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
688
688
  end
689
-
690
- outputter.print_target_info(
691
- group_targets_by_source,
692
- inventory.source,
693
- config.default_inventoryfile,
694
- target_flag
695
- )
696
689
  end
697
690
 
698
- # Returns a hash of targets sorted by those that are found in the
699
- # 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.
700
693
  #
701
- private def group_targets_by_source
702
- # Retrieve the known group and target names. This needs to be done before
703
- # updating targets, as that will add adhoc targets to the inventory.
704
- known_names = inventory.target_names
705
-
706
- update_targets(options)
707
-
708
- inventory_targets, adhoc_targets = options[:targets].partition do |target|
709
- 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]
710
706
  end
711
-
712
- { inventory: inventory_targets, adhoc: adhoc_targets }
713
- end
714
-
715
- def list_groups
716
- outputter.print_groups(inventory.group_names.sort, inventory.source, config.default_inventoryfile)
717
707
  end
718
708
 
719
- # Looks up a value with Hiera as if in a plan outside an apply block, using
720
- # provided variable values for interpolations
709
+ # List content that ships with Bolt.
721
710
  #
722
- def plan_lookup(key, plan_vars: {})
723
- result = pal.plan_hierarchy_lookup(key, plan_vars: plan_vars)
724
- outputter.print_plan_lookup(result)
725
- 0
726
- end
727
-
728
- # Looks up a value with Hiera, using targets as the contexts to perform the
729
- # look ups in. This should return the same value as a lookup in an apply block.
711
+ # @param options [Hash] The CLI options.
730
712
  #
731
- def lookup(key, targets, plan_vars: {})
732
- executor = Bolt::Executor.new(
733
- config.concurrency,
734
- analytics,
735
- options[:noop],
736
- config.modified_concurrency,
737
- config.future
738
- )
739
-
740
- executor.subscribe(outputter) if config.format == 'human'
741
- executor.subscribe(log_outputter)
742
- executor.publish_event(type: :plan_start, plan: nil)
743
-
744
- results = outputter.spin do
745
- pal.lookup(
746
- key,
747
- targets,
748
- inventory,
749
- executor,
750
- plan_vars: plan_vars
751
- )
752
- end
753
-
754
- executor.shutdown
755
- outputter.print_result_set(results)
756
-
757
- results.ok ? 0 : 1
758
- end
759
-
760
- def run_plan(plan_name, plan_arguments, nodes, options)
761
- unless nodes.empty?
762
- if plan_arguments['nodes'] || plan_arguments['targets']
763
- key = plan_arguments.include?('nodes') ? 'nodes' : 'targets'
764
- raise Bolt::CLIError,
765
- "A plan's '#{key}' parameter can be specified using the --#{key} option, but in that " \
766
- "case it must not be specified as a separate #{key}=<value> parameter nor included " \
767
- "in the JSON data passed in the --params option"
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
768
722
  end
769
-
770
- plan_params = pal.get_plan_info(plan_name)['parameters']
771
- target_param = plan_params.dig('targets', 'type') =~ /TargetSpec/
772
- node_param = plan_params.include?('nodes')
773
-
774
- if node_param && target_param
775
- msg = "Plan parameters include both 'nodes' and 'targets' with type 'TargetSpec', " \
776
- "neither will populated with the value for --nodes or --targets."
777
- Bolt::Logger.warn("nodes_targets_parameters", msg)
778
- elsif node_param
779
- plan_arguments['nodes'] = nodes.join(',')
780
- elsif target_param
781
- plan_arguments['targets'] = nodes.join(',')
723
+ content['Task'] = default_content.list_tasks.each_with_object([]) do |iter, col|
724
+ col << iter&.first
782
725
  end
783
726
  end
784
727
 
785
- plan_context = { plan_name: plan_name,
786
- params: plan_arguments }
787
-
788
- executor = Bolt::Executor.new(config.concurrency,
789
- analytics,
790
- options[:noop],
791
- config.modified_concurrency,
792
- config.future)
793
- if %w[human rainbow].include?(options.fetch(:format, 'human'))
794
- executor.subscribe(outputter)
795
- else
796
- # Only subscribe to out module events for JSON outputter
797
- executor.subscribe(outputter, %i[message verbose])
798
- end
799
-
800
- executor.subscribe(log_outputter)
801
- executor.start_plan(plan_context)
802
- result = pal.run_plan(plan_name, plan_arguments, executor, inventory, puppetdb_client)
803
-
804
- # If a non-bolt exception bubbles up the plan won't get finished
805
- executor.finish_plan(result)
806
- executor.shutdown
807
- rerun.update(result)
808
-
809
- outputter.print_plan_result(result)
810
- result.ok? ? 0 : 1
728
+ content
811
729
  end
812
730
 
813
- def apply_manifest(code, targets, filename = nil, noop = false)
814
- Puppet[:tasks] = false
815
- ast = pal.parse_manifest(code, filename)
816
-
817
- if defined?(ast.body) &&
818
- (ast.body.is_a?(Puppet::Pops::Model::HostClassDefinition) ||
819
- ast.body.is_a?(Puppet::Pops::Model::ResourceTypeDefinition))
820
- message = "Manifest only contains definitions and will result in no changes on the targets. "\
821
- "Definitions must be declared for their resources to be applied. You can read more "\
822
- "about defining and declaring classes and types in the Puppet documentation at "\
823
- "https://puppet.com/docs/puppet/latest/lang_classes.html and "\
824
- "https://puppet.com/docs/puppet/latest/lang_defined_types.html"
825
- Bolt::Logger.warn("empty_manifest", message)
826
- end
827
-
828
- executor = Bolt::Executor.new(config.concurrency,
829
- analytics,
830
- noop,
831
- config.modified_concurrency,
832
- config.future)
833
- executor.subscribe(outputter) if options.fetch(:format, 'human') == 'human'
834
- executor.subscribe(log_outputter)
835
- # apply logging looks like plan logging, so tell the outputter we're in a
836
- # plan even though we're not
837
- executor.publish_event(type: :plan_start, plan: nil)
838
-
839
- results = nil
840
- elapsed_time = Benchmark.realtime do
841
- apply_prep_results = pal.in_plan_compiler(executor, inventory, puppetdb_client) do |compiler|
842
- compiler.call_function('apply_prep', targets, '_catch_errors' => true)
843
- end
731
+ # Check and warn if Bolt is installed as a gem.
732
+ #
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
844
739
 
845
- apply_results = pal.with_bolt_executor(executor, inventory, puppetdb_client) do
846
- Puppet.lookup(:apply_executor)
847
- .apply_ast(ast, apply_prep_results.ok_set.targets, catch_errors: true, noop: noop)
848
- end
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
849
743
 
850
- results = Bolt::ResultSet.new(apply_prep_results.error_set.results + apply_results.results)
744
+ Bolt::Logger.warn("gem_install", msg)
851
745
  end
852
-
853
- executor.shutdown
854
- outputter.print_apply_result(results, elapsed_time)
855
- rerun.update(results)
856
-
857
- results.ok ? 0 : 1
858
- end
859
-
860
- def list_modules
861
- outputter.print_module_list(pal.list_modules)
862
- end
863
-
864
- def show_module(name)
865
- outputter.print_module_info(**pal.show_module(name))
866
- end
867
-
868
- def list_plugins
869
- outputter.print_plugin_list(plugins.list_plugins, pal.user_modulepath)
870
- end
871
-
872
- def generate_types
873
- # generate_types will surface a nice error with helpful message if it fails
874
- pal.generate_types(cache: true)
875
- 0
876
746
  end
877
747
 
878
- # Installs modules declared in the project configuration file.
748
+ # Print a fatal error. Print using the outputter if it's configured.
749
+ # Otherwise, mock the output by printing directly to stdout.
879
750
  #
880
- def install_project_modules(project, config, force, resolve)
881
- assert_project_file(project)
882
-
883
- if project.modules.empty? && resolve != false
884
- outputter.print_message(
885
- "Project configuration file #{project.project_file} does not "\
886
- "specify any module dependencies. Nothing to do."
887
- )
888
- return 0
889
- end
890
-
891
- installer = Bolt::ModuleInstaller.new(outputter, pal)
892
-
893
- ok = outputter.spin do
894
- installer.install(project.modules,
895
- project.puppetfile,
896
- project.managed_moduledir,
897
- config,
898
- force: force,
899
- resolve: resolve)
751
+ # @param error [StandardError] The error to print.
752
+ #
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)
900
760
  end
901
-
902
- ok ? 0 : 1
903
761
  end
904
762
 
905
- # Adds a single module to the project.
763
+ # Query whether Bolt is installed as a gem or package by checking if all
764
+ # built-in modules are installed.
906
765
  #
907
- def add_project_module(name, project, config)
908
- assert_project_file(project)
909
-
910
- installer = Bolt::ModuleInstaller.new(outputter, pal)
911
-
912
- ok = outputter.spin do
913
- installer.add(name,
914
- project.modules,
915
- project.puppetfile,
916
- project.managed_moduledir,
917
- project.project_file,
918
- config)
919
- end
920
-
921
- ok ? 0 : 1
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
- # Asserts that there is a project configuration file.
771
+ # Parse parameters for tasks and plans.
925
772
  #
926
- def assert_project_file(project)
927
- unless project.project_file?
928
- command = Bolt::Util.powershell? ? 'New-BoltProject' : 'bolt project init'
929
-
930
- msg = "Could not find project configuration file #{project.project_file}, unable "\
931
- "to install modules. To create a Bolt project, run '#{command}'."
932
-
933
- raise Bolt::Error.new(msg, 'bolt/missing-project-config-error')
773
+ # @param options [Hash] Options from the calling method.
774
+ #
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
+ {}
934
780
  end
935
781
  end
936
782
 
937
- # Loads a Puppetfile and installs its modules.
783
+ # Parse variables for lookups.
938
784
  #
939
- def install_puppetfile(puppetfile_config, puppetfile, moduledir)
940
- outputter.print_message("Installing modules from Puppetfile")
941
- installer = Bolt::ModuleInstaller.new(outputter, pal)
942
- ok = outputter.spin do
943
- installer.install_puppetfile(puppetfile, moduledir, puppetfile_config)
944
- end
945
-
946
- ok ? 0 : 1
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) }]
947
790
  end
948
791
 
949
- def pal
950
- @pal ||= Bolt::PAL.new(Bolt::Config::Modulepath.new(config.modulepath),
951
- config.hiera_config,
952
- config.project.resource_types,
953
- config.compile_concurrency,
954
- config.trusted_external,
955
- config.apply_settings,
956
- config.project)
957
- end
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]}"
958
803
 
959
- # Collects the list of Bolt guides and maps them to their topics.
960
- def guides
961
- @guides ||= begin
962
- root_path = File.expand_path(File.join(__dir__, '..', '..', 'guides'))
963
- files = Dir.children(root_path).sort
964
-
965
- files.each_with_object({}) do |file, guides|
966
- next if file !~ /\.(yaml|yml)\z/
967
- # The ".*" here removes any suffix
968
- topic = File.basename(file, ".*")
969
- guides[topic] = File.join(root_path, file)
970
- end
971
- rescue SystemCallError => e
972
- raise Bolt::FileError.new("#{e.message}: unable to load guides directory", root_path)
804
+ if options[:action] == 'show' && options[:object]
805
+ screen += '_object'
973
806
  end
974
- end
975
807
 
976
- # Display the list of available Bolt guides.
977
- def list_topics
978
- outputter.print_topics(guides.keys)
979
- 0
980
- end
981
-
982
- # Display a specific Bolt guide.
983
- def show_guide(topic)
984
- if guides[topic]
985
- analytics.event('Guide', 'known_topic', label: topic)
986
-
987
- begin
988
- guide = Bolt::Util.read_yaml_hash(guides[topic], 'guide')
989
- rescue SystemCallError => e
990
- raise Bolt::FileError("#{e.message}: unable to load guide page", filepath)
991
- end
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
992
817
 
993
- # Make sure both topic and guide keys are defined
994
- unless (%w[topic guide] - guide.keys).empty?
995
- msg = "Guide file #{guides[topic]} must have a 'topic' key and 'guide' key, but has #{guide.keys} keys."
996
- raise Bolt::Error.new(msg, 'bolt/invalid-guide')
997
- end
998
-
999
- outputter.print_guide(**Bolt::Util.symbolize_top_level_keys(guide))
1000
- else
1001
- analytics.event('Guide', 'unknown_topic', label: topic)
1002
- outputter.print_message("Did not find guide for topic '#{topic}'.\n\n")
1003
- list_topics
1004
- end
1005
- 0
1006
- 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
+ }
1007
824
 
1008
- def validate_file(type, path, allow_dir = false)
1009
- if path.nil?
1010
- 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
+ )
1011
832
  end
1012
833
 
1013
- Bolt::Util.validate_file(type, path, allow_dir)
834
+ analytics.screen_view(screen, **screen_view_fields)
1014
835
  end
1015
836
 
1016
- # Returns the path to a file. If the path is an absolute or relative to
1017
- # a file, and the file exists, returns the path as-is. Otherwise, checks if
1018
- # the path is a Puppet file path and looks for the file in a module's files
1019
- # directory.
837
+ # Issue a deprecation warning if the user is running an unsupported version
838
+ # of PowerShell on the controller.
1020
839
  #
1021
- def find_file(path, future_file_paths)
1022
- return path if File.exist?(path) || Pathname.new(path).absolute?
1023
- modulepath = Bolt::Config::Modulepath.new(config.modulepath)
1024
- modules = Bolt::Module.discover(modulepath.full_modulepath, config.project)
1025
- mod, file = path.split(File::SEPARATOR, 2)
1026
-
1027
- if modules[mod]
1028
- @logger.debug("Did not find file at #{File.expand_path(path)}, checking in module '#{mod}'")
1029
- found = Bolt::Util.find_file_in_module(modules[mod].path, file || "", future_file_paths)
1030
- path = found.nil? ? File.join(modules[mod].path, 'files', file) : found
1031
- end
1032
- path
1033
- 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)
1034
845
 
1035
- def rerun
1036
- @rerun ||= Bolt::Rerun.new(config.rerunfile, config.save_rerun)
1037
- end
846
+ return unless !stdout.empty? && stdout.to_i < 3
1038
847
 
1039
- def outputter
1040
- @outputter ||= Bolt::Outputter.for_format(config.format,
1041
- config.color,
1042
- options[:verbose],
1043
- config.trace,
1044
- config.spinner)
848
+ msg = "Detected PowerShell 2 on controller. PowerShell 2 is unsupported."
849
+ Bolt::Logger.deprecation_warning("powershell_2_controller", msg)
850
+ end
1045
851
  end
1046
852
 
1047
- def log_outputter
1048
- @log_outputter ||= Bolt::Outputter::Logger.new(options[:verbose], config.trace)
1049
- 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
1050
866
 
1051
- def analytics
1052
- @analytics ||= begin
1053
- client = Bolt::Analytics.build_client(config.analytics)
1054
- client.bundled_content = bundled_content
1055
- client
867
+ inventory_cli_opts = %i[authentication escalation transports].each_with_object([]) do |key, acc|
868
+ acc.concat(Bolt::BoltOptionParser::OPTIONS[key])
1056
869
  end
1057
- end
1058
870
 
1059
- def bundled_content
1060
- # If the bundled content directory is empty, Bolt is likely installed as a gem.
1061
- if ENV['BOLT_GEM'].nil? && incomplete_install?
1062
- msg = <<~MSG.chomp
1063
- Bolt might be installed as a gem. To use Bolt reliably and with all of its
1064
- dependencies, uninstall the 'bolt' gem and install Bolt as a package:
1065
- https://puppet.com/docs/bolt/latest/bolt_installing.html
1066
-
1067
- If you meant to install Bolt as a gem and want to disable this warning,
1068
- set the BOLT_GEM environment variable.
1069
- MSG
871
+ inventory_cli_opts.concat(%w[no-host-key-check no-ssl no-ssl-verify no-tty])
1070
872
 
1071
- Bolt::Logger.warn("gem_install", msg)
1072
- end
873
+ conflicting_options = Set.new(opts.keys.map(&:to_s)).intersection(inventory_cli_opts)
1073
874
 
1074
- # We only need to enumerate bundled content when running a task or plan
1075
- content = { 'Plan' => [],
1076
- 'Task' => [],
1077
- 'Plugin' => Bolt::Plugin::BUILTIN_PLUGINS }
1078
- if %w[plan task].include?(options[:subcommand]) && options[:action] == 'run'
1079
- default_content = Bolt::PAL.new(Bolt::Config::Modulepath.new([]), nil, nil)
1080
- content['Plan'] = default_content.list_plans.each_with_object([]) do |iter, col|
1081
- col << iter&.first
1082
- end
1083
- content['Task'] = default_content.list_tasks.each_with_object([]) do |iter, col|
1084
- col << iter&.first
1085
- 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
+ )
1086
880
  end
1087
-
1088
- content
1089
881
  end
1090
882
 
1091
- # Gem installs include the aggregate, canary, and puppetdb_fact modules, while
1092
- # package installs include modules listed in the Bolt repo Puppetfile
1093
- def incomplete_install?
1094
- builtin_module_list = %w[aggregate canary puppetdb_fact secure_env_vars puppet_connect]
1095
- (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
1096
890
  end
1097
891
 
1098
- # Mimicks the output from Outputter::Human#fatal_error. This should be used to print
1099
- # errors prior to config being loaded, as the outputter relies on config being loaded.
1100
- def fatal_error(error)
1101
- if $stdout.isatty
1102
- $stdout.puts("\033[31m#{error.message}\033[0m")
1103
- else
1104
- $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!
1105
901
  end
902
+
903
+ yield
904
+ ensure
905
+ Signal.trap :INT, handler if handler
1106
906
  end
1107
907
  end
1108
908
  end