bolt 2.15.0 → 2.20.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 (96) hide show
  1. checksums.yaml +4 -4
  2. data/bolt-modules/boltlib/lib/puppet/functions/add_facts.rb +1 -0
  3. data/bolt-modules/boltlib/lib/puppet/functions/add_to_group.rb +1 -0
  4. data/bolt-modules/boltlib/lib/puppet/functions/apply_prep.rb +20 -9
  5. data/bolt-modules/boltlib/lib/puppet/functions/catch_errors.rb +1 -0
  6. data/bolt-modules/boltlib/lib/puppet/functions/download_file.rb +123 -0
  7. data/bolt-modules/boltlib/lib/puppet/functions/facts.rb +1 -0
  8. data/bolt-modules/boltlib/lib/puppet/functions/fail_plan.rb +1 -0
  9. data/bolt-modules/boltlib/lib/puppet/functions/get_resources.rb +1 -0
  10. data/bolt-modules/boltlib/lib/puppet/functions/get_target.rb +1 -0
  11. data/bolt-modules/boltlib/lib/puppet/functions/get_targets.rb +1 -0
  12. data/bolt-modules/boltlib/lib/puppet/functions/puppetdb_fact.rb +1 -0
  13. data/bolt-modules/boltlib/lib/puppet/functions/puppetdb_query.rb +1 -0
  14. data/bolt-modules/boltlib/lib/puppet/functions/remove_from_group.rb +1 -0
  15. data/bolt-modules/boltlib/lib/puppet/functions/resolve_references.rb +1 -0
  16. data/bolt-modules/boltlib/lib/puppet/functions/resource.rb +1 -0
  17. data/bolt-modules/boltlib/lib/puppet/functions/run_command.rb +3 -0
  18. data/bolt-modules/boltlib/lib/puppet/functions/run_plan.rb +2 -1
  19. data/bolt-modules/boltlib/lib/puppet/functions/run_script.rb +7 -4
  20. data/bolt-modules/boltlib/lib/puppet/functions/run_task.rb +2 -1
  21. data/bolt-modules/boltlib/lib/puppet/functions/set_config.rb +1 -0
  22. data/bolt-modules/boltlib/lib/puppet/functions/set_feature.rb +1 -0
  23. data/bolt-modules/boltlib/lib/puppet/functions/set_resources.rb +1 -0
  24. data/bolt-modules/boltlib/lib/puppet/functions/set_var.rb +1 -0
  25. data/bolt-modules/boltlib/lib/puppet/functions/upload_file.rb +1 -0
  26. data/bolt-modules/boltlib/lib/puppet/functions/vars.rb +1 -0
  27. data/bolt-modules/boltlib/lib/puppet/functions/wait_until_available.rb +1 -0
  28. data/bolt-modules/boltlib/lib/puppet/functions/without_default_logging.rb +1 -0
  29. data/bolt-modules/boltlib/lib/puppet/functions/write_file.rb +1 -0
  30. data/bolt-modules/ctrl/lib/puppet/functions/ctrl/do_until.rb +2 -0
  31. data/bolt-modules/ctrl/lib/puppet/functions/ctrl/sleep.rb +2 -0
  32. data/bolt-modules/file/lib/puppet/functions/file/exists.rb +1 -0
  33. data/bolt-modules/file/lib/puppet/functions/file/join.rb +2 -0
  34. data/bolt-modules/file/lib/puppet/functions/file/read.rb +2 -0
  35. data/bolt-modules/file/lib/puppet/functions/file/readable.rb +2 -0
  36. data/bolt-modules/file/lib/puppet/functions/file/write.rb +2 -0
  37. data/bolt-modules/out/lib/puppet/functions/out/message.rb +2 -0
  38. data/bolt-modules/prompt/lib/puppet/functions/prompt.rb +1 -0
  39. data/bolt-modules/system/lib/puppet/functions/system/env.rb +2 -0
  40. data/lib/bolt/applicator.rb +21 -15
  41. data/lib/bolt/apply_result.rb +1 -1
  42. data/lib/bolt/bolt_option_parser.rb +55 -20
  43. data/lib/bolt/catalog.rb +3 -2
  44. data/lib/bolt/cli.rb +116 -47
  45. data/lib/bolt/config.rb +48 -148
  46. data/lib/bolt/config/options.rb +488 -0
  47. data/lib/bolt/config/transport/base.rb +16 -16
  48. data/lib/bolt/config/transport/docker.rb +9 -23
  49. data/lib/bolt/config/transport/local.rb +6 -44
  50. data/lib/bolt/config/transport/options.rb +460 -0
  51. data/lib/bolt/config/transport/orch.rb +9 -18
  52. data/lib/bolt/config/transport/remote.rb +3 -6
  53. data/lib/bolt/config/transport/ssh.rb +74 -154
  54. data/lib/bolt/config/transport/winrm.rb +18 -47
  55. data/lib/bolt/executor.rb +15 -0
  56. data/lib/bolt/inventory/group.rb +4 -3
  57. data/lib/bolt/inventory/inventory.rb +4 -17
  58. data/lib/bolt/inventory/target.rb +18 -5
  59. data/lib/bolt/logger.rb +24 -1
  60. data/lib/bolt/outputter.rb +1 -1
  61. data/lib/bolt/outputter/rainbow.rb +14 -3
  62. data/lib/bolt/pal.rb +31 -11
  63. data/lib/bolt/pal/yaml_plan/evaluator.rb +19 -2
  64. data/lib/bolt/pal/yaml_plan/step.rb +11 -2
  65. data/lib/bolt/pal/yaml_plan/step/download.rb +38 -0
  66. data/lib/bolt/pal/yaml_plan/step/upload.rb +3 -3
  67. data/lib/bolt/plugin/module.rb +2 -4
  68. data/lib/bolt/plugin/puppetdb.rb +3 -2
  69. data/lib/bolt/project.rb +41 -44
  70. data/lib/bolt/puppetdb/client.rb +2 -0
  71. data/lib/bolt/puppetdb/config.rb +16 -0
  72. data/lib/bolt/result.rb +7 -0
  73. data/lib/bolt/shell/bash.rb +53 -45
  74. data/lib/bolt/shell/powershell.rb +23 -12
  75. data/lib/bolt/shell/powershell/snippets.rb +15 -6
  76. data/lib/bolt/transport/base.rb +24 -0
  77. data/lib/bolt/transport/docker.rb +17 -5
  78. data/lib/bolt/transport/docker/connection.rb +20 -2
  79. data/lib/bolt/transport/local/connection.rb +14 -1
  80. data/lib/bolt/transport/orch.rb +20 -0
  81. data/lib/bolt/transport/simple.rb +6 -0
  82. data/lib/bolt/transport/ssh.rb +7 -1
  83. data/lib/bolt/transport/ssh/connection.rb +9 -1
  84. data/lib/bolt/transport/ssh/exec_connection.rb +23 -2
  85. data/lib/bolt/transport/winrm/connection.rb +109 -8
  86. data/lib/bolt/util.rb +26 -11
  87. data/lib/bolt/version.rb +1 -1
  88. data/lib/bolt_server/transport_app.rb +3 -2
  89. data/lib/bolt_spec/bolt_context.rb +7 -2
  90. data/lib/bolt_spec/plans.rb +15 -2
  91. data/lib/bolt_spec/plans/action_stubs.rb +2 -1
  92. data/lib/bolt_spec/plans/action_stubs/download_stub.rb +66 -0
  93. data/lib/bolt_spec/plans/mock_executor.rb +14 -1
  94. data/lib/bolt_spec/run.rb +22 -0
  95. data/libexec/bolt_catalog +3 -2
  96. metadata +20 -29
@@ -96,6 +96,8 @@ module Bolt
96
96
  @http = HTTPClient.new
97
97
  @http.ssl_config.set_client_cert_file(@config.cert, @config.key) if @config.cert
98
98
  @http.ssl_config.add_trust_ca(@config.cacert)
99
+ @http.connect_timeout = @config.connect_timeout if @config.connect_timeout
100
+ @http.receive_timeout = @config.read_timeout if @config.read_timeout
99
101
 
100
102
  @http
101
103
  end
@@ -132,6 +132,22 @@ module Bolt
132
132
  end
133
133
  end
134
134
 
135
+ def connect_timeout
136
+ validate_timeout('connect_timeout')
137
+ @settings['connect_timeout']
138
+ end
139
+
140
+ def read_timeout
141
+ validate_timeout('read_timeout')
142
+ @settings['read_timeout']
143
+ end
144
+
145
+ def validate_timeout(timeout)
146
+ unless @settings[timeout].nil? || (@settings[timeout].is_a?(Integer) && @settings[timeout] > 0)
147
+ raise Bolt::PuppetDBError, "#{timeout} must be a positive integer, received #{@settings[timeout]}"
148
+ end
149
+ end
150
+
135
151
  def to_hash
136
152
  @settings.dup
137
153
  end
@@ -79,6 +79,13 @@ module Bolt
79
79
  new(target, message: "Uploaded '#{source}' to '#{target.host}:#{destination}'", action: 'upload', object: source)
80
80
  end
81
81
 
82
+ def self.for_download(target, source, destination, download)
83
+ msg = "Downloaded '#{target.host}:#{source}' to '#{destination}'"
84
+ value = { 'path' => download }
85
+
86
+ new(target, value: value, message: msg, action: 'download', object: source)
87
+ end
88
+
82
89
  # Satisfies the Puppet datatypes API
83
90
  def self.from_asserted_args(target, value)
84
91
  new(target, value: value)
@@ -23,7 +23,7 @@ module Bolt
23
23
 
24
24
  def run_command(command, options = {})
25
25
  running_as(options[:run_as]) do
26
- output = execute(command, sudoable: true)
26
+ output = execute(command, environment: options[:env_vars], sudoable: true)
27
27
  Bolt::Result.for_command(target,
28
28
  output.stdout.string,
29
29
  output.stderr.string,
@@ -35,9 +35,9 @@ module Bolt
35
35
  def upload(source, destination, options = {})
36
36
  running_as(options[:run_as]) do
37
37
  with_tmpdir do |dir|
38
- basename = File.basename(destination)
38
+ basename = File.basename(source)
39
39
  tmpfile = File.join(dir.to_s, basename)
40
- conn.copy_file(source, tmpfile)
40
+ conn.upload_file(source, tmpfile)
41
41
  # pass over file ownership if we're using run-as to be a different user
42
42
  dir.chown(run_as)
43
43
  result = execute(['mv', '-f', tmpfile, destination], sudoable: true)
@@ -50,6 +50,27 @@ module Bolt
50
50
  end
51
51
  end
52
52
 
53
+ def download(source, destination, options = {})
54
+ running_as(options[:run_as]) do
55
+ # Target OS may be either Unix or Windows. Without knowing the target OS before-hand
56
+ # we can't assume whether the path separator is '/' or '\'. Assume we're connecting
57
+ # to a target with Unix and then check if the path exists after downloading.
58
+ download = File.join(destination, Bolt::Util.unix_basename(source))
59
+
60
+ conn.download_file(source, destination, download)
61
+
62
+ # If the download path doesn't exist, then the file was likely downloaded from Windows
63
+ # using a source path with backslashes (e.g. 'C:\Users\Administrator\foo'). The file
64
+ # should be saved to the expected location, so update the download path assuming a
65
+ # Windows basename so the result shows the correct local path.
66
+ unless File.exist?(download)
67
+ download = File.join(destination, Bolt::Util.windows_basename(source))
68
+ end
69
+
70
+ Bolt::Result.for_download(target, source, destination, download)
71
+ end
72
+ end
73
+
53
74
  def run_script(script, arguments, options = {})
54
75
  # unpack any Sensitive data
55
76
  arguments = unwrap_sensitive_args(arguments)
@@ -58,7 +79,7 @@ module Bolt
58
79
  with_tmpdir do |dir|
59
80
  path = write_executable(dir.to_s, script)
60
81
  dir.chown(run_as)
61
- output = execute([path, *arguments], sudoable: true)
82
+ output = execute([path, *arguments], environment: options[:env_vars], sudoable: true)
62
83
  Bolt::Result.for_command(target,
63
84
  output.stdout.string,
64
85
  output.stderr.string,
@@ -95,7 +116,7 @@ module Bolt
95
116
  task_dir = File.join(dir.to_s, task.tasks_dir)
96
117
  dir.mkdirs([task.tasks_dir] + extra_files.map { |file| File.dirname(file['name']) })
97
118
  extra_files.each do |file|
98
- conn.copy_file(file['path'], File.join(dir.to_s, file['name']))
119
+ conn.upload_file(file['path'], File.join(dir.to_s, file['name']))
99
120
  end
100
121
  end
101
122
 
@@ -170,7 +191,7 @@ module Bolt
170
191
  def check_sudo(out, inp, stdin)
171
192
  buffer = out.readpartial(CHUNK_SIZE)
172
193
  # Split on newlines, including the newline
173
- lines = buffer.split(/(?<=[\n])/)
194
+ lines = buffer.split(/(?<=\n)/)
174
195
  # handle_sudo will return the line if it is not a sudo prompt or error
175
196
  lines.map! { |line| handle_sudo(inp, line, stdin) }
176
197
  lines.join("")
@@ -257,7 +278,7 @@ module Bolt
257
278
  def write_executable(dir, file, filename = nil)
258
279
  filename ||= File.basename(file)
259
280
  remote_path = File.join(dir.to_s, filename)
260
- conn.copy_file(file, remote_path)
281
+ conn.upload_file(file, remote_path)
261
282
  make_executable(remote_path)
262
283
  remote_path
263
284
  end
@@ -277,31 +298,8 @@ module Bolt
277
298
  end
278
299
  end
279
300
 
280
- # In the case where a task is run with elevated privilege and needs stdin
281
- # a random string is echoed to stderr indicating that the stdin is available
282
- # for task input data because the sudo password has already either been
283
- # provided on stdin or was not needed.
284
- def prepend_sudo_success(sudo_id, command_str)
285
- command_str = "cd; #{command_str}" if conn.reset_cwd?
286
- "sh -c #{Shellwords.shellescape("echo #{sudo_id} 1>&2; #{command_str}")}"
287
- end
288
-
289
- def prepend_chdir(command_str)
290
- "sh -c #{Shellwords.shellescape("cd; #{command_str}")}"
291
- end
292
-
293
- # A helper to build up a single string that contains all of the options for
294
- # privilege escalation. A wrapper script is used to direct task input to stdin
295
- # when a tty is allocated and thus we do not need to prepend_sudo_success when
296
- # using the wrapper or when the task does not require stdin data.
297
- def build_sudoable_command_str(command_str, sudo_str, sudo_id, options)
298
- if options[:stdin] && !options[:wrapper]
299
- "#{sudo_str} #{prepend_sudo_success(sudo_id, command_str)}"
300
- elsif conn.reset_cwd?
301
- "#{sudo_str} #{prepend_chdir(command_str)}"
302
- else
303
- "#{sudo_str} #{command_str}"
304
- end
301
+ def sudo_success(sudo_id)
302
+ "echo #{sudo_id} 1>&2"
305
303
  end
306
304
 
307
305
  # Returns string with the interpreter conditionally prepended
@@ -322,27 +320,37 @@ module Bolt
322
320
  escalate = sudoable && run_as && conn.user != run_as
323
321
  use_sudo = escalate && @target.options['run-as-command'].nil?
324
322
 
325
- command_str = inject_interpreter(options[:interpreter], command)
323
+ # Depending on the transport, whether we're using sudo and whether
324
+ # there are environment variables to set, we may need to stitch
325
+ # together multiple commands into a single sh invocation
326
+ commands = [inject_interpreter(options[:interpreter], command)]
326
327
 
327
328
  if options[:environment]
328
- env_decls = options[:environment].map do |env, val|
329
+ env_decl = options[:environment].map do |env, val|
329
330
  "#{env}=#{Shellwords.shellescape(val)}"
330
- end
331
- command_str = "#{env_decls.join(' ')} #{command_str}"
331
+ end.join(' ')
332
332
  end
333
333
 
334
334
  if escalate
335
- if use_sudo
336
- sudo_exec = target.options['sudo-executable'] || "sudo"
337
- sudo_flags = [sudo_exec, "-S", "-H", "-u", run_as, "-p", sudo_prompt]
338
- sudo_flags += ["-E"] if options[:environment]
339
- sudo_str = Shellwords.shelljoin(sudo_flags)
340
- else
341
- sudo_str = Shellwords.shelljoin(@target.options['run-as-command'] + [run_as])
342
- end
343
- command_str = build_sudoable_command_str(command_str, sudo_str, @sudo_id, options)
335
+ sudo_str = if use_sudo
336
+ sudo_exec = target.options['sudo-executable'] || "sudo"
337
+ sudo_flags = [sudo_exec, "-S", "-H", "-u", run_as, "-p", sudo_prompt]
338
+ Shellwords.shelljoin(sudo_flags)
339
+ else
340
+ Shellwords.shelljoin(@target.options['run-as-command'] + [run_as])
341
+ end
342
+ commands.unshift('cd') if conn.reset_cwd?
343
+ commands.unshift(sudo_success(@sudo_id)) if options[:stdin] && !options[:wrapper]
344
344
  end
345
345
 
346
+ command_str = if sudo_str || env_decl
347
+ "sh -c #{Shellwords.shellescape(commands.join('; '))}"
348
+ else
349
+ commands.last
350
+ end
351
+
352
+ command_str = [sudo_str, env_decl, command_str].compact.join(' ')
353
+
346
354
  @logger.debug { "Executing: #{command_str}" }
347
355
 
348
356
  in_buffer = if !use_sudo && options[:stdin]
@@ -71,8 +71,10 @@ module Bolt
71
71
  end
72
72
  end
73
73
 
74
- def set_env(arg, val)
75
- "[Environment]::SetEnvironmentVariable('#{arg}', @'\n#{val}\n'@)"
74
+ def env_declarations(env_vars)
75
+ env_vars.map do |var, val|
76
+ "[Environment]::SetEnvironmentVariable('#{var}', @'\n#{val}\n'@)"
77
+ end
76
78
  end
77
79
 
78
80
  def quote_string(string)
@@ -83,7 +85,7 @@ module Bolt
83
85
  filename ||= File.basename(file)
84
86
  validate_extensions(File.extname(filename))
85
87
  destination = "#{dir}\\#{filename}"
86
- conn.copy_file(file, destination)
88
+ conn.upload_file(file, destination)
87
89
  destination
88
90
  end
89
91
 
@@ -111,7 +113,8 @@ module Bolt
111
113
  end
112
114
 
113
115
  def mkdirs(dirs)
114
- mkdir_command = "mkdir -Force #{dirs.uniq.sort.join(',')}"
116
+ paths = dirs.uniq.sort.join('","')
117
+ mkdir_command = "mkdir -Force -Path (\"#{paths}\")"
115
118
  result = execute(mkdir_command)
116
119
  if result.exit_code != 0
117
120
  message = "Could not create directories: #{result.stderr.string}"
@@ -161,11 +164,19 @@ module Bolt
161
164
  end
162
165
 
163
166
  def upload(source, destination, _options = {})
164
- conn.copy_file(source, destination)
167
+ conn.upload_file(source, destination)
165
168
  Bolt::Result.for_upload(target, source, destination)
166
169
  end
167
170
 
168
- def run_command(command, _options = {})
171
+ def download(source, destination, _options = {})
172
+ download = File.join(destination, Bolt::Util.windows_basename(source))
173
+ conn.download_file(source, destination, download)
174
+ Bolt::Result.for_download(target, source, destination, download)
175
+ end
176
+
177
+ def run_command(command, options = {})
178
+ command = [*env_declarations(options[:env_vars]), command].join("\r\n") if options[:env_vars]
179
+
169
180
  output = execute(command)
170
181
  Bolt::Result.for_command(target,
171
182
  output.stdout.string,
@@ -174,7 +185,7 @@ module Bolt
174
185
  'command', command)
175
186
  end
176
187
 
177
- def run_script(script, arguments, _options = {})
188
+ def run_script(script, arguments, options = {})
178
189
  # unpack any Sensitive data
179
190
  arguments = unwrap_sensitive_args(arguments)
180
191
  with_tmpdir do |dir|
@@ -186,6 +197,8 @@ module Bolt
186
197
  args += escape_arguments(arguments)
187
198
  execute_process(path, args)
188
199
  end
200
+ command = [*env_declarations(options[:env_vars]), command].join("\r\n") if options[:env_vars]
201
+
189
202
  output = execute(command)
190
203
  Bolt::Result.for_command(target,
191
204
  output.stdout.string,
@@ -213,7 +226,7 @@ module Bolt
213
226
  task_dir = File.join(dir, task.tasks_dir)
214
227
  mkdirs([task_dir] + extra_files.map { |file| File.join(dir, File.dirname(file['name'])) })
215
228
  extra_files.each do |file|
216
- conn.copy_file(file['path'], File.join(dir, file['name']))
229
+ conn.upload_file(file['path'], File.join(dir, file['name']))
217
230
  end
218
231
  end
219
232
 
@@ -236,9 +249,7 @@ module Bolt
236
249
  end
237
250
 
238
251
  env_assignments = if Bolt::Task::ENVIRONMENT_METHODS.include?(input_method)
239
- envify_params(arguments).map do |(arg, val)|
240
- set_env(arg, val)
241
- end
252
+ env_declarations(envify_params(arguments))
242
253
  else
243
254
  []
244
255
  end
@@ -257,7 +268,7 @@ module Bolt
257
268
  return with_tmpdir do |dir|
258
269
  command += "\r\nif (!$?) { if($LASTEXITCODE) { exit $LASTEXITCODE } else { exit 1 } }"
259
270
  script_file = File.join(dir, "#{SecureRandom.uuid}_wrapper.ps1")
260
- conn.copy_file(StringIO.new(command), script_file)
271
+ conn.upload_file(StringIO.new(command), script_file)
261
272
  args = escape_arguments([script_file])
262
273
  script_invocation = ['powershell.exe', *PS_ARGS, '-File', *args].join(' ')
263
274
  execute(script_invocation)
@@ -85,12 +85,21 @@ module Bolt
85
85
 
86
86
  def shell_init
87
87
  <<~PS
88
- $ENV:PATH += ";${ENV:ProgramFiles}\\Puppet Labs\\Puppet\\bin\\;" +
89
- "${ENV:ProgramFiles}\\Puppet Labs\\Puppet\\puppet\\bin;" +
90
- "${ENV:ProgramFiles}\\Puppet Labs\\Puppet\\sys\\ruby\\bin\\"
91
- $ENV:RUBYLIB = "${ENV:ProgramFiles}\\Puppet Labs\\Puppet\\puppet\\lib;" +
92
- "${ENV:ProgramFiles}\\Puppet Labs\\Puppet\\facter\\lib;" +
93
- "${ENV:ProgramFiles}\\Puppet Labs\\Puppet\\hiera\\lib;" +
88
+ $installRegKey = Get-ItemProperty -Path "HKLM:\\Software\\Puppet Labs\\Puppet" -ErrorAction 0
89
+ if(![string]::IsNullOrEmpty($installRegKey.RememberedInstallDir64)){
90
+ $boltBaseDir = $installRegKey.RememberedInstallDir64
91
+ }elseif(![string]::IsNullOrEmpty($installRegKey.RememberedInstallDir)){
92
+ $boltBaseDir = $installRegKey.RememberedInstallDir
93
+ }else{
94
+ $boltBaseDir = "${ENV:ProgramFiles}\\Puppet Labs\\Puppet"
95
+ }
96
+
97
+ $ENV:PATH += ";${boltBaseDir}\\bin\\;" +
98
+ "${boltBaseDir}\\puppet\\bin;" +
99
+ "${boltBaseDir}\\sys\\ruby\\bin\\"
100
+ $ENV:RUBYLIB = "${boltBaseDir}\\puppet\\lib;" +
101
+ "${boltBaseDir}\\facter\\lib;" +
102
+ "${boltBaseDir}\\hiera\\lib;" +
94
103
  $ENV:RUBYLIB
95
104
 
96
105
  Add-Type -AssemblyName System.ServiceModel.Web, System.Runtime.Serialization
@@ -167,6 +167,25 @@ module Bolt
167
167
  end
168
168
  end
169
169
 
170
+ # Downloads the given source file from a batch of targets to the destination location
171
+ # on the host.
172
+ #
173
+ # The default implementation only supports batches of size 1 and will fail otherwise.
174
+ #
175
+ # Transports may override this method to implement their own batch processing.
176
+ def batch_download(targets, source, destination, options = {}, &callback)
177
+ require 'erb'
178
+
179
+ assert_batch_size_one("batch_download()", targets)
180
+ target = targets.first
181
+ with_events(target, callback, 'download') do
182
+ escaped_name = ERB::Util.url_encode(target.safe_name)
183
+ target_destination = File.expand_path(escaped_name, destination)
184
+ @logger.debug { "Downloading: '#{source}' on #{target.safe_name} to #{target_destination}" }
185
+ download(target, source, target_destination, options)
186
+ end
187
+ end
188
+
170
189
  def batch_connected?(targets)
171
190
  assert_batch_size_one("connected?()", targets)
172
191
  connected?(targets.first)
@@ -201,6 +220,11 @@ module Bolt
201
220
  raise NotImplementedError, "upload() must be implemented by the transport class"
202
221
  end
203
222
 
223
+ # Transports should override this method with their own implementation of file download.
224
+ def download(*_args)
225
+ raise NotImplementedError, "download() must be implemented by the transport class"
226
+ end
227
+
204
228
  # Transports should override this method with their own implementation of a connection test.
205
229
  def connected?(_targets)
206
230
  raise NotImplementedError, "connected?() must be implemented by the transport class"
@@ -20,7 +20,7 @@ module Bolt
20
20
  def upload(target, source, destination, _options = {})
21
21
  with_connection(target) do |conn|
22
22
  conn.with_remote_tmpdir do |dir|
23
- basename = File.basename(destination)
23
+ basename = File.basename(source)
24
24
  tmpfile = "#{dir}/#{basename}"
25
25
  if File.directory?(source)
26
26
  conn.write_remote_directory(source, tmpfile)
@@ -38,8 +38,18 @@ module Bolt
38
38
  end
39
39
  end
40
40
 
41
+ def download(target, source, destination, _options = {})
42
+ with_connection(target) do |conn|
43
+ download = File.join(destination, Bolt::Util.unix_basename(source))
44
+ conn.download_remote_content(source, destination)
45
+ Bolt::Result.for_download(target, source, destination, download)
46
+ end
47
+ end
48
+
41
49
  def run_command(target, command, options = {})
42
- options[:tty] = target.options['tty']
50
+ execute_options = {}
51
+ execute_options[:tty] = target.options['tty']
52
+ execute_options[:environment] = options[:env_vars]
43
53
 
44
54
  if target.options['shell-command'] && !target.options['shell-command'].empty?
45
55
  # escape any double quotes in command
@@ -47,19 +57,21 @@ module Bolt
47
57
  command = "#{target.options['shell-command']} \" #{command}\""
48
58
  end
49
59
  with_connection(target) do |conn|
50
- stdout, stderr, exitcode = conn.execute(*Shellwords.split(command), options)
60
+ stdout, stderr, exitcode = conn.execute(*Shellwords.split(command), execute_options)
51
61
  Bolt::Result.for_command(target, stdout, stderr, exitcode, 'command', command)
52
62
  end
53
63
  end
54
64
 
55
- def run_script(target, script, arguments, _options = {})
65
+ def run_script(target, script, arguments, options = {})
56
66
  # unpack any Sensitive data
57
67
  arguments = unwrap_sensitive_args(arguments)
68
+ execute_options = {}
69
+ execute_options[:environment] = options[:env_vars]
58
70
 
59
71
  with_connection(target) do |conn|
60
72
  conn.with_remote_tmpdir do |dir|
61
73
  remote_path = conn.write_remote_executable(dir, script)
62
- stdout, stderr, exitcode = conn.execute(remote_path, *arguments, {})
74
+ stdout, stderr, exitcode = conn.execute(remote_path, *arguments, execute_options)
63
75
  Bolt::Result.for_command(target, stdout, stderr, exitcode, 'script', script)
64
76
  end
65
77
  end
@@ -82,7 +82,9 @@ module Bolt
82
82
  def write_remote_file(source, destination)
83
83
  @logger.debug { "Uploading #{source}, to #{destination}" }
84
84
  _, stdout_str, status = execute_local_docker_command('cp', [source, "#{container_id}:#{destination}"])
85
- raise "Error writing file to container #{@container_id}: #{stdout_str}" unless status.exitstatus.zero?
85
+ unless status.exitstatus.zero?
86
+ raise "Error writing file to container #{@container_id}: #{stdout_str}"
87
+ end
86
88
  rescue StandardError => e
87
89
  raise Bolt::Node::FileError.new(e.message, 'WRITE_ERROR')
88
90
  end
@@ -90,7 +92,23 @@ module Bolt
90
92
  def write_remote_directory(source, destination)
91
93
  @logger.debug { "Uploading #{source}, to #{destination}" }
92
94
  _, stdout_str, status = execute_local_docker_command('cp', [source, "#{container_id}:#{destination}"])
93
- raise "Error writing directory to container #{@container_id}: #{stdout_str}" unless status.exitstatus.zero?
95
+ unless status.exitstatus.zero?
96
+ raise "Error writing directory to container #{@container_id}: #{stdout_str}"
97
+ end
98
+ rescue StandardError => e
99
+ raise Bolt::Node::FileError.new(e.message, 'WRITE_ERROR')
100
+ end
101
+
102
+ def download_remote_content(source, destination)
103
+ @logger.debug { "Downloading #{source} to #{destination}" }
104
+ # Create the destination directory, otherwise copying a source directory with Docker will
105
+ # copy the *contents* of the directory.
106
+ # https://docs.docker.com/engine/reference/commandline/cp/
107
+ FileUtils.mkdir_p(destination)
108
+ _, stdout_str, status = execute_local_docker_command('cp', ["#{container_id}:#{source}", destination])
109
+ unless status.exitstatus.zero?
110
+ raise "Error downloading content from container #{@container_id}: #{stdout_str}"
111
+ end
94
112
  rescue StandardError => e
95
113
  raise Bolt::Node::FileError.new(e.message, 'WRITE_ERROR')
96
114
  end