bolt 2.32.0 → 2.36.0

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of bolt might be problematic. Click here for more details.

Files changed (75) hide show
  1. checksums.yaml +4 -4
  2. data/Puppetfile +6 -6
  3. data/bolt-modules/boltlib/lib/puppet/datatypes/applyresult.rb +1 -0
  4. data/bolt-modules/boltlib/lib/puppet/functions/catch_errors.rb +1 -3
  5. data/bolt-modules/boltlib/lib/puppet/functions/download_file.rb +17 -6
  6. data/bolt-modules/boltlib/lib/puppet/functions/facts.rb +6 -0
  7. data/bolt-modules/boltlib/lib/puppet/functions/parallelize.rb +56 -0
  8. data/bolt-modules/boltlib/lib/puppet/functions/puppetdb_query.rb +2 -2
  9. data/bolt-modules/boltlib/lib/puppet/functions/run_command.rb +24 -6
  10. data/bolt-modules/boltlib/lib/puppet/functions/run_script.rb +27 -8
  11. data/bolt-modules/boltlib/lib/puppet/functions/run_task.rb +21 -1
  12. data/bolt-modules/boltlib/lib/puppet/functions/run_task_with.rb +18 -1
  13. data/bolt-modules/boltlib/lib/puppet/functions/upload_file.rb +24 -6
  14. data/guides/logging.txt +18 -0
  15. data/lib/bolt/analytics.rb +27 -8
  16. data/lib/bolt/apply_result.rb +3 -3
  17. data/lib/bolt/bolt_option_parser.rb +43 -15
  18. data/lib/bolt/cli.rb +79 -227
  19. data/lib/bolt/config.rb +131 -52
  20. data/lib/bolt/config/options.rb +46 -8
  21. data/lib/bolt/config/transport/base.rb +10 -19
  22. data/lib/bolt/config/transport/local.rb +0 -7
  23. data/lib/bolt/config/transport/options.rb +1 -1
  24. data/lib/bolt/config/transport/ssh.rb +8 -14
  25. data/lib/bolt/config/validator.rb +231 -0
  26. data/lib/bolt/error.rb +37 -3
  27. data/lib/bolt/executor.rb +103 -17
  28. data/lib/bolt/inventory/group.rb +2 -1
  29. data/lib/bolt/module_installer.rb +2 -1
  30. data/lib/bolt/module_installer/specs/forge_spec.rb +5 -4
  31. data/lib/bolt/module_installer/specs/git_spec.rb +4 -3
  32. data/lib/bolt/outputter/human.rb +21 -9
  33. data/lib/bolt/outputter/rainbow.rb +1 -1
  34. data/lib/bolt/pal.rb +48 -30
  35. data/lib/bolt/pal/yaml_plan.rb +11 -2
  36. data/lib/bolt/pal/yaml_plan/evaluator.rb +23 -1
  37. data/lib/bolt/pal/yaml_plan/loader.rb +14 -9
  38. data/lib/bolt/plan_creator.rb +160 -0
  39. data/lib/bolt/plugin.rb +1 -8
  40. data/lib/bolt/project.rb +30 -36
  41. data/lib/bolt/project_manager.rb +199 -0
  42. data/lib/bolt/{project_migrator/config.rb → project_manager/config_migrator.rb} +43 -5
  43. data/lib/bolt/{project_migrator/inventory.rb → project_manager/inventory_migrator.rb} +5 -5
  44. data/lib/bolt/{project_migrator/base.rb → project_manager/migrator.rb} +2 -2
  45. data/lib/bolt/{project_migrator/modules.rb → project_manager/module_migrator.rb} +3 -3
  46. data/lib/bolt/puppetdb/client.rb +3 -2
  47. data/lib/bolt/puppetdb/config.rb +9 -8
  48. data/lib/bolt/result.rb +23 -11
  49. data/lib/bolt/shell/bash.rb +12 -7
  50. data/lib/bolt/shell/powershell.rb +12 -7
  51. data/lib/bolt/task/run.rb +1 -1
  52. data/lib/bolt/transport/base.rb +18 -18
  53. data/lib/bolt/transport/docker.rb +23 -6
  54. data/lib/bolt/transport/orch.rb +23 -19
  55. data/lib/bolt/transport/orch/connection.rb +10 -3
  56. data/lib/bolt/transport/remote.rb +3 -3
  57. data/lib/bolt/transport/simple.rb +6 -6
  58. data/lib/bolt/transport/ssh/exec_connection.rb +6 -2
  59. data/lib/bolt/util.rb +19 -7
  60. data/lib/bolt/version.rb +1 -1
  61. data/lib/bolt/yarn.rb +23 -0
  62. data/lib/bolt_server/base_config.rb +3 -1
  63. data/lib/bolt_server/config.rb +3 -1
  64. data/lib/bolt_server/file_cache.rb +2 -0
  65. data/lib/bolt_server/schemas/partials/task.json +2 -2
  66. data/lib/bolt_server/transport_app.rb +42 -11
  67. data/lib/bolt_spec/plans/action_stubs/command_stub.rb +1 -1
  68. data/lib/bolt_spec/plans/action_stubs/script_stub.rb +1 -1
  69. data/lib/bolt_spec/plans/mock_executor.rb +9 -6
  70. data/libexec/apply_catalog.rb +1 -1
  71. data/libexec/custom_facts.rb +1 -1
  72. data/libexec/query_resources.rb +1 -1
  73. metadata +12 -14
  74. data/lib/bolt/project_migrator.rb +0 -80
  75. data/modules/secure_env_vars/plans/init.pp +0 -20
@@ -2,6 +2,7 @@
2
2
 
3
3
  require 'bolt/error'
4
4
  require 'bolt/util'
5
+ require 'bolt/config/validator'
5
6
  require 'bolt/config/transport/options'
6
7
 
7
8
  module Bolt
@@ -90,6 +91,14 @@ module Bolt
90
91
  self::OPTIONS
91
92
  end
92
93
 
94
+ def self.schema
95
+ {
96
+ type: Hash,
97
+ properties: self::TRANSPORT_OPTIONS.slice(*self::OPTIONS),
98
+ _plugin: true
99
+ }
100
+ end
101
+
93
102
  private def defaults
94
103
  unless defined? self.class::DEFAULTS
95
104
  raise NotImplementedError,
@@ -116,25 +125,7 @@ module Bolt
116
125
 
117
126
  # Validation defaults to just asserting the option types
118
127
  private def validate
119
- assert_type
120
- end
121
-
122
- # Validates that each option is the correct type. Types are loaded from the TRANSPORT_OPTIONS hash.
123
- private def assert_type
124
- @config.each_pair do |opt, val|
125
- types = Array(TRANSPORT_OPTIONS.dig(opt, :type)).compact
126
-
127
- next if val.nil? || types.empty? || types.include?(val.class)
128
-
129
- # Ruby doesn't have a Boolean class, so add it to the types list if TrueClass or FalseClass
130
- # are present.
131
- if types.include?(TrueClass) || types.include?(FalseClass)
132
- types = types - [TrueClass, FalseClass] + ['Boolean']
133
- end
134
-
135
- raise Bolt::ValidationError,
136
- "#{opt} must be of type #{types.join(', ')}; received #{val.class} #{val.inspect} "
137
- end
128
+ Bolt::Config::Validator.new.validate(@config.compact, self.class.schema.fetch(:properties))
138
129
  end
139
130
  end
140
131
  end
@@ -29,13 +29,6 @@ module Bolt
29
29
  if @config['interpreters']
30
30
  @config['interpreters'] = normalize_interpreters(@config['interpreters'])
31
31
  end
32
-
33
- if (run_as_cmd = @config['run-as-command'])
34
- unless run_as_cmd.all? { |n| n.is_a?(String) }
35
- raise Bolt::ValidationError,
36
- "run-as-command must be an Array of Strings, received #{run_as_cmd.class} #{run_as_cmd.inspect}"
37
- end
38
- end
39
32
  end
40
33
  end
41
34
  end
@@ -357,7 +357,7 @@ module Bolt
357
357
  description: "The URL of the host used for API requests.",
358
358
  format: "uri",
359
359
  _plugin: true,
360
- _example: "https://api.example.com"
360
+ _example: "https://api.example.com:8143"
361
361
  },
362
362
  "shell-command" => {
363
363
  type: String,
@@ -73,6 +73,14 @@ module Bolt
73
73
  %w[ssh-command native-ssh].concat(OPTIONS)
74
74
  end
75
75
 
76
+ def self.schema
77
+ {
78
+ type: Hash,
79
+ properties: self::TRANSPORT_OPTIONS.slice(*(self::OPTIONS + self::NATIVE_OPTIONS)),
80
+ _plugin: true
81
+ }
82
+ end
83
+
76
84
  private def filter(unfiltered)
77
85
  # Because we filter before merging config together it's impossible to
78
86
  # know whether both ssh-command *and* native-ssh will be specified
@@ -87,12 +95,6 @@ module Bolt
87
95
  super
88
96
 
89
97
  if (key_opt = @config['private-key'])
90
- unless key_opt.instance_of?(String) || (key_opt.instance_of?(Hash) && key_opt.include?('key-data'))
91
- raise Bolt::ValidationError,
92
- "private-key option must be a path to a private key file or a Hash containing the 'key-data', "\
93
- "received #{key_opt.class} #{key_opt}"
94
- end
95
-
96
98
  if key_opt.instance_of?(String)
97
99
  @config['private-key'] = File.expand_path(key_opt, @project)
98
100
 
@@ -114,14 +116,6 @@ module Bolt
114
116
  "Unsupported login-shell #{@config['login-shell']}. Supported shells are #{LOGIN_SHELLS.join(', ')}"
115
117
  end
116
118
 
117
- %w[encryption-algorithms host-key-algorithms kex-algorithms mac-algorithms run-as-command].each do |opt|
118
- next unless @config.key?(opt)
119
- unless @config[opt].all? { |n| n.is_a?(String) }
120
- raise Bolt::ValidationError,
121
- "#{opt} must be an Array of Strings, received #{@config[opt].inspect}"
122
- end
123
- end
124
-
125
119
  if @config['login-shell'] == 'powershell'
126
120
  %w[tty run-as].each do |key|
127
121
  if @config[key]
@@ -0,0 +1,231 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bolt/error'
4
+
5
+ # This class validates config against a schema, raising an error that includes
6
+ # details about any invalid configuration.
7
+ #
8
+ module Bolt
9
+ class Config
10
+ class Validator
11
+ attr_reader :deprecations, :warnings
12
+
13
+ def initialize
14
+ @errors = []
15
+ @deprecations = []
16
+ @warnings = []
17
+ @path = []
18
+ end
19
+
20
+ # This is the entry method for validating data against the schema.
21
+ # It loops over each key-value pair in the data hash and validates
22
+ # the value against the relevant schema definition.
23
+ #
24
+ def validate(data, schema, location = nil)
25
+ @location = location
26
+
27
+ validate_keys(data.keys, schema.keys)
28
+
29
+ data.each_pair do |key, value|
30
+ next unless schema.key?(key)
31
+
32
+ @path.push(key)
33
+
34
+ check_deprecated(key, schema[key], location)
35
+ validate_value(value, schema[key])
36
+ ensure
37
+ @path.pop
38
+ end
39
+
40
+ raise_error
41
+ end
42
+
43
+ # Adds a warning if the given option is deprecated.
44
+ #
45
+ def check_deprecated(key, definition, location)
46
+ if definition.key?(:_deprecation)
47
+ message = "Option '#{path}' "
48
+ message += "at #{location} " if location
49
+ message += "is deprecated. #{definition[:_deprecation]}"
50
+ @deprecations << { option: key, message: message }
51
+ end
52
+ end
53
+
54
+ # Raises a ValidationError if there are any errors. All error messages
55
+ # created during validation are concatenated into a single error
56
+ # message.
57
+ #
58
+ private def raise_error
59
+ return unless @errors.any?
60
+
61
+ message = "Invalid configuration"
62
+ message += " at #{@location}" if @location
63
+ message += ":\n"
64
+ message += @errors.map { |error| "\s\s#{error}" }.join("\n")
65
+
66
+ raise Bolt::ValidationError, message
67
+ end
68
+
69
+ # Validate an individual value. This performs validation that is
70
+ # common to all values, including type validation. After validating
71
+ # the value's type, the value is passed off to an individual
72
+ # validation method for the value's type.
73
+ #
74
+ private def validate_value(value, definition)
75
+ return if plugin_reference?(value, definition)
76
+ return unless valid_type?(value, definition)
77
+
78
+ case value
79
+ when Hash
80
+ validate_hash(value, definition)
81
+ when Array
82
+ validate_array(value, definition)
83
+ when String
84
+ validate_string(value, definition)
85
+ when Numeric
86
+ validate_number(value, definition)
87
+ end
88
+ end
89
+
90
+ # Validates a hash value, logging errors for any validations that fail.
91
+ # This will enumerate each key-value pair in the hash and validate each
92
+ # value individually.
93
+ #
94
+ private def validate_hash(value, definition)
95
+ properties = definition[:properties] ? definition[:properties].keys : []
96
+
97
+ if definition[:properties] && definition[:additionalProperties].nil?
98
+ validate_keys(value.keys, properties)
99
+ end
100
+
101
+ if definition[:required] && (definition[:required] - value.keys).any?
102
+ missing = definition[:required] - value.keys
103
+ @errors << "Value at '#{path}' is missing required keys #{missing.join(', ')}"
104
+ end
105
+
106
+ value.each_pair do |key, val|
107
+ @path.push(key)
108
+
109
+ if properties.include?(key)
110
+ validate_value(val, definition[:properties][key])
111
+ elsif definition[:additionalProperties]
112
+ validate_value(val, definition[:additionalProperties])
113
+ end
114
+ ensure
115
+ @path.pop
116
+ end
117
+ end
118
+
119
+ # Validates an array value, logging errors for any validations that fail.
120
+ # This will enumerate the items in the array and validate each item
121
+ # individually.
122
+ #
123
+ private def validate_array(value, definition)
124
+ if definition[:uniqueItems] && value.size != value.uniq.size
125
+ @errors << "Value at '#{path}' must not include duplicate elements"
126
+ return
127
+ end
128
+
129
+ return unless definition.key?(:items)
130
+
131
+ value.each_with_index do |item, index|
132
+ @path.push(index)
133
+ validate_value(item, definition[:items])
134
+ ensure
135
+ @path.pop
136
+ end
137
+ end
138
+
139
+ # Validates a string value, logging errors for any validations that fail.
140
+ #
141
+ private def validate_string(value, definition)
142
+ if definition.key?(:enum) && !definition[:enum].include?(value)
143
+ message = "Value at '#{path}' must be "
144
+ message += "one of " if definition[:enum].count > 1
145
+ message += definition[:enum].join(', ')
146
+ multitype_error(message, value, definition)
147
+ end
148
+ end
149
+
150
+ # Validates a numeric value, logging errors for any validations that fail.
151
+ #
152
+ private def validate_number(value, definition)
153
+ if definition.key?(:minimum) && value < definition[:minimum]
154
+ @errors << "Value at '#{path}' must be a minimum of #{definition[:minimum]}"
155
+ end
156
+ end
157
+
158
+ # Adds warnings for unknown config options.
159
+ #
160
+ private def validate_keys(keys, known_keys)
161
+ (keys - known_keys).each do |key|
162
+ message = "Unknown option '#{key}'"
163
+ message += " at '#{path}'" if @path.any?
164
+ message += " at #{@location}" if @location
165
+ message += "."
166
+ @warnings << message
167
+ end
168
+ end
169
+
170
+ # Returns true if a value is a plugin reference. This also validates whether
171
+ # a value can be a plugin reference in the first place. If the value is a
172
+ # plugin reference but cannot be one according to the schema, then this will
173
+ # log an error.
174
+ #
175
+ private def plugin_reference?(value, definition)
176
+ if value.is_a?(Hash) && value.key?('_plugin')
177
+ unless definition[:_plugin]
178
+ @errors << "Value at '#{path}' is a plugin reference, which is unsupported at "\
179
+ "this location"
180
+ end
181
+
182
+ true
183
+ else
184
+ false
185
+ end
186
+ end
187
+
188
+ # Asserts the type for each option against the type specified in the schema
189
+ # definition. The schema definition can specify multiple valid types, so the
190
+ # value needs to only match one of the types to be valid. Returns early if
191
+ # there is no type in the definition (in practice this shouldn't happen, but
192
+ # this will safeguard against any dev mistakes).
193
+ #
194
+ private def valid_type?(value, definition)
195
+ return unless definition.key?(:type)
196
+
197
+ types = Array(definition[:type])
198
+
199
+ if types.include?(value.class)
200
+ true
201
+ else
202
+ if types.include?(TrueClass) || types.include?(FalseClass)
203
+ types = types - [TrueClass, FalseClass] + ['Boolean']
204
+ end
205
+
206
+ @errors << "Value at '#{path}' must be of type #{types.join(' or ')}"
207
+
208
+ false
209
+ end
210
+ end
211
+
212
+ # Adds an error that includes additional helpful information for values
213
+ # that accept multiple types.
214
+ #
215
+ private def multitype_error(message, value, definition)
216
+ if Array(definition[:type]).count > 1
217
+ types = Array(definition[:type]) - [value.class]
218
+ message += " or must be of type #{types.join(' or ')}"
219
+ end
220
+
221
+ @errors << message
222
+ end
223
+
224
+ # Returns the formatted path for the key.
225
+ #
226
+ private def path
227
+ @path.join('.')
228
+ end
229
+ end
230
+ end
231
+ end
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'bolt/util'
4
+
3
5
  module Bolt
4
6
  class Error < RuntimeError
5
7
  attr_reader :kind, :details, :issue_code, :error_code
@@ -24,6 +26,10 @@ module Bolt
24
26
  h
25
27
  end
26
28
 
29
+ def add_filelineno(details)
30
+ @details.merge!(details) unless @details['file']
31
+ end
32
+
27
33
  def to_json(opts = nil)
28
34
  to_h.to_json(opts)
29
35
  end
@@ -33,13 +39,17 @@ module Bolt
33
39
  end
34
40
 
35
41
  def self.unknown_task(task)
36
- new("Could not find a task named \"#{task}\". For a list of available tasks, run \"bolt task show\"",
37
- 'bolt/unknown-task')
42
+ command = Bolt::Util.powershell? ? "Get-BoltTask" : "bolt task show"
43
+ new(
44
+ "Could not find a task named '#{task}'. For a list of available tasks, run '#{command}'.",
45
+ 'bolt/unknown-task'
46
+ )
38
47
  end
39
48
 
40
49
  def self.unknown_plan(plan)
50
+ command = Bolt::Util.powershell? ? "Get-BoltPlan" : "bolt plan show"
41
51
  new(
42
- "Could not find a plan named \"#{plan}\". For a list of available plans, run \"bolt plan show\"",
52
+ "Could not find a plan named '#{plan}'. For a list of available plans, run '#{command}'.",
43
53
  'bolt/unknown-plan'
44
54
  )
45
55
  end
@@ -80,6 +90,20 @@ module Bolt
80
90
  end
81
91
  end
82
92
 
93
+ class ParallelFailure < Bolt::Error
94
+ def initialize(results, failed_indices)
95
+ details = {
96
+ 'action' => 'parallelize',
97
+ 'failed_indices' => failed_indices,
98
+ 'results' => results
99
+ }
100
+ message = "Plan aborted: parallel block failed on #{failed_indices.length} target"
101
+ message += "s" unless failed_indices.length == 1
102
+ super(message, 'bolt/parallel-failure', details)
103
+ @error_code = 2
104
+ end
105
+ end
106
+
83
107
  class PlanFailure < Error
84
108
  def initialize(*args)
85
109
  super(*args)
@@ -121,6 +145,16 @@ module Bolt
121
145
  end
122
146
  end
123
147
 
148
+ class InvalidParallelResult < Error
149
+ def initialize(result_str, file, line)
150
+ super("Parallel block returned an invalid result: #{result_str}",
151
+ 'bolt/invalid-plan-result',
152
+ { 'file' => file,
153
+ 'line' => line,
154
+ 'result_string' => result_str })
155
+ end
156
+ end
157
+
124
158
  class ValidationError < Bolt::Error
125
159
  def initialize(msg)
126
160
  super(msg, 'bolt/validation-error')
@@ -17,6 +17,7 @@ require 'bolt/transport/orch'
17
17
  require 'bolt/transport/local'
18
18
  require 'bolt/transport/docker'
19
19
  require 'bolt/transport/remote'
20
+ require 'bolt/yarn'
20
21
 
21
22
  module Bolt
22
23
  TRANSPORTS = {
@@ -29,7 +30,7 @@ module Bolt
29
30
  }.freeze
30
31
 
31
32
  class Executor
32
- attr_reader :noop, :transports
33
+ attr_reader :noop, :transports, :in_parallel
33
34
  attr_accessor :run_as
34
35
 
35
36
  def initialize(concurrency = 1,
@@ -60,6 +61,7 @@ module Bolt
60
61
 
61
62
  @noop = noop
62
63
  @run_as = nil
64
+ @in_parallel = false
63
65
  @pool = if concurrency > 0
64
66
  Concurrent::ThreadPoolExecutor.new(name: 'exec', max_threads: concurrency)
65
67
  else
@@ -84,6 +86,14 @@ module Bolt
84
86
  self
85
87
  end
86
88
 
89
+ def unsubscribe(subscriber, types = nil)
90
+ if types.nil? || types.sort == @subscribers[subscriber]&.sort
91
+ @subscribers.delete(subscriber)
92
+ elsif @subscribers[subscriber].is_a?(Array)
93
+ @subscribers[subscriber] = @subscribers[subscriber] - types
94
+ end
95
+ end
96
+
87
97
  def publish_event(event)
88
98
  @subscribers.each do |subscriber, types|
89
99
  # If types isn't set or if the subscriber is subscribed to
@@ -246,54 +256,54 @@ module Bolt
246
256
  @logger.trace { "Failed to submit analytics event: #{e.message}" }
247
257
  end
248
258
 
249
- def with_node_logging(description, batch)
250
- @logger.info("#{description} on #{batch.map(&:safe_name)}")
259
+ def with_node_logging(description, batch, log_level = :info)
260
+ @logger.send(log_level, "#{description} on #{batch.map(&:safe_name)}")
251
261
  result = yield
252
- @logger.info(result.to_json)
262
+ @logger.send(log_level, result.to_json)
253
263
  result
254
264
  end
255
265
 
256
- def run_command(targets, command, options = {})
266
+ def run_command(targets, command, options = {}, position = [])
257
267
  description = options.fetch(:description, "command '#{command}'")
258
268
  log_action(description, targets) do
259
269
  options[:run_as] = run_as if run_as && !options.key?(:run_as)
260
270
 
261
271
  batch_execute(targets) do |transport, batch|
262
272
  with_node_logging("Running command '#{command}'", batch) do
263
- transport.batch_command(batch, command, options, &method(:publish_event))
273
+ transport.batch_command(batch, command, options, position, &method(:publish_event))
264
274
  end
265
275
  end
266
276
  end
267
277
  end
268
278
 
269
- def run_script(targets, script, arguments, options = {})
279
+ def run_script(targets, script, arguments, options = {}, position = [])
270
280
  description = options.fetch(:description, "script #{script}")
271
281
  log_action(description, targets) do
272
282
  options[:run_as] = run_as if run_as && !options.key?(:run_as)
273
283
 
274
284
  batch_execute(targets) do |transport, batch|
275
285
  with_node_logging("Running script #{script} with '#{arguments.to_json}'", batch) do
276
- transport.batch_script(batch, script, arguments, options, &method(:publish_event))
286
+ transport.batch_script(batch, script, arguments, options, position, &method(:publish_event))
277
287
  end
278
288
  end
279
289
  end
280
290
  end
281
291
 
282
- def run_task(targets, task, arguments, options = {})
292
+ def run_task(targets, task, arguments, options = {}, position = [], log_level = :info)
283
293
  description = options.fetch(:description, "task #{task.name}")
284
294
  log_action(description, targets) do
285
295
  options[:run_as] = run_as if run_as && !options.key?(:run_as)
286
296
  arguments['_task'] = task.name
287
297
 
288
298
  batch_execute(targets) do |transport, batch|
289
- with_node_logging("Running task #{task.name} with '#{arguments.to_json}'", batch) do
290
- transport.batch_task(batch, task, arguments, options, &method(:publish_event))
299
+ with_node_logging("Running task #{task.name} with '#{arguments.to_json}'", batch, log_level) do
300
+ transport.batch_task(batch, task, arguments, options, position, &method(:publish_event))
291
301
  end
292
302
  end
293
303
  end
294
304
  end
295
305
 
296
- def run_task_with(target_mapping, task, options = {})
306
+ def run_task_with(target_mapping, task, options = {}, position = [])
297
307
  targets = target_mapping.keys
298
308
  description = options.fetch(:description, "task #{task.name}")
299
309
 
@@ -303,26 +313,26 @@ module Bolt
303
313
 
304
314
  batch_execute(targets) do |transport, batch|
305
315
  with_node_logging("Running task #{task.name}'", batch) do
306
- transport.batch_task_with(batch, task, target_mapping, options, &method(:publish_event))
316
+ transport.batch_task_with(batch, task, target_mapping, options, position, &method(:publish_event))
307
317
  end
308
318
  end
309
319
  end
310
320
  end
311
321
 
312
- def upload_file(targets, source, destination, options = {})
322
+ def upload_file(targets, source, destination, options = {}, position = [])
313
323
  description = options.fetch(:description, "file upload from #{source} to #{destination}")
314
324
  log_action(description, targets) do
315
325
  options[:run_as] = run_as if run_as && !options.key?(:run_as)
316
326
 
317
327
  batch_execute(targets) do |transport, batch|
318
328
  with_node_logging("Uploading file #{source} to #{destination}", batch) do
319
- transport.batch_upload(batch, source, destination, options, &method(:publish_event))
329
+ transport.batch_upload(batch, source, destination, options, position, &method(:publish_event))
320
330
  end
321
331
  end
322
332
  end
323
333
  end
324
334
 
325
- def download_file(targets, source, destination, options = {})
335
+ def download_file(targets, source, destination, options = {}, position = [])
326
336
  description = options.fetch(:description, "file download from #{source} to #{destination}")
327
337
 
328
338
  begin
@@ -337,7 +347,7 @@ module Bolt
337
347
 
338
348
  batch_execute(targets) do |transport, batch|
339
349
  with_node_logging("Downloading file #{source} to #{destination}", batch) do
340
- transport.batch_download(batch, source, destination, options, &method(:publish_event))
350
+ transport.batch_download(batch, source, destination, options, position, &method(:publish_event))
341
351
  end
342
352
  end
343
353
  end
@@ -347,6 +357,82 @@ module Bolt
347
357
  plan.call_by_name_with_scope(scope, params, true)
348
358
  end
349
359
 
360
+ def create_yarn(scope, block, object, index)
361
+ fiber = Fiber.new do
362
+ # Create the new scope
363
+ newscope = Puppet::Parser::Scope.new(scope.compiler)
364
+ local = Puppet::Parser::Scope::LocalScope.new
365
+
366
+ # Compress the current scopes into a single vars hash to add to the new scope
367
+ current_scope = scope.effective_symtable(true)
368
+ until current_scope.nil?
369
+ current_scope.instance_variable_get(:@symbols)&.each_pair { |k, v| local[k] = v }
370
+ current_scope = current_scope.parent
371
+ end
372
+ newscope.push_ephemerals([local])
373
+
374
+ begin
375
+ result = catch(:return) do
376
+ args = { block.parameters[0][1].to_s => object }
377
+ block.closure.call_by_name_with_scope(newscope, args, true)
378
+ end
379
+
380
+ # If we got a return from the block, get it's value
381
+ # Otherwise the result is the last line from the block
382
+ result = result.value if result.is_a?(Puppet::Pops::Evaluator::Return)
383
+
384
+ # Validate the result is a PlanResult
385
+ unless Puppet::Pops::Types::TypeParser.singleton.parse('Boltlib::PlanResult').instance?(result)
386
+ raise Bolt::InvalidParallelResult.new(result.to_s, *Puppet::Pops::PuppetStack.top_of_stack)
387
+ end
388
+
389
+ result
390
+ rescue Puppet::PreformattedError => e
391
+ if e.cause.is_a?(Bolt::Error)
392
+ e.cause
393
+ else
394
+ raise e
395
+ end
396
+ end
397
+ end
398
+
399
+ Bolt::Yarn.new(fiber, index)
400
+ end
401
+
402
+ def handle_event(event)
403
+ case event[:type]
404
+ when :node_result
405
+ @thread_completed = true
406
+ end
407
+ end
408
+
409
+ def round_robin(skein)
410
+ subscribe(self, [:node_result])
411
+ results = Array.new(skein.length)
412
+ @in_parallel = true
413
+
414
+ until skein.empty?
415
+ @thread_completed = false
416
+ r = nil
417
+
418
+ skein.each do |yarn|
419
+ if yarn.alive?
420
+ r = yarn.resume
421
+ else
422
+ results[yarn.index] = yarn.value
423
+ skein.delete(yarn)
424
+ end
425
+ end
426
+
427
+ next unless r == 'unfinished'
428
+ sleep(0.1) until @thread_completed || skein.empty?
429
+ end
430
+
431
+ @in_parallel = false
432
+ unsubscribe(self, [:node_result])
433
+ results
434
+ end
435
+
350
436
  class TimeoutError < RuntimeError; end
351
437
 
352
438
  def wait_until_available(targets,