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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2e66e28078af9324bb7d5668ee5079535d829913a2dcb4bce65daa4d471d4524
4
- data.tar.gz: a821d82c490030be200c0d4ab5dc4a56a1abf1aa862d69d1e4c6a4595e47952d
3
+ metadata.gz: 41eb3934a2ddce5d1021b66f5aca6eccfdb91bdedd5dc08cfdc1cfb4664f91e8
4
+ data.tar.gz: 8a04142fd9de53e8812cff0c540098a8548a8b18245e300835896b457a86d348
5
5
  SHA512:
6
- metadata.gz: 120dd18c9478c105387f2ad3634276cbaae05fe3638cc01d378aa9cbb1c423bf737991b7278ab4f96eb0a7cca39cd3d259cbba3d1734cea6ac071ba0695da901
7
- data.tar.gz: 61171ff383d06883e6f6ffa0cafc423a80d41ea0f2d45c3c0e4b76c7037a4626ad7a4b8b625e3a013e077b9e96cdafaab06cad6d5a490dae392af3fbb410d0fe
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.6.0'
35
+ mod 'puppetlabs-aws_inventory', '0.7.0'
36
36
  mod 'puppetlabs-azure_inventory', '0.5.0'
37
- mod 'puppetlabs-gcloud_inventory', '0.2.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
@@ -12,5 +12,6 @@ type Boltlib::PlanResult = Variant[Boolean,
12
12
  ResultSet,
13
13
  Target,
14
14
  ResourceInstance,
15
+ ContainerResult,
15
16
  Array[Boltlib::PlanResult],
16
17
  Hash[String, Boltlib::PlanResult]]
@@ -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
@@ -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 = env_vars.each_with_object([]) do |env_var, acc|
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
- _stdout, stderr, status = execute_local_command('cp', [source, "#{container_id}:#{destination}"])
92
- unless status.exitstatus.zero?
93
- raise "Error writing to container #{container_id}: #{stderr}"
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
- _stdout, stderr, status = execute_local_command('cp', ["#{container_id}:#{source}", destination])
106
- unless status.exitstatus.zero?
107
- raise "Error downloading content from container #{container_id}: #{stderr}"
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
- command_options = ['--format', '{{json .}}'].concat(arguments)
139
- stdout, _stderr, _status = execute_local_command(subcommand, command_options)
140
- extract_json(stdout)
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 stdout_string [String] The string to convert
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Bolt
4
- VERSION = '3.4.0'
4
+ VERSION = '3.5.0'
5
5
  end
@@ -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.0
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-23 00:00:00.000000000 Z
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