bolt 3.5.0 → 3.8.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.
- checksums.yaml +4 -4
- data/Puppetfile +3 -3
- data/bolt-modules/boltlib/lib/puppet/datatypes/applyresult.rb +26 -0
- data/bolt-modules/boltlib/lib/puppet/datatypes/containerresult.rb +27 -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_script.rb +5 -1
- data/bolt-modules/boltlib/lib/puppet/functions/upload_file.rb +5 -1
- data/bolt-modules/boltlib/lib/puppet/functions/write_file.rb +1 -0
- data/bolt-modules/ctrl/lib/puppet/functions/ctrl/do_until.rb +2 -0
- data/bolt-modules/file/lib/puppet/functions/file/exists.rb +9 -3
- data/bolt-modules/file/lib/puppet/functions/file/read.rb +6 -2
- data/bolt-modules/file/lib/puppet/functions/file/readable.rb +8 -3
- 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/applicator.rb +1 -1
- data/lib/bolt/bolt_option_parser.rb +351 -225
- data/lib/bolt/catalog.rb +2 -1
- data/lib/bolt/cli.rb +122 -55
- 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/executor.rb +15 -11
- data/lib/bolt/inventory.rb +5 -4
- data/lib/bolt/inventory/inventory.rb +3 -2
- data/lib/bolt/module_installer/specs/git_spec.rb +10 -6
- data/lib/bolt/outputter/human.rb +194 -79
- data/lib/bolt/outputter/json.rb +10 -4
- data/lib/bolt/pal.rb +45 -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 +5 -0
- data/lib/bolt/shell/bash.rb +23 -10
- data/lib/bolt/transport/docker.rb +1 -1
- data/lib/bolt/transport/docker/connection.rb +10 -6
- 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 +71 -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 +2 -1
- metadata +10 -2
data/lib/bolt/outputter/json.rb
CHANGED
@@ -83,6 +83,10 @@ module Bolt
|
|
83
83
|
@stream.puts result.to_json
|
84
84
|
end
|
85
85
|
|
86
|
+
def print_result_set(result_set)
|
87
|
+
@stream.puts result_set.to_json
|
88
|
+
end
|
89
|
+
|
86
90
|
def print_topics(topics)
|
87
91
|
print_table('topics' => topics)
|
88
92
|
end
|
@@ -100,12 +104,12 @@ module Bolt
|
|
100
104
|
moduledir: moduledir.to_s }.to_json)
|
101
105
|
end
|
102
106
|
|
103
|
-
def print_targets(target_list,
|
107
|
+
def print_targets(target_list, inventory_source, default_inventory, _target_flag)
|
104
108
|
@stream.puts ::JSON.pretty_generate(
|
105
109
|
inventory: {
|
106
110
|
targets: target_list[:inventory].map(&:name),
|
107
111
|
count: target_list[:inventory].count,
|
108
|
-
file:
|
112
|
+
file: (inventory_source || default_inventory).to_s
|
109
113
|
},
|
110
114
|
adhoc: {
|
111
115
|
targets: target_list[:adhoc].map(&:name),
|
@@ -116,14 +120,16 @@ module Bolt
|
|
116
120
|
)
|
117
121
|
end
|
118
122
|
|
119
|
-
def print_target_info(
|
123
|
+
def print_target_info(target_list, _inventory_source, _default_inventory, _target_flag)
|
124
|
+
targets = target_list.values.flatten
|
125
|
+
|
120
126
|
@stream.puts ::JSON.pretty_generate(
|
121
127
|
targets: targets.map(&:detail),
|
122
128
|
count: targets.count
|
123
129
|
)
|
124
130
|
end
|
125
131
|
|
126
|
-
def print_groups(groups)
|
132
|
+
def print_groups(groups, _inventory_source, _default_inventory)
|
127
133
|
count = groups.count
|
128
134
|
@stream.puts({ groups: groups,
|
129
135
|
count: count }.to_json)
|
data/lib/bolt/pal.rb
CHANGED
@@ -615,5 +615,50 @@ module Bolt
|
|
615
615
|
rescue Bolt::Error => e
|
616
616
|
Bolt::PlanResult.new(e, 'failure')
|
617
617
|
end
|
618
|
+
|
619
|
+
def lookup(key, targets, inventory, executor, _concurrency)
|
620
|
+
# Install the puppet-agent package and collect facts. Facts are
|
621
|
+
# automatically added to the targets.
|
622
|
+
in_plan_compiler(executor, inventory, nil) do |compiler|
|
623
|
+
compiler.call_function('apply_prep', targets)
|
624
|
+
end
|
625
|
+
|
626
|
+
overrides = {
|
627
|
+
bolt_inventory: inventory,
|
628
|
+
bolt_project: @project
|
629
|
+
}
|
630
|
+
|
631
|
+
# Do a lookup with a catalog compiler, which uses the 'hierarchy' key in
|
632
|
+
# Hiera config.
|
633
|
+
results = targets.map do |target|
|
634
|
+
node = Puppet::Node.from_data_hash(
|
635
|
+
'name' => target.name,
|
636
|
+
'parameters' => { 'clientcert' => target.name }
|
637
|
+
)
|
638
|
+
|
639
|
+
trusted = Puppet::Context::TrustedInformation.local(node).to_h
|
640
|
+
|
641
|
+
env_conf = {
|
642
|
+
modulepath: @modulepath.full_modulepath,
|
643
|
+
facts: target.facts,
|
644
|
+
variables: target.vars
|
645
|
+
}
|
646
|
+
|
647
|
+
with_puppet_settings do
|
648
|
+
Puppet::Pal.in_tmp_environment(target.name, **env_conf) do |pal|
|
649
|
+
Puppet.override(overrides) do
|
650
|
+
Puppet.lookup(:pal_current_node).trusted_data = trusted
|
651
|
+
pal.with_catalog_compiler do |compiler|
|
652
|
+
Bolt::Result.for_lookup(target, key, compiler.call_function('lookup', key))
|
653
|
+
rescue StandardError => e
|
654
|
+
Bolt::Result.from_exception(target, e)
|
655
|
+
end
|
656
|
+
end
|
657
|
+
end
|
658
|
+
end
|
659
|
+
end
|
660
|
+
|
661
|
+
Bolt::ResultSet.new(results)
|
662
|
+
end
|
618
663
|
end
|
619
664
|
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
@@ -28,6 +28,11 @@ module Bolt
|
|
28
28
|
%w[file line].zip(position).to_h.compact
|
29
29
|
end
|
30
30
|
|
31
|
+
def self.for_lookup(target, key, value)
|
32
|
+
val = { 'value' => value }
|
33
|
+
new(target, value: val, action: 'lookup', object: key)
|
34
|
+
end
|
35
|
+
|
31
36
|
def self.for_command(target, value, action, command, position)
|
32
37
|
details = create_details(position)
|
33
38
|
unless value['exit_code'] == 0
|
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,6 +34,10 @@ 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
|
+
|
37
41
|
private def env_hash
|
38
42
|
# Set the DOCKER_HOST if we are using a non-default service-url
|
39
43
|
@docker_host.nil? ? {} : { 'DOCKER_HOST' => @docker_host }
|
@@ -42,8 +46,8 @@ module Bolt
|
|
42
46
|
def connect
|
43
47
|
# We don't actually have a connection, but we do need to
|
44
48
|
# check that the container exists and is running.
|
45
|
-
output = execute_local_json_command('ps')
|
46
|
-
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 }
|
47
51
|
raise "Could not find a container with name or ID matching '#{target.host}'" if index.nil?
|
48
52
|
# Now find the indepth container information
|
49
53
|
output = execute_local_json_command('inspect', [output[index]["ID"]])
|
@@ -90,7 +94,7 @@ module Bolt
|
|
90
94
|
|
91
95
|
def upload_file(source, destination)
|
92
96
|
@logger.trace { "Uploading #{source} to #{destination}" }
|
93
|
-
_out, err, stat =
|
97
|
+
_out, err, stat = run_cmd(['cp', source, "#{container_id}:#{destination}"], env_hash)
|
94
98
|
unless stat.exitstatus.zero?
|
95
99
|
raise "Error writing to container #{container_id}: #{err}"
|
96
100
|
end
|
@@ -104,7 +108,7 @@ module Bolt
|
|
104
108
|
# copy the *contents* of the directory.
|
105
109
|
# https://docs.docker.com/engine/reference/commandline/cp/
|
106
110
|
FileUtils.mkdir_p(destination)
|
107
|
-
_out, err, stat =
|
111
|
+
_out, err, stat = run_cmd(['cp', "#{container_id}:#{source}", destination], env_hash)
|
108
112
|
unless stat.exitstatus.zero?
|
109
113
|
raise "Error downloading content from container #{container_id}: #{err}"
|
110
114
|
end
|
@@ -119,9 +123,9 @@ module Bolt
|
|
119
123
|
# @param arguments [Array] Arguments to pass to the docker command
|
120
124
|
# e.g. 'src' and 'dest' for `docker cp <src> <dest>
|
121
125
|
# @return [Object] Ruby object representation of the JSON string
|
122
|
-
|
126
|
+
def execute_local_json_command(subcommand, arguments = [])
|
123
127
|
cmd = [subcommand, '--format', '{{json .}}'].concat(arguments)
|
124
|
-
out, _err, _stat =
|
128
|
+
out, _err, _stat = run_cmd(cmd, env_hash)
|
125
129
|
extract_json(out)
|
126
130
|
end
|
127
131
|
|
@@ -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
|