bolt 3.4.0 → 3.7.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 (55) hide show
  1. checksums.yaml +4 -4
  2. data/Puppetfile +2 -2
  3. data/bolt-modules/boltlib/lib/puppet/datatypes/applyresult.rb +26 -0
  4. data/bolt-modules/boltlib/lib/puppet/datatypes/containerresult.rb +51 -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_container.rb +162 -0
  15. data/bolt-modules/boltlib/lib/puppet/functions/write_file.rb +1 -0
  16. data/bolt-modules/boltlib/types/planresult.pp +1 -0
  17. data/bolt-modules/ctrl/lib/puppet/functions/ctrl/do_until.rb +2 -0
  18. data/guides/guide.txt +17 -0
  19. data/guides/links.txt +13 -0
  20. data/guides/targets.txt +29 -0
  21. data/guides/transports.txt +23 -0
  22. data/lib/bolt/analytics.rb +4 -8
  23. data/lib/bolt/bolt_option_parser.rb +329 -225
  24. data/lib/bolt/cli.rb +58 -29
  25. data/lib/bolt/config.rb +11 -7
  26. data/lib/bolt/config/options.rb +41 -9
  27. data/lib/bolt/config/transport/podman.rb +33 -0
  28. data/lib/bolt/container_result.rb +105 -0
  29. data/lib/bolt/error.rb +15 -0
  30. data/lib/bolt/executor.rb +17 -13
  31. data/lib/bolt/inventory.rb +5 -4
  32. data/lib/bolt/inventory/inventory.rb +3 -2
  33. data/lib/bolt/inventory/options.rb +9 -0
  34. data/lib/bolt/inventory/target.rb +16 -0
  35. data/lib/bolt/module_installer/specs/git_spec.rb +10 -6
  36. data/lib/bolt/outputter/human.rb +242 -76
  37. data/lib/bolt/outputter/json.rb +6 -4
  38. data/lib/bolt/outputter/logger.rb +17 -0
  39. data/lib/bolt/pal/yaml_plan/step.rb +4 -2
  40. data/lib/bolt/plan_creator.rb +2 -2
  41. data/lib/bolt/plugin.rb +13 -11
  42. data/lib/bolt/puppetdb/client.rb +54 -0
  43. data/lib/bolt/result.rb +1 -4
  44. data/lib/bolt/shell/bash.rb +23 -10
  45. data/lib/bolt/transport/docker.rb +1 -1
  46. data/lib/bolt/transport/docker/connection.rb +23 -34
  47. data/lib/bolt/transport/podman.rb +19 -0
  48. data/lib/bolt/transport/podman/connection.rb +98 -0
  49. data/lib/bolt/transport/ssh/connection.rb +3 -6
  50. data/lib/bolt/util.rb +34 -0
  51. data/lib/bolt/version.rb +1 -1
  52. data/lib/bolt_server/transport_app.rb +3 -0
  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 +13 -2
data/lib/bolt/error.rb CHANGED
@@ -61,6 +61,21 @@ module Bolt
61
61
  end
62
62
  end
63
63
 
64
+ class ContainerFailure < Bolt::Error
65
+ attr_reader :result
66
+
67
+ def initialize(result)
68
+ details = {
69
+ 'value' => result.value,
70
+ 'object' => result.object
71
+ }
72
+ message = "Plan aborted: Running container '#{result.object}' failed."
73
+ super(message, 'bolt/container-failure', details)
74
+ @result = result
75
+ @error_code = 2
76
+ end
77
+ end
78
+
64
79
  class RunFailure < Bolt::Error
65
80
  attr_reader :result_set
66
81
 
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
@@ -217,7 +221,7 @@ module Bolt
217
221
  results
218
222
  end
219
223
 
220
- def report_transport(transport, count)
224
+ private def report_transport(transport, count)
221
225
  name = transport.class.name.split('::').last.downcase
222
226
  unless @reported_transports.include?(name)
223
227
  @analytics&.event('Transport', 'initialize', label: name, value: count)
@@ -475,7 +479,7 @@ module Bolt
475
479
  Time.now
476
480
  end
477
481
 
478
- def wait_until(timeout, retry_interval)
482
+ private def wait_until(timeout, retry_interval)
479
483
  start = wait_now
480
484
  until yield
481
485
  raise(TimeoutError, 'Timed out waiting for target') if (wait_now - start).to_i >= timeout
@@ -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
@@ -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",
@@ -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}:"))
@@ -180,6 +213,25 @@ module Bolt
180
213
  @stream.puts(colorize(:green, message))
181
214
  end
182
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
+
183
235
  def print_plan_start(event)
184
236
  @plan_depth += 1
185
237
  # We use this event to both mark the start of a plan _and_ to enable
@@ -241,7 +293,7 @@ module Bolt
241
293
  end
242
294
 
243
295
  def print_tasks(tasks, modulepath)
244
- command = Bolt::Util.powershell? ? 'Get-BoltTask -Task <TASK NAME>' : 'bolt task show <TASK NAME>'
296
+ command = Bolt::Util.powershell? ? 'Get-BoltTask -Name <TASK NAME>' : 'bolt task show <TASK NAME>'
245
297
 
246
298
  tasks = tasks.map do |name, description|
247
299
  description = truncate(description, 72)
@@ -262,78 +314,115 @@ module Bolt
262
314
 
263
315
  # @param [Hash] task A hash representing the task
264
316
  def print_task_info(task)
265
- # Building lots of strings...
266
- pretty_params = +""
267
- task_info = +""
268
- usage = if Bolt::Util.powershell?
269
- +"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)
270
325
  else
271
- +"bolt task run #{task.name} --targets <targets>"
326
+ indent(2, 'No description')
272
327
  end
273
-
274
- task.parameters&.each do |k, v|
275
- pretty_params << "- #{k}: #{v['type'] || 'Any'}\n"
276
- pretty_params << " Default: #{v['default'].inspect}\n" if v.key?('default')
277
- pretty_params << " #{v['description']}\n" if v['description']
278
- usage << if v['type']&.start_with?("Optional")
279
- " [#{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>]"
280
341
  else
281
- " #{k}=<value>"
342
+ " #{name}=<value>"
282
343
  end
283
344
  end
284
345
 
285
- if task.supports_noop
286
- 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
287
360
  end
288
361
 
289
- task_info << "\n#{task.name}"
290
- task_info << " - #{task.description}" if task.description
291
- task_info << "\n\n"
292
- task_info << "USAGE:\n#{usage}\n\n"
293
- task_info << "PARAMETERS:\n#{pretty_params}\n" unless pretty_params.empty?
294
- task_info << "MODULE:\n"
295
-
362
+ # Add module location
296
363
  path = task.files.first['path'].chomp("/tasks/#{task.files.first['name']}")
297
- task_info << if path.start_with?(Bolt::Config::Modulepath::MODULES_PATH)
298
- "built-in module"
299
- else
300
- path
301
- end
302
- @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
303
372
  end
304
373
 
305
374
  # @param [Hash] plan A hash representing the plan
306
375
  def print_plan_info(plan)
307
- # Building lots of strings...
308
- pretty_params = +""
309
- plan_info = +""
310
- usage = if Bolt::Util.powershell?
311
- +"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)
312
384
  else
313
- +"bolt plan run #{plan['name']}"
385
+ indent(2, 'No description')
314
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
399
+
400
+ # Add usage
401
+ info << colorize(:cyan, "Usage\n")
402
+ info << indent(2, wrap(usage))
403
+ info << "\n"
315
404
 
316
- plan['parameters'].each do |name, p|
317
- pretty_params << "- #{name}: #{p['type']}\n"
318
- pretty_params << " Default: #{p['default_value']}\n" unless p['default_value'].nil?
319
- pretty_params << " #{p['description']}\n" if p['description']
320
- usage << (p.include?('default_value') ? " [#{name}=<value>]" : " #{name}=<value>")
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
321
415
  end
322
416
 
323
- plan_info << "\n#{plan['name']}"
324
- plan_info << " - #{plan['description']}" if plan['description']
325
- plan_info << "\n\n"
326
- plan_info << "USAGE:\n#{usage}\n\n"
327
- plan_info << "PARAMETERS:\n#{pretty_params}\n" unless plan['parameters'].empty?
328
- 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
329
424
 
330
- path = plan['module']
331
- plan_info << if path.start_with?(Bolt::Config::Modulepath::MODULES_PATH)
332
- "built-in module"
333
- else
334
- path
335
- end
336
- @stream.puts(plan_info)
425
+ @stream.puts info
337
426
  end
338
427
 
339
428
  def print_plans(plans, modulepath)
@@ -394,42 +483,115 @@ module Bolt
394
483
  end
395
484
  end
396
485
 
397
- def print_targets(target_list, inventoryfile)
486
+ def print_targets(target_list, inventory_source, default_inventory, target_flag)
398
487
  adhoc = colorize(:yellow, "(Not found in inventory file)")
399
488
 
400
489
  targets = []
401
490
  targets += target_list[:inventory].map { |target| [target.name, nil] }
402
491
  targets += target_list[:adhoc].map { |target| [target.name, adhoc] }
403
492
 
404
- if targets.any?
405
- @stream.puts format_table(targets, 0, 2)
406
- @stream.puts
407
- end
493
+ info = +''
408
494
 
409
- @stream.puts "INVENTORY FILE:"
410
- if File.exist?(inventoryfile)
411
- @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
412
527
  else
413
- @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")
414
530
  end
415
531
 
416
- @stream.puts "\nTARGET COUNT:"
417
- @stream.puts "#{targets.count} total, #{target_list[:inventory].count} from inventory, "\
418
- "#{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
419
536
  end
420
537
 
421
- def print_target_info(targets)
422
- @stream.puts ::JSON.pretty_generate(
423
- targets: targets.map(&:detail)
424
- )
425
- count = "#{targets.count} target#{'s' unless targets.count == 1}"
426
- @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"
549
+ end
550
+
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
427
577
  end
428
578
 
429
- def print_groups(groups)
430
- count = "#{groups.count} group#{'s' unless groups.count == 1}"
431
- @stream.puts groups.join("\n")
432
- @stream.puts colorize(:green, count)
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
433
595
  end
434
596
 
435
597
  # @param [Bolt::ResultSet] apply_result A ResultSet object representing the result of a `bolt apply`
@@ -445,6 +607,10 @@ module Bolt
445
607
  @stream.puts("Plan completed successfully with no result")
446
608
  when Bolt::ApplyFailure, Bolt::RunFailure
447
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)
448
614
  when Bolt::ResultSet
449
615
  print_result_set(value)
450
616
  else