bolt 3.0.0 → 3.5.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 (76) hide show
  1. checksums.yaml +4 -4
  2. data/Puppetfile +13 -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/apply_result.rb +1 -1
  15. data/lib/bolt/bolt_option_parser.rb +6 -3
  16. data/lib/bolt/cli.rb +96 -16
  17. data/lib/bolt/config.rb +4 -0
  18. data/lib/bolt/config/options.rb +21 -3
  19. data/lib/bolt/config/transport/lxd.rb +23 -0
  20. data/lib/bolt/config/transport/options.rb +8 -1
  21. data/lib/bolt/container_result.rb +105 -0
  22. data/lib/bolt/error.rb +15 -0
  23. data/lib/bolt/executor.rb +22 -7
  24. data/lib/bolt/inventory/options.rb +9 -0
  25. data/lib/bolt/inventory/target.rb +16 -0
  26. data/lib/bolt/logger.rb +8 -0
  27. data/lib/bolt/module_installer.rb +2 -2
  28. data/lib/bolt/module_installer/puppetfile.rb +2 -2
  29. data/lib/bolt/module_installer/specs/forge_spec.rb +2 -2
  30. data/lib/bolt/module_installer/specs/git_spec.rb +2 -2
  31. data/lib/bolt/node/output.rb +14 -4
  32. data/lib/bolt/outputter/human.rb +106 -23
  33. data/lib/bolt/outputter/logger.rb +17 -0
  34. data/lib/bolt/pal.rb +25 -4
  35. data/lib/bolt/pal/yaml_plan.rb +1 -2
  36. data/lib/bolt/pal/yaml_plan/evaluator.rb +5 -141
  37. data/lib/bolt/pal/yaml_plan/step.rb +91 -31
  38. data/lib/bolt/pal/yaml_plan/step/command.rb +21 -13
  39. data/lib/bolt/pal/yaml_plan/step/download.rb +15 -16
  40. data/lib/bolt/pal/yaml_plan/step/eval.rb +11 -11
  41. data/lib/bolt/pal/yaml_plan/step/message.rb +13 -4
  42. data/lib/bolt/pal/yaml_plan/step/plan.rb +19 -15
  43. data/lib/bolt/pal/yaml_plan/step/resources.rb +82 -21
  44. data/lib/bolt/pal/yaml_plan/step/script.rb +36 -17
  45. data/lib/bolt/pal/yaml_plan/step/task.rb +19 -16
  46. data/lib/bolt/pal/yaml_plan/step/upload.rb +16 -17
  47. data/lib/bolt/pal/yaml_plan/transpiler.rb +3 -3
  48. data/lib/bolt/plan_creator.rb +1 -1
  49. data/lib/bolt/project_manager.rb +1 -1
  50. data/lib/bolt/project_manager/module_migrator.rb +1 -1
  51. data/lib/bolt/result.rb +11 -15
  52. data/lib/bolt/shell.rb +16 -0
  53. data/lib/bolt/shell/bash.rb +61 -31
  54. data/lib/bolt/shell/bash/tmpdir.rb +2 -2
  55. data/lib/bolt/shell/powershell.rb +34 -12
  56. data/lib/bolt/shell/powershell/snippets.rb +30 -3
  57. data/lib/bolt/task.rb +1 -1
  58. data/lib/bolt/transport/base.rb +0 -9
  59. data/lib/bolt/transport/docker.rb +1 -125
  60. data/lib/bolt/transport/docker/connection.rb +77 -167
  61. data/lib/bolt/transport/lxd.rb +26 -0
  62. data/lib/bolt/transport/lxd/connection.rb +99 -0
  63. data/lib/bolt/transport/orch.rb +13 -5
  64. data/lib/bolt/transport/ssh/connection.rb +1 -1
  65. data/lib/bolt/transport/winrm/connection.rb +1 -1
  66. data/lib/bolt/util.rb +31 -0
  67. data/lib/bolt/version.rb +1 -1
  68. data/lib/bolt_server/transport_app.rb +61 -33
  69. data/lib/bolt_spec/bolt_context.rb +9 -4
  70. data/lib/bolt_spec/plans.rb +1 -109
  71. data/lib/bolt_spec/plans/action_stubs.rb +1 -1
  72. data/lib/bolt_spec/plans/action_stubs/command_stub.rb +8 -1
  73. data/lib/bolt_spec/plans/action_stubs/script_stub.rb +8 -1
  74. data/lib/bolt_spec/plans/mock_executor.rb +90 -7
  75. data/modules/puppet_connect/plans/test_input_data.pp +65 -7
  76. metadata +9 -2
@@ -0,0 +1,23 @@
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 LXD < Base
10
+ OPTIONS = %w[
11
+ cleanup
12
+ remote
13
+ tmpdir
14
+ ].freeze
15
+
16
+ DEFAULTS = {
17
+ 'cleanup' => true,
18
+ 'remote' => 'local'
19
+ }.freeze
20
+ end
21
+ end
22
+ end
23
+ end
@@ -32,7 +32,7 @@ module Bolt
32
32
  "cleanup" => {
33
33
  type: [TrueClass, FalseClass],
34
34
  description: "Whether to clean up temporary files created on targets. When running commands on a target, "\
35
- "Bolt may create temporary files. After completing the command, these files are "\
35
+ "Bolt might create temporary files. After completing the command, these files are "\
36
36
  "automatically deleted. This value can be set to 'false' if you wish to leave these "\
37
37
  "temporary files on the target.",
38
38
  _plugin: true,
@@ -266,6 +266,13 @@ module Bolt
266
266
  _plugin: true,
267
267
  _example: "BOLT.PRODUCTION"
268
268
  },
269
+ "remote" => {
270
+ type: String,
271
+ description: "The LXD remote host to use.",
272
+ _default: "local",
273
+ _plugin: false,
274
+ _example: 'myremote'
275
+ },
269
276
  "run-as" => {
270
277
  type: String,
271
278
  description: "The user to run commands as after login. The run-as user must be different than the "\
@@ -0,0 +1,105 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+ require 'bolt/error'
5
+ require 'bolt/result'
6
+
7
+ module Bolt
8
+ class ContainerResult
9
+ attr_reader :value, :object
10
+
11
+ def self.from_exception(exception, exit_code, image, position: [])
12
+ details = Bolt::Result.create_details(position)
13
+ error = {
14
+ 'kind' => 'puppetlabs.tasks/container-error',
15
+ 'issue_code' => 'CONTAINER_ERROR',
16
+ 'msg' => "Error running container '#{image}': #{exception}",
17
+ 'details' => details
18
+ }
19
+ error['details']['exit_code'] = exit_code
20
+ ContainerResult.new({ '_error' => error }, object: image)
21
+ end
22
+
23
+ def _pcore_init_hash
24
+ { 'value' => @value,
25
+ 'object' => @image }
26
+ end
27
+
28
+ # First argument can't be named given the way that Puppet deserializes variables
29
+ def initialize(value = nil, object: nil)
30
+ @value = value || {}
31
+ @object = object
32
+ end
33
+
34
+ def eql?(other)
35
+ self.class == other.class &&
36
+ value == other.value
37
+ end
38
+ alias == eql?
39
+
40
+ def [](key)
41
+ value[key]
42
+ end
43
+
44
+ def to_json(opts = nil)
45
+ to_data.to_json(opts)
46
+ end
47
+ alias to_s to_json
48
+
49
+ # This is the value with all non-UTF-8 characters removed, suitable for
50
+ # printing or converting to JSON. It *should* only be possible to have
51
+ # non-UTF-8 characters in stdout/stderr keys as they are not allowed from
52
+ # tasks but we scrub the whole thing just in case.
53
+ def safe_value
54
+ Bolt::Util.walk_vals(value) do |val|
55
+ if val.is_a?(String)
56
+ # Replace invalid bytes with hex codes, ie. \xDE\xAD\xBE\xEF
57
+ val.scrub { |c| c.bytes.map { |b| "\\x" + b.to_s(16).upcase }.join }
58
+ else
59
+ val
60
+ end
61
+ end
62
+ end
63
+
64
+ def stdout
65
+ value['stdout']
66
+ end
67
+
68
+ def stderr
69
+ value['stderr']
70
+ end
71
+
72
+ def to_data
73
+ {
74
+ "object" => object,
75
+ "status" => status,
76
+ "value" => safe_value
77
+ }
78
+ end
79
+
80
+ def status
81
+ ok? ? 'success' : 'failure'
82
+ end
83
+
84
+ def ok?
85
+ error_hash.nil?
86
+ end
87
+ alias ok ok?
88
+ alias success? ok?
89
+
90
+ # This allows access to errors outside puppet compilation
91
+ # it should be prefered over error in bolt code
92
+ def error_hash
93
+ value['_error']
94
+ end
95
+
96
+ # Warning: This will fail outside of a compilation.
97
+ # Use error_hash inside bolt.
98
+ # Is it crazy for this to behave differently outside a compiler?
99
+ def error
100
+ if error_hash
101
+ Puppet::DataTypes::Error.from_asserted_hash(error_hash)
102
+ end
103
+ end
104
+ end
105
+ end
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
@@ -4,6 +4,7 @@
4
4
  require 'English'
5
5
  require 'json'
6
6
  require 'logging'
7
+ require 'pathname'
7
8
  require 'set'
8
9
  require 'bolt/analytics'
9
10
  require 'bolt/result'
@@ -15,6 +16,7 @@ require 'bolt/transport/ssh'
15
16
  require 'bolt/transport/winrm'
16
17
  require 'bolt/transport/orch'
17
18
  require 'bolt/transport/local'
19
+ require 'bolt/transport/lxd'
18
20
  require 'bolt/transport/docker'
19
21
  require 'bolt/transport/remote'
20
22
  require 'bolt/yarn'
@@ -25,6 +27,7 @@ module Bolt
25
27
  winrm: Bolt::Transport::WinRM,
26
28
  pcp: Bolt::Transport::Orch,
27
29
  local: Bolt::Transport::Local,
30
+ lxd: Bolt::Transport::LXD,
28
31
  docker: Bolt::Transport::Docker,
29
32
  remote: Bolt::Transport::Remote
30
33
  }.freeze
@@ -39,7 +42,6 @@ module Bolt
39
42
  modified_concurrency = false)
40
43
  # lazy-load expensive gem code
41
44
  require 'concurrent'
42
-
43
45
  @analytics = analytics
44
46
  @logger = Bolt::Logger.logger(self)
45
47
 
@@ -121,8 +123,8 @@ module Bolt
121
123
  def queue_execute(targets)
122
124
  if @warn_concurrency && targets.length > @concurrency
123
125
  @warn_concurrency = false
124
- msg = "The ulimit is low, which may cause file limit issues. Default concurrency has been set to "\
125
- "'#{@concurrency}' to mitigate those issues, which may cause Bolt to run slow. "\
126
+ msg = "The ulimit is low, which might cause file limit issues. Default concurrency has been set to "\
127
+ "'#{@concurrency}' to mitigate those issues, which might cause Bolt to run slow. "\
126
128
  "Disable this warning by configuring ulimit using 'ulimit -n <limit>' in your shell "\
127
129
  "configuration, or by configuring Bolt's concurrency. "\
128
130
  "See https://puppet.com/docs/bolt/latest/bolt_known_issues.html for details."
@@ -215,7 +217,7 @@ module Bolt
215
217
  results
216
218
  end
217
219
 
218
- def report_transport(transport, count)
220
+ private def report_transport(transport, count)
219
221
  name = transport.class.name.split('::').last.downcase
220
222
  unless @reported_transports.include?(name)
221
223
  @analytics&.event('Transport', 'initialize', label: name, value: count)
@@ -231,6 +233,11 @@ module Bolt
231
233
  @analytics.report_bundled_content(mode, name)
232
234
  end
233
235
 
236
+ def report_file_source(plan_function, source)
237
+ label = Pathname.new(source).absolute? ? 'absolute' : 'module'
238
+ @analytics&.event('Plan', plan_function, label: label)
239
+ end
240
+
234
241
  def report_apply(statement_count, resource_counts)
235
242
  data = { statement_count: statement_count }
236
243
 
@@ -468,7 +475,7 @@ module Bolt
468
475
  Time.now
469
476
  end
470
477
 
471
- def wait_until(timeout, retry_interval)
478
+ private def wait_until(timeout, retry_interval)
472
479
  start = wait_now
473
480
  until yield
474
481
  raise(TimeoutError, 'Timed out waiting for target') if (wait_now - start).to_i >= timeout
@@ -477,22 +484,30 @@ module Bolt
477
484
  end
478
485
 
479
486
  def prompt(prompt, options)
480
- @prompting = true
481
487
  unless $stdin.tty?
488
+ return options[:default] if options[:default]
482
489
  raise Bolt::Error.new('STDIN is not a tty, unable to prompt', 'bolt/no-tty-error')
483
490
  end
484
491
 
485
- $stderr.print("#{prompt}: ")
492
+ @prompting = true
493
+
494
+ if options[:default] && !options[:sensitive]
495
+ $stderr.print("#{prompt} [#{options[:default]}]: ")
496
+ else
497
+ $stderr.print("#{prompt}: ")
498
+ end
486
499
 
487
500
  value = if options[:sensitive]
488
501
  $stdin.noecho(&:gets).to_s.chomp
489
502
  else
490
503
  $stdin.gets.to_s.chomp
491
504
  end
505
+
492
506
  @prompting = false
493
507
 
494
508
  $stderr.puts if options[:sensitive]
495
509
 
510
+ value = options[:default] if value.empty?
496
511
  value
497
512
  end
498
513
 
@@ -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
@@ -53,10 +53,21 @@ module Bolt
53
53
  string.sub(/\s\z/, '')
54
54
  end
55
55
 
56
+ # Wraps a string to the specified width. Lines only wrap
57
+ # at whitespace.
58
+ #
56
59
  def wrap(string, width = 80)
60
+ return string unless string.is_a?(String)
57
61
  string.gsub(/(.{1,#{width}})(\s+|\Z)/, "\\1\n")
58
62
  end
59
63
 
64
+ # Trims a string to a specified width, adding an ellipsis if it's longer.
65
+ #
66
+ def truncate(string, width = 80)
67
+ return string unless string.is_a?(String) && string.length > width
68
+ string.lines.first[0...width].gsub(/\s\w+\s*$/, '...')
69
+ end
70
+
60
71
  def handle_event(event)
61
72
  case event[:type]
62
73
  when :enable_default_output
@@ -81,6 +92,10 @@ module Bolt
81
92
  print_plan_start(event)
82
93
  when :plan_finish
83
94
  print_plan_finish(event)
95
+ when :container_start
96
+ print_container_start(event) if plan_logging?
97
+ when :container_finish
98
+ print_container_finish(event) if plan_logging?
84
99
  when :start_spin
85
100
  start_spin
86
101
  when :stop_spin
@@ -101,6 +116,34 @@ module Bolt
101
116
  @stream.puts(colorize(:green, "Started on #{target.safe_name}..."))
102
117
  end
103
118
 
119
+ def print_container_result(result)
120
+ if result.success?
121
+ @stream.puts(colorize(:green, "Finished running container #{result.object}:"))
122
+ else
123
+ @stream.puts(colorize(:red, "Failed running container #{result.object}:"))
124
+ end
125
+
126
+ if result.error_hash
127
+ @stream.puts(colorize(:red, remove_trail(indent(2, result.error_hash['msg']))))
128
+ return 0
129
+ end
130
+
131
+ # Only print results if there's something other than empty string and hash
132
+ safe_value = result.safe_value
133
+ if safe_value['stdout'].strip.empty? && safe_value['stderr'].strip.empty?
134
+ @stream.puts(indent(2, "Running container #{result.object} completed successfully with no result"))
135
+ else
136
+ unless safe_value['stdout'].strip && 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
144
+ end
145
+ end
146
+
104
147
  def print_result(result)
105
148
  if result.success?
106
149
  @stream.puts(colorize(:green, "Finished on #{result.target.safe_name}:"))
@@ -131,16 +174,9 @@ module Bolt
131
174
  end
132
175
 
133
176
  # Use special handling if the result looks like a command or script result
134
- if result.generic_value.keys == %w[stdout stderr exit_code]
177
+ if result.generic_value.keys == %w[stdout stderr merged_output exit_code]
135
178
  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
179
+ @stream.puts(indent(2, safe_value['merged_output'])) unless safe_value['merged_output'].strip.empty?
144
180
  elsif result.generic_value.any?
145
181
  @stream.puts(indent(2, ::JSON.pretty_generate(result.generic_value)))
146
182
  end
@@ -176,6 +212,25 @@ module Bolt
176
212
  @stream.puts(colorize(:green, message))
177
213
  end
178
214
 
215
+ def print_container_start(image:, **_kwargs)
216
+ @stream.puts(colorize(:green, "Starting: run container '#{image}'"))
217
+ end
218
+
219
+ def print_container_finish(event)
220
+ result = if event[:result].is_a?(Bolt::ContainerFailure)
221
+ event[:result].result
222
+ else
223
+ event[:result]
224
+ end
225
+
226
+ if result.success?
227
+ @stream.puts(colorize(:green, "Finished: run container '#{result.object}' succeeded."))
228
+ else
229
+ @stream.puts(colorize(:red, "Finished: run container '#{result.object}' failed."))
230
+ end
231
+ print_container_result(result) if @verbose
232
+ end
233
+
179
234
  def print_plan_start(event)
180
235
  @plan_depth += 1
181
236
  # We use this event to both mark the start of a plan _and_ to enable
@@ -218,11 +273,11 @@ module Bolt
218
273
  @stream.puts total_msg
219
274
  end
220
275
 
221
- def print_table(results, padding_left = 0, padding_right = 3)
276
+ def format_table(results, padding_left = 0, padding_right = 3)
222
277
  # lazy-load expensive gem code
223
278
  require 'terminal-table'
224
279
 
225
- @stream.puts Terminal::Table.new(
280
+ Terminal::Table.new(
226
281
  rows: results,
227
282
  style: {
228
283
  border_x: '',
@@ -238,10 +293,22 @@ module Bolt
238
293
 
239
294
  def print_tasks(tasks, modulepath)
240
295
  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.")
296
+
297
+ tasks = tasks.map do |name, description|
298
+ description = truncate(description, 72)
299
+ [name, description]
300
+ end
301
+
302
+ @stream.puts colorize(:cyan, 'Tasks')
303
+ @stream.puts tasks.any? ? format_table(tasks, 2) : indent(2, 'No available tasks')
304
+ @stream.puts
305
+
306
+ @stream.puts colorize(:cyan, 'Modulepath')
307
+ @stream.puts indent(2, modulepath.join(File::PATH_SEPARATOR))
308
+ @stream.puts
309
+
310
+ @stream.puts colorize(:cyan, 'Additional information')
311
+ @stream.puts indent(2, "Use '#{command}' to view details and parameters for a specific task.")
245
312
  end
246
313
 
247
314
  # @param [Hash] task A hash representing the task
@@ -259,7 +326,7 @@ module Bolt
259
326
  pretty_params << "- #{k}: #{v['type'] || 'Any'}\n"
260
327
  pretty_params << " Default: #{v['default'].inspect}\n" if v.key?('default')
261
328
  pretty_params << " #{v['description']}\n" if v['description']
262
- usage << if v['type'].start_with?("Optional")
329
+ usage << if v['type']&.start_with?("Optional")
263
330
  " [#{k}=<value>]"
264
331
  else
265
332
  " #{k}=<value>"
@@ -267,7 +334,7 @@ module Bolt
267
334
  end
268
335
 
269
336
  if task.supports_noop
270
- usage << Bolt::Util.powershell? ? '[-Noop]' : '[--noop]'
337
+ usage << (Bolt::Util.powershell? ? ' [-Noop]' : ' [--noop]')
271
338
  end
272
339
 
273
340
  task_info << "\n#{task.name}"
@@ -322,10 +389,22 @@ module Bolt
322
389
 
323
390
  def print_plans(plans, modulepath)
324
391
  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.")
392
+
393
+ plans = plans.map do |name, description|
394
+ description = truncate(description, 72)
395
+ [name, description]
396
+ end
397
+
398
+ @stream.puts colorize(:cyan, 'Plans')
399
+ @stream.puts plans.any? ? format_table(plans, 2) : indent(2, 'No available plans')
400
+ @stream.puts
401
+
402
+ @stream.puts colorize(:cyan, 'Modulepath')
403
+ @stream.puts indent(2, modulepath.join(File::PATH_SEPARATOR))
404
+ @stream.puts
405
+
406
+ @stream.puts colorize(:cyan, 'Additional information')
407
+ @stream.puts indent(2, "Use '#{command}' to view details and parameters for a specific plan.")
329
408
  end
330
409
 
331
410
  def print_topics(topics)
@@ -359,7 +438,7 @@ module Bolt
359
438
  [m[:name], version]
360
439
  end
361
440
 
362
- print_table(module_info, 2, 1)
441
+ @stream.puts format_table(module_info, 2, 1)
363
442
  end
364
443
 
365
444
  @stream.write("\n")
@@ -374,7 +453,7 @@ module Bolt
374
453
  targets += target_list[:adhoc].map { |target| [target.name, adhoc] }
375
454
 
376
455
  if targets.any?
377
- print_table(targets, 0, 2)
456
+ @stream.puts format_table(targets, 0, 2)
378
457
  @stream.puts
379
458
  end
380
459
 
@@ -417,6 +496,10 @@ module Bolt
417
496
  @stream.puts("Plan completed successfully with no result")
418
497
  when Bolt::ApplyFailure, Bolt::RunFailure
419
498
  print_result_set(value.result_set)
499
+ when Bolt::ContainerResult
500
+ print_container_result(value)
501
+ when Bolt::ContainerFailure
502
+ print_container_result(value.result)
420
503
  when Bolt::ResultSet
421
504
  print_result_set(value)
422
505
  else