bolt 3.1.0 → 3.6.1

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 (78) hide show
  1. checksums.yaml +4 -4
  2. data/Puppetfile +11 -11
  3. data/bolt-modules/boltlib/lib/puppet/datatypes/containerresult.rb +24 -0
  4. data/bolt-modules/boltlib/lib/puppet/functions/add_facts.rb +1 -1
  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_plan.rb +2 -2
  8. data/bolt-modules/boltlib/lib/puppet/functions/run_script.rb +44 -5
  9. data/bolt-modules/boltlib/lib/puppet/functions/upload_file.rb +1 -1
  10. data/bolt-modules/boltlib/types/planresult.pp +1 -0
  11. data/bolt-modules/file/lib/puppet/functions/file/read.rb +3 -2
  12. data/bolt-modules/prompt/lib/puppet/functions/prompt.rb +20 -2
  13. data/bolt-modules/prompt/lib/puppet/functions/prompt/menu.rb +103 -0
  14. data/lib/bolt/analytics.rb +4 -8
  15. data/lib/bolt/apply_result.rb +1 -1
  16. data/lib/bolt/bolt_option_parser.rb +6 -3
  17. data/lib/bolt/cli.rb +121 -36
  18. data/lib/bolt/config.rb +15 -7
  19. data/lib/bolt/config/options.rb +62 -12
  20. data/lib/bolt/config/transport/lxd.rb +23 -0
  21. data/lib/bolt/config/transport/options.rb +8 -1
  22. data/lib/bolt/config/transport/podman.rb +33 -0
  23. data/lib/bolt/container_result.rb +105 -0
  24. data/lib/bolt/error.rb +15 -0
  25. data/lib/bolt/executor.rb +37 -18
  26. data/lib/bolt/inventory/options.rb +9 -0
  27. data/lib/bolt/inventory/target.rb +16 -0
  28. data/lib/bolt/logger.rb +8 -0
  29. data/lib/bolt/module_installer.rb +2 -2
  30. data/lib/bolt/module_installer/puppetfile.rb +2 -2
  31. data/lib/bolt/module_installer/specs/forge_spec.rb +2 -2
  32. data/lib/bolt/module_installer/specs/git_spec.rb +2 -2
  33. data/lib/bolt/node/output.rb +14 -4
  34. data/lib/bolt/outputter/human.rb +259 -90
  35. data/lib/bolt/outputter/json.rb +3 -1
  36. data/lib/bolt/outputter/logger.rb +17 -0
  37. data/lib/bolt/pal.rb +24 -4
  38. data/lib/bolt/pal/yaml_plan.rb +1 -2
  39. data/lib/bolt/pal/yaml_plan/evaluator.rb +5 -141
  40. data/lib/bolt/pal/yaml_plan/step.rb +91 -31
  41. data/lib/bolt/pal/yaml_plan/step/command.rb +21 -13
  42. data/lib/bolt/pal/yaml_plan/step/download.rb +15 -16
  43. data/lib/bolt/pal/yaml_plan/step/eval.rb +11 -11
  44. data/lib/bolt/pal/yaml_plan/step/message.rb +13 -4
  45. data/lib/bolt/pal/yaml_plan/step/plan.rb +19 -15
  46. data/lib/bolt/pal/yaml_plan/step/resources.rb +82 -21
  47. data/lib/bolt/pal/yaml_plan/step/script.rb +36 -17
  48. data/lib/bolt/pal/yaml_plan/step/task.rb +19 -16
  49. data/lib/bolt/pal/yaml_plan/step/upload.rb +16 -17
  50. data/lib/bolt/pal/yaml_plan/transpiler.rb +3 -3
  51. data/lib/bolt/plan_creator.rb +1 -1
  52. data/lib/bolt/plugin.rb +13 -11
  53. data/lib/bolt/project_manager.rb +1 -1
  54. data/lib/bolt/project_manager/module_migrator.rb +1 -1
  55. data/lib/bolt/result.rb +5 -14
  56. data/lib/bolt/shell.rb +16 -0
  57. data/lib/bolt/shell/bash.rb +68 -30
  58. data/lib/bolt/shell/bash/tmpdir.rb +2 -2
  59. data/lib/bolt/shell/powershell.rb +28 -11
  60. data/lib/bolt/task.rb +1 -1
  61. data/lib/bolt/transport/docker.rb +1 -1
  62. data/lib/bolt/transport/docker/connection.rb +21 -32
  63. data/lib/bolt/transport/lxd.rb +26 -0
  64. data/lib/bolt/transport/lxd/connection.rb +99 -0
  65. data/lib/bolt/transport/orch.rb +13 -5
  66. data/lib/bolt/transport/podman.rb +19 -0
  67. data/lib/bolt/transport/podman/connection.rb +98 -0
  68. data/lib/bolt/transport/ssh/connection.rb +1 -1
  69. data/lib/bolt/transport/winrm/connection.rb +1 -1
  70. data/lib/bolt/util.rb +42 -0
  71. data/lib/bolt/version.rb +1 -1
  72. data/lib/bolt_server/transport_app.rb +16 -1
  73. data/lib/bolt_spec/plans/action_stubs.rb +1 -1
  74. data/lib/bolt_spec/plans/action_stubs/command_stub.rb +8 -1
  75. data/lib/bolt_spec/plans/action_stubs/script_stub.rb +8 -1
  76. data/lib/bolt_spec/plans/mock_executor.rb +91 -7
  77. data/modules/puppet_connect/plans/test_input_data.pp +22 -0
  78. metadata +12 -2
@@ -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
data/lib/bolt/logger.rb CHANGED
@@ -91,6 +91,14 @@ module Bolt
91
91
  Logging.logger[:root].appenders.any?
92
92
  end
93
93
 
94
+ def self.stream
95
+ @stream
96
+ end
97
+
98
+ def self.stream=(stream)
99
+ @stream = stream
100
+ end
101
+
94
102
  # A helper to ensure the Logging library is always initialized with our
95
103
  # custom log levels before retrieving a Logger instance.
96
104
  def self.logger(name)
@@ -45,7 +45,7 @@ module Bolt
45
45
  # specss. If that fails, fall back to resolving from project specs.
46
46
  # This prevents Bolt from modifying installed modules unless there is
47
47
  # a version conflict.
48
- @outputter.print_action_step("Resolving module dependencies, this may take a moment")
48
+ @outputter.print_action_step("Resolving module dependencies, this might take a moment")
49
49
 
50
50
  @outputter.start_spin
51
51
  begin
@@ -156,7 +156,7 @@ module Bolt
156
156
  # If forcibly installing or if there is no Puppetfile, resolve
157
157
  # and write a Puppetfile.
158
158
  if force || !path.exist?
159
- @outputter.print_action_step("Resolving module dependencies, this may take a moment")
159
+ @outputter.print_action_step("Resolving module dependencies, this might take a moment")
160
160
 
161
161
  # This doesn't use the block as it's more testable to just mock *_spin
162
162
  @outputter.start_spin
@@ -36,7 +36,7 @@ module Bolt
36
36
  raise Bolt::ValidationError, <<~MSG
37
37
  Unable to parse Puppetfile #{path}:
38
38
  #{parsed.validation_errors.join("\n\n")}.
39
- This may not be a Puppetfile managed by Bolt.
39
+ This Puppetfile might not be managed by Bolt.
40
40
  MSG
41
41
  end
42
42
 
@@ -106,7 +106,7 @@ module Bolt
106
106
 
107
107
  #{unsatisfied_specs.map(&:to_hash).to_yaml.lines.drop(1).join.chomp}
108
108
 
109
- This may not be a Puppetfile managed by Bolt. To forcibly overwrite the
109
+ This Puppetfile might not be managed by Bolt. To forcibly overwrite the
110
110
  Puppetfile, run '#{command}'.
111
111
  MESSAGE
112
112
 
@@ -39,8 +39,8 @@ module Bolt
39
39
  unless (match = name.match(NAME_REGEX))
40
40
  raise Bolt::ValidationError,
41
41
  "Invalid name for Forge module specification: #{name}. Name must match "\
42
- "'owner/name'. Owner segment may only include letters or digits. Name "\
43
- "segment must start with a lowercase letter and may only include lowercase "\
42
+ "'owner/name'. Owner segment can only include letters or digits. Name "\
43
+ "segment must start with a lowercase letter and can only include lowercase "\
44
44
  "letters, digits, and underscores."
45
45
  end
46
46
 
@@ -49,8 +49,8 @@ module Bolt
49
49
  unless (match = name.match(NAME_REGEX))
50
50
  raise Bolt::ValidationError,
51
51
  "Invalid name for Git module specification: #{name}. Name must match "\
52
- "'name' or 'owner/name'. Owner segment may only include letters or digits. "\
53
- "Name segment must start with a lowercase letter and may only include "\
52
+ "'name' or 'owner/name'. Owner segment can only include letters or digits. "\
53
+ "Name segment must start with a lowercase letter and can only include "\
54
54
  "lowercase letters, digits, and underscores."
55
55
  end
56
56
 
@@ -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",
@@ -53,10 +54,21 @@ module Bolt
53
54
  string.sub(/\s\z/, '')
54
55
  end
55
56
 
57
+ # Wraps a string to the specified width. Lines only wrap
58
+ # at whitespace.
59
+ #
56
60
  def wrap(string, width = 80)
61
+ return string unless string.is_a?(String)
57
62
  string.gsub(/(.{1,#{width}})(\s+|\Z)/, "\\1\n")
58
63
  end
59
64
 
65
+ # Trims a string to a specified width, adding an ellipsis if it's longer.
66
+ #
67
+ def truncate(string, width = 80)
68
+ return string unless string.is_a?(String) && string.length > width
69
+ string.lines.first[0...width].gsub(/\s\w+\s*$/, '...')
70
+ end
71
+
60
72
  def handle_event(event)
61
73
  case event[:type]
62
74
  when :enable_default_output
@@ -81,6 +93,10 @@ module Bolt
81
93
  print_plan_start(event)
82
94
  when :plan_finish
83
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?
84
100
  when :start_spin
85
101
  start_spin
86
102
  when :stop_spin
@@ -101,6 +117,34 @@ module Bolt
101
117
  @stream.puts(colorize(:green, "Started on #{target.safe_name}..."))
102
118
  end
103
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
+
104
148
  def print_result(result)
105
149
  if result.success?
106
150
  @stream.puts(colorize(:green, "Finished on #{result.target.safe_name}:"))
@@ -131,16 +175,9 @@ module Bolt
131
175
  end
132
176
 
133
177
  # Use special handling if the result looks like a command or script result
134
- if result.generic_value.keys == %w[stdout stderr exit_code]
178
+ if result.generic_value.keys == %w[stdout stderr merged_output exit_code]
135
179
  safe_value = result.safe_value
136
- unless safe_value['stdout'].strip.empty?
137
- @stream.puts(indent(2, "STDOUT:"))
138
- @stream.puts(indent(4, safe_value['stdout']))
139
- end
140
- unless safe_value['stderr'].strip.empty?
141
- @stream.puts(indent(2, "STDERR:"))
142
- @stream.puts(indent(4, safe_value['stderr']))
143
- end
180
+ @stream.puts(indent(2, safe_value['merged_output'])) unless safe_value['merged_output'].strip.empty?
144
181
  elsif result.generic_value.any?
145
182
  @stream.puts(indent(2, ::JSON.pretty_generate(result.generic_value)))
146
183
  end
@@ -176,6 +213,25 @@ module Bolt
176
213
  @stream.puts(colorize(:green, message))
177
214
  end
178
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
+
179
235
  def print_plan_start(event)
180
236
  @plan_depth += 1
181
237
  # We use this event to both mark the start of a plan _and_ to enable
@@ -218,11 +274,11 @@ module Bolt
218
274
  @stream.puts total_msg
219
275
  end
220
276
 
221
- def print_table(results, padding_left = 0, padding_right = 3)
277
+ def format_table(results, padding_left = 0, padding_right = 3)
222
278
  # lazy-load expensive gem code
223
279
  require 'terminal-table'
224
280
 
225
- @stream.puts Terminal::Table.new(
281
+ Terminal::Table.new(
226
282
  rows: results,
227
283
  style: {
228
284
  border_x: '',
@@ -238,94 +294,155 @@ module Bolt
238
294
 
239
295
  def print_tasks(tasks, modulepath)
240
296
  command = Bolt::Util.powershell? ? 'Get-BoltTask -Task <TASK NAME>' : 'bolt task show <TASK NAME>'
241
- tasks.any? ? print_table(tasks) : print_message('No available tasks')
242
- print_message("\nMODULEPATH:\n#{modulepath.join(File::PATH_SEPARATOR)}\n"\
243
- "\nUse '#{command}' to view "\
244
- "details and parameters for a specific task.")
297
+
298
+ tasks = tasks.map do |name, description|
299
+ description = truncate(description, 72)
300
+ [name, description]
301
+ end
302
+
303
+ @stream.puts colorize(:cyan, 'Tasks')
304
+ @stream.puts tasks.any? ? format_table(tasks, 2) : indent(2, 'No available tasks')
305
+ @stream.puts
306
+
307
+ @stream.puts colorize(:cyan, 'Modulepath')
308
+ @stream.puts indent(2, modulepath.join(File::PATH_SEPARATOR))
309
+ @stream.puts
310
+
311
+ @stream.puts colorize(:cyan, 'Additional information')
312
+ @stream.puts indent(2, "Use '#{command}' to view details and parameters for a specific task.")
245
313
  end
246
314
 
247
315
  # @param [Hash] task A hash representing the task
248
316
  def print_task_info(task)
249
- # Building lots of strings...
250
- pretty_params = +""
251
- task_info = +""
252
- usage = if Bolt::Util.powershell?
253
- +"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)
254
325
  else
255
- +"bolt task run #{task.name} --targets <targets>"
326
+ indent(2, 'No description')
256
327
  end
257
-
258
- task.parameters&.each do |k, v|
259
- pretty_params << "- #{k}: #{v['type'] || 'Any'}\n"
260
- pretty_params << " Default: #{v['default'].inspect}\n" if v.key?('default')
261
- pretty_params << " #{v['description']}\n" if v['description']
262
- usage << if v['type'].start_with?("Optional")
263
- " [#{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>]"
264
341
  else
265
- " #{k}=<value>"
342
+ " #{name}=<value>"
266
343
  end
267
344
  end
268
345
 
269
- if task.supports_noop
270
- 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
271
360
  end
272
361
 
273
- task_info << "\n#{task.name}"
274
- task_info << " - #{task.description}" if task.description
275
- task_info << "\n\n"
276
- task_info << "USAGE:\n#{usage}\n\n"
277
- task_info << "PARAMETERS:\n#{pretty_params}\n" unless pretty_params.empty?
278
- task_info << "MODULE:\n"
279
-
362
+ # Add module location
280
363
  path = task.files.first['path'].chomp("/tasks/#{task.files.first['name']}")
281
- task_info << if path.start_with?(Bolt::Config::Modulepath::MODULES_PATH)
282
- "built-in module"
283
- else
284
- path
285
- end
286
- @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
287
372
  end
288
373
 
289
374
  # @param [Hash] plan A hash representing the plan
290
375
  def print_plan_info(plan)
291
- # Building lots of strings...
292
- pretty_params = +""
293
- plan_info = +""
294
- usage = if Bolt::Util.powershell?
295
- +"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)
296
384
  else
297
- +"bolt plan run #{plan['name']}"
385
+ indent(2, 'No description')
298
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
299
399
 
300
- plan['parameters'].each do |name, p|
301
- pretty_params << "- #{name}: #{p['type']}\n"
302
- pretty_params << " Default: #{p['default_value']}\n" unless p['default_value'].nil?
303
- pretty_params << " #{p['description']}\n" if p['description']
304
- 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
305
415
  end
306
416
 
307
- plan_info << "\n#{plan['name']}"
308
- plan_info << " - #{plan['description']}" if plan['description']
309
- plan_info << "\n\n"
310
- plan_info << "USAGE:\n#{usage}\n\n"
311
- plan_info << "PARAMETERS:\n#{pretty_params}\n" unless plan['parameters'].empty?
312
- 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
313
424
 
314
- path = plan['module']
315
- plan_info << if path.start_with?(Bolt::Config::Modulepath::MODULES_PATH)
316
- "built-in module"
317
- else
318
- path
319
- end
320
- @stream.puts(plan_info)
425
+ @stream.puts info
321
426
  end
322
427
 
323
428
  def print_plans(plans, modulepath)
324
429
  command = Bolt::Util.powershell? ? 'Get-BoltPlan -Name <PLAN NAME>' : 'bolt plan show <PLAN NAME>'
325
- plans.any? ? print_table(plans) : print_message('No available plans')
326
- print_message("\nMODULEPATH:\n#{modulepath.join(File::PATH_SEPARATOR)}\n"\
327
- "\nUse '#{command}' to view "\
328
- "details and parameters for a specific plan.")
430
+
431
+ plans = plans.map do |name, description|
432
+ description = truncate(description, 72)
433
+ [name, description]
434
+ end
435
+
436
+ @stream.puts colorize(:cyan, 'Plans')
437
+ @stream.puts plans.any? ? format_table(plans, 2) : indent(2, 'No available plans')
438
+ @stream.puts
439
+
440
+ @stream.puts colorize(:cyan, 'Modulepath')
441
+ @stream.puts indent(2, modulepath.join(File::PATH_SEPARATOR))
442
+ @stream.puts
443
+
444
+ @stream.puts colorize(:cyan, 'Additional information')
445
+ @stream.puts indent(2, "Use '#{command}' to view details and parameters for a specific plan.")
329
446
  end
330
447
 
331
448
  def print_topics(topics)
@@ -359,7 +476,7 @@ module Bolt
359
476
  [m[:name], version]
360
477
  end
361
478
 
362
- print_table(module_info, 2, 1)
479
+ @stream.puts format_table(module_info, 2, 1)
363
480
  end
364
481
 
365
482
  @stream.write("\n")
@@ -373,29 +490,77 @@ module Bolt
373
490
  targets += target_list[:inventory].map { |target| [target.name, nil] }
374
491
  targets += target_list[:adhoc].map { |target| [target.name, adhoc] }
375
492
 
376
- if targets.any?
377
- print_table(targets, 0, 2)
378
- @stream.puts
379
- end
493
+ info = +''
380
494
 
381
- @stream.puts "INVENTORY FILE:"
382
- if File.exist?(inventoryfile)
383
- @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
+ @stream.puts info
505
+
506
+ print_inventory_summary(
507
+ target_list[:inventory].count,
508
+ target_list[:adhoc].count,
509
+ inventoryfile
510
+ )
511
+ end
512
+
513
+ def print_target_info(target_list, inventoryfile)
514
+ adhoc_targets = target_list[:adhoc].map(&:name).to_set
515
+ inventory_targets = target_list[:inventory].map(&:name).to_set
516
+ targets = target_list.values.flatten.sort_by(&:name)
517
+
518
+ info = +''
519
+
520
+ if targets.any?
521
+ adhoc = colorize(:yellow, " (Not found in inventory file)")
522
+
523
+ targets.each do |target|
524
+ info << colorize(:cyan, target.name)
525
+ info << adhoc if adhoc_targets.include?(target.name)
526
+ info << "\n"
527
+ info << indent(2, target.detail.to_yaml.lines.drop(1).join)
528
+ info << "\n"
529
+ end
384
530
  else
385
- @stream.puts wrap("Tried to load inventory from #{inventoryfile}, but the file does not exist")
531
+ info << colorize(:cyan, "Targets\n")
532
+ info << indent(2, "No targets\n\n")
386
533
  end
387
534
 
388
- @stream.puts "\nTARGET COUNT:"
389
- @stream.puts "#{targets.count} total, #{target_list[:inventory].count} from inventory, "\
390
- "#{target_list[:adhoc].count} adhoc"
391
- end
535
+ @stream.puts info
392
536
 
393
- def print_target_info(targets)
394
- @stream.puts ::JSON.pretty_generate(
395
- targets: targets.map(&:detail)
537
+ print_inventory_summary(
538
+ inventory_targets.count,
539
+ adhoc_targets.count,
540
+ inventoryfile
396
541
  )
397
- count = "#{targets.count} target#{'s' unless targets.count == 1}"
398
- @stream.puts colorize(:green, count)
542
+ end
543
+
544
+ private def print_inventory_summary(inventory_count, adhoc_count, inventoryfile)
545
+ info = +''
546
+
547
+ # Add inventory file source
548
+ info << colorize(:cyan, "Inventory file\n")
549
+ info << if File.exist?(inventoryfile)
550
+ indent(2, "#{inventoryfile}\n")
551
+ else
552
+ indent(2, wrap("Tried to load inventory from #{inventoryfile}, but the file does not exist\n"))
553
+ end
554
+ info << "\n"
555
+
556
+ # Add target count summary
557
+ count = "#{inventory_count + adhoc_count} total, "\
558
+ "#{inventory_count} from inventory, "\
559
+ "#{adhoc_count} adhoc"
560
+ info << colorize(:cyan, "Target count\n")
561
+ info << indent(2, count)
562
+
563
+ @stream.puts info
399
564
  end
400
565
 
401
566
  def print_groups(groups)
@@ -417,6 +582,10 @@ module Bolt
417
582
  @stream.puts("Plan completed successfully with no result")
418
583
  when Bolt::ApplyFailure, Bolt::RunFailure
419
584
  print_result_set(value.result_set)
585
+ when Bolt::ContainerResult
586
+ print_container_result(value)
587
+ when Bolt::ContainerFailure
588
+ print_container_result(value.result)
420
589
  when Bolt::ResultSet
421
590
  print_result_set(value)
422
591
  else