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.

@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'bolt/inventory/group2'
4
+ require 'bolt/inventory/target'
4
5
 
5
6
  module Bolt
6
7
  class Inventory
@@ -23,7 +24,7 @@ module Bolt
23
24
  @group_lookup = {}
24
25
  # The targets hash is the canonical source for all targets in inventory
25
26
  @targets = {}
26
- @groups.resolve_aliases(@groups.target_aliases, @groups.target_names)
27
+ @groups.resolve_string_targets(@groups.target_aliases, @groups.all_targets)
27
28
  collect_groups
28
29
  end
29
30
 
@@ -49,34 +50,26 @@ module Bolt
49
50
  end
50
51
 
51
52
  def target_names
52
- @groups.target_names
53
+ @groups.all_targets
53
54
  end
54
55
  # alias for analytics
55
56
  alias node_names target_names
56
57
 
57
58
  def get_targets(targets)
58
- flat_target_list(targets).map { |t| update_target(t) }
59
+ target_array = expand_targets(targets)
60
+ if target_array.is_a? Array
61
+ target_array.flatten.uniq(&:name)
62
+ else
63
+ [target_array]
64
+ end
59
65
  end
60
66
 
61
67
  def get_target(target)
62
- target_array = flat_target_list(target)
68
+ target_array = get_targets(target)
63
69
  if target_array.count > 1
64
70
  raise ValidationError.new("'#{target}' refers to #{target_array.count} targets", nil)
65
71
  end
66
- get_targets(target_array.first).first
67
- end
68
-
69
- def add_to_group(targets, desired_group)
70
- if group_names.include?(desired_group)
71
- targets.each do |target|
72
- if group_names.include?(target.name)
73
- raise ValidationError.new("Group #{target.name} conflicts with target of the same name", target.name)
74
- end
75
- add_target(@groups, target, desired_group)
76
- end
77
- else
78
- raise ValidationError.new("Group #{desired_group} does not exist in inventory", nil)
79
- end
72
+ target_array.first
80
73
  end
81
74
 
82
75
  def data_hash
@@ -92,79 +85,9 @@ module Bolt
92
85
  end
93
86
 
94
87
  #### PRIVATE ####
95
- #
96
- # For debugging only now
97
- def groups_in(target_name)
98
- @groups.data_for(target_name)['groups'] || {}
99
- end
100
- private :groups_in
101
-
102
- # Look for _plugins
103
- def config_plugin(data)
104
- Bolt::Util.walk_vals(data) do |val|
105
- if val.is_a?(Concurrent::Delay)
106
- # We should raise any error from the delay now
107
- val.value!
108
- else
109
- val
110
- end
111
- end
112
- end
113
- private :config_plugin
114
-
115
- # Pass a target to get_targets for a public version of this
116
- def update_target(target)
117
- # Ensure all targets in inventory are included in the all group.
118
- unless @groups.target_names.include?(target.name)
119
- add_to_group([target], 'all')
120
- end
121
-
122
- # Get merged data between targets and groups
123
- data = @groups.data_for(target.name)
124
- data ||= {}
125
-
126
- unless data['config']
127
- @logger.debug("Did not find config for #{target.name} in inventory")
128
- data['config'] = {}
129
- end
130
-
131
- # Add defaults for special 'localhost' target (currently just config and features)
132
- if target.name == 'localhost'
133
- data = Bolt::Inventory.localhost_defaults(data)
134
- end
135
-
136
- # Data from inventory
137
- data['config'] = config_plugin(data['config'])
138
- # Data from set_config (make sure to resolve plugins)
139
- resolved_target_config = config_plugin(@targets[target.name]['config'] || {})
140
- data['config'] = Bolt::Util.deep_merge(data['config'], resolved_target_config)
141
-
142
- # Use Config object to ensure config section is treated consistently with config file
143
- conf = @config.deep_clone
144
- conf.update_from_inventory(data['config'])
145
- conf.validate
146
-
147
- # Recompute the target cached state with the merged data
148
- update_target_state(target, conf, data)
149
-
150
- unless target.transport.nil? || Bolt::TRANSPORTS.include?(target.transport.to_sym)
151
- raise Bolt::UnknownTransportError.new(target.transport, target.uri)
152
- end
153
-
154
- target
155
- end
156
- private :update_target
157
-
158
- # This algorithm for getting a flat list of targets is used several times.
159
- def flat_target_list(targets)
160
- target_array = expand_targets(targets)
161
- if target_array.is_a? Array
162
- target_array.flatten.uniq(&:name)
163
- else
164
- [target_array]
165
- end
88
+ def group_data_for(target_name)
89
+ @groups.group_collect(target_name)
166
90
  end
167
- private :flat_target_list
168
91
 
169
92
  # If target is a group name, expand it to the members of that group.
170
93
  # Else match against targets in inventory by name or alias.
@@ -172,13 +95,13 @@ module Bolt
172
95
  # Else fall back to [target] if no matches are found.
173
96
  def resolve_name(target)
174
97
  if (group = @group_lookup[target])
175
- group.target_names
98
+ group.all_targets
176
99
  else
177
100
  # Try to wildcard match targets in inventory
178
101
  # Ignore case because hostnames are generally case-insensitive
179
102
  regexp = Regexp.new("^#{Regexp.escape(target).gsub('\*', '.*?')}$", Regexp::IGNORECASE)
180
103
 
181
- targets = @groups.target_names.select { |targ| targ =~ regexp }
104
+ targets = @groups.all_targets.select { |targ| targ =~ regexp }
182
105
  targets += @groups.target_aliases.select { |target_alias, _target| target_alias =~ regexp }.values
183
106
 
184
107
  if targets.empty?
@@ -201,8 +124,12 @@ module Bolt
201
124
  targets.split(/[[:space:],]+/).reject(&:empty?).map do |name|
202
125
  ts = resolve_name(name)
203
126
  ts.map do |t|
204
- # If the target exists, return it, otherwise create one
205
- @targets[t] ? @targets[t]['self'] : create_target(t)
127
+ # If the target doesn't exist, evaluate it from the inventory.
128
+ # Then return a Bolt::Target2.
129
+ unless @targets.key?(t)
130
+ @targets[t] = create_target_from_inventory(t)
131
+ end
132
+ Bolt::Target2.new(t, self)
206
133
  end
207
134
  end
208
135
  end
@@ -211,9 +138,9 @@ module Bolt
211
138
 
212
139
  def add_target(current_group, target, desired_group)
213
140
  if current_group.name == desired_group
214
- current_group.add_target(target.target_data_hash)
141
+ current_group.add_target(target)
215
142
  @groups.validate
216
- update_target(target)
143
+ target.invalidate_group_cache!
217
144
  return true
218
145
  end
219
146
  # Recurse on children Groups if not desired_group
@@ -223,170 +150,88 @@ module Bolt
223
150
  end
224
151
  private :add_target
225
152
 
226
- # This is effectively the init method for Target2
227
- def create_target(target_name, target_hash = nil)
228
- # Prefer target hash, then data from inventoryfile, allow for uri only with empty hash
229
- data = target_hash || @groups.target_collect(target_name) || {}
230
- data = { 'uri' => target_name } if data['uri'].nil? && data['name'].nil?
231
- data['uri_obj'] = Bolt::Inventory::Inventory2.parse_uri(data['uri'])
232
-
233
- if data['uri'] && data['name'].nil?
234
- data['name'] = data['uri']
235
- data['safe_name'] = data['uri_obj'].omit(:password).to_str.sub(%r{^//}, '')
236
- elsif data['name']
237
- data['safe_name'] = data['name']
238
- else
239
- data['name'] = target_name
240
- data['safe_name'] = if data['uri_obj']
241
- data['uri_obj'].omit(:password).to_str.sub(%r{^//}, '')
242
- else
243
- target_name
244
- end
245
- end
246
- unless data['name'].ascii_only?
247
- raise ValidationError.new("Target name must be ASCII characters: #{data['name']}", nil)
248
- end
249
- # Data set on target itself (either in inventory, target.new or with set_config)
250
- data['config'] ||= {}
251
- data['vars'] ||= {}
252
- data['facts'] ||= {}
253
- data['features'] = data['features'] ? Set.new(data['features']) : Set.new
254
- data['groups'] ||= []
255
- data['options'] ||= {}
256
- data['plugin_hooks'] ||= {}
257
- data['target_alias'] ||= []
258
-
259
- # Every call to update_target will rebuild this state based on merging together target, group, and config data
260
- data['cached_state'] = {}
261
-
262
- target = Target2.new(nil, data['name'])
263
- target.inventory = self
264
- data['self'] = target
265
- @targets[data['name']] = data
153
+ # Pull in a target definition from the inventory file and evaluate any
154
+ # associated references. This is used when a target is resolved by
155
+ # get_targets.
156
+ def create_target_from_inventory(target_name)
157
+ target_data = @groups.target_collect(target_name) || { 'uri' => target_name }
158
+
159
+ target = Bolt::Inventory::Target.new(target_data, self)
160
+ @targets[target.name] = target
161
+
162
+ add_to_group([target], 'all')
163
+
266
164
  target
267
165
  end
268
- private :create_target
269
166
 
167
+ # Add a brand new target, overriding any existing target with the same
168
+ # name. This method does not honor target config from the inventory. This
169
+ # is used when Target.new is called from a plan.
270
170
  def create_target_from_plan(data)
271
- t_name = data['name'] || data['uri']
272
-
273
171
  # If target already exists, delete old and replace with new, otherwise add to new to all group
274
- if @targets[t_name]
275
- @targets.delete(t_name)
276
- t = create_target(t_name, data)
277
- update_target(t)
278
- else
279
- t = create_target(t_name, data)
280
- update_target(t)
281
- add_to_group([t], 'all')
172
+ new_target = Bolt::Inventory::Target.new(data, self)
173
+ existing_target = @targets.key?(new_target.name)
174
+ @targets[new_target.name] = new_target
175
+
176
+ unless existing_target
177
+ add_to_group([new_target], 'all')
282
178
  end
283
- t
179
+
180
+ new_target
284
181
  end
285
182
 
286
- def self.parse_uri(string)
287
- require 'addressable/uri'
288
- if string.nil?
289
- nil
290
- # Forbid empty uri
291
- elsif string.empty?
292
- raise Bolt::ParseError, "Could not parse target URI: URI is empty string"
293
- elsif string =~ %r{^[^:]+://}
294
- Addressable::URI.parse(string)
183
+ def add_to_group(targets, desired_group)
184
+ if group_names.include?(desired_group)
185
+ targets.each do |target|
186
+ if group_names.include?(target.name)
187
+ raise ValidationError.new("Group #{target.name} conflicts with target of the same name", target.name)
188
+ end
189
+ # Add the inventory copy of the target
190
+ add_target(@groups, @targets[target.name], desired_group)
191
+ end
295
192
  else
296
- # Initialize with an empty scheme to ensure we parse the hostname correctly
297
- Addressable::URI.parse("//#{string}")
193
+ raise ValidationError.new("Group #{desired_group} does not exist in inventory", nil)
298
194
  end
299
- rescue Addressable::URI::InvalidURIError => e
300
- raise Bolt::ParseError, "Could not parse target URI: #{e.message}"
301
195
  end
302
196
 
303
197
  def set_var(target, var_hash)
304
- @targets[target.name]['vars'] = @targets[target.name]['vars'].merge(var_hash)
305
- update_target(target)
198
+ @targets[target.name].set_var(var_hash)
306
199
  end
307
200
 
308
201
  def vars(target)
309
- @targets[target.name]['cached_state']['vars'] || {}
202
+ @targets[target.name].vars
310
203
  end
311
204
 
312
205
  def add_facts(target, new_facts = {})
313
- @targets[target.name]['facts'] = Bolt::Util.deep_merge(@targets[target.name]['facts'], new_facts)
314
- update_target(target)
206
+ @targets[target.name].add_facts(new_facts)
315
207
  # rubocop:disable Style/GlobalVars
316
208
  $future ? target : facts(target)
317
209
  # rubocop:enable Style/GlobalVars
318
210
  end
319
211
 
320
212
  def facts(target)
321
- @targets[target.name]['cached_state']['facts'] || {}
213
+ @targets[target.name].facts
322
214
  end
323
215
 
324
216
  def set_feature(target, feature, value = true)
325
- if value
326
- @targets[target.name]['features'] << feature
327
- else
328
- @targets[target.name]['features'].delete(feature)
329
- end
330
- update_target(target)
217
+ @targets[target.name].set_feature(feature, value)
331
218
  end
332
219
 
333
220
  def features(target)
334
- if @targets[target.name]['cached_state']['features']
335
- Set.new(@targets[target.name]['cached_state']['features'])
336
- else
337
- Set.new
338
- end
221
+ @targets[target.name].features
339
222
  end
340
223
 
341
224
  def plugin_hooks(target)
342
- @targets[target.name]['cached_state']['plugin_hooks'] || {}
225
+ @targets[target.name].plugin_hooks
343
226
  end
344
227
 
345
228
  def set_config(target, key_or_key_path, value)
346
- config = key_or_key_path.empty? ? value : build_config_hash([key_or_key_path].flatten, value)
347
- @targets[target.name]['config'] = @targets[target.name]['config'].merge(config)
348
- update_target(target)
229
+ @targets[target.name].set_config(key_or_key_path, value)
349
230
  end
350
231
 
351
232
  def target_config(target)
352
- @targets[target.name]['cached_state']['config'] || {}
353
- end
354
-
355
- def build_config_hash(key_or_key_path, value)
356
- # https://stackoverflow.com/questions/5095077/ruby-convert-array-to-nested-hash
357
- key_or_key_path.reverse.inject(value) { |acc, key| { key => acc } }
358
- end
359
- private :build_config_hash
360
-
361
- def update_target_state(target, conf, merged_data)
362
- @targets[target.name]['protocol'] = conf.transport_conf[:transport]
363
- t_conf = conf.transport_conf[:transports][target.transport.to_sym] || {}
364
- @targets[target.name]['user'] = t_conf['user']
365
- @targets[target.name]['password'] = t_conf['password']
366
- @targets[target.name]['port'] = t_conf['port']
367
- @targets[target.name]['host'] = t_conf['host']
368
- @targets[target.name]['options'] = t_conf
369
-
370
- @targets[target.name]['cached_state'] = merged_data
371
-
372
- target_facts = @targets[target.name]['facts'] || {}
373
- new_facts = merged_data['facts'] || {}
374
- @targets[target.name]['cached_state']['facts'] = Bolt::Util.deep_merge(new_facts, target_facts)
375
-
376
- target_vars = @targets[target.name]['vars'] || {}
377
- new_vars = merged_data['vars'] || {}
378
- @targets[target.name]['cached_state']['vars'] = new_vars.merge(target_vars)
379
-
380
- target_features = Set.new(@targets[target.name]['features'])
381
- new_features = Set.new(merged_data['features'])
382
- @targets[target.name]['cached_state']['features'] = new_features.merge(target_features)
383
-
384
- target_plugin_hooks = @targets[target.name]['plugin_hooks'] || {}
385
- new_plugin_hooks = merged_data['plugin_hooks'] || {}
386
- plugin_hooks_from_inv = new_plugin_hooks.merge(target_plugin_hooks)
387
- @targets[target.name]['cached_state']['plugin_hooks'] = conf.plugin_hooks.merge(plugin_hooks_from_inv)
233
+ @targets[target.name].config
388
234
  end
389
- private :update_target_state
390
235
  end
391
236
  end
392
237
  end
@@ -0,0 +1,227 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Bolt
4
+ class Inventory
5
+ # This class represents the active state of a target within the inventory.
6
+ class Target
7
+ attr_reader :name, :uri, :safe_name
8
+
9
+ def initialize(target_data, inventory)
10
+ unless target_data['name'] || target_data['uri']
11
+ raise Bolt::Inventory::ValidationError.new("Target must have either a name or uri", nil)
12
+ end
13
+
14
+ @logger = Logging.logger[inventory]
15
+
16
+ # If the target isn't mentioned by any groups, it won't have a uri or
17
+ # name and we will use the target_name as both
18
+ @uri = target_data['uri']
19
+ @uri_obj = self.class.parse_uri(@uri)
20
+
21
+ # If the target has a name, use that as the safe name. Otherwise, turn
22
+ # the uri into a safe name by omitting the password.
23
+ if target_data['name']
24
+ @name = target_data['name']
25
+ @safe_name = target_data['name']
26
+ else
27
+ @name = @uri
28
+ @safe_name = @uri_obj.omit(:password).to_str.sub(%r{^//}, '')
29
+ end
30
+
31
+ @config = target_data['config'] || {}
32
+ @vars = target_data['vars'] || {}
33
+ @facts = target_data['facts'] || {}
34
+ @features = target_data['features'] || Set.new
35
+ @options = target_data['options'] || {}
36
+ @plugin_hooks = target_data['plugin_hooks'] || {}
37
+ @target_alias = target_data['target_alias'] || []
38
+
39
+ @inventory = inventory
40
+
41
+ validate
42
+ end
43
+
44
+ def vars
45
+ # XXX Return vars from the cache
46
+ group_cache['vars'].merge(@vars)
47
+ end
48
+
49
+ # This method isn't actually an accessor and we want the name to
50
+ # correspond to the Puppet function
51
+ # rubocop:disable Naming/AccessorMethodName
52
+ def set_var(var_hash)
53
+ @vars.merge!(var_hash)
54
+ end
55
+ # rubocop:enable Naming/AccessorMethodName
56
+
57
+ def facts
58
+ # XXX Return facts from the cache
59
+ Bolt::Util.deep_merge(group_cache['facts'], @facts)
60
+ end
61
+
62
+ def add_facts(new_facts = {})
63
+ @facts = Bolt::Util.deep_merge(@facts, new_facts)
64
+ end
65
+
66
+ def features
67
+ group_cache['features'] + @features
68
+ end
69
+
70
+ def set_feature(feature, value = true)
71
+ if value
72
+ @features << feature
73
+ else
74
+ @features.delete(feature)
75
+ end
76
+ end
77
+
78
+ def plugin_hooks
79
+ # Merge plugin_hooks from the config file with any defined by the group
80
+ # or assigned dynamically to the target
81
+ @inventory.config.plugin_hooks.merge(group_cache['plugin_hooks']).merge(@plugin_hooks)
82
+ end
83
+
84
+ def set_config(key_or_key_path, value)
85
+ if key_or_key_path.empty?
86
+ @config = value
87
+ else
88
+ *path, key = Array(key_or_key_path)
89
+ location = path.inject(@config) do |working_object, p|
90
+ working_object[p] ||= {}
91
+ end
92
+ location[key] = value
93
+ end
94
+ invalidate_config_cache!
95
+ end
96
+
97
+ def invalidate_group_cache!
98
+ @group_cache = nil
99
+ # The config cache depends on the group cache, so invalidate it as well
100
+ invalidate_config_cache!
101
+ end
102
+
103
+ def invalidate_config_cache!
104
+ @transport_config_cache = nil
105
+ end
106
+
107
+ # Computing the transport config is expensive as it requires cloning the
108
+ # base config, so we cache the effective config
109
+ def transport_config_cache
110
+ if @transport_config_cache.nil?
111
+ merged_config = Bolt::Util.deep_merge(group_cache['config'], @config)
112
+ # Use the base config to ensure we handle the config validation and
113
+ # munging correctly
114
+ config = @inventory.config.deep_clone
115
+ config.update_from_inventory(merged_config)
116
+ config.validate
117
+ @transport_config_cache = {
118
+ 'transport' => config.transport_conf[:transport],
119
+ 'transports' => config.transport_conf[:transports]
120
+ }
121
+ end
122
+
123
+ @transport_config_cache
124
+ end
125
+
126
+ # Validate the target. This implicitly also primes the group and config
127
+ # caches and resolves any config references in the target's groups.
128
+ def validate
129
+ unless name.ascii_only?
130
+ raise Bolt::Inventory::ValidationError.new("Target name must be ASCII characters: #{@name}", nil)
131
+ end
132
+
133
+ unless transport.nil? || Bolt::TRANSPORTS.include?(transport.to_sym)
134
+ raise Bolt::UnknownTransportError.new(transport, uri)
135
+ end
136
+ end
137
+
138
+ def host
139
+ @uri_obj.hostname || transport_config['host']
140
+ end
141
+
142
+ def port
143
+ @uri_obj.port || transport_config['port']
144
+ end
145
+
146
+ def protocol
147
+ transport
148
+ end
149
+
150
+ def transport
151
+ @uri_obj.scheme || transport_config_cache['transport']
152
+ end
153
+
154
+ def user
155
+ Addressable::URI.unencode_component(@uri_obj.user) || transport_config['user']
156
+ end
157
+
158
+ def password
159
+ Addressable::URI.unencode_component(@uri_obj.password) || transport_config['password']
160
+ end
161
+
162
+ def options
163
+ transport_config.dup
164
+ end
165
+
166
+ # We only want to look up transport config keys for the configured
167
+ # transport
168
+ def transport_config
169
+ transport_config_cache['transports'][transport.to_sym]
170
+ end
171
+
172
+ def config
173
+ Bolt::Util.deep_merge(group_cache['config'], @config)
174
+ end
175
+
176
+ def group_cache
177
+ if @group_cache.nil?
178
+ group_data = @inventory.group_data_for(@name)
179
+
180
+ unless group_data && group_data['config']
181
+ @logger.debug("Did not find config for #{self} in inventory")
182
+ end
183
+
184
+ group_data ||= {
185
+ 'config' => {},
186
+ 'vars' => {},
187
+ 'facts' => {},
188
+ 'features' => Set.new,
189
+ 'options' => {},
190
+ 'plugin_hooks' => {},
191
+ 'target_alias' => []
192
+ }
193
+
194
+ # This should be handled by `get_targets`
195
+ if @name == 'localhost'
196
+ group_data = Bolt::Inventory.localhost_defaults(group_data)
197
+ end
198
+
199
+ @group_cache = group_data
200
+ end
201
+
202
+ @group_cache
203
+ end
204
+
205
+ def to_s
206
+ @safe_name
207
+ end
208
+
209
+ def self.parse_uri(string)
210
+ require 'addressable/uri'
211
+ if string.nil?
212
+ Addressable::URI.new
213
+ # Forbid empty uri
214
+ elsif string.empty?
215
+ raise Bolt::ParseError, "Could not parse target URI: URI is empty string"
216
+ elsif string =~ %r{^[^:]+://}
217
+ Addressable::URI.parse(string)
218
+ else
219
+ # Initialize with an empty scheme to ensure we parse the hostname correctly
220
+ Addressable::URI.parse("//#{string}")
221
+ end
222
+ rescue Addressable::URI::InvalidURIError => e
223
+ raise Bolt::ParseError, "Could not parse target URI: #{e.message}"
224
+ end
225
+ end
226
+ end
227
+ end