bolt 3.4.0 → 3.5.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 +2 -2
- data/bolt-modules/boltlib/lib/puppet/datatypes/containerresult.rb +24 -0
- data/bolt-modules/boltlib/lib/puppet/functions/run_container.rb +162 -0
- data/bolt-modules/boltlib/types/planresult.pp +1 -0
- data/lib/bolt/container_result.rb +105 -0
- data/lib/bolt/error.rb +15 -0
- data/lib/bolt/executor.rb +2 -2
- data/lib/bolt/inventory/options.rb +9 -0
- data/lib/bolt/inventory/target.rb +16 -0
- data/lib/bolt/outputter/human.rb +55 -0
- data/lib/bolt/outputter/logger.rb +17 -0
- data/lib/bolt/result.rb +1 -4
- data/lib/bolt/transport/docker/connection.rb +16 -31
- data/lib/bolt/util.rb +23 -0
- data/lib/bolt/version.rb +1 -1
- data/lib/bolt_spec/plans/mock_executor.rb +90 -11
- data/modules/puppet_connect/plans/test_input_data.pp +22 -0
- metadata +5 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 41eb3934a2ddce5d1021b66f5aca6eccfdb91bdedd5dc08cfdc1cfb4664f91e8
|
4
|
+
data.tar.gz: 8a04142fd9de53e8812cff0c540098a8548a8b18245e300835896b457a86d348
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 24bc37edf14e9c5d3ef1b8df5d5ed9dd0161ebb44da96e0b2777703c5b71182179ebed740e5d67c04bad41b9ecdb6e0a40d5fcb5ec08eb59171da36a3d5a3756
|
7
|
+
data.tar.gz: 0e5795469709939091282a51df744076e87e40e5dad0f5bc6395d0d7927dcbf5f788bccf23bd5c455c29232faa958ddcbe5130626eb82d056404f8cb1e088cdd
|
data/Puppetfile
CHANGED
@@ -32,9 +32,9 @@ mod 'puppetlabs-ruby_plugin_helper', '0.2.0'
|
|
32
32
|
mod 'puppetlabs-stdlib', '7.0.0'
|
33
33
|
|
34
34
|
# Plugin modules
|
35
|
-
mod 'puppetlabs-aws_inventory', '0.
|
35
|
+
mod 'puppetlabs-aws_inventory', '0.7.0'
|
36
36
|
mod 'puppetlabs-azure_inventory', '0.5.0'
|
37
|
-
mod 'puppetlabs-gcloud_inventory', '0.
|
37
|
+
mod 'puppetlabs-gcloud_inventory', '0.3.0'
|
38
38
|
mod 'puppetlabs-http_request', '0.2.2'
|
39
39
|
mod 'puppetlabs-pkcs7', '0.1.1'
|
40
40
|
mod 'puppetlabs-secure_env_vars', '0.2.0'
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
Puppet::DataTypes.create_type('ContainerResult') do
|
4
|
+
interface <<-PUPPET
|
5
|
+
attributes => {
|
6
|
+
'value' => Hash[String[1], Data],
|
7
|
+
},
|
8
|
+
functions => {
|
9
|
+
'[]' => Callable[[String[1]], Data],
|
10
|
+
error => Callable[[], Optional[Error]],
|
11
|
+
ok => Callable[[], Boolean],
|
12
|
+
status => Callable[[], String],
|
13
|
+
stdout => Callable[[], String],
|
14
|
+
stderr => Callable[[], String],
|
15
|
+
to_data => Callable[[], Hash]
|
16
|
+
}
|
17
|
+
PUPPET
|
18
|
+
|
19
|
+
load_file('bolt/container_result')
|
20
|
+
|
21
|
+
# Needed for Puppet to recognize Bolt::ContainerResult as a Puppet object when deserializing
|
22
|
+
Bolt::ContainerResult.include(Puppet::Pops::Types::PuppetObject)
|
23
|
+
implementation_class Bolt::ContainerResult
|
24
|
+
end
|
@@ -0,0 +1,162 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'bolt/container_result'
|
4
|
+
require 'bolt/error'
|
5
|
+
require 'bolt/util'
|
6
|
+
|
7
|
+
# Run a container and return its output to stdout and stderr.
|
8
|
+
#
|
9
|
+
# > **Note:** Not available in apply block
|
10
|
+
Puppet::Functions.create_function(:run_container) do
|
11
|
+
# Run a container.
|
12
|
+
# @param image The name of the image to run.
|
13
|
+
# @param options A hash of additional options.
|
14
|
+
# @option options [Boolean] _catch_errors Whether to catch raised errors.
|
15
|
+
# @option options [String] cmd A command to run in the container.
|
16
|
+
# @option options [Hash[String, Data]] env_vars Map of environment variables to set.
|
17
|
+
# @option options [Hash[Integer, Integer]] ports A map of container ports to
|
18
|
+
# publish. Keys are the host port, values are the corresponding container
|
19
|
+
# port.
|
20
|
+
# @option options [Boolean] rm Whether to remove the container once it exits.
|
21
|
+
# @option options [Hash[String, String]] volumes A map of absolute paths on
|
22
|
+
# the host to absolute paths on the remote to mount.
|
23
|
+
# @option options [String] workdir The working directory within the container.
|
24
|
+
# @return Output from the container.
|
25
|
+
# @example Run Nginx proxy manager
|
26
|
+
# run_container('jc21/nginx-proxy-manager', 'ports' => { 80 => 80, 81 => 81, 443 => 443 })
|
27
|
+
dispatch :run_container do
|
28
|
+
param 'String[1]', :image
|
29
|
+
optional_param 'Hash[String[1], Any]', :options
|
30
|
+
return_type 'ContainerResult'
|
31
|
+
end
|
32
|
+
|
33
|
+
def run_container(image, options = {})
|
34
|
+
unless Puppet[:tasks]
|
35
|
+
raise Puppet::ParseErrorWithIssue
|
36
|
+
.from_issue_and_stack(Bolt::PAL::Issues::PLAN_OPERATION_NOT_SUPPORTED_WHEN_COMPILING, action: 'run_container')
|
37
|
+
end
|
38
|
+
|
39
|
+
# Send Analytics Report
|
40
|
+
executor = Puppet.lookup(:bolt_executor)
|
41
|
+
executor.report_function_call(self.class.name)
|
42
|
+
|
43
|
+
options = options.transform_keys { |k| k.sub(/^_/, '').to_sym }
|
44
|
+
validate_options(options)
|
45
|
+
|
46
|
+
if options.key?(:env_vars)
|
47
|
+
options[:env_vars] = options[:env_vars].transform_values do |val|
|
48
|
+
[Array, Hash].include?(val.class) ? val.to_json : val
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
if options[:ports]
|
53
|
+
ports = options[:ports].each_with_object([]) do |(host_port, container_port), acc|
|
54
|
+
acc << "-p"
|
55
|
+
acc << "#{host_port}:#{container_port}"
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
if options[:volumes]
|
60
|
+
volumes = options[:volumes].each_with_object([]) do |(host_path, remote_path), acc|
|
61
|
+
begin
|
62
|
+
FileUtils.mkdir_p(host_path)
|
63
|
+
rescue StandardError => e
|
64
|
+
message = "Unable to create host volume directory #{host_path}: #{e.message}"
|
65
|
+
raise Bolt::Error.new(message, 'bolt/file-error')
|
66
|
+
end
|
67
|
+
acc << "-v"
|
68
|
+
acc << "#{host_path}:#{remote_path}"
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
# Run the container
|
73
|
+
# `docker run` will automatically pull the image if it isn't already downloaded
|
74
|
+
cmd = %w[run]
|
75
|
+
cmd += Bolt::Util.format_env_vars_for_cli(options[:env_vars]) if options[:env_vars]
|
76
|
+
cmd += volumes if volumes
|
77
|
+
cmd += ports if ports
|
78
|
+
cmd << "--rm" if options[:rm]
|
79
|
+
cmd += %W[-w #{options[:workdir]}] if options[:workdir]
|
80
|
+
cmd << image
|
81
|
+
cmd += Shellwords.shellsplit(options[:cmd]) if options[:cmd]
|
82
|
+
|
83
|
+
executor.publish_event(type: :container_start, image: image)
|
84
|
+
out, err, status = Bolt::Util.exec_docker(cmd)
|
85
|
+
|
86
|
+
o = out.is_a?(String) ? out.dup.force_encoding('utf-8') : out
|
87
|
+
e = err.is_a?(String) ? err.dup.force_encoding('utf-8') : err
|
88
|
+
|
89
|
+
unless status.exitstatus.zero?
|
90
|
+
result = Bolt::ContainerResult.from_exception(e,
|
91
|
+
status.exitstatus,
|
92
|
+
image,
|
93
|
+
position: Puppet::Pops::PuppetStack.top_of_stack)
|
94
|
+
executor.publish_event(type: :container_finish, result: result)
|
95
|
+
if options[:catch_errors]
|
96
|
+
return result
|
97
|
+
else
|
98
|
+
raise Bolt::ContainerFailure, result
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
value = { 'stdout' => o, 'stderr' => e, 'exit_code' => status.exitstatus }
|
103
|
+
result = Bolt::ContainerResult.new(value, object: image)
|
104
|
+
executor.publish_event(type: :container_finish, result: result)
|
105
|
+
result
|
106
|
+
end
|
107
|
+
|
108
|
+
def validate_options(options)
|
109
|
+
if options.key?(:env_vars)
|
110
|
+
ev = options[:env_vars]
|
111
|
+
unless ev.is_a?(Hash)
|
112
|
+
msg = "Option 'env_vars' must be a hash. Received #{ev} which is a #{ev.class}"
|
113
|
+
raise Bolt::ValidationError, msg
|
114
|
+
end
|
115
|
+
|
116
|
+
if (bad_keys = ev.keys.reject { |k| k.is_a?(String) }).any?
|
117
|
+
msg = "Keys for option 'env_vars' must be strings: #{bad_keys.map(&:inspect).join(', ')}"
|
118
|
+
raise Bolt::ValidationError, msg
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
if options.key?(:volumes)
|
123
|
+
volumes = options[:volumes]
|
124
|
+
unless volumes.is_a?(Hash)
|
125
|
+
msg = "Option 'volumes' must be a hash. Received #{volumes} which is a #{volumes.class}"
|
126
|
+
raise Bolt::ValidationError, msg
|
127
|
+
end
|
128
|
+
|
129
|
+
if (bad_vs = volumes.reject { |k, v| k.is_a?(String) && v.is_a?(String) }).any?
|
130
|
+
msg = "Option 'volumes' only accepts strings for keys and values. "\
|
131
|
+
"Received: #{bad_vs.map(&:inspect).join(', ')}"
|
132
|
+
raise Bolt::ValidationError, msg
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
if options.key?(:cmd) && !options[:cmd].is_a?(String)
|
137
|
+
cmd = options[:cmd]
|
138
|
+
msg = "Option 'cmd' must be a string. Received #{cmd} which is a #{cmd.class}"
|
139
|
+
raise Bolt::ValidationError, msg
|
140
|
+
end
|
141
|
+
|
142
|
+
if options.key?(:workdir) && !options[:workdir].is_a?(String)
|
143
|
+
wd = options[:workdir]
|
144
|
+
msg = "Option 'workdir' must be a string. Received #{wd} which is a #{wd.class}"
|
145
|
+
raise Bolt::ValidationError, msg
|
146
|
+
end
|
147
|
+
|
148
|
+
if options.key?(:ports)
|
149
|
+
ports = options[:ports]
|
150
|
+
unless ports.is_a?(Hash)
|
151
|
+
msg = "Option 'ports' must be a hash. Received #{ports} which is a #{ports.class}"
|
152
|
+
raise Bolt::ValidationError, msg
|
153
|
+
end
|
154
|
+
|
155
|
+
if (bad_ps = ports.reject { |k, v| k.is_a?(Integer) && v.is_a?(Integer) }).any?
|
156
|
+
msg = "Option 'ports' only accepts integers for keys and values. "\
|
157
|
+
"Received: #{bad_ps.map(&:inspect).join(', ')}"
|
158
|
+
raise Bolt::ValidationError, msg
|
159
|
+
end
|
160
|
+
end
|
161
|
+
end
|
162
|
+
end
|
@@ -0,0 +1,105 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'json'
|
4
|
+
require 'bolt/error'
|
5
|
+
require 'bolt/result'
|
6
|
+
|
7
|
+
module Bolt
|
8
|
+
class ContainerResult
|
9
|
+
attr_reader :value, :object
|
10
|
+
|
11
|
+
def self.from_exception(exception, exit_code, image, position: [])
|
12
|
+
details = Bolt::Result.create_details(position)
|
13
|
+
error = {
|
14
|
+
'kind' => 'puppetlabs.tasks/container-error',
|
15
|
+
'issue_code' => 'CONTAINER_ERROR',
|
16
|
+
'msg' => "Error running container '#{image}': #{exception}",
|
17
|
+
'details' => details
|
18
|
+
}
|
19
|
+
error['details']['exit_code'] = exit_code
|
20
|
+
ContainerResult.new({ '_error' => error }, object: image)
|
21
|
+
end
|
22
|
+
|
23
|
+
def _pcore_init_hash
|
24
|
+
{ 'value' => @value,
|
25
|
+
'object' => @image }
|
26
|
+
end
|
27
|
+
|
28
|
+
# First argument can't be named given the way that Puppet deserializes variables
|
29
|
+
def initialize(value = nil, object: nil)
|
30
|
+
@value = value || {}
|
31
|
+
@object = object
|
32
|
+
end
|
33
|
+
|
34
|
+
def eql?(other)
|
35
|
+
self.class == other.class &&
|
36
|
+
value == other.value
|
37
|
+
end
|
38
|
+
alias == eql?
|
39
|
+
|
40
|
+
def [](key)
|
41
|
+
value[key]
|
42
|
+
end
|
43
|
+
|
44
|
+
def to_json(opts = nil)
|
45
|
+
to_data.to_json(opts)
|
46
|
+
end
|
47
|
+
alias to_s to_json
|
48
|
+
|
49
|
+
# This is the value with all non-UTF-8 characters removed, suitable for
|
50
|
+
# printing or converting to JSON. It *should* only be possible to have
|
51
|
+
# non-UTF-8 characters in stdout/stderr keys as they are not allowed from
|
52
|
+
# tasks but we scrub the whole thing just in case.
|
53
|
+
def safe_value
|
54
|
+
Bolt::Util.walk_vals(value) do |val|
|
55
|
+
if val.is_a?(String)
|
56
|
+
# Replace invalid bytes with hex codes, ie. \xDE\xAD\xBE\xEF
|
57
|
+
val.scrub { |c| c.bytes.map { |b| "\\x" + b.to_s(16).upcase }.join }
|
58
|
+
else
|
59
|
+
val
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
def stdout
|
65
|
+
value['stdout']
|
66
|
+
end
|
67
|
+
|
68
|
+
def stderr
|
69
|
+
value['stderr']
|
70
|
+
end
|
71
|
+
|
72
|
+
def to_data
|
73
|
+
{
|
74
|
+
"object" => object,
|
75
|
+
"status" => status,
|
76
|
+
"value" => safe_value
|
77
|
+
}
|
78
|
+
end
|
79
|
+
|
80
|
+
def status
|
81
|
+
ok? ? 'success' : 'failure'
|
82
|
+
end
|
83
|
+
|
84
|
+
def ok?
|
85
|
+
error_hash.nil?
|
86
|
+
end
|
87
|
+
alias ok ok?
|
88
|
+
alias success? ok?
|
89
|
+
|
90
|
+
# This allows access to errors outside puppet compilation
|
91
|
+
# it should be prefered over error in bolt code
|
92
|
+
def error_hash
|
93
|
+
value['_error']
|
94
|
+
end
|
95
|
+
|
96
|
+
# Warning: This will fail outside of a compilation.
|
97
|
+
# Use error_hash inside bolt.
|
98
|
+
# Is it crazy for this to behave differently outside a compiler?
|
99
|
+
def error
|
100
|
+
if error_hash
|
101
|
+
Puppet::DataTypes::Error.from_asserted_hash(error_hash)
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
data/lib/bolt/error.rb
CHANGED
@@ -61,6 +61,21 @@ module Bolt
|
|
61
61
|
end
|
62
62
|
end
|
63
63
|
|
64
|
+
class ContainerFailure < Bolt::Error
|
65
|
+
attr_reader :result
|
66
|
+
|
67
|
+
def initialize(result)
|
68
|
+
details = {
|
69
|
+
'value' => result.value,
|
70
|
+
'object' => result.object
|
71
|
+
}
|
72
|
+
message = "Plan aborted: Running container '#{result.object}' failed."
|
73
|
+
super(message, 'bolt/container-failure', details)
|
74
|
+
@result = result
|
75
|
+
@error_code = 2
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
64
79
|
class RunFailure < Bolt::Error
|
65
80
|
attr_reader :result_set
|
66
81
|
|
data/lib/bolt/executor.rb
CHANGED
@@ -217,7 +217,7 @@ module Bolt
|
|
217
217
|
results
|
218
218
|
end
|
219
219
|
|
220
|
-
def report_transport(transport, count)
|
220
|
+
private def report_transport(transport, count)
|
221
221
|
name = transport.class.name.split('::').last.downcase
|
222
222
|
unless @reported_transports.include?(name)
|
223
223
|
@analytics&.event('Transport', 'initialize', label: name, value: count)
|
@@ -475,7 +475,7 @@ module Bolt
|
|
475
475
|
Time.now
|
476
476
|
end
|
477
477
|
|
478
|
-
def wait_until(timeout, retry_interval)
|
478
|
+
private def wait_until(timeout, retry_interval)
|
479
479
|
start = wait_now
|
480
480
|
until yield
|
481
481
|
raise(TimeoutError, 'Timed out waiting for target') if (wait_now - start).to_i >= timeout
|
@@ -11,8 +11,10 @@ module Bolt
|
|
11
11
|
facts
|
12
12
|
features
|
13
13
|
groups
|
14
|
+
plugin_hooks
|
14
15
|
targets
|
15
16
|
vars
|
17
|
+
version
|
16
18
|
].freeze
|
17
19
|
|
18
20
|
# Definitions used to validate the data.
|
@@ -123,6 +125,13 @@ module Bolt
|
|
123
125
|
description: "A map of variables for the group or target.",
|
124
126
|
type: Hash,
|
125
127
|
_plugin: true
|
128
|
+
},
|
129
|
+
"version" => {
|
130
|
+
description: "The version of the inventory file.",
|
131
|
+
type: Integer,
|
132
|
+
_plugin: false,
|
133
|
+
_example: 2,
|
134
|
+
_default: 2
|
126
135
|
}
|
127
136
|
}.freeze
|
128
137
|
end
|
@@ -92,6 +92,7 @@ module Bolt
|
|
92
92
|
end
|
93
93
|
|
94
94
|
def add_facts(new_facts = {})
|
95
|
+
validate_fact_names(new_facts)
|
95
96
|
@facts = Bolt::Util.deep_merge(@facts, new_facts)
|
96
97
|
end
|
97
98
|
|
@@ -153,9 +154,24 @@ module Bolt
|
|
153
154
|
raise Bolt::UnknownTransportError.new(transport, uri)
|
154
155
|
end
|
155
156
|
|
157
|
+
validate_fact_names(facts)
|
158
|
+
|
156
159
|
transport_config
|
157
160
|
end
|
158
161
|
|
162
|
+
# Validate fact names and issue a deprecation warning if any fact names have a dot.
|
163
|
+
#
|
164
|
+
private def validate_fact_names(facts)
|
165
|
+
if (dotted = facts.keys.select { |name| name.include?('.') }).any?
|
166
|
+
Bolt::Logger.deprecate(
|
167
|
+
'dotted_fact_name',
|
168
|
+
"Target '#{safe_name}' includes dotted fact names: '#{dotted.join("', '")}'. Dotted fact "\
|
169
|
+
"names are deprecated and Bolt does not automatically convert facts with dotted names to "\
|
170
|
+
"structured facts. For more information, see https://pup.pt/bolt-dotted-facts"
|
171
|
+
)
|
172
|
+
end
|
173
|
+
end
|
174
|
+
|
159
175
|
def host
|
160
176
|
@uri_obj.hostname || transport_config['host']
|
161
177
|
end
|
data/lib/bolt/outputter/human.rb
CHANGED
@@ -92,6 +92,10 @@ module Bolt
|
|
92
92
|
print_plan_start(event)
|
93
93
|
when :plan_finish
|
94
94
|
print_plan_finish(event)
|
95
|
+
when :container_start
|
96
|
+
print_container_start(event) if plan_logging?
|
97
|
+
when :container_finish
|
98
|
+
print_container_finish(event) if plan_logging?
|
95
99
|
when :start_spin
|
96
100
|
start_spin
|
97
101
|
when :stop_spin
|
@@ -112,6 +116,34 @@ module Bolt
|
|
112
116
|
@stream.puts(colorize(:green, "Started on #{target.safe_name}..."))
|
113
117
|
end
|
114
118
|
|
119
|
+
def print_container_result(result)
|
120
|
+
if result.success?
|
121
|
+
@stream.puts(colorize(:green, "Finished running container #{result.object}:"))
|
122
|
+
else
|
123
|
+
@stream.puts(colorize(:red, "Failed running container #{result.object}:"))
|
124
|
+
end
|
125
|
+
|
126
|
+
if result.error_hash
|
127
|
+
@stream.puts(colorize(:red, remove_trail(indent(2, result.error_hash['msg']))))
|
128
|
+
return 0
|
129
|
+
end
|
130
|
+
|
131
|
+
# Only print results if there's something other than empty string and hash
|
132
|
+
safe_value = result.safe_value
|
133
|
+
if safe_value['stdout'].strip.empty? && safe_value['stderr'].strip.empty?
|
134
|
+
@stream.puts(indent(2, "Running container #{result.object} completed successfully with no result"))
|
135
|
+
else
|
136
|
+
unless safe_value['stdout'].strip && safe_value['stdout'].strip.empty?
|
137
|
+
@stream.puts(indent(2, "STDOUT:"))
|
138
|
+
@stream.puts(indent(4, safe_value['stdout']))
|
139
|
+
end
|
140
|
+
unless safe_value['stderr'].strip.empty?
|
141
|
+
@stream.puts(indent(2, "STDERR:"))
|
142
|
+
@stream.puts(indent(4, safe_value['stderr']))
|
143
|
+
end
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
115
147
|
def print_result(result)
|
116
148
|
if result.success?
|
117
149
|
@stream.puts(colorize(:green, "Finished on #{result.target.safe_name}:"))
|
@@ -180,6 +212,25 @@ module Bolt
|
|
180
212
|
@stream.puts(colorize(:green, message))
|
181
213
|
end
|
182
214
|
|
215
|
+
def print_container_start(image:, **_kwargs)
|
216
|
+
@stream.puts(colorize(:green, "Starting: run container '#{image}'"))
|
217
|
+
end
|
218
|
+
|
219
|
+
def print_container_finish(event)
|
220
|
+
result = if event[:result].is_a?(Bolt::ContainerFailure)
|
221
|
+
event[:result].result
|
222
|
+
else
|
223
|
+
event[:result]
|
224
|
+
end
|
225
|
+
|
226
|
+
if result.success?
|
227
|
+
@stream.puts(colorize(:green, "Finished: run container '#{result.object}' succeeded."))
|
228
|
+
else
|
229
|
+
@stream.puts(colorize(:red, "Finished: run container '#{result.object}' failed."))
|
230
|
+
end
|
231
|
+
print_container_result(result) if @verbose
|
232
|
+
end
|
233
|
+
|
183
234
|
def print_plan_start(event)
|
184
235
|
@plan_depth += 1
|
185
236
|
# We use this event to both mark the start of a plan _and_ to enable
|
@@ -445,6 +496,10 @@ module Bolt
|
|
445
496
|
@stream.puts("Plan completed successfully with no result")
|
446
497
|
when Bolt::ApplyFailure, Bolt::RunFailure
|
447
498
|
print_result_set(value.result_set)
|
499
|
+
when Bolt::ContainerResult
|
500
|
+
print_container_result(value)
|
501
|
+
when Bolt::ContainerFailure
|
502
|
+
print_container_result(value.result)
|
448
503
|
when Bolt::ResultSet
|
449
504
|
print_result_set(value)
|
450
505
|
else
|
@@ -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
|
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
|
@@ -34,6 +34,11 @@ module Bolt
|
|
34
34
|
@container_info["Id"]
|
35
35
|
end
|
36
36
|
|
37
|
+
private def env_hash
|
38
|
+
# Set the DOCKER_HOST if we are using a non-default service-url
|
39
|
+
@docker_host.nil? ? {} : { 'DOCKER_HOST' => @docker_host }
|
40
|
+
end
|
41
|
+
|
37
42
|
def connect
|
38
43
|
# We don't actually have a connection, but we do need to
|
39
44
|
# check that the container exists and is running.
|
@@ -54,10 +59,7 @@ module Bolt
|
|
54
59
|
end
|
55
60
|
|
56
61
|
def add_env_vars(env_vars)
|
57
|
-
@env_vars =
|
58
|
-
acc << "--env"
|
59
|
-
acc << "#{env_var[0]}=#{env_var[1]}"
|
60
|
-
end
|
62
|
+
@env_vars = Bolt::Util.format_env_vars_for_cli(env_vars)
|
61
63
|
end
|
62
64
|
|
63
65
|
# Executes a command inside the target container. This is called from the shell class.
|
@@ -88,9 +90,9 @@ module Bolt
|
|
88
90
|
|
89
91
|
def upload_file(source, destination)
|
90
92
|
@logger.trace { "Uploading #{source} to #{destination}" }
|
91
|
-
|
92
|
-
unless
|
93
|
-
raise "Error writing to container #{container_id}: #{
|
93
|
+
_out, err, stat = Bolt::Util.exec_docker(['cp', source, "#{container_id}:#{destination}"], env_hash)
|
94
|
+
unless stat.exitstatus.zero?
|
95
|
+
raise "Error writing to container #{container_id}: #{err}"
|
94
96
|
end
|
95
97
|
rescue StandardError => e
|
96
98
|
raise Bolt::Node::FileError.new(e.message, 'WRITE_ERROR')
|
@@ -102,31 +104,14 @@ module Bolt
|
|
102
104
|
# copy the *contents* of the directory.
|
103
105
|
# https://docs.docker.com/engine/reference/commandline/cp/
|
104
106
|
FileUtils.mkdir_p(destination)
|
105
|
-
|
106
|
-
unless
|
107
|
-
raise "Error downloading content from container #{container_id}: #{
|
107
|
+
_out, err, stat = Bolt::Util.exec_docker(['cp', "#{container_id}:#{source}", destination], env_hash)
|
108
|
+
unless stat.exitstatus.zero?
|
109
|
+
raise "Error downloading content from container #{container_id}: #{err}"
|
108
110
|
end
|
109
111
|
rescue StandardError => e
|
110
112
|
raise Bolt::Node::FileError.new(e.message, 'WRITE_ERROR')
|
111
113
|
end
|
112
114
|
|
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
115
|
# Executes a Docker CLI command and parses the output in JSON format
|
131
116
|
#
|
132
117
|
# @param subcommand [String] The docker subcommand to run
|
@@ -135,14 +120,14 @@ module Bolt
|
|
135
120
|
# e.g. 'src' and 'dest' for `docker cp <src> <dest>
|
136
121
|
# @return [Object] Ruby object representation of the JSON string
|
137
122
|
private def execute_local_json_command(subcommand, arguments = [])
|
138
|
-
|
139
|
-
|
140
|
-
extract_json(
|
123
|
+
cmd = [subcommand, '--format', '{{json .}}'].concat(arguments)
|
124
|
+
out, _err, _stat = Bolt::Util.exec_docker(cmd, env_hash)
|
125
|
+
extract_json(out)
|
141
126
|
end
|
142
127
|
|
143
128
|
# Converts the JSON encoded STDOUT string from the docker cli into ruby objects
|
144
129
|
#
|
145
|
-
# @param
|
130
|
+
# @param stdout [String] The string to convert
|
146
131
|
# @return [Object] Ruby object representation of the JSON string
|
147
132
|
private def extract_json(stdout)
|
148
133
|
# The output from the docker format command is a JSON string per line.
|
data/lib/bolt/util.rb
CHANGED
@@ -332,6 +332,29 @@ module Bolt
|
|
332
332
|
end
|
333
333
|
end
|
334
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
|
+
# Formats a map of environment variables to be passed to a command that
|
347
|
+
# accepts repeated `--env` flags
|
348
|
+
#
|
349
|
+
# @param env_vars [Hash] A map of environment variables keys and their values
|
350
|
+
# @return [String]
|
351
|
+
def format_env_vars_for_cli(env_vars)
|
352
|
+
@env_vars = env_vars.each_with_object([]) do |(key, value), acc|
|
353
|
+
acc << "--env"
|
354
|
+
acc << "#{key}=#{value}"
|
355
|
+
end
|
356
|
+
end
|
357
|
+
|
335
358
|
def unix_basename(path)
|
336
359
|
raise Bolt::ValidationError, "path must be a String, received #{path.class} #{path}" unless path.is_a?(String)
|
337
360
|
path.split('/').last
|
data/lib/bolt/version.rb
CHANGED
@@ -17,7 +17,7 @@ module BoltSpec
|
|
17
17
|
|
18
18
|
# Nothing on the executor is 'public'
|
19
19
|
class MockExecutor
|
20
|
-
attr_reader :noop, :error_message, :in_parallel
|
20
|
+
attr_reader :noop, :error_message, :in_parallel, :transports
|
21
21
|
attr_accessor :run_as, :transport_features, :execute_any_plan
|
22
22
|
|
23
23
|
def initialize(modulepath)
|
@@ -91,6 +91,14 @@ module BoltSpec
|
|
91
91
|
result
|
92
92
|
end
|
93
93
|
|
94
|
+
def run_task_with(target_mapping, task, options = {}, _position = [])
|
95
|
+
resultsets = target_mapping.map do |target, arguments|
|
96
|
+
run_task([target], task, arguments, options)
|
97
|
+
end.compact
|
98
|
+
|
99
|
+
Bolt::ResultSet.new(resultsets.map(&:results).flatten)
|
100
|
+
end
|
101
|
+
|
94
102
|
def download_file(targets, source, destination, options = {}, _position = [])
|
95
103
|
result = nil
|
96
104
|
if (doub = @download_doubles[source] || @download_doubles[:default])
|
@@ -210,16 +218,6 @@ module BoltSpec
|
|
210
218
|
yield
|
211
219
|
end
|
212
220
|
|
213
|
-
def report_function_call(_function); end
|
214
|
-
|
215
|
-
def report_bundled_content(_mode, _name); end
|
216
|
-
|
217
|
-
def report_file_source(_plan_function, _source); end
|
218
|
-
|
219
|
-
def report_apply(_statements, _resources); end
|
220
|
-
|
221
|
-
def report_yaml_plan(_plan); end
|
222
|
-
|
223
221
|
def publish_event(event)
|
224
222
|
if event[:type] == :message
|
225
223
|
unless @stub_out_message
|
@@ -257,6 +255,87 @@ module BoltSpec
|
|
257
255
|
end.new(transport_features)
|
258
256
|
end
|
259
257
|
# End apply_prep mocking
|
258
|
+
|
259
|
+
# Evaluates a `parallelize()` block and returns the result. Normally,
|
260
|
+
# Bolt's executor wraps this in a Yarn for each object passed to the
|
261
|
+
# `parallelize()` function, and then executes them in parallel before
|
262
|
+
# returning the result from the block. However, in BoltSpec the block is
|
263
|
+
# executed for each object sequentially, and this function returns the
|
264
|
+
# result itself.
|
265
|
+
#
|
266
|
+
def create_yarn(scope, block, object, _index)
|
267
|
+
# Create the new scope
|
268
|
+
newscope = Puppet::Parser::Scope.new(scope.compiler)
|
269
|
+
local = Puppet::Parser::Scope::LocalScope.new
|
270
|
+
|
271
|
+
# Compress the current scopes into a single vars hash to add to the new scope
|
272
|
+
current_scope = scope.effective_symtable(true)
|
273
|
+
until current_scope.nil?
|
274
|
+
current_scope.instance_variable_get(:@symbols)&.each_pair { |k, v| local[k] = v }
|
275
|
+
current_scope = current_scope.parent
|
276
|
+
end
|
277
|
+
newscope.push_ephemerals([local])
|
278
|
+
|
279
|
+
begin
|
280
|
+
result = catch(:return) do
|
281
|
+
args = { block.parameters[0][1].to_s => object }
|
282
|
+
block.closure.call_by_name_with_scope(newscope, args, true)
|
283
|
+
end
|
284
|
+
|
285
|
+
# If we got a return from the block, get it's value
|
286
|
+
# Otherwise the result is the last line from the block
|
287
|
+
result = result.value if result.is_a?(Puppet::Pops::Evaluator::Return)
|
288
|
+
|
289
|
+
# Validate the result is a PlanResult
|
290
|
+
unless Puppet::Pops::Types::TypeParser.singleton.parse('Boltlib::PlanResult').instance?(result)
|
291
|
+
raise Bolt::InvalidParallelResult.new(result.to_s, *Puppet::Pops::PuppetStack.top_of_stack)
|
292
|
+
end
|
293
|
+
|
294
|
+
result
|
295
|
+
rescue Puppet::PreformattedError => e
|
296
|
+
if e.cause.is_a?(Bolt::Error)
|
297
|
+
e.cause
|
298
|
+
else
|
299
|
+
raise e
|
300
|
+
end
|
301
|
+
end
|
302
|
+
end
|
303
|
+
|
304
|
+
# BoltSpec already evaluated the `parallelize()` block for each object
|
305
|
+
# passed to the function, so these results can be returned as-is.
|
306
|
+
#
|
307
|
+
def round_robin(results)
|
308
|
+
results
|
309
|
+
end
|
310
|
+
|
311
|
+
# Public methods on Bolt::Executor that need to be mocked so there aren't
|
312
|
+
# "undefined method" errors.
|
313
|
+
|
314
|
+
def batch_execute(_targets); end
|
315
|
+
|
316
|
+
def finish_plan(_plan_result); end
|
317
|
+
|
318
|
+
def handle_event(_event); end
|
319
|
+
|
320
|
+
def prompt(_prompt, _options); end
|
321
|
+
|
322
|
+
def report_function_call(_function); end
|
323
|
+
|
324
|
+
def report_bundled_content(_mode, _name); end
|
325
|
+
|
326
|
+
def report_file_source(_plan_function, _source); end
|
327
|
+
|
328
|
+
def report_apply(_statements, _resources); end
|
329
|
+
|
330
|
+
def report_yaml_plan(_plan); end
|
331
|
+
|
332
|
+
def shutdown; end
|
333
|
+
|
334
|
+
def start_plan(_plan_context); end
|
335
|
+
|
336
|
+
def subscribe(_subscriber, _types = nil); end
|
337
|
+
|
338
|
+
def unsubscribe(_subscriber, _types = nil); end
|
260
339
|
end
|
261
340
|
end
|
262
341
|
end
|
@@ -13,6 +13,19 @@
|
|
13
13
|
#
|
14
14
|
plan puppet_connect::test_input_data(TargetSpec $targets = 'all') {
|
15
15
|
$targs = get_targets($targets)
|
16
|
+
$unique_plugins = $targs.group_by |$t| {$t.plugin_hooks['puppet_library']}
|
17
|
+
if ($unique_plugins.keys.length > 1) {
|
18
|
+
out::message('Multiple puppet_library plugin hooks detected')
|
19
|
+
$unique_plugins.each |$plug, $target_list| {
|
20
|
+
$target_message = if ($target_list.length > 10) {
|
21
|
+
"${target_list.length} targets"
|
22
|
+
} else {
|
23
|
+
$target_list.join(', ')
|
24
|
+
}
|
25
|
+
out::message("Plugin hook ${plug} configured for ${target_message}")
|
26
|
+
}
|
27
|
+
fail_plan("The puppet_library plugin config must be the same across all targets")
|
28
|
+
}
|
16
29
|
$targs.each |$target| {
|
17
30
|
case $target.transport {
|
18
31
|
'ssh': {
|
@@ -59,6 +72,15 @@ plan puppet_connect::test_input_data(TargetSpec $targets = 'all') {
|
|
59
72
|
fail_plan("Inventory contains target ${target} with unsupported transport, must be ssh or winrm")
|
60
73
|
}
|
61
74
|
}
|
75
|
+
|
76
|
+
# Bolt defaults to using the "module" based form of the puppet_agent plugin. Connect defaults
|
77
|
+
# to using the "task" based form as *only* the task based form in supported in Connect. This check
|
78
|
+
# ensures that if the default is not being used, only task based plugins are allowed.
|
79
|
+
$plugin = $target.plugin_hooks["puppet_library"]
|
80
|
+
$user_configured_plugin = $plugin != { "plugin"=> "puppet_agent", "stop_service"=> true }
|
81
|
+
if ($user_configured_plugin and $plugin["plugin"] != "task"){
|
82
|
+
fail_plan("Only task plugins are acceptable for puppet_library hook")
|
83
|
+
}
|
62
84
|
}
|
63
85
|
# The SSH/WinRM transports will report an 'unknown host' error for targets where
|
64
86
|
# 'host' is unknown so run_command's implementation will take care of raising that
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: bolt
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 3.
|
4
|
+
version: 3.5.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Puppet
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2021-03-
|
11
|
+
date: 2021-03-29 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: addressable
|
@@ -398,6 +398,7 @@ extra_rdoc_files: []
|
|
398
398
|
files:
|
399
399
|
- Puppetfile
|
400
400
|
- bolt-modules/boltlib/lib/puppet/datatypes/applyresult.rb
|
401
|
+
- bolt-modules/boltlib/lib/puppet/datatypes/containerresult.rb
|
401
402
|
- bolt-modules/boltlib/lib/puppet/datatypes/resourceinstance.rb
|
402
403
|
- bolt-modules/boltlib/lib/puppet/datatypes/result.rb
|
403
404
|
- bolt-modules/boltlib/lib/puppet/datatypes/resultset.rb
|
@@ -419,6 +420,7 @@ files:
|
|
419
420
|
- bolt-modules/boltlib/lib/puppet/functions/resolve_references.rb
|
420
421
|
- bolt-modules/boltlib/lib/puppet/functions/resource.rb
|
421
422
|
- bolt-modules/boltlib/lib/puppet/functions/run_command.rb
|
423
|
+
- bolt-modules/boltlib/lib/puppet/functions/run_container.rb
|
422
424
|
- bolt-modules/boltlib/lib/puppet/functions/run_plan.rb
|
423
425
|
- bolt-modules/boltlib/lib/puppet/functions/run_script.rb
|
424
426
|
- bolt-modules/boltlib/lib/puppet/functions/run_task.rb
|
@@ -474,6 +476,7 @@ files:
|
|
474
476
|
- lib/bolt/config/transport/remote.rb
|
475
477
|
- lib/bolt/config/transport/ssh.rb
|
476
478
|
- lib/bolt/config/transport/winrm.rb
|
479
|
+
- lib/bolt/container_result.rb
|
477
480
|
- lib/bolt/error.rb
|
478
481
|
- lib/bolt/executor.rb
|
479
482
|
- lib/bolt/inventory.rb
|