bolt 1.34.0 → 1.35.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 37db36042df92f069108154b4a08d21095a1c84f2403b454a9a2ebf74c73fef2
4
- data.tar.gz: ede33892ae497dd185eba5c45d4f2ca7fafb0c5c607aa6c8d50f1fb9b70d267a
3
+ metadata.gz: 795f28f4c6cb07e73c21931930b94859b00965416a800746a6e974eade2e637e
4
+ data.tar.gz: 3d3becef8defffea7e2db0218bac41d9a8215d84ec9617da941378fb7d42809c
5
5
  SHA512:
6
- metadata.gz: a5e2af615b54b7f116c47b3b78c16776882e086f8a41cf3520336ff6e3cd0f670a3ff9e36083d2cecdc60ab9d2df14be424e57cac3a275113e1f4dddb73b2c54
7
- data.tar.gz: 90cc1192bba5fc7e8b7b9314c05b4dc37c867fd55f2d69e09a33d8f4c9330207da8f8a646c279f956a72def157ff875991605778923c93df009351b6b9afd03b
6
+ metadata.gz: 80cd0fe982cf6097316e34703627e4da6aaadb1bd58070c154d96b550d8d7c25528842128771f7600a4cb68fdf7f400987dd687d1a26f3cabea32695ec4b24ec
7
+ data.tar.gz: 8e2ec2983b1d6d78d17170f7090a4628be18f6e970332ee627c0f0c474d1a2dd7a6ac8575efec374cd77afc65ce3399b5c51b82791e3ed98174a697c718ef93b
@@ -33,7 +33,7 @@ Puppet::Functions.create_function(:apply_prep) do
33
33
 
34
34
  def get_task(name, params = {})
35
35
  tasksig = script_compiler.task_signature(name)
36
- raise Bolt::Error.new("#{name} could not be found", 'bolt/apply-prep') unless tasksig
36
+ raise Bolt::Error.new("Task '#{name}' could not be found", 'bolt/apply-prep') unless tasksig
37
37
 
38
38
  errors = []
39
39
  unless tasksig.runnable_with?(params) { |msg| errors << msg }
@@ -2,17 +2,30 @@
2
2
 
3
3
  # Repeat the block until it returns a truthy value. Returns the value.
4
4
  Puppet::Functions.create_function(:'ctrl::do_until') do
5
+ # @param options Additional options: 'until'
5
6
  # @example Run a task until it succeeds
6
7
  # ctrl::do_until() || {
7
- # run_task('test', $target, _catch_errors => true).ok?
8
+ # run_task('test', $target, _catch_errors => true).ok()
8
9
  # }
10
+ #
11
+ # @example Run a task until it succeeds or fails 10 times
12
+ # ctrl::do_until('limit' => 10) || {
13
+ # run_task('test', $target, _catch_errors => true).ok()
14
+ # }
15
+ #
9
16
  dispatch :do_until do
17
+ optional_param 'Hash[String[1], Any]', :options
10
18
  block_param
11
19
  end
12
20
 
13
- def do_until
21
+ def do_until(options = { 'limit' => 0 })
14
22
  Puppet.lookup(:bolt_executor) {}&.report_function_call(self.class.name)
15
- until (x = yield); end
23
+ limit = options['limit']
24
+ i = 0
25
+ until (x = yield)
26
+ i += 1
27
+ break if limit != 0 && i >= limit
28
+ end
16
29
  x
17
30
  end
18
31
  end
@@ -71,7 +71,7 @@ module Bolt
71
71
  @save_rerun = true
72
72
  @puppetfile_config = {}
73
73
  @plugins = {}
74
- @plugin_hooks = { 'puppet_library' => { 'plugin' => 'install_agent' } }
74
+ @plugin_hooks = { 'puppet_library' => { 'plugin' => 'puppet_agent', 'stop_service' => true } }
75
75
 
76
76
  # add an entry for the default console logger
77
77
  @log = { 'console' => {} }
@@ -269,7 +269,7 @@ module Bolt
269
269
  private :resolve_name
270
270
 
271
271
  def create_target(data)
272
- Target.new(data)
272
+ Bolt::Target.new(data)
273
273
  end
274
274
  private :create_target
275
275
 
@@ -153,7 +153,7 @@ module Bolt
153
153
  @nodes.each_key do |n|
154
154
  # Require nodes to be parseable as a Target.
155
155
  begin
156
- Target.new(n)
156
+ Bolt::Target.new(n)
157
157
  rescue Bolt::ParseError => e
158
158
  @logger.debug(e)
159
159
  raise ValidationError.new("Invalid node name #{n}", @name)
@@ -2,153 +2,164 @@
2
2
 
3
3
  require 'bolt/inventory/group'
4
4
  require 'bolt/inventory/inventory2'
5
+ require 'bolt/inventory/target'
5
6
 
6
7
  module Bolt
7
8
  class Inventory
8
9
  class Group2
9
- attr_accessor :name, :targets, :aliases, :name_or_alias, :groups,
10
- :config, :facts, :vars, :features, :plugin_hooks
10
+ attr_accessor :name, :groups
11
11
 
12
12
  # THESE are duplicates with the old groups for now.
13
13
  # Regex used to validate group names and target aliases.
14
14
  NAME_REGEX = /\A[a-z0-9_][a-z0-9_-]*\Z/.freeze
15
15
 
16
- DATA_KEYS = %w[name config facts vars features plugin_hooks].freeze
17
- TARGET_KEYS = DATA_KEYS + %w[alias uri]
18
- GROUP_KEYS = DATA_KEYS + %w[groups targets]
16
+ DATA_KEYS = %w[config facts vars features plugin_hooks].freeze
17
+ TARGET_KEYS = DATA_KEYS + %w[name alias uri]
18
+ GROUP_KEYS = DATA_KEYS + %w[name groups targets]
19
19
  CONFIG_KEYS = Bolt::TRANSPORTS.keys.map(&:to_s) + ['transport']
20
20
 
21
- def initialize(data, plugins)
21
+ def initialize(input, plugins)
22
22
  @logger = Logging.logger[self]
23
- raise ValidationError.new("Expected group to be a Hash, not #{data.class}", nil) unless data.is_a?(Hash)
24
- raise ValidationError.new("Cannot set group with plugin", nil) if data.key?('_plugin')
25
- raise ValidationError.new("Group does not have a name", nil) unless data.key?('name')
26
23
  @plugins = plugins
27
24
 
28
- %w[name vars features facts plugin_hooks].each do |key|
29
- validate_config_plugin(data[key], key, nil)
30
- end
25
+ input = resolve_top_level_references(input) if reference?(input)
26
+
27
+ raise ValidationError.new("Group does not have a name", nil) unless input.key?('name')
28
+
29
+ @name = resolve_references(input['name'])
31
30
 
32
- @name = data['name']
33
31
  raise ValidationError.new("Group name must be a String, not #{@name.inspect}", nil) unless @name.is_a?(String)
34
32
  raise ValidationError.new("Invalid group name #{@name}", @name) unless @name =~ NAME_REGEX
35
33
 
36
- # DEPRECATION : remove this before finalization
37
- if data.key?('target-lookups')
38
- msg = "'target-lookups' are no longer a separate key. Merge 'target-lookups' and 'targets' lists and replace 'plugin' with '_plugin'" # rubocop:disable Metrics/LineLength
39
- raise ValidationError.new(msg, @name)
40
- end
34
+ validate_group_input(input)
41
35
 
42
- unless (unexpected_keys = data.keys - GROUP_KEYS).empty?
43
- msg = "Found unexpected key(s) #{unexpected_keys.join(', ')} in group #{@name}"
44
- @logger.warn(msg)
45
- end
36
+ @input = input
46
37
 
47
- @vars = fetch_value(data, 'vars', Hash)
48
- @facts = fetch_value(data, 'facts', Hash)
49
- @features = fetch_value(data, 'features', Array)
50
- @plugin_hooks = fetch_value(data, 'plugin_hooks', Hash)
38
+ validate_data_keys(@input)
51
39
 
52
- @config = config_only_plugin(fetch_value(data, 'config', Hash))
40
+ targets = resolve_top_level_references(input.fetch('targets', []))
53
41
 
54
- unless (unexpected_keys = @config.keys - CONFIG_KEYS).empty?
55
- msg = "Found unexpected key(s) #{unexpected_keys.join(', ')} in config for group #{@name}"
56
- @logger.warn(msg)
57
- end
58
-
59
- targets = fetch_value(data, 'targets', Array)
60
- groups = fetch_value(data, 'groups', Array)
61
-
62
- @targets = {}
42
+ @unresolved_targets = {}
43
+ @resolved_targets = {}
44
+ @targets = Set.new
63
45
  # @target_objects = {}
64
46
  @aliases = {}
65
- @name_or_alias = []
47
+ @string_targets = []
66
48
 
67
- targets.each do |target|
68
- # If target is a string, it can refer to either a target name or
69
- # alias. Which can't be determined until all groups have been
70
- # resolved, and requires a depth-first traversal to categorize them.
49
+ Array(targets).each do |target|
50
+ # If target is a string, it can either be trivially defining a target
51
+ # or it could be a name/alias of a target defined in another group.
52
+ # We can't tell the difference until all groups have been resolved,
53
+ # so we store the string on its own here and process it later.
71
54
  if target.is_a?(String)
72
- @name_or_alias << target
55
+ @string_targets << target
73
56
  # Handle plugins at this level so that lookups cannot trigger recursive lookups
74
57
  elsif target.is_a?(Hash)
75
- if target.include?('_plugin')
76
- lookup_targets(target)
77
- else
78
- add_target(target)
79
- end
58
+ add_target_definition(target)
80
59
  else
81
60
  raise ValidationError.new("Node entry must be a String or Hash, not #{target.class}", @name)
82
61
  end
83
62
  end
84
63
 
85
- @groups = groups.map { |g| Group2.new(g, plugins) }
64
+ groups = input.fetch('groups', [])
65
+ # 'groups' can be a _plugin reference, in which case we want to resolve
66
+ # it. That can itself return a reference, so we want to keep resolving
67
+ # them until we have a value. We don't just use resolve_references
68
+ # though, since that will resolve any nested references and we want to
69
+ # leave it to the group to do that lazily.
70
+ groups = resolve_top_level_references(groups)
71
+
72
+ @groups = Array(groups).map { |g| Group2.new(g, plugins) }
86
73
  end
87
74
 
88
- def validate_config_plugin(data, key, group_name = nil)
89
- if data.is_a?(Hash) && data.include?('_plugin')
90
- if group_name
91
- raise ValidationError.new("Cannot set target #{key.inspect} with plugin", group_name)
92
- else
93
- raise ValidationError.new("Cannot set group #{key.inspect} with plugin", nil)
94
- end
75
+ # Evaluate all _plugin references in a data structure. Leaves are
76
+ # evaluated and then their parents are evaluated with references replaced
77
+ # by their values. If the result of a reference contains more references,
78
+ # they are resolved again before continuing to ascend the tree. The final
79
+ # result will not contain any references.
80
+ def resolve_references(data)
81
+ Bolt::Util.postwalk_vals(data) do |value|
82
+ reference?(value) ? resolve_references(resolve_single_reference(value)) : value
95
83
  end
96
-
97
- Bolt::Util.walk_vals(data, true) do |v|
98
- validate_config_plugin(v, key, group_name)
84
+ end
85
+ private :resolve_references
86
+
87
+ # Iteratively resolves "top-level" references until the result no longer
88
+ # has top-level references. A top-level reference is one which is not
89
+ # contained within another hash. It may be either the actual top-level
90
+ # result or arbitrarily nested within arrays. If parameters of the
91
+ # reference are themselves references, they will be looked. Any remaining
92
+ # references nested inside the result will *not* be evaluated once the
93
+ # top-level result is not a reference. This is used to resolve the
94
+ # `targets` and `groups` keys which are allowed to be references or
95
+ # arrays of references, but which may return data with nested references
96
+ # that should be resolved lazily. The end result will either be a single
97
+ # hash or a flat array of hashes.
98
+ def resolve_top_level_references(data)
99
+ if data.is_a?(Array)
100
+ data.flat_map { |elem| resolve_top_level_references(elem) }
101
+ elsif reference?(data)
102
+ partially_resolved = data.map do |k, v|
103
+ [k, resolve_references(v)]
104
+ end.to_h
105
+ fully_resolved = resolve_single_reference(partially_resolved)
106
+ # The top-level reference may have returned more references, so repeat the process
107
+ resolve_top_level_references(fully_resolved)
108
+ else
109
+ data
99
110
  end
100
111
  end
101
- private :validate_config_plugin
102
-
103
- def config_only_plugin(data)
104
- Bolt::Util.walk_vals(data) do |value|
105
- if value.is_a?(Hash) && value.include?('_plugin')
106
- plugin_name = value['_plugin']
107
- begin
108
- hook = @plugins.get_hook(plugin_name, :resolve_reference)
109
- rescue Bolt::Plugin::PluginError => e
110
- raise ValidationError.new(e.message, @name)
111
- end
112
+ private :resolve_top_level_references
112
113
 
113
- begin
114
- validate_proc = @plugins.get_hook(plugin_name, :validate_resolve_reference)
115
- rescue Bolt::Plugin::PluginError
116
- validate_proc = proc { |*args| }
117
- end
114
+ # Evaluates a single reference. The value returned may be another
115
+ # reference.
116
+ def resolve_single_reference(reference)
117
+ plugin_name = reference['_plugin']
118
+ begin
119
+ hook = @plugins.get_hook(plugin_name, :resolve_reference)
120
+ rescue Bolt::Plugin::PluginError => e
121
+ raise ValidationError.new(e.message, @name)
122
+ end
123
+
124
+ begin
125
+ validate_proc = @plugins.get_hook(plugin_name, :validate_resolve_reference)
126
+ rescue Bolt::Plugin::PluginError
127
+ validate_proc = proc { |*args| }
128
+ end
118
129
 
119
- validate_proc.call(value)
130
+ validate_proc.call(reference)
120
131
 
121
- Concurrent::Delay.new do
122
- begin
123
- hook.call(value)
124
- rescue StandardError => e
125
- loc = "resolve_reference in #{@name}"
126
- raise Bolt::Plugin::PluginError::ExecutionError.new(e.message, plugin_name, loc)
127
- end
128
- end
129
- else
130
- value
131
- end
132
+ begin
133
+ # Evaluate the plugin and then recursively evaluate any plugin returned by it.
134
+ hook.call(reference)
135
+ rescue StandardError => e
136
+ loc = "resolve_reference in #{@name}"
137
+ raise Bolt::Plugin::PluginError::ExecutionError.new(e.message, plugin_name, loc)
132
138
  end
133
139
  end
134
- private :config_only_plugin
140
+ private :resolve_single_reference
141
+
142
+ # Checks whether a given value is a _plugin reference
143
+ def reference?(input)
144
+ input.is_a?(Hash) && input.key?('_plugin')
145
+ end
135
146
 
136
147
  def target_data(target_name)
137
- if (data = @targets[target_name])
138
- { 'config' => data['config'] || {},
139
- 'vars' => data['vars'] || {},
140
- 'facts' => data['facts'] || {},
141
- 'features' => data['features'] || [],
142
- 'plugin_hooks' => data['plugin_hooks'] || {},
143
- # This allows us to determine if a target was found?
144
- 'name' => data['name'] || nil,
145
- 'uri' => data['uri'] || nil,
148
+ if @unresolved_targets.key?(target_name)
149
+ target = @unresolved_targets.delete(target_name)
150
+ resolved_data = resolve_data_keys(target, target_name).merge(
151
+ 'name' => target['name'],
152
+ 'uri' => target['uri'],
146
153
  # groups come from group_data
147
- 'groups' => [] }
154
+ 'groups' => []
155
+ )
156
+ @resolved_targets[target_name] = resolved_data
157
+ else
158
+ @resolved_targets[target_name]
148
159
  end
149
160
  end
150
161
 
151
- def add_target(target)
162
+ def add_target_definition(target)
152
163
  # This check ensures target lookup plugins do not returns bare strings.
153
164
  # Remove it if we decide to allows task plugins to return string node
154
165
  # names.
@@ -156,12 +167,9 @@ module Bolt
156
167
  raise ValidationError.new("Node entry must be a Hash, not #{target.class}", @name)
157
168
  end
158
169
 
159
- # This check prevents plugins from returning plugins
160
- raise ValidationError.new("Cannot set target with plugin", @name) if target.key?('_plugin')
161
- target.each do |k, v|
162
- next if k == 'config'
163
- validate_config_plugin(v, k, @name)
164
- end
170
+ target['name'] = resolve_references(target['name']) if target.key?('name')
171
+ target['uri'] = resolve_references(target['uri']) if target.key?('uri')
172
+ target['alias'] = resolve_references(target['alias']) if target.key?('alias')
165
173
 
166
174
  t_name = target['name'] || target['uri']
167
175
 
@@ -173,7 +181,7 @@ module Bolt
173
181
  raise ValidationError.new("Target name must be ASCII characters: #{target}", @name)
174
182
  end
175
183
 
176
- if @targets.include?(t_name)
184
+ if local_targets.include?(t_name)
177
185
  @logger.warn("Ignoring duplicate target in #{@name}: #{target}")
178
186
  return
179
187
  end
@@ -183,17 +191,7 @@ module Bolt
183
191
  @logger.warn(msg)
184
192
  end
185
193
 
186
- unless target['config'].nil? || target['config'].is_a?(Hash)
187
- raise ValidationError.new("Invalid configuration for target: #{t_name}", @name)
188
- end
189
-
190
- config_keys = target['config']&.keys || []
191
- unless (unexpected_keys = config_keys - CONFIG_KEYS).empty?
192
- msg = "Found unexpected key(s) #{unexpected_keys.join(', ')} in config for target #{t_name}"
193
- @logger.warn(msg)
194
- end
195
-
196
- target['config'] = config_only_plugin(target['config'])
194
+ validate_data_keys(target, t_name)
197
195
 
198
196
  if target.include?('alias')
199
197
  aliases = target['alias']
@@ -213,24 +211,11 @@ module Bolt
213
211
  end
214
212
  end
215
213
 
216
- @targets[t_name] = target
214
+ @unresolved_targets[t_name] = target
217
215
  end
218
216
 
219
- def lookup_targets(lookup)
220
- begin
221
- hook = @plugins.get_hook(lookup['_plugin'], :resolve_reference)
222
- rescue Bolt::Plugin::PluginError => e
223
- raise ValidationError.new(e.message, @name)
224
- end
225
-
226
- begin
227
- targets = hook.call(lookup)
228
- rescue StandardError => e
229
- loc = "resolve_reference in #{@name}"
230
- raise Bolt::Plugin::PluginError::ExecutionError.new(e.message, lookup['_plugin'], loc)
231
- end
232
-
233
- targets.each { |target| add_target(target) }
217
+ def add_target(target)
218
+ @resolved_targets[target.name] = { 'name' => target.name }
234
219
  end
235
220
 
236
221
  def data_merge(data1, data2)
@@ -253,37 +238,30 @@ module Bolt
253
238
  }
254
239
  end
255
240
 
256
- private def fetch_value(data, key, type)
257
- value = data.fetch(key, type.new)
258
- unless value.is_a?(type)
259
- raise ValidationError.new("Expected #{key} to be of type #{type}, not #{value.class}", @name)
260
- end
261
- value
262
- end
263
-
264
- def resolve_aliases(aliases, target_names)
265
- @name_or_alias.each do |name_or_alias|
266
- # If an alias is found, insert the name into this group. Otherwise use the name as a new target's uri.
267
- if target_names.include?(name_or_alias)
268
- @targets[name_or_alias] = { 'name' => name_or_alias }
269
- elsif (target_name = aliases[name_or_alias])
270
- if @targets.include?(target_name)
271
- @logger.warn("Ignoring duplicate target in #{@name}: #{target_name}")
241
+ def resolve_string_targets(aliases, known_targets)
242
+ @string_targets.each do |string_target|
243
+ # If this is the name of a target defined elsewhere, then insert the
244
+ # target into this group as just a name. Otherwise, add a new target
245
+ # with the string as the URI.
246
+ if known_targets.include?(string_target)
247
+ @unresolved_targets[string_target] = { 'name' => string_target }
248
+ # If this is an alias for an existing target, then add it to this group
249
+ elsif (canonical_name = aliases[string_target])
250
+ if local_targets.include?(canonical_name)
251
+ @logger.warn("Ignoring duplicate target in #{@name}: #{canonical_name}")
272
252
  else
273
- @targets[target_name] = { 'name' => target_name }
253
+ @unresolved_targets[canonical_name] = { 'name' => canonical_name }
274
254
  end
255
+ # If it's not the name or alias of an existing target, then make a
256
+ # new target using the string as the URI
257
+ elsif local_targets.include?(string_target)
258
+ @logger.warn("Ignoring duplicate target in #{@name}: #{string_target}")
275
259
  else
276
- target_name = name_or_alias
277
-
278
- if @targets.include?(target_name)
279
- @logger.warn("Ignoring duplicate target in #{@name}: #{target_name}")
280
- else
281
- @targets[target_name] = { 'uri' => target_name }
282
- end
260
+ @unresolved_targets[string_target] = { 'uri' => string_target }
283
261
  end
284
262
  end
285
263
 
286
- @groups.each { |g| g.resolve_aliases(aliases, target_names) }
264
+ @groups.each { |g| g.resolve_string_targets(aliases, known_targets) }
287
265
  end
288
266
 
289
267
  private def alias_conflict(name, target1, target2)
@@ -302,50 +280,73 @@ module Bolt
302
280
  "Node name #{name} conflicts with alias of the same name"
303
281
  end
304
282
 
305
- def validate(used_names = Set.new, target_names = Set.new, aliased = {}, depth = 0)
283
+ def validate_group_input(input)
284
+ raise ValidationError.new("Expected group to be a Hash, not #{input.class}", nil) unless input.is_a?(Hash)
285
+
286
+ # DEPRECATION : remove this before finalization
287
+ if input.key?('target-lookups')
288
+ msg = "'target-lookups' are no longer a separate key. Merge 'target-lookups' and 'targets' lists and replace 'plugin' with '_plugin'" # rubocop:disable Metrics/LineLength
289
+ raise ValidationError.new(msg, @name)
290
+ end
291
+
292
+ unless (unexpected_keys = input.keys - GROUP_KEYS).empty?
293
+ msg = "Found unexpected key(s) #{unexpected_keys.join(', ')} in group #{@name}"
294
+ @logger.warn(msg)
295
+ end
296
+
297
+ Bolt::Util.walk_keys(input) do |key|
298
+ if reference?(key)
299
+ raise ValidationError.new("Group keys cannot be specified as _plugin references", @name)
300
+ else
301
+ key
302
+ end
303
+ end
304
+ end
305
+
306
+ def validate(used_group_names = Set.new, used_target_names = Set.new, used_aliases = {})
306
307
  # Test if this group name conflicts with anything used before.
307
- raise ValidationError.new("Tried to redefine group #{@name}", @name) if used_names.include?(@name)
308
- raise ValidationError.new(group_target_conflict(@name), @name) if target_names.include?(@name)
309
- raise ValidationError.new(group_alias_conflict(@name), @name) if aliased.include?(@name)
308
+ raise ValidationError.new("Tried to redefine group #{@name}", @name) if used_group_names.include?(@name)
309
+ raise ValidationError.new(group_target_conflict(@name), @name) if used_target_names.include?(@name)
310
+ raise ValidationError.new(group_alias_conflict(@name), @name) if used_aliases.include?(@name)
310
311
 
311
- used_names << @name
312
+ used_group_names << @name
312
313
 
313
314
  # Collect target names and aliases into a list used to validate that subgroups don't conflict.
314
315
  # Used names validate that previously used group names don't conflict with new target names/aliases.
315
- @targets.each do |t_name, t_data|
316
+ @unresolved_targets.merge(@resolved_targets).each do |t_name, t_data|
316
317
  # Require targets to be parseable as a Target.
317
318
  begin
318
319
  # Catch malformed URI here
319
- Bolt::Inventory::Inventory2.parse_uri(t_data['uri'])
320
+ Bolt::Inventory::Target.parse_uri(t_data['uri'])
320
321
  rescue Bolt::ParseError => e
321
322
  @logger.debug(e)
322
323
  raise ValidationError.new("Invalid target uri #{t_data['uri']}", @name)
323
324
  end
324
325
 
325
- raise ValidationError.new(group_target_conflict(t_name), @name) if used_names.include?(t_name)
326
- if aliased.include?(t_name)
326
+ raise ValidationError.new(group_target_conflict(t_name), @name) if used_group_names.include?(t_name)
327
+ if used_aliases.include?(t_name)
327
328
  raise ValidationError.new(alias_target_conflict(t_name), @name)
328
329
  end
329
330
 
330
- target_names << t_name
331
+ used_target_names << t_name
331
332
  end
332
333
 
333
334
  @aliases.each do |n, target|
334
- raise ValidationError.new(group_alias_conflict(n), @name) if used_names.include?(n)
335
- if target_names.include?(n)
335
+ raise ValidationError.new(group_alias_conflict(n), @name) if used_group_names.include?(n)
336
+ if used_target_names.include?(n)
336
337
  raise ValidationError.new(alias_target_conflict(n), @name)
337
338
  end
338
339
 
339
- if aliased.include?(n)
340
- raise ValidationError.new(alias_conflict(n, target, aliased[n]), @name)
340
+ if used_aliases.include?(n)
341
+ raise ValidationError.new(alias_conflict(n, target, used_aliases[n]), @name)
341
342
  end
342
343
 
343
- aliased[n] = target
344
+ used_aliases[n] = target
344
345
  end
345
346
 
346
347
  @groups.each do |g|
347
348
  begin
348
- g.validate(used_names, target_names, aliased, depth + 1)
349
+ g.validate(used_group_names, used_target_names, used_aliases)
349
350
  rescue ValidationError => e
350
351
  e.add_parent(@name)
351
352
  raise e
@@ -355,35 +356,57 @@ module Bolt
355
356
  nil
356
357
  end
357
358
 
358
- # The data functions below expect and return nil or a hash of the schema
359
- # {'config' => Hash, 'vars' => Hash, 'facts' => Hash, 'features' => Array,
360
- # 'plugin_hooks' => Hash, 'groups' => Array}
361
- def data_for(target_name)
362
- data_merge(group_collect(target_name), target_collect(target_name))
359
+ def resolve_data_keys(data, target = nil)
360
+ result = {
361
+ 'config' => resolve_references(data.fetch('config', {})),
362
+ 'vars' => resolve_references(data.fetch('vars', {})),
363
+ 'facts' => resolve_references(data.fetch('facts', {})),
364
+ 'features' => resolve_references(data.fetch('features', [])),
365
+ 'plugin_hooks' => resolve_references(data.fetch('plugin_hooks', {}))
366
+ }
367
+ validate_data_keys(result, target)
368
+ result['features'] = Set.new(result['features'].flatten)
369
+ result
370
+ end
371
+
372
+ def validate_data_keys(data, target = nil)
373
+ {
374
+ 'config' => Hash,
375
+ 'vars' => Hash,
376
+ 'facts' => Hash,
377
+ 'features' => Array,
378
+ 'plugin_hooks' => Hash
379
+ }.each do |key, expected_type|
380
+ next if !data.key?(key) || data[key].is_a?(expected_type) || reference?(data[key])
381
+
382
+ msg = +"Expected #{key} to be of type #{expected_type}, not #{data[key].class}"
383
+ msg << " for target #{target}" if target
384
+ raise ValidationError.new(msg, @name)
385
+ end
386
+ unless reference?(data['config'])
387
+ unexpected_keys = data.fetch('config', {}).keys - CONFIG_KEYS
388
+ if unexpected_keys.any?
389
+ msg = +"Found unexpected key(s) #{unexpected_keys.join(', ')} in config for"
390
+ msg << " target #{target} in" if target
391
+ msg << " group #{@name}"
392
+ @logger.warn(msg)
393
+ end
394
+ end
363
395
  end
364
396
 
365
397
  def group_data
366
- { 'config' => @config,
367
- 'vars' => @vars,
368
- 'facts' => @facts,
369
- 'features' => @features,
370
- 'plugin_hooks' => @plugin_hooks,
371
- 'groups' => [@name] }
398
+ @group_data ||= resolve_data_keys(@input).merge('groups' => [@name])
372
399
  end
373
400
 
374
- def empty_data
375
- { 'config' => {},
376
- 'vars' => {},
377
- 'facts' => {},
378
- 'features' => [],
379
- 'plugin_hooks' => {},
380
- 'groups' => [] }
401
+ # Returns targets contained directly within the group, ignoring subgroups
402
+ def local_targets
403
+ Set.new(@unresolved_targets.keys) + Set.new(@resolved_targets.keys)
381
404
  end
382
405
 
383
406
  # Returns all targets contained within the group, which includes targets from subgroups.
384
- def target_names
385
- @groups.inject(local_target_names) do |acc, g|
386
- acc.merge(g.target_names)
407
+ def all_targets
408
+ @groups.inject(local_targets) do |acc, g|
409
+ acc.merge(g.all_targets)
387
410
  end
388
411
  end
389
412
 
@@ -401,35 +424,28 @@ module Bolt
401
424
  end
402
425
  end
403
426
 
404
- def local_target_names
405
- Set.new(@targets.keys)
406
- end
407
- private :local_target_names
408
-
409
427
  def target_collect(target_name)
410
- data = @groups.inject(nil) do |acc, g|
411
- if (d = g.target_collect(target_name))
412
- data_merge(d, acc)
413
- else
414
- acc
415
- end
428
+ child_data = @groups.map { |group| group.target_collect(target_name) }
429
+ # Data from earlier groups wins
430
+ child_result = child_data.inject do |acc, group_data|
431
+ data_merge(group_data, acc)
416
432
  end
417
- data_merge(target_data(target_name), data)
433
+ # Children override the parent
434
+ data_merge(target_data(target_name), child_result)
418
435
  end
419
436
 
420
437
  def group_collect(target_name)
421
- data = @groups.inject(nil) do |acc, g|
422
- if (d = g.data_for(target_name))
423
- data_merge(d, acc)
424
- else
425
- acc
426
- end
438
+ child_data = @groups.map { |group| group.group_collect(target_name) }
439
+ # Data from earlier groups wins
440
+ child_result = child_data.inject do |acc, group_data|
441
+ data_merge(group_data, acc)
427
442
  end
428
443
 
429
- if data
430
- data_merge(group_data, data)
431
- elsif @targets.include?(target_name)
432
- group_data
444
+ # If this group has the target or one of the child groups has the
445
+ # target, return the data, otherwise return nil
446
+ if child_result || local_targets.include?(target_name)
447
+ # Children override the parent
448
+ data_merge(group_data, child_result)
433
449
  end
434
450
  end
435
451
  end