bolt 3.5.0 → 3.8.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 +3 -3
  3. data/bolt-modules/boltlib/lib/puppet/datatypes/applyresult.rb +26 -0
  4. data/bolt-modules/boltlib/lib/puppet/datatypes/containerresult.rb +27 -0
  5. data/bolt-modules/boltlib/lib/puppet/datatypes/resourceinstance.rb +43 -0
  6. data/bolt-modules/boltlib/lib/puppet/datatypes/result.rb +29 -0
  7. data/bolt-modules/boltlib/lib/puppet/datatypes/resultset.rb +34 -0
  8. data/bolt-modules/boltlib/lib/puppet/datatypes/target.rb +55 -0
  9. data/bolt-modules/boltlib/lib/puppet/functions/add_to_group.rb +1 -0
  10. data/bolt-modules/boltlib/lib/puppet/functions/apply_prep.rb +1 -0
  11. data/bolt-modules/boltlib/lib/puppet/functions/parallelize.rb +1 -0
  12. data/bolt-modules/boltlib/lib/puppet/functions/puppetdb_command.rb +66 -0
  13. data/bolt-modules/boltlib/lib/puppet/functions/remove_from_group.rb +1 -0
  14. data/bolt-modules/boltlib/lib/puppet/functions/run_script.rb +5 -1
  15. data/bolt-modules/boltlib/lib/puppet/functions/upload_file.rb +5 -1
  16. data/bolt-modules/boltlib/lib/puppet/functions/write_file.rb +1 -0
  17. data/bolt-modules/ctrl/lib/puppet/functions/ctrl/do_until.rb +2 -0
  18. data/bolt-modules/file/lib/puppet/functions/file/exists.rb +9 -3
  19. data/bolt-modules/file/lib/puppet/functions/file/read.rb +6 -2
  20. data/bolt-modules/file/lib/puppet/functions/file/readable.rb +8 -3
  21. data/guides/guide.txt +17 -0
  22. data/guides/links.txt +13 -0
  23. data/guides/targets.txt +29 -0
  24. data/guides/transports.txt +23 -0
  25. data/lib/bolt/analytics.rb +4 -8
  26. data/lib/bolt/applicator.rb +1 -1
  27. data/lib/bolt/bolt_option_parser.rb +351 -225
  28. data/lib/bolt/catalog.rb +2 -1
  29. data/lib/bolt/cli.rb +122 -55
  30. data/lib/bolt/config.rb +11 -7
  31. data/lib/bolt/config/options.rb +41 -9
  32. data/lib/bolt/config/transport/podman.rb +33 -0
  33. data/lib/bolt/executor.rb +15 -11
  34. data/lib/bolt/inventory.rb +5 -4
  35. data/lib/bolt/inventory/inventory.rb +3 -2
  36. data/lib/bolt/module_installer/specs/git_spec.rb +10 -6
  37. data/lib/bolt/outputter/human.rb +194 -79
  38. data/lib/bolt/outputter/json.rb +10 -4
  39. data/lib/bolt/pal.rb +45 -0
  40. data/lib/bolt/pal/yaml_plan/step.rb +4 -2
  41. data/lib/bolt/plan_creator.rb +2 -2
  42. data/lib/bolt/plugin.rb +13 -11
  43. data/lib/bolt/puppetdb/client.rb +54 -0
  44. data/lib/bolt/result.rb +5 -0
  45. data/lib/bolt/shell/bash.rb +23 -10
  46. data/lib/bolt/transport/docker.rb +1 -1
  47. data/lib/bolt/transport/docker/connection.rb +10 -6
  48. data/lib/bolt/transport/podman.rb +19 -0
  49. data/lib/bolt/transport/podman/connection.rb +98 -0
  50. data/lib/bolt/transport/ssh/connection.rb +3 -6
  51. data/lib/bolt/util.rb +71 -0
  52. data/lib/bolt/version.rb +1 -1
  53. data/lib/bolt_server/transport_app.rb +3 -0
  54. data/lib/bolt_spec/plans/mock_executor.rb +2 -1
  55. metadata +10 -2
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bolt/error'
4
+ require 'bolt/config/transport/base'
5
+
6
+ module Bolt
7
+ class Config
8
+ module Transport
9
+ class Podman < Base
10
+ OPTIONS = %w[
11
+ cleanup
12
+ host
13
+ interpreters
14
+ shell-command
15
+ tmpdir
16
+ tty
17
+ ].freeze
18
+
19
+ DEFAULTS = {
20
+ 'cleanup' => true
21
+ }.freeze
22
+
23
+ private def validate
24
+ super
25
+
26
+ if @config['interpreters']
27
+ @config['interpreters'] = normalize_interpreters(@config['interpreters'])
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
data/lib/bolt/executor.rb CHANGED
@@ -12,34 +12,37 @@ require 'bolt/config'
12
12
  require 'bolt/result_set'
13
13
  require 'bolt/puppetdb'
14
14
  # Load transports
15
- require 'bolt/transport/ssh'
16
- require 'bolt/transport/winrm'
17
- require 'bolt/transport/orch'
15
+ require 'bolt/transport/docker'
18
16
  require 'bolt/transport/local'
19
17
  require 'bolt/transport/lxd'
20
- require 'bolt/transport/docker'
18
+ require 'bolt/transport/orch'
19
+ require 'bolt/transport/podman'
21
20
  require 'bolt/transport/remote'
21
+ require 'bolt/transport/ssh'
22
+ require 'bolt/transport/winrm'
22
23
  require 'bolt/yarn'
23
24
 
24
25
  module Bolt
25
26
  TRANSPORTS = {
26
- ssh: Bolt::Transport::SSH,
27
- winrm: Bolt::Transport::WinRM,
28
- pcp: Bolt::Transport::Orch,
27
+ docker: Bolt::Transport::Docker,
29
28
  local: Bolt::Transport::Local,
30
29
  lxd: Bolt::Transport::LXD,
31
- docker: Bolt::Transport::Docker,
32
- remote: Bolt::Transport::Remote
30
+ pcp: Bolt::Transport::Orch,
31
+ podman: Bolt::Transport::Podman,
32
+ remote: Bolt::Transport::Remote,
33
+ ssh: Bolt::Transport::SSH,
34
+ winrm: Bolt::Transport::WinRM
33
35
  }.freeze
34
36
 
35
37
  class Executor
36
- attr_reader :noop, :transports, :in_parallel
38
+ attr_reader :noop, :transports, :in_parallel, :future
37
39
  attr_accessor :run_as
38
40
 
39
41
  def initialize(concurrency = 1,
40
42
  analytics = Bolt::Analytics::NoopClient.new,
41
43
  noop = false,
42
- modified_concurrency = false)
44
+ modified_concurrency = false,
45
+ future = {})
43
46
  # lazy-load expensive gem code
44
47
  require 'concurrent'
45
48
  @analytics = analytics
@@ -64,6 +67,7 @@ module Bolt
64
67
  @noop = noop
65
68
  @run_as = nil
66
69
  @in_parallel = false
70
+ @future = future
67
71
  @pool = if concurrency > 0
68
72
  Concurrent::ThreadPoolExecutor.new(name: 'exec', max_threads: concurrency)
69
73
  else
@@ -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
 
@@ -67,8 +67,7 @@ module Bolt
67
67
  elsif git.start_with?('https://github.com')
68
68
  git.split('https://github.com/').last.split('.git').first
69
69
  else
70
- raise Bolt::ValidationError,
71
- "Invalid git source: #{git}. Only GitHub modules are supported."
70
+ raise Bolt::ValidationError, invalid_git_msg(git)
72
71
  end
73
72
 
74
73
  [git, repo]
@@ -89,6 +88,14 @@ module Bolt
89
88
  }
90
89
  end
91
90
 
91
+ # Returns an error message that the provided repo is not a git repo or
92
+ # is private.
93
+ #
94
+ private def invalid_git_msg(repo_name)
95
+ "#{repo_name} is not a public GitHub repository. See https://pup.pt/no-resolve "\
96
+ "for information on how to install this module."
97
+ end
98
+
92
99
  # Returns a PuppetfileResolver::Model::GitModule object for resolving.
93
100
  #
94
101
  def to_resolver_module
@@ -157,10 +164,7 @@ module Bolt
157
164
 
158
165
  raise Bolt::Error.new(message, 'bolt/github-api-rate-limit-error')
159
166
  when Net::HTTPNotFound
160
- raise Bolt::Error.new(
161
- "#{git} is not a git repository.",
162
- "bolt/missing-git-repository-error"
163
- )
167
+ raise Bolt::Error.new(invalid_git_msg(git), "bolt/missing-git-repository-error")
164
168
  else
165
169
  raise Bolt::Error.new(
166
170
  "Ref #{ref} at #{git} is not a commit, tag, or branch.",
@@ -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",
@@ -173,12 +174,16 @@ module Bolt
173
174
  @stream.puts(remove_trail(indent(2, result.message)))
174
175
  end
175
176
 
176
- # Use special handling if the result looks like a command or script result
177
- if result.generic_value.keys == %w[stdout stderr merged_output exit_code]
177
+ case result.action
178
+ when 'command', 'script'
178
179
  safe_value = result.safe_value
179
180
  @stream.puts(indent(2, safe_value['merged_output'])) unless safe_value['merged_output'].strip.empty?
180
- elsif result.generic_value.any?
181
- @stream.puts(indent(2, ::JSON.pretty_generate(result.generic_value)))
181
+ when 'lookup'
182
+ @stream.puts(indent(2, result['value']))
183
+ else
184
+ if result.generic_value.any?
185
+ @stream.puts(indent(2, ::JSON.pretty_generate(result.generic_value)))
186
+ end
182
187
  end
183
188
  end
184
189
  end
@@ -292,7 +297,7 @@ module Bolt
292
297
  end
293
298
 
294
299
  def print_tasks(tasks, modulepath)
295
- command = Bolt::Util.powershell? ? 'Get-BoltTask -Task <TASK NAME>' : 'bolt task show <TASK NAME>'
300
+ command = Bolt::Util.powershell? ? 'Get-BoltTask -Name <TASK NAME>' : 'bolt task show <TASK NAME>'
296
301
 
297
302
  tasks = tasks.map do |name, description|
298
303
  description = truncate(description, 72)
@@ -313,78 +318,115 @@ module Bolt
313
318
 
314
319
  # @param [Hash] task A hash representing the task
315
320
  def print_task_info(task)
316
- # Building lots of strings...
317
- pretty_params = +""
318
- task_info = +""
319
- usage = if Bolt::Util.powershell?
320
- +"Invoke-BoltTask -Name #{task.name} -Targets <targets>"
321
+ params = (task.parameters || []).sort
322
+
323
+ info = +''
324
+
325
+ # Add task name and description
326
+ info << colorize(:cyan, "#{task.name}\n")
327
+ info << if task.description
328
+ indent(2, task.description.chomp)
321
329
  else
322
- +"bolt task run #{task.name} --targets <targets>"
330
+ indent(2, 'No description')
323
331
  end
332
+ info << "\n\n"
324
333
 
325
- task.parameters&.each do |k, v|
326
- pretty_params << "- #{k}: #{v['type'] || 'Any'}\n"
327
- pretty_params << " Default: #{v['default'].inspect}\n" if v.key?('default')
328
- pretty_params << " #{v['description']}\n" if v['description']
329
- usage << if v['type']&.start_with?("Optional")
330
- " [#{k}=<value>]"
334
+ # Build usage string
335
+ usage = +''
336
+ usage << if Bolt::Util.powershell?
337
+ "Invoke-BoltTask -Name #{task.name} -Targets <targets>"
338
+ else
339
+ "bolt task run #{task.name} --targets <targets>"
340
+ end
341
+ usage << (Bolt::Util.powershell? ? ' [-Noop]' : ' [--noop]') if task.supports_noop
342
+ params.each do |name, data|
343
+ usage << if data['type']&.start_with?('Optional')
344
+ " [#{name}=<value>]"
331
345
  else
332
- " #{k}=<value>"
346
+ " #{name}=<value>"
333
347
  end
334
348
  end
335
349
 
336
- if task.supports_noop
337
- usage << (Bolt::Util.powershell? ? ' [-Noop]' : ' [--noop]')
350
+ # Add usage
351
+ info << colorize(:cyan, "Usage\n")
352
+ info << indent(2, wrap(usage))
353
+ info << "\n"
354
+
355
+ # Add parameters, if any
356
+ if params.any?
357
+ info << colorize(:cyan, "Parameters\n")
358
+ params.each do |name, data|
359
+ info << indent(2, "#{colorize(:yellow, name)} #{colorize(:dim, data['type'] || 'Any')}\n")
360
+ info << indent(4, "#{wrap(data['description']).chomp}\n") if data['description']
361
+ info << indent(4, "Default: #{data['default'].inspect}\n") if data.key?('default')
362
+ info << "\n"
363
+ end
338
364
  end
339
365
 
340
- task_info << "\n#{task.name}"
341
- task_info << " - #{task.description}" if task.description
342
- task_info << "\n\n"
343
- task_info << "USAGE:\n#{usage}\n\n"
344
- task_info << "PARAMETERS:\n#{pretty_params}\n" unless pretty_params.empty?
345
- task_info << "MODULE:\n"
346
-
366
+ # Add module location
347
367
  path = task.files.first['path'].chomp("/tasks/#{task.files.first['name']}")
348
- task_info << if path.start_with?(Bolt::Config::Modulepath::MODULES_PATH)
349
- "built-in module"
350
- else
351
- path
352
- end
353
- @stream.puts(task_info)
368
+ info << colorize(:cyan, "Module\n")
369
+ info << if path.start_with?(Bolt::Config::Modulepath::MODULES_PATH)
370
+ indent(2, 'built-in module')
371
+ else
372
+ indent(2, path)
373
+ end
374
+
375
+ @stream.puts info
354
376
  end
355
377
 
356
378
  # @param [Hash] plan A hash representing the plan
357
379
  def print_plan_info(plan)
358
- # Building lots of strings...
359
- pretty_params = +""
360
- plan_info = +""
361
- usage = if Bolt::Util.powershell?
362
- +"Invoke-BoltPlan -Name #{plan['name']}"
380
+ params = plan['parameters'].sort
381
+
382
+ info = +''
383
+
384
+ # Add plan name and description
385
+ info << colorize(:cyan, "#{plan['name']}\n")
386
+ info << if plan['description']
387
+ indent(2, plan['description'].chomp)
363
388
  else
364
- +"bolt plan run #{plan['name']}"
389
+ indent(2, 'No description')
365
390
  end
391
+ info << "\n\n"
366
392
 
367
- plan['parameters'].each do |name, p|
368
- pretty_params << "- #{name}: #{p['type']}\n"
369
- pretty_params << " Default: #{p['default_value']}\n" unless p['default_value'].nil?
370
- pretty_params << " #{p['description']}\n" if p['description']
371
- usage << (p.include?('default_value') ? " [#{name}=<value>]" : " #{name}=<value>")
393
+ # Build the usage string
394
+ usage = +''
395
+ usage << if Bolt::Util.powershell?
396
+ "Invoke-BoltPlan -Name #{plan['name']}"
397
+ else
398
+ "bolt plan run #{plan['name']}"
399
+ end
400
+ params.each do |name, data|
401
+ usage << (data.include?('default_value') ? " [#{name}=<value>]" : " #{name}=<value>")
372
402
  end
373
403
 
374
- plan_info << "\n#{plan['name']}"
375
- plan_info << " - #{plan['description']}" if plan['description']
376
- plan_info << "\n\n"
377
- plan_info << "USAGE:\n#{usage}\n\n"
378
- plan_info << "PARAMETERS:\n#{pretty_params}\n" unless plan['parameters'].empty?
379
- plan_info << "MODULE:\n"
404
+ # Add usage
405
+ info << colorize(:cyan, "Usage\n")
406
+ info << indent(2, wrap(usage))
407
+ info << "\n"
380
408
 
381
- path = plan['module']
382
- plan_info << if path.start_with?(Bolt::Config::Modulepath::MODULES_PATH)
383
- "built-in module"
384
- else
385
- path
386
- end
387
- @stream.puts(plan_info)
409
+ # Add parameters, if any
410
+ if params.any?
411
+ info << colorize(:cyan, "Parameters\n")
412
+
413
+ params.each do |name, data|
414
+ info << indent(2, "#{colorize(:yellow, name)} #{colorize(:dim, data['type'])}\n")
415
+ info << indent(4, "#{wrap(data['description']).chomp}\n") if data['description']
416
+ info << indent(4, "Default: #{data['default_value']}\n") unless data['default_value'].nil?
417
+ info << "\n"
418
+ end
419
+ end
420
+
421
+ # Add module location
422
+ info << colorize(:cyan, "Module\n")
423
+ info << if plan['module'].start_with?(Bolt::Config::Modulepath::MODULES_PATH)
424
+ indent(2, 'built-in module')
425
+ else
426
+ indent(2, plan['module'])
427
+ end
428
+
429
+ @stream.puts info
388
430
  end
389
431
 
390
432
  def print_plans(plans, modulepath)
@@ -445,42 +487,115 @@ module Bolt
445
487
  end
446
488
  end
447
489
 
448
- def print_targets(target_list, inventoryfile)
490
+ def print_targets(target_list, inventory_source, default_inventory, target_flag)
449
491
  adhoc = colorize(:yellow, "(Not found in inventory file)")
450
492
 
451
493
  targets = []
452
494
  targets += target_list[:inventory].map { |target| [target.name, nil] }
453
495
  targets += target_list[:adhoc].map { |target| [target.name, adhoc] }
454
496
 
455
- if targets.any?
456
- @stream.puts format_table(targets, 0, 2)
457
- @stream.puts
458
- end
497
+ info = +''
459
498
 
460
- @stream.puts "INVENTORY FILE:"
461
- if File.exist?(inventoryfile)
462
- @stream.puts inventoryfile
499
+ # Add target list
500
+ info << colorize(:cyan, "Targets\n")
501
+ info << if targets.any?
502
+ format_table(targets, 2, 2).to_s
503
+ else
504
+ indent(2, 'No targets')
505
+ end
506
+ info << "\n\n"
507
+
508
+ info << format_inventory_source(inventory_source, default_inventory)
509
+ info << format_target_summary(target_list[:inventory].count, target_list[:adhoc].count, target_flag, false)
510
+
511
+ @stream.puts info
512
+ end
513
+
514
+ def print_target_info(target_list, inventory_source, default_inventory, target_flag)
515
+ adhoc_targets = target_list[:adhoc].map(&:name).to_set
516
+ inventory_targets = target_list[:inventory].map(&:name).to_set
517
+ targets = target_list.values.flatten.sort_by(&:name)
518
+
519
+ info = +''
520
+
521
+ if targets.any?
522
+ adhoc = colorize(:yellow, " (Not found in inventory file)")
523
+
524
+ targets.each do |target|
525
+ info << colorize(:cyan, target.name)
526
+ info << adhoc if adhoc_targets.include?(target.name)
527
+ info << "\n"
528
+ info << indent(2, target.detail.to_yaml.lines.drop(1).join)
529
+ info << "\n"
530
+ end
463
531
  else
464
- @stream.puts wrap("Tried to load inventory from #{inventoryfile}, but the file does not exist")
532
+ info << colorize(:cyan, "Targets\n")
533
+ info << indent(2, "No targets\n\n")
465
534
  end
466
535
 
467
- @stream.puts "\nTARGET COUNT:"
468
- @stream.puts "#{targets.count} total, #{target_list[:inventory].count} from inventory, "\
469
- "#{target_list[:adhoc].count} adhoc"
536
+ info << format_inventory_source(inventory_source, default_inventory)
537
+ info << format_target_summary(inventory_targets.count, adhoc_targets.count, target_flag, true)
538
+
539
+ @stream.puts info
470
540
  end
471
541
 
472
- def print_target_info(targets)
473
- @stream.puts ::JSON.pretty_generate(
474
- targets: targets.map(&:detail)
475
- )
476
- count = "#{targets.count} target#{'s' unless targets.count == 1}"
477
- @stream.puts colorize(:green, count)
542
+ private def format_inventory_source(inventory_source, default_inventory)
543
+ info = +''
544
+
545
+ # Add inventory file source
546
+ info << colorize(:cyan, "Inventory source\n")
547
+ info << if inventory_source
548
+ indent(2, "#{inventory_source}\n")
549
+ else
550
+ indent(2, wrap("Tried to load inventory from #{default_inventory}, but the file does not exist\n"))
551
+ end
552
+ info << "\n"
478
553
  end
479
554
 
480
- def print_groups(groups)
481
- count = "#{groups.count} group#{'s' unless groups.count == 1}"
482
- @stream.puts groups.join("\n")
483
- @stream.puts colorize(:green, count)
555
+ private def format_target_summary(inventory_count, adhoc_count, target_flag, detail_flag)
556
+ info = +''
557
+
558
+ # Add target count summary
559
+ count = "#{inventory_count + adhoc_count} total, "\
560
+ "#{inventory_count} from inventory, "\
561
+ "#{adhoc_count} adhoc"
562
+ info << colorize(:cyan, "Target count\n")
563
+ info << indent(2, count)
564
+
565
+ # Add filtering information
566
+ unless target_flag && detail_flag
567
+ info << colorize(:cyan, "\n\nAdditional information\n")
568
+
569
+ unless target_flag
570
+ opt = Bolt::Util.windows? ? "'-Targets', '-Query', or '-Rerun'" : "'--targets', '--query', or '--rerun'"
571
+ info << indent(2, "Use the #{opt} option to view specific targets\n")
572
+ end
573
+
574
+ unless detail_flag
575
+ opt = Bolt::Util.windows? ? '-Detail' : '--detail'
576
+ info << indent(2, "Use the '#{opt}' option to view target configuration and data")
577
+ end
578
+ end
579
+
580
+ info
581
+ end
582
+
583
+ def print_groups(groups, inventory_source, default_inventory)
584
+ info = +''
585
+
586
+ # Add group list
587
+ info << colorize(:cyan, "Groups\n")
588
+ info << indent(2, groups.join("\n"))
589
+ info << "\n\n"
590
+
591
+ # Add inventory file source
592
+ info << format_inventory_source(inventory_source, default_inventory)
593
+
594
+ # Add group count summary
595
+ info << colorize(:cyan, "Group count\n")
596
+ info << indent(2, "#{groups.count} total")
597
+
598
+ @stream.puts info
484
599
  end
485
600
 
486
601
  # @param [Bolt::ResultSet] apply_result A ResultSet object representing the result of a `bolt apply`