bolt 3.14.1 → 3.17.0

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

Potentially problematic release.


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

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