bolt 3.3.0 → 3.7.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 (55) hide show
  1. checksums.yaml +4 -4
  2. data/Puppetfile +5 -5
  3. data/bolt-modules/boltlib/lib/puppet/datatypes/containerresult.rb +24 -0
  4. data/bolt-modules/boltlib/lib/puppet/functions/puppetdb_command.rb +66 -0
  5. data/bolt-modules/boltlib/lib/puppet/functions/run_command.rb +20 -2
  6. data/bolt-modules/boltlib/lib/puppet/functions/run_container.rb +162 -0
  7. data/bolt-modules/boltlib/lib/puppet/functions/run_script.rb +19 -2
  8. data/bolt-modules/boltlib/types/planresult.pp +1 -0
  9. data/bolt-modules/prompt/lib/puppet/functions/prompt.rb +20 -2
  10. data/bolt-modules/prompt/lib/puppet/functions/prompt/menu.rb +103 -0
  11. data/guides/targets.txt +31 -0
  12. data/lib/bolt/analytics.rb +4 -8
  13. data/lib/bolt/bolt_option_parser.rb +35 -17
  14. data/lib/bolt/cli.rb +109 -28
  15. data/lib/bolt/config.rb +11 -7
  16. data/lib/bolt/config/options.rb +41 -9
  17. data/lib/bolt/config/transport/lxd.rb +3 -1
  18. data/lib/bolt/config/transport/options.rb +7 -0
  19. data/lib/bolt/config/transport/podman.rb +33 -0
  20. data/lib/bolt/container_result.rb +105 -0
  21. data/lib/bolt/error.rb +15 -0
  22. data/lib/bolt/executor.rb +27 -15
  23. data/lib/bolt/inventory.rb +5 -4
  24. data/lib/bolt/inventory/inventory.rb +3 -2
  25. data/lib/bolt/inventory/options.rb +9 -0
  26. data/lib/bolt/inventory/target.rb +16 -0
  27. data/lib/bolt/node/output.rb +14 -4
  28. data/lib/bolt/outputter/human.rb +243 -84
  29. data/lib/bolt/outputter/json.rb +6 -4
  30. data/lib/bolt/outputter/logger.rb +17 -0
  31. data/lib/bolt/pal.rb +22 -2
  32. data/lib/bolt/pal/yaml_plan/step.rb +4 -2
  33. data/lib/bolt/pal/yaml_plan/step/command.rb +8 -0
  34. data/lib/bolt/pal/yaml_plan/step/script.rb +4 -0
  35. data/lib/bolt/pal/yaml_plan/transpiler.rb +2 -2
  36. data/lib/bolt/plan_creator.rb +2 -2
  37. data/lib/bolt/plugin.rb +13 -11
  38. data/lib/bolt/puppetdb/client.rb +54 -0
  39. data/lib/bolt/result.rb +5 -14
  40. data/lib/bolt/shell/bash.rb +33 -22
  41. data/lib/bolt/shell/powershell.rb +6 -8
  42. data/lib/bolt/transport/docker.rb +1 -1
  43. data/lib/bolt/transport/docker/connection.rb +21 -32
  44. data/lib/bolt/transport/lxd/connection.rb +5 -5
  45. data/lib/bolt/transport/orch.rb +13 -5
  46. data/lib/bolt/transport/podman.rb +19 -0
  47. data/lib/bolt/transport/podman/connection.rb +98 -0
  48. data/lib/bolt/util.rb +42 -0
  49. data/lib/bolt/version.rb +1 -1
  50. data/lib/bolt_server/transport_app.rb +3 -0
  51. data/lib/bolt_spec/plans/action_stubs/command_stub.rb +8 -1
  52. data/lib/bolt_spec/plans/action_stubs/script_stub.rb +8 -1
  53. data/lib/bolt_spec/plans/mock_executor.rb +91 -11
  54. data/modules/puppet_connect/plans/test_input_data.pp +22 -0
  55. metadata +11 -2
@@ -86,6 +86,7 @@ module Bolt
86
86
  if config.default_inventoryfile.exist?
87
87
  logger.debug("Loaded inventory from #{config.default_inventoryfile}")
88
88
  else
89
+ source = nil
89
90
  logger.debug("Tried to load inventory from #{config.default_inventoryfile}, but the file does not exist")
90
91
  end
91
92
  end
@@ -100,17 +101,17 @@ module Bolt
100
101
  validator.warnings.each { |warning| Bolt::Logger.warn(warning[:id], warning[:msg]) }
101
102
  end
102
103
 
103
- inventory = create_version(data, config.transport, config.transports, plugins)
104
+ inventory = create_version(data, config.transport, config.transports, plugins, source)
104
105
  inventory.validate
105
106
  inventory
106
107
  end
107
108
 
108
- def self.create_version(data, transport, transports, plugins)
109
+ def self.create_version(data, transport, transports, plugins, source = nil)
109
110
  version = (data || {}).delete('version') { 2 }
110
111
 
111
112
  case version
112
113
  when 2
113
- Bolt::Inventory::Inventory.new(data, transport, transports, plugins)
114
+ Bolt::Inventory::Inventory.new(data, transport, transports, plugins, source)
114
115
  else
115
116
  raise ValidationError.new("Unsupported version #{version} specified in inventory", nil)
116
117
  end
@@ -120,7 +121,7 @@ module Bolt
120
121
  config = Bolt::Config.default
121
122
  plugins = Bolt::Plugin.setup(config, nil)
122
123
 
123
- create_version({}, config.transport, config.transports, plugins)
124
+ create_version({}, config.transport, config.transports, plugins, nil)
124
125
  end
125
126
  end
126
127
  end
@@ -6,7 +6,7 @@ require 'bolt/inventory/target'
6
6
  module Bolt
7
7
  class Inventory
8
8
  class Inventory
9
- attr_reader :targets, :plugins, :config, :transport
9
+ attr_reader :config, :plugins, :source, :targets, :transport
10
10
 
11
11
  class WildcardError < Bolt::Error
12
12
  def initialize(target)
@@ -15,7 +15,7 @@ module Bolt
15
15
  end
16
16
 
17
17
  # TODO: Pass transport config instead of config object
18
- def initialize(data, transport, transports, plugins)
18
+ def initialize(data, transport, transports, plugins, source = nil)
19
19
  @logger = Bolt::Logger.logger(self)
20
20
  @data = data || {}
21
21
  @transport = transport
@@ -24,6 +24,7 @@ module Bolt
24
24
  @groups = Group.new(@data, plugins, all_group: true)
25
25
  @group_lookup = {}
26
26
  @targets = {}
27
+ @source = source
27
28
 
28
29
  @groups.resolve_string_targets(@groups.target_aliases, @groups.all_targets)
29
30
 
@@ -11,8 +11,10 @@ module Bolt
11
11
  facts
12
12
  features
13
13
  groups
14
+ plugin_hooks
14
15
  targets
15
16
  vars
17
+ version
16
18
  ].freeze
17
19
 
18
20
  # Definitions used to validate the data.
@@ -123,6 +125,13 @@ module Bolt
123
125
  description: "A map of variables for the group or target.",
124
126
  type: Hash,
125
127
  _plugin: true
128
+ },
129
+ "version" => {
130
+ description: "The version of the inventory file.",
131
+ type: Integer,
132
+ _plugin: false,
133
+ _example: 2,
134
+ _default: 2
126
135
  }
127
136
  }.freeze
128
137
  end
@@ -92,6 +92,7 @@ module Bolt
92
92
  end
93
93
 
94
94
  def add_facts(new_facts = {})
95
+ validate_fact_names(new_facts)
95
96
  @facts = Bolt::Util.deep_merge(@facts, new_facts)
96
97
  end
97
98
 
@@ -153,9 +154,24 @@ module Bolt
153
154
  raise Bolt::UnknownTransportError.new(transport, uri)
154
155
  end
155
156
 
157
+ validate_fact_names(facts)
158
+
156
159
  transport_config
157
160
  end
158
161
 
162
+ # Validate fact names and issue a deprecation warning if any fact names have a dot.
163
+ #
164
+ private def validate_fact_names(facts)
165
+ if (dotted = facts.keys.select { |name| name.include?('.') }).any?
166
+ Bolt::Logger.deprecate(
167
+ 'dotted_fact_name',
168
+ "Target '#{safe_name}' includes dotted fact names: '#{dotted.join("', '")}'. Dotted fact "\
169
+ "names are deprecated and Bolt does not automatically convert facts with dotted names to "\
170
+ "structured facts. For more information, see https://pup.pt/bolt-dotted-facts"
171
+ )
172
+ end
173
+ end
174
+
159
175
  def host
160
176
  @uri_obj.hostname || transport_config['host']
161
177
  end
@@ -6,13 +6,23 @@ require 'bolt/result'
6
6
  module Bolt
7
7
  class Node
8
8
  class Output
9
- attr_reader :stdout, :stderr
9
+ attr_reader :stderr, :stdout, :merged_output
10
10
  attr_accessor :exit_code
11
11
 
12
12
  def initialize
13
- @stdout = StringIO.new
14
- @stderr = StringIO.new
15
- @exit_code = 'unknown'
13
+ @stdout = StringIO.new
14
+ @stderr = StringIO.new
15
+ @merged_output = StringIO.new
16
+ @exit_code = 'unknown'
17
+ end
18
+
19
+ def to_h
20
+ {
21
+ 'stdout' => @stdout.string,
22
+ 'stderr' => @stderr.string,
23
+ 'merged_output' => @merged_output.string,
24
+ 'exit_code' => @exit_code
25
+ }
16
26
  end
17
27
  end
18
28
  end
@@ -6,6 +6,7 @@ module Bolt
6
6
  class Outputter
7
7
  class Human < Bolt::Outputter
8
8
  COLORS = {
9
+ dim: "2", # Dim, the other color of the rainbow
9
10
  red: "31",
10
11
  green: "32",
11
12
  yellow: "33",
@@ -92,6 +93,10 @@ module Bolt
92
93
  print_plan_start(event)
93
94
  when :plan_finish
94
95
  print_plan_finish(event)
96
+ when :container_start
97
+ print_container_start(event) if plan_logging?
98
+ when :container_finish
99
+ print_container_finish(event) if plan_logging?
95
100
  when :start_spin
96
101
  start_spin
97
102
  when :stop_spin
@@ -112,6 +117,34 @@ module Bolt
112
117
  @stream.puts(colorize(:green, "Started on #{target.safe_name}..."))
113
118
  end
114
119
 
120
+ def print_container_result(result)
121
+ if result.success?
122
+ @stream.puts(colorize(:green, "Finished running container #{result.object}:"))
123
+ else
124
+ @stream.puts(colorize(:red, "Failed running container #{result.object}:"))
125
+ end
126
+
127
+ if result.error_hash
128
+ @stream.puts(colorize(:red, remove_trail(indent(2, result.error_hash['msg']))))
129
+ return 0
130
+ end
131
+
132
+ # Only print results if there's something other than empty string and hash
133
+ safe_value = result.safe_value
134
+ if safe_value['stdout'].strip.empty? && safe_value['stderr'].strip.empty?
135
+ @stream.puts(indent(2, "Running container #{result.object} completed successfully with no result"))
136
+ else
137
+ unless safe_value['stdout'].strip && safe_value['stdout'].strip.empty?
138
+ @stream.puts(indent(2, "STDOUT:"))
139
+ @stream.puts(indent(4, safe_value['stdout']))
140
+ end
141
+ unless safe_value['stderr'].strip.empty?
142
+ @stream.puts(indent(2, "STDERR:"))
143
+ @stream.puts(indent(4, safe_value['stderr']))
144
+ end
145
+ end
146
+ end
147
+
115
148
  def print_result(result)
116
149
  if result.success?
117
150
  @stream.puts(colorize(:green, "Finished on #{result.target.safe_name}:"))
@@ -142,16 +175,9 @@ module Bolt
142
175
  end
143
176
 
144
177
  # Use special handling if the result looks like a command or script result
145
- if result.generic_value.keys == %w[stdout stderr exit_code]
178
+ if result.generic_value.keys == %w[stdout stderr merged_output exit_code]
146
179
  safe_value = result.safe_value
147
- unless safe_value['stdout'].strip.empty?
148
- @stream.puts(indent(2, "STDOUT:"))
149
- @stream.puts(indent(4, safe_value['stdout']))
150
- end
151
- unless safe_value['stderr'].strip.empty?
152
- @stream.puts(indent(2, "STDERR:"))
153
- @stream.puts(indent(4, safe_value['stderr']))
154
- end
180
+ @stream.puts(indent(2, safe_value['merged_output'])) unless safe_value['merged_output'].strip.empty?
155
181
  elsif result.generic_value.any?
156
182
  @stream.puts(indent(2, ::JSON.pretty_generate(result.generic_value)))
157
183
  end
@@ -187,6 +213,25 @@ module Bolt
187
213
  @stream.puts(colorize(:green, message))
188
214
  end
189
215
 
216
+ def print_container_start(image:, **_kwargs)
217
+ @stream.puts(colorize(:green, "Starting: run container '#{image}'"))
218
+ end
219
+
220
+ def print_container_finish(event)
221
+ result = if event[:result].is_a?(Bolt::ContainerFailure)
222
+ event[:result].result
223
+ else
224
+ event[:result]
225
+ end
226
+
227
+ if result.success?
228
+ @stream.puts(colorize(:green, "Finished: run container '#{result.object}' succeeded."))
229
+ else
230
+ @stream.puts(colorize(:red, "Finished: run container '#{result.object}' failed."))
231
+ end
232
+ print_container_result(result) if @verbose
233
+ end
234
+
190
235
  def print_plan_start(event)
191
236
  @plan_depth += 1
192
237
  # We use this event to both mark the start of a plan _and_ to enable
@@ -269,78 +314,115 @@ module Bolt
269
314
 
270
315
  # @param [Hash] task A hash representing the task
271
316
  def print_task_info(task)
272
- # Building lots of strings...
273
- pretty_params = +""
274
- task_info = +""
275
- usage = if Bolt::Util.powershell?
276
- +"Invoke-BoltTask -Name #{task.name} -Targets <targets>"
317
+ params = (task.parameters || []).sort
318
+
319
+ info = +''
320
+
321
+ # Add task name and description
322
+ info << colorize(:cyan, "#{task.name}\n")
323
+ info << if task.description
324
+ indent(2, task.description.chomp)
277
325
  else
278
- +"bolt task run #{task.name} --targets <targets>"
326
+ indent(2, 'No description')
279
327
  end
280
-
281
- task.parameters&.each do |k, v|
282
- pretty_params << "- #{k}: #{v['type'] || 'Any'}\n"
283
- pretty_params << " Default: #{v['default'].inspect}\n" if v.key?('default')
284
- pretty_params << " #{v['description']}\n" if v['description']
285
- usage << if v['type'].start_with?("Optional")
286
- " [#{k}=<value>]"
328
+ info << "\n\n"
329
+
330
+ # Build usage string
331
+ usage = +''
332
+ usage << if Bolt::Util.powershell?
333
+ "Invoke-BoltTask -Name #{task.name} -Targets <targets>"
334
+ else
335
+ "bolt task run #{task.name} --targets <targets>"
336
+ end
337
+ usage << (Bolt::Util.powershell? ? ' [-Noop]' : ' [--noop]') if task.supports_noop
338
+ params.each do |name, data|
339
+ usage << if data['type']&.start_with?('Optional')
340
+ " [#{name}=<value>]"
287
341
  else
288
- " #{k}=<value>"
342
+ " #{name}=<value>"
289
343
  end
290
344
  end
291
345
 
292
- if task.supports_noop
293
- usage << Bolt::Util.powershell? ? '[-Noop]' : '[--noop]'
346
+ # Add usage
347
+ info << colorize(:cyan, "Usage\n")
348
+ info << indent(2, wrap(usage))
349
+ info << "\n"
350
+
351
+ # Add parameters, if any
352
+ if params.any?
353
+ info << colorize(:cyan, "Parameters\n")
354
+ params.each do |name, data|
355
+ info << indent(2, "#{colorize(:yellow, name)} #{colorize(:dim, data['type'] || 'Any')}\n")
356
+ info << indent(4, "#{wrap(data['description']).chomp}\n") if data['description']
357
+ info << indent(4, "Default: #{data['default'].inspect}\n") if data.key?('default')
358
+ info << "\n"
359
+ end
294
360
  end
295
361
 
296
- task_info << "\n#{task.name}"
297
- task_info << " - #{task.description}" if task.description
298
- task_info << "\n\n"
299
- task_info << "USAGE:\n#{usage}\n\n"
300
- task_info << "PARAMETERS:\n#{pretty_params}\n" unless pretty_params.empty?
301
- task_info << "MODULE:\n"
302
-
362
+ # Add module location
303
363
  path = task.files.first['path'].chomp("/tasks/#{task.files.first['name']}")
304
- task_info << if path.start_with?(Bolt::Config::Modulepath::MODULES_PATH)
305
- "built-in module"
306
- else
307
- path
308
- end
309
- @stream.puts(task_info)
364
+ info << colorize(:cyan, "Module\n")
365
+ info << if path.start_with?(Bolt::Config::Modulepath::MODULES_PATH)
366
+ indent(2, 'built-in module')
367
+ else
368
+ indent(2, path)
369
+ end
370
+
371
+ @stream.puts info
310
372
  end
311
373
 
312
374
  # @param [Hash] plan A hash representing the plan
313
375
  def print_plan_info(plan)
314
- # Building lots of strings...
315
- pretty_params = +""
316
- plan_info = +""
317
- usage = if Bolt::Util.powershell?
318
- +"Invoke-BoltPlan -Name #{plan['name']}"
376
+ params = plan['parameters'].sort
377
+
378
+ info = +''
379
+
380
+ # Add plan name and description
381
+ info << colorize(:cyan, "#{plan['name']}\n")
382
+ info << if plan['description']
383
+ indent(2, plan['description'].chomp)
319
384
  else
320
- +"bolt plan run #{plan['name']}"
385
+ indent(2, 'No description')
321
386
  end
387
+ info << "\n\n"
388
+
389
+ # Build the usage string
390
+ usage = +''
391
+ usage << if Bolt::Util.powershell?
392
+ "Invoke-BoltPlan -Name #{plan['name']}"
393
+ else
394
+ "bolt plan run #{plan['name']}"
395
+ end
396
+ params.each do |name, data|
397
+ usage << (data.include?('default_value') ? " [#{name}=<value>]" : " #{name}=<value>")
398
+ end
322
399
 
323
- plan['parameters'].each do |name, p|
324
- pretty_params << "- #{name}: #{p['type']}\n"
325
- pretty_params << " Default: #{p['default_value']}\n" unless p['default_value'].nil?
326
- pretty_params << " #{p['description']}\n" if p['description']
327
- usage << (p.include?('default_value') ? " [#{name}=<value>]" : " #{name}=<value>")
400
+ # Add usage
401
+ info << colorize(:cyan, "Usage\n")
402
+ info << indent(2, wrap(usage))
403
+ info << "\n"
404
+
405
+ # Add parameters, if any
406
+ if params.any?
407
+ info << colorize(:cyan, "Parameters\n")
408
+
409
+ params.each do |name, data|
410
+ info << indent(2, "#{colorize(:yellow, name)} #{colorize(:dim, data['type'])}\n")
411
+ info << indent(4, "#{wrap(data['description']).chomp}\n") if data['description']
412
+ info << indent(4, "Default: #{data['default_value']}\n") unless data['default_value'].nil?
413
+ info << "\n"
414
+ end
328
415
  end
329
416
 
330
- plan_info << "\n#{plan['name']}"
331
- plan_info << " - #{plan['description']}" if plan['description']
332
- plan_info << "\n\n"
333
- plan_info << "USAGE:\n#{usage}\n\n"
334
- plan_info << "PARAMETERS:\n#{pretty_params}\n" unless plan['parameters'].empty?
335
- plan_info << "MODULE:\n"
417
+ # Add module location
418
+ info << colorize(:cyan, "Module\n")
419
+ info << if plan['module'].start_with?(Bolt::Config::Modulepath::MODULES_PATH)
420
+ indent(2, 'built-in module')
421
+ else
422
+ indent(2, plan['module'])
423
+ end
336
424
 
337
- path = plan['module']
338
- plan_info << if path.start_with?(Bolt::Config::Modulepath::MODULES_PATH)
339
- "built-in module"
340
- else
341
- path
342
- end
343
- @stream.puts(plan_info)
425
+ @stream.puts info
344
426
  end
345
427
 
346
428
  def print_plans(plans, modulepath)
@@ -401,42 +483,115 @@ module Bolt
401
483
  end
402
484
  end
403
485
 
404
- def print_targets(target_list, inventoryfile)
486
+ def print_targets(target_list, inventory_source, default_inventory, target_flag)
405
487
  adhoc = colorize(:yellow, "(Not found in inventory file)")
406
488
 
407
489
  targets = []
408
490
  targets += target_list[:inventory].map { |target| [target.name, nil] }
409
491
  targets += target_list[:adhoc].map { |target| [target.name, adhoc] }
410
492
 
411
- if targets.any?
412
- @stream.puts format_table(targets, 0, 2)
413
- @stream.puts
414
- end
493
+ info = +''
415
494
 
416
- @stream.puts "INVENTORY FILE:"
417
- if File.exist?(inventoryfile)
418
- @stream.puts inventoryfile
495
+ # Add target list
496
+ info << colorize(:cyan, "Targets\n")
497
+ info << if targets.any?
498
+ format_table(targets, 2, 2).to_s
499
+ else
500
+ indent(2, 'No targets')
501
+ end
502
+ info << "\n\n"
503
+
504
+ info << format_inventory_source(inventory_source, default_inventory)
505
+ info << format_target_summary(target_list[:inventory].count, target_list[:adhoc].count, target_flag, false)
506
+
507
+ @stream.puts info
508
+ end
509
+
510
+ def print_target_info(target_list, inventory_source, default_inventory, target_flag)
511
+ adhoc_targets = target_list[:adhoc].map(&:name).to_set
512
+ inventory_targets = target_list[:inventory].map(&:name).to_set
513
+ targets = target_list.values.flatten.sort_by(&:name)
514
+
515
+ info = +''
516
+
517
+ if targets.any?
518
+ adhoc = colorize(:yellow, " (Not found in inventory file)")
519
+
520
+ targets.each do |target|
521
+ info << colorize(:cyan, target.name)
522
+ info << adhoc if adhoc_targets.include?(target.name)
523
+ info << "\n"
524
+ info << indent(2, target.detail.to_yaml.lines.drop(1).join)
525
+ info << "\n"
526
+ end
419
527
  else
420
- @stream.puts wrap("Tried to load inventory from #{inventoryfile}, but the file does not exist")
528
+ info << colorize(:cyan, "Targets\n")
529
+ info << indent(2, "No targets\n\n")
421
530
  end
422
531
 
423
- @stream.puts "\nTARGET COUNT:"
424
- @stream.puts "#{targets.count} total, #{target_list[:inventory].count} from inventory, "\
425
- "#{target_list[:adhoc].count} adhoc"
532
+ info << format_inventory_source(inventory_source, default_inventory)
533
+ info << format_target_summary(inventory_targets.count, adhoc_targets.count, target_flag, true)
534
+
535
+ @stream.puts info
426
536
  end
427
537
 
428
- def print_target_info(targets)
429
- @stream.puts ::JSON.pretty_generate(
430
- targets: targets.map(&:detail)
431
- )
432
- count = "#{targets.count} target#{'s' unless targets.count == 1}"
433
- @stream.puts colorize(:green, count)
538
+ private def format_inventory_source(inventory_source, default_inventory)
539
+ info = +''
540
+
541
+ # Add inventory file source
542
+ info << colorize(:cyan, "Inventory source\n")
543
+ info << if inventory_source
544
+ indent(2, "#{inventory_source}\n")
545
+ else
546
+ indent(2, wrap("Tried to load inventory from #{default_inventory}, but the file does not exist\n"))
547
+ end
548
+ info << "\n"
434
549
  end
435
550
 
436
- def print_groups(groups)
437
- count = "#{groups.count} group#{'s' unless groups.count == 1}"
438
- @stream.puts groups.join("\n")
439
- @stream.puts colorize(:green, count)
551
+ private def format_target_summary(inventory_count, adhoc_count, target_flag, detail_flag)
552
+ info = +''
553
+
554
+ # Add target count summary
555
+ count = "#{inventory_count + adhoc_count} total, "\
556
+ "#{inventory_count} from inventory, "\
557
+ "#{adhoc_count} adhoc"
558
+ info << colorize(:cyan, "Target count\n")
559
+ info << indent(2, count)
560
+
561
+ # Add filtering information
562
+ unless target_flag && detail_flag
563
+ info << colorize(:cyan, "\n\nAdditional information\n")
564
+
565
+ unless target_flag
566
+ opt = Bolt::Util.windows? ? "'-Targets', '-Query', or '-Rerun'" : "'--targets', '--query', or '--rerun'"
567
+ info << indent(2, "Use the #{opt} option to view specific targets\n")
568
+ end
569
+
570
+ unless detail_flag
571
+ opt = Bolt::Util.windows? ? '-Detail' : '--detail'
572
+ info << indent(2, "Use the '#{opt}' option to view target configuration and data")
573
+ end
574
+ end
575
+
576
+ info
577
+ end
578
+
579
+ def print_groups(groups, inventory_source, default_inventory)
580
+ info = +''
581
+
582
+ # Add group list
583
+ info << colorize(:cyan, "Groups\n")
584
+ info << indent(2, groups.join("\n"))
585
+ info << "\n\n"
586
+
587
+ # Add inventory file source
588
+ info << format_inventory_source(inventory_source, default_inventory)
589
+
590
+ # Add group count summary
591
+ info << colorize(:cyan, "Group count\n")
592
+ info << indent(2, "#{groups.count} total")
593
+
594
+ @stream.puts info
440
595
  end
441
596
 
442
597
  # @param [Bolt::ResultSet] apply_result A ResultSet object representing the result of a `bolt apply`
@@ -452,6 +607,10 @@ module Bolt
452
607
  @stream.puts("Plan completed successfully with no result")
453
608
  when Bolt::ApplyFailure, Bolt::RunFailure
454
609
  print_result_set(value.result_set)
610
+ when Bolt::ContainerResult
611
+ print_container_result(value)
612
+ when Bolt::ContainerFailure
613
+ print_container_result(value.result)
455
614
  when Bolt::ResultSet
456
615
  print_result_set(value)
457
616
  else