bolt 3.13.0 → 3.16.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

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 +1 -1
  3. data/bolt-modules/boltlib/lib/puppet/functions/apply_prep.rb +137 -104
  4. data/bolt-modules/boltlib/lib/puppet/functions/background.rb +2 -1
  5. data/bolt-modules/boltlib/lib/puppet/functions/parallelize.rb +5 -1
  6. data/bolt-modules/boltlib/lib/puppet/functions/run_plan.rb +13 -0
  7. data/bolt-modules/boltlib/lib/puppet/functions/wait.rb +47 -7
  8. data/bolt-modules/out/lib/puppet/functions/out/message.rb +4 -2
  9. data/bolt-modules/out/lib/puppet/functions/out/verbose.rb +4 -2
  10. data/guides/{debugging.txt → debugging.yaml} +5 -6
  11. data/guides/{inventory.txt → inventory.yaml} +6 -7
  12. data/guides/{links.txt → links.yaml} +3 -4
  13. data/guides/{logging.txt → logging.yaml} +5 -6
  14. data/guides/{module.txt → module.yaml} +5 -6
  15. data/guides/{modulepath.txt → modulepath.yaml} +5 -6
  16. data/guides/{project.txt → project.yaml} +6 -7
  17. data/guides/{targets.txt → targets.yaml} +5 -6
  18. data/guides/{transports.txt → transports.yaml} +6 -7
  19. data/lib/bolt/analytics.rb +3 -20
  20. data/lib/bolt/application.rb +620 -0
  21. data/lib/bolt/bolt_option_parser.rb +17 -5
  22. data/lib/bolt/cli.rb +592 -772
  23. data/lib/bolt/config/transport/options.rb +12 -0
  24. data/lib/bolt/config/transport/ssh.rb +7 -0
  25. data/lib/bolt/executor.rb +12 -4
  26. data/lib/bolt/fiber_executor.rb +63 -14
  27. data/lib/bolt/module_installer/puppetfile.rb +24 -10
  28. data/lib/bolt/outputter/human.rb +199 -43
  29. data/lib/bolt/outputter/json.rb +66 -43
  30. data/lib/bolt/outputter/logger.rb +1 -1
  31. data/lib/bolt/pal.rb +67 -14
  32. data/lib/bolt/pal/yaml_plan/step.rb +2 -0
  33. data/lib/bolt/pal/yaml_plan/step/message.rb +0 -8
  34. data/lib/bolt/pal/yaml_plan/step/verbose.rb +31 -0
  35. data/lib/bolt/pal/yaml_plan/transpiler.rb +1 -1
  36. data/lib/bolt/plan_creator.rb +2 -20
  37. data/lib/bolt/plan_future.rb +23 -3
  38. data/lib/bolt/plan_result.rb +1 -1
  39. data/lib/bolt/plugin/task.rb +1 -1
  40. data/lib/bolt/project.rb +0 -7
  41. data/lib/bolt/result_set.rb +2 -1
  42. data/lib/bolt/transport/local/connection.rb +17 -1
  43. data/lib/bolt/transport/orch/connection.rb +13 -1
  44. data/lib/bolt/transport/ssh/exec_connection.rb +3 -1
  45. data/lib/bolt/version.rb +1 -1
  46. data/lib/bolt_server/file_cache.rb +12 -0
  47. data/lib/bolt_server/schemas/action-apply.json +32 -0
  48. data/lib/bolt_server/schemas/action-apply_prep.json +19 -0
  49. data/lib/bolt_server/schemas/partials/target-ssh.json +4 -0
  50. data/lib/bolt_server/schemas/partials/target-winrm.json +4 -0
  51. data/lib/bolt_server/transport_app.rb +180 -60
  52. data/lib/bolt_spec/plans/mock_executor.rb +16 -6
  53. metadata +23 -15
  54. data/guides/guide.txt +0 -17
  55. data/lib/bolt/secret.rb +0 -37
@@ -16,6 +16,18 @@ module Bolt
16
16
  _default: false,
17
17
  _example: true
18
18
  },
19
+ "batch-mode" => {
20
+ type: [TrueClass, FalseClass],
21
+ description: "Whether to disable password querying. When set to `false`, SSH will fall back to "\
22
+ "prompting for a password if key authentication fails. This might cause Bolt to hang. "\
23
+ "To prevent Bolt from hanging, you can configure `ssh-command` to use an SSH utility "\
24
+ "such as sshpass that supports providing a password non-interactively. For more "\
25
+ "information, see [Providing a password non-interactively using "\
26
+ "`native-ssh`](troubleshooting.md#providing-a-password-non-interactively-using-native-ssh).",
27
+ _plugin: true,
28
+ _default: true,
29
+ _example: false
30
+ },
19
31
  "bundled-ruby" => {
20
32
  description: "Whether to use the Ruby bundled with Bolt packages for local targets.",
21
33
  type: [TrueClass, FalseClass],
@@ -34,6 +34,7 @@ module Bolt
34
34
 
35
35
  # Options available when using the native ssh transport
36
36
  NATIVE_OPTIONS = %w[
37
+ batch-mode
37
38
  cleanup
38
39
  copy-command
39
40
  host
@@ -49,6 +50,7 @@ module Bolt
49
50
  ].concat(RUN_AS_OPTIONS).sort.freeze
50
51
 
51
52
  DEFAULTS = {
53
+ "batch-mode" => true,
52
54
  "cleanup" => true,
53
55
  "connect-timeout" => 10,
54
56
  "disconnect-timeout" => 5,
@@ -124,6 +126,11 @@ module Bolt
124
126
  msg = 'Cannot use native SSH transport with load-config set to false'
125
127
  raise Bolt::ValidationError, msg
126
128
  end
129
+
130
+ if !@config['batch-mode'] && !@config['ssh-command']
131
+ raise Bolt::ValidationError,
132
+ 'Must set ssh-command when batch-mode is set to false'
133
+ end
127
134
  end
128
135
  end
129
136
  end
data/lib/bolt/executor.rb CHANGED
@@ -381,8 +381,16 @@ module Bolt
381
381
  # overloaded while also minimizing the Puppet lookups needed from plan
382
382
  # functions
383
383
  #
384
- def create_future(scope: nil, name: nil, &block)
385
- @fiber_executor.create_future(scope: scope, name: name, &block)
384
+ def create_future(plan_id:, scope: nil, name: nil, &block)
385
+ @fiber_executor.create_future(scope: scope, name: name, plan_id: plan_id, &block)
386
+ end
387
+
388
+ def get_current_future(fiber:)
389
+ @fiber_executor.get_current_future(fiber: fiber)
390
+ end
391
+
392
+ def get_current_plan_id(fiber:)
393
+ @fiber_executor.get_current_plan_id(fiber: fiber)
386
394
  end
387
395
 
388
396
  def plan_complete?
@@ -401,8 +409,8 @@ module Bolt
401
409
  @fiber_executor.wait(futures, **opts)
402
410
  end
403
411
 
404
- def plan_futures
405
- @fiber_executor.plan_futures
412
+ def get_futures_for_plan(plan_id:)
413
+ @fiber_executor.get_futures_for_plan(plan_id: plan_id)
406
414
  end
407
415
 
408
416
  # Execute a plan function concurrently. This function accepts the executor
@@ -5,18 +5,19 @@ require 'bolt/plan_future'
5
5
 
6
6
  module Bolt
7
7
  class FiberExecutor
8
- attr_reader :plan_futures
8
+ attr_reader :active_futures, :finished_futures
9
9
 
10
10
  def initialize
11
11
  @logger = Bolt::Logger.logger(self)
12
12
  @id = 0
13
- @plan_futures = []
13
+ @active_futures = []
14
+ @finished_futures = []
14
15
  end
15
16
 
16
17
  # Whether there is more than one fiber running in parallel.
17
18
  #
18
19
  def in_parallel?
19
- plan_futures.length > 1
20
+ active_futures.length > 1
20
21
  end
21
22
 
22
23
  # Creates a new Puppet scope from the current Plan scope so that variables
@@ -24,7 +25,7 @@ module Bolt
24
25
  # Then creates a new Fiber to execute the block, wraps the Fiber in a
25
26
  # Bolt::PlanFuture, and returns the Bolt::PlanFuture.
26
27
  #
27
- def create_future(scope: nil, name: nil)
28
+ def create_future(plan_id:, scope: nil, name: nil)
28
29
  newscope = nil
29
30
  if scope
30
31
  # Save existing variables to the new scope before starting the future
@@ -46,13 +47,16 @@ module Bolt
46
47
  end
47
48
 
48
49
  # PlanFutures are assigned an ID, which is just a global incrementing
49
- # integer. The main plan should always have ID 0.
50
+ # integer. The main plan should always have ID 0. They also have a
51
+ # plan_id, which identifies which plan spawned them. This is used for
52
+ # tracking which Futures to wait on when `wait()` is called without
53
+ # arguments.
50
54
  @id += 1
51
- future = Bolt::PlanFuture.new(future, @id, name)
55
+ future = Bolt::PlanFuture.new(future, @id, name: name, plan_id: plan_id, scope: newscope)
52
56
  @logger.trace("Created future #{future.name}")
53
57
 
54
58
  # Register the PlanFuture with the FiberExecutor to be executed
55
- plan_futures << future
59
+ active_futures << future
56
60
  future
57
61
  end
58
62
 
@@ -63,12 +67,16 @@ module Bolt
63
67
  # the PlanFuture and remove the PlanFuture from the FiberExecutor.
64
68
  #
65
69
  def round_robin
66
- plan_futures.each do |future|
67
- # If the Fiber is still running and can be resumed, then resume it
70
+ active_futures.each do |future|
71
+ # If the Fiber is still running and can be resumed, then resume it.
72
+ # Override Puppet's global_scope to prevent ephemerals in other scopes
73
+ # from being popped off in the wrong order due to race conditions.
74
+ # This primarily happens when running executor functions from custom
75
+ # Puppet language functions, but may happen elsewhere.
68
76
  @logger.trace("Checking future '#{future.name}'")
69
77
  if future.alive?
70
78
  @logger.trace("Resuming future '#{future.name}'")
71
- future.resume
79
+ Puppet.override(global_scope: future.scope) { future.resume }
72
80
  end
73
81
 
74
82
  # Once we've restarted the Fiber, check to see if it's finished again
@@ -78,19 +86,19 @@ module Bolt
78
86
 
79
87
  # If the future errored and the main plan has already exited, log the
80
88
  # error at warn level.
81
- unless plan_futures.map(&:id).include?(0) || future.state == "done"
89
+ unless active_futures.map(&:id).include?(0) || future.state == "done"
82
90
  Bolt::Logger.warn('errored_futures', "Error in future '#{future.name}': #{future.value}")
83
91
  end
84
92
 
85
93
  # Remove the PlanFuture from the FiberExecutor.
86
- plan_futures.delete(future)
94
+ finished_futures.push(active_futures.delete(future))
87
95
  end
88
96
 
89
97
  # If the Fiber immediately returned or if the Fiber is blocking on a
90
98
  # `wait` call, Bolt should pause for long enough that something can
91
99
  # execute before checking again. This mitigates CPU
92
100
  # thrashing.
93
- return unless plan_futures.all? { |f| %i[returned_immediately unfinished].include?(f.value) }
101
+ return unless active_futures.all? { |f| %i[returned_immediately unfinished].include?(f.value) }
94
102
  @logger.trace("Nothing can be resumed. Rechecking in 0.5 seconds.")
95
103
 
96
104
  sleep(0.5)
@@ -101,12 +109,53 @@ module Bolt
101
109
  # Bolt can exit.
102
110
  #
103
111
  def plan_complete?
104
- plan_futures.empty?
112
+ active_futures.empty?
113
+ end
114
+
115
+ def all_futures
116
+ active_futures + finished_futures
117
+ end
118
+
119
+ # Get the PlanFuture object that is currently executing
120
+ #
121
+ def get_current_future(fiber:)
122
+ all_futures.select { |f| f.fiber == fiber }.first
123
+ end
124
+
125
+ # Get the plan invocation ID for the PlanFuture that is currently executing
126
+ #
127
+ def get_current_plan_id(fiber:)
128
+ get_current_future(fiber: fiber).current_plan
129
+ end
130
+
131
+ # Get the Future objects associated with a particular plan invocation.
132
+ #
133
+ def get_futures_for_plan(plan_id:)
134
+ all_futures.select { |f| f.original_plan == plan_id }
105
135
  end
106
136
 
107
137
  # Block until the provided PlanFuture objects have finished, or the timeout is reached.
108
138
  #
109
139
  def wait(futures, timeout: nil, catch_errors: false, **_kwargs)
140
+ if futures.nil?
141
+ results = []
142
+ plan_id = get_current_plan_id(fiber: Fiber.current)
143
+ # Recollect the futures for this plan until all of the futures have
144
+ # finished. This ensures that we include futures created inside of
145
+ # futures being waited on.
146
+ until (futures = get_futures_for_plan(plan_id: plan_id)).map(&:alive?).none?
147
+ if futures.map(&:fiber).include?(Fiber.current)
148
+ msg = "The wait() function cannot be called with no arguments inside a "\
149
+ "background block in the same plan."
150
+ raise Bolt::Error.new(msg, 'bolt/infinite-wait')
151
+ end
152
+ # Wait for all the futures we know about so far before recollecting
153
+ # Futures for the plan and waiting again
154
+ results = wait(futures, timeout: timeout, catch_errors: catch_errors)
155
+ end
156
+ return results
157
+ end
158
+
110
159
  if timeout.nil?
111
160
  Fiber.yield(:unfinished) until futures.map(&:alive?).none?
112
161
  else
@@ -97,20 +97,34 @@ module Bolt
97
97
  end
98
98
  end
99
99
 
100
- return if unsatisfied_specs.empty?
101
-
100
+ versionless_mods = @modules.select { |mod| mod.is_a?(ForgeModule) && mod.version.nil? }
102
101
  command = Bolt::Util.windows? ? 'Install-BoltModule -Force' : 'bolt module install --force'
103
102
 
104
- message = <<~MESSAGE.chomp
105
- Puppetfile does not include modules that satisfy the following specifications:
103
+ if unsatisfied_specs.any?
104
+ message = <<~MESSAGE.chomp
105
+ Puppetfile does not include modules that satisfy the following specifications:
106
+
107
+ #{unsatisfied_specs.map(&:to_hash).to_yaml.lines.drop(1).join.chomp}
108
+
109
+ This Puppetfile might not be managed by Bolt. To forcibly overwrite the
110
+ Puppetfile, run '#{command}'.
111
+ MESSAGE
106
112
 
107
- #{unsatisfied_specs.map(&:to_hash).to_yaml.lines.drop(1).join.chomp}
108
-
109
- This Puppetfile might not be managed by Bolt. To forcibly overwrite the
110
- Puppetfile, run '#{command}'.
111
- MESSAGE
113
+ raise Bolt::Error.new(message, 'bolt/missing-module-specs')
114
+ end
112
115
 
113
- raise Bolt::Error.new(message, 'bolt/missing-module-specs')
116
+ if versionless_mods.any?
117
+ message = <<~MESSAGE.chomp
118
+ Puppetfile includes Forge modules without a version requirement:
119
+
120
+ #{versionless_mods.map(&:to_spec).join.chomp}
121
+
122
+ This Puppetfile might not be managed by Bolt. To forcibly overwrite the
123
+ Puppetfile, run '#{command}'.
124
+ MESSAGE
125
+
126
+ raise Bolt::Error.new(message, 'bolt/missing-module-version-specs')
127
+ end
114
128
  end
115
129
  end
116
130
  end
@@ -310,7 +310,12 @@ module Bolt
310
310
  )
311
311
  end
312
312
 
313
- def print_tasks(tasks, modulepath)
313
+ # List available tasks.
314
+ #
315
+ # @param tasks [Array] A list of task names and descriptions.
316
+ # @param modulepath [Array] The modulepath.
317
+ #
318
+ def print_tasks(tasks:, modulepath:)
314
319
  command = Bolt::Util.powershell? ? 'Get-BoltTask -Name <TASK NAME>' : 'bolt task show <TASK NAME>'
315
320
 
316
321
  tasks = tasks.map do |name, description|
@@ -330,8 +335,11 @@ module Bolt
330
335
  @stream.puts indent(2, "Use '#{command}' to view details and parameters for a specific task.")
331
336
  end
332
337
 
333
- # @param [Hash] task A hash representing the task
334
- def print_task_info(task)
338
+ # Print information about a task.
339
+ #
340
+ # @param task [Bolt::Task] The task information.
341
+ #
342
+ def print_task_info(task:)
335
343
  params = (task.parameters || []).sort
336
344
 
337
345
  info = +''
@@ -341,7 +349,7 @@ module Bolt
341
349
  info << if task.description
342
350
  indent(2, task.description.chomp)
343
351
  else
344
- indent(2, 'No description')
352
+ indent(2, 'No description available.')
345
353
  end
346
354
  info << "\n\n"
347
355
 
@@ -400,7 +408,7 @@ module Bolt
400
408
  info << if plan['description']
401
409
  indent(2, plan['description'].chomp)
402
410
  else
403
- indent(2, 'No description')
411
+ indent(2, 'No description available.')
404
412
  end
405
413
  info << "\n\n"
406
414
 
@@ -443,7 +451,7 @@ module Bolt
443
451
  @stream.puts info
444
452
  end
445
453
 
446
- def print_plans(plans, modulepath)
454
+ def print_plans(plans:, modulepath:)
447
455
  command = Bolt::Util.powershell? ? 'Get-BoltPlan -Name <PLAN NAME>' : 'bolt plan show <PLAN NAME>'
448
456
 
449
457
  plans = plans.map do |name, description|
@@ -463,7 +471,11 @@ module Bolt
463
471
  @stream.puts indent(2, "Use '#{command}' to view details and parameters for a specific plan.")
464
472
  end
465
473
 
466
- def print_topics(topics)
474
+ # Print available guide topics.
475
+ #
476
+ # @param topics [Array] The available topics.
477
+ #
478
+ def print_topics(topics:, **_kwargs)
467
479
  info = +"#{colorize(:cyan, 'Topics')}\n"
468
480
  info << indent(2, topics.join("\n"))
469
481
  info << "\n\n#{colorize(:cyan, 'Additional information')}\n"
@@ -471,8 +483,20 @@ module Bolt
471
483
  @stream.puts info
472
484
  end
473
485
 
474
- def print_guide(guide, _topic)
475
- @stream.puts(guide)
486
+ # Print the guide for the specified topic.
487
+ #
488
+ # @param guide [String] The guide.
489
+ #
490
+ def print_guide(topic:, guide:, documentation: nil, **_kwargs)
491
+ info = +"#{colorize(:cyan, topic)}\n"
492
+ info << indent(2, guide)
493
+
494
+ if documentation
495
+ info << "\n#{colorize(:cyan, 'Documentation')}\n"
496
+ info << indent(2, documentation.join("\n"))
497
+ end
498
+
499
+ @stream.puts info
476
500
  end
477
501
 
478
502
  def print_plan_lookup(value)
@@ -480,15 +504,19 @@ module Bolt
480
504
  end
481
505
 
482
506
  def print_module_list(module_list)
507
+ info = +''
508
+
483
509
  module_list.each do |path, modules|
484
- if (mod = modules.find { |m| m[:internal_module_group] })
485
- @stream.puts(colorize(:cyan, mod[:internal_module_group]))
486
- else
487
- @stream.puts(colorize(:cyan, path))
488
- end
510
+ info << if (mod = modules.find { |m| m[:internal_module_group] })
511
+ colorize(:cyan, mod[:internal_module_group])
512
+ else
513
+ colorize(:cyan, path)
514
+ end
515
+
516
+ info << "\n"
489
517
 
490
518
  if modules.empty?
491
- @stream.puts('(no modules installed)')
519
+ info << '(no modules installed)'
492
520
  else
493
521
  module_info = modules.map do |m|
494
522
  version = if m[:version].nil?
@@ -500,24 +528,100 @@ module Bolt
500
528
  [m[:name], version]
501
529
  end
502
530
 
503
- @stream.puts format_table(module_info, 2, 1)
531
+ info << format_table(module_info, 2, 1).to_s
504
532
  end
505
533
 
506
- @stream.write("\n")
534
+ info << "\n\n"
507
535
  end
536
+
537
+ command = Bolt::Util.powershell? ? 'Get-BoltModule -Name <MODULE>' : 'bolt module show <MODULE>'
538
+ info << colorize(:cyan, "Additional information\n")
539
+ info << indent(2, "Use '#{command}' to view details for a specific module.")
540
+
541
+ @stream.puts info
508
542
  end
509
543
 
510
- def print_plugin_list(plugin_list, modulepath)
544
+ # Prints detailed module information.
545
+ #
546
+ # @param name [String] The module's short name.
547
+ # @param metadata [Hash] The module's metadata.
548
+ # @param path [String] The path to the module.
549
+ # @param plans [Array] The module's plans.
550
+ # @param tasks [Array] The module's tasks.
551
+ #
552
+ def print_module_info(name:, metadata:, path:, plans:, tasks:, **_kwargs)
553
+ info = +''
554
+
555
+ info << colorize(:cyan, name)
556
+
557
+ info << colorize(:dim, " [#{metadata['version']}]") if metadata['version']
558
+ info << "\n"
559
+
560
+ info << if metadata['summary']
561
+ indent(2, wrap(metadata['summary'].strip, 76))
562
+ else
563
+ indent(2, "No description available.\n")
564
+ end
565
+ info << "\n"
566
+
567
+ if tasks.any?
568
+ length = tasks.map(&:first).map(&:length).max
569
+ data = tasks.map { |task, desc| [task, truncate(desc, 76 - length)] }
570
+ info << colorize(:cyan, "Tasks\n")
571
+ info << format_table(data, 2).to_s
572
+ info << "\n\n"
573
+ end
574
+
575
+ if plans.any?
576
+ length = plans.map(&:first).map(&:length).max
577
+ data = plans.map { |plan, desc| [plan, truncate(desc, 76 - length)] }
578
+ info << colorize(:cyan, "Plans\n")
579
+ info << format_table(data, 2).to_s
580
+ info << "\n\n"
581
+ end
582
+
583
+ if metadata['operatingsystem_support']&.any?
584
+ supported = metadata['operatingsystem_support'].map do |os|
585
+ [os['operatingsystem'], os['operatingsystemrelease']&.join(', ')]
586
+ end
587
+
588
+ info << colorize(:cyan, "Operating system support\n")
589
+ info << format_table(supported, 2).to_s
590
+ info << "\n\n"
591
+ end
592
+
593
+ if metadata['dependencies']&.any?
594
+ dependencies = metadata['dependencies'].map do |dep|
595
+ [dep['name'], dep['version_requirement']]
596
+ end
597
+
598
+ info << colorize(:cyan, "Dependencies\n")
599
+ info << format_table(dependencies, 2).to_s
600
+ info << "\n\n"
601
+ end
602
+
603
+ info << colorize(:cyan, "Path\n")
604
+ info << if path.start_with?(Bolt::Config::Modulepath::MODULES_PATH) ||
605
+ path.start_with?(Bolt::Config::Modulepath::BOLTLIB_PATH)
606
+ indent(2, 'built-in module')
607
+ else
608
+ indent(2, path)
609
+ end
610
+
611
+ @stream.puts info
612
+ end
613
+
614
+ def print_plugin_list(plugins:, modulepath:)
511
615
  info = +''
512
- length = plugin_list.values.map(&:keys).flatten.map(&:length).max + 4
616
+ length = plugins.values.map(&:keys).flatten.map(&:length).max + 4
513
617
 
514
- plugin_list.each do |hook, plugins|
515
- next if plugins.empty?
618
+ plugins.each do |hook, plugin|
619
+ next if plugin.empty?
516
620
  next if hook == :validate_resolve_reference
517
621
 
518
622
  info << colorize(:cyan, "#{hook}\n")
519
623
 
520
- plugins.each do |name, description|
624
+ plugin.each do |name, description|
521
625
  info << indent(2, name.ljust(length))
522
626
  info << truncate(description, 80 - length) if description
523
627
  info << "\n"
@@ -535,12 +639,37 @@ module Bolt
535
639
  @stream.puts info.chomp
536
640
  end
537
641
 
538
- def print_targets(target_list, inventory_source, default_inventory, target_flag)
539
- adhoc = colorize(:yellow, "(Not found in inventory file)")
642
+ def print_new_plan(name:, path:)
643
+ if Bolt::Util.powershell?
644
+ show_command = 'Get-BoltPlan -Name '
645
+ run_command = 'Invoke-BoltPlan -Name '
646
+ else
647
+ show_command = 'bolt plan show'
648
+ run_command = 'bolt plan run'
649
+ end
650
+
651
+ print_message(<<~OUTPUT)
652
+ Created plan '#{name}' at '#{path}'
653
+
654
+ Show this plan with:
655
+ #{show_command} #{name}
656
+ Run this plan with:
657
+ #{run_command} #{name}
658
+ OUTPUT
659
+ end
660
+
661
+ # Print target names and where they came from.
662
+ #
663
+ # @param adhoc [Hash] Adhoc targets provided on the command line.
664
+ # @param inventory [Hash] Targets provided from the inventory.
665
+ # @param flag [Boolean] Whether a targeting command-line option was used.
666
+ #
667
+ def print_targets(adhoc:, inventory:, flag:, **_kwargs)
668
+ adhoc_text = colorize(:yellow, "(Not found in inventory file)")
540
669
 
541
670
  targets = []
542
- targets += target_list[:inventory].map { |target| [target.name, nil] }
543
- targets += target_list[:adhoc].map { |target| [target.name, adhoc] }
671
+ targets += inventory[:targets].map { |target| [target['name'], nil] }
672
+ targets += adhoc[:targets].map { |target| [target['name'], adhoc_text] }
544
673
 
545
674
  info = +''
546
675
 
@@ -553,27 +682,31 @@ module Bolt
553
682
  end
554
683
  info << "\n\n"
555
684
 
556
- info << format_inventory_source(inventory_source, default_inventory)
557
- info << format_target_summary(target_list[:inventory].count, target_list[:adhoc].count, target_flag, false)
685
+ info << format_inventory_source(inventory[:file], inventory[:default])
686
+ info << format_target_summary(inventory[:count], adhoc[:count], flag, false)
558
687
 
559
688
  @stream.puts info
560
689
  end
561
690
 
562
- def print_target_info(target_list, inventory_source, default_inventory, target_flag)
563
- adhoc_targets = target_list[:adhoc].map(&:name).to_set
564
- inventory_targets = target_list[:inventory].map(&:name).to_set
565
- targets = target_list.values.flatten.sort_by(&:name)
691
+ # Print detailed target information.
692
+ #
693
+ # @param adhoc [Hash] Adhoc targets provided on the command line.
694
+ # @param inventory [Hash] Targets provided from the inventory.
695
+ # @param flag [Boolean] Whether a targeting command-line option was used.
696
+ #
697
+ def print_target_info(adhoc:, inventory:, flag:, **_kwargs)
698
+ targets = (adhoc[:targets] + inventory[:targets]).sort_by { |t| t['name'] }
566
699
 
567
700
  info = +''
568
701
 
569
702
  if targets.any?
570
- adhoc = colorize(:yellow, " (Not found in inventory file)")
703
+ adhoc_text = colorize(:yellow, " (Not found in inventory file)")
571
704
 
572
705
  targets.each do |target|
573
- info << colorize(:cyan, target.name)
574
- info << adhoc if adhoc_targets.include?(target.name)
706
+ info << colorize(:cyan, target['name'])
707
+ info << adhoc_text if adhoc[:targets].include?(target)
575
708
  info << "\n"
576
- info << indent(2, target.detail.to_yaml.lines.drop(1).join)
709
+ info << indent(2, target.to_yaml.lines.drop(1).join)
577
710
  info << "\n"
578
711
  end
579
712
  else
@@ -581,8 +714,8 @@ module Bolt
581
714
  info << indent(2, "No targets\n\n")
582
715
  end
583
716
 
584
- info << format_inventory_source(inventory_source, default_inventory)
585
- info << format_target_summary(inventory_targets.count, adhoc_targets.count, target_flag, true)
717
+ info << format_inventory_source(inventory[:file], inventory[:default])
718
+ info << format_target_summary(inventory[:count], adhoc[:count], flag, true)
586
719
 
587
720
  @stream.puts info
588
721
  end
@@ -628,7 +761,13 @@ module Bolt
628
761
  info
629
762
  end
630
763
 
631
- def print_groups(groups, inventory_source, default_inventory)
764
+ # Print inventory group information.
765
+ #
766
+ # @param count [Integer] Number of groups in the inventory.
767
+ # @param groups [Array] Names of groups in the inventory.
768
+ # @param inventory [Hash] Where the inventory was loaded from.
769
+ #
770
+ def print_groups(count:, groups:, inventory:)
632
771
  info = +''
633
772
 
634
773
  # Add group list
@@ -637,18 +776,18 @@ module Bolt
637
776
  info << "\n\n"
638
777
 
639
778
  # Add inventory file source
640
- info << format_inventory_source(inventory_source, default_inventory)
779
+ info << format_inventory_source(inventory[:source], inventory[:default])
641
780
 
642
781
  # Add group count summary
643
782
  info << colorize(:cyan, "Group count\n")
644
- info << indent(2, "#{groups.count} total")
783
+ info << indent(2, "#{count} total")
645
784
 
646
785
  @stream.puts info
647
786
  end
648
787
 
649
788
  # @param [Bolt::ResultSet] apply_result A ResultSet object representing the result of a `bolt apply`
650
- def print_apply_result(apply_result, elapsed_time)
651
- print_summary(apply_result, elapsed_time)
789
+ def print_apply_result(apply_result)
790
+ print_summary(apply_result, apply_result.elapsed_time)
652
791
  end
653
792
 
654
793
  # @param [Bolt::PlanResult] plan_result A PlanResult object
@@ -665,6 +804,12 @@ module Bolt
665
804
  print_container_result(value.result)
666
805
  when Bolt::ResultSet
667
806
  print_result_set(value)
807
+ when Bolt::Result
808
+ print_result(value)
809
+ when Bolt::ApplyResult
810
+ print_apply_result(value)
811
+ when Bolt::Error
812
+ print_bolt_error(**value.to_h.transform_keys(&:to_sym))
668
813
  else
669
814
  @stream.puts(::JSON.pretty_generate(plan_result, quirks_mode: true))
670
815
  end
@@ -704,6 +849,17 @@ module Bolt
704
849
  @stream.puts(colorize(:red, message))
705
850
  end
706
851
 
852
+ def print_bolt_error(msg:, details:, **_kwargs)
853
+ err = msg
854
+ if (f = details[:file])
855
+ err += "\n (file: #{f}"
856
+ err += ", line: #{details[:line]}" if details[:line]
857
+ err += ", column: #{details[:column]}" if details[:column]
858
+ err += ")"
859
+ end
860
+ @stream.puts(colorize(:red, err))
861
+ end
862
+
707
863
  def print_prompt(prompt)
708
864
  @stream.print(colorize(:cyan, indent(4, prompt)))
709
865
  end