bolt 3.13.0 → 3.16.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 (55) hide show
  1. checksums.yaml +4 -4
  2. data/Puppetfile +1 -1
  3. data/bolt-modules/boltlib/lib/puppet/functions/apply_prep.rb +137 -104
  4. data/bolt-modules/boltlib/lib/puppet/functions/background.rb +2 -1
  5. data/bolt-modules/boltlib/lib/puppet/functions/parallelize.rb +5 -1
  6. data/bolt-modules/boltlib/lib/puppet/functions/run_plan.rb +13 -0
  7. data/bolt-modules/boltlib/lib/puppet/functions/wait.rb +47 -7
  8. data/bolt-modules/out/lib/puppet/functions/out/message.rb +4 -2
  9. data/bolt-modules/out/lib/puppet/functions/out/verbose.rb +4 -2
  10. data/guides/{debugging.txt → debugging.yaml} +5 -6
  11. data/guides/{inventory.txt → inventory.yaml} +6 -7
  12. data/guides/{links.txt → links.yaml} +3 -4
  13. data/guides/{logging.txt → logging.yaml} +5 -6
  14. data/guides/{module.txt → module.yaml} +5 -6
  15. data/guides/{modulepath.txt → modulepath.yaml} +5 -6
  16. data/guides/{project.txt → project.yaml} +6 -7
  17. data/guides/{targets.txt → targets.yaml} +5 -6
  18. data/guides/{transports.txt → transports.yaml} +6 -7
  19. data/lib/bolt/analytics.rb +3 -20
  20. data/lib/bolt/application.rb +620 -0
  21. data/lib/bolt/bolt_option_parser.rb +17 -5
  22. data/lib/bolt/cli.rb +592 -772
  23. data/lib/bolt/config/transport/options.rb +12 -0
  24. data/lib/bolt/config/transport/ssh.rb +7 -0
  25. data/lib/bolt/executor.rb +12 -4
  26. data/lib/bolt/fiber_executor.rb +63 -14
  27. data/lib/bolt/module_installer/puppetfile.rb +24 -10
  28. data/lib/bolt/outputter/human.rb +199 -43
  29. data/lib/bolt/outputter/json.rb +66 -43
  30. data/lib/bolt/outputter/logger.rb +1 -1
  31. data/lib/bolt/pal.rb +67 -14
  32. data/lib/bolt/pal/yaml_plan/step.rb +2 -0
  33. data/lib/bolt/pal/yaml_plan/step/message.rb +0 -8
  34. data/lib/bolt/pal/yaml_plan/step/verbose.rb +31 -0
  35. data/lib/bolt/pal/yaml_plan/transpiler.rb +1 -1
  36. data/lib/bolt/plan_creator.rb +2 -20
  37. data/lib/bolt/plan_future.rb +23 -3
  38. data/lib/bolt/plan_result.rb +1 -1
  39. data/lib/bolt/plugin/task.rb +1 -1
  40. data/lib/bolt/project.rb +0 -7
  41. data/lib/bolt/result_set.rb +2 -1
  42. data/lib/bolt/transport/local/connection.rb +17 -1
  43. data/lib/bolt/transport/orch/connection.rb +13 -1
  44. data/lib/bolt/transport/ssh/exec_connection.rb +3 -1
  45. data/lib/bolt/version.rb +1 -1
  46. data/lib/bolt_server/file_cache.rb +12 -0
  47. data/lib/bolt_server/schemas/action-apply.json +32 -0
  48. data/lib/bolt_server/schemas/action-apply_prep.json +19 -0
  49. data/lib/bolt_server/schemas/partials/target-ssh.json +4 -0
  50. data/lib/bolt_server/schemas/partials/target-winrm.json +4 -0
  51. data/lib/bolt_server/transport_app.rb +180 -60
  52. data/lib/bolt_spec/plans/mock_executor.rb +16 -6
  53. metadata +23 -15
  54. data/guides/guide.txt +0 -17
  55. data/lib/bolt/secret.rb +0 -37
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 80a2245a2ac92c15284c4d6206f5480c6cf72033b32b773a98303b634f853e54
4
- data.tar.gz: 4a6e868eba652ea23cb8530a135e3187fae81296d63df8bf9b54d5c2117df615
3
+ metadata.gz: 71d383b473f9356ff41f574bb0abe4aea9a9f8c3ac5055a459acffe0da290af1
4
+ data.tar.gz: 00d10803d865a9d6f0cb6eb7162e4a17f619b88e6702b559e0153b3796d732d0
5
5
  SHA512:
6
- metadata.gz: f7373197cc0401a971ff8e68ed70ef31f90d2087d09a5a0a5178124e40d2aae59546db768480fb5ea65f2b2993d60104abcf137df281863ddaf37c69d58365e5
7
- data.tar.gz: 338c72c31cc379aaf45adae5a122a2a2e9fe6dede738aec4b46c21b5102d16a00ee5a494514dd80bc9f4fcd922ee08e62164e64d34987d40ad1d7383ff475363
6
+ metadata.gz: eaad9c12aab69e9d7c9f1d8b312623b8bcca4f26f983beb08b1d09c4568aafe0e430d639b88903df4e1a498a58c4c9d8add5d4b8b3930fe1dec3124bc15afad3
7
+ data.tar.gz: a05ec6c181fc6aa51c73ebfd928d73513d5506c1b0705bed4637bfa9490a6f1c010019c9209f7ed28c134f003a063ff6f4add490a55d658998f566d00c5ff017
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.1'
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'
@@ -8,16 +8,18 @@ require 'bolt/task'
8
8
  # installed using either the configured plugin or the `task` plugin with the
9
9
  # `puppet_agent::install` task.
10
10
  #
11
- # Agent installation will be skipped if the target includes the `puppet-agent` feature, either as a
12
- # property of its transport (PCP) or by explicitly setting it as a feature in Bolt's inventory.
11
+ # Agent installation will be skipped if the target includes the `puppet-agent`
12
+ # feature, either as a property of its transport (PCP) or by explicitly setting
13
+ # it as a feature in Bolt's inventory.
13
14
  #
14
15
  # > **Note:** Not available in apply block
15
16
  Puppet::Functions.create_function(:apply_prep) do
16
17
  # @param targets A pattern or array of patterns identifying a set of targets.
17
18
  # @param options Options hash.
19
+ # @option options [Boolean] _catch_errors Whether to catch raised errors.
18
20
  # @option options [Array] _required_modules An array of modules to sync to the target.
19
21
  # @option options [String] _run_as User to run as using privilege escalation.
20
- # @return [nil]
22
+ # @return [Bolt::ResultSet]
21
23
  # @example Prepare targets by name.
22
24
  # apply_prep('target1,target2')
23
25
  dispatch :apply_prep do
@@ -25,138 +27,169 @@ Puppet::Functions.create_function(:apply_prep) do
25
27
  optional_param 'Hash[String, Data]', :options
26
28
  end
27
29
 
28
- def script_compiler
29
- @script_compiler ||= Puppet::Pal::ScriptCompiler.new(closure_scope.compiler)
30
- end
30
+ def apply_prep(target_spec, options = {})
31
+ unless Puppet[:tasks]
32
+ raise Puppet::ParseErrorWithIssue
33
+ .from_issue_and_stack(Bolt::PAL::Issues::PLAN_OPERATION_NOT_SUPPORTED_WHEN_COMPILING, action: 'apply_prep')
34
+ end
31
35
 
32
- def inventory
33
- @inventory ||= Puppet.lookup(:bolt_inventory)
34
- end
36
+ options = options.slice(*%w[_catch_errors _required_modules _run_as])
37
+ targets = inventory.get_targets(target_spec)
35
38
 
36
- def get_task(name, params = {})
37
- tasksig = script_compiler.task_signature(name)
38
- raise Bolt::Error.new("Task '#{name}' could not be found", 'bolt/apply-prep') unless tasksig
39
+ executor.report_function_call(self.class.name)
39
40
 
40
- errors = []
41
- unless tasksig.runnable_with?(params) { |msg| errors << msg }
42
- # This relies on runnable with printing a partial message before the first real error
43
- raise Bolt::ValidationError, "Invalid parameters for #{errors.join("\n")}"
41
+ executor.log_action('install puppet and gather facts', targets) do
42
+ executor.without_default_logging do
43
+ install_results = install_agents(targets, options)
44
+ facts_results = get_facts(install_results.ok_set.targets, options)
45
+
46
+ Bolt::ResultSet.new(install_results.error_set.results + facts_results.results)
47
+ end
44
48
  end
49
+ end
45
50
 
46
- Bolt::Task.from_task_signature(tasksig)
51
+ def applicator
52
+ @applicator ||= Puppet.lookup(:apply_executor)
47
53
  end
48
54
 
49
- # rubocop:disable Naming/AccessorMethodName
50
- def set_agent_feature(target)
51
- inventory.set_feature(target, 'puppet-agent')
55
+ def executor
56
+ @executor ||= Puppet.lookup(:bolt_executor)
52
57
  end
53
- # rubocop:enable Naming/AccessorMethodName
54
58
 
59
+ def inventory
60
+ @inventory ||= Puppet.lookup(:bolt_inventory)
61
+ end
62
+
63
+ # Runs a task. This method is called by the puppet_library hook.
64
+ #
55
65
  def run_task(targets, task, args = {}, options = {})
56
66
  executor.run_task(targets, task, args, options)
57
67
  end
58
68
 
59
- # Returns true if the target has the puppet-agent feature defined, either from inventory or transport.
60
- def agent?(target, executor, inventory)
69
+ # Returns true if the target has the puppet-agent feature defined, either from
70
+ # inventory or transport.
71
+ #
72
+ private def agent?(target)
61
73
  inventory.features(target).include?('puppet-agent') ||
62
- executor.transport(target.transport).provided_features.include?('puppet-agent') || target.remote?
74
+ executor.transport(target.transport).provided_features.include?('puppet-agent') ||
75
+ target.remote?
63
76
  end
64
77
 
65
- def executor
66
- @executor ||= Puppet.lookup(:bolt_executor)
78
+ # Generate the plugin tarball.
79
+ #
80
+ private def build_plugin_tarball(required_modules)
81
+ if required_modules.any?
82
+ Puppet.debug("Syncing only required modules: #{required_modules.join(',')}.")
83
+ end
84
+
85
+ tarball = applicator.build_plugin_tarball do |mod|
86
+ next unless required_modules.empty? || required_modules.include?(mod.name)
87
+ search_dirs = []
88
+ search_dirs << mod.plugins if mod.plugins?
89
+ search_dirs << mod.pluginfacts if mod.pluginfacts?
90
+ search_dirs
91
+ end
92
+
93
+ Puppet::Pops::Types::PSensitiveType::Sensitive.new(tarball)
67
94
  end
68
95
 
69
- def apply_prep(target_spec, options = {})
70
- unless Puppet[:tasks]
71
- raise Puppet::ParseErrorWithIssue
72
- .from_issue_and_stack(Bolt::PAL::Issues::PLAN_OPERATION_NOT_SUPPORTED_WHEN_COMPILING, action: 'apply_prep')
96
+ # Install the puppet-agent package on targets that need it.
97
+ #
98
+ private def install_agents(targets, options)
99
+ results = []
100
+
101
+ agent_targets, agentless_targets = targets.partition { |target| agent?(target) }
102
+
103
+ agent_targets.each do |target|
104
+ Puppet.debug("Puppet Agent feature declared for #{target}")
105
+ results << Bolt::Result.new(target)
73
106
  end
74
107
 
75
- # Unfreeze this
76
- options = options.slice(*%w[_run_as _required_modules])
108
+ unless agentless_targets.empty?
109
+ hooks, errors = get_hooks(agentless_targets, options)
110
+ hook_results = run_hooks(hooks)
77
111
 
78
- applicator = Puppet.lookup(:apply_executor)
112
+ hook_results.each do |result|
113
+ next unless result.ok?
114
+ inventory.set_feature(result.target, 'puppet-agent')
115
+ end
79
116
 
80
- executor.report_function_call(self.class.name)
117
+ results.concat(hook_results).concat(errors)
118
+ end
81
119
 
82
- targets = inventory.get_targets(target_spec)
120
+ Bolt::ResultSet.new(results).tap do |resultset|
121
+ unless resultset.ok? || options['_catch_errors']
122
+ raise Bolt::RunFailure.new(resultset.error_set, 'apply_prep')
123
+ end
124
+ end
125
+ end
83
126
 
84
- required_modules = options.delete('_required_modules').to_a
85
- if required_modules.any?
86
- Puppet.debug("Syncing only required modules: #{required_modules.join(',')}.")
127
+ # Retrieve facts from each target and add them to inventory.
128
+ #
129
+ private def get_facts(targets, options)
130
+ return Bolt::ResultSet.new([]) unless targets.any?
131
+
132
+ task = applicator.custom_facts_task
133
+ args = { 'plugins' => build_plugin_tarball(options.delete('_required_modules').to_a) }
134
+ results = run_task(targets, task, args, options)
135
+
136
+ unless results.ok? || options['_catch_errors']
137
+ raise Bolt::RunFailure.new(results, 'run_task', task.name)
87
138
  end
88
139
 
89
- # Gather facts, including custom facts
90
- plugins = applicator.build_plugin_tarball do |mod|
91
- next unless required_modules.empty? || required_modules.include?(mod.name)
92
- search_dirs = []
93
- search_dirs << mod.plugins if mod.plugins?
94
- search_dirs << mod.pluginfacts if mod.pluginfacts?
95
- search_dirs
140
+ results.each do |result|
141
+ next unless result.ok?
142
+
143
+ if unsupported_puppet?(result['clientversion'])
144
+ Bolt::Logger.deprecate(
145
+ "unsupported_puppet",
146
+ "Detected unsupported Puppet agent version #{result['clientversion']} on target "\
147
+ "#{result.target}. Bolt supports Puppet agent 6.0.0 and higher."
148
+ )
149
+ end
150
+
151
+ inventory.add_facts(result.target, result.value)
96
152
  end
97
153
 
98
- executor.log_action('install puppet and gather facts', targets) do
99
- executor.without_default_logging do
100
- # Skip targets that include the puppet-agent feature, as we know an agent will be available.
101
- agent_targets, need_install_targets = targets.partition { |target| agent?(target, executor, inventory) }
102
- agent_targets.each { |target| Puppet.debug "Puppet Agent feature declared for #{target.name}" }
103
- unless need_install_targets.empty?
104
- # lazy-load expensive gem code
105
- require 'concurrent'
106
- pool = Concurrent::ThreadPoolExecutor.new
107
-
108
- hooks = need_install_targets.map do |t|
109
- opts = t.plugin_hooks&.fetch('puppet_library').dup
110
- plugin_name = opts.delete('plugin')
111
- hook = inventory.plugins.get_hook(plugin_name, :puppet_library)
112
- # Give plan function options precedence over inventory options
113
- { 'target' => t,
114
- 'hook_proc' => hook.call(opts.merge(options), t, self) }
115
- rescue StandardError => e
116
- Bolt::Result.from_exception(t, e)
117
- end
118
-
119
- hook_errors, ok_hooks = hooks.partition { |h| h.is_a?(Bolt::Result) }
120
-
121
- futures = ok_hooks.map do |hash|
122
- Concurrent::Future.execute(executor: pool) do
123
- hash['hook_proc'].call
124
- end
125
- end
126
-
127
- results = futures.zip(ok_hooks).map do |f, hash|
128
- f.value || Bolt::Result.from_exception(hash['target'], f.reason)
129
- end
130
- set = Bolt::ResultSet.new(results + hook_errors)
131
- raise Bolt::RunFailure.new(set.error_set, 'apply_prep') unless set.ok
132
-
133
- need_install_targets.each { |target| set_agent_feature(target) }
134
- end
135
-
136
- task = applicator.custom_facts_task
137
- arguments = { 'plugins' => Puppet::Pops::Types::PSensitiveType::Sensitive.new(plugins) }
138
- results = run_task(targets, task, arguments, options)
139
-
140
- # TODO: Standardize RunFailure type with error above
141
- raise Bolt::RunFailure.new(results, 'run_task', task.name) unless results.ok?
142
-
143
- results.each do |result|
144
- # Log a warning if the client version is < 6
145
- if unsupported_puppet?(result['clientversion'])
146
- Bolt::Logger.deprecate(
147
- "unsupported_puppet",
148
- "Detected unsupported Puppet agent version #{result['clientversion']} on target "\
149
- "#{result.target}. Bolt supports Puppet agent 6.0.0 and higher."
150
- )
151
- end
152
-
153
- inventory.add_facts(result.target, result.value)
154
- end
154
+ results
155
+ end
156
+
157
+ # Return a list of targets and their puppet_library hooks.
158
+ #
159
+ private def get_hooks(targets, options)
160
+ hooks = []
161
+ errors = []
162
+
163
+ targets.each do |target|
164
+ plugin_opts = target.plugin_hooks.fetch('puppet_library').dup
165
+ plugin_name = plugin_opts.delete('plugin')
166
+ hook = inventory.plugins.get_hook(plugin_name, :puppet_library)
167
+
168
+ hooks << { 'target' => target,
169
+ 'proc' => hook.call(plugin_opts.merge(options), target, self) }
170
+ rescue StandardError => e
171
+ errors << Bolt::Result.from_exception(target, e)
172
+ end
173
+
174
+ [hooks, errors]
175
+ end
176
+
177
+ # Runs the puppet_library hook for each target, returning the result
178
+ # of each.
179
+ #
180
+ private def run_hooks(hooks)
181
+ require 'concurrent'
182
+ pool = Concurrent::ThreadPoolExecutor.new
183
+
184
+ futures = hooks.map do |hook|
185
+ Concurrent::Future.execute(executor: pool) do
186
+ hook['proc'].call
155
187
  end
156
188
  end
157
189
 
158
- # Return nothing
159
- nil
190
+ futures.zip(hooks).map do |future, hook|
191
+ future.value || Bolt::Result.from_exception(hook['target'], future.reason)
192
+ end
160
193
  end
161
194
 
162
195
  # Returns true if the client's major version is < 6.
@@ -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