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
data/lib/bolt/task.rb CHANGED
@@ -148,7 +148,7 @@ module Bolt
148
148
 
149
149
  if unknown_keys.any?
150
150
  msg = "Metadata for task '#{@name}' contains unknown keys: #{unknown_keys.join(', ')}."
151
- msg += " This could be a typo in the task metadata or may result in incorrect behavior."
151
+ msg += " This could be a typo in the task metadata or might result in incorrect behavior."
152
152
  Bolt::Logger.warn("unknown_task_metadata_keys", msg)
153
153
  end
154
154
  end
@@ -2,7 +2,7 @@
2
2
 
3
3
  require 'json'
4
4
  require 'shellwords'
5
- require 'bolt/transport/base'
5
+ require 'bolt/transport/simple'
6
6
 
7
7
  module Bolt
8
8
  module Transport
@@ -34,6 +34,15 @@ module Bolt
34
34
  @container_info["Id"]
35
35
  end
36
36
 
37
+ def run_cmd(cmd, env_vars)
38
+ Bolt::Util.exec_docker(cmd, env_vars)
39
+ end
40
+
41
+ private def env_hash
42
+ # Set the DOCKER_HOST if we are using a non-default service-url
43
+ @docker_host.nil? ? {} : { 'DOCKER_HOST' => @docker_host }
44
+ end
45
+
37
46
  def connect
38
47
  # We don't actually have a connection, but we do need to
39
48
  # check that the container exists and is running.
@@ -54,10 +63,7 @@ module Bolt
54
63
  end
55
64
 
56
65
  def add_env_vars(env_vars)
57
- @env_vars = env_vars.each_with_object([]) do |env_var, acc|
58
- acc << "--env"
59
- acc << "#{env_var[0]}=#{env_var[1]}"
60
- end
66
+ @env_vars = Bolt::Util.format_env_vars_for_cli(env_vars)
61
67
  end
62
68
 
63
69
  # Executes a command inside the target container. This is called from the shell class.
@@ -88,9 +94,9 @@ module Bolt
88
94
 
89
95
  def upload_file(source, destination)
90
96
  @logger.trace { "Uploading #{source} to #{destination}" }
91
- _stdout, stderr, status = execute_local_command('cp', [source, "#{container_id}:#{destination}"])
92
- unless status.exitstatus.zero?
93
- raise "Error writing to container #{container_id}: #{stderr}"
97
+ _out, err, stat = run_cmd(['cp', source, "#{container_id}:#{destination}"], env_hash)
98
+ unless stat.exitstatus.zero?
99
+ raise "Error writing to container #{container_id}: #{err}"
94
100
  end
95
101
  rescue StandardError => e
96
102
  raise Bolt::Node::FileError.new(e.message, 'WRITE_ERROR')
@@ -102,31 +108,14 @@ module Bolt
102
108
  # copy the *contents* of the directory.
103
109
  # https://docs.docker.com/engine/reference/commandline/cp/
104
110
  FileUtils.mkdir_p(destination)
105
- _stdout, stderr, status = execute_local_command('cp', ["#{container_id}:#{source}", destination])
106
- unless status.exitstatus.zero?
107
- raise "Error downloading content from container #{container_id}: #{stderr}"
111
+ _out, err, stat = run_cmd(['cp', "#{container_id}:#{source}", destination], env_hash)
112
+ unless stat.exitstatus.zero?
113
+ raise "Error downloading content from container #{container_id}: #{err}"
108
114
  end
109
115
  rescue StandardError => e
110
116
  raise Bolt::Node::FileError.new(e.message, 'WRITE_ERROR')
111
117
  end
112
118
 
113
- # Executes a Docker CLI command. This is useful for running commands as
114
- # part of this class without having to go through the `execute`
115
- # function and manage pipes.
116
- #
117
- # @param subcommand [String] The docker subcommand to run
118
- # e.g. 'inspect' for `docker inspect`
119
- # @param arguments [Array] Arguments to pass to the docker command
120
- # e.g. 'src' and 'dest' for `docker cp <src> <dest>
121
- # @return [String, String, Process::Status] The output of the command: STDOUT, STDERR, Process Status
122
- private def execute_local_command(subcommand, arguments = [])
123
- # Set the DOCKER_HOST if we are using a non-default service-url
124
- env_hash = @docker_host.nil? ? {} : { 'DOCKER_HOST' => @docker_host }
125
- docker_command = [subcommand].concat(arguments)
126
-
127
- Open3.capture3(env_hash, 'docker', *docker_command, { binmode: true })
128
- end
129
-
130
119
  # Executes a Docker CLI command and parses the output in JSON format
131
120
  #
132
121
  # @param subcommand [String] The docker subcommand to run
@@ -134,15 +123,15 @@ module Bolt
134
123
  # @param arguments [Array] Arguments to pass to the docker command
135
124
  # e.g. 'src' and 'dest' for `docker cp <src> <dest>
136
125
  # @return [Object] Ruby object representation of the JSON string
137
- private def execute_local_json_command(subcommand, arguments = [])
138
- command_options = ['--format', '{{json .}}'].concat(arguments)
139
- stdout, _stderr, _status = execute_local_command(subcommand, command_options)
140
- extract_json(stdout)
126
+ def execute_local_json_command(subcommand, arguments = [])
127
+ cmd = [subcommand, '--format', '{{json .}}'].concat(arguments)
128
+ out, _err, _stat = run_cmd(cmd, env_hash)
129
+ extract_json(out)
141
130
  end
142
131
 
143
132
  # Converts the JSON encoded STDOUT string from the docker cli into ruby objects
144
133
  #
145
- # @param stdout_string [String] The string to convert
134
+ # @param stdout [String] The string to convert
146
135
  # @return [Object] Ruby object representation of the JSON string
147
136
  private def extract_json(stdout)
148
137
  # The output from the docker format command is a JSON string per line.
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bolt/logger'
4
+ require 'bolt/node/errors'
5
+ require 'bolt/transport/simple'
6
+
7
+ module Bolt
8
+ module Transport
9
+ class LXD < Simple
10
+ def provided_features
11
+ ['shell']
12
+ end
13
+
14
+ def with_connection(target, options = {})
15
+ Bolt::Logger.warn_once("lxd_experimental",
16
+ "The LXD transport is experimental, and might "\
17
+ "include breaking changes between minor versions.")
18
+ conn = Connection.new(target, options)
19
+ conn.connect
20
+ yield conn
21
+ end
22
+ end
23
+ end
24
+ end
25
+
26
+ require 'bolt/transport/lxd/connection'
@@ -0,0 +1,99 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'logging'
4
+ require 'bolt/node/errors'
5
+
6
+ module Bolt
7
+ module Transport
8
+ class LXD < Simple
9
+ class Connection
10
+ attr_reader :user, :target
11
+
12
+ def initialize(target, options)
13
+ raise Bolt::ValidationError, "Target #{target.safe_name} does not have a host" unless target.host
14
+
15
+ @target = target
16
+ @user = ENV['USER'] || Etc.getlogin
17
+ @options = options
18
+ @logger = Bolt::Logger.logger(target.safe_name)
19
+ @logger.trace("Initializing LXD connection to #{target.safe_name}")
20
+ end
21
+
22
+ def shell
23
+ Bolt::Shell::Bash.new(target, self)
24
+ end
25
+
26
+ def container_id
27
+ "#{@target.transport_config['remote']}:#{@target.host}"
28
+ end
29
+
30
+ def connect
31
+ out, err, status = execute_local_command(%W[list #{container_id} --format json])
32
+ unless status.exitstatus.zero?
33
+ raise "Error listing available containers: #{err}"
34
+ end
35
+ containers = JSON.parse(out)
36
+ if containers.empty?
37
+ raise "Could not find a container with name or ID matching '#{container_id}'"
38
+ end
39
+ @logger.trace("Opened session")
40
+ true
41
+ rescue StandardError => e
42
+ raise Bolt::Node::ConnectError.new(
43
+ "Failed to connect to #{container_id}: #{e.message}",
44
+ 'CONNECT_ERROR'
45
+ )
46
+ end
47
+
48
+ def add_env_vars(env_vars)
49
+ @env_vars = env_vars.each_with_object([]) do |env_var, acc|
50
+ acc << "--env"
51
+ acc << "#{env_var[0]}=#{Shellwords.shellescape(env_var[1])}"
52
+ end
53
+ end
54
+
55
+ def execute(command)
56
+ lxc_command = %w[lxc exec]
57
+ lxc_command += @env_vars if @env_vars
58
+ lxc_command += %W[#{container_id} -- sh -c #{Shellwords.shellescape(command)}]
59
+
60
+ @logger.trace { "Executing: #{lxc_command.join(' ')}" }
61
+ Open3.popen3(lxc_command.join(' '))
62
+ end
63
+
64
+ private def execute_local_command(command)
65
+ Open3.capture3('lxc', *command, { binmode: true })
66
+ end
67
+
68
+ def upload_file(source, destination)
69
+ @logger.trace { "Uploading #{source} to #{destination}" }
70
+ args = %w[--create-dirs]
71
+ if File.directory?(source)
72
+ args << '--recursive'
73
+ # If we don't do this, LXD will upload to
74
+ # /tmp/d2020-11/d2020-11/dir instead of /tmp/d2020-11/dir
75
+ destination = Pathname.new(destination).dirname.to_s
76
+ end
77
+ cmd = %w[file push] + args + %W[#{source} #{container_id}#{destination}]
78
+ _out, err, stat = execute_local_command(cmd)
79
+ unless stat.exitstatus.zero?
80
+ raise "Error writing to #{container_id}: #{err}"
81
+ end
82
+ rescue StandardError => e
83
+ raise Bolt::Node::FileError.new(e.message, 'WRITE_ERROR')
84
+ end
85
+
86
+ def download_file(source, destination, _download)
87
+ @logger.trace { "Downloading #{source} to #{destination}" }
88
+ FileUtils.mkdir_p(destination)
89
+ _out, err, stat = execute_local_command(%W[file pull --recursive #{container_id}#{source} #{destination}])
90
+ unless stat.exitstatus.zero?
91
+ raise "Error downloading content from container #{container_id}: #{err}"
92
+ end
93
+ rescue StandardError => e
94
+ raise Bolt::Node::FileError.new(e.message, 'WRITE_ERROR')
95
+ end
96
+ end
97
+ end
98
+ end
99
+ end
@@ -59,6 +59,18 @@ module Bolt
59
59
  # the result otherwise make sure an error is generated
60
60
  if state == 'finished' || (result && result['_error'])
61
61
  if result['_error']
62
+ unless result['_error'].is_a?(Hash)
63
+ result['_error'] = { 'kind' => 'puppetlabs.tasks/task-error',
64
+ 'issue_code' => 'TASK_ERROR',
65
+ 'msg' => result['_error'],
66
+ 'details' => {} }
67
+ end
68
+
69
+ result['_error']['details'] ||= {}
70
+ unless result['_error']['details'].is_a?(Hash)
71
+ deets = result['_error']['details']
72
+ result['_error']['details'] = { 'msg' => deets }
73
+ end
62
74
  file_line = %w[file line].zip(position).to_h.compact
63
75
  result['_error']['details'].merge!(file_line) unless result['_error']['details']['file']
64
76
  end
@@ -252,11 +264,7 @@ module Bolt
252
264
 
253
265
  # If we get here, there's no error so we don't need the file or line
254
266
  # number
255
- Bolt::Result.for_command(target,
256
- result.value['stdout'],
257
- result.value['stderr'],
258
- result.value['exit_code'],
259
- action, obj, [])
267
+ Bolt::Result.for_command(target, result.value, action, obj, [])
260
268
  end
261
269
  end
262
270
  end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+ require 'shellwords'
5
+ require 'bolt/transport/base'
6
+
7
+ module Bolt
8
+ module Transport
9
+ class Podman < Docker
10
+ def with_connection(target)
11
+ conn = Connection.new(target)
12
+ conn.connect
13
+ yield conn
14
+ end
15
+ end
16
+ end
17
+ end
18
+
19
+ require 'bolt/transport/podman/connection'
@@ -0,0 +1,98 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'logging'
4
+ require 'bolt/node/errors'
5
+
6
+ module Bolt
7
+ module Transport
8
+ class Podman < Docker
9
+ class Connection < Connection
10
+ attr_reader :user, :target
11
+
12
+ def initialize(target)
13
+ raise Bolt::ValidationError, "Target #{target.safe_name} does not have a host" unless target.host
14
+ @target = target
15
+ @user = ENV['USER'] || Etc.getlogin
16
+ @logger = Bolt::Logger.logger(target.safe_name)
17
+ @container_info = {}
18
+ @logger.trace("Initializing podman connection to #{target.safe_name}")
19
+ end
20
+
21
+ def run_cmd(cmd, env_vars)
22
+ Bolt::Util.exec_podman(cmd, env_vars)
23
+ end
24
+
25
+ def shell
26
+ @shell ||= if Bolt::Util.windows?
27
+ Bolt::Shell::Powershell.new(target, self)
28
+ else
29
+ Bolt::Shell::Bash.new(target, self)
30
+ end
31
+ end
32
+
33
+ def connect
34
+ # We don't actually have a connection, but we do need to
35
+ # check that the container exists and is running.
36
+ ps = execute_local_json_command('ps')
37
+ container = Array(ps).find { |item|
38
+ item["ID"].to_s.eql?(@target.host) ||
39
+ item["Id"].to_s.start_with?(@target.host) ||
40
+ Array(item["Names"]).include?(@target.host)
41
+ }
42
+ raise "Could not find a container with name or ID matching '#{@target.host}'" if container.nil?
43
+ # Now find the indepth container information
44
+ id = container["ID"] || container["Id"]
45
+ output = execute_local_json_command('inspect', [id])
46
+ # Store the container information for later
47
+ @container_info = output.first
48
+ @logger.trace { "Opened session" }
49
+ true
50
+ rescue StandardError => e
51
+ raise Bolt::Node::ConnectError.new(
52
+ "Failed to connect to #{target.safe_name}: #{e.message}",
53
+ 'CONNECT_ERROR'
54
+ )
55
+ end
56
+
57
+ # Executes a command inside the target container. This is called from the shell class.
58
+ #
59
+ # @param command [string] The command to run
60
+ def execute(command)
61
+ args = []
62
+ args += %w[--interactive]
63
+ args += %w[--tty] if target.options['tty']
64
+ args += @env_vars if @env_vars
65
+
66
+ if target.options['shell-command'] && !target.options['shell-command'].empty?
67
+ # escape any double quotes in command
68
+ command = command.gsub('"', '\"')
69
+ command = "#{target.options['shell-command']} \"#{command}\""
70
+ end
71
+
72
+ podman_command = %w[podman exec] + args + [container_id] + Shellwords.split(command)
73
+ @logger.trace { "Executing: #{podman_command.join(' ')}" }
74
+
75
+ Open3.popen3(*podman_command)
76
+ rescue StandardError
77
+ @logger.trace { "Command aborted" }
78
+ raise
79
+ end
80
+
81
+ # Converts the JSON encoded STDOUT string from the podman cli into ruby objects
82
+ #
83
+ # @param stdout [String] The string to convert
84
+ # @return [Object] Ruby object representation of the JSON string
85
+ private def extract_json(stdout)
86
+ # Podman renders the output in pretty JSON, which results in a newline
87
+ # appearing in the output before the closing bracket.
88
+ # should we only get a single line with no newline at all, we also
89
+ # assume it is a single minified JSON object
90
+ stdout.strip!
91
+ newline = stdout.index("\n") || -1
92
+ bracket = stdout.index('}') || -1
93
+ JSON.parse(stdout) if bracket > newline
94
+ end
95
+ end
96
+ end
97
+ end
98
+ end
@@ -230,7 +230,7 @@ module Bolt
230
230
  end
231
231
  [in_wr, out_rd, err_rd, th]
232
232
  rescue Errno::EMFILE => e
233
- msg = "#{e.message}. This may be resolved by increasing your user limit "\
233
+ msg = "#{e.message}. This might be resolved by increasing your user limit "\
234
234
  "with 'ulimit -n 1024'. See https://puppet.com/docs/bolt/latest/bolt_known_issues.html for details."
235
235
  raise Bolt::Error.new(msg, 'bolt/too-many-files')
236
236
  end
@@ -130,7 +130,7 @@ module Bolt
130
130
 
131
131
  [inp, out_rd, err_rd, th]
132
132
  rescue Errno::EMFILE => e
133
- msg = "#{e.message}. This may be resolved by increasing your user limit "\
133
+ msg = "#{e.message}. This might be resolved by increasing your user limit "\
134
134
  "with 'ulimit -n 1024'. See https://puppet.com/docs/bolt/latest/bolt_known_issues.html for details."
135
135
  raise Bolt::Error.new(msg, 'bolt/too-many-files')
136
136
  rescue StandardError
data/lib/bolt/util.rb CHANGED
@@ -77,6 +77,14 @@ module Bolt
77
77
  File.exist?(path) ? read_yaml_hash(path, file_name) : {}
78
78
  end
79
79
 
80
+ def first_runs_free
81
+ Bolt::Config.user_path + '.first_runs_free'
82
+ end
83
+
84
+ def first_run?
85
+ Bolt::Config.user_path && !File.exist?(first_runs_free)
86
+ end
87
+
80
88
  # Accepts a path with either 'plans' or 'tasks' in it and determines
81
89
  # the name of the module
82
90
  def module_name(path)
@@ -324,6 +332,40 @@ module Bolt
324
332
  end
325
333
  end
326
334
 
335
+ # Executes a Docker CLI command. This is useful for running commands as
336
+ # part of this class without having to go through the `execute`
337
+ # function and manage pipes.
338
+ #
339
+ # @param cmd [String] The docker command and arguments to run
340
+ # e.g. 'cp <src> <dest>' for `docker cp <src> <dest>`
341
+ # @return [String, String, Process::Status] The output of the command: STDOUT, STDERR, Process Status
342
+ def exec_docker(cmd, env = {})
343
+ Open3.capture3(env, 'docker', *cmd, { binmode: true })
344
+ end
345
+
346
+ # Executes a Podman CLI command. This is useful for running commands as
347
+ # part of this class without having to go through the `execute`
348
+ # function and manage pipes.
349
+ #
350
+ # @param cmd [String] The podman command and arguments to run
351
+ # e.g. 'cp <src> <dest>' for `podman cp <src> <dest>`
352
+ # @return [String, String, Process::Status] The output of the command: STDOUT, STDERR, Process Status
353
+ def exec_podman(cmd, env = {})
354
+ Open3.capture3(env, 'podman', *cmd, { binmode: true })
355
+ end
356
+
357
+ # Formats a map of environment variables to be passed to a command that
358
+ # accepts repeated `--env` flags
359
+ #
360
+ # @param env_vars [Hash] A map of environment variables keys and their values
361
+ # @return [String]
362
+ def format_env_vars_for_cli(env_vars)
363
+ @env_vars = env_vars.each_with_object([]) do |(key, value), acc|
364
+ acc << "--env"
365
+ acc << "#{key}=#{value}"
366
+ end
367
+ end
368
+
327
369
  def unix_basename(path)
328
370
  raise Bolt::ValidationError, "path must be a String, received #{path.class} #{path}" unless path.is_a?(String)
329
371
  path.split('/').last