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
@@ -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:
@@ -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
@@ -203,12 +194,17 @@ module Bolt
203
194
  end
204
195
 
205
196
  def to_data
197
+ serialized_value = safe_value
198
+ if serialized_value.key?('_sensitive') &&
199
+ serialized_value['_sensitive'].is_a?(Puppet::Pops::Types::PSensitiveType::Sensitive)
200
+ serialized_value['_sensitive'] = serialized_value['_sensitive'].to_s
201
+ end
206
202
  {
207
203
  "target" => @target.name,
208
204
  "action" => action,
209
205
  "object" => object,
210
206
  "status" => status,
211
- "value" => safe_value
207
+ "value" => serialized_value
212
208
  }
213
209
  end
214
210
 
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}")
@@ -331,10 +345,15 @@ module Bolt
331
345
  # together multiple commands into a single sh invocation
332
346
  commands = [inject_interpreter(options[:interpreter], command)]
333
347
 
348
+ # Let the transport handle adding environment variables if it's custom.
334
349
  if options[:environment]
335
- env_decl = options[:environment].map do |env, val|
336
- "#{env}=#{Shellwords.shellescape(val)}"
337
- end.join(' ')
350
+ if defined? conn.add_env_vars
351
+ conn.add_env_vars(options[:environment])
352
+ else
353
+ env_decl = options[:environment].map do |env, val|
354
+ "#{env}=#{Shellwords.shellescape(val)}"
355
+ end.join(' ')
356
+ end
338
357
  end
339
358
 
340
359
  if escalate
@@ -385,14 +404,24 @@ module Bolt
385
404
  # See if we can read from out or err, or write to in
386
405
  ready_read, ready_write, = select(read_streams.keys, write_stream, nil, timeout)
387
406
 
388
- # Read from out and err
389
407
  ready_read&.each do |stream|
408
+ stream_name = stream == out ? 'out' : 'err'
390
409
  # Check for sudo prompt
391
- read_streams[stream] << if use_sudo
392
- check_sudo(stream, inp, options[:stdin])
393
- else
394
- stream.readpartial(CHUNK_SIZE)
395
- end
410
+ to_print = if use_sudo
411
+ check_sudo(stream, inp, options[:stdin])
412
+ else
413
+ stream.readpartial(CHUNK_SIZE)
414
+ end
415
+
416
+ if !to_print.chomp.empty? && @stream_logger
417
+ formatted = to_print.lines.map do |msg|
418
+ "[#{@target.safe_name}] #{stream_name}: #{msg.chomp}"
419
+ end.join("\n")
420
+ @stream_logger.warn(formatted)
421
+ end
422
+
423
+ read_streams[stream] << to_print
424
+ result_output.merged_output << to_print
396
425
  rescue EOFError
397
426
  end
398
427
 
@@ -440,9 +469,10 @@ module Bolt
440
469
  when 0
441
470
  @logger.trace { "Command `#{command_str}` returned successfully" }
442
471
  when 126
443
- msg = "\n\nThis may be caused by the default tmpdir being mounted "\
472
+ msg = "\n\nThis might be caused by the default tmpdir being mounted "\
444
473
  "using 'noexec'. See http://pup.pt/task-failure for details and workarounds."
445
- result_output.stderr << msg
474
+ result_output.stderr << msg
475
+ result_output.merged_output << msg
446
476
  @logger.trace { "Command #{command_str} failed with exit code #{result_output.exit_code}" }
447
477
  else
448
478
  @logger.trace { "Command #{command_str} failed with exit code #{result_output.exit_code}" }
@@ -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)
@@ -274,7 +275,12 @@ module Bolt
274
275
  []
275
276
  end
276
277
 
277
- output = execute([Snippets.shell_init, *env_assignments, command].join("\n"))
278
+ output = execute([
279
+ Snippets.shell_init,
280
+ Snippets.append_ps_module_path(dir),
281
+ *env_assignments,
282
+ command
283
+ ].join("\n"))
278
284
 
279
285
  Bolt::Result.for_task(target, output.stdout.string,
280
286
  output.stderr.string,
@@ -305,12 +311,28 @@ module Bolt
305
311
  # the proper encoding so the string isn't later misinterpreted
306
312
  encoding = out.external_encoding
307
313
  out.binmode
308
- 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
309
323
  end
310
324
  stderr = Thread.new do
311
325
  encoding = err.external_encoding
312
326
  err.binmode
313
- 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
314
336
  end
315
337
 
316
338
  stdout.join