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.
- checksums.yaml +7 -0
- data/Puppetfile +52 -0
- data/bolt-modules/boltlib/lib/puppet/datatypes/applyresult.rb +60 -0
- data/bolt-modules/boltlib/lib/puppet/datatypes/containerresult.rb +51 -0
- data/bolt-modules/boltlib/lib/puppet/datatypes/future.rb +25 -0
- data/bolt-modules/boltlib/lib/puppet/datatypes/resourceinstance.rb +71 -0
- data/bolt-modules/boltlib/lib/puppet/datatypes/result.rb +55 -0
- data/bolt-modules/boltlib/lib/puppet/datatypes/resultset.rb +65 -0
- data/bolt-modules/boltlib/lib/puppet/datatypes/target.rb +93 -0
- data/bolt-modules/boltlib/lib/puppet/functions/add_facts.rb +33 -0
- data/bolt-modules/boltlib/lib/puppet/functions/add_to_group.rb +38 -0
- data/bolt-modules/boltlib/lib/puppet/functions/apply_prep.rb +208 -0
- data/bolt-modules/boltlib/lib/puppet/functions/background.rb +62 -0
- data/bolt-modules/boltlib/lib/puppet/functions/catch_errors.rb +57 -0
- data/bolt-modules/boltlib/lib/puppet/functions/download_file.rb +130 -0
- data/bolt-modules/boltlib/lib/puppet/functions/facts.rb +31 -0
- data/bolt-modules/boltlib/lib/puppet/functions/fail_plan.rb +52 -0
- data/bolt-modules/boltlib/lib/puppet/functions/get_resources.rb +87 -0
- data/bolt-modules/boltlib/lib/puppet/functions/get_target.rb +34 -0
- data/bolt-modules/boltlib/lib/puppet/functions/get_targets.rb +35 -0
- data/bolt-modules/boltlib/lib/puppet/functions/parallelize.rb +74 -0
- data/bolt-modules/boltlib/lib/puppet/functions/puppetdb_command.rb +97 -0
- data/bolt-modules/boltlib/lib/puppet/functions/puppetdb_fact.rb +47 -0
- data/bolt-modules/boltlib/lib/puppet/functions/puppetdb_query.rb +52 -0
- data/bolt-modules/boltlib/lib/puppet/functions/remove_from_group.rb +40 -0
- data/bolt-modules/boltlib/lib/puppet/functions/resolve_references.rb +42 -0
- data/bolt-modules/boltlib/lib/puppet/functions/resource.rb +53 -0
- data/bolt-modules/boltlib/lib/puppet/functions/run_command.rb +106 -0
- data/bolt-modules/boltlib/lib/puppet/functions/run_container.rb +162 -0
- data/bolt-modules/boltlib/lib/puppet/functions/run_plan.rb +291 -0
- data/bolt-modules/boltlib/lib/puppet/functions/run_script.rb +145 -0
- data/bolt-modules/boltlib/lib/puppet/functions/run_task.rb +164 -0
- data/bolt-modules/boltlib/lib/puppet/functions/run_task_with.rb +211 -0
- data/bolt-modules/boltlib/lib/puppet/functions/set_config.rb +48 -0
- data/bolt-modules/boltlib/lib/puppet/functions/set_feature.rb +43 -0
- data/bolt-modules/boltlib/lib/puppet/functions/set_resources.rb +145 -0
- data/bolt-modules/boltlib/lib/puppet/functions/set_var.rb +38 -0
- data/bolt-modules/boltlib/lib/puppet/functions/upload_file.rb +101 -0
- data/bolt-modules/boltlib/lib/puppet/functions/vars.rb +29 -0
- data/bolt-modules/boltlib/lib/puppet/functions/wait.rb +131 -0
- data/bolt-modules/boltlib/lib/puppet/functions/wait_until_available.rb +59 -0
- data/bolt-modules/boltlib/lib/puppet/functions/without_default_logging.rb +39 -0
- data/bolt-modules/boltlib/lib/puppet/functions/write_file.rb +50 -0
- data/bolt-modules/boltlib/types/planresult.pp +18 -0
- data/bolt-modules/boltlib/types/targetspec.pp +7 -0
- data/bolt-modules/ctrl/lib/puppet/functions/ctrl/do_until.rb +42 -0
- data/bolt-modules/ctrl/lib/puppet/functions/ctrl/sleep.rb +20 -0
- data/bolt-modules/dir/lib/puppet/functions/dir/children.rb +35 -0
- data/bolt-modules/file/lib/puppet/functions/file/delete.rb +21 -0
- data/bolt-modules/file/lib/puppet/functions/file/exists.rb +28 -0
- data/bolt-modules/file/lib/puppet/functions/file/join.rb +20 -0
- data/bolt-modules/file/lib/puppet/functions/file/read.rb +33 -0
- data/bolt-modules/file/lib/puppet/functions/file/readable.rb +28 -0
- data/bolt-modules/file/lib/puppet/functions/file/write.rb +24 -0
- data/bolt-modules/log/lib/puppet/functions/log/debug.rb +39 -0
- data/bolt-modules/log/lib/puppet/functions/log/error.rb +40 -0
- data/bolt-modules/log/lib/puppet/functions/log/fatal.rb +40 -0
- data/bolt-modules/log/lib/puppet/functions/log/info.rb +39 -0
- data/bolt-modules/log/lib/puppet/functions/log/trace.rb +39 -0
- data/bolt-modules/log/lib/puppet/functions/log/warn.rb +41 -0
- data/bolt-modules/out/lib/puppet/functions/out/message.rb +36 -0
- data/bolt-modules/out/lib/puppet/functions/out/verbose.rb +35 -0
- data/bolt-modules/prompt/lib/puppet/functions/prompt/menu.rb +103 -0
- data/bolt-modules/prompt/lib/puppet/functions/prompt.rb +65 -0
- data/bolt-modules/system/lib/puppet/functions/system/env.rb +20 -0
- data/exe/bolt +17 -0
- data/guides/debugging.yaml +27 -0
- data/guides/inventory.yaml +23 -0
- data/guides/links.yaml +12 -0
- data/guides/logging.yaml +17 -0
- data/guides/module.yaml +18 -0
- data/guides/modulepath.yaml +24 -0
- data/guides/project.yaml +21 -0
- data/guides/targets.yaml +28 -0
- data/guides/transports.yaml +22 -0
- data/lib/bolt/analytics.rb +233 -0
- data/lib/bolt/application.rb +806 -0
- data/lib/bolt/applicator.rb +368 -0
- data/lib/bolt/apply_inventory.rb +93 -0
- data/lib/bolt/apply_result.rb +154 -0
- data/lib/bolt/apply_target.rb +90 -0
- data/lib/bolt/bolt_option_parser.rb +1226 -0
- data/lib/bolt/catalog/logging.rb +15 -0
- data/lib/bolt/catalog.rb +144 -0
- data/lib/bolt/cli.rb +949 -0
- data/lib/bolt/config/modulepath.rb +30 -0
- data/lib/bolt/config/options.rb +673 -0
- data/lib/bolt/config/transport/base.rb +133 -0
- data/lib/bolt/config/transport/docker.rb +34 -0
- data/lib/bolt/config/transport/jail.rb +33 -0
- data/lib/bolt/config/transport/local.rb +39 -0
- data/lib/bolt/config/transport/lxd.rb +34 -0
- data/lib/bolt/config/transport/options.rb +431 -0
- data/lib/bolt/config/transport/orch.rb +41 -0
- data/lib/bolt/config/transport/podman.rb +33 -0
- data/lib/bolt/config/transport/remote.rb +24 -0
- data/lib/bolt/config/transport/ssh.rb +138 -0
- data/lib/bolt/config/transport/winrm.rb +63 -0
- data/lib/bolt/config.rb +515 -0
- data/lib/bolt/container_result.rb +105 -0
- data/lib/bolt/error.rb +194 -0
- data/lib/bolt/executor.rb +539 -0
- data/lib/bolt/fiber_executor.rb +190 -0
- data/lib/bolt/inventory/group.rb +446 -0
- data/lib/bolt/inventory/inventory.rb +391 -0
- data/lib/bolt/inventory/options.rb +139 -0
- data/lib/bolt/inventory/target.rb +293 -0
- data/lib/bolt/inventory.rb +120 -0
- data/lib/bolt/logger.rb +252 -0
- data/lib/bolt/module.rb +54 -0
- data/lib/bolt/module_installer/installer.rb +44 -0
- data/lib/bolt/module_installer/puppetfile/forge_module.rb +54 -0
- data/lib/bolt/module_installer/puppetfile/git_module.rb +37 -0
- data/lib/bolt/module_installer/puppetfile/module.rb +26 -0
- data/lib/bolt/module_installer/puppetfile.rb +131 -0
- data/lib/bolt/module_installer/resolver.rb +129 -0
- data/lib/bolt/module_installer/specs/forge_spec.rb +91 -0
- data/lib/bolt/module_installer/specs/git_spec.rb +150 -0
- data/lib/bolt/module_installer/specs/id/base.rb +116 -0
- data/lib/bolt/module_installer/specs/id/gitclone.rb +120 -0
- data/lib/bolt/module_installer/specs/id/github.rb +90 -0
- data/lib/bolt/module_installer/specs/id/gitlab.rb +92 -0
- data/lib/bolt/module_installer/specs.rb +95 -0
- data/lib/bolt/module_installer.rb +208 -0
- data/lib/bolt/node/errors.rb +55 -0
- data/lib/bolt/node/output.rb +29 -0
- data/lib/bolt/outputter/human.rb +958 -0
- data/lib/bolt/outputter/json.rb +205 -0
- data/lib/bolt/outputter/logger.rb +76 -0
- data/lib/bolt/outputter/rainbow.rb +118 -0
- data/lib/bolt/outputter.rb +57 -0
- data/lib/bolt/pal/issues.rb +19 -0
- data/lib/bolt/pal/logging.rb +17 -0
- data/lib/bolt/pal/yaml_plan/evaluator.rb +83 -0
- data/lib/bolt/pal/yaml_plan/loader.rb +94 -0
- data/lib/bolt/pal/yaml_plan/parameter.rb +63 -0
- data/lib/bolt/pal/yaml_plan/step/command.rb +45 -0
- data/lib/bolt/pal/yaml_plan/step/download.rb +37 -0
- data/lib/bolt/pal/yaml_plan/step/eval.rb +42 -0
- data/lib/bolt/pal/yaml_plan/step/message.rb +31 -0
- data/lib/bolt/pal/yaml_plan/step/plan.rb +42 -0
- data/lib/bolt/pal/yaml_plan/step/resources.rb +170 -0
- data/lib/bolt/pal/yaml_plan/step/script.rb +62 -0
- data/lib/bolt/pal/yaml_plan/step/task.rb +42 -0
- data/lib/bolt/pal/yaml_plan/step/upload.rb +37 -0
- data/lib/bolt/pal/yaml_plan/step/verbose.rb +31 -0
- data/lib/bolt/pal/yaml_plan/step.rb +223 -0
- data/lib/bolt/pal/yaml_plan/transpiler.rb +90 -0
- data/lib/bolt/pal/yaml_plan.rb +172 -0
- data/lib/bolt/pal.rb +847 -0
- data/lib/bolt/plan_creator.rb +219 -0
- data/lib/bolt/plan_future.rb +86 -0
- data/lib/bolt/plan_result.rb +44 -0
- data/lib/bolt/plugin/cache.rb +76 -0
- data/lib/bolt/plugin/env_var.rb +54 -0
- data/lib/bolt/plugin/module.rb +276 -0
- data/lib/bolt/plugin/prompt.rb +36 -0
- data/lib/bolt/plugin/puppet_connect_data.rb +84 -0
- data/lib/bolt/plugin/puppetdb.rb +124 -0
- data/lib/bolt/plugin/task.rb +72 -0
- data/lib/bolt/plugin.rb +380 -0
- data/lib/bolt/project.rb +219 -0
- data/lib/bolt/project_manager/config_migrator.rb +113 -0
- data/lib/bolt/project_manager/inventory_migrator.rb +67 -0
- data/lib/bolt/project_manager/migrator.rb +39 -0
- data/lib/bolt/project_manager/module_migrator.rb +203 -0
- data/lib/bolt/project_manager.rb +221 -0
- data/lib/bolt/puppetdb/client.rb +153 -0
- data/lib/bolt/puppetdb/config.rb +176 -0
- data/lib/bolt/puppetdb/instance.rb +146 -0
- data/lib/bolt/puppetdb.rb +15 -0
- data/lib/bolt/r10k_log_proxy.rb +30 -0
- data/lib/bolt/rerun.rb +55 -0
- data/lib/bolt/resource_instance.rb +133 -0
- data/lib/bolt/result.rb +247 -0
- data/lib/bolt/result_set.rb +128 -0
- data/lib/bolt/shell/bash/tmpdir.rb +62 -0
- data/lib/bolt/shell/bash.rb +516 -0
- data/lib/bolt/shell/powershell/snippets.rb +181 -0
- data/lib/bolt/shell/powershell.rb +365 -0
- data/lib/bolt/shell.rb +105 -0
- data/lib/bolt/target.rb +174 -0
- data/lib/bolt/task/puppet_server.rb +27 -0
- data/lib/bolt/task/run.rb +55 -0
- data/lib/bolt/task.rb +163 -0
- data/lib/bolt/transport/base.rb +252 -0
- data/lib/bolt/transport/docker/connection.rb +150 -0
- data/lib/bolt/transport/docker.rb +23 -0
- data/lib/bolt/transport/jail/connection.rb +81 -0
- data/lib/bolt/transport/jail.rb +21 -0
- data/lib/bolt/transport/local/connection.rb +106 -0
- data/lib/bolt/transport/local.rb +20 -0
- data/lib/bolt/transport/lxd/connection.rb +115 -0
- data/lib/bolt/transport/lxd.rb +26 -0
- data/lib/bolt/transport/orch/connection.rb +111 -0
- data/lib/bolt/transport/orch.rb +271 -0
- data/lib/bolt/transport/podman/connection.rb +102 -0
- data/lib/bolt/transport/podman.rb +19 -0
- data/lib/bolt/transport/remote.rb +41 -0
- data/lib/bolt/transport/simple.rb +54 -0
- data/lib/bolt/transport/ssh/connection.rb +321 -0
- data/lib/bolt/transport/ssh/exec_connection.rb +140 -0
- data/lib/bolt/transport/ssh.rb +48 -0
- data/lib/bolt/transport/winrm/connection.rb +378 -0
- data/lib/bolt/transport/winrm.rb +33 -0
- data/lib/bolt/util/format.rb +68 -0
- data/lib/bolt/util/puppet_log_level.rb +21 -0
- data/lib/bolt/util.rb +465 -0
- data/lib/bolt/validator.rb +227 -0
- data/lib/bolt/version.rb +5 -0
- data/lib/bolt.rb +8 -0
- data/lib/bolt_server/acl.rb +39 -0
- data/lib/bolt_server/base_config.rb +112 -0
- data/lib/bolt_server/config.rb +64 -0
- data/lib/bolt_server/file_cache.rb +200 -0
- data/lib/bolt_server/request_error.rb +11 -0
- data/lib/bolt_server/schemas/action-check_node_connections.json +14 -0
- data/lib/bolt_server/schemas/action-run_command.json +12 -0
- data/lib/bolt_server/schemas/action-run_script.json +47 -0
- data/lib/bolt_server/schemas/action-run_task.json +20 -0
- data/lib/bolt_server/schemas/action-upload_file.json +47 -0
- data/lib/bolt_server/schemas/partials/target-any.json +10 -0
- data/lib/bolt_server/schemas/partials/target-ssh.json +88 -0
- data/lib/bolt_server/schemas/partials/target-winrm.json +67 -0
- data/lib/bolt_server/schemas/partials/task.json +94 -0
- data/lib/bolt_server/schemas/transport-ssh.json +25 -0
- data/lib/bolt_server/schemas/transport-winrm.json +19 -0
- data/lib/bolt_server/transport_app.rb +554 -0
- data/lib/bolt_spec/bolt_context.rb +226 -0
- data/lib/bolt_spec/plans/action_stubs/command_stub.rb +51 -0
- data/lib/bolt_spec/plans/action_stubs/download_stub.rb +66 -0
- data/lib/bolt_spec/plans/action_stubs/plan_stub.rb +55 -0
- data/lib/bolt_spec/plans/action_stubs/script_stub.rb +59 -0
- data/lib/bolt_spec/plans/action_stubs/task_stub.rb +57 -0
- data/lib/bolt_spec/plans/action_stubs/upload_stub.rb +65 -0
- data/lib/bolt_spec/plans/action_stubs.rb +196 -0
- data/lib/bolt_spec/plans/mock_executor.rb +361 -0
- data/lib/bolt_spec/plans/publish_stub.rb +49 -0
- data/lib/bolt_spec/plans.rb +190 -0
- data/lib/bolt_spec/run.rb +246 -0
- data/lib/logging_extensions/logging.rb +13 -0
- data/libexec/apply_catalog.rb +130 -0
- data/libexec/bolt_catalog +68 -0
- data/libexec/custom_facts.rb +63 -0
- data/libexec/query_resources.rb +75 -0
- data/modules/aggregate/lib/puppet/functions/aggregate/count.rb +21 -0
- data/modules/aggregate/lib/puppet/functions/aggregate/nodes.rb +22 -0
- data/modules/aggregate/lib/puppet/functions/aggregate/targets.rb +21 -0
- data/modules/aggregate/plans/count.pp +56 -0
- data/modules/aggregate/plans/targets.pp +56 -0
- data/modules/canary/lib/puppet/functions/canary/merge.rb +13 -0
- data/modules/canary/lib/puppet/functions/canary/random_split.rb +22 -0
- data/modules/canary/lib/puppet/functions/canary/skip.rb +25 -0
- data/modules/canary/plans/init.pp +100 -0
- data/modules/puppet_connect/plans/test_input_data.pp +94 -0
- data/modules/puppetdb_fact/plans/init.pp +20 -0
- data/resources/bolt_bash_completion.sh +214 -0
- 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
|