bolt 2.44.0 → 3.4.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 (80) hide show
  1. checksums.yaml +4 -4
  2. data/Puppetfile +11 -9
  3. data/bolt-modules/boltlib/lib/puppet/functions/add_facts.rb +1 -1
  4. data/bolt-modules/boltlib/lib/puppet/functions/apply_prep.rb +25 -0
  5. data/bolt-modules/boltlib/lib/puppet/functions/run_command.rb +20 -2
  6. data/bolt-modules/boltlib/lib/puppet/functions/run_plan.rb +2 -2
  7. data/bolt-modules/boltlib/lib/puppet/functions/run_script.rb +44 -5
  8. data/bolt-modules/boltlib/lib/puppet/functions/upload_file.rb +1 -1
  9. data/bolt-modules/boltlib/lib/puppet/functions/wait_until_available.rb +7 -3
  10. data/bolt-modules/file/lib/puppet/functions/file/read.rb +3 -2
  11. data/bolt-modules/prompt/lib/puppet/functions/prompt.rb +20 -2
  12. data/bolt-modules/prompt/lib/puppet/functions/prompt/menu.rb +103 -0
  13. data/lib/bolt/apply_result.rb +1 -1
  14. data/lib/bolt/bolt_option_parser.rb +9 -123
  15. data/lib/bolt/cli.rb +125 -127
  16. data/lib/bolt/config.rb +39 -214
  17. data/lib/bolt/config/options.rb +34 -125
  18. data/lib/bolt/config/transport/local.rb +1 -0
  19. data/lib/bolt/config/transport/lxd.rb +23 -0
  20. data/lib/bolt/config/transport/options.rb +9 -2
  21. data/lib/bolt/executor.rb +20 -5
  22. data/lib/bolt/logger.rb +9 -1
  23. data/lib/bolt/module_installer.rb +2 -2
  24. data/lib/bolt/module_installer/puppetfile.rb +2 -2
  25. data/lib/bolt/module_installer/specs/forge_spec.rb +2 -2
  26. data/lib/bolt/module_installer/specs/git_spec.rb +2 -2
  27. data/lib/bolt/node/output.rb +14 -4
  28. data/lib/bolt/outputter/human.rb +52 -24
  29. data/lib/bolt/outputter/json.rb +16 -16
  30. data/lib/bolt/pal.rb +26 -5
  31. data/lib/bolt/pal/yaml_plan.rb +1 -2
  32. data/lib/bolt/pal/yaml_plan/evaluator.rb +5 -153
  33. data/lib/bolt/pal/yaml_plan/step.rb +91 -52
  34. data/lib/bolt/pal/yaml_plan/step/command.rb +21 -13
  35. data/lib/bolt/pal/yaml_plan/step/download.rb +15 -16
  36. data/lib/bolt/pal/yaml_plan/step/eval.rb +11 -11
  37. data/lib/bolt/pal/yaml_plan/step/message.rb +13 -4
  38. data/lib/bolt/pal/yaml_plan/step/plan.rb +19 -15
  39. data/lib/bolt/pal/yaml_plan/step/resources.rb +82 -21
  40. data/lib/bolt/pal/yaml_plan/step/script.rb +36 -17
  41. data/lib/bolt/pal/yaml_plan/step/task.rb +19 -16
  42. data/lib/bolt/pal/yaml_plan/step/upload.rb +16 -17
  43. data/lib/bolt/pal/yaml_plan/transpiler.rb +3 -3
  44. data/lib/bolt/plan_creator.rb +1 -1
  45. data/lib/bolt/plugin/module.rb +0 -23
  46. data/lib/bolt/plugin/puppet_connect_data.rb +45 -3
  47. data/lib/bolt/project.rb +16 -56
  48. data/lib/bolt/project_manager.rb +5 -4
  49. data/lib/bolt/project_manager/module_migrator.rb +7 -6
  50. data/lib/bolt/result.rb +10 -11
  51. data/lib/bolt/shell.rb +16 -0
  52. data/lib/bolt/shell/bash.rb +61 -31
  53. data/lib/bolt/shell/bash/tmpdir.rb +2 -2
  54. data/lib/bolt/shell/powershell.rb +35 -14
  55. data/lib/bolt/shell/powershell/snippets.rb +37 -150
  56. data/lib/bolt/task.rb +1 -1
  57. data/lib/bolt/transport/base.rb +0 -9
  58. data/lib/bolt/transport/docker.rb +1 -125
  59. data/lib/bolt/transport/docker/connection.rb +86 -161
  60. data/lib/bolt/transport/local.rb +1 -9
  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 +8 -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 +4 -0
  75. data/modules/aggregate/plans/count.pp +21 -0
  76. data/modules/aggregate/plans/targets.pp +21 -0
  77. data/modules/puppet_connect/plans/test_input_data.pp +67 -0
  78. data/modules/puppetdb_fact/plans/init.pp +10 -0
  79. metadata +7 -3
  80. data/modules/aggregate/plans/nodes.pp +0 -36
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
@@ -74,15 +74,6 @@ module Bolt
74
74
  interpreters[Pathname(executable).extname] if interpreters
75
75
  end
76
76
 
77
- # Transform a parameter map to an environment variable map, with parameter names prefixed
78
- # with 'PT_' and values transformed to JSON unless they're strings.
79
- def envify_params(params)
80
- params.each_with_object({}) do |(k, v), h|
81
- v = v.to_json unless v.is_a?(String)
82
- h["PT_#{k}"] = v
83
- end
84
- end
85
-
86
77
  # Raises an error if more than one target was given in the batch.
87
78
  #
88
79
  # The default implementations of batch_* strictly assume the transport is
@@ -6,7 +6,7 @@ require 'bolt/transport/base'
6
6
 
7
7
  module Bolt
8
8
  module Transport
9
- class Docker < Base
9
+ class Docker < Simple
10
10
  def provided_features
11
11
  ['shell']
12
12
  end
@@ -16,130 +16,6 @@ module Bolt
16
16
  conn.connect
17
17
  yield conn
18
18
  end
19
-
20
- def upload(target, source, destination, _options = {})
21
- with_connection(target) do |conn|
22
- conn.with_remote_tmpdir do |dir|
23
- basename = File.basename(source)
24
- tmpfile = "#{dir}/#{basename}"
25
- if File.directory?(source)
26
- conn.write_remote_directory(source, tmpfile)
27
- else
28
- conn.write_remote_file(source, tmpfile)
29
- end
30
-
31
- _, stderr, exitcode = conn.execute('mv', tmpfile, destination, {})
32
- if exitcode != 0
33
- message = "Could not move temporary file '#{tmpfile}' to #{destination}: #{stderr}"
34
- raise Bolt::Node::FileError.new(message, 'MV_ERROR')
35
- end
36
- end
37
- Bolt::Result.for_upload(target, source, destination)
38
- end
39
- end
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
-
49
- def run_command(target, command, options = {}, position = [])
50
- execute_options = {}
51
- execute_options[:tty] = target.options['tty']
52
- execute_options[:environment] = options[:env_vars]
53
-
54
- if target.options['shell-command'] && !target.options['shell-command'].empty?
55
- # escape any double quotes in command
56
- command = command.gsub('"', '\"')
57
- command = "#{target.options['shell-command']} \" #{command}\""
58
- end
59
- with_connection(target) do |conn|
60
- stdout, stderr, exitcode = conn.execute(*Shellwords.split(command), execute_options)
61
- Bolt::Result.for_command(target,
62
- stdout,
63
- stderr,
64
- exitcode,
65
- 'command',
66
- command,
67
- position)
68
- end
69
- end
70
-
71
- def run_script(target, script, arguments, options = {}, position = [])
72
- # unpack any Sensitive data
73
- arguments = unwrap_sensitive_args(arguments)
74
- execute_options = {}
75
- execute_options[:environment] = options[:env_vars]
76
-
77
- with_connection(target) do |conn|
78
- conn.with_remote_tmpdir do |dir|
79
- remote_path = conn.write_remote_executable(dir, script)
80
- stdout, stderr, exitcode = conn.execute(remote_path, *arguments, execute_options)
81
- Bolt::Result.for_command(target,
82
- stdout,
83
- stderr,
84
- exitcode,
85
- 'script',
86
- script,
87
- position)
88
- end
89
- end
90
- end
91
-
92
- def run_task(target, task, arguments, _options = {}, position = [])
93
- implementation = task.select_implementation(target, provided_features)
94
- executable = implementation['path']
95
- input_method = implementation['input_method']
96
- extra_files = implementation['files']
97
- input_method ||= 'both'
98
-
99
- # unpack any Sensitive data
100
- arguments = unwrap_sensitive_args(arguments)
101
- with_connection(target) do |conn|
102
- execute_options = {}
103
- execute_options[:interpreter] = select_interpreter(executable, target.options['interpreters'])
104
- conn.with_remote_tmpdir do |dir|
105
- if extra_files.empty?
106
- task_dir = dir
107
- else
108
- # TODO: optimize upload of directories
109
- arguments['_installdir'] = dir
110
- task_dir = File.join(dir, task.tasks_dir)
111
- conn.mkdirs([task_dir] + extra_files.map { |file| File.join(dir, File.dirname(file['name'])) })
112
- extra_files.each do |file|
113
- conn.write_remote_file(file['path'], File.join(dir, file['name']))
114
- end
115
- end
116
-
117
- remote_task_path = conn.write_remote_executable(task_dir, executable)
118
-
119
- if Bolt::Task::STDIN_METHODS.include?(input_method)
120
- execute_options[:stdin] = StringIO.new(JSON.dump(arguments))
121
- end
122
-
123
- if Bolt::Task::ENVIRONMENT_METHODS.include?(input_method)
124
- execute_options[:environment] = envify_params(arguments)
125
- end
126
-
127
- stdout, stderr, exitcode = conn.execute(remote_task_path, execute_options)
128
- Bolt::Result.for_task(target,
129
- stdout,
130
- stderr,
131
- exitcode,
132
- task.name,
133
- position)
134
- end
135
- end
136
- end
137
-
138
- def connected?(target)
139
- with_connection(target) { true }
140
- rescue Bolt::Node::ConnectError
141
- false
142
- end
143
19
  end
144
20
  end
145
21
  end
@@ -5,227 +5,152 @@ require 'bolt/node/errors'
5
5
 
6
6
  module Bolt
7
7
  module Transport
8
- class Docker < Base
8
+ class Docker < Simple
9
9
  class Connection
10
+ attr_reader :user, :target
11
+
10
12
  def initialize(target)
11
13
  raise Bolt::ValidationError, "Target #{target.safe_name} does not have a host" unless target.host
12
14
  @target = target
15
+ @user = ENV['USER'] || Etc.getlogin
13
16
  @logger = Bolt::Logger.logger(target.safe_name)
14
- @docker_host = @target.options['service-url']
15
- @logger.trace("Initializing docker connection to #{@target.safe_name}")
17
+ @container_info = {}
18
+ @docker_host = target.options['service-url']
19
+ @logger.trace("Initializing docker connection to #{target.safe_name}")
20
+ end
21
+
22
+ def shell
23
+ @shell ||= if Bolt::Util.windows?
24
+ Bolt::Shell::Powershell.new(target, self)
25
+ else
26
+ Bolt::Shell::Bash.new(target, self)
27
+ end
28
+ end
29
+
30
+ # The full ID of the target container
31
+ #
32
+ # @return [String] The full ID of the target container
33
+ def container_id
34
+ @container_info["Id"]
16
35
  end
17
36
 
18
37
  def connect
19
38
  # We don't actually have a connection, but we do need to
20
39
  # check that the container exists and is running.
21
- output = execute_local_docker_json_command('ps')
22
- index = output.find_index { |item| item["ID"] == @target.host || item["Names"] == @target.host }
23
- raise "Could not find a container with name or ID matching '#{@target.host}'" if index.nil?
40
+ output = execute_local_json_command('ps')
41
+ index = output.find_index { |item| item["ID"] == target.host || item["Names"] == target.host }
42
+ raise "Could not find a container with name or ID matching '#{target.host}'" if index.nil?
24
43
  # Now find the indepth container information
25
- output = execute_local_docker_json_command('inspect', [output[index]["ID"]])
44
+ output = execute_local_json_command('inspect', [output[index]["ID"]])
26
45
  # Store the container information for later
27
46
  @container_info = output[0]
28
47
  @logger.trace { "Opened session" }
29
48
  true
30
49
  rescue StandardError => e
31
50
  raise Bolt::Node::ConnectError.new(
32
- "Failed to connect to #{@target.safe_name}: #{e.message}",
51
+ "Failed to connect to #{target.safe_name}: #{e.message}",
33
52
  'CONNECT_ERROR'
34
53
  )
35
54
  end
36
55
 
37
- # Executes a command inside the target container
38
- #
39
- # @param command [Array] The command to run, expressed as an array of strings
40
- # @param options [Hash] command specific options
41
- # @option opts [String] :interpreter statements that are prefixed to the command e.g `/bin/bash` or `cmd.exe /c`
42
- # @option opts [Hash] :environment A hash of environment variables that will be injected into the command
43
- # @option opts [IO] :stdin An IO object that will be used to redirect STDIN for the docker command
44
- def execute(*command, options)
45
- command.unshift(options[:interpreter]) if options[:interpreter]
46
- # Build the `--env` parameters
47
- envs = []
48
- if options[:environment]
49
- options[:environment].each { |env, val| envs.concat(['--env', "#{env}=#{val}"]) }
56
+ 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]}"
50
60
  end
61
+ end
51
62
 
52
- command_options = []
53
- # Need to be interactive if redirecting STDIN
54
- command_options << '--interactive' unless options[:stdin].nil?
55
- command_options << '--tty' if options[:tty]
56
- command_options.concat(envs) unless envs.empty?
57
- command_options << container_id
58
- command_options.concat(command)
59
-
60
- @logger.trace { "Executing: exec #{command_options}" }
63
+ # Executes a command inside the target container. This is called from the shell class.
64
+ #
65
+ # @param command [string] The command to run
66
+ def execute(command)
67
+ args = []
68
+ # CODEREVIEW: Is it always safe to pass --interactive?
69
+ args += %w[--interactive]
70
+ args += %w[--tty] if target.options['tty']
71
+ args += %W[--env DOCKER_HOST=#{@docker_host}] if @docker_host
72
+ args += @env_vars if @env_vars
73
+
74
+ if target.options['shell-command'] && !target.options['shell-command'].empty?
75
+ # escape any double quotes in command
76
+ command = command.gsub('"', '\"')
77
+ command = "#{target.options['shell-command']} \"#{command}\""
78
+ end
61
79
 
62
- stdout_str, stderr_str, status = execute_local_docker_command('exec', command_options, options[:stdin])
80
+ docker_command = %w[docker exec] + args + [container_id] + Shellwords.split(command)
81
+ @logger.trace { "Executing: #{docker_command.join(' ')}" }
63
82
 
64
- # The actual result is the exitstatus not the process object
65
- status = status.nil? ? -32768 : status.exitstatus
66
- if status == 0
67
- @logger.trace { "Command returned successfully" }
68
- else
69
- @logger.trace { "Command failed with exit code #{status}" }
70
- end
71
- stdout_str.force_encoding(Encoding::UTF_8)
72
- stderr_str.force_encoding(Encoding::UTF_8)
73
- # Normalise line endings
74
- stdout_str.gsub!("\r\n", "\n")
75
- stderr_str.gsub!("\r\n", "\n")
76
- [stdout_str, stderr_str, status]
83
+ Open3.popen3(*docker_command)
77
84
  rescue StandardError
78
85
  @logger.trace { "Command aborted" }
79
86
  raise
80
87
  end
81
88
 
82
- def write_remote_file(source, destination)
83
- @logger.trace { "Uploading #{source} to #{destination}" }
84
- _, stdout_str, status = execute_local_docker_command('cp', [source, "#{container_id}:#{destination}"])
85
- unless status.exitstatus.zero?
86
- raise "Error writing file to container #{@container_id}: #{stdout_str}"
87
- end
88
- rescue StandardError => e
89
- raise Bolt::Node::FileError.new(e.message, 'WRITE_ERROR')
90
- end
91
-
92
- def write_remote_directory(source, destination)
89
+ def upload_file(source, destination)
93
90
  @logger.trace { "Uploading #{source} to #{destination}" }
94
- _, stdout_str, status = execute_local_docker_command('cp', [source, "#{container_id}:#{destination}"])
91
+ _stdout, stderr, status = execute_local_command('cp', [source, "#{container_id}:#{destination}"])
95
92
  unless status.exitstatus.zero?
96
- raise "Error writing directory to container #{@container_id}: #{stdout_str}"
93
+ raise "Error writing to container #{container_id}: #{stderr}"
97
94
  end
98
95
  rescue StandardError => e
99
96
  raise Bolt::Node::FileError.new(e.message, 'WRITE_ERROR')
100
97
  end
101
98
 
102
- def download_remote_content(source, destination)
99
+ def download_file(source, destination, _download)
103
100
  @logger.trace { "Downloading #{source} to #{destination}" }
104
101
  # Create the destination directory, otherwise copying a source directory with Docker will
105
102
  # copy the *contents* of the directory.
106
103
  # https://docs.docker.com/engine/reference/commandline/cp/
107
104
  FileUtils.mkdir_p(destination)
108
- _, stdout_str, status = execute_local_docker_command('cp', ["#{container_id}:#{source}", destination])
105
+ _stdout, stderr, status = execute_local_command('cp', ["#{container_id}:#{source}", destination])
109
106
  unless status.exitstatus.zero?
110
- raise "Error downloading content from container #{@container_id}: #{stdout_str}"
107
+ raise "Error downloading content from container #{container_id}: #{stderr}"
111
108
  end
112
109
  rescue StandardError => e
113
110
  raise Bolt::Node::FileError.new(e.message, 'WRITE_ERROR')
114
111
  end
115
112
 
116
- def mkdirs(dirs)
117
- _, stderr, exitcode = execute('mkdir', '-p', *dirs, {})
118
- if exitcode != 0
119
- message = "Could not create directories: #{stderr}"
120
- raise Bolt::Node::FileError.new(message, 'MKDIR_ERROR')
121
- end
122
- end
123
-
124
- def make_tmpdir
125
- tmpdir = @target.options.fetch('tmpdir', container_tmpdir)
126
- tmppath = "#{tmpdir}/#{SecureRandom.uuid}"
127
-
128
- stdout, stderr, exitcode = execute('mkdir', '-m', '700', tmppath, {})
129
- if exitcode != 0
130
- raise Bolt::Node::FileError.new("Could not make tmpdir: #{stderr}", 'TMPDIR_ERROR')
131
- end
132
- tmppath || stdout.first
133
- end
134
-
135
- def with_remote_tmpdir
136
- dir = make_tmpdir
137
- yield dir
138
- ensure
139
- if dir
140
- if @target.options['cleanup']
141
- _, stderr, exitcode = execute('rm', '-rf', dir, {})
142
- if exitcode != 0
143
- Bolt::Logger.warn("fail_cleanup", "Failed to clean up tmpdir '#{dir}': #{stderr}")
144
- end
145
- else
146
- Bolt::Logger.warn("skip_cleanup", "Skipping cleanup of tmpdir '#{dir}'")
147
- end
148
- end
149
- end
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)
150
126
 
151
- def write_remote_executable(dir, file, filename = nil)
152
- filename ||= File.basename(file)
153
- remote_path = File.join(dir.to_s, filename)
154
- write_remote_file(file, remote_path)
155
- make_executable(remote_path)
156
- remote_path
127
+ Open3.capture3(env_hash, 'docker', *docker_command, { binmode: true })
157
128
  end
158
129
 
159
- def make_executable(path)
160
- _, stderr, exitcode = execute('chmod', 'u+x', path, {})
161
- if exitcode != 0
162
- message = "Could not make file '#{path}' executable: #{stderr}"
163
- raise Bolt::Node::FileError.new(message, 'CHMOD_ERROR')
164
- end
130
+ # Executes a Docker CLI command and parses the output in JSON format
131
+ #
132
+ # @param subcommand [String] The docker subcommand to run
133
+ # e.g. 'inspect' for `docker inspect`
134
+ # @param arguments [Array] Arguments to pass to the docker command
135
+ # e.g. 'src' and 'dest' for `docker cp <src> <dest>
136
+ # @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)
165
141
  end
166
142
 
167
- private
168
-
169
143
  # Converts the JSON encoded STDOUT string from the docker cli into ruby objects
170
144
  #
171
145
  # @param stdout_string [String] The string to convert
172
146
  # @return [Object] Ruby object representation of the JSON string
173
- def extract_json(stdout_string)
147
+ private def extract_json(stdout)
174
148
  # The output from the docker format command is a JSON string per line.
175
149
  # We can't do a direct convert but this helper method will convert it into
176
150
  # an array of Objects
177
- stdout_string.split("\n")
178
- .reject { |str| str.strip.empty? }
179
- .map { |str| JSON.parse(str) }
180
- end
181
-
182
- # rubocop:disable Layout/LineLength
183
- # Executes a Docker CLI command
184
- #
185
- # @param subcommand [String] The docker subcommand to run e.g. 'inspect' for `docker inspect`
186
- # @param command_options [Array] Additional command options e.g. ['--size'] for `docker inspect --size`
187
- # @param redir_stdin [IO] IO object which will be use to as STDIN in the docker command. Default is nil, which does not perform redirection
188
- # @return [String, String, Process::Status] The output of the command: STDOUT, STDERR, Process Status
189
- # rubocop:enable Layout/LineLength
190
- def execute_local_docker_command(subcommand, command_options = [], redir_stdin = nil)
191
- env_hash = {}
192
- # Set the DOCKER_HOST if we are using a non-default service-url
193
- env_hash['DOCKER_HOST'] = @docker_host unless @docker_host.nil?
194
-
195
- command_options = [] if command_options.nil?
196
- docker_command = [subcommand].concat(command_options)
197
-
198
- # Always use binary mode for any text data
199
- capture_options = { binmode: true }
200
- capture_options[:stdin_data] = redir_stdin unless redir_stdin.nil?
201
- stdout_str, stderr_str, status = Open3.capture3(env_hash, 'docker', *docker_command, capture_options)
202
- [stdout_str, stderr_str, status]
203
- end
204
-
205
- # Executes a Docker CLI command and parses the output in JSON format
206
- #
207
- # @param subcommand [String] The docker subcommand to run e.g. 'inspect' for `docker inspect`
208
- # @param command_options [Array] Additional command options e.g. ['--size'] for `docker inspect --size`
209
- # @return [Object] Ruby object representation of the JSON string
210
- def execute_local_docker_json_command(subcommand, command_options = [])
211
- command_options = [] if command_options.nil?
212
- command_options = ['--format', '{{json .}}'].concat(command_options)
213
- stdout_str, _stderr_str, _status = execute_local_docker_command(subcommand, command_options)
214
- extract_json(stdout_str)
215
- end
216
-
217
- # The full ID of the target container
218
- #
219
- # @return [String] The full ID of the target container
220
- def container_id
221
- @container_info["Id"]
222
- end
223
-
224
- # The temp path inside the target container
225
- #
226
- # @return [String] The absolute path to the temp directory
227
- def container_tmpdir
228
- '/tmp'
151
+ stdout.split("\n")
152
+ .reject { |str| str.strip.empty? }
153
+ .map { |str| JSON.parse(str) }
229
154
  end
230
155
  end
231
156
  end