bolt 3.4.0 → 3.7.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.
- checksums.yaml +4 -4
- data/Puppetfile +2 -2
- data/bolt-modules/boltlib/lib/puppet/datatypes/applyresult.rb +26 -0
- data/bolt-modules/boltlib/lib/puppet/datatypes/containerresult.rb +51 -0
- data/bolt-modules/boltlib/lib/puppet/datatypes/resourceinstance.rb +43 -0
- data/bolt-modules/boltlib/lib/puppet/datatypes/result.rb +29 -0
- data/bolt-modules/boltlib/lib/puppet/datatypes/resultset.rb +34 -0
- data/bolt-modules/boltlib/lib/puppet/datatypes/target.rb +55 -0
- data/bolt-modules/boltlib/lib/puppet/functions/add_to_group.rb +1 -0
- data/bolt-modules/boltlib/lib/puppet/functions/apply_prep.rb +1 -0
- data/bolt-modules/boltlib/lib/puppet/functions/parallelize.rb +1 -0
- data/bolt-modules/boltlib/lib/puppet/functions/puppetdb_command.rb +66 -0
- data/bolt-modules/boltlib/lib/puppet/functions/remove_from_group.rb +1 -0
- data/bolt-modules/boltlib/lib/puppet/functions/run_container.rb +162 -0
- data/bolt-modules/boltlib/lib/puppet/functions/write_file.rb +1 -0
- data/bolt-modules/boltlib/types/planresult.pp +1 -0
- data/bolt-modules/ctrl/lib/puppet/functions/ctrl/do_until.rb +2 -0
- data/guides/guide.txt +17 -0
- data/guides/links.txt +13 -0
- data/guides/targets.txt +29 -0
- data/guides/transports.txt +23 -0
- data/lib/bolt/analytics.rb +4 -8
- data/lib/bolt/bolt_option_parser.rb +329 -225
- data/lib/bolt/cli.rb +58 -29
- data/lib/bolt/config.rb +11 -7
- data/lib/bolt/config/options.rb +41 -9
- data/lib/bolt/config/transport/podman.rb +33 -0
- data/lib/bolt/container_result.rb +105 -0
- data/lib/bolt/error.rb +15 -0
- data/lib/bolt/executor.rb +17 -13
- data/lib/bolt/inventory.rb +5 -4
- data/lib/bolt/inventory/inventory.rb +3 -2
- data/lib/bolt/inventory/options.rb +9 -0
- data/lib/bolt/inventory/target.rb +16 -0
- data/lib/bolt/module_installer/specs/git_spec.rb +10 -6
- data/lib/bolt/outputter/human.rb +242 -76
- data/lib/bolt/outputter/json.rb +6 -4
- data/lib/bolt/outputter/logger.rb +17 -0
- data/lib/bolt/pal/yaml_plan/step.rb +4 -2
- data/lib/bolt/plan_creator.rb +2 -2
- data/lib/bolt/plugin.rb +13 -11
- data/lib/bolt/puppetdb/client.rb +54 -0
- data/lib/bolt/result.rb +1 -4
- data/lib/bolt/shell/bash.rb +23 -10
- data/lib/bolt/transport/docker.rb +1 -1
- data/lib/bolt/transport/docker/connection.rb +23 -34
- data/lib/bolt/transport/podman.rb +19 -0
- data/lib/bolt/transport/podman/connection.rb +98 -0
- data/lib/bolt/transport/ssh/connection.rb +3 -6
- data/lib/bolt/util.rb +34 -0
- data/lib/bolt/version.rb +1 -1
- data/lib/bolt_server/transport_app.rb +3 -0
- data/lib/bolt_spec/plans/mock_executor.rb +91 -11
- data/modules/puppet_connect/plans/test_input_data.pp +22 -0
- metadata +13 -2
data/lib/bolt/outputter/json.rb
CHANGED
@@ -100,12 +100,12 @@ module Bolt
|
|
100
100
|
moduledir: moduledir.to_s }.to_json)
|
101
101
|
end
|
102
102
|
|
103
|
-
def print_targets(target_list,
|
103
|
+
def print_targets(target_list, inventory_source, default_inventory, _target_flag)
|
104
104
|
@stream.puts ::JSON.pretty_generate(
|
105
105
|
inventory: {
|
106
106
|
targets: target_list[:inventory].map(&:name),
|
107
107
|
count: target_list[:inventory].count,
|
108
|
-
file:
|
108
|
+
file: (inventory_source || default_inventory).to_s
|
109
109
|
},
|
110
110
|
adhoc: {
|
111
111
|
targets: target_list[:adhoc].map(&:name),
|
@@ -116,14 +116,16 @@ module Bolt
|
|
116
116
|
)
|
117
117
|
end
|
118
118
|
|
119
|
-
def print_target_info(
|
119
|
+
def print_target_info(target_list, _inventory_source, _default_inventory, _target_flag)
|
120
|
+
targets = target_list.values.flatten
|
121
|
+
|
120
122
|
@stream.puts ::JSON.pretty_generate(
|
121
123
|
targets: targets.map(&:detail),
|
122
124
|
count: targets.count
|
123
125
|
)
|
124
126
|
end
|
125
127
|
|
126
|
-
def print_groups(groups)
|
128
|
+
def print_groups(groups, _inventory_source, _default_inventory)
|
127
129
|
count = groups.count
|
128
130
|
@stream.puts({ groups: groups,
|
129
131
|
count: count }.to_json)
|
@@ -20,6 +20,10 @@ module Bolt
|
|
20
20
|
log_plan_start(event)
|
21
21
|
when :plan_finish
|
22
22
|
log_plan_finish(event)
|
23
|
+
when :container_start
|
24
|
+
log_container_start(event)
|
25
|
+
when :container_finish
|
26
|
+
log_container_finish(event)
|
23
27
|
end
|
24
28
|
end
|
25
29
|
|
@@ -48,6 +52,19 @@ module Bolt
|
|
48
52
|
duration = event[:duration]
|
49
53
|
@logger.info("Finished: plan #{plan} in #{duration.round(2)} sec")
|
50
54
|
end
|
55
|
+
|
56
|
+
def log_container_start(event)
|
57
|
+
@logger.info("Starting: run container '#{event[:image]}'")
|
58
|
+
end
|
59
|
+
|
60
|
+
def log_container_finish(event)
|
61
|
+
result = event[:result]
|
62
|
+
if result.success?
|
63
|
+
@logger.info("Finished: run container '#{result.object}' succeeded.")
|
64
|
+
else
|
65
|
+
@logger.info("Finished: run container '#{result.object}' failed.")
|
66
|
+
end
|
67
|
+
end
|
51
68
|
end
|
52
69
|
end
|
53
70
|
end
|
@@ -122,9 +122,11 @@ module Bolt
|
|
122
122
|
raise StepError.new("Parameters key must be a hash", body['name'], step_number)
|
123
123
|
end
|
124
124
|
|
125
|
-
metaparams =
|
125
|
+
metaparams = body['parameters'].keys
|
126
|
+
.select { |key| key.start_with?('_') }
|
127
|
+
.map { |key| key.sub(/^_/, '') }
|
126
128
|
|
127
|
-
if (dups = body
|
129
|
+
if (dups = body.keys & metaparams).any?
|
128
130
|
raise StepError.new(
|
129
131
|
"Cannot specify metaparameters when using top-level keys with same name: #{dups.join(', ')}",
|
130
132
|
body['name'],
|
data/lib/bolt/plan_creator.rb
CHANGED
@@ -36,8 +36,8 @@ module Bolt
|
|
36
36
|
prefix, _, basename = segment_plan_name(plan_name)
|
37
37
|
|
38
38
|
unless prefix == project.name
|
39
|
-
message = "
|
40
|
-
|
39
|
+
message = "Incomplete plan name: A plan name must be prefixed with the name of the "\
|
40
|
+
"project or module. Did you mean '#{project.name}::#{plan_name}'?"
|
41
41
|
|
42
42
|
raise Bolt::ValidationError, message
|
43
43
|
end
|
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
|
-
|
239
|
-
|
240
|
-
|
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
|
-
|
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
|
|
data/lib/bolt/puppetdb/client.rb
CHANGED
@@ -95,6 +95,60 @@ module Bolt
|
|
95
95
|
make_query(query, path)
|
96
96
|
end
|
97
97
|
|
98
|
+
# Sends a command to PuppetDB using version 1 of the commands API.
|
99
|
+
# https://puppet.com/docs/puppetdb/latest/api/command/v1/commands.html
|
100
|
+
#
|
101
|
+
# @param command [String] The command to invoke.
|
102
|
+
# @param version [Integer] The version of the command to invoke.
|
103
|
+
# @param payload [Hash] The payload to send with the command.
|
104
|
+
# @return A UUID identifying the submitted command.
|
105
|
+
#
|
106
|
+
def send_command(command, version, payload)
|
107
|
+
command = command.dup.force_encoding('utf-8')
|
108
|
+
body = JSON.generate(payload)
|
109
|
+
|
110
|
+
# PDB requires the following query parameters to the POST request.
|
111
|
+
# Error early if there's no certname, as PDB does not return a
|
112
|
+
# message indicating it's required.
|
113
|
+
unless payload['certname']
|
114
|
+
raise Bolt::Error.new(
|
115
|
+
"Payload must include 'certname', unable to invoke command.",
|
116
|
+
'bolt/pdb-command'
|
117
|
+
)
|
118
|
+
end
|
119
|
+
|
120
|
+
url = uri.tap do |u|
|
121
|
+
u.path = 'pdb/cmd/v1'
|
122
|
+
u.query_values = { 'command' => command,
|
123
|
+
'version' => version,
|
124
|
+
'certname' => payload['certname'] }
|
125
|
+
end
|
126
|
+
|
127
|
+
# Send the command to PDB
|
128
|
+
begin
|
129
|
+
@logger.debug("Sending PuppetDB command '#{command}' to #{url}")
|
130
|
+
response = http_client.post(url.to_s, body: body, header: headers)
|
131
|
+
rescue StandardError => e
|
132
|
+
raise Bolt::PuppetDBFailoverError, "Failed to invoke PuppetDB command: #{e}"
|
133
|
+
end
|
134
|
+
|
135
|
+
@logger.debug("Got response code #{response.code} from PuppetDB")
|
136
|
+
if response.code != 200
|
137
|
+
raise Bolt::PuppetDBError, "Failed to invoke PuppetDB command: #{response.body}"
|
138
|
+
end
|
139
|
+
|
140
|
+
# Return the UUID string from the response body
|
141
|
+
begin
|
142
|
+
JSON.parse(response.body).fetch('uuid', nil)
|
143
|
+
rescue JSON::ParserError
|
144
|
+
raise Bolt::PuppetDBError, "Unable to parse response as JSON: #{response.body}"
|
145
|
+
end
|
146
|
+
rescue Bolt::PuppetDBFailoverError => e
|
147
|
+
@logger.error("Request to puppetdb at #{@current_url} failed with #{e}.")
|
148
|
+
reject_url
|
149
|
+
send_command(command, version, payload)
|
150
|
+
end
|
151
|
+
|
98
152
|
def http_client
|
99
153
|
return @http if @http
|
100
154
|
# lazy-load expensive gem code
|
data/lib/bolt/result.rb
CHANGED
@@ -164,15 +164,12 @@ module Bolt
|
|
164
164
|
target == other.target &&
|
165
165
|
value == other.value
|
166
166
|
end
|
167
|
+
alias == eql?
|
167
168
|
|
168
169
|
def [](key)
|
169
170
|
value[key]
|
170
171
|
end
|
171
172
|
|
172
|
-
def ==(other)
|
173
|
-
eql?(other)
|
174
|
-
end
|
175
|
-
|
176
173
|
def to_json(opts = nil)
|
177
174
|
to_data.to_json(opts)
|
178
175
|
end
|
data/lib/bolt/shell/bash.rb
CHANGED
@@ -396,7 +396,12 @@ module Bolt
|
|
396
396
|
# See if there's a sudo prompt
|
397
397
|
if use_sudo
|
398
398
|
ready_read = select([err], nil, nil, timeout * 5)
|
399
|
-
|
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
|
400
405
|
end
|
401
406
|
|
402
407
|
# True while the process is running or waiting for IO input
|
@@ -412,14 +417,7 @@ module Bolt
|
|
412
417
|
else
|
413
418
|
stream.readpartial(CHUNK_SIZE)
|
414
419
|
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
|
-
|
420
|
+
log_stream(to_print, stream_name)
|
423
421
|
read_streams[stream] << to_print
|
424
422
|
result_output.merged_output << to_print
|
425
423
|
rescue EOFError
|
@@ -458,7 +456,13 @@ module Bolt
|
|
458
456
|
# Read any remaining data in the pipe. Do not wait for
|
459
457
|
# EOF in case the pipe is inherited by a child process.
|
460
458
|
read_streams.each do |stream, _|
|
461
|
-
|
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
|
+
}
|
462
466
|
rescue Errno::EAGAIN, EOFError
|
463
467
|
end
|
464
468
|
result_output.stdout << read_streams[out]
|
@@ -489,6 +493,15 @@ module Bolt
|
|
489
493
|
def sudo_prompt
|
490
494
|
'[sudo] Bolt needs to run as another user, password: '
|
491
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
|
492
505
|
end
|
493
506
|
end
|
494
507
|
end
|
@@ -34,11 +34,20 @@ 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.
|
40
|
-
output = execute_local_json_command('ps')
|
41
|
-
index = output.find_index { |item| item["ID"]
|
49
|
+
output = execute_local_json_command('ps', ['--no-trunc'])
|
50
|
+
index = output.find_index { |item| item["ID"].start_with?(target.host) || item["Names"] == target.host }
|
42
51
|
raise "Could not find a container with name or ID matching '#{target.host}'" if index.nil?
|
43
52
|
# Now find the indepth container information
|
44
53
|
output = execute_local_json_command('inspect', [output[index]["ID"]])
|
@@ -54,10 +63,7 @@ module Bolt
|
|
54
63
|
end
|
55
64
|
|
56
65
|
def add_env_vars(env_vars)
|
57
|
-
@env_vars =
|
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
|
-
|
92
|
-
unless
|
93
|
-
raise "Error writing to container #{container_id}: #{
|
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
|
-
|
106
|
-
unless
|
107
|
-
raise "Error downloading content from container #{container_id}: #{
|
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
|
-
|
138
|
-
|
139
|
-
|
140
|
-
extract_json(
|
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
|
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,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
|