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
@@ -5,31 +5,30 @@ module Bolt
5
5
  class YamlPlan
6
6
  class Step
7
7
  class Upload < Step
8
- def self.allowed_keys
9
- super + Set['destination', 'upload']
8
+ def self.option_keys
9
+ Set['catch_errors', 'run_as']
10
10
  end
11
11
 
12
12
  def self.required_keys
13
- Set['upload', 'destination', 'targets']
13
+ Set['destination', 'targets', 'upload']
14
14
  end
15
15
 
16
- def initialize(step_body)
17
- super
18
- @source = step_body['upload']
19
- @destination = step_body['destination']
20
- end
21
-
22
- def transpile
23
- code = String.new(" ")
24
- code << "$#{@name} = " if @name
16
+ # Returns an array of arguments to pass to the step's function call
17
+ #
18
+ private def format_args(body)
19
+ opts = format_options(body)
25
20
 
26
- fn = 'upload_file'
27
- args = [@source, @destination, @targets]
28
- args << @description if @description
21
+ args = [body['upload'], body['destination'], body['targets']]
22
+ args << body['description'] if body['description']
23
+ args << opts if opts.any?
29
24
 
30
- code << function_call(fn, args)
25
+ args
26
+ end
31
27
 
32
- code << "\n"
28
+ # Returns the function corresponding to the step
29
+ #
30
+ private def function
31
+ 'upload_file'
33
32
  end
34
33
  end
35
34
  end
@@ -14,8 +14,8 @@ module Bolt
14
14
  end
15
15
  end
16
16
 
17
- def transpile(relative_path)
18
- @plan_path = File.expand_path(relative_path)
17
+ def transpile(plan_path)
18
+ @plan_path = plan_path
19
19
  @modulename = Bolt::Util.module_name(@plan_path)
20
20
  @filename = @plan_path.split(File::SEPARATOR)[-1]
21
21
  validate_path
@@ -29,7 +29,7 @@ module Bolt
29
29
 
30
30
  plan_string = String.new('')
31
31
  plan_string << "# #{plan_object.description}\n" if plan_object.description
32
- plan_string << "# WARNING: This is an autogenerated plan. It may not behave as expected.\n"
32
+ plan_string << "# WARNING: This is an autogenerated plan. It might not behave as expected.\n"
33
33
  plan_string << "# @private #{plan_object.private}\n" unless plan_object.private.nil?
34
34
  plan_string << "#{param_descriptions}\n" unless param_descriptions.empty?
35
35
 
@@ -22,7 +22,7 @@ module Bolt
22
22
  Invalid plan name '#{plan_name}'. Plan names are composed of one or more name segments
23
23
  separated by double colons '::'.
24
24
 
25
- Each name segment must begin with a lowercase letter, and may only include lowercase
25
+ Each name segment must begin with a lowercase letter, and can only include lowercase
26
26
  letters, digits, and underscores.
27
27
 
28
28
  Examples of valid plan names:
data/lib/bolt/plugin.rb CHANGED
@@ -178,8 +178,6 @@ module Bolt
178
178
  end
179
179
 
180
180
  def add_ruby_plugin(plugin_name)
181
- raise PluginError::LoadingDisabled, plugin_name unless @load_plugins
182
-
183
181
  cls_name = Bolt::Util.snake_name_to_class_name(plugin_name)
184
182
  filename = "bolt/plugin/#{plugin_name}"
185
183
  require filename
@@ -203,8 +201,6 @@ module Bolt
203
201
  }
204
202
 
205
203
  mod = modules[plugin_name]
206
- raise PluginError::Unknown, plugin_name unless mod&.plugin?
207
- raise PluginError::LoadingDisabled, plugin_name unless @load_plugins
208
204
 
209
205
  plugin = Bolt::Plugin::Module.load(mod, opts)
210
206
  add_plugin(plugin)
@@ -224,6 +220,12 @@ module Bolt
224
220
  end
225
221
  end
226
222
 
223
+ def known_plugin?(plugin_name)
224
+ @plugins.include?(plugin_name) ||
225
+ RUBY_PLUGINS.include?(plugin_name) ||
226
+ (modules.include?(plugin_name) && modules[plugin_name].plugin?)
227
+ end
228
+
227
229
  def get_hook(plugin_name, hook)
228
230
  plugin = by_name(plugin_name)
229
231
  raise PluginError::Unknown, plugin_name unless plugin
@@ -235,16 +237,16 @@ module Bolt
235
237
 
236
238
  # Calling by_name or get_hook will load any module based plugin automatically
237
239
  def by_name(plugin_name)
238
- return @plugins[plugin_name] if @plugins.include?(plugin_name)
239
- begin
240
- if RUBY_PLUGINS.include?(plugin_name)
240
+ if known_plugin?(plugin_name)
241
+ if @plugins.include?(plugin_name)
242
+ @plugins[plugin_name]
243
+ elsif !@load_plugins
244
+ raise PluginError::LoadingDisabled, plugin_name
245
+ elsif RUBY_PLUGINS.include?(plugin_name)
241
246
  add_ruby_plugin(plugin_name)
242
- elsif !@unknown.include?(plugin_name)
247
+ else
243
248
  add_module_plugin(plugin_name)
244
249
  end
245
- rescue PluginError::Unknown
246
- @unknown << plugin_name
247
- nil
248
250
  end
249
251
  end
250
252
 
@@ -143,7 +143,7 @@ module Bolt
143
143
  @outputter.print_message("Migrating project #{@config.project.path}\n\n")
144
144
 
145
145
  @outputter.print_action_step(
146
- "Migrating a Bolt project may make irreversible changes to the project's "\
146
+ "Migrating a Bolt project might make irreversible changes to the project's "\
147
147
  "configuration and inventory files. Before continuing, make sure the "\
148
148
  "project has a backup or uses a version control system."
149
149
  )
@@ -66,7 +66,7 @@ module Bolt
66
66
  # Attempt to resolve dependencies
67
67
  begin
68
68
  @outputter.print_message('')
69
- @outputter.print_action_step("Resolving module dependencies, this may take a moment")
69
+ @outputter.print_action_step("Resolving module dependencies, this might take a moment")
70
70
  puppetfile = Bolt::ModuleInstaller::Resolver.new.resolve(specs)
71
71
  rescue Bolt::Error => e
72
72
  @outputter.print_action_error("#{e.message}\nSkipping module migration.")
data/lib/bolt/result.rb CHANGED
@@ -28,20 +28,14 @@ module Bolt
28
28
  %w[file line].zip(position).to_h.compact
29
29
  end
30
30
 
31
- def self.for_command(target, stdout, stderr, exit_code, action, command, position)
32
- value = {
33
- 'stdout' => stdout,
34
- 'stderr' => stderr,
35
- 'exit_code' => exit_code
36
- }
37
-
31
+ def self.for_command(target, value, action, command, position)
38
32
  details = create_details(position)
39
- unless exit_code == 0
40
- details['exit_code'] = exit_code
33
+ unless value['exit_code'] == 0
34
+ details['exit_code'] = value['exit_code']
41
35
  value['_error'] = {
42
36
  'kind' => 'puppetlabs.tasks/command-error',
43
37
  'issue_code' => 'COMMAND_ERROR',
44
- 'msg' => "The command failed with exit code #{exit_code}",
38
+ 'msg' => "The command failed with exit code #{value['exit_code']}",
45
39
  'details' => details
46
40
  }
47
41
  end
@@ -170,15 +164,12 @@ module Bolt
170
164
  target == other.target &&
171
165
  value == other.value
172
166
  end
167
+ alias == eql?
173
168
 
174
169
  def [](key)
175
170
  value[key]
176
171
  end
177
172
 
178
- def ==(other)
179
- eql?(other)
180
- end
181
-
182
173
  def to_json(opts = nil)
183
174
  to_data.to_json(opts)
184
175
  end
data/lib/bolt/shell.rb CHANGED
@@ -8,6 +8,22 @@ module Bolt
8
8
  @target = target
9
9
  @conn = conn
10
10
  @logger = Bolt::Logger.logger(@target.safe_name)
11
+
12
+ if Bolt::Logger.stream
13
+ Bolt::Logger.warn_once("stream_experimental",
14
+ "The 'stream' option is experimental, and might "\
15
+ "include breaking changes between minor versions.")
16
+ @stream_logger = Bolt::Logger.logger(:stream)
17
+ # Don't send stream messages to the parent logger
18
+ @stream_logger.additive = false
19
+
20
+ # Log stream messages without any other data or color
21
+ pattern = Logging.layouts.pattern(pattern: '%m\n')
22
+ @stream_logger.appenders = Logging.appenders.stdout(
23
+ 'console',
24
+ layout: pattern
25
+ )
26
+ end
11
27
  end
12
28
 
13
29
  def run_command(*_args)
@@ -12,7 +12,6 @@ module Bolt
12
12
  super
13
13
 
14
14
  @run_as = nil
15
-
16
15
  @sudo_id = SecureRandom.uuid
17
16
  @sudo_password = @target.options['sudo-password'] || @target.password
18
17
  end
@@ -25,9 +24,7 @@ module Bolt
25
24
  running_as(options[:run_as]) do
26
25
  output = execute(command, environment: options[:env_vars], sudoable: true)
27
26
  Bolt::Result.for_command(target,
28
- output.stdout.string,
29
- output.stderr.string,
30
- output.exit_code,
27
+ output.to_h,
31
28
  'command',
32
29
  command,
33
30
  position)
@@ -54,19 +51,35 @@ module Bolt
54
51
 
55
52
  def download(source, destination, options = {})
56
53
  running_as(options[:run_as]) do
57
- # Target OS may be either Unix or Windows. Without knowing the target OS before-hand
58
- # we can't assume whether the path separator is '/' or '\'. Assume we're connecting
59
- # to a target with Unix and then check if the path exists after downloading.
60
54
  download = File.join(destination, Bolt::Util.unix_basename(source))
61
55
 
62
- conn.download_file(source, destination, download)
56
+ # If using run-as, the file is copied to a tmpdir and chowned to the
57
+ # connecting user. This is a workaround for limitations in net-ssh that
58
+ # only allow for downloading files as the connecting user, which is a
59
+ # problem for users who cannot connect to targets as the root user.
60
+ # This temporary copy should *always* be deleted.
61
+ if run_as
62
+ with_tmpdir(force_cleanup: true) do |dir|
63
+ tmpfile = File.join(dir.to_s, Bolt::Util.unix_basename(source))
64
+
65
+ result = execute(['cp', '-r', source, dir.to_s], sudoable: true)
66
+
67
+ if result.exit_code != 0
68
+ message = "Could not copy file '#{source}' to temporary directory '#{dir}': #{result.stderr.string}"
69
+ raise Bolt::Node::FileError.new(message, 'CP_ERROR')
70
+ end
63
71
 
64
- # If the download path doesn't exist, then the file was likely downloaded from Windows
65
- # using a source path with backslashes (e.g. 'C:\Users\Administrator\foo'). The file
66
- # should be saved to the expected location, so update the download path assuming a
67
- # Windows basename so the result shows the correct local path.
68
- unless File.exist?(download)
69
- download = File.join(destination, Bolt::Util.windows_basename(source))
72
+ # We need to force the chown, otherwise this will just return
73
+ # without doing anything since the chown user is the same as the
74
+ # connecting user.
75
+ dir.chown(conn.user, force: true)
76
+
77
+ conn.download_file(tmpfile, destination, download)
78
+ end
79
+ # If not using run-as, we can skip creating a temporary copy and just
80
+ # download the file directly.
81
+ else
82
+ conn.download_file(source, destination, download)
70
83
  end
71
84
 
72
85
  Bolt::Result.for_download(target, source, destination, download)
@@ -83,9 +96,7 @@ module Bolt
83
96
  dir.chown(run_as)
84
97
  output = execute([path, *arguments], environment: options[:env_vars], sudoable: true)
85
98
  Bolt::Result.for_command(target,
86
- output.stdout.string,
87
- output.stderr.string,
88
- output.exit_code,
99
+ output.to_h,
89
100
  'script',
90
101
  script,
91
102
  position)
@@ -134,18 +145,21 @@ module Bolt
134
145
 
135
146
  remote_task_path = write_executable(task_dir, executable)
136
147
 
148
+ execute_options[:stdin] = stdin
149
+
137
150
  # Avoid the horrors of passing data on stdin via a tty on multiple platforms
138
151
  # by writing a wrapper script that directs stdin to the task.
139
152
  if stdin && target.options['tty']
140
153
  wrapper = make_wrapper_stringio(remote_task_path, stdin, execute_options[:interpreter])
154
+ # Wrapper script handles interpreter and stdin. Delete these execute options
141
155
  execute_options.delete(:interpreter)
156
+ execute_options.delete(:stdin)
142
157
  execute_options[:wrapper] = true
143
158
  remote_task_path = write_executable(dir, wrapper, 'wrapper.sh')
144
159
  end
145
160
 
146
161
  dir.chown(run_as)
147
162
 
148
- execute_options[:stdin] = stdin
149
163
  execute_options[:sudoable] = true if run_as
150
164
  output = execute(remote_task_path, **execute_options)
151
165
  end
@@ -291,12 +305,12 @@ module Bolt
291
305
 
292
306
  # A helper to create and delete a tmpdir on the remote system. Yields the
293
307
  # directory name.
294
- def with_tmpdir
308
+ def with_tmpdir(force_cleanup: false)
295
309
  dir = make_tmpdir
296
310
  yield dir
297
311
  ensure
298
312
  if dir
299
- if target.options['cleanup']
313
+ if target.options['cleanup'] || force_cleanup
300
314
  dir.delete
301
315
  else
302
316
  Bolt::Logger.warn("skip_cleanup", "Skipping cleanup of tmpdir #{dir}")
@@ -382,7 +396,12 @@ module Bolt
382
396
  # See if there's a sudo prompt
383
397
  if use_sudo
384
398
  ready_read = select([err], nil, nil, timeout * 5)
385
- read_streams[err] << check_sudo(err, inp, options[:stdin]) if ready_read
399
+ to_print = check_sudo(err, inp, options[:stdin]) if ready_read
400
+ unless to_print.nil?
401
+ log_stream(to_print, 'err')
402
+ read_streams[err] << to_print
403
+ result_output.merged_output << to_print
404
+ end
386
405
  end
387
406
 
388
407
  # True while the process is running or waiting for IO input
@@ -390,14 +409,17 @@ module Bolt
390
409
  # See if we can read from out or err, or write to in
391
410
  ready_read, ready_write, = select(read_streams.keys, write_stream, nil, timeout)
392
411
 
393
- # Read from out and err
394
412
  ready_read&.each do |stream|
413
+ stream_name = stream == out ? 'out' : 'err'
395
414
  # Check for sudo prompt
396
- read_streams[stream] << if use_sudo
397
- check_sudo(stream, inp, options[:stdin])
398
- else
399
- stream.readpartial(CHUNK_SIZE)
400
- end
415
+ to_print = if use_sudo
416
+ check_sudo(stream, inp, options[:stdin])
417
+ else
418
+ stream.readpartial(CHUNK_SIZE)
419
+ end
420
+ log_stream(to_print, stream_name)
421
+ read_streams[stream] << to_print
422
+ result_output.merged_output << to_print
401
423
  rescue EOFError
402
424
  end
403
425
 
@@ -434,7 +456,13 @@ module Bolt
434
456
  # Read any remaining data in the pipe. Do not wait for
435
457
  # EOF in case the pipe is inherited by a child process.
436
458
  read_streams.each do |stream, _|
437
- loop { read_streams[stream] << stream.read_nonblock(CHUNK_SIZE) }
459
+ stream_name = stream == out ? 'out' : 'err'
460
+ loop {
461
+ to_print = stream.read_nonblock(CHUNK_SIZE)
462
+ log_stream(to_print, stream_name)
463
+ read_streams[stream] << to_print
464
+ result_output.merged_output << to_print
465
+ }
438
466
  rescue Errno::EAGAIN, EOFError
439
467
  end
440
468
  result_output.stdout << read_streams[out]
@@ -445,9 +473,10 @@ module Bolt
445
473
  when 0
446
474
  @logger.trace { "Command `#{command_str}` returned successfully" }
447
475
  when 126
448
- msg = "\n\nThis may be caused by the default tmpdir being mounted "\
476
+ msg = "\n\nThis might be caused by the default tmpdir being mounted "\
449
477
  "using 'noexec'. See http://pup.pt/task-failure for details and workarounds."
450
- result_output.stderr << msg
478
+ result_output.stderr << msg
479
+ result_output.merged_output << msg
451
480
  @logger.trace { "Command #{command_str} failed with exit code #{result_output.exit_code}" }
452
481
  else
453
482
  @logger.trace { "Command #{command_str} failed with exit code #{result_output.exit_code}" }
@@ -464,6 +493,15 @@ module Bolt
464
493
  def sudo_prompt
465
494
  '[sudo] Bolt needs to run as another user, password: '
466
495
  end
496
+
497
+ private def log_stream(to_print, stream_name)
498
+ if !to_print.chomp.empty? && @stream_logger
499
+ formatted = to_print.lines.map do |msg|
500
+ "[#{@target.safe_name}] #{stream_name}: #{msg.chomp}"
501
+ end.join("\n")
502
+ @stream_logger.warn(formatted)
503
+ end
504
+ end
467
505
  end
468
506
  end
469
507
  end
@@ -24,8 +24,8 @@ module Bolt
24
24
  end
25
25
  end
26
26
 
27
- def chown(owner)
28
- return if owner.nil? || owner == @owner
27
+ def chown(owner, force: false)
28
+ return if owner.nil? || (owner == @owner && !force)
29
29
 
30
30
  result = @shell.execute(['id', '-g', owner])
31
31
  if result.exit_code != 0
@@ -195,9 +195,7 @@ module Bolt
195
195
  wrap_command = conn.is_a?(Bolt::Transport::Local::Connection)
196
196
  output = execute(command, wrap_command)
197
197
  Bolt::Result.for_command(target,
198
- output.stdout.string,
199
- output.stderr.string,
200
- output.exit_code,
198
+ output.to_h,
201
199
  'command',
202
200
  command,
203
201
  position)
@@ -208,20 +206,23 @@ module Bolt
208
206
  arguments = unwrap_sensitive_args(arguments)
209
207
  with_tmpdir do |dir|
210
208
  script_path = write_executable(dir, script)
211
- command = if powershell_file?(script_path)
209
+ command = if powershell_file?(script_path) && options[:pwsh_params]
210
+ # Scripts run with pwsh_params can be run like tasks
211
+ Snippets.ps_task(script_path, options[:pwsh_params])
212
+ elsif powershell_file?(script_path)
212
213
  Snippets.run_script(arguments, script_path)
213
214
  else
214
215
  path, args = *process_from_extension(script_path)
215
216
  args += escape_arguments(arguments)
216
217
  execute_process(path, args)
217
218
  end
218
- command = [*env_declarations(options[:env_vars]), command].join("\r\n") if options[:env_vars]
219
+ env_assignments = options[:env_vars] ? env_declarations(options[:env_vars]) : []
220
+ shell_init = options[:pwsh_params] ? Snippets.shell_init : ''
221
+
222
+ output = execute([shell_init, *env_assignments, command].join("\r\n"))
219
223
 
220
- output = execute(command)
221
224
  Bolt::Result.for_command(target,
222
- output.stdout.string,
223
- output.stderr.string,
224
- output.exit_code,
225
+ output.to_h,
225
226
  'script',
226
227
  script,
227
228
  position)
@@ -310,12 +311,28 @@ module Bolt
310
311
  # the proper encoding so the string isn't later misinterpreted
311
312
  encoding = out.external_encoding
312
313
  out.binmode
313
- result.stdout << out.read.force_encoding(encoding)
314
+ to_print = out.read.force_encoding(encoding)
315
+ if !to_print.chomp.empty? && @stream_logger
316
+ formatted = to_print.lines.map do |msg|
317
+ "[#{@target.safe_name}] out: #{msg.chomp}"
318
+ end.join("\n")
319
+ @stream_logger.warn(formatted)
320
+ end
321
+ result.stdout << to_print
322
+ result.merged_output << to_print
314
323
  end
315
324
  stderr = Thread.new do
316
325
  encoding = err.external_encoding
317
326
  err.binmode
318
- result.stderr << err.read.force_encoding(encoding)
327
+ to_print = err.read.force_encoding(encoding)
328
+ if !to_print.chomp.empty? && @stream_logger
329
+ formatted = to_print.lines.map do |msg|
330
+ "[#{@target.safe_name}] err: #{msg.chomp}"
331
+ end.join("\n")
332
+ @stream_logger.warn(formatted)
333
+ end
334
+ result.stderr << to_print
335
+ result.merged_output << to_print
319
336
  end
320
337
 
321
338
  stdout.join