bolt 3.6.1 → 3.9.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 (67) hide show
  1. checksums.yaml +4 -4
  2. data/Puppetfile +3 -3
  3. data/bolt-modules/boltlib/lib/puppet/datatypes/applyresult.rb +26 -0
  4. data/bolt-modules/boltlib/lib/puppet/datatypes/containerresult.rb +27 -0
  5. data/bolt-modules/boltlib/lib/puppet/datatypes/future.rb +25 -0
  6. data/bolt-modules/boltlib/lib/puppet/datatypes/resourceinstance.rb +43 -0
  7. data/bolt-modules/boltlib/lib/puppet/datatypes/result.rb +29 -0
  8. data/bolt-modules/boltlib/lib/puppet/datatypes/resultset.rb +34 -0
  9. data/bolt-modules/boltlib/lib/puppet/datatypes/target.rb +55 -0
  10. data/bolt-modules/boltlib/lib/puppet/functions/add_to_group.rb +1 -0
  11. data/bolt-modules/boltlib/lib/puppet/functions/apply_prep.rb +10 -6
  12. data/bolt-modules/boltlib/lib/puppet/functions/background.rb +61 -0
  13. data/bolt-modules/boltlib/lib/puppet/functions/download_file.rb +5 -9
  14. data/bolt-modules/boltlib/lib/puppet/functions/parallelize.rb +29 -13
  15. data/bolt-modules/boltlib/lib/puppet/functions/puppetdb_command.rb +66 -0
  16. data/bolt-modules/boltlib/lib/puppet/functions/remove_from_group.rb +1 -0
  17. data/bolt-modules/boltlib/lib/puppet/functions/run_command.rb +5 -15
  18. data/bolt-modules/boltlib/lib/puppet/functions/run_script.rb +10 -18
  19. data/bolt-modules/boltlib/lib/puppet/functions/run_task.rb +5 -17
  20. data/bolt-modules/boltlib/lib/puppet/functions/run_task_with.rb +5 -15
  21. data/bolt-modules/boltlib/lib/puppet/functions/upload_file.rb +10 -18
  22. data/bolt-modules/boltlib/lib/puppet/functions/wait.rb +91 -0
  23. data/bolt-modules/boltlib/lib/puppet/functions/write_file.rb +1 -0
  24. data/bolt-modules/boltlib/types/planresult.pp +1 -0
  25. data/bolt-modules/ctrl/lib/puppet/functions/ctrl/do_until.rb +2 -0
  26. data/bolt-modules/file/lib/puppet/functions/file/exists.rb +9 -3
  27. data/bolt-modules/file/lib/puppet/functions/file/read.rb +6 -2
  28. data/bolt-modules/file/lib/puppet/functions/file/readable.rb +8 -3
  29. data/guides/guide.txt +17 -0
  30. data/guides/inventory.txt +5 -0
  31. data/guides/links.txt +13 -0
  32. data/guides/targets.txt +29 -0
  33. data/guides/transports.txt +23 -0
  34. data/lib/bolt/applicator.rb +4 -3
  35. data/lib/bolt/bolt_option_parser.rb +353 -227
  36. data/lib/bolt/catalog.rb +2 -1
  37. data/lib/bolt/cli.rb +94 -36
  38. data/lib/bolt/config/options.rb +2 -1
  39. data/lib/bolt/config/transport/docker.rb +5 -1
  40. data/lib/bolt/config/transport/lxd.rb +1 -1
  41. data/lib/bolt/config/transport/options.rb +2 -1
  42. data/lib/bolt/config/transport/podman.rb +5 -1
  43. data/lib/bolt/error.rb +11 -1
  44. data/lib/bolt/executor.rb +51 -72
  45. data/lib/bolt/fiber_executor.rb +141 -0
  46. data/lib/bolt/inventory.rb +5 -4
  47. data/lib/bolt/inventory/inventory.rb +3 -2
  48. data/lib/bolt/logger.rb +1 -1
  49. data/lib/bolt/module_installer/specs.rb +1 -1
  50. data/lib/bolt/module_installer/specs/git_spec.rb +10 -6
  51. data/lib/bolt/outputter/human.rb +59 -29
  52. data/lib/bolt/outputter/json.rb +8 -4
  53. data/lib/bolt/pal.rb +64 -3
  54. data/lib/bolt/pal/yaml_plan/step.rb +4 -2
  55. data/lib/bolt/plan_creator.rb +2 -2
  56. data/lib/bolt/plan_future.rb +66 -0
  57. data/lib/bolt/puppetdb/client.rb +54 -0
  58. data/lib/bolt/result.rb +5 -0
  59. data/lib/bolt/transport/docker/connection.rb +7 -4
  60. data/lib/bolt/transport/lxd/connection.rb +4 -0
  61. data/lib/bolt/transport/podman/connection.rb +4 -0
  62. data/lib/bolt/transport/ssh/connection.rb +3 -6
  63. data/lib/bolt/util.rb +73 -1
  64. data/lib/bolt/version.rb +1 -1
  65. data/lib/bolt_spec/plans/mock_executor.rb +42 -45
  66. metadata +12 -3
  67. data/lib/bolt/yarn.rb +0 -23
data/lib/bolt/pal.rb CHANGED
@@ -607,13 +607,74 @@ module Bolt
607
607
  end
608
608
  end
609
609
 
610
- def run_plan(plan_name, params, executor = nil, inventory = nil, pdb_client = nil, applicator = nil)
610
+ def run_plan(plan_name, params, executor, inventory = nil, pdb_client = nil, applicator = nil)
611
+ # Start the round robin inside the plan compiler, so that
612
+ # backgrounded tasks can finish once the main plan exits
611
613
  in_plan_compiler(executor, inventory, pdb_client, applicator) do |compiler|
612
- r = compiler.call_function('run_plan', plan_name, params.merge('_bolt_api_call' => true))
613
- Bolt::PlanResult.from_pcore(r, 'success')
614
+ # Create a Fiber for the main plan. This will be run along with any
615
+ # other Fibers created during the plan run in the round_robin, with the
616
+ # main plan always taking precedence in being resumed.
617
+ future = executor.create_future(name: plan_name) do |_scope|
618
+ r = compiler.call_function('run_plan', plan_name, params.merge('_bolt_api_call' => true))
619
+ Bolt::PlanResult.from_pcore(r, 'success')
620
+ rescue Bolt::Error => e
621
+ Bolt::PlanResult.new(e, 'failure')
622
+ end
623
+
624
+ # Round robin until all Fibers, including the main plan, have finished.
625
+ # This will stay alive until backgrounded tasks have finished.
626
+ executor.round_robin until executor.plan_complete?
627
+
628
+ # Return the result from the main plan
629
+ future.value
614
630
  end
615
631
  rescue Bolt::Error => e
616
632
  Bolt::PlanResult.new(e, 'failure')
617
633
  end
634
+
635
+ def lookup(key, targets, inventory, executor, _concurrency)
636
+ # Install the puppet-agent package and collect facts. Facts are
637
+ # automatically added to the targets.
638
+ in_plan_compiler(executor, inventory, nil) do |compiler|
639
+ compiler.call_function('apply_prep', targets)
640
+ end
641
+
642
+ overrides = {
643
+ bolt_inventory: inventory,
644
+ bolt_project: @project
645
+ }
646
+
647
+ # Do a lookup with a catalog compiler, which uses the 'hierarchy' key in
648
+ # Hiera config.
649
+ results = targets.map do |target|
650
+ node = Puppet::Node.from_data_hash(
651
+ 'name' => target.name,
652
+ 'parameters' => { 'clientcert' => target.name }
653
+ )
654
+
655
+ trusted = Puppet::Context::TrustedInformation.local(node).to_h
656
+
657
+ env_conf = {
658
+ modulepath: @modulepath.full_modulepath,
659
+ facts: target.facts,
660
+ variables: target.vars
661
+ }
662
+
663
+ with_puppet_settings do
664
+ Puppet::Pal.in_tmp_environment(target.name, **env_conf) do |pal|
665
+ Puppet.override(overrides) do
666
+ Puppet.lookup(:pal_current_node).trusted_data = trusted
667
+ pal.with_catalog_compiler do |compiler|
668
+ Bolt::Result.for_lookup(target, key, compiler.call_function('lookup', key))
669
+ rescue StandardError => e
670
+ Bolt::Result.from_exception(target, e)
671
+ end
672
+ end
673
+ end
674
+ end
675
+ end
676
+
677
+ Bolt::ResultSet.new(results)
678
+ end
618
679
  end
619
680
  end
@@ -122,9 +122,11 @@ module Bolt
122
122
  raise StepError.new("Parameters key must be a hash", body['name'], step_number)
123
123
  end
124
124
 
125
- metaparams = option_keys.map { |key| "_#{key}" }
125
+ metaparams = body['parameters'].keys
126
+ .select { |key| key.start_with?('_') }
127
+ .map { |key| key.sub(/^_/, '') }
126
128
 
127
- if (dups = body['parameters'].keys & metaparams).any?
129
+ if (dups = body.keys & metaparams).any?
128
130
  raise StepError.new(
129
131
  "Cannot specify metaparameters when using top-level keys with same name: #{dups.join(', ')}",
130
132
  body['name'],
@@ -36,8 +36,8 @@ module Bolt
36
36
  prefix, _, basename = segment_plan_name(plan_name)
37
37
 
38
38
  unless prefix == project.name
39
- message = "First segment of plan name '#{plan_name}' must match project name '#{project.name}'. "\
40
- "Did you mean '#{project.name}::#{plan_name}'?"
39
+ message = "Incomplete plan name: A plan name must be prefixed with the name of the "\
40
+ "project or module. Did you mean '#{project.name}::#{plan_name}'?"
41
41
 
42
42
  raise Bolt::ValidationError, message
43
43
  end
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'fiber'
4
+
5
+ module Bolt
6
+ class PlanFuture
7
+ attr_reader :fiber, :id
8
+ attr_accessor :value
9
+
10
+ def initialize(fiber, id, name = nil)
11
+ @fiber = fiber
12
+ @id = id
13
+ @name = name
14
+ @value = nil
15
+ end
16
+
17
+ def name
18
+ @name || @id
19
+ end
20
+
21
+ def to_s
22
+ "Future '#{name}'"
23
+ end
24
+
25
+ def alive?
26
+ fiber.alive?
27
+ end
28
+
29
+ def raise(exception)
30
+ # Make sure the value gets set
31
+ @value = exception
32
+ # This was introduced in Ruby 2.7
33
+ begin
34
+ # Raise an exception to kill the Fiber. If the Fiber has not been
35
+ # resumed yet, or is already terminated this will raise a FiberError.
36
+ # We don't especially care about the FiberError, as long as the Fiber
37
+ # doesn't report itself as alive.
38
+ fiber.raise(exception)
39
+ rescue FiberError
40
+ # If the Fiber is still alive, resume it with a block to raise the
41
+ # exception which will terminate it.
42
+ if fiber.alive?
43
+ fiber.resume { raise(exception) }
44
+ end
45
+ end
46
+ end
47
+
48
+ def resume
49
+ if fiber.alive?
50
+ @value = fiber.resume
51
+ else
52
+ @value
53
+ end
54
+ end
55
+
56
+ def state
57
+ if fiber.alive?
58
+ "running"
59
+ elsif value.is_a?(Exception)
60
+ "error"
61
+ else
62
+ "done"
63
+ end
64
+ end
65
+ end
66
+ end
@@ -95,6 +95,60 @@ module Bolt
95
95
  make_query(query, path)
96
96
  end
97
97
 
98
+ # Sends a command to PuppetDB using version 1 of the commands API.
99
+ # https://puppet.com/docs/puppetdb/latest/api/command/v1/commands.html
100
+ #
101
+ # @param command [String] The command to invoke.
102
+ # @param version [Integer] The version of the command to invoke.
103
+ # @param payload [Hash] The payload to send with the command.
104
+ # @return A UUID identifying the submitted command.
105
+ #
106
+ def send_command(command, version, payload)
107
+ command = command.dup.force_encoding('utf-8')
108
+ body = JSON.generate(payload)
109
+
110
+ # PDB requires the following query parameters to the POST request.
111
+ # Error early if there's no certname, as PDB does not return a
112
+ # message indicating it's required.
113
+ unless payload['certname']
114
+ raise Bolt::Error.new(
115
+ "Payload must include 'certname', unable to invoke command.",
116
+ 'bolt/pdb-command'
117
+ )
118
+ end
119
+
120
+ url = uri.tap do |u|
121
+ u.path = 'pdb/cmd/v1'
122
+ u.query_values = { 'command' => command,
123
+ 'version' => version,
124
+ 'certname' => payload['certname'] }
125
+ end
126
+
127
+ # Send the command to PDB
128
+ begin
129
+ @logger.debug("Sending PuppetDB command '#{command}' to #{url}")
130
+ response = http_client.post(url.to_s, body: body, header: headers)
131
+ rescue StandardError => e
132
+ raise Bolt::PuppetDBFailoverError, "Failed to invoke PuppetDB command: #{e}"
133
+ end
134
+
135
+ @logger.debug("Got response code #{response.code} from PuppetDB")
136
+ if response.code != 200
137
+ raise Bolt::PuppetDBError, "Failed to invoke PuppetDB command: #{response.body}"
138
+ end
139
+
140
+ # Return the UUID string from the response body
141
+ begin
142
+ JSON.parse(response.body).fetch('uuid', nil)
143
+ rescue JSON::ParserError
144
+ raise Bolt::PuppetDBError, "Unable to parse response as JSON: #{response.body}"
145
+ end
146
+ rescue Bolt::PuppetDBFailoverError => e
147
+ @logger.error("Request to puppetdb at #{@current_url} failed with #{e}.")
148
+ reject_url
149
+ send_command(command, version, payload)
150
+ end
151
+
98
152
  def http_client
99
153
  return @http if @http
100
154
  # lazy-load expensive gem code
data/lib/bolt/result.rb CHANGED
@@ -28,6 +28,11 @@ module Bolt
28
28
  %w[file line].zip(position).to_h.compact
29
29
  end
30
30
 
31
+ def self.for_lookup(target, key, value)
32
+ val = { 'value' => value }
33
+ new(target, value: val, action: 'lookup', object: key)
34
+ end
35
+
31
36
  def self.for_command(target, value, action, command, position)
32
37
  details = create_details(position)
33
38
  unless value['exit_code'] == 0
@@ -27,6 +27,10 @@ module Bolt
27
27
  end
28
28
  end
29
29
 
30
+ def reset_cwd?
31
+ true
32
+ end
33
+
30
34
  # The full ID of the target container
31
35
  #
32
36
  # @return [String] The full ID of the target container
@@ -46,8 +50,8 @@ module Bolt
46
50
  def connect
47
51
  # We don't actually have a connection, but we do need to
48
52
  # check that the container exists and is running.
49
- output = execute_local_json_command('ps')
50
- index = output.find_index { |item| item["ID"] == target.host || item["Names"] == target.host }
53
+ output = execute_local_json_command('ps', ['--no-trunc'])
54
+ index = output.find_index { |item| item["ID"].start_with?(target.host) || item["Names"] == target.host }
51
55
  raise "Could not find a container with name or ID matching '#{target.host}'" if index.nil?
52
56
  # Now find the indepth container information
53
57
  output = execute_local_json_command('inspect', [output[index]["ID"]])
@@ -74,7 +78,6 @@ module Bolt
74
78
  # CODEREVIEW: Is it always safe to pass --interactive?
75
79
  args += %w[--interactive]
76
80
  args += %w[--tty] if target.options['tty']
77
- args += %W[--env DOCKER_HOST=#{@docker_host}] if @docker_host
78
81
  args += @env_vars if @env_vars
79
82
 
80
83
  if target.options['shell-command'] && !target.options['shell-command'].empty?
@@ -86,7 +89,7 @@ module Bolt
86
89
  docker_command = %w[docker exec] + args + [container_id] + Shellwords.split(command)
87
90
  @logger.trace { "Executing: #{docker_command.join(' ')}" }
88
91
 
89
- Open3.popen3(*docker_command)
92
+ Open3.popen3(env_hash, *docker_command)
90
93
  rescue StandardError
91
94
  @logger.trace { "Command aborted" }
92
95
  raise
@@ -23,6 +23,10 @@ module Bolt
23
23
  Bolt::Shell::Bash.new(target, self)
24
24
  end
25
25
 
26
+ def reset_cwd?
27
+ true
28
+ end
29
+
26
30
  def container_id
27
31
  "#{@target.transport_config['remote']}:#{@target.host}"
28
32
  end
@@ -30,6 +30,10 @@ module Bolt
30
30
  end
31
31
  end
32
32
 
33
+ def reset_cwd?
34
+ true
35
+ end
36
+
33
37
  def connect
34
38
  # We don't actually have a connection, but we do need to
35
39
  # check that the container exists and is running.
@@ -39,8 +39,6 @@ module Bolt
39
39
  end
40
40
  end
41
41
 
42
- PAGEANT_NAME = "Pageant\0".encode(Encoding::UTF_16LE)
43
-
44
42
  def connect
45
43
  options = {
46
44
  logger: @transport_logger,
@@ -115,10 +113,9 @@ module Bolt
115
113
  options[:use_agent] = false
116
114
  end
117
115
  elsif Bolt::Util.windows?
118
- require 'Win32API' # case matters in this require!
119
- # https://docs.microsoft.com/en-us/windows/desktop/api/winuser/nf-winuser-findwindoww
120
- @find_window ||= Win32API.new('user32', 'FindWindowW', %w[P P], 'L')
121
- if @find_window.call(nil, PAGEANT_NAME).to_i == 0
116
+ pageant = Net::SSH::Authentication::Pageant::Win.FindWindow("Pageant", "Pageant")
117
+ # If pageant is not running
118
+ if pageant.to_i == 0
122
119
  @logger.debug { "Disabling use_agent in net-ssh: pageant process not running" }
123
120
  options[:use_agent] = false
124
121
  end
data/lib/bolt/util.rb CHANGED
@@ -78,11 +78,83 @@ module Bolt
78
78
  end
79
79
 
80
80
  def first_runs_free
81
+ # If this fails, use the system path instead
82
+ FileUtils.mkdir_p(Bolt::Config.user_path)
81
83
  Bolt::Config.user_path + '.first_runs_free'
84
+ rescue StandardError
85
+ begin
86
+ # If using the system path fails, then don't bother with the welcome
87
+ # message
88
+ FileUtils.mkdir_p(Bolt::Config.system_path)
89
+ Bolt::Config.system_path + '.first_runs_free'
90
+ rescue StandardError
91
+ nil
92
+ end
82
93
  end
83
94
 
84
95
  def first_run?
85
- Bolt::Config.user_path && !File.exist?(first_runs_free)
96
+ !first_runs_free.nil? &&
97
+ !File.exist?(first_runs_free)
98
+ end
99
+
100
+ # If Puppet is loaded, we aleady have the path to the module and should
101
+ # just get it. This takes the path to a file provided by the user and a
102
+ # Puppet Parser scope object and tries to find the file, either as an
103
+ # absolute path or Puppet module syntax lookup. Returns the path to the
104
+ # file if found, or nil.
105
+ #
106
+ def find_file_from_scope(file, scope, fallback = false)
107
+ # If we got an absolute path, just return that.
108
+ return file if Pathname.new(file).absolute?
109
+
110
+ module_name, file_pattern = Bolt::Util.split_path(file)
111
+ # Get the absolute path to the module root from the scope
112
+ mod_path = scope.compiler.environment.module(module_name)&.path
113
+
114
+ # Search the module for the file, falling back to new-style paths if enabled.
115
+ find_file_in_module(mod_path, file_pattern, fallback) if mod_path
116
+ end
117
+
118
+ # This method is used by Bolt to find files when provided a
119
+ # module-style path without loading Puppet. It takes the absolute path to
120
+ # the module root and the module-style path minus the module name and
121
+ # searches subdirectories in the module in order of precedence.
122
+ #
123
+ def find_file_in_module(module_path, module_file, fallback = false)
124
+ # If the first part of the path is 'scripts' or 'files', the path may
125
+ # be a new-style file location and should fall back to the new path.
126
+ subdir_or_file = split_path(module_file).first
127
+ case subdir_or_file
128
+ # For any subdirs that may indicate the user passed a new-style path,
129
+ # first look in 'mymod/files/<relative_path>' (old-style) then fall
130
+ # back to 'mymod/<relative_path>' (new-style) if enabled.
131
+ when 'scripts', 'files'
132
+ search_module(module_path, module_file, fallback)
133
+ else
134
+ # If the path definitely isn't new-style, only look in the 'files/'
135
+ # directory.
136
+ search_module(module_path, module_file)
137
+ end
138
+ end
139
+
140
+ # This searches a module for files under 'files/' or 'scripts/',
141
+ # optionally falling back to the new style of file loading. It takes the
142
+ # absolute path to the module root, the relative path provided by the
143
+ # user, and whether to fall back to the new-style script loading if the
144
+ # file isn't found in 'files/'.
145
+ #
146
+ private def search_module(module_path, module_file, fallback = false)
147
+ if File.exist?(File.join(module_path, 'files', module_file))
148
+ File.join(module_path, 'files', module_file)
149
+ elsif File.exist?(File.join(module_path, module_file)) && fallback
150
+ File.join(module_path, module_file)
151
+ end
152
+ end
153
+
154
+ # Copied directly from puppet/lib/puppet/parser/files.rb
155
+ #
156
+ def split_path(path)
157
+ path.split(File::SEPARATOR, 2)
86
158
  end
87
159
 
88
160
  # Accepts a path with either 'plans' or 'tasks' in it and determines
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.6.1'
4
+ VERSION = '3.9.0'
5
5
  end
@@ -17,13 +17,12 @@ module BoltSpec
17
17
 
18
18
  # Nothing on the executor is 'public'
19
19
  class MockExecutor
20
- attr_reader :noop, :error_message, :in_parallel, :transports, :future
20
+ attr_reader :noop, :error_message, :transports, :future
21
21
  attr_accessor :run_as, :transport_features, :execute_any_plan
22
22
 
23
23
  def initialize(modulepath)
24
24
  @noop = false
25
25
  @run_as = nil
26
- @in_parallel = false
27
26
  @future = {}
28
27
  @error_message = nil
29
28
  @allow_apply = false
@@ -38,6 +37,7 @@ module BoltSpec
38
37
  @execute_any_plan = true
39
38
  # plans that are allowed to be executed by the @executor_real
40
39
  @allowed_exec_plans = {}
40
+ @id = 0
41
41
  end
42
42
 
43
43
  def module_file_id(file)
@@ -257,58 +257,53 @@ module BoltSpec
257
257
  end
258
258
  # End apply_prep mocking
259
259
 
260
- # Evaluates a `parallelize()` block and returns the result. Normally,
261
- # Bolt's executor wraps this in a Yarn for each object passed to the
262
- # `parallelize()` function, and then executes them in parallel before
263
- # returning the result from the block. However, in BoltSpec the block is
264
- # executed for each object sequentially, and this function returns the
265
- # result itself.
266
- #
267
- def create_yarn(scope, block, object, _index)
268
- # Create the new scope
269
- newscope = Puppet::Parser::Scope.new(scope.compiler)
270
- local = Puppet::Parser::Scope::LocalScope.new
271
-
272
- # Compress the current scopes into a single vars hash to add to the new scope
273
- current_scope = scope.effective_symtable(true)
274
- until current_scope.nil?
275
- current_scope.instance_variable_get(:@symbols)&.each_pair { |k, v| local[k] = v }
276
- current_scope = current_scope.parent
277
- end
278
- newscope.push_ephemerals([local])
279
-
280
- begin
281
- result = catch(:return) do
282
- args = { block.parameters[0][1].to_s => object }
283
- block.closure.call_by_name_with_scope(newscope, args, true)
284
- end
285
-
286
- # If we got a return from the block, get it's value
287
- # Otherwise the result is the last line from the block
288
- result = result.value if result.is_a?(Puppet::Pops::Evaluator::Return)
260
+ # Parallel function mocking
261
+ def run_in_thread
262
+ yield
263
+ end
289
264
 
290
- # Validate the result is a PlanResult
291
- unless Puppet::Pops::Types::TypeParser.singleton.parse('Boltlib::PlanResult').instance?(result)
292
- raise Bolt::InvalidParallelResult.new(result.to_s, *Puppet::Pops::PuppetStack.top_of_stack)
293
- end
265
+ def in_parallel?
266
+ false
267
+ end
294
268
 
295
- result
296
- rescue Puppet::PreformattedError => e
297
- if e.cause.is_a?(Bolt::Error)
298
- e.cause
299
- else
300
- raise e
269
+ def create_future(scope: nil, name: nil)
270
+ newscope = nil
271
+ if scope
272
+ # Create the new scope
273
+ newscope = Puppet::Parser::Scope.new(scope.compiler)
274
+ local = Puppet::Parser::Scope::LocalScope.new
275
+
276
+ # Compress the current scopes into a single vars hash to add to the new scope
277
+ current_scope = scope.effective_symtable(true)
278
+ until current_scope.nil?
279
+ current_scope.instance_variable_get(:@symbols)&.each_pair { |k, v| local[k] = v }
280
+ current_scope = current_scope.parent
301
281
  end
282
+ newscope.push_ephemerals([local])
302
283
  end
284
+
285
+ # Execute "futures" serially when running in BoltSpec
286
+ result = yield newscope
287
+ @id += 1
288
+ future = Bolt::PlanFuture.new(nil, @id, name: name)
289
+ future.value = result
290
+ future
303
291
  end
304
292
 
305
- # BoltSpec already evaluated the `parallelize()` block for each object
306
- # passed to the function, so these results can be returned as-is.
307
- #
308
- def round_robin(results)
293
+ def wait(results, _timeout, **_kwargs)
309
294
  results
310
295
  end
311
296
 
297
+ # Since Futures are executed immediately once created, this will always
298
+ # be true by the time it's called.
299
+ def plan_complete?
300
+ true
301
+ end
302
+
303
+ def plan_futures
304
+ []
305
+ end
306
+
312
307
  # Public methods on Bolt::Executor that need to be mocked so there aren't
313
308
  # "undefined method" errors.
314
309
 
@@ -337,6 +332,8 @@ module BoltSpec
337
332
  def subscribe(_subscriber, _types = nil); end
338
333
 
339
334
  def unsubscribe(_subscriber, _types = nil); end
335
+
336
+ def round_robin; end
340
337
  end
341
338
  end
342
339
  end