openbolt 5.0.0.rc1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (258) hide show
  1. checksums.yaml +7 -0
  2. data/Puppetfile +52 -0
  3. data/bolt-modules/boltlib/lib/puppet/datatypes/applyresult.rb +60 -0
  4. data/bolt-modules/boltlib/lib/puppet/datatypes/containerresult.rb +51 -0
  5. data/bolt-modules/boltlib/lib/puppet/datatypes/future.rb +25 -0
  6. data/bolt-modules/boltlib/lib/puppet/datatypes/resourceinstance.rb +71 -0
  7. data/bolt-modules/boltlib/lib/puppet/datatypes/result.rb +55 -0
  8. data/bolt-modules/boltlib/lib/puppet/datatypes/resultset.rb +65 -0
  9. data/bolt-modules/boltlib/lib/puppet/datatypes/target.rb +93 -0
  10. data/bolt-modules/boltlib/lib/puppet/functions/add_facts.rb +33 -0
  11. data/bolt-modules/boltlib/lib/puppet/functions/add_to_group.rb +38 -0
  12. data/bolt-modules/boltlib/lib/puppet/functions/apply_prep.rb +208 -0
  13. data/bolt-modules/boltlib/lib/puppet/functions/background.rb +62 -0
  14. data/bolt-modules/boltlib/lib/puppet/functions/catch_errors.rb +57 -0
  15. data/bolt-modules/boltlib/lib/puppet/functions/download_file.rb +130 -0
  16. data/bolt-modules/boltlib/lib/puppet/functions/facts.rb +31 -0
  17. data/bolt-modules/boltlib/lib/puppet/functions/fail_plan.rb +52 -0
  18. data/bolt-modules/boltlib/lib/puppet/functions/get_resources.rb +87 -0
  19. data/bolt-modules/boltlib/lib/puppet/functions/get_target.rb +34 -0
  20. data/bolt-modules/boltlib/lib/puppet/functions/get_targets.rb +35 -0
  21. data/bolt-modules/boltlib/lib/puppet/functions/parallelize.rb +74 -0
  22. data/bolt-modules/boltlib/lib/puppet/functions/puppetdb_command.rb +97 -0
  23. data/bolt-modules/boltlib/lib/puppet/functions/puppetdb_fact.rb +47 -0
  24. data/bolt-modules/boltlib/lib/puppet/functions/puppetdb_query.rb +52 -0
  25. data/bolt-modules/boltlib/lib/puppet/functions/remove_from_group.rb +40 -0
  26. data/bolt-modules/boltlib/lib/puppet/functions/resolve_references.rb +42 -0
  27. data/bolt-modules/boltlib/lib/puppet/functions/resource.rb +53 -0
  28. data/bolt-modules/boltlib/lib/puppet/functions/run_command.rb +106 -0
  29. data/bolt-modules/boltlib/lib/puppet/functions/run_container.rb +162 -0
  30. data/bolt-modules/boltlib/lib/puppet/functions/run_plan.rb +291 -0
  31. data/bolt-modules/boltlib/lib/puppet/functions/run_script.rb +145 -0
  32. data/bolt-modules/boltlib/lib/puppet/functions/run_task.rb +164 -0
  33. data/bolt-modules/boltlib/lib/puppet/functions/run_task_with.rb +211 -0
  34. data/bolt-modules/boltlib/lib/puppet/functions/set_config.rb +48 -0
  35. data/bolt-modules/boltlib/lib/puppet/functions/set_feature.rb +43 -0
  36. data/bolt-modules/boltlib/lib/puppet/functions/set_resources.rb +145 -0
  37. data/bolt-modules/boltlib/lib/puppet/functions/set_var.rb +38 -0
  38. data/bolt-modules/boltlib/lib/puppet/functions/upload_file.rb +101 -0
  39. data/bolt-modules/boltlib/lib/puppet/functions/vars.rb +29 -0
  40. data/bolt-modules/boltlib/lib/puppet/functions/wait.rb +131 -0
  41. data/bolt-modules/boltlib/lib/puppet/functions/wait_until_available.rb +59 -0
  42. data/bolt-modules/boltlib/lib/puppet/functions/without_default_logging.rb +39 -0
  43. data/bolt-modules/boltlib/lib/puppet/functions/write_file.rb +50 -0
  44. data/bolt-modules/boltlib/types/planresult.pp +18 -0
  45. data/bolt-modules/boltlib/types/targetspec.pp +7 -0
  46. data/bolt-modules/ctrl/lib/puppet/functions/ctrl/do_until.rb +42 -0
  47. data/bolt-modules/ctrl/lib/puppet/functions/ctrl/sleep.rb +20 -0
  48. data/bolt-modules/dir/lib/puppet/functions/dir/children.rb +35 -0
  49. data/bolt-modules/file/lib/puppet/functions/file/delete.rb +21 -0
  50. data/bolt-modules/file/lib/puppet/functions/file/exists.rb +28 -0
  51. data/bolt-modules/file/lib/puppet/functions/file/join.rb +20 -0
  52. data/bolt-modules/file/lib/puppet/functions/file/read.rb +33 -0
  53. data/bolt-modules/file/lib/puppet/functions/file/readable.rb +28 -0
  54. data/bolt-modules/file/lib/puppet/functions/file/write.rb +24 -0
  55. data/bolt-modules/log/lib/puppet/functions/log/debug.rb +39 -0
  56. data/bolt-modules/log/lib/puppet/functions/log/error.rb +40 -0
  57. data/bolt-modules/log/lib/puppet/functions/log/fatal.rb +40 -0
  58. data/bolt-modules/log/lib/puppet/functions/log/info.rb +39 -0
  59. data/bolt-modules/log/lib/puppet/functions/log/trace.rb +39 -0
  60. data/bolt-modules/log/lib/puppet/functions/log/warn.rb +41 -0
  61. data/bolt-modules/out/lib/puppet/functions/out/message.rb +36 -0
  62. data/bolt-modules/out/lib/puppet/functions/out/verbose.rb +35 -0
  63. data/bolt-modules/prompt/lib/puppet/functions/prompt/menu.rb +103 -0
  64. data/bolt-modules/prompt/lib/puppet/functions/prompt.rb +65 -0
  65. data/bolt-modules/system/lib/puppet/functions/system/env.rb +20 -0
  66. data/exe/bolt +17 -0
  67. data/guides/debugging.yaml +27 -0
  68. data/guides/inventory.yaml +23 -0
  69. data/guides/links.yaml +12 -0
  70. data/guides/logging.yaml +17 -0
  71. data/guides/module.yaml +18 -0
  72. data/guides/modulepath.yaml +24 -0
  73. data/guides/project.yaml +21 -0
  74. data/guides/targets.yaml +28 -0
  75. data/guides/transports.yaml +22 -0
  76. data/lib/bolt/analytics.rb +233 -0
  77. data/lib/bolt/application.rb +806 -0
  78. data/lib/bolt/applicator.rb +368 -0
  79. data/lib/bolt/apply_inventory.rb +93 -0
  80. data/lib/bolt/apply_result.rb +154 -0
  81. data/lib/bolt/apply_target.rb +90 -0
  82. data/lib/bolt/bolt_option_parser.rb +1226 -0
  83. data/lib/bolt/catalog/logging.rb +15 -0
  84. data/lib/bolt/catalog.rb +144 -0
  85. data/lib/bolt/cli.rb +949 -0
  86. data/lib/bolt/config/modulepath.rb +30 -0
  87. data/lib/bolt/config/options.rb +673 -0
  88. data/lib/bolt/config/transport/base.rb +133 -0
  89. data/lib/bolt/config/transport/docker.rb +34 -0
  90. data/lib/bolt/config/transport/jail.rb +33 -0
  91. data/lib/bolt/config/transport/local.rb +39 -0
  92. data/lib/bolt/config/transport/lxd.rb +34 -0
  93. data/lib/bolt/config/transport/options.rb +431 -0
  94. data/lib/bolt/config/transport/orch.rb +41 -0
  95. data/lib/bolt/config/transport/podman.rb +33 -0
  96. data/lib/bolt/config/transport/remote.rb +24 -0
  97. data/lib/bolt/config/transport/ssh.rb +138 -0
  98. data/lib/bolt/config/transport/winrm.rb +63 -0
  99. data/lib/bolt/config.rb +515 -0
  100. data/lib/bolt/container_result.rb +105 -0
  101. data/lib/bolt/error.rb +194 -0
  102. data/lib/bolt/executor.rb +539 -0
  103. data/lib/bolt/fiber_executor.rb +190 -0
  104. data/lib/bolt/inventory/group.rb +446 -0
  105. data/lib/bolt/inventory/inventory.rb +391 -0
  106. data/lib/bolt/inventory/options.rb +139 -0
  107. data/lib/bolt/inventory/target.rb +293 -0
  108. data/lib/bolt/inventory.rb +120 -0
  109. data/lib/bolt/logger.rb +252 -0
  110. data/lib/bolt/module.rb +54 -0
  111. data/lib/bolt/module_installer/installer.rb +44 -0
  112. data/lib/bolt/module_installer/puppetfile/forge_module.rb +54 -0
  113. data/lib/bolt/module_installer/puppetfile/git_module.rb +37 -0
  114. data/lib/bolt/module_installer/puppetfile/module.rb +26 -0
  115. data/lib/bolt/module_installer/puppetfile.rb +131 -0
  116. data/lib/bolt/module_installer/resolver.rb +129 -0
  117. data/lib/bolt/module_installer/specs/forge_spec.rb +91 -0
  118. data/lib/bolt/module_installer/specs/git_spec.rb +150 -0
  119. data/lib/bolt/module_installer/specs/id/base.rb +116 -0
  120. data/lib/bolt/module_installer/specs/id/gitclone.rb +120 -0
  121. data/lib/bolt/module_installer/specs/id/github.rb +90 -0
  122. data/lib/bolt/module_installer/specs/id/gitlab.rb +92 -0
  123. data/lib/bolt/module_installer/specs.rb +95 -0
  124. data/lib/bolt/module_installer.rb +208 -0
  125. data/lib/bolt/node/errors.rb +55 -0
  126. data/lib/bolt/node/output.rb +29 -0
  127. data/lib/bolt/outputter/human.rb +958 -0
  128. data/lib/bolt/outputter/json.rb +205 -0
  129. data/lib/bolt/outputter/logger.rb +76 -0
  130. data/lib/bolt/outputter/rainbow.rb +118 -0
  131. data/lib/bolt/outputter.rb +57 -0
  132. data/lib/bolt/pal/issues.rb +19 -0
  133. data/lib/bolt/pal/logging.rb +17 -0
  134. data/lib/bolt/pal/yaml_plan/evaluator.rb +83 -0
  135. data/lib/bolt/pal/yaml_plan/loader.rb +94 -0
  136. data/lib/bolt/pal/yaml_plan/parameter.rb +63 -0
  137. data/lib/bolt/pal/yaml_plan/step/command.rb +45 -0
  138. data/lib/bolt/pal/yaml_plan/step/download.rb +37 -0
  139. data/lib/bolt/pal/yaml_plan/step/eval.rb +42 -0
  140. data/lib/bolt/pal/yaml_plan/step/message.rb +31 -0
  141. data/lib/bolt/pal/yaml_plan/step/plan.rb +42 -0
  142. data/lib/bolt/pal/yaml_plan/step/resources.rb +170 -0
  143. data/lib/bolt/pal/yaml_plan/step/script.rb +62 -0
  144. data/lib/bolt/pal/yaml_plan/step/task.rb +42 -0
  145. data/lib/bolt/pal/yaml_plan/step/upload.rb +37 -0
  146. data/lib/bolt/pal/yaml_plan/step/verbose.rb +31 -0
  147. data/lib/bolt/pal/yaml_plan/step.rb +223 -0
  148. data/lib/bolt/pal/yaml_plan/transpiler.rb +90 -0
  149. data/lib/bolt/pal/yaml_plan.rb +172 -0
  150. data/lib/bolt/pal.rb +847 -0
  151. data/lib/bolt/plan_creator.rb +219 -0
  152. data/lib/bolt/plan_future.rb +86 -0
  153. data/lib/bolt/plan_result.rb +44 -0
  154. data/lib/bolt/plugin/cache.rb +76 -0
  155. data/lib/bolt/plugin/env_var.rb +54 -0
  156. data/lib/bolt/plugin/module.rb +276 -0
  157. data/lib/bolt/plugin/prompt.rb +36 -0
  158. data/lib/bolt/plugin/puppet_connect_data.rb +84 -0
  159. data/lib/bolt/plugin/puppetdb.rb +124 -0
  160. data/lib/bolt/plugin/task.rb +72 -0
  161. data/lib/bolt/plugin.rb +380 -0
  162. data/lib/bolt/project.rb +219 -0
  163. data/lib/bolt/project_manager/config_migrator.rb +113 -0
  164. data/lib/bolt/project_manager/inventory_migrator.rb +67 -0
  165. data/lib/bolt/project_manager/migrator.rb +39 -0
  166. data/lib/bolt/project_manager/module_migrator.rb +203 -0
  167. data/lib/bolt/project_manager.rb +221 -0
  168. data/lib/bolt/puppetdb/client.rb +153 -0
  169. data/lib/bolt/puppetdb/config.rb +176 -0
  170. data/lib/bolt/puppetdb/instance.rb +146 -0
  171. data/lib/bolt/puppetdb.rb +15 -0
  172. data/lib/bolt/r10k_log_proxy.rb +30 -0
  173. data/lib/bolt/rerun.rb +55 -0
  174. data/lib/bolt/resource_instance.rb +133 -0
  175. data/lib/bolt/result.rb +247 -0
  176. data/lib/bolt/result_set.rb +128 -0
  177. data/lib/bolt/shell/bash/tmpdir.rb +62 -0
  178. data/lib/bolt/shell/bash.rb +516 -0
  179. data/lib/bolt/shell/powershell/snippets.rb +181 -0
  180. data/lib/bolt/shell/powershell.rb +365 -0
  181. data/lib/bolt/shell.rb +105 -0
  182. data/lib/bolt/target.rb +174 -0
  183. data/lib/bolt/task/puppet_server.rb +27 -0
  184. data/lib/bolt/task/run.rb +55 -0
  185. data/lib/bolt/task.rb +163 -0
  186. data/lib/bolt/transport/base.rb +252 -0
  187. data/lib/bolt/transport/docker/connection.rb +150 -0
  188. data/lib/bolt/transport/docker.rb +23 -0
  189. data/lib/bolt/transport/jail/connection.rb +81 -0
  190. data/lib/bolt/transport/jail.rb +21 -0
  191. data/lib/bolt/transport/local/connection.rb +106 -0
  192. data/lib/bolt/transport/local.rb +20 -0
  193. data/lib/bolt/transport/lxd/connection.rb +115 -0
  194. data/lib/bolt/transport/lxd.rb +26 -0
  195. data/lib/bolt/transport/orch/connection.rb +111 -0
  196. data/lib/bolt/transport/orch.rb +271 -0
  197. data/lib/bolt/transport/podman/connection.rb +102 -0
  198. data/lib/bolt/transport/podman.rb +19 -0
  199. data/lib/bolt/transport/remote.rb +41 -0
  200. data/lib/bolt/transport/simple.rb +54 -0
  201. data/lib/bolt/transport/ssh/connection.rb +321 -0
  202. data/lib/bolt/transport/ssh/exec_connection.rb +140 -0
  203. data/lib/bolt/transport/ssh.rb +48 -0
  204. data/lib/bolt/transport/winrm/connection.rb +378 -0
  205. data/lib/bolt/transport/winrm.rb +33 -0
  206. data/lib/bolt/util/format.rb +68 -0
  207. data/lib/bolt/util/puppet_log_level.rb +21 -0
  208. data/lib/bolt/util.rb +465 -0
  209. data/lib/bolt/validator.rb +227 -0
  210. data/lib/bolt/version.rb +5 -0
  211. data/lib/bolt.rb +8 -0
  212. data/lib/bolt_server/acl.rb +39 -0
  213. data/lib/bolt_server/base_config.rb +112 -0
  214. data/lib/bolt_server/config.rb +64 -0
  215. data/lib/bolt_server/file_cache.rb +200 -0
  216. data/lib/bolt_server/request_error.rb +11 -0
  217. data/lib/bolt_server/schemas/action-check_node_connections.json +14 -0
  218. data/lib/bolt_server/schemas/action-run_command.json +12 -0
  219. data/lib/bolt_server/schemas/action-run_script.json +47 -0
  220. data/lib/bolt_server/schemas/action-run_task.json +20 -0
  221. data/lib/bolt_server/schemas/action-upload_file.json +47 -0
  222. data/lib/bolt_server/schemas/partials/target-any.json +10 -0
  223. data/lib/bolt_server/schemas/partials/target-ssh.json +88 -0
  224. data/lib/bolt_server/schemas/partials/target-winrm.json +67 -0
  225. data/lib/bolt_server/schemas/partials/task.json +94 -0
  226. data/lib/bolt_server/schemas/transport-ssh.json +25 -0
  227. data/lib/bolt_server/schemas/transport-winrm.json +19 -0
  228. data/lib/bolt_server/transport_app.rb +554 -0
  229. data/lib/bolt_spec/bolt_context.rb +226 -0
  230. data/lib/bolt_spec/plans/action_stubs/command_stub.rb +51 -0
  231. data/lib/bolt_spec/plans/action_stubs/download_stub.rb +66 -0
  232. data/lib/bolt_spec/plans/action_stubs/plan_stub.rb +55 -0
  233. data/lib/bolt_spec/plans/action_stubs/script_stub.rb +59 -0
  234. data/lib/bolt_spec/plans/action_stubs/task_stub.rb +57 -0
  235. data/lib/bolt_spec/plans/action_stubs/upload_stub.rb +65 -0
  236. data/lib/bolt_spec/plans/action_stubs.rb +196 -0
  237. data/lib/bolt_spec/plans/mock_executor.rb +361 -0
  238. data/lib/bolt_spec/plans/publish_stub.rb +49 -0
  239. data/lib/bolt_spec/plans.rb +190 -0
  240. data/lib/bolt_spec/run.rb +246 -0
  241. data/lib/logging_extensions/logging.rb +13 -0
  242. data/libexec/apply_catalog.rb +130 -0
  243. data/libexec/bolt_catalog +68 -0
  244. data/libexec/custom_facts.rb +63 -0
  245. data/libexec/query_resources.rb +75 -0
  246. data/modules/aggregate/lib/puppet/functions/aggregate/count.rb +21 -0
  247. data/modules/aggregate/lib/puppet/functions/aggregate/nodes.rb +22 -0
  248. data/modules/aggregate/lib/puppet/functions/aggregate/targets.rb +21 -0
  249. data/modules/aggregate/plans/count.pp +56 -0
  250. data/modules/aggregate/plans/targets.pp +56 -0
  251. data/modules/canary/lib/puppet/functions/canary/merge.rb +13 -0
  252. data/modules/canary/lib/puppet/functions/canary/random_split.rb +22 -0
  253. data/modules/canary/lib/puppet/functions/canary/skip.rb +25 -0
  254. data/modules/canary/plans/init.pp +100 -0
  255. data/modules/puppet_connect/plans/test_input_data.pp +94 -0
  256. data/modules/puppetdb_fact/plans/init.pp +20 -0
  257. data/resources/bolt_bash_completion.sh +214 -0
  258. metadata +735 -0
@@ -0,0 +1,190 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../bolt/logger'
4
+ require_relative '../bolt/plan_future'
5
+
6
+ module Bolt
7
+ class FiberExecutor
8
+ attr_reader :active_futures, :finished_futures
9
+
10
+ def initialize
11
+ @logger = Bolt::Logger.logger(self)
12
+ @id = 0
13
+ @active_futures = []
14
+ @finished_futures = []
15
+ end
16
+
17
+ # Whether there is more than one fiber running in parallel.
18
+ #
19
+ def in_parallel?
20
+ active_futures.length > 1
21
+ end
22
+
23
+ # Creates a new Puppet scope from the current Plan scope so that variables
24
+ # can be used inside the block and won't interact with the outer scope.
25
+ # Then creates a new Fiber to execute the block, wraps the Fiber in a
26
+ # Bolt::PlanFuture, and returns the Bolt::PlanFuture.
27
+ #
28
+ def create_future(plan_id:, scope: nil, name: nil)
29
+ newscope = nil
30
+ if scope
31
+ # Save existing variables to the new scope before starting the future
32
+ # itself so that if the plan returns before the backgrounded block
33
+ # starts, we still have the variables.
34
+ newscope = Puppet::Parser::Scope.new(scope.compiler)
35
+ local = Puppet::Parser::Scope::LocalScope.new
36
+
37
+ # Compress the current scopes into a single vars hash to add to the new scope
38
+ scope.to_hash(true, true).each_pair { |k, v| local[k] = v }
39
+ newscope.push_ephemerals([local])
40
+ end
41
+
42
+ # Create a new Fiber that will execute the provided block.
43
+ future = Fiber.new do
44
+ # Yield the new scope - this should be ignored by the block if
45
+ # `newscope` is nil.
46
+ yield newscope
47
+ end
48
+
49
+ # PlanFutures are assigned an ID, which is just a global incrementing
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.
54
+ @id += 1
55
+ future = Bolt::PlanFuture.new(future, @id, name: name, plan_id: plan_id, scope: newscope)
56
+ @logger.trace("Created future #{future.name}")
57
+
58
+ # Register the PlanFuture with the FiberExecutor to be executed
59
+ active_futures << future
60
+ future
61
+ end
62
+
63
+ # Visit each PlanFuture registered with the FiberExecutor and resume it.
64
+ # Fibers will yield themselves back, either if they kicked off a
65
+ # long-running process or if the current long-running process hasn't
66
+ # completed. If the Fiber finishes after being resumed, store the result in
67
+ # the PlanFuture and remove the PlanFuture from the FiberExecutor.
68
+ #
69
+ def round_robin
70
+ active_futures.each do |future|
71
+ # If the Fiber is still running and can be resumed, then resume it.
72
+ # Override Puppet's global_scope to prevent ephemerals in other scopes
73
+ # from being popped off in the wrong order due to race conditions.
74
+ # This primarily happens when running executor functions from custom
75
+ # Puppet language functions, but may happen elsewhere.
76
+ @logger.trace("Checking future '#{future.name}'")
77
+ if future.alive?
78
+ @logger.trace("Resuming future '#{future.name}'")
79
+ Puppet.override(global_scope: future.scope) { future.resume }
80
+ end
81
+
82
+ # Once we've restarted the Fiber, check to see if it's finished again
83
+ # and cleanup if it has.
84
+ next if future.alive?
85
+ @logger.trace("Cleaning up future '#{future.name}'")
86
+
87
+ # If the future errored and the main plan has already exited, log the
88
+ # error at warn level.
89
+ unless active_futures.map(&:id).include?(0) || future.state == "done"
90
+ Bolt::Logger.warn('errored_futures', "Error in future '#{future.name}': #{future.value}")
91
+ end
92
+
93
+ # Remove the PlanFuture from the FiberExecutor.
94
+ finished_futures.push(active_futures.delete(future))
95
+ end
96
+
97
+ # If the Fiber immediately returned or if the Fiber is blocking on a
98
+ # `wait` call, Bolt should pause for long enough that something can
99
+ # execute before checking again. This mitigates CPU
100
+ # thrashing.
101
+ return unless active_futures.all? { |f| %i[returned_immediately unfinished].include?(f.value) }
102
+ @logger.trace("Nothing can be resumed. Rechecking in 0.5 seconds.")
103
+
104
+ sleep(0.5)
105
+ end
106
+
107
+ # Whether all PlanFutures have finished executing, indicating that the
108
+ # entire plan (main plan and any PlanFutures it spawned) has finished and
109
+ # Bolt can exit.
110
+ #
111
+ def plan_complete?
112
+ active_futures.empty?
113
+ end
114
+
115
+ def all_futures
116
+ active_futures + finished_futures
117
+ end
118
+
119
+ # Get the PlanFuture object that is currently executing
120
+ #
121
+ def get_current_future(fiber:)
122
+ all_futures.select { |f| f.fiber == fiber }.first
123
+ end
124
+
125
+ # Get the plan invocation ID for the PlanFuture that is currently executing
126
+ #
127
+ def get_current_plan_id(fiber:)
128
+ get_current_future(fiber: fiber).current_plan
129
+ end
130
+
131
+ # Get the Future objects associated with a particular plan invocation.
132
+ #
133
+ def get_futures_for_plan(plan_id:)
134
+ all_futures.select { |f| f.original_plan == plan_id }
135
+ end
136
+
137
+ # Block until the provided PlanFuture objects have finished, or the timeout is reached.
138
+ #
139
+ def wait(futures, timeout: nil, catch_errors: false, **_kwargs)
140
+ if futures.nil?
141
+ results = []
142
+ plan_id = get_current_plan_id(fiber: Fiber.current)
143
+ # Recollect the futures for this plan until all of the futures have
144
+ # finished. This ensures that we include futures created inside of
145
+ # futures being waited on.
146
+ until (futures = get_futures_for_plan(plan_id: plan_id)).map(&:alive?).none?
147
+ if futures.map(&:fiber).include?(Fiber.current)
148
+ msg = "The wait() function cannot be called with no arguments inside a "\
149
+ "background block in the same plan."
150
+ raise Bolt::Error.new(msg, 'bolt/infinite-wait')
151
+ end
152
+ # Wait for all the futures we know about so far before recollecting
153
+ # Futures for the plan and waiting again
154
+ results = wait(futures, timeout: timeout, catch_errors: catch_errors)
155
+ end
156
+ return results
157
+ end
158
+
159
+ if timeout.nil?
160
+ Fiber.yield(:unfinished) until futures.map(&:alive?).none?
161
+ else
162
+ start = Time.now
163
+ Fiber.yield(:unfinished) until (Time.now - start > timeout) || futures.map(&:alive?).none?
164
+ # Raise an error for any futures that are still alive
165
+ futures.each do |f|
166
+ if f.alive?
167
+ f.raise(Bolt::FutureTimeoutError.new(f.name, timeout))
168
+ end
169
+ end
170
+ end
171
+
172
+ results = futures.map(&:value)
173
+
174
+ failed_indices = results.each_index.select do |i|
175
+ results[i].is_a?(Bolt::Error)
176
+ end
177
+
178
+ if failed_indices.any?
179
+ if catch_errors
180
+ failed_indices.each { |i| results[i] = results[i].to_puppet_error }
181
+ else
182
+ # Do this after handling errors for simplicity and pretty printing
183
+ raise Bolt::ParallelFailure.new(results, failed_indices)
184
+ end
185
+ end
186
+
187
+ results
188
+ end
189
+ end
190
+ end
@@ -0,0 +1,446 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../../bolt/config/options'
4
+ require_relative '../../bolt/inventory/group'
5
+ require_relative '../../bolt/inventory/inventory'
6
+ require_relative '../../bolt/inventory/target'
7
+
8
+ module Bolt
9
+ class Inventory
10
+ class Group
11
+ attr_accessor :name, :groups
12
+
13
+ # Illegal characters that are not permitted in group names or aliases.
14
+ # These characters are delimiters for target and group names and allowing
15
+ # them would cause unexpected behavior.
16
+ ILLEGAL_CHARS = /[\s,]/.freeze
17
+
18
+ # NOTE: All keys should have a corresponding schema property in schemas/bolt-inventory.schema.json
19
+ DATA_KEYS = %w[config facts vars features plugin_hooks].freeze
20
+ TARGET_KEYS = DATA_KEYS + %w[name alias uri]
21
+ GROUP_KEYS = DATA_KEYS + %w[name groups targets]
22
+ CONFIG_KEYS = Bolt::Config::INVENTORY_OPTIONS.keys
23
+
24
+ def initialize(input, plugins, all_group: false)
25
+ @logger = Bolt::Logger.logger(self)
26
+ @plugins = plugins
27
+
28
+ input = @plugins.resolve_top_level_references(input) if @plugins.reference?(input)
29
+
30
+ if all_group
31
+ if input.key?('name') && input['name'] != 'all'
32
+ Bolt::Logger.warn(
33
+ "top_level_group_name",
34
+ "Top-level group '#{input['name']}' cannot specify a name, using 'all' instead."
35
+ )
36
+ end
37
+
38
+ input = input.merge('name' => 'all')
39
+ end
40
+
41
+ raise ValidationError.new("Group does not have a name", nil) unless input.key?('name')
42
+
43
+ @name = @plugins.resolve_references(input['name'])
44
+
45
+ raise ValidationError.new("Group name must be a String, not #{@name.inspect}", nil) unless @name.is_a?(String)
46
+
47
+ if (illegal_char = @name.match(ILLEGAL_CHARS))
48
+ raise ValidationError.new("Illegal character '#{illegal_char}' in group name '#{@name}'", @name)
49
+ end
50
+
51
+ validate_group_input(input)
52
+
53
+ @input = input
54
+
55
+ validate_data_keys(@input)
56
+
57
+ targets = @plugins.resolve_top_level_references(input.fetch('targets', []))
58
+
59
+ @unresolved_targets = {}
60
+ @resolved_targets = {}
61
+
62
+ @aliases = {}
63
+ @string_targets = []
64
+
65
+ Array(targets).each do |target|
66
+ # If target is a string, it can either be trivially defining a target
67
+ # or it could be a name/alias of a target defined in another group.
68
+ # We can't tell the difference until all groups have been resolved,
69
+ # so we store the string on its own here and process it later.
70
+ case target
71
+ when String
72
+ @string_targets << target
73
+ # Handle plugins at this level so that lookups cannot trigger recursive lookups
74
+ when Hash
75
+ add_target_definition(target)
76
+ else
77
+ raise ValidationError.new("Target entry must be a String or Hash, not #{target.class}", @name)
78
+ end
79
+ end
80
+
81
+ groups = input.fetch('groups', [])
82
+ # 'groups' can be a _plugin reference, in which case we want to resolve
83
+ # it. That can itself return a reference, so we want to keep resolving
84
+ # them until we have a value. We don't just use resolve_references
85
+ # though, since that will resolve any nested references and we want to
86
+ # leave it to the group to do that lazily.
87
+ groups = @plugins.resolve_top_level_references(groups)
88
+
89
+ @groups = Array(groups).map { |g| Group.new(g, plugins) }
90
+ end
91
+
92
+ def target_data(target_name)
93
+ if @unresolved_targets.key?(target_name)
94
+ target = @unresolved_targets.delete(target_name)
95
+ resolved_data = resolve_data_keys(target, target_name).merge(
96
+ 'name' => target['name'],
97
+ 'uri' => target['uri'],
98
+ 'alias' => target['alias'],
99
+ # groups come from group_data
100
+ 'groups' => []
101
+ )
102
+ @resolved_targets[target_name] = resolved_data
103
+ else
104
+ @resolved_targets[target_name]
105
+ end
106
+ end
107
+
108
+ def all_target_names
109
+ @unresolved_targets.keys + @resolved_targets.keys
110
+ end
111
+
112
+ def add_target_definition(target)
113
+ # This check ensures target lookup plugins do not returns bare strings.
114
+ # Remove it if we decide to allows task plugins to return string Target
115
+ # names.
116
+ unless target.is_a?(Hash)
117
+ raise ValidationError.new("Target entry must be a Hash, not #{target.class}", @name)
118
+ end
119
+
120
+ target['name'] = @plugins.resolve_references(target['name']) if target.key?('name')
121
+ target['uri'] = @plugins.resolve_references(target['uri']) if target.key?('uri')
122
+ target['alias'] = @plugins.resolve_references(target['alias']) if target.key?('alias')
123
+
124
+ t_name = target['name'] || target['uri']
125
+
126
+ if t_name.nil? || t_name.empty?
127
+ raise ValidationError.new("No name or uri for target: #{target}", @name)
128
+ end
129
+
130
+ unless t_name.is_a? String
131
+ raise ValidationError.new("Target name must be a String, not #{t_name.class}", @name)
132
+ end
133
+
134
+ unless t_name.ascii_only?
135
+ raise ValidationError.new("Target name must be ASCII characters: #{target}", @name)
136
+ end
137
+
138
+ if contains_target?(t_name)
139
+ @logger.debug("Ignoring duplicate target in #{@name}: #{target}")
140
+ return
141
+ end
142
+
143
+ unless (unexpected_keys = target.keys - TARGET_KEYS).empty?
144
+ msg = "Found unexpected key(s) #{unexpected_keys.join(', ')} in target #{t_name}"
145
+ Bolt::Logger.warn("unknown_target_keys", msg)
146
+ end
147
+
148
+ validate_data_keys(target, t_name)
149
+
150
+ if target.include?('alias')
151
+ aliases = target['alias']
152
+ aliases = [aliases] if aliases.is_a?(String)
153
+ unless aliases.is_a?(Array)
154
+ msg = "Alias entry on #{t_name} must be a String or Array, not #{aliases.class}"
155
+ raise ValidationError.new(msg, @name)
156
+ end
157
+
158
+ insert_alia(t_name, aliases)
159
+ end
160
+
161
+ @unresolved_targets[t_name] = target
162
+ end
163
+
164
+ def remove_target(target)
165
+ @resolved_targets.delete(target.name)
166
+ @unresolved_targets.delete(target.name)
167
+ end
168
+
169
+ def add_target(target)
170
+ @resolved_targets[target.name] = { 'name' => target.name }
171
+ end
172
+
173
+ def insert_alia(target_name, aliases)
174
+ aliases.each do |alia|
175
+ if (illegal_char = alia.match(ILLEGAL_CHARS))
176
+ raise ValidationError.new("Illegal character '#{illegal_char}' in alias '#{alia}'", @name)
177
+ end
178
+
179
+ if (found = @aliases[alia])
180
+ raise ValidationError.new(alias_conflict(alia, found, target_name), @name)
181
+ end
182
+ @aliases[alia] = target_name
183
+ end
184
+ end
185
+
186
+ def clear_alia(target_name)
187
+ @aliases.reject! { |_alias, name| name == target_name }
188
+ end
189
+
190
+ def data_merge(data1, data2)
191
+ if data2.nil? || data1.nil?
192
+ return data2 || data1
193
+ end
194
+
195
+ {
196
+ 'config' => Bolt::Util.deep_merge(data1['config'], data2['config']),
197
+ 'name' => data1['name'] || data2['name'],
198
+ 'uri' => data1['uri'] || data2['uri'],
199
+ # Collect all aliases across all groups for each target uri
200
+ 'alias' => [*data1['alias'], *data2['alias']],
201
+ # Shallow merge instead of deep merge so that vars with a hash value
202
+ # are assigned a new hash, rather than merging the existing value
203
+ # with the value meant to replace it
204
+ 'vars' => data1['vars'].merge(data2['vars']),
205
+ 'facts' => Bolt::Util.deep_merge(data1['facts'], data2['facts']),
206
+ 'features' => data1['features'] | data2['features'],
207
+ 'plugin_hooks' => data1['plugin_hooks'].merge(data2['plugin_hooks']),
208
+ 'groups' => data2['groups'] + data1['groups']
209
+ }
210
+ end
211
+
212
+ def resolve_string_targets(aliases, known_targets)
213
+ @string_targets.each do |string_target|
214
+ # If this is the name of a target defined elsewhere, then insert the
215
+ # target into this group as just a name. Otherwise, add a new target
216
+ # with the string as the URI.
217
+ if known_targets.include?(string_target)
218
+ @unresolved_targets[string_target] = { 'name' => string_target }
219
+ # If this is an alias for an existing target, then add it to this group
220
+ elsif (canonical_name = aliases[string_target])
221
+ if contains_target?(canonical_name)
222
+ @logger.debug("Ignoring duplicate target in #{@name}: #{canonical_name}")
223
+ else
224
+ @unresolved_targets[canonical_name] = { 'name' => canonical_name }
225
+ end
226
+ # If it's not the name or alias of an existing target, then make a
227
+ # new target using the string as the URI
228
+ elsif contains_target?(string_target)
229
+ @logger.debug("Ignoring duplicate target in #{@name}: #{string_target}")
230
+ else
231
+ @unresolved_targets[string_target] = { 'uri' => string_target }
232
+ end
233
+ end
234
+ @groups.each { |g| g.resolve_string_targets(aliases, known_targets) }
235
+ end
236
+
237
+ private def alias_conflict(name, target1, target2)
238
+ "Alias #{name} refers to multiple targets: #{target1} and #{target2}"
239
+ end
240
+
241
+ private def group_alias_conflict(name)
242
+ "Group #{name} conflicts with alias of the same name"
243
+ end
244
+
245
+ private def group_target_conflict(name)
246
+ "Group #{name} conflicts with target of the same name"
247
+ end
248
+
249
+ private def alias_target_conflict(name)
250
+ "Target name #{name} conflicts with alias of the same name"
251
+ end
252
+
253
+ def validate_group_input(input)
254
+ raise ValidationError.new("Expected group to be a Hash, not #{input.class}", nil) unless input.is_a?(Hash)
255
+
256
+ # DEPRECATION : remove this before finalization
257
+ if input.key?('target-lookups')
258
+ msg = "'target-lookups' are no longer a separate key. Merge 'target-lookups' and 'targets' lists and replace 'plugin' with '_plugin'" # rubocop:disable Layout/LineLength
259
+ raise ValidationError.new(msg, @name)
260
+ end
261
+
262
+ if input.key?('nodes')
263
+ command = Bolt::Util.powershell? ? 'Update-BoltProject' : 'bolt project migrate'
264
+ msg = <<~MSG.chomp
265
+ Found 'nodes' key in group #{@name}. This looks like a v1 inventory file, which is
266
+ no longer supported by Bolt. Migrate to a v2 inventory file automatically using
267
+ '#{command}'.
268
+ MSG
269
+ raise ValidationError.new(msg, nil)
270
+ end
271
+
272
+ unless (unexpected_keys = input.keys - GROUP_KEYS).empty?
273
+ msg = "Found unexpected key(s) #{unexpected_keys.join(', ')} in group #{@name}"
274
+ Bolt::Logger.warn("unknown_group_keys", msg)
275
+ end
276
+ end
277
+
278
+ def validate(used_group_names = Set.new, used_target_names = Set.new, used_aliases = {})
279
+ # Test if this group name conflicts with anything used before.
280
+ raise ValidationError.new("Tried to redefine group #{@name}", @name) if used_group_names.include?(@name)
281
+ raise ValidationError.new(group_target_conflict(@name), @name) if used_target_names.include?(@name)
282
+ raise ValidationError.new(group_alias_conflict(@name), @name) if used_aliases.include?(@name)
283
+
284
+ used_group_names << @name
285
+
286
+ # Collect target names and aliases into a list used to validate that subgroups don't conflict.
287
+ # Used names validate that previously used group names don't conflict with new target names/aliases.
288
+ @unresolved_targets.merge(@resolved_targets).each do |t_name, t_data|
289
+ # Require targets to be parseable as a Target.
290
+ begin
291
+ # Catch malformed URI here
292
+ Bolt::Inventory::Target.parse_uri(t_data['uri'])
293
+ rescue Bolt::ParseError => e
294
+ @logger.debug(e)
295
+ raise ValidationError.new("Invalid target uri #{t_data['uri']}", @name)
296
+ end
297
+
298
+ raise ValidationError.new(group_target_conflict(t_name), @name) if used_group_names.include?(t_name)
299
+ if used_aliases.include?(t_name)
300
+ raise ValidationError.new(alias_target_conflict(t_name), @name)
301
+ end
302
+
303
+ used_target_names << t_name
304
+ end
305
+
306
+ @aliases.each do |n, target|
307
+ raise ValidationError.new(group_alias_conflict(n), @name) if used_group_names.include?(n)
308
+ if used_target_names.include?(n)
309
+ raise ValidationError.new(alias_target_conflict(n), @name)
310
+ end
311
+
312
+ if used_aliases.include?(n)
313
+ raise ValidationError.new(alias_conflict(n, target, used_aliases[n]), @name)
314
+ end
315
+
316
+ used_aliases[n] = target
317
+ end
318
+
319
+ @groups.each do |g|
320
+ g.validate(used_group_names, used_target_names, used_aliases)
321
+ rescue ValidationError => e
322
+ e.add_parent(@name)
323
+ raise e
324
+ end
325
+
326
+ nil
327
+ end
328
+
329
+ def resolve_data_keys(data, target = nil)
330
+ result = {
331
+ 'config' => @plugins.resolve_references(data.fetch('config', {})),
332
+ 'vars' => @plugins.resolve_references(data.fetch('vars', {})),
333
+ 'facts' => @plugins.resolve_references(data.fetch('facts', {})),
334
+ 'features' => @plugins.resolve_references(data.fetch('features', [])),
335
+ 'plugin_hooks' => @plugins.resolve_references(data.fetch('plugin_hooks', {}))
336
+ }
337
+
338
+ validate_data_keys(result, target)
339
+
340
+ Bolt::Config::Options::TRANSPORT_CONFIG.each_key do |transport|
341
+ next unless result['config'].key?(transport)
342
+ transport_config = result['config'][transport]
343
+ next unless transport_config.is_a?(Hash)
344
+ transport_config = Bolt::Util.postwalk_vals(transport_config) do |val|
345
+ if val.is_a?(Hash)
346
+ val = val.compact
347
+ val = nil if val.empty?
348
+ end
349
+ val
350
+ end
351
+ # the transport config is user-specified data so we
352
+ # still want to preserve it even if it exclusively
353
+ # contains nil-resolved keys
354
+ result['config'][transport] = transport_config || {}
355
+ end
356
+
357
+ result['features'] = Set.new(result['features'].flatten)
358
+ result
359
+ end
360
+
361
+ def validate_data_keys(data, target = nil)
362
+ {
363
+ 'config' => Hash,
364
+ 'vars' => Hash,
365
+ 'facts' => Hash,
366
+ 'features' => Array,
367
+ 'plugin_hooks' => Hash
368
+ }.each do |key, expected_type|
369
+ next if !data.key?(key) || data[key].is_a?(expected_type) || @plugins.reference?(data[key])
370
+
371
+ msg = +"Expected #{key} to be of type #{expected_type}, not #{data[key].class}"
372
+ msg << " for target #{target}" if target
373
+ raise ValidationError.new(msg, @name)
374
+ end
375
+ unless @plugins.reference?(data['config'])
376
+ unexpected_keys = data.fetch('config', {}).keys - CONFIG_KEYS
377
+ if unexpected_keys.any?
378
+ msg = +"Found unexpected key(s) #{unexpected_keys.join(', ')} in config for"
379
+ msg << " target #{target} in" if target
380
+ msg << " group #{@name}"
381
+ Bolt::Logger.warn("unknown_config_keys", msg)
382
+ end
383
+ end
384
+ end
385
+
386
+ def group_data
387
+ @group_data ||= resolve_data_keys(@input).merge('groups' => [@name])
388
+ end
389
+
390
+ # Returns targets contained directly within the group, ignoring subgroups
391
+ def local_targets
392
+ Set.new(@unresolved_targets.keys) + Set.new(@resolved_targets.keys)
393
+ end
394
+
395
+ def contains_target?(target_name)
396
+ @unresolved_targets.key?(target_name) || @resolved_targets.key?(target_name)
397
+ end
398
+
399
+ # Returns all targets contained within the group, which includes targets from subgroups.
400
+ def all_targets
401
+ @groups.inject(local_targets) do |acc, g|
402
+ acc.merge(g.all_targets)
403
+ end
404
+ end
405
+
406
+ # Returns a mapping of aliases to targets contained within the group, which includes subgroups.
407
+ def target_aliases
408
+ @groups.inject(@aliases) do |acc, g|
409
+ acc.merge(g.target_aliases)
410
+ end
411
+ end
412
+
413
+ # Return a mapping of group names to group.
414
+ def collect_groups
415
+ @groups.inject(name => self) do |acc, g|
416
+ acc.merge(g.collect_groups)
417
+ end
418
+ end
419
+
420
+ def target_collect(target_name)
421
+ child_data = @groups.map { |group| group.target_collect(target_name) }
422
+ # Data from earlier groups wins
423
+ child_result = child_data.inject do |acc, group_data|
424
+ data_merge(group_data, acc)
425
+ end
426
+ # Children override the parent
427
+ data_merge(target_data(target_name), child_result)
428
+ end
429
+
430
+ def group_collect(target_name)
431
+ child_data = @groups.map { |group| group.group_collect(target_name) }
432
+ # Data from earlier groups wins
433
+ child_result = child_data.inject do |acc, group_data|
434
+ data_merge(group_data, acc)
435
+ end
436
+
437
+ # If this group has the target or one of the child groups has the
438
+ # target, return the data, otherwise return nil
439
+ if child_result || contains_target?(target_name)
440
+ # Children override the parent
441
+ data_merge(group_data, child_result)
442
+ end
443
+ end
444
+ end
445
+ end
446
+ end