bolt 1.49.0 → 2.0.0
Sign up to get free protection for your applications and to get access to all the features.
Potentially problematic release.
This version of bolt might be problematic. Click here for more details.
- checksums.yaml +4 -4
- data/Puppetfile +6 -6
- data/bolt-modules/boltlib/lib/puppet/datatypes/target.rb +24 -45
- data/bolt-modules/boltlib/lib/puppet/functions/add_facts.rb +3 -3
- data/bolt-modules/boltlib/lib/puppet/functions/add_to_group.rb +1 -1
- data/bolt-modules/boltlib/lib/puppet/functions/apply_prep.rb +10 -12
- data/bolt-modules/boltlib/lib/puppet/functions/catch_errors.rb +1 -1
- data/bolt-modules/boltlib/lib/puppet/functions/fail_plan.rb +3 -3
- data/bolt-modules/boltlib/lib/puppet/functions/get_resources.rb +5 -4
- data/bolt-modules/boltlib/lib/puppet/functions/get_target.rb +1 -3
- data/bolt-modules/boltlib/lib/puppet/functions/get_targets.rb +1 -2
- data/bolt-modules/boltlib/lib/puppet/functions/puppetdb_fact.rb +2 -2
- data/bolt-modules/boltlib/lib/puppet/functions/remove_from_group.rb +2 -2
- data/bolt-modules/boltlib/lib/puppet/functions/resolve_references.rb +1 -1
- data/bolt-modules/boltlib/lib/puppet/functions/run_command.rb +7 -3
- data/bolt-modules/boltlib/lib/puppet/functions/run_plan.rb +15 -31
- data/bolt-modules/boltlib/lib/puppet/functions/run_script.rb +9 -5
- data/bolt-modules/boltlib/lib/puppet/functions/run_task.rb +9 -3
- data/bolt-modules/boltlib/lib/puppet/functions/set_config.rb +4 -3
- data/bolt-modules/boltlib/lib/puppet/functions/set_feature.rb +6 -6
- data/bolt-modules/boltlib/lib/puppet/functions/set_var.rb +2 -2
- data/bolt-modules/boltlib/lib/puppet/functions/upload_file.rb +7 -3
- data/bolt-modules/boltlib/lib/puppet/functions/wait_until_available.rb +6 -2
- data/bolt-modules/boltlib/lib/puppet/functions/without_default_logging.rb +2 -2
- data/bolt-modules/ctrl/lib/puppet/functions/ctrl/do_until.rb +2 -1
- data/bolt-modules/file/lib/puppet/functions/file/exists.rb +2 -1
- data/bolt-modules/file/lib/puppet/functions/file/join.rb +1 -0
- data/bolt-modules/file/lib/puppet/functions/file/read.rb +1 -0
- data/bolt-modules/file/lib/puppet/functions/file/readable.rb +2 -1
- data/bolt-modules/out/lib/puppet/functions/out/message.rb +1 -1
- data/bolt-modules/system/lib/puppet/functions/system/env.rb +1 -0
- data/lib/bolt/applicator.rb +70 -118
- data/lib/bolt/apply_target.rb +1 -1
- data/lib/bolt/bolt_option_parser.rb +21 -37
- data/lib/bolt/catalog.rb +5 -22
- data/lib/bolt/catalog/logging.rb +1 -1
- data/lib/bolt/cli.rb +33 -44
- data/lib/bolt/config.rb +15 -18
- data/lib/bolt/error.rb +2 -2
- data/lib/bolt/executor.rb +32 -40
- data/lib/bolt/inventory.rb +9 -367
- data/lib/bolt/inventory/group.rb +293 -182
- data/lib/bolt/inventory/{inventory2.rb → inventory.rb} +25 -14
- data/lib/bolt/inventory/target.rb +1 -1
- data/lib/bolt/module.rb +4 -4
- data/lib/bolt/outputter/human.rb +11 -6
- data/lib/bolt/outputter/json.rb +3 -11
- data/lib/bolt/pal.rb +1 -2
- data/lib/bolt/pal/yaml_plan/step/resources.rb +1 -1
- data/lib/bolt/plugin.rb +1 -1
- data/lib/bolt/plugin/module.rb +19 -36
- data/lib/bolt/plugin/prompt.rb +2 -4
- data/lib/bolt/puppetdb/config.rb +1 -3
- data/lib/bolt/result.rb +3 -6
- data/lib/bolt/secret/base.rb +0 -6
- data/lib/bolt/target.rb +8 -219
- data/lib/bolt/transport/local/shell.rb +9 -13
- data/lib/bolt/transport/orch.rb +3 -5
- data/lib/bolt/transport/ssh.rb +1 -0
- data/lib/bolt/transport/ssh/connection.rb +1 -4
- data/lib/bolt/transport/winrm/connection.rb +1 -1
- data/lib/bolt/util.rb +2 -8
- data/lib/bolt/version.rb +1 -1
- data/lib/bolt_server/transport_app.rb +35 -17
- data/lib/bolt_spec/plans.rb +8 -2
- data/libexec/bolt_catalog +19 -5
- metadata +4 -8
- data/exe/bolt-inventory-pdb +0 -13
- data/lib/bolt/inventory/group2.rb +0 -403
- data/lib/bolt_ext/puppetdb_inventory.rb +0 -129
data/lib/bolt/inventory.rb
CHANGED
@@ -3,9 +3,10 @@
|
|
3
3
|
require 'set'
|
4
4
|
require 'bolt/config'
|
5
5
|
require 'bolt/inventory/group'
|
6
|
-
require 'bolt/inventory/
|
6
|
+
require 'bolt/inventory/inventory'
|
7
7
|
require 'bolt/target'
|
8
8
|
require 'bolt/util'
|
9
|
+
require 'bolt/plugin'
|
9
10
|
require 'yaml'
|
10
11
|
|
11
12
|
module Bolt
|
@@ -43,7 +44,7 @@ module Bolt
|
|
43
44
|
end
|
44
45
|
end
|
45
46
|
|
46
|
-
def self.from_config(config, plugins
|
47
|
+
def self.from_config(config, plugins)
|
47
48
|
if ENV.include?(ENVIRONMENT_VAR)
|
48
49
|
begin
|
49
50
|
data = YAML.safe_load(ENV[ENVIRONMENT_VAR])
|
@@ -65,378 +66,19 @@ module Bolt
|
|
65
66
|
end
|
66
67
|
|
67
68
|
def self.create_version(data, config, plugins)
|
68
|
-
version = (data || {}).delete('version') {
|
69
|
+
version = (data || {}).delete('version') { 2 }
|
69
70
|
case version
|
70
|
-
when 1
|
71
|
-
new(data, config, plugins: plugins)
|
72
71
|
when 2
|
73
|
-
Bolt::Inventory::
|
72
|
+
Bolt::Inventory::Inventory.new(data, config, plugins: plugins)
|
74
73
|
else
|
75
74
|
raise ValidationError.new("Unsupported version #{version} specified in inventory", nil)
|
76
75
|
end
|
77
76
|
end
|
78
77
|
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
@logger = Logging.logger[self]
|
84
|
-
# Config is saved to add config options to targets
|
85
|
-
@config = config || Bolt::Config.default
|
86
|
-
@data = data ||= {}
|
87
|
-
@groups = Group.new(data.merge('name' => 'all'))
|
88
|
-
@group_lookup = {}
|
89
|
-
@target_vars = target_vars
|
90
|
-
@target_facts = target_facts
|
91
|
-
@target_features = target_features
|
92
|
-
@plugins = plugins
|
93
|
-
@target_plugin_hooks = target_plugin_hooks
|
94
|
-
|
95
|
-
@groups.resolve_aliases(@groups.node_aliases)
|
96
|
-
collect_groups
|
97
|
-
|
98
|
-
deprecation unless @data.empty?
|
99
|
-
end
|
100
|
-
|
101
|
-
def validate
|
102
|
-
@groups.validate
|
103
|
-
end
|
104
|
-
|
105
|
-
def version
|
106
|
-
1
|
107
|
-
end
|
108
|
-
|
109
|
-
def deprecation
|
110
|
-
msg = <<~MSG
|
111
|
-
Deprecation Warning: Starting with Bolt 2.0, inventory file v1 will no longer
|
112
|
-
be supported. v1 inventory files can be automatically migrated to v2 using
|
113
|
-
'bolt project migrate'. Inventory files are modified in place and will not
|
114
|
-
preserve formatting or comments.
|
115
|
-
MSG
|
116
|
-
@logger.warn(msg)
|
117
|
-
end
|
118
|
-
private :deprecation
|
119
|
-
|
120
|
-
def collect_groups
|
121
|
-
# Provide a lookup map for finding a group by name
|
122
|
-
@group_lookup = @groups.collect_groups
|
123
|
-
end
|
124
|
-
|
125
|
-
def group_names
|
126
|
-
@group_lookup.keys
|
127
|
-
end
|
128
|
-
|
129
|
-
def node_names
|
130
|
-
@groups.node_names
|
131
|
-
end
|
132
|
-
|
133
|
-
def plugin_hooks(target)
|
134
|
-
@target_plugin_hooks[target.name] || {}
|
135
|
-
end
|
136
|
-
|
137
|
-
def get_targets(targets)
|
138
|
-
targets = expand_targets(targets)
|
139
|
-
targets = if targets.is_a? Array
|
140
|
-
targets.flatten.uniq(&:name)
|
141
|
-
else
|
142
|
-
[targets]
|
143
|
-
end
|
144
|
-
targets.map { |t| update_target(t) }
|
145
|
-
end
|
146
|
-
|
147
|
-
def add_to_group(targets, desired_group)
|
148
|
-
if group_names.include?(desired_group)
|
149
|
-
targets.each do |target|
|
150
|
-
if group_names.include?(target.name)
|
151
|
-
raise ValidationError.new("Group #{target.name} conflicts with node of the same name", target.name)
|
152
|
-
end
|
153
|
-
add_node(@groups, target, desired_group)
|
154
|
-
end
|
155
|
-
else
|
156
|
-
raise ValidationError.new("Group #{desired_group} does not exist in inventory", nil)
|
157
|
-
end
|
158
|
-
end
|
159
|
-
|
160
|
-
def remove_from_group(target, desired_group)
|
161
|
-
unless target.length == 1
|
162
|
-
raise ValidationError.new("'remove_from_group' expects a single Target, got #{target.length}", nil)
|
163
|
-
end
|
164
|
-
|
165
|
-
if desired_group == 'all'
|
166
|
-
raise ValidationError.new("Cannot remove Target from Group 'all'", nil)
|
167
|
-
end
|
168
|
-
|
169
|
-
if group_names.include?(desired_group)
|
170
|
-
invalidate_group_cache!(target.first)
|
171
|
-
remove_node(@groups, target.first, desired_group)
|
172
|
-
else
|
173
|
-
raise ValidationError.new("Group #{desired_group} does not exist in inventory", nil)
|
174
|
-
end
|
175
|
-
end
|
176
|
-
|
177
|
-
def set_var(target, var_hash)
|
178
|
-
set_vars_from_hash(target.name, var_hash)
|
179
|
-
end
|
180
|
-
|
181
|
-
def vars(target)
|
182
|
-
@target_vars[target.name] || {}
|
183
|
-
end
|
184
|
-
|
185
|
-
def add_facts(target, new_facts = {})
|
186
|
-
@logger.warn("No facts to add") if new_facts.empty?
|
187
|
-
facts = set_facts(target.name, new_facts)
|
188
|
-
# rubocop:disable Style/GlobalVars
|
189
|
-
$future ? target : facts
|
190
|
-
# rubocop:enable Style/GlobalVars
|
191
|
-
end
|
192
|
-
|
193
|
-
def facts(target)
|
194
|
-
@target_facts[target.name] || {}
|
195
|
-
end
|
196
|
-
|
197
|
-
def set_feature(target, feature, value = true)
|
198
|
-
@target_features[target.name] ||= Set.new
|
199
|
-
if value
|
200
|
-
@target_features[target.name] << feature
|
201
|
-
else
|
202
|
-
@target_features[target.name].delete(feature)
|
203
|
-
end
|
204
|
-
end
|
205
|
-
|
206
|
-
def features(target)
|
207
|
-
@target_features[target.name] || Set.new
|
208
|
-
end
|
209
|
-
|
210
|
-
def target_alias(target)
|
211
|
-
@groups.node_aliases.each_with_object([]) do |(alia, name), acc|
|
212
|
-
if target.name == name
|
213
|
-
acc << alia
|
214
|
-
end
|
215
|
-
end.uniq
|
216
|
-
end
|
217
|
-
|
218
|
-
def data_hash
|
219
|
-
{
|
220
|
-
data: @data,
|
221
|
-
target_hash: {
|
222
|
-
target_vars: @target_vars,
|
223
|
-
target_facts: @target_facts,
|
224
|
-
target_features: @target_features
|
225
|
-
},
|
226
|
-
config: @config.transport_data_get
|
227
|
-
}
|
228
|
-
end
|
229
|
-
|
230
|
-
#### PRIVATE ####
|
231
|
-
#
|
232
|
-
# For debugging only now
|
233
|
-
def groups_in(node_name)
|
234
|
-
@groups.data_for(node_name)['groups'] || {}
|
235
|
-
end
|
236
|
-
private :groups_in
|
237
|
-
|
238
|
-
# TODO: Possibly refactor this once inventory v2 is more stable
|
239
|
-
def self.localhost_defaults(data)
|
240
|
-
defaults = {
|
241
|
-
'config' => {
|
242
|
-
'transport' => 'local',
|
243
|
-
'local' => { 'interpreters' => { '.rb' => RbConfig.ruby } }
|
244
|
-
},
|
245
|
-
'features' => ['puppet-agent']
|
246
|
-
}
|
247
|
-
data = Bolt::Util.deep_merge(defaults, data)
|
248
|
-
# If features is an empty array deep_merge won't add the puppet-agent
|
249
|
-
data['features'] += ['puppet-agent'] if data['features'].empty?
|
250
|
-
data
|
251
|
-
end
|
252
|
-
|
253
|
-
# Pass a target to get_targets for a public version of this
|
254
|
-
# Should this reconfigure configured targets?
|
255
|
-
def update_target(target)
|
256
|
-
data = @groups.data_for(target.name)
|
257
|
-
data ||= {}
|
258
|
-
|
259
|
-
unless data['config']
|
260
|
-
@logger.debug("Did not find config for #{target.name} in inventory")
|
261
|
-
data['config'] = {}
|
262
|
-
end
|
263
|
-
|
264
|
-
data = self.class.localhost_defaults(data) if target.name == 'localhost'
|
265
|
-
# These should only get set from the inventory if they have not yet
|
266
|
-
# been instantiated
|
267
|
-
set_vars_from_hash(target.name, data['vars']) unless @target_vars[target.name]
|
268
|
-
set_facts(target.name, data['facts']) unless @target_facts[target.name]
|
269
|
-
data['features']&.each { |feature| set_feature(target, feature) } unless @target_features[target.name]
|
270
|
-
unless @target_plugin_hooks[target.name]
|
271
|
-
set_plugin_hooks(target.name, (@plugins&.plugin_hooks || {}).merge(data['plugin_hooks'] || {}))
|
272
|
-
end
|
273
|
-
|
274
|
-
# Use Config object to ensure config section is treated consistently with config file
|
275
|
-
conf = @config.deep_clone
|
276
|
-
conf.update_from_inventory(data['config'])
|
277
|
-
conf.validate
|
278
|
-
|
279
|
-
target.update_conf(conf.transport_conf)
|
280
|
-
|
281
|
-
unless target.transport.nil? || Bolt::TRANSPORTS.include?(target.transport.to_sym)
|
282
|
-
raise Bolt::UnknownTransportError.new(target.transport, target.uri)
|
283
|
-
end
|
284
|
-
|
285
|
-
target
|
286
|
-
end
|
287
|
-
private :update_target
|
288
|
-
|
289
|
-
# If target is a group name, expand it to the members of that group.
|
290
|
-
# Else match against nodes in inventory by name or alias.
|
291
|
-
# If a wildcard string, error if no matches are found.
|
292
|
-
# Else fall back to [target] if no matches are found.
|
293
|
-
def resolve_name(target)
|
294
|
-
if (group = @group_lookup[target])
|
295
|
-
group.node_names
|
296
|
-
else
|
297
|
-
# Try to wildcard match nodes in inventory
|
298
|
-
# Ignore case because hostnames are generally case-insensitive
|
299
|
-
regexp = Regexp.new("^#{Regexp.escape(target).gsub('\*', '.*?')}$", Regexp::IGNORECASE)
|
300
|
-
|
301
|
-
nodes = @groups.node_names.select { |node| node =~ regexp }
|
302
|
-
nodes += @groups.node_aliases.select { |target_alias, _node| target_alias =~ regexp }.values
|
303
|
-
|
304
|
-
if nodes.empty?
|
305
|
-
raise(WildcardError, target) if target.include?('*')
|
306
|
-
[target]
|
307
|
-
else
|
308
|
-
nodes
|
309
|
-
end
|
310
|
-
end
|
311
|
-
end
|
312
|
-
private :resolve_name
|
313
|
-
|
314
|
-
def create_target(data)
|
315
|
-
Bolt::Target.new(data)
|
316
|
-
end
|
317
|
-
private :create_target
|
318
|
-
|
319
|
-
def expand_targets(targets)
|
320
|
-
if targets.is_a? Bolt::Target
|
321
|
-
targets.inventory = self
|
322
|
-
targets
|
323
|
-
elsif targets.is_a? Array
|
324
|
-
targets.map { |tish| expand_targets(tish) }
|
325
|
-
elsif targets.is_a? String
|
326
|
-
# Expand a comma-separated list
|
327
|
-
targets.split(/[[:space:],]+/).reject(&:empty?).map do |name|
|
328
|
-
ts = resolve_name(name)
|
329
|
-
ts.map do |t|
|
330
|
-
target = create_target(t)
|
331
|
-
target.inventory = self
|
332
|
-
target
|
333
|
-
end
|
334
|
-
end
|
335
|
-
end
|
336
|
-
end
|
337
|
-
private :expand_targets
|
338
|
-
|
339
|
-
def set_vars_from_hash(tname, data)
|
340
|
-
if data
|
341
|
-
# Instantiate empty vars hash in case no vars are defined
|
342
|
-
@target_vars[tname] ||= {}
|
343
|
-
# Assign target new merged vars hash
|
344
|
-
# This is essentially a copy-on-write to maintain the immutability of @target_vars
|
345
|
-
@target_vars[tname] = @target_vars[tname].merge(data).freeze
|
346
|
-
end
|
347
|
-
end
|
348
|
-
private :set_vars_from_hash
|
349
|
-
|
350
|
-
def set_facts(tname, hash)
|
351
|
-
if hash
|
352
|
-
@target_facts[tname] ||= {}
|
353
|
-
@target_facts[tname] = Bolt::Util.deep_merge(@target_facts[tname], hash).freeze
|
354
|
-
end
|
355
|
-
end
|
356
|
-
private :set_facts
|
357
|
-
|
358
|
-
def set_plugin_hooks(tname, hash)
|
359
|
-
if hash
|
360
|
-
@target_plugin_hooks[tname] ||= {}
|
361
|
-
@target_plugin_hooks[tname].merge!(hash)
|
362
|
-
end
|
363
|
-
end
|
364
|
-
private :set_plugin_hooks
|
365
|
-
|
366
|
-
def remove_node(current_group, target, desired_group, track = { 'all' => nil })
|
367
|
-
# Remove the target from the group
|
368
|
-
if current_group.name == desired_group
|
369
|
-
current_group.nodes.delete(target.name)
|
370
|
-
# If the target remains in the group, add the group data to the cache
|
371
|
-
elsif current_group.node_names.include?(target.name)
|
372
|
-
add_group_data_to_cache(current_group, target, track)
|
373
|
-
end
|
374
|
-
current_group.groups.each do |child_group|
|
375
|
-
# If target was in current group, remove it from all child groups
|
376
|
-
desired_group = child_group.name if current_group.name == desired_group
|
377
|
-
track[child_group.name] = current_group
|
378
|
-
remove_node(child_group, target, desired_group)
|
379
|
-
end
|
380
|
-
end
|
381
|
-
private :remove_node
|
382
|
-
|
383
|
-
def add_node(current_group, target, desired_group, track = { 'all' => nil })
|
384
|
-
if current_group.name == desired_group
|
385
|
-
# Group to add to is found
|
386
|
-
t_name = target.name
|
387
|
-
# Add target to nodes hash
|
388
|
-
current_group.nodes[t_name] = { 'name' => t_name }.merge(target.options)
|
389
|
-
add_group_data_to_cache(current_group, target, track)
|
390
|
-
return true
|
391
|
-
end
|
392
|
-
# Recurse on children Groups if not desired_group
|
393
|
-
current_group.groups.each do |child_group|
|
394
|
-
track[child_group.name] = current_group
|
395
|
-
add_node(child_group, target, desired_group, track)
|
396
|
-
end
|
397
|
-
end
|
398
|
-
private :add_node
|
399
|
-
|
400
|
-
def add_group_data_to_cache(current_group, target, track)
|
401
|
-
t_name = target.name
|
402
|
-
current_group_data = { facts: current_group.facts,
|
403
|
-
vars: current_group.vars,
|
404
|
-
features: current_group.features,
|
405
|
-
plugin_hooks: current_group.plugin_hooks }
|
406
|
-
data = inherit_data(track, current_group.name, current_group_data)
|
407
|
-
set_facts(t_name, @target_facts[t_name] ? data[:facts].merge(@target_facts[t_name]) : data[:facts])
|
408
|
-
set_vars_from_hash(t_name, @target_vars[t_name] ? data[:vars].merge(@target_vars[t_name]) : data[:vars])
|
409
|
-
data[:features].each do |feature|
|
410
|
-
set_feature(target, feature)
|
411
|
-
end
|
412
|
-
hook_data = @config.plugin_hooks.merge(data[:plugin_hooks])
|
413
|
-
hash = if @target_plugin_hooks[t_name]
|
414
|
-
hook_data.merge(@target_plugin_hooks[t_name])
|
415
|
-
else
|
416
|
-
hook_data
|
417
|
-
end
|
418
|
-
set_plugin_hooks(t_name, hash)
|
419
|
-
end
|
420
|
-
private :add_group_data_to_cache
|
421
|
-
|
422
|
-
def inherit_data(track, name, data)
|
423
|
-
unless track[name].nil?
|
424
|
-
data[:facts] = track[name].facts.merge(data[:facts])
|
425
|
-
data[:vars] = track[name].vars.merge(data[:vars])
|
426
|
-
data[:features].concat(track[name].features)
|
427
|
-
data[:plugin_hooks] = track[name].plugin_hooks.merge(data[:plugin_hooks])
|
428
|
-
inherit_data(track, track[name].name, data)
|
429
|
-
end
|
430
|
-
data
|
431
|
-
end
|
432
|
-
private :inherit_data
|
433
|
-
|
434
|
-
def invalidate_group_cache!(target)
|
435
|
-
@target_facts.delete(target.name)
|
436
|
-
@target_features.delete(target.name)
|
437
|
-
@target_vars.delete(target.name)
|
438
|
-
@target_plugin_hooks.delete(target.name)
|
78
|
+
def self.empty
|
79
|
+
config = Bolt::Config.default
|
80
|
+
plugins = Bolt::Plugin.setup(config, nil, nil, Bolt::Analytics::NoopClient)
|
81
|
+
create_version({}, config, plugins)
|
439
82
|
end
|
440
|
-
private :invalidate_group_cache!
|
441
83
|
end
|
442
84
|
end
|
data/lib/bolt/inventory/group.rb
CHANGED
@@ -1,258 +1,376 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require 'bolt/inventory/group'
|
4
|
+
require 'bolt/inventory/inventory'
|
5
|
+
require 'bolt/inventory/target'
|
6
|
+
|
3
7
|
module Bolt
|
4
8
|
class Inventory
|
5
|
-
# Group is a specific implementation of Inventory based on nested
|
6
|
-
# structured data.
|
7
9
|
class Group
|
8
|
-
attr_accessor :name, :
|
9
|
-
:config, :rest, :facts, :vars, :features, :plugin_hooks
|
10
|
+
attr_accessor :name, :groups
|
10
11
|
|
11
12
|
# Regex used to validate group names and target aliases.
|
12
13
|
NAME_REGEX = /\A[a-z0-9_][a-z0-9_-]*\Z/.freeze
|
13
14
|
|
14
|
-
DATA_KEYS = %w[
|
15
|
-
|
16
|
-
GROUP_KEYS = DATA_KEYS + %w[groups
|
15
|
+
DATA_KEYS = %w[config facts vars features plugin_hooks].freeze
|
16
|
+
TARGET_KEYS = DATA_KEYS + %w[name alias uri]
|
17
|
+
GROUP_KEYS = DATA_KEYS + %w[name groups targets]
|
17
18
|
CONFIG_KEYS = Bolt::TRANSPORTS.keys.map(&:to_s) + ['transport']
|
18
19
|
|
19
|
-
def initialize(
|
20
|
+
def initialize(input, plugins)
|
20
21
|
@logger = Logging.logger[self]
|
22
|
+
@plugins = plugins
|
23
|
+
|
24
|
+
input = @plugins.resolve_top_level_references(input) if @plugins.reference?(input)
|
21
25
|
|
22
|
-
raise ValidationError.new("
|
23
|
-
|
26
|
+
raise ValidationError.new("Group does not have a name", nil) unless input.key?('name')
|
27
|
+
|
28
|
+
@name = @plugins.resolve_references(input['name'])
|
24
29
|
|
25
|
-
@name = data['name']
|
26
30
|
raise ValidationError.new("Group name must be a String, not #{@name.inspect}", nil) unless @name.is_a?(String)
|
27
31
|
raise ValidationError.new("Invalid group name #{@name}", @name) unless @name =~ NAME_REGEX
|
28
32
|
|
29
|
-
|
30
|
-
msg = "Found unexpected key(s) #{unexpected_keys.join(', ')} in group #{@name}"
|
31
|
-
@logger.warn(msg)
|
32
|
-
end
|
33
|
+
validate_group_input(input)
|
33
34
|
|
34
|
-
@
|
35
|
-
@facts = fetch_value(data, 'facts', Hash)
|
36
|
-
@features = fetch_value(data, 'features', Array)
|
37
|
-
@plugin_hooks = fetch_value(data, 'plugin_hooks', Hash)
|
38
|
-
@config = fetch_value(data, 'config', Hash)
|
35
|
+
@input = input
|
39
36
|
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
end
|
37
|
+
validate_data_keys(@input)
|
38
|
+
|
39
|
+
targets = @plugins.resolve_top_level_references(input.fetch('targets', []))
|
44
40
|
|
45
|
-
|
46
|
-
|
41
|
+
@unresolved_targets = {}
|
42
|
+
@resolved_targets = {}
|
47
43
|
|
48
|
-
@nodes = {}
|
49
44
|
@aliases = {}
|
50
|
-
|
51
|
-
|
52
|
-
|
45
|
+
@string_targets = []
|
46
|
+
|
47
|
+
Array(targets).each do |target|
|
48
|
+
# If target is a string, it can either be trivially defining a target
|
49
|
+
# or it could be a name/alias of a target defined in another group.
|
50
|
+
# We can't tell the difference until all groups have been resolved,
|
51
|
+
# so we store the string on its own here and process it later.
|
52
|
+
if target.is_a?(String)
|
53
|
+
@string_targets << target
|
54
|
+
# Handle plugins at this level so that lookups cannot trigger recursive lookups
|
55
|
+
elsif target.is_a?(Hash)
|
56
|
+
add_target_definition(target)
|
57
|
+
else
|
58
|
+
raise ValidationError.new("Target entry must be a String or Hash, not #{target.class}", @name)
|
53
59
|
end
|
60
|
+
end
|
54
61
|
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
62
|
+
groups = input.fetch('groups', [])
|
63
|
+
# 'groups' can be a _plugin reference, in which case we want to resolve
|
64
|
+
# it. That can itself return a reference, so we want to keep resolving
|
65
|
+
# them until we have a value. We don't just use resolve_references
|
66
|
+
# though, since that will resolve any nested references and we want to
|
67
|
+
# leave it to the group to do that lazily.
|
68
|
+
groups = @plugins.resolve_top_level_references(groups)
|
59
69
|
|
60
|
-
|
61
|
-
|
70
|
+
@groups = Array(groups).map { |g| Group.new(g, plugins) }
|
71
|
+
end
|
62
72
|
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
73
|
+
def target_data(target_name)
|
74
|
+
if @unresolved_targets.key?(target_name)
|
75
|
+
target = @unresolved_targets.delete(target_name)
|
76
|
+
resolved_data = resolve_data_keys(target, target_name).merge(
|
77
|
+
'name' => target['name'],
|
78
|
+
'uri' => target['uri'],
|
79
|
+
'alias' => target['alias'],
|
80
|
+
# groups come from group_data
|
81
|
+
'groups' => []
|
82
|
+
)
|
83
|
+
@resolved_targets[target_name] = resolved_data
|
84
|
+
else
|
85
|
+
@resolved_targets[target_name]
|
86
|
+
end
|
87
|
+
end
|
67
88
|
|
68
|
-
|
69
|
-
|
70
|
-
|
89
|
+
def all_target_names
|
90
|
+
@unresolved_targets.keys + @resolved_targets.keys
|
91
|
+
end
|
71
92
|
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
93
|
+
def add_target_definition(target)
|
94
|
+
# This check ensures target lookup plugins do not returns bare strings.
|
95
|
+
# Remove it if we decide to allows task plugins to return string Target
|
96
|
+
# names.
|
97
|
+
unless target.is_a?(Hash)
|
98
|
+
raise ValidationError.new("Target entry must be a Hash, not #{target.class}", @name)
|
99
|
+
end
|
100
|
+
|
101
|
+
target['name'] = @plugins.resolve_references(target['name']) if target.key?('name')
|
102
|
+
target['uri'] = @plugins.resolve_references(target['uri']) if target.key?('uri')
|
103
|
+
target['alias'] = @plugins.resolve_references(target['alias']) if target.key?('alias')
|
104
|
+
|
105
|
+
t_name = target['name'] || target['uri']
|
106
|
+
|
107
|
+
if t_name.nil? || t_name.empty?
|
108
|
+
raise ValidationError.new("No name or uri for target: #{target}", @name)
|
109
|
+
end
|
110
|
+
|
111
|
+
unless t_name.is_a? String
|
112
|
+
raise ValidationError.new("Target name must be a String, not #{t_name.class}", @name)
|
113
|
+
end
|
114
|
+
|
115
|
+
unless t_name.ascii_only?
|
116
|
+
raise ValidationError.new("Target name must be ASCII characters: #{target}", @name)
|
117
|
+
end
|
118
|
+
|
119
|
+
if local_targets.include?(t_name)
|
120
|
+
@logger.warn("Ignoring duplicate target in #{@name}: #{target}")
|
121
|
+
return
|
122
|
+
end
|
123
|
+
|
124
|
+
unless (unexpected_keys = target.keys - TARGET_KEYS).empty?
|
125
|
+
msg = "Found unexpected key(s) #{unexpected_keys.join(', ')} in target #{t_name}"
|
126
|
+
@logger.warn(msg)
|
127
|
+
end
|
77
128
|
|
78
|
-
|
129
|
+
validate_data_keys(target, t_name)
|
79
130
|
|
80
|
-
|
131
|
+
if target.include?('alias')
|
132
|
+
aliases = target['alias']
|
81
133
|
aliases = [aliases] if aliases.is_a?(String)
|
82
134
|
unless aliases.is_a?(Array)
|
83
|
-
msg = "Alias entry on #{
|
135
|
+
msg = "Alias entry on #{t_name} must be a String or Array, not #{aliases.class}"
|
84
136
|
raise ValidationError.new(msg, @name)
|
85
137
|
end
|
86
138
|
|
87
|
-
aliases
|
88
|
-
raise ValidationError.new("Invalid alias #{alia}", @name) unless alia =~ NAME_REGEX
|
89
|
-
|
90
|
-
if (found = @aliases[alia])
|
91
|
-
raise ValidationError.new(alias_conflict(alia, found, node['name']), @name)
|
92
|
-
end
|
93
|
-
@aliases[alia] = node['name']
|
94
|
-
end
|
139
|
+
insert_alia(t_name, aliases)
|
95
140
|
end
|
96
141
|
|
97
|
-
|
98
|
-
|
99
|
-
|
142
|
+
@unresolved_targets[t_name] = target
|
143
|
+
end
|
144
|
+
|
145
|
+
def remove_target(target)
|
146
|
+
@resolved_targets.delete(target.name)
|
147
|
+
@unresolved_targets.delete(target.name)
|
148
|
+
end
|
100
149
|
|
101
|
-
|
150
|
+
def add_target(target)
|
151
|
+
@resolved_targets[target.name] = { 'name' => target.name }
|
102
152
|
end
|
103
153
|
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
154
|
+
def insert_alia(target_name, aliases)
|
155
|
+
aliases.each do |alia|
|
156
|
+
raise ValidationError.new("Invalid alias #{alia}", @name) unless alia =~ NAME_REGEX
|
157
|
+
|
158
|
+
if (found = @aliases[alia])
|
159
|
+
raise ValidationError.new(alias_conflict(alia, found, target_name), @name)
|
160
|
+
end
|
161
|
+
@aliases[alia] = target_name
|
108
162
|
end
|
109
|
-
value
|
110
163
|
end
|
111
164
|
|
112
|
-
def
|
113
|
-
@
|
114
|
-
|
115
|
-
|
165
|
+
def clear_alia(target_name)
|
166
|
+
@aliases.reject! { |_alias, name| name == target_name }
|
167
|
+
end
|
168
|
+
|
169
|
+
def data_merge(data1, data2)
|
170
|
+
if data2.nil? || data1.nil?
|
171
|
+
return data2 || data1
|
172
|
+
end
|
173
|
+
|
174
|
+
{
|
175
|
+
'config' => Bolt::Util.deep_merge(data1['config'], data2['config']),
|
176
|
+
'name' => data1['name'] || data2['name'],
|
177
|
+
'uri' => data1['uri'] || data2['uri'],
|
178
|
+
# Collect all aliases across all groups for each target uri
|
179
|
+
'alias' => [*data1['alias'], *data2['alias']],
|
180
|
+
# Shallow merge instead of deep merge so that vars with a hash value
|
181
|
+
# are assigned a new hash, rather than merging the existing value
|
182
|
+
# with the value meant to replace it
|
183
|
+
'vars' => data1['vars'].merge(data2['vars']),
|
184
|
+
'facts' => Bolt::Util.deep_merge(data1['facts'], data2['facts']),
|
185
|
+
'features' => data1['features'] | data2['features'],
|
186
|
+
'plugin_hooks' => data1['plugin_hooks'].merge(data2['plugin_hooks']),
|
187
|
+
'groups' => data2['groups'] + data1['groups']
|
188
|
+
}
|
189
|
+
end
|
116
190
|
|
117
|
-
|
118
|
-
|
191
|
+
def resolve_string_targets(aliases, known_targets)
|
192
|
+
@string_targets.each do |string_target|
|
193
|
+
# If this is the name of a target defined elsewhere, then insert the
|
194
|
+
# target into this group as just a name. Otherwise, add a new target
|
195
|
+
# with the string as the URI.
|
196
|
+
if known_targets.include?(string_target)
|
197
|
+
@unresolved_targets[string_target] = { 'name' => string_target }
|
198
|
+
# If this is an alias for an existing target, then add it to this group
|
199
|
+
elsif (canonical_name = aliases[string_target])
|
200
|
+
if local_targets.include?(canonical_name)
|
201
|
+
@logger.warn("Ignoring duplicate target in #{@name}: #{canonical_name}")
|
202
|
+
else
|
203
|
+
@unresolved_targets[canonical_name] = { 'name' => canonical_name }
|
204
|
+
end
|
205
|
+
# If it's not the name or alias of an existing target, then make a
|
206
|
+
# new target using the string as the URI
|
207
|
+
elsif local_targets.include?(string_target)
|
208
|
+
@logger.warn("Ignoring duplicate target in #{@name}: #{string_target}")
|
119
209
|
else
|
120
|
-
@
|
210
|
+
@unresolved_targets[string_target] = { 'uri' => string_target }
|
121
211
|
end
|
122
212
|
end
|
123
213
|
|
124
|
-
@groups.each { |g| g.
|
214
|
+
@groups.each { |g| g.resolve_string_targets(aliases, known_targets) }
|
125
215
|
end
|
126
216
|
|
127
|
-
private def alias_conflict(name,
|
128
|
-
"Alias #{name} refers to multiple targets: #{
|
217
|
+
private def alias_conflict(name, target1, target2)
|
218
|
+
"Alias #{name} refers to multiple targets: #{target1} and #{target2}"
|
129
219
|
end
|
130
220
|
|
131
221
|
private def group_alias_conflict(name)
|
132
222
|
"Group #{name} conflicts with alias of the same name"
|
133
223
|
end
|
134
224
|
|
135
|
-
private def
|
136
|
-
"Group #{name} conflicts with
|
225
|
+
private def group_target_conflict(name)
|
226
|
+
"Group #{name} conflicts with target of the same name"
|
227
|
+
end
|
228
|
+
|
229
|
+
private def alias_target_conflict(name)
|
230
|
+
"Target name #{name} conflicts with alias of the same name"
|
137
231
|
end
|
138
232
|
|
139
|
-
|
140
|
-
"
|
233
|
+
def validate_group_input(input)
|
234
|
+
raise ValidationError.new("Expected group to be a Hash, not #{input.class}", nil) unless input.is_a?(Hash)
|
235
|
+
|
236
|
+
# DEPRECATION : remove this before finalization
|
237
|
+
if input.key?('target-lookups')
|
238
|
+
msg = "'target-lookups' are no longer a separate key. Merge 'target-lookups' and 'targets' lists and replace 'plugin' with '_plugin'" # rubocop:disable Layout/LineLength
|
239
|
+
raise ValidationError.new(msg, @name)
|
240
|
+
end
|
241
|
+
|
242
|
+
if input.key?('nodes')
|
243
|
+
msg = <<~MSG.chomp
|
244
|
+
Found 'nodes' key in group #{@name}. This looks like a v1 inventory file, which is
|
245
|
+
no longer supported by Bolt. Migrate to a v2 inventory file automatically using
|
246
|
+
'bolt project migrate'.
|
247
|
+
MSG
|
248
|
+
raise ValidationError.new(msg, nil)
|
249
|
+
end
|
250
|
+
|
251
|
+
unless (unexpected_keys = input.keys - GROUP_KEYS).empty?
|
252
|
+
msg = "Found unexpected key(s) #{unexpected_keys.join(', ')} in group #{@name}"
|
253
|
+
@logger.warn(msg)
|
254
|
+
end
|
255
|
+
|
256
|
+
Bolt::Util.walk_keys(input) do |key|
|
257
|
+
if @plugins.reference?(key)
|
258
|
+
raise ValidationError.new("Group keys cannot be specified as _plugin references", @name)
|
259
|
+
else
|
260
|
+
key
|
261
|
+
end
|
262
|
+
end
|
141
263
|
end
|
142
264
|
|
143
|
-
def validate(
|
265
|
+
def validate(used_group_names = Set.new, used_target_names = Set.new, used_aliases = {})
|
144
266
|
# Test if this group name conflicts with anything used before.
|
145
|
-
raise ValidationError.new("Tried to redefine group #{@name}", @name) if
|
146
|
-
raise ValidationError.new(
|
147
|
-
raise ValidationError.new(group_alias_conflict(@name), @name) if
|
267
|
+
raise ValidationError.new("Tried to redefine group #{@name}", @name) if used_group_names.include?(@name)
|
268
|
+
raise ValidationError.new(group_target_conflict(@name), @name) if used_target_names.include?(@name)
|
269
|
+
raise ValidationError.new(group_alias_conflict(@name), @name) if used_aliases.include?(@name)
|
148
270
|
|
149
|
-
|
271
|
+
used_group_names << @name
|
150
272
|
|
151
|
-
# Collect
|
152
|
-
# Used names validate that previously used group names don't conflict with new
|
153
|
-
@
|
154
|
-
# Require
|
273
|
+
# Collect target names and aliases into a list used to validate that subgroups don't conflict.
|
274
|
+
# Used names validate that previously used group names don't conflict with new target names/aliases.
|
275
|
+
@unresolved_targets.merge(@resolved_targets).each do |t_name, t_data|
|
276
|
+
# Require targets to be parseable as a Target.
|
155
277
|
begin
|
156
|
-
|
278
|
+
# Catch malformed URI here
|
279
|
+
Bolt::Inventory::Target.parse_uri(t_data['uri'])
|
157
280
|
rescue Bolt::ParseError => e
|
158
281
|
@logger.debug(e)
|
159
|
-
raise ValidationError.new("Invalid
|
282
|
+
raise ValidationError.new("Invalid target uri #{t_data['uri']}", @name)
|
160
283
|
end
|
161
284
|
|
162
|
-
raise ValidationError.new(
|
163
|
-
|
285
|
+
raise ValidationError.new(group_target_conflict(t_name), @name) if used_group_names.include?(t_name)
|
286
|
+
if used_aliases.include?(t_name)
|
287
|
+
raise ValidationError.new(alias_target_conflict(t_name), @name)
|
288
|
+
end
|
164
289
|
|
165
|
-
|
290
|
+
used_target_names << t_name
|
166
291
|
end
|
167
292
|
|
168
293
|
@aliases.each do |n, target|
|
169
|
-
raise ValidationError.new(group_alias_conflict(n), @name) if
|
170
|
-
|
294
|
+
raise ValidationError.new(group_alias_conflict(n), @name) if used_group_names.include?(n)
|
295
|
+
if used_target_names.include?(n)
|
296
|
+
raise ValidationError.new(alias_target_conflict(n), @name)
|
297
|
+
end
|
171
298
|
|
172
|
-
if
|
173
|
-
raise ValidationError.new(alias_conflict(n, target,
|
299
|
+
if used_aliases.include?(n)
|
300
|
+
raise ValidationError.new(alias_conflict(n, target, used_aliases[n]), @name)
|
174
301
|
end
|
175
302
|
|
176
|
-
|
303
|
+
used_aliases[n] = target
|
177
304
|
end
|
178
305
|
|
179
306
|
@groups.each do |g|
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
raise e
|
185
|
-
end
|
307
|
+
g.validate(used_group_names, used_target_names, used_aliases)
|
308
|
+
rescue ValidationError => e
|
309
|
+
e.add_parent(@name)
|
310
|
+
raise e
|
186
311
|
end
|
187
312
|
|
188
313
|
nil
|
189
314
|
end
|
190
315
|
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
316
|
+
def resolve_data_keys(data, target = nil)
|
317
|
+
result = {
|
318
|
+
'config' => @plugins.resolve_references(data.fetch('config', {})),
|
319
|
+
'vars' => @plugins.resolve_references(data.fetch('vars', {})),
|
320
|
+
'facts' => @plugins.resolve_references(data.fetch('facts', {})),
|
321
|
+
'features' => @plugins.resolve_references(data.fetch('features', [])),
|
322
|
+
'plugin_hooks' => @plugins.resolve_references(data.fetch('plugin_hooks', {}))
|
323
|
+
}
|
324
|
+
validate_data_keys(result, target)
|
325
|
+
result['features'] = Set.new(result['features'].flatten)
|
326
|
+
result
|
195
327
|
end
|
196
328
|
|
197
|
-
def
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
329
|
+
def validate_data_keys(data, target = nil)
|
330
|
+
{
|
331
|
+
'config' => Hash,
|
332
|
+
'vars' => Hash,
|
333
|
+
'facts' => Hash,
|
334
|
+
'features' => Array,
|
335
|
+
'plugin_hooks' => Hash
|
336
|
+
}.each do |key, expected_type|
|
337
|
+
next if !data.key?(key) || data[key].is_a?(expected_type) || @plugins.reference?(data[key])
|
338
|
+
|
339
|
+
msg = +"Expected #{key} to be of type #{expected_type}, not #{data[key].class}"
|
340
|
+
msg << " for target #{target}" if target
|
341
|
+
raise ValidationError.new(msg, @name)
|
342
|
+
end
|
343
|
+
unless @plugins.reference?(data['config'])
|
344
|
+
unexpected_keys = data.fetch('config', {}).keys - CONFIG_KEYS
|
345
|
+
if unexpected_keys.any?
|
346
|
+
msg = +"Found unexpected key(s) #{unexpected_keys.join(', ')} in config for"
|
347
|
+
msg << " target #{target} in" if target
|
348
|
+
msg << " group #{@name}"
|
349
|
+
@logger.warn(msg)
|
350
|
+
end
|
206
351
|
end
|
207
352
|
end
|
208
353
|
|
209
354
|
def group_data
|
210
|
-
|
211
|
-
'vars' => @vars,
|
212
|
-
'facts' => @facts,
|
213
|
-
'features' => @features,
|
214
|
-
'plugin_hooks' => @plugin_hooks,
|
215
|
-
'groups' => [@name] }
|
355
|
+
@group_data ||= resolve_data_keys(@input).merge('groups' => [@name])
|
216
356
|
end
|
217
357
|
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
'facts' => {},
|
222
|
-
'features' => [],
|
223
|
-
'plugin_hooks' => {},
|
224
|
-
'groups' => [] }
|
225
|
-
end
|
226
|
-
|
227
|
-
def data_merge(data1, data2)
|
228
|
-
if data2.nil? || data1.nil?
|
229
|
-
return data2 || data1
|
230
|
-
end
|
231
|
-
|
232
|
-
{
|
233
|
-
'config' => Bolt::Util.deep_merge(data1['config'], data2['config']),
|
234
|
-
# Shallow merge instead of deep merge so that vars with a hash value
|
235
|
-
# are assigned a new hash, rather than merging the existing value
|
236
|
-
# with the value meant to replace it
|
237
|
-
'vars' => data1['vars'].merge(data2['vars']),
|
238
|
-
'facts' => Bolt::Util.deep_merge(data1['facts'], data2['facts']),
|
239
|
-
'features' => data1['features'] | data2['features'],
|
240
|
-
'plugin_hooks' => data1['plugin_hooks'].merge(data2['plugin_hooks']),
|
241
|
-
'groups' => data2['groups'] + data1['groups']
|
242
|
-
}
|
358
|
+
# Returns targets contained directly within the group, ignoring subgroups
|
359
|
+
def local_targets
|
360
|
+
Set.new(@unresolved_targets.keys) + Set.new(@resolved_targets.keys)
|
243
361
|
end
|
244
362
|
|
245
|
-
# Returns all
|
246
|
-
def
|
247
|
-
@groups.inject(
|
248
|
-
acc.merge(g.
|
363
|
+
# Returns all targets contained within the group, which includes targets from subgroups.
|
364
|
+
def all_targets
|
365
|
+
@groups.inject(local_targets) do |acc, g|
|
366
|
+
acc.merge(g.all_targets)
|
249
367
|
end
|
250
368
|
end
|
251
369
|
|
252
|
-
# Returns a mapping of aliases to
|
253
|
-
def
|
370
|
+
# Returns a mapping of aliases to targets contained within the group, which includes subgroups.
|
371
|
+
def target_aliases
|
254
372
|
@groups.inject(@aliases) do |acc, g|
|
255
|
-
acc.merge(g.
|
373
|
+
acc.merge(g.target_aliases)
|
256
374
|
end
|
257
375
|
end
|
258
376
|
|
@@ -263,35 +381,28 @@ module Bolt
|
|
263
381
|
end
|
264
382
|
end
|
265
383
|
|
266
|
-
def
|
267
|
-
|
268
|
-
|
269
|
-
|
270
|
-
|
271
|
-
def node_collect(node_name)
|
272
|
-
data = @groups.inject(nil) do |acc, g|
|
273
|
-
if (d = g.node_collect(node_name))
|
274
|
-
data_merge(d, acc)
|
275
|
-
else
|
276
|
-
acc
|
277
|
-
end
|
384
|
+
def target_collect(target_name)
|
385
|
+
child_data = @groups.map { |group| group.target_collect(target_name) }
|
386
|
+
# Data from earlier groups wins
|
387
|
+
child_result = child_data.inject do |acc, group_data|
|
388
|
+
data_merge(group_data, acc)
|
278
389
|
end
|
279
|
-
|
390
|
+
# Children override the parent
|
391
|
+
data_merge(target_data(target_name), child_result)
|
280
392
|
end
|
281
393
|
|
282
|
-
def group_collect(
|
283
|
-
|
284
|
-
|
285
|
-
|
286
|
-
|
287
|
-
acc
|
288
|
-
end
|
394
|
+
def group_collect(target_name)
|
395
|
+
child_data = @groups.map { |group| group.group_collect(target_name) }
|
396
|
+
# Data from earlier groups wins
|
397
|
+
child_result = child_data.inject do |acc, group_data|
|
398
|
+
data_merge(group_data, acc)
|
289
399
|
end
|
290
400
|
|
291
|
-
|
292
|
-
|
293
|
-
|
294
|
-
|
401
|
+
# If this group has the target or one of the child groups has the
|
402
|
+
# target, return the data, otherwise return nil
|
403
|
+
if child_result || local_targets.include?(target_name)
|
404
|
+
# Children override the parent
|
405
|
+
data_merge(group_data, child_result)
|
295
406
|
end
|
296
407
|
end
|
297
408
|
end
|