bolt 3.15.0 → 3.18.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 (111) hide show
  1. checksums.yaml +4 -4
  2. data/Puppetfile +7 -7
  3. data/bolt-modules/boltlib/lib/puppet/functions/run_script.rb +1 -4
  4. data/bolt-modules/boltlib/lib/puppet/functions/upload_file.rb +1 -4
  5. data/bolt-modules/file/lib/puppet/functions/file/exists.rb +1 -4
  6. data/bolt-modules/file/lib/puppet/functions/file/read.rb +1 -4
  7. data/bolt-modules/file/lib/puppet/functions/file/readable.rb +1 -4
  8. data/lib/bolt/analytics.rb +4 -21
  9. data/lib/bolt/application.rb +627 -0
  10. data/lib/bolt/applicator.rb +7 -6
  11. data/lib/bolt/apply_inventory.rb +3 -3
  12. data/lib/bolt/apply_result.rb +2 -2
  13. data/lib/bolt/bolt_option_parser.rb +27 -3
  14. data/lib/bolt/catalog.rb +10 -11
  15. data/lib/bolt/cli.rb +607 -809
  16. data/lib/bolt/config/modulepath.rb +1 -1
  17. data/lib/bolt/config/options.rb +32 -13
  18. data/lib/bolt/config/transport/base.rb +4 -4
  19. data/lib/bolt/config/transport/docker.rb +2 -2
  20. data/lib/bolt/config/transport/local.rb +2 -2
  21. data/lib/bolt/config/transport/lxd.rb +2 -2
  22. data/lib/bolt/config/transport/orch.rb +2 -2
  23. data/lib/bolt/config/transport/podman.rb +2 -2
  24. data/lib/bolt/config/transport/remote.rb +2 -2
  25. data/lib/bolt/config/transport/ssh.rb +2 -2
  26. data/lib/bolt/config/transport/winrm.rb +2 -2
  27. data/lib/bolt/config.rb +5 -5
  28. data/lib/bolt/container_result.rb +2 -2
  29. data/lib/bolt/error.rb +1 -1
  30. data/lib/bolt/executor.rb +17 -14
  31. data/lib/bolt/fiber_executor.rb +9 -5
  32. data/lib/bolt/inventory/group.rb +4 -4
  33. data/lib/bolt/inventory/inventory.rb +64 -41
  34. data/lib/bolt/inventory/options.rb +1 -1
  35. data/lib/bolt/inventory/target.rb +1 -1
  36. data/lib/bolt/inventory.rb +10 -17
  37. data/lib/bolt/module_installer/installer.rb +2 -2
  38. data/lib/bolt/module_installer/puppetfile/forge_module.rb +1 -1
  39. data/lib/bolt/module_installer/puppetfile/git_module.rb +1 -1
  40. data/lib/bolt/module_installer/puppetfile/module.rb +1 -1
  41. data/lib/bolt/module_installer/puppetfile.rb +27 -13
  42. data/lib/bolt/module_installer/resolver.rb +3 -3
  43. data/lib/bolt/module_installer/specs/forge_spec.rb +1 -1
  44. data/lib/bolt/module_installer/specs/git_spec.rb +1 -1
  45. data/lib/bolt/module_installer/specs.rb +3 -3
  46. data/lib/bolt/module_installer.rb +6 -6
  47. data/lib/bolt/node/errors.rb +1 -1
  48. data/lib/bolt/node/output.rb +1 -1
  49. data/lib/bolt/outputter/human.rb +85 -34
  50. data/lib/bolt/outputter/json.rb +63 -38
  51. data/lib/bolt/outputter/logger.rb +1 -1
  52. data/lib/bolt/outputter/rainbow.rb +1 -1
  53. data/lib/bolt/outputter.rb +4 -4
  54. data/lib/bolt/pal/logging.rb +1 -1
  55. data/lib/bolt/pal/yaml_plan/evaluator.rb +1 -1
  56. data/lib/bolt/pal/yaml_plan/loader.rb +2 -2
  57. data/lib/bolt/pal/yaml_plan/step.rb +11 -11
  58. data/lib/bolt/pal/yaml_plan/transpiler.rb +3 -3
  59. data/lib/bolt/pal/yaml_plan.rb +2 -2
  60. data/lib/bolt/pal.rb +11 -11
  61. data/lib/bolt/plan_creator.rb +88 -29
  62. data/lib/bolt/plan_future.rb +11 -6
  63. data/lib/bolt/plan_result.rb +3 -3
  64. data/lib/bolt/plugin/cache.rb +2 -2
  65. data/lib/bolt/plugin/module.rb +1 -1
  66. data/lib/bolt/plugin.rb +18 -24
  67. data/lib/bolt/project.rb +4 -11
  68. data/lib/bolt/project_manager/config_migrator.rb +1 -1
  69. data/lib/bolt/project_manager/inventory_migrator.rb +1 -1
  70. data/lib/bolt/project_manager/migrator.rb +1 -1
  71. data/lib/bolt/project_manager/module_migrator.rb +5 -5
  72. data/lib/bolt/project_manager.rb +23 -4
  73. data/lib/bolt/puppetdb/config.rb +1 -1
  74. data/lib/bolt/puppetdb.rb +3 -3
  75. data/lib/bolt/result.rb +1 -1
  76. data/lib/bolt/result_set.rb +2 -1
  77. data/lib/bolt/shell/bash.rb +12 -2
  78. data/lib/bolt/shell/powershell.rb +9 -2
  79. data/lib/bolt/shell.rb +2 -2
  80. data/lib/bolt/target.rb +2 -2
  81. data/lib/bolt/transport/base.rb +1 -1
  82. data/lib/bolt/transport/docker/connection.rb +1 -1
  83. data/lib/bolt/transport/docker.rb +2 -2
  84. data/lib/bolt/transport/local/connection.rb +19 -3
  85. data/lib/bolt/transport/local.rb +3 -3
  86. data/lib/bolt/transport/lxd/connection.rb +1 -1
  87. data/lib/bolt/transport/lxd.rb +4 -4
  88. data/lib/bolt/transport/orch/connection.rb +13 -1
  89. data/lib/bolt/transport/orch.rb +2 -2
  90. data/lib/bolt/transport/podman/connection.rb +1 -1
  91. data/lib/bolt/transport/podman.rb +2 -2
  92. data/lib/bolt/transport/remote.rb +2 -2
  93. data/lib/bolt/transport/simple.rb +3 -3
  94. data/lib/bolt/transport/ssh/connection.rb +3 -3
  95. data/lib/bolt/transport/ssh.rb +5 -5
  96. data/lib/bolt/transport/winrm/connection.rb +2 -2
  97. data/lib/bolt/transport/winrm.rb +3 -3
  98. data/lib/bolt/util.rb +8 -31
  99. data/lib/bolt/validator.rb +1 -1
  100. data/lib/bolt/version.rb +1 -1
  101. data/lib/bolt.rb +1 -1
  102. data/lib/bolt_server/file_cache.rb +12 -0
  103. data/lib/bolt_server/schemas/action-apply.json +32 -0
  104. data/lib/bolt_server/schemas/action-apply_prep.json +19 -0
  105. data/lib/bolt_server/transport_app.rb +129 -26
  106. data/lib/bolt_spec/bolt_context.rb +7 -5
  107. data/lib/bolt_spec/plans/mock_executor.rb +3 -3
  108. data/lib/bolt_spec/run.rb +1 -1
  109. data/libexec/bolt_catalog +1 -1
  110. metadata +7 -5
  111. data/lib/bolt/secret.rb +0 -37
data/lib/bolt/cli.rb CHANGED
@@ -9,29 +9,30 @@ require 'json'
9
9
  require 'io/console'
10
10
  require 'logging'
11
11
  require 'optparse'
12
- require 'bolt/analytics'
13
- require 'bolt/bolt_option_parser'
14
- require 'bolt/config'
15
- require 'bolt/error'
16
- require 'bolt/executor'
17
- require 'bolt/inventory'
18
- require 'bolt/logger'
19
- require 'bolt/module_installer'
20
- require 'bolt/outputter'
21
- require 'bolt/pal'
22
- require 'bolt/plan_creator'
23
- require 'bolt/plugin'
24
- require 'bolt/project_manager'
25
- require 'bolt/puppetdb'
26
- require 'bolt/rerun'
27
- require 'bolt/secret'
28
- require 'bolt/target'
29
- require 'bolt/version'
12
+ require_relative '../bolt/analytics'
13
+ require_relative '../bolt/application'
14
+ require_relative '../bolt/bolt_option_parser'
15
+ require_relative '../bolt/config'
16
+ require_relative '../bolt/error'
17
+ require_relative '../bolt/executor'
18
+ require_relative '../bolt/inventory'
19
+ require_relative '../bolt/logger'
20
+ require_relative '../bolt/module_installer'
21
+ require_relative '../bolt/outputter'
22
+ require_relative '../bolt/pal'
23
+ require_relative '../bolt/plugin'
24
+ require_relative '../bolt/project_manager'
25
+ require_relative '../bolt/puppetdb'
26
+ require_relative '../bolt/rerun'
27
+ require_relative '../bolt/target'
28
+ require_relative '../bolt/version'
30
29
 
31
30
  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,23 @@ module Bolt
51
52
 
52
53
  TARGETING_OPTIONS = %i[query rerun targets].freeze
53
54
 
54
- attr_reader :config, :options
55
+ SUCCESS = 0
56
+ FAILURE = 1
55
57
 
56
58
  def initialize(argv)
57
59
  Bolt::Logger.initialize_logging
58
60
  @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)
61
+ @argv = argv
66
62
  end
67
- private :inventory
68
63
 
69
- def help?(remaining)
64
+ # TODO: Move this to the parser.
65
+ #
66
+ # Query whether the help text needs to be displayed.
67
+ #
68
+ # @param remaining [Array] Remaining arguments after parsing the command.
69
+ # @param options [Hash] The CLI options.
70
+ #
71
+ private def help?(options, remaining)
70
72
  # Set the subcommand
71
73
  options[:subcommand] = remaining.shift
72
74
 
@@ -84,24 +86,90 @@ module Bolt
84
86
 
85
87
  options[:help]
86
88
  end
87
- private :help?
88
89
 
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.
90
+ # TODO: Move most of this to the parser.
91
+ #
92
+ # Parse the command and validate options. All errors that are raised here
93
+ # are not handled by the outputter, as it relies on config being loaded.
92
94
  #
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
95
  def parse
98
- parse_command
99
- load_config
100
- finalize_setup
96
+ with_error_handling do
97
+ options = {}
98
+ parser = BoltOptionParser.new(options)
99
+
100
+ # This part aims to handle both `bolt <mode> --help` and `bolt help <mode>`.
101
+ remaining = parser.permute(@argv) unless @argv.empty?
102
+
103
+ if @argv.empty? || help?(options, remaining)
104
+ # If the subcommand is not enabled, display the default
105
+ # help text
106
+ options[:subcommand] = nil unless COMMANDS.include?(options[:subcommand])
107
+
108
+ if Bolt::Util.first_run?
109
+ FileUtils.touch(Bolt::Util.first_runs_free)
110
+
111
+ if options[:subcommand].nil? && $stdout.isatty
112
+ welcome_message
113
+ raise Bolt::CLIExit
114
+ end
115
+ end
116
+
117
+ # Update the parser for the subcommand (or lack thereof)
118
+ parser.update
119
+ puts parser.help
120
+ raise Bolt::CLIExit
121
+ end
122
+
123
+ if options[:version]
124
+ puts Bolt::VERSION
125
+ raise Bolt::CLIExit
126
+ end
127
+
128
+ options[:object] = remaining.shift
129
+
130
+ # Handle reading a command from a file
131
+ if options[:subcommand] == 'command' && options[:object]
132
+ options[:object] = Bolt::Util.get_arg_input(options[:object])
133
+ end
134
+
135
+ # Only parse params for task or plan
136
+ if %w[task plan].include?(options[:subcommand])
137
+ params, remaining = remaining.partition { |s| s =~ /.+=/ }
138
+ if options[:params]
139
+ unless params.empty?
140
+ raise Bolt::CLIError,
141
+ "Parameters must be specified through either the --params " \
142
+ "option or param=value pairs, not both"
143
+ end
144
+ options[:params_parsed] = true
145
+ elsif params.any?
146
+ options[:params_parsed] = false
147
+ options[:params] = Hash[params.map { |a| a.split('=', 2) }]
148
+ else
149
+ options[:params_parsed] = true
150
+ options[:params] = {}
151
+ end
152
+ end
153
+ options[:leftovers] = remaining
154
+
155
+ # Default to verbose for everything except plans
156
+ unless options.key?(:verbose)
157
+ options[:verbose] = options[:subcommand] != 'plan'
158
+ end
159
+
160
+ validate(options)
161
+ validate_ps_version
162
+
163
+ options
164
+ end
101
165
  end
102
166
 
103
- # Prints a welcome message when users first install Bolt and run `bolt`, `bolt help` or `bolt --help`
104
- def welcome_message
167
+ # TODO: Move this to the parser.
168
+ #
169
+ # Print a welcome message when users first install Bolt and run `bolt`,
170
+ # `bolt help` or `bolt --help`.
171
+ #
172
+ private def welcome_message
105
173
  bolt = <<~BOLT
106
174
  `.::-`
107
175
  `.-:///////-.`
@@ -142,151 +210,15 @@ module Bolt
142
210
  $stdout.print message
143
211
  end
144
212
 
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)
213
+ # TODO: Move this to the parser.
214
+ #
215
+ # Validate the command. Ensure that the subcommand and action are
216
+ # recognized, all required arguments are specified, and only supported
217
+ # command-line options are used.
218
+ #
219
+ # @param options [Hash] The CLI options.
220
+ #
221
+ private def validate(options)
290
222
  unless COMMANDS.include?(options[:subcommand])
291
223
  command = Bolt::Util.powershell? ? 'Get-Command -Module PuppetBolt' : 'bolt help'
292
224
  raise Bolt::CLIError,
@@ -351,12 +283,34 @@ module Bolt
351
283
  raise Bolt::CLIError, "Must specify a module name."
352
284
  end
353
285
 
286
+ if options[:action] == 'convert' && !options[:object]
287
+ raise Bolt::CLIError, "Must specify a plan."
288
+ end
289
+
354
290
  if options[:subcommand] == 'module' && options[:action] == 'install' && options[:object]
355
291
  command = Bolt::Util.powershell? ? 'Add-BoltModule -Module' : 'bolt module add'
356
292
  raise Bolt::CLIError, "Invalid argument '#{options[:object]}'. To add a new module to "\
357
293
  "the project, run '#{command} #{options[:object]}'."
358
294
  end
359
295
 
296
+ if %w[download upload].include?(options[:action])
297
+ raise Bolt::CLIError, "Must specify a source" unless options[:object]
298
+
299
+ if options[:leftovers].empty?
300
+ raise Bolt::CLIError, "Must specify a destination"
301
+ elsif options[:leftovers].size > 1
302
+ raise Bolt::CLIError, "Unknown arguments #{options[:leftovers].drop(1).join(', ')}"
303
+ end
304
+ end
305
+
306
+ if options[:subcommand] == 'group' && options[:object]
307
+ raise Bolt::CLIError, "Unknown argument #{options[:object]}"
308
+ end
309
+
310
+ if options[:action] == 'generate-types' && options[:object]
311
+ raise Bolt::CLIError, "Unknown argument #{options[:object]}"
312
+ end
313
+
360
314
  if !%w[file script lookup].include?(options[:subcommand]) &&
361
315
  !options[:leftovers].empty?
362
316
  raise Bolt::CLIError,
@@ -381,728 +335,572 @@ module Bolt
381
335
  "Option '--env-var' can only be specified when running a command or script"
382
336
  end
383
337
  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
338
 
400
- def plugins
401
- @plugins ||= Bolt::Plugin.setup(config, pal, analytics)
339
+ validate_targeting_options(options)
402
340
  end
403
341
 
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
342
+ # Validates that only one targeting option is provided and that commands
343
+ # requiring a targeting option received one.
344
+ #
345
+ # @param options [Hash] The CLI options.
346
+ #
347
+ private def validate_targeting_options(options)
348
+ target_opts = options.slice(*TARGETING_OPTIONS)
349
+ target_string = "'--targets', '--rerun', or '--query'"
416
350
 
417
- inventory_cli_opts = %i[authentication escalation transports].each_with_object([]) do |key, acc|
418
- acc.concat(Bolt::BoltOptionParser::OPTIONS[key])
351
+ if target_opts.length > 1
352
+ raise Bolt::CLIError, "Only one targeting option can be specified: #{target_string}"
419
353
  end
420
354
 
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)
355
+ return if %w[guide module plan project secret].include?(options[:subcommand]) ||
356
+ %w[convert new show].include?(options[:action]) ||
357
+ options[:plan_hierarchy]
424
358
 
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
- )
359
+ if target_opts.empty?
360
+ raise Bolt::CLIError, "Command requires a targeting option: #{target_string}"
430
361
  end
431
362
  end
432
363
 
364
+ # Execute a Bolt command. The +options+ hash includes the subcommand and
365
+ # action to be run, as well as any additional arguments and options for the
366
+ # command.
367
+ #
368
+ # @param options [Hash] The CLI options.
369
+ #
433
370
  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
371
+ with_signal_handling do
372
+ with_error_handling do
373
+ # TODO: Separate from options hash and pass as own args.
374
+ command = options[:subcommand]
375
+ action = options[:action]
376
+
377
+ #
378
+ # INITIALIZE CORE CLASSES
379
+ #
380
+
381
+ project = if ENV['BOLT_PROJECT']
382
+ Bolt::Project.create_project(ENV['BOLT_PROJECT'], 'environment')
383
+ elsif options[:project]
384
+ dir = Pathname.new(options[:project])
385
+ if (dir + Bolt::Project::BOLTDIR_NAME).directory?
386
+ Bolt::Project.create_project(dir + Bolt::Project::BOLTDIR_NAME)
387
+ else
388
+ Bolt::Project.create_project(dir)
389
+ end
390
+ else
391
+ Bolt::Project.find_boltdir(Dir.pwd)
392
+ end
457
393
 
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))
394
+ config = Bolt::Config.from_project(project, options)
463
395
 
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
396
+ @outputter = Bolt::Outputter.for_format(
397
+ config.format,
398
+ config.color,
399
+ options[:verbose],
400
+ config.trace,
401
+ config.spinner
402
+ )
472
403
 
473
- analytics.screen_view(screen, **screen_view_fields)
404
+ @rerun = Bolt::Rerun.new(config.rerunfile, config.save_rerun)
474
405
 
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
406
+ # TODO: Subscribe this to the executor.
407
+ analytics = begin
408
+ client = Bolt::Analytics.build_client(config.analytics)
409
+ client.bundled_content = bundled_content(options)
410
+ client
483
411
  end
484
- when 'plan'
485
- if options[:object]
486
- show_plan(options[:object])
487
- else
488
- list_plans
412
+
413
+ Bolt::Logger.configure(config.log, config.color, config.disable_warnings)
414
+ Bolt::Logger.stream = config.stream
415
+ Bolt::Logger.analytics = analytics
416
+ Bolt::Logger.flush_queue
417
+
418
+ executor = Bolt::Executor.new(
419
+ config.concurrency,
420
+ analytics,
421
+ options[:noop],
422
+ config.modified_concurrency,
423
+ config.future
424
+ )
425
+
426
+ pal = Bolt::PAL.new(
427
+ Bolt::Config::Modulepath.new(config.modulepath),
428
+ config.hiera_config,
429
+ config.project.resource_types,
430
+ config.compile_concurrency,
431
+ config.trusted_external,
432
+ config.apply_settings,
433
+ config.project
434
+ )
435
+
436
+ plugins = Bolt::Plugin.new(config, pal, analytics)
437
+
438
+ inventory = Bolt::Inventory.from_config(config, plugins)
439
+
440
+ log_outputter = Bolt::Outputter::Logger.new(options[:verbose], config.trace)
441
+
442
+ #
443
+ # FINALIZING SETUP
444
+ #
445
+
446
+ check_gem_install
447
+ warn_inventory_overrides_cli(config, options)
448
+ submit_screen_view(analytics, config, inventory, options)
449
+ options[:targets] = process_target_list(plugins.puppetdb_client, @rerun, options)
450
+
451
+ # TODO: Fix casing issue in Windows.
452
+ config.check_path_case('modulepath', config.modulepath)
453
+
454
+ if options[:clear_cache]
455
+ FileUtils.rm(config.project.plugin_cache_file) if File.exist?(config.project.plugin_cache_file)
456
+ FileUtils.rm(config.project.task_cache_file) if File.exist?(config.project.task_cache_file)
457
+ FileUtils.rm(config.project.plan_cache_file) if File.exist?(config.project.plan_cache_file)
489
458
  end
490
- when 'inventory'
491
- if options[:detail]
492
- show_targets
459
+
460
+ case command
461
+ when 'apply', 'lookup'
462
+ if %w[human rainbow].include?(config.format)
463
+ executor.subscribe(outputter)
464
+ end
465
+ when 'plan'
466
+ if %w[human rainbow].include?(config.format)
467
+ executor.subscribe(outputter)
468
+ else
469
+ executor.subscribe(outputter, %i[message verbose])
470
+ end
493
471
  else
494
- list_targets
472
+ executor.subscribe(outputter)
495
473
  end
496
- when 'group'
497
- list_groups
498
- when 'module'
499
- if options[:object]
500
- show_module(options[:object])
501
- else
502
- list_modules
474
+
475
+ executor.subscribe(log_outputter)
476
+
477
+ # TODO: Figure out where this should really go. It doesn't seem to
478
+ # make sense in the application, since the params should already
479
+ # be data when they reach that point.
480
+ if %w[plan task].include?(command) && action == 'run'
481
+ options[:params] = parse_params(
482
+ command,
483
+ options[:object],
484
+ pal,
485
+ **options.slice(:params, :params_parsed)
486
+ )
503
487
  end
504
- when 'plugin'
505
- list_plugins
488
+
489
+ application = Bolt::Application.new(
490
+ analytics: analytics,
491
+ config: config,
492
+ executor: executor,
493
+ inventory: inventory,
494
+ pal: pal,
495
+ plugins: plugins
496
+ )
497
+
498
+ process_command(application, command, action, options)
499
+ ensure
500
+ analytics&.finish
506
501
  end
507
- return 0
508
- when 'convert'
509
- pal.convert_plan(options[:object])
510
- return 0
511
502
  end
503
+ end
512
504
 
513
- message = 'There might be processes left executing on some nodes.'
505
+ # Process the command.
506
+ #
507
+ # @param app [Bolt::Application] The application.
508
+ # @param command [String] The command.
509
+ # @param action [String, NilClass] The action.
510
+ # @param options [Hash] The CLI options.
511
+ #
512
+ private def process_command(app, command, action, options)
513
+ case command
514
+ when 'apply'
515
+ results = outputter.spin do
516
+ app.apply(options[:object], options[:targets], **options.slice(:code, :noop))
517
+ end
518
+ rerun.update(results)
519
+ app.shutdown
520
+ outputter.print_apply_result(results)
521
+ results.ok? ? SUCCESS : FAILURE
514
522
 
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
523
+ when 'command'
524
+ outputter.print_head
525
+ results = app.run_command(options[:object], options[:targets], **options.slice(:env_vars))
526
+ rerun.update(results)
527
+ app.shutdown
528
+ outputter.print_summary(results, results.elapsed_time)
529
+ results.ok? ? SUCCESS : FAILURE
530
+
531
+ when 'file'
532
+ case action
533
+ when 'download'
534
+ outputter.print_head
535
+ results = app.download_file(options[:object], options[:leftovers].first, options[:targets])
536
+ rerun.update(results)
537
+ app.shutdown
538
+ outputter.print_summary(results, results.elapsed_time)
539
+ results.ok? ? SUCCESS : FAILURE
540
+ when 'upload'
541
+ outputter.print_head
542
+ results = app.upload_file(options[:object], options[:leftovers].first, options[:targets])
543
+ rerun.update(results)
544
+ app.shutdown
545
+ outputter.print_summary(results, results.elapsed_time)
546
+ results.ok? ? SUCCESS : FAILURE
547
+ end
548
+
549
+ when 'group'
550
+ outputter.print_groups(**app.list_groups)
551
+ SUCCESS
518
552
 
519
- case options[:subcommand]
520
553
  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
554
+ if options[:object]
555
+ outputter.print_guide(**app.show_guide(options[:object]))
556
+ else
557
+ outputter.print_topics(**app.list_guides)
533
558
  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)
559
+ SUCCESS
560
+
561
+ when 'inventory'
562
+ targets = app.show_inventory(options[:targets])
563
+ .merge(flag: !options[:targets].nil?)
564
+ if options[:detail]
565
+ outputter.print_target_info(**targets)
566
+ else
567
+ outputter.print_targets(**targets)
541
568
  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)
569
+ SUCCESS
570
+
571
+ when 'lookup'
572
+ options[:vars] = parse_vars(options[:leftovers])
573
+ if options[:plan_hierarchy]
574
+ outputter.print_plan_lookup(app.plan_lookup(options[:object], **options.slice(:vars)))
575
+ SUCCESS
576
+ else
577
+ results = outputter.spin do
578
+ app.lookup(options[:object], options[:targets], **options.slice(:vars))
579
+ end
580
+ rerun.update(results)
581
+ app.shutdown
582
+ outputter.print_result_set(results)
583
+ results.ok? ? SUCCESS : FAILURE
555
584
  end
585
+
556
586
  when 'module'
557
- case options[:action]
587
+ case action
558
588
  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])
589
+ ok = outputter.spin { app.add_module(options[:object], outputter) }
590
+ ok ? SUCCESS : FAILURE
562
591
  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]))
592
+ app.generate_types
593
+ SUCCESS
594
+ when 'install'
595
+ ok = outputter.spin { app.install_modules(outputter, **options.slice(:force, :resolve)) }
596
+ ok ? SUCCESS : FAILURE
597
+ when 'show'
598
+ if options[:object]
599
+ outputter.print_module_info(**app.show_module(options[:object]))
600
+ else
601
+ outputter.print_module_list(app.list_modules)
602
+ end
603
+ SUCCESS
571
604
  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
605
 
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
606
+ when 'plan'
607
+ case action
608
+ when 'convert'
609
+ app.convert_plan(options[:object])
610
+ SUCCESS
611
+ when 'new'
612
+ result = app.new_plan(options[:object], **options.slice(:puppet, :plan_script))
613
+ outputter.print_new_plan(**result)
614
+ SUCCESS
615
+ when 'run'
616
+ result = app.run_plan(options[:object], options[:targets], **options.slice(:params))
617
+ rerun.update(result)
618
+ app.shutdown
619
+ outputter.print_plan_result(result)
620
+ result.ok? ? SUCCESS : FAILURE
621
+ when 'show'
622
+ if options[:object]
623
+ outputter.print_plan_info(app.show_plan(options[:object]))
624
+ else
625
+ outputter.print_plans(**app.list_plans(**options.slice(:filter)))
626
+ end
627
+ SUCCESS
625
628
  end
626
629
 
627
- executor.shutdown
628
- rerun.update(results)
630
+ when 'plugin'
631
+ outputter.print_plugin_list(**app.list_plugins)
632
+ SUCCESS
629
633
 
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
634
+ when 'project'
635
+ case action
636
+ when 'init'
637
+ app.create_project(options[:object], outputter, **options.slice(:modules))
638
+ SUCCESS
639
+ when 'migrate'
640
+ app.migrate_project(outputter)
641
+ SUCCESS
642
+ end
667
643
 
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
644
+ when 'script'
645
+ outputter.print_head
646
+ opts = options.slice(:env_vars).merge(arguments: options[:leftovers])
647
+ results = app.run_script(options[:object], options[:targets], **opts)
648
+ rerun.update(results)
649
+ app.shutdown
650
+ outputter.print_summary(results, results.elapsed_time)
651
+ results.ok? ? SUCCESS : FAILURE
674
652
 
675
- outputter.print_targets(
676
- group_targets_by_source,
677
- inventory.source,
678
- config.default_inventoryfile,
679
- target_flag
680
- )
681
- end
653
+ when 'secret'
654
+ case action
655
+ when 'createkeys'
656
+ result = app.create_secret_keys(**options.slice(:force, :plugin))
657
+ outputter.print_message(result)
658
+ SUCCESS
659
+ when 'decrypt'
660
+ result = app.decrypt_secret(options[:object], **options.slice(:plugin))
661
+ outputter.print_message(result)
662
+ SUCCESS
663
+ when 'encrypt'
664
+ result = app.encrypt_secret(options[:object], **options.slice(:plugin))
665
+ outputter.print_message(result)
666
+ SUCCESS
667
+ end
682
668
 
683
- def show_targets
684
- if options.keys.any? { |key| TARGETING_OPTIONS.include?(key) }
685
- target_flag = true
686
- else
687
- options[:targets] = 'all'
669
+ when 'task'
670
+ case action
671
+ when 'run'
672
+ outputter.print_head
673
+ results = app.run_task(options[:object], options[:targets], **options.slice(:params))
674
+ rerun.update(results)
675
+ app.shutdown
676
+ outputter.print_summary(results, results.elapsed_time)
677
+ results.ok? ? SUCCESS : FAILURE
678
+ when 'show'
679
+ if options[:object]
680
+ outputter.print_task_info(**app.show_task(options[:object]))
681
+ else
682
+ outputter.print_tasks(**app.list_tasks(**options.slice(:filter)))
683
+ end
684
+ SUCCESS
685
+ end
688
686
  end
689
-
690
- outputter.print_target_info(
691
- group_targets_by_source,
692
- inventory.source,
693
- config.default_inventoryfile,
694
- target_flag
695
- )
696
687
  end
697
688
 
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.
689
+ # Process the target list by turning a PuppetDB query or rerun mode into a
690
+ # list of target names.
700
691
  #
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)
692
+ # @param pdb_client [Bolt::PuppetDB::Client] The PuppetDB client.
693
+ # @param rerun [Bolt::Rerun] The Rerun instance.
694
+ # @param options [Hash] The CLI options.
695
+ # @return [Hash] The target list.
696
+ #
697
+ private def process_target_list(pdb_client, rerun, options)
698
+ if options[:query]
699
+ pdb_client.query_certnames(options[:query])
700
+ elsif options[:rerun]
701
+ rerun.get_targets(options[:rerun])
702
+ elsif options[:targets]
703
+ options[:targets]
710
704
  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
705
  end
718
706
 
719
- # Looks up a value with Hiera as if in a plan outside an apply block, using
720
- # provided variable values for interpolations
707
+ # List content that ships with Bolt.
721
708
  #
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.
709
+ # @param options [Hash] The CLI options.
730
710
  #
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"
711
+ private def bundled_content(options)
712
+ # We only need to enumerate bundled content when running a task or plan
713
+ content = { 'Plan' => [],
714
+ 'Task' => [],
715
+ 'Plugin' => Bolt::Plugin::BUILTIN_PLUGINS }
716
+ if %w[plan task].include?(options[:subcommand]) && options[:action] == 'run'
717
+ default_content = Bolt::PAL.new(Bolt::Config::Modulepath.new([]), nil, nil)
718
+ content['Plan'] = default_content.list_plans.each_with_object([]) do |iter, col|
719
+ col << iter&.first
768
720
  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(',')
721
+ content['Task'] = default_content.list_tasks.each_with_object([]) do |iter, col|
722
+ col << iter&.first
782
723
  end
783
724
  end
784
725
 
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
726
+ content
811
727
  end
812
728
 
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
729
+ # Check and warn if Bolt is installed as a gem.
730
+ #
731
+ private def check_gem_install
732
+ if ENV['BOLT_GEM'].nil? && incomplete_install?
733
+ msg = <<~MSG.chomp
734
+ Bolt might be installed as a gem. To use Bolt reliably and with all of its
735
+ dependencies, uninstall the 'bolt' gem and install Bolt as a package:
736
+ https://puppet.com/docs/bolt/latest/bolt_installing.html
844
737
 
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
738
+ If you meant to install Bolt as a gem and want to disable this warning,
739
+ set the BOLT_GEM environment variable.
740
+ MSG
849
741
 
850
- results = Bolt::ResultSet.new(apply_prep_results.error_set.results + apply_results.results)
742
+ Bolt::Logger.warn("gem_install", msg)
851
743
  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
744
  end
877
745
 
878
- # Installs modules declared in the project configuration file.
746
+ # Print a fatal error. Print using the outputter if it's configured.
747
+ # Otherwise, mock the output by printing directly to stdout.
879
748
  #
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)
749
+ # @param error [StandardError] The error to print.
750
+ #
751
+ private def fatal_error(error)
752
+ if @outputter
753
+ @outputter.fatal_error(error)
754
+ elsif $stdout.isatty
755
+ $stdout.puts("\033[31m#{error.message}\033[0m")
756
+ else
757
+ $stdout.puts(error.message)
900
758
  end
901
-
902
- ok ? 0 : 1
903
759
  end
904
760
 
905
- # Adds a single module to the project.
761
+ # Query whether Bolt is installed as a gem or package by checking if all
762
+ # built-in modules are installed.
906
763
  #
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
764
+ private def incomplete_install?
765
+ builtin_module_list = %w[aggregate canary puppetdb_fact secure_env_vars puppet_connect]
766
+ (Dir.children(Bolt::Config::Modulepath::MODULES_PATH) - builtin_module_list).empty?
922
767
  end
923
768
 
924
- # Asserts that there is a project configuration file.
769
+ # Parse parameters for tasks and plans.
925
770
  #
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')
771
+ # @param options [Hash] Options from the calling method.
772
+ #
773
+ private def parse_params(command, object, pal, params: nil, params_parsed: nil)
774
+ if params
775
+ params_parsed ? params : pal.parse_params(command, object, params)
776
+ else
777
+ {}
934
778
  end
935
779
  end
936
780
 
937
- # Loads a Puppetfile and installs its modules.
781
+ # Parse variables for lookups.
938
782
  #
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
783
+ # @param vars [Array, NilClass] Unparsed variables.
784
+ #
785
+ private def parse_vars(vars)
786
+ return unless vars
787
+ Hash[vars.map { |a| a.split('=', 2) }]
947
788
  end
948
789
 
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
790
+ # TODO: See if this can be moved to Bolt::Analytics.
791
+ #
792
+ # Submit a screen view to the analytics client.
793
+ #
794
+ # @param analytics [Bolt::Analytics] The analytics client.
795
+ # @param config [Bolt::Config] The config.
796
+ # @param inventory [Bolt::Inventory] The inventory.
797
+ # @param options [Hash] The CLI options.
798
+ #
799
+ private def submit_screen_view(analytics, config, inventory, options)
800
+ screen = "#{options[:subcommand]}_#{options[:action]}"
958
801
 
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)
802
+ if options[:action] == 'show' && options[:object]
803
+ screen += '_object'
973
804
  end
974
- end
975
805
 
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
806
+ pp_count, yaml_count = if File.exist?(config.project.plans_path)
807
+ %w[pp yaml].map do |extension|
808
+ Find.find(config.project.plans_path.to_s)
809
+ .grep(/.*\.#{extension}/)
810
+ .length
811
+ end
812
+ else
813
+ [0, 0]
814
+ end
992
815
 
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
816
+ screen_view_fields = {
817
+ output_format: config.format,
818
+ boltdir_type: config.project.type,
819
+ puppet_plan_count: pp_count,
820
+ yaml_plan_count: yaml_count
821
+ }
1007
822
 
1008
- def validate_file(type, path, allow_dir = false)
1009
- if path.nil?
1010
- raise Bolt::CLIError, "A #{type} must be specified"
823
+ if options.key?(:targets)
824
+ screen_view_fields.merge!(
825
+ target_nodes: options[:targets].count,
826
+ inventory_nodes: inventory.node_names.count,
827
+ inventory_groups: inventory.group_names.count,
828
+ inventory_version: inventory.version
829
+ )
1011
830
  end
1012
831
 
1013
- Bolt::Util.validate_file(type, path, allow_dir)
832
+ analytics.screen_view(screen, **screen_view_fields)
1014
833
  end
1015
834
 
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.
835
+ # Issue a deprecation warning if the user is running an unsupported version
836
+ # of PowerShell on the controller.
1020
837
  #
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
838
+ private def validate_ps_version
839
+ if Bolt::Util.powershell?
840
+ command = "powershell.exe -NoProfile -NonInteractive -NoLogo -ExecutionPolicy "\
841
+ "Bypass -Command $PSVersionTable.PSVersion.Major"
842
+ stdout, _stderr, _status = Open3.capture3(command)
1034
843
 
1035
- def rerun
1036
- @rerun ||= Bolt::Rerun.new(config.rerunfile, config.save_rerun)
1037
- end
844
+ return unless !stdout.empty? && stdout.to_i < 3
1038
845
 
1039
- def outputter
1040
- @outputter ||= Bolt::Outputter.for_format(config.format,
1041
- config.color,
1042
- options[:verbose],
1043
- config.trace,
1044
- config.spinner)
846
+ msg = "Detected PowerShell 2 on controller. PowerShell 2 is unsupported."
847
+ Bolt::Logger.deprecation_warning("powershell_2_controller", msg)
848
+ end
1045
849
  end
1046
850
 
1047
- def log_outputter
1048
- @log_outputter ||= Bolt::Outputter::Logger.new(options[:verbose], config.trace)
1049
- end
851
+ # Warn the user that transport configuration options set from the command
852
+ # line may be overridden by transport configuration set in the inventory.
853
+ #
854
+ # @param opts [Hash] The CLI options.
855
+ #
856
+ private def warn_inventory_overrides_cli(config, opts)
857
+ inventory_source = if ENV[Bolt::Inventory::ENVIRONMENT_VAR]
858
+ Bolt::Inventory::ENVIRONMENT_VAR
859
+ elsif config.inventoryfile
860
+ config.inventoryfile
861
+ elsif File.exist?(config.default_inventoryfile)
862
+ config.default_inventoryfile
863
+ end
1050
864
 
1051
- def analytics
1052
- @analytics ||= begin
1053
- client = Bolt::Analytics.build_client(config.analytics)
1054
- client.bundled_content = bundled_content
1055
- client
865
+ inventory_cli_opts = %i[authentication escalation transports].each_with_object([]) do |key, acc|
866
+ acc.concat(Bolt::BoltOptionParser::OPTIONS[key])
1056
867
  end
1057
- end
1058
868
 
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
869
+ inventory_cli_opts.concat(%w[no-host-key-check no-ssl no-ssl-verify no-tty])
1070
870
 
1071
- Bolt::Logger.warn("gem_install", msg)
1072
- end
871
+ conflicting_options = Set.new(opts.keys.map(&:to_s)).intersection(inventory_cli_opts)
1073
872
 
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
873
+ if inventory_source && conflicting_options.any?
874
+ Bolt::Logger.warn(
875
+ "cli_overrides",
876
+ "CLI arguments #{conflicting_options.to_a} might be overridden by Inventory: #{inventory_source}"
877
+ )
1086
878
  end
1087
-
1088
- content
1089
879
  end
1090
880
 
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?
881
+ # Handle and print errors.
882
+ #
883
+ private def with_error_handling
884
+ yield
885
+ rescue Bolt::Error => e
886
+ fatal_error(e)
887
+ raise e
1096
888
  end
1097
889
 
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)
890
+ # Handle signals.
891
+ #
892
+ private def with_signal_handling
893
+ handler = Signal.trap :INT do |signo|
894
+ Bolt::Logger.logger(self).info(
895
+ "Exiting after receiving SIG#{Signal.signame(signo)} signal. "\
896
+ "There might be processes left executing on some targets."
897
+ )
898
+ exit!
1105
899
  end
900
+
901
+ yield
902
+ ensure
903
+ Signal.trap :INT, handler if handler
1106
904
  end
1107
905
  end
1108
906
  end