bolt 3.13.0 → 3.14.1

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 (40) hide show
  1. checksums.yaml +4 -4
  2. data/Puppetfile +1 -1
  3. data/bolt-modules/boltlib/lib/puppet/functions/background.rb +2 -1
  4. data/bolt-modules/boltlib/lib/puppet/functions/parallelize.rb +5 -1
  5. data/bolt-modules/boltlib/lib/puppet/functions/run_plan.rb +13 -0
  6. data/bolt-modules/boltlib/lib/puppet/functions/wait.rb +47 -7
  7. data/bolt-modules/out/lib/puppet/functions/out/message.rb +4 -2
  8. data/bolt-modules/out/lib/puppet/functions/out/verbose.rb +4 -2
  9. data/lib/bolt/analytics.rb +1 -1
  10. data/lib/bolt/bolt_option_parser.rb +4 -1
  11. data/lib/bolt/cli.rb +21 -6
  12. data/lib/bolt/config/transport/options.rb +12 -0
  13. data/lib/bolt/config/transport/ssh.rb +7 -0
  14. data/lib/bolt/executor.rb +12 -4
  15. data/lib/bolt/fiber_executor.rb +57 -12
  16. data/lib/bolt/outputter/human.rb +117 -12
  17. data/lib/bolt/outputter/json.rb +3 -5
  18. data/lib/bolt/outputter/logger.rb +1 -1
  19. data/lib/bolt/pal.rb +36 -3
  20. data/lib/bolt/pal/yaml_plan/step.rb +2 -0
  21. data/lib/bolt/pal/yaml_plan/step/message.rb +0 -8
  22. data/lib/bolt/pal/yaml_plan/step/verbose.rb +31 -0
  23. data/lib/bolt/plan_future.rb +21 -6
  24. data/lib/bolt/transport/ssh/exec_connection.rb +3 -1
  25. data/lib/bolt/version.rb +1 -1
  26. data/lib/bolt_server/schemas/partials/target-ssh.json +4 -0
  27. data/lib/bolt_server/schemas/partials/target-winrm.json +4 -0
  28. data/lib/bolt_server/transport_app.rb +81 -50
  29. data/lib/bolt_spec/plans/mock_executor.rb +16 -6
  30. metadata +11 -14
  31. data/guides/debugging.txt +0 -28
  32. data/guides/guide.txt +0 -17
  33. data/guides/inventory.txt +0 -24
  34. data/guides/links.txt +0 -13
  35. data/guides/logging.txt +0 -18
  36. data/guides/module.txt +0 -19
  37. data/guides/modulepath.txt +0 -25
  38. data/guides/project.txt +0 -22
  39. data/guides/targets.txt +0 -29
  40. data/guides/transports.txt +0 -23
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 80a2245a2ac92c15284c4d6206f5480c6cf72033b32b773a98303b634f853e54
4
- data.tar.gz: 4a6e868eba652ea23cb8530a135e3187fae81296d63df8bf9b54d5c2117df615
3
+ metadata.gz: e9dbc0b26225fd929d594a6719b55491bad8647650d257b0bbd1bf0eb2424f7c
4
+ data.tar.gz: ff0df82a3a02edfbf05bdbfe39814d2d5535719dbb6b80659bfff5d28651da3a
5
5
  SHA512:
6
- metadata.gz: f7373197cc0401a971ff8e68ed70ef31f90d2087d09a5a0a5178124e40d2aae59546db768480fb5ea65f2b2993d60104abcf137df281863ddaf37c69d58365e5
7
- data.tar.gz: 338c72c31cc379aaf45adae5a122a2a2e9fe6dede738aec4b46c21b5102d16a00ee5a494514dd80bc9f4fcd922ee08e62164e64d34987d40ad1d7383ff475363
6
+ metadata.gz: 61a484916431f7da5b3fa8992641bdf382f0d34eaf2f054e41638e27dc48b7118c07f7729a54246a0d6e19e2a74cedc549fdcc513ba26651f84b470f2c7b187e
7
+ data.tar.gz: d5bf6f496c13253e83dd7d9625058846000a5b339a13fd8d6d922b61ed57b61618125c5de76377f1e01d946e16bd220313659cc066ec793527a2c03013264bc0
data/Puppetfile CHANGED
@@ -35,7 +35,7 @@ mod 'puppetlabs-stdlib', '7.1.0'
35
35
  mod 'puppetlabs-aws_inventory', '0.7.0'
36
36
  mod 'puppetlabs-azure_inventory', '0.5.0'
37
37
  mod 'puppetlabs-gcloud_inventory', '0.3.0'
38
- mod 'puppetlabs-http_request', '0.2.2'
38
+ mod 'puppetlabs-http_request', '0.3.0'
39
39
  mod 'puppetlabs-pkcs7', '0.1.2'
40
40
  mod 'puppetlabs-secure_env_vars', '0.2.0'
41
41
  mod 'puppetlabs-terraform', '0.6.1'
@@ -31,7 +31,8 @@ Puppet::Functions.create_function(:background, Puppet::Functions::InternalFuncti
31
31
  executor = Puppet.lookup(:bolt_executor)
32
32
  executor.report_function_call(self.class.name)
33
33
 
34
- executor.create_future(scope: scope, name: name) do |newscope|
34
+ plan_id = executor.get_current_plan_id(fiber: Fiber.current)
35
+ executor.create_future(scope: scope, name: name, plan_id: plan_id) do |newscope|
35
36
  # Catch 'return' calls inside the block
36
37
  result = catch(:return) do
37
38
  # Execute the block. Individual plan steps in the block will yield
@@ -34,7 +34,11 @@ Puppet::Functions.create_function(:parallelize, Puppet::Functions::InternalFunct
34
34
  executor.report_function_call(self.class.name)
35
35
 
36
36
  futures = data.map do |object|
37
- executor.create_future(scope: scope) do |newscope|
37
+ # We're going to immediately wait for these futures, *and* don't want
38
+ # their results to be returned as part of `wait()`, so use a 'dummy'
39
+ # value as the plan_id. This could also be nil, though in general we want
40
+ # to require Futures to have a plan stack so that they don't get lost.
41
+ executor.create_future(scope: scope, plan_id: 'parallel') do |newscope|
38
42
  # Catch 'return' calls inside the block
39
43
  result = catch(:return) do
40
44
  # Add the object to the block parameters
@@ -125,6 +125,17 @@ Puppet::Functions.create_function(:run_plan, Puppet::Functions::InternalFunction
125
125
  params = wrap_sensitive_parameters(params, closure.parameters)
126
126
  end
127
127
 
128
+ # This can be anything as long as it's unique
129
+ plan_instance_id = SecureRandom.uuid
130
+
131
+ # Add the plan invocation ID to the plan_stack for the PlanFuture the plan is
132
+ # running in so that we know the PlanFuture is running in a new plan
133
+ # invocation. This can be nil in test cases and when `wait()` isn't
134
+ # supported.
135
+ current_future = executor.get_current_future(fiber: Fiber.current)
136
+ # Safe operator to make testing easier
137
+ current_future&.plan_stack&.unshift(plan_instance_id)
138
+
128
139
  # wrap plan execution in logging messages
129
140
  executor.log_plan(plan_name) do
130
141
  result = nil
@@ -150,6 +161,8 @@ Puppet::Functions.create_function(:run_plan, Puppet::Functions::InternalFunction
150
161
  raise e
151
162
  end
152
163
  ensure
164
+ # Pop the plan invocation ID off of the plan_id stack for the Future.
165
+ current_future&.plan_stack&.shift
153
166
  if run_as
154
167
  executor.run_as = old_run_as
155
168
  end
@@ -1,14 +1,13 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'bolt/logger'
4
- require 'bolt/target'
5
4
 
6
5
  # Wait for a Future or array of Futures to finish and return results,
7
6
  # optionally with a timeout.
8
7
  #
9
8
  # > **Note:** Not available in apply block
10
9
  Puppet::Functions.create_function(:wait, Puppet::Functions::InternalFunction) do
11
- # Wait for Future(s) to finish
10
+ # Wait for Futures to finish.
12
11
  # @param futures A Bolt Future object or array of Bolt Futures to wait on.
13
12
  # @param options A hash of additional options.
14
13
  # @option options [Boolean] _catch_errors Whether to catch raised errors.
@@ -27,7 +26,37 @@ Puppet::Functions.create_function(:wait, Puppet::Functions::InternalFunction) do
27
26
  return_type 'Array[Boltlib::PlanResult]'
28
27
  end
29
28
 
30
- # Wait for Future(s) to finish with timeout
29
+ # Wait for all Futures in the current plan to finish.
30
+ # @param options A hash of additional options.
31
+ # @option options [Boolean] _catch_errors Whether to catch raised errors.
32
+ # @return A Result or Results from the Futures
33
+ # @example Perform multiple tasks in the background, then wait for all of them to finish
34
+ # background() || { upload_file("./large_file", "/opt/jfrog/...", $targets) }
35
+ # background() || { run_task("db::migrate", $targets) }
36
+ # # Wait for all futures in the plan to finish and return all results
37
+ # $results = wait()
38
+ dispatch :wait_for_all do
39
+ optional_param 'Hash[String[1], Any]', :options
40
+ return_type 'Array[Boltlib::PlanResult]'
41
+ end
42
+
43
+ # Wait for all Futures in the current plan to finish with a timeout.
44
+ # @param timeout How long to wait for Futures to finish before raising a Timeout error.
45
+ # @param options A hash of additional options.
46
+ # @option options [Boolean] _catch_errors Whether to catch raised errors.
47
+ # @return A Result or Results from the Futures
48
+ # @example Perform multiple tasks in the background, then wait for all of them to finish with a timeout
49
+ # background() || { upload_file("./large_file", "/opt/jfrog/...", $targets) }
50
+ # background() || { run_task("db::migrate", $targets) }
51
+ # # Wait for all futures in the plan to finish and return all results
52
+ # $results = wait(30)
53
+ dispatch :wait_for_all_with_timeout do
54
+ param 'Variant[Integer[0], Float[0.0]]', :timeout
55
+ optional_param 'Hash[String[1], Any]', :options
56
+ return_type 'Array[Boltlib::PlanResult]'
57
+ end
58
+
59
+ # Wait for Futures to finish with timeout.
31
60
  # @param futures A Bolt Future object or array of Bolt Futures to wait on.
32
61
  # @param timeout How long to wait for Futures to finish before raising a Timeout error.
33
62
  # @param options A hash of additional options.
@@ -58,14 +87,22 @@ Puppet::Functions.create_function(:wait, Puppet::Functions::InternalFunction) do
58
87
  end
59
88
 
60
89
  def wait(futures, options = {})
61
- inner_wait(futures, nil, options)
90
+ inner_wait(futures: futures, options: options)
91
+ end
92
+
93
+ def wait_for_all(options = {})
94
+ inner_wait(options: options)
95
+ end
96
+
97
+ def wait_for_all_with_timeout(timeout, options = {})
98
+ inner_wait(timeout: timeout, options: options)
62
99
  end
63
100
 
64
101
  def wait_with_timeout(futures, timeout, options = {})
65
- inner_wait(futures, timeout, options)
102
+ inner_wait(futures: futures, timeout: timeout, options: options)
66
103
  end
67
104
 
68
- def inner_wait(futures, timeout = nil, options = {})
105
+ def inner_wait(futures: nil, timeout: nil, options: {})
69
106
  unless Puppet[:tasks]
70
107
  raise Puppet::ParseErrorWithIssue
71
108
  .from_issue_and_stack(Bolt::PAL::Issues::PLAN_OPERATION_NOT_SUPPORTED_WHEN_COMPILING, action: 'wait')
@@ -85,7 +122,10 @@ Puppet::Functions.create_function(:wait, Puppet::Functions::InternalFunction) do
85
122
  executor = Puppet.lookup(:bolt_executor)
86
123
  executor.report_function_call(self.class.name)
87
124
 
88
- futures = Array(futures)
125
+ # If we get a single Future, make sure it's an array. If we didn't get any
126
+ # futures pass that on to wait so we can continue collecting any futures
127
+ # that are created while waiting on existing futures.
128
+ futures = Array(futures) unless futures.nil?
89
129
  executor.wait(futures, **valid)
90
130
  end
91
131
  end
@@ -5,7 +5,9 @@ require 'bolt/util/format'
5
5
  # Output a message for the user.
6
6
  #
7
7
  # This will print a message to stdout when using the human output format,
8
- # and print to stderr when using the json output format
8
+ # and print to stderr when using the json output format. Messages are
9
+ # also logged at the `info` level. For more information about logs, see
10
+ # [Logs](logs.md).
9
11
  #
10
12
  # > **Note:** Not available in apply block
11
13
  Puppet::Functions.create_function(:'out::message') do
@@ -26,7 +28,7 @@ Puppet::Functions.create_function(:'out::message') do
26
28
 
27
29
  Puppet.lookup(:bolt_executor).tap do |executor|
28
30
  executor.report_function_call(self.class.name)
29
- executor.publish_event(type: :message, message: Bolt::Util::Format.stringify(message))
31
+ executor.publish_event(type: :message, message: Bolt::Util::Format.stringify(message), level: :info)
30
32
  end
31
33
 
32
34
  nil
@@ -5,7 +5,9 @@ require 'bolt/util/format'
5
5
  # Output a message for the user when running in verbose mode.
6
6
  #
7
7
  # This will print a message to stdout when using the human output format,
8
- # and print to stderr when using the json output format.
8
+ # and print to stderr when using the json output format. Messages are
9
+ # also logged at the `debug` level. For more information about logs, see
10
+ # [Logs](logs.md).
9
11
  #
10
12
  # > **Note:** Not available in apply block
11
13
  Puppet::Functions.create_function(:'out::verbose') do
@@ -25,7 +27,7 @@ Puppet::Functions.create_function(:'out::verbose') do
25
27
 
26
28
  Puppet.lookup(:bolt_executor).tap do |executor|
27
29
  executor.report_function_call(self.class.name)
28
- executor.publish_event(type: :verbose, message: Bolt::Util::Format.stringify(message))
30
+ executor.publish_event(type: :verbose, message: Bolt::Util::Format.stringify(message), level: :debug)
29
31
  end
30
32
 
31
33
  nil
@@ -33,7 +33,7 @@ module Bolt
33
33
  logger = Bolt::Logger.logger(self)
34
34
  begin
35
35
  config_file = config_path
36
- config = load_config(config_file)
36
+ config = enabled ? load_config(config_file) : {}
37
37
  rescue ArgumentError
38
38
  config = { 'disabled' => true }
39
39
  end
@@ -507,11 +507,14 @@ module Bolt
507
507
  show
508
508
 
509
509
  #{colorize(:cyan, 'Usage')}
510
- bolt module show [options]
510
+ bolt module show [module name] [options]
511
511
 
512
512
  #{colorize(:cyan, 'Description')}
513
513
  List modules available to the Bolt project.
514
514
 
515
+ Providing the name of a module will display detailed documentation for
516
+ the module.
517
+
515
518
  #{colorize(:cyan, 'Documentation')}
516
519
  To learn more about Bolt modules, run 'bolt guide module'.
517
520
  HELP
data/lib/bolt/cli.rb CHANGED
@@ -494,7 +494,11 @@ module Bolt
494
494
  when 'group'
495
495
  list_groups
496
496
  when 'module'
497
- list_modules
497
+ if options[:object]
498
+ show_module(options[:object])
499
+ else
500
+ list_modules
501
+ end
498
502
  when 'plugin'
499
503
  list_plugins
500
504
  end
@@ -852,6 +856,10 @@ module Bolt
852
856
  outputter.print_module_list(pal.list_modules)
853
857
  end
854
858
 
859
+ def show_module(name)
860
+ outputter.print_module_info(**pal.show_module(name))
861
+ end
862
+
855
863
  def list_plugins
856
864
  outputter.print_plugin_list(plugins.list_plugins, pal.user_modulepath)
857
865
  end
@@ -950,8 +958,9 @@ module Bolt
950
958
  files = Dir.children(root_path).sort
951
959
 
952
960
  files.each_with_object({}) do |file, guides|
953
- next if file !~ /\.txt\z/
954
- topic = File.basename(file, '.txt')
961
+ next if file !~ /\.(yaml|yml)\z/
962
+ # The ".*" here removes any suffix
963
+ topic = File.basename(file, ".*")
955
964
  guides[topic] = File.join(root_path, file)
956
965
  end
957
966
  rescue SystemCallError => e
@@ -961,7 +970,7 @@ module Bolt
961
970
 
962
971
  # Display the list of available Bolt guides.
963
972
  def list_topics
964
- outputter.print_topics(guides.keys - ['guide'])
973
+ outputter.print_topics(guides.keys)
965
974
  0
966
975
  end
967
976
 
@@ -971,12 +980,18 @@ module Bolt
971
980
  analytics.event('Guide', 'known_topic', label: topic)
972
981
 
973
982
  begin
974
- guide = File.read(guides[topic])
983
+ guide = Bolt::Util.read_yaml_hash(guides[topic], 'guide')
975
984
  rescue SystemCallError => e
976
985
  raise Bolt::FileError("#{e.message}: unable to load guide page", filepath)
977
986
  end
978
987
 
979
- outputter.print_guide(guide, topic)
988
+ # Make sure both topic and guide keys are defined
989
+ unless (%w[topic guide] - guide.keys).empty?
990
+ msg = "Guide file #{guides[topic]} must have a 'topic' key and 'guide' key, but has #{guide.keys} keys."
991
+ raise Bolt::Error.new(msg, 'bolt/invalid-guide')
992
+ end
993
+
994
+ outputter.print_guide(**Bolt::Util.symbolize_top_level_keys(guide))
980
995
  else
981
996
  analytics.event('Guide', 'unknown_topic', label: topic)
982
997
  outputter.print_message("Did not find guide for topic '#{topic}'.\n\n")
@@ -16,6 +16,18 @@ module Bolt
16
16
  _default: false,
17
17
  _example: true
18
18
  },
19
+ "batch-mode" => {
20
+ type: [TrueClass, FalseClass],
21
+ description: "Whether to disable password querying. When set to `false`, SSH will fall back to "\
22
+ "prompting for a password if key authentication fails. This might cause Bolt to hang. "\
23
+ "To prevent Bolt from hanging, you can configure `ssh-command` to use an SSH utility "\
24
+ "such as sshpass that supports providing a password non-interactively. For more "\
25
+ "information, see [Providing a password non-interactively using "\
26
+ "`native-ssh`](troubleshooting.md#providing-a-password-non-interactively-using-native-ssh).",
27
+ _plugin: true,
28
+ _default: true,
29
+ _example: false
30
+ },
19
31
  "bundled-ruby" => {
20
32
  description: "Whether to use the Ruby bundled with Bolt packages for local targets.",
21
33
  type: [TrueClass, FalseClass],
@@ -34,6 +34,7 @@ module Bolt
34
34
 
35
35
  # Options available when using the native ssh transport
36
36
  NATIVE_OPTIONS = %w[
37
+ batch-mode
37
38
  cleanup
38
39
  copy-command
39
40
  host
@@ -49,6 +50,7 @@ module Bolt
49
50
  ].concat(RUN_AS_OPTIONS).sort.freeze
50
51
 
51
52
  DEFAULTS = {
53
+ "batch-mode" => true,
52
54
  "cleanup" => true,
53
55
  "connect-timeout" => 10,
54
56
  "disconnect-timeout" => 5,
@@ -124,6 +126,11 @@ module Bolt
124
126
  msg = 'Cannot use native SSH transport with load-config set to false'
125
127
  raise Bolt::ValidationError, msg
126
128
  end
129
+
130
+ if !@config['batch-mode'] && !@config['ssh-command']
131
+ raise Bolt::ValidationError,
132
+ 'Must set ssh-command when batch-mode is set to false'
133
+ end
127
134
  end
128
135
  end
129
136
  end
data/lib/bolt/executor.rb CHANGED
@@ -381,8 +381,16 @@ module Bolt
381
381
  # overloaded while also minimizing the Puppet lookups needed from plan
382
382
  # functions
383
383
  #
384
- def create_future(scope: nil, name: nil, &block)
385
- @fiber_executor.create_future(scope: scope, name: name, &block)
384
+ def create_future(plan_id:, scope: nil, name: nil, &block)
385
+ @fiber_executor.create_future(scope: scope, name: name, plan_id: plan_id, &block)
386
+ end
387
+
388
+ def get_current_future(fiber:)
389
+ @fiber_executor.get_current_future(fiber: fiber)
390
+ end
391
+
392
+ def get_current_plan_id(fiber:)
393
+ @fiber_executor.get_current_plan_id(fiber: fiber)
386
394
  end
387
395
 
388
396
  def plan_complete?
@@ -401,8 +409,8 @@ module Bolt
401
409
  @fiber_executor.wait(futures, **opts)
402
410
  end
403
411
 
404
- def plan_futures
405
- @fiber_executor.plan_futures
412
+ def get_futures_for_plan(plan_id:)
413
+ @fiber_executor.get_futures_for_plan(plan_id: plan_id)
406
414
  end
407
415
 
408
416
  # Execute a plan function concurrently. This function accepts the executor
@@ -5,18 +5,19 @@ require 'bolt/plan_future'
5
5
 
6
6
  module Bolt
7
7
  class FiberExecutor
8
- attr_reader :plan_futures
8
+ attr_reader :active_futures, :finished_futures
9
9
 
10
10
  def initialize
11
11
  @logger = Bolt::Logger.logger(self)
12
12
  @id = 0
13
- @plan_futures = []
13
+ @active_futures = []
14
+ @finished_futures = []
14
15
  end
15
16
 
16
17
  # Whether there is more than one fiber running in parallel.
17
18
  #
18
19
  def in_parallel?
19
- plan_futures.length > 1
20
+ active_futures.length > 1
20
21
  end
21
22
 
22
23
  # Creates a new Puppet scope from the current Plan scope so that variables
@@ -24,7 +25,7 @@ module Bolt
24
25
  # Then creates a new Fiber to execute the block, wraps the Fiber in a
25
26
  # Bolt::PlanFuture, and returns the Bolt::PlanFuture.
26
27
  #
27
- def create_future(scope: nil, name: nil)
28
+ def create_future(plan_id:, scope: nil, name: nil)
28
29
  newscope = nil
29
30
  if scope
30
31
  # Save existing variables to the new scope before starting the future
@@ -46,13 +47,16 @@ module Bolt
46
47
  end
47
48
 
48
49
  # PlanFutures are assigned an ID, which is just a global incrementing
49
- # integer. The main plan should always have ID 0.
50
+ # integer. The main plan should always have ID 0. They also have a
51
+ # plan_id, which identifies which plan spawned them. This is used for
52
+ # tracking which Futures to wait on when `wait()` is called without
53
+ # arguments.
50
54
  @id += 1
51
- future = Bolt::PlanFuture.new(future, @id, name)
55
+ future = Bolt::PlanFuture.new(future, @id, name: name, plan_id: plan_id)
52
56
  @logger.trace("Created future #{future.name}")
53
57
 
54
58
  # Register the PlanFuture with the FiberExecutor to be executed
55
- plan_futures << future
59
+ active_futures << future
56
60
  future
57
61
  end
58
62
 
@@ -63,7 +67,7 @@ module Bolt
63
67
  # the PlanFuture and remove the PlanFuture from the FiberExecutor.
64
68
  #
65
69
  def round_robin
66
- plan_futures.each do |future|
70
+ active_futures.each do |future|
67
71
  # If the Fiber is still running and can be resumed, then resume it
68
72
  @logger.trace("Checking future '#{future.name}'")
69
73
  if future.alive?
@@ -78,19 +82,19 @@ module Bolt
78
82
 
79
83
  # If the future errored and the main plan has already exited, log the
80
84
  # error at warn level.
81
- unless plan_futures.map(&:id).include?(0) || future.state == "done"
85
+ unless active_futures.map(&:id).include?(0) || future.state == "done"
82
86
  Bolt::Logger.warn('errored_futures', "Error in future '#{future.name}': #{future.value}")
83
87
  end
84
88
 
85
89
  # Remove the PlanFuture from the FiberExecutor.
86
- plan_futures.delete(future)
90
+ finished_futures.push(active_futures.delete(future))
87
91
  end
88
92
 
89
93
  # If the Fiber immediately returned or if the Fiber is blocking on a
90
94
  # `wait` call, Bolt should pause for long enough that something can
91
95
  # execute before checking again. This mitigates CPU
92
96
  # thrashing.
93
- return unless plan_futures.all? { |f| %i[returned_immediately unfinished].include?(f.value) }
97
+ return unless active_futures.all? { |f| %i[returned_immediately unfinished].include?(f.value) }
94
98
  @logger.trace("Nothing can be resumed. Rechecking in 0.5 seconds.")
95
99
 
96
100
  sleep(0.5)
@@ -101,12 +105,53 @@ module Bolt
101
105
  # Bolt can exit.
102
106
  #
103
107
  def plan_complete?
104
- plan_futures.empty?
108
+ active_futures.empty?
109
+ end
110
+
111
+ def all_futures
112
+ active_futures + finished_futures
113
+ end
114
+
115
+ # Get the PlanFuture object that is currently executing
116
+ #
117
+ def get_current_future(fiber:)
118
+ all_futures.select { |f| f.fiber == fiber }.first
119
+ end
120
+
121
+ # Get the plan invocation ID for the PlanFuture that is currently executing
122
+ #
123
+ def get_current_plan_id(fiber:)
124
+ get_current_future(fiber: fiber).current_plan
125
+ end
126
+
127
+ # Get the Future objects associated with a particular plan invocation.
128
+ #
129
+ def get_futures_for_plan(plan_id:)
130
+ all_futures.select { |f| f.original_plan == plan_id }
105
131
  end
106
132
 
107
133
  # Block until the provided PlanFuture objects have finished, or the timeout is reached.
108
134
  #
109
135
  def wait(futures, timeout: nil, catch_errors: false, **_kwargs)
136
+ if futures.nil?
137
+ results = []
138
+ plan_id = get_current_plan_id(fiber: Fiber.current)
139
+ # Recollect the futures for this plan until all of the futures have
140
+ # finished. This ensures that we include futures created inside of
141
+ # futures being waited on.
142
+ until (futures = get_futures_for_plan(plan_id: plan_id)).map(&:alive?).none?
143
+ if futures.map(&:fiber).include?(Fiber.current)
144
+ msg = "The wait() function cannot be called with no arguments inside a "\
145
+ "background block in the same plan."
146
+ raise Bolt::Error.new(msg, 'bolt/infinite-wait')
147
+ end
148
+ # Wait for all the futures we know about so far before recollecting
149
+ # Futures for the plan and waiting again
150
+ results = wait(futures, timeout: timeout, catch_errors: catch_errors)
151
+ end
152
+ return results
153
+ end
154
+
110
155
  if timeout.nil?
111
156
  Fiber.yield(:unfinished) until futures.map(&:alive?).none?
112
157
  else