bolt 1.31.1 → 1.32.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.

@@ -5,6 +5,7 @@ require 'bolt/inventory/group2'
5
5
  module Bolt
6
6
  class Inventory
7
7
  class Inventory2
8
+ attr_reader :targets, :plugins, :config
8
9
  # This uses "targets" in the message instead of "nodes"
9
10
  class WildcardError < Bolt::Error
10
11
  def initialize(target)
@@ -12,11 +13,7 @@ module Bolt
12
13
  end
13
14
  end
14
15
 
15
- attr_reader :plugins, :config
16
-
17
- def initialize(data, config = nil, plugins: nil, target_vars: {},
18
- target_facts: {}, target_features: {},
19
- target_plugin_hooks: {})
16
+ def initialize(data, config = nil, plugins: nil)
20
17
  @logger = Logging.logger[self]
21
18
  # Config is saved to add config options to targets
22
19
  @config = config || Bolt::Config.default
@@ -24,10 +21,8 @@ module Bolt
24
21
  @groups = Group2.new(@data.merge('name' => 'all'), plugins)
25
22
  @plugins = plugins
26
23
  @group_lookup = {}
27
- @target_vars = target_vars
28
- @target_facts = target_facts
29
- @target_features = target_features
30
- @target_plugin_hooks = target_plugin_hooks
24
+ # The targets hash is the canonical source for all targets in inventory
25
+ @targets = {}
31
26
  @groups.resolve_aliases(@groups.target_aliases, @groups.target_names)
32
27
  collect_groups
33
28
  end
@@ -40,6 +35,10 @@ module Bolt
40
35
  2
41
36
  end
42
37
 
38
+ def target_implementation_class
39
+ Bolt::Target2
40
+ end
41
+
43
42
  def collect_groups
44
43
  # Provide a lookup map for finding a group by name
45
44
  @group_lookup = @groups.collect_groups
@@ -56,13 +55,15 @@ module Bolt
56
55
  alias node_names target_names
57
56
 
58
57
  def get_targets(targets)
59
- targets = expand_targets(targets)
60
- targets = if targets.is_a? Array
61
- targets.flatten.uniq(&:name)
62
- else
63
- [targets]
64
- end
65
- targets.map { |t| update_target(t) }
58
+ flat_target_list(targets).map { |t| update_target(t) }
59
+ end
60
+
61
+ def get_target(target)
62
+ target_array = flat_target_list(target)
63
+ if target_array.count > 1
64
+ raise ValidationError.new("'#{target}' refers to #{target_array.count} targets", nil)
65
+ end
66
+ get_targets(target_array.first).first
66
67
  end
67
68
 
68
69
  def add_to_group(targets, desired_group)
@@ -78,41 +79,6 @@ module Bolt
78
79
  end
79
80
  end
80
81
 
81
- def set_var(target, key, value)
82
- data = { key => value }
83
- set_vars_from_hash(target.name, data)
84
- end
85
-
86
- def vars(target)
87
- @target_vars[target.name] || {}
88
- end
89
-
90
- def add_facts(target, new_facts = {})
91
- @logger.warn("No facts to add") if new_facts.empty?
92
- set_facts(target.name, new_facts)
93
- end
94
-
95
- def facts(target)
96
- @target_facts[target.name] || {}
97
- end
98
-
99
- def set_feature(target, feature, value = true)
100
- @target_features[target.name] ||= Set.new
101
- if value
102
- @target_features[target.name] << feature
103
- else
104
- @target_features[target.name].delete(feature)
105
- end
106
- end
107
-
108
- def features(target)
109
- @target_features[target.name] || Set.new
110
- end
111
-
112
- def plugin_hooks(target)
113
- @target_plugin_hooks[target.name] || {}
114
- end
115
-
116
82
  def data_hash
117
83
  {
118
84
  data: {},
@@ -147,8 +113,13 @@ module Bolt
147
113
  private :config_plugin
148
114
 
149
115
  # Pass a target to get_targets for a public version of this
150
- # Should this reconfigure configured targets?
151
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
152
123
  data = @groups.data_for(target.name)
153
124
  data ||= {}
154
125
 
@@ -157,23 +128,24 @@ module Bolt
157
128
  data['config'] = {}
158
129
  end
159
130
 
160
- data = Bolt::Inventory.localhost_defaults(data) if target.name == 'localhost'
161
- # These should only get set from the inventory if they have not yet
162
- # been instantiated
163
- set_vars_from_hash(target.name, data['vars']) unless @target_vars[target.name]
164
- set_facts(target.name, data['facts']) unless @target_facts[target.name]
165
- data['features']&.each { |feature| set_feature(target, feature) } unless @target_features[target.name]
166
- unless @target_plugin_hooks[target.name]
167
- set_plugin_hooks(target.name, @config.plugin_hooks.merge(data['plugin_hooks'] || {}))
131
+ # Add defaults for special 'localhost' target (currently just config and features)
132
+ if target.name == 'localhost'
133
+ data = Bolt::Inventory.localhost_defaults(data)
168
134
  end
135
+
136
+ # Data from inventory
169
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)
170
141
 
171
142
  # Use Config object to ensure config section is treated consistently with config file
172
143
  conf = @config.deep_clone
173
144
  conf.update_from_inventory(data['config'])
174
145
  conf.validate
175
146
 
176
- target.update_conf(conf.transport_conf)
147
+ # Recompute the target cached state with the merged data
148
+ update_target_state(target, conf.transport_conf, data)
177
149
 
178
150
  unless target.transport.nil? || Bolt::TRANSPORTS.include?(target.transport.to_sym)
179
151
  raise Bolt::UnknownTransportError.new(target.transport, target.uri)
@@ -183,6 +155,17 @@ module Bolt
183
155
  end
184
156
  private :update_target
185
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
166
+ end
167
+ private :flat_target_list
168
+
186
169
  # If target is a group name, expand it to the members of that group.
187
170
  # Else match against targets in inventory by name or alias.
188
171
  # If a wildcard string, error if no matches are found.
@@ -209,8 +192,7 @@ module Bolt
209
192
  private :resolve_name
210
193
 
211
194
  def expand_targets(targets)
212
- if targets.is_a? Bolt::Target
213
- targets.inventory = self
195
+ if targets.is_a? Bolt::Target2
214
196
  targets
215
197
  elsif targets.is_a? Array
216
198
  targets.map { |tish| expand_targets(tish) }
@@ -219,102 +201,189 @@ module Bolt
219
201
  targets.split(/[[:space:],]+/).reject(&:empty?).map do |name|
220
202
  ts = resolve_name(name)
221
203
  ts.map do |t|
222
- target = create_target(t)
223
- target.inventory = self
224
- target
204
+ # If the target exists, return it, otherwise create one
205
+ @targets[t] ? @targets[t]['self'] : create_target(t)
225
206
  end
226
207
  end
227
208
  end
228
209
  end
229
210
  private :expand_targets
230
211
 
231
- def set_vars_from_hash(tname, data)
232
- if data
233
- # Instantiate empty vars hash in case no vars are defined
234
- @target_vars[tname] ||= {}
235
- # Assign target new merged vars hash
236
- # This is essentially a copy-on-write to maintain the immutability of @target_vars
237
- @target_vars[tname] = @target_vars[tname].merge(data).freeze
212
+ def add_target(current_group, target, desired_group)
213
+ if current_group.name == desired_group
214
+ current_group.add_target(target.target_data_hash)
215
+ @groups.validate
216
+ update_target(target)
217
+ return true
218
+ end
219
+ # Recurse on children Groups if not desired_group
220
+ current_group.groups.each do |child_group|
221
+ add_target(child_group, target, desired_group)
238
222
  end
239
223
  end
240
- private :set_vars_from_hash
224
+ private :add_target
241
225
 
242
- def set_facts(tname, hash)
243
- if hash
244
- @target_facts[tname] ||= {}
245
- @target_facts[tname] = Bolt::Util.deep_merge(@target_facts[tname], hash).freeze
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)
246
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
266
+ target
247
267
  end
248
- private :set_facts
268
+ private :create_target
269
+
270
+ def create_target_from_plan(data)
271
+ t_name = data['name'] || data['uri']
249
272
 
250
- def set_plugin_hooks(tname, hash)
251
- if hash
252
- @target_plugin_hooks[tname] ||= {}
253
- @target_plugin_hooks[tname].merge!(hash)
273
+ # 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')
254
282
  end
283
+ t
255
284
  end
256
- private :set_plugin_hooks
257
285
 
258
- def add_target(current_group, target, desired_group, track = { 'all' => nil })
259
- if current_group.name == desired_group
260
- # Group to add to is found
261
- t_name = target.name
262
- # Add target to targets hash
263
- target_hash = { 'name' => t_name }.merge(target.options)
264
- target_hash['uri'] = target.uri if target.uri
265
- current_group.targets[t_name] = target_hash
266
-
267
- # Inherit facts, vars, and features from hierarchy
268
- current_group_data = { facts: current_group.facts,
269
- vars: current_group.vars,
270
- features: current_group.features,
271
- plugin_hooks: current_group.plugin_hooks }
272
- data = inherit_data(track, current_group.name, current_group_data)
273
- set_facts(t_name, @target_facts[t_name] ? data[:facts].merge(@target_facts[t_name]) : data[:facts])
274
- set_vars_from_hash(t_name, @target_vars[t_name] ? data[:vars].merge(@target_vars[t_name]) : data[:vars])
275
- data[:features].each do |feature|
276
- set_feature(target, feature)
277
- end
278
- hook_data = @config.plugin_hooks.merge(data[:plugin_hooks])
279
- hash = if @target_plugin_hooks[t_name]
280
- hook_data.merge(@target_plugin_hooks[t_name])
281
- else
282
- hook_data
283
- end
284
- set_plugin_hooks(t_name, hash)
285
- return true
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)
295
+ else
296
+ # Initialize with an empty scheme to ensure we parse the hostname correctly
297
+ Addressable::URI.parse("//#{string}")
286
298
  end
287
- # Recurse on children Groups if not desired_group
288
- current_group.groups.each do |child_group|
289
- track[child_group.name] = current_group
290
- add_target(child_group, target, desired_group, track)
299
+ rescue Addressable::URI::InvalidURIError => e
300
+ raise Bolt::ParseError, "Could not parse target URI: #{e.message}"
301
+ end
302
+
303
+ def set_var(target, var_hash)
304
+ @targets[target.name]['vars'] = @targets[target.name]['vars'].merge(var_hash)
305
+ update_target(target)
306
+ end
307
+
308
+ def vars(target)
309
+ @targets[target.name]['cached_state']['vars'] || {}
310
+ end
311
+
312
+ 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)
315
+ facts(target)
316
+ end
317
+
318
+ def facts(target)
319
+ @targets[target.name]['cached_state']['facts'] || {}
320
+ end
321
+
322
+ def set_feature(target, feature, value = true)
323
+ if value
324
+ @targets[target.name]['features'] << feature
325
+ else
326
+ @targets[target.name]['features'].delete(feature)
291
327
  end
328
+ update_target(target)
292
329
  end
293
- private :add_target
294
330
 
295
- def inherit_data(track, name, data)
296
- unless track[name].nil?
297
- data[:facts] = track[name].facts.merge(data[:facts])
298
- data[:vars] = track[name].vars.merge(data[:vars])
299
- data[:features].concat(track[name].features)
300
- data[:plugin_hooks] = track[name].plugin_hooks.merge(data[:plugin_hooks])
301
- inherit_data(track, track[name].name, data)
331
+ def features(target)
332
+ if @targets[target.name]['cached_state']['features']
333
+ Set.new(@targets[target.name]['cached_state']['features'])
334
+ else
335
+ Set.new
302
336
  end
303
- data
304
337
  end
305
- private :inherit_data
306
338
 
307
- def create_target(target_name)
308
- data = @groups.data_for(target_name) || {}
309
- name_opt = {}
310
- name_opt['name'] = data['name'] if data['name']
339
+ def plugin_hooks(target)
340
+ @targets[target.name]['cached_state']['plugin_hooks'] || {}
341
+ end
311
342
 
312
- # If there is no name then this target was only referred to as a string.
313
- uri = data['uri']
314
- uri ||= target_name unless data['name']
343
+ def set_config(target, key_or_key_path, value)
344
+ config = key_or_key_path.empty? ? value : build_config_hash([key_or_key_path].flatten, value)
345
+ @targets[target.name]['config'] = @targets[target.name]['config'].merge(config)
346
+ update_target(target)
347
+ end
348
+
349
+ def target_config(target)
350
+ @targets[target.name]['cached_state']['config'] || {}
351
+ end
315
352
 
316
- Target.new(uri, name_opt)
353
+ def build_config_hash(key_or_key_path, value)
354
+ # https://stackoverflow.com/questions/5095077/ruby-convert-array-to-nested-hash
355
+ key_or_key_path.reverse.inject(value) { |acc, key| { key => acc } }
356
+ end
357
+ private :build_config_hash
358
+
359
+ def update_target_state(target, conf, merged_data)
360
+ @targets[target.name]['protocol'] = conf[:transport]
361
+ t_conf = conf[:transports][target.transport.to_sym] || {}
362
+ @targets[target.name]['user'] = t_conf['user']
363
+ @targets[target.name]['password'] = t_conf['password']
364
+ @targets[target.name]['port'] = t_conf['port']
365
+ @targets[target.name]['host'] = t_conf['host']
366
+ @targets[target.name]['options'] = t_conf
367
+
368
+ @targets[target.name]['cached_state'] = merged_data
369
+
370
+ target_facts = @targets[target.name]['facts'] || {}
371
+ new_facts = merged_data['facts'] || {}
372
+ @targets[target.name]['cached_state']['facts'] = Bolt::Util.deep_merge(new_facts, target_facts)
373
+
374
+ target_vars = @targets[target.name]['vars'] || {}
375
+ new_vars = merged_data['vars'] || {}
376
+ @targets[target.name]['cached_state']['vars'] = new_vars.merge(target_vars)
377
+
378
+ target_features = Set.new(@targets[target.name]['features'])
379
+ new_features = Set.new(merged_data['features'])
380
+ @targets[target.name]['cached_state']['features'] = new_features.merge(target_features)
381
+
382
+ target_plugin_hooks = @targets[target.name]['plugin_hooks'] || {}
383
+ new_plugin_hooks = merged_data['plugin_hooks'] || {}
384
+ @targets[target.name]['cached_state']['plugin_hooks'] = new_plugin_hooks.merge(target_plugin_hooks)
317
385
  end
386
+ private :update_target_state
318
387
  end
319
388
  end
320
389
  end
@@ -139,9 +139,8 @@ module Bolt
139
139
  end
140
140
  end
141
141
 
142
- def set_var(target, key, value)
143
- data = { key => value }
144
- set_vars_from_hash(target.name, data)
142
+ def set_var(target, var_hash)
143
+ set_vars_from_hash(target.name, var_hash)
145
144
  end
146
145
 
147
146
  def vars(target)
@@ -8,6 +8,12 @@ module Bolt
8
8
  Puppet::Pops::Issues.issue :PLAN_OPERATION_NOT_SUPPORTED_WHEN_COMPILING, :action do
9
9
  "Plan language function '#{action}' cannot be used from declarative manifest code or apply blocks"
10
10
  end
11
+
12
+ # Inventory version 2
13
+ UNSUPPORTED_INVENTORY_VERSION =
14
+ Puppet::Pops::Issues.issue :UNSUPPORTED_INVENTORY_VERSION, :action do
15
+ "Plan language function '#{action}' cannot be used with Inventory v1"
16
+ end
11
17
  end
12
18
  end
13
19
  end
data/lib/bolt/pal.rb CHANGED
@@ -39,7 +39,7 @@ module Bolt
39
39
 
40
40
  attr_reader :modulepath
41
41
 
42
- def initialize(modulepath, hiera_config, max_compiles = Etc.nprocessors)
42
+ def initialize(modulepath, hiera_config, resource_types, max_compiles = Etc.nprocessors)
43
43
  # Nothing works without initialized this global state. Reinitializing
44
44
  # is safe and in practice only happens in tests
45
45
  self.class.load_puppet
@@ -48,6 +48,7 @@ module Bolt
48
48
  @modulepath = [BOLTLIB_PATH, *modulepath, MODULES_PATH]
49
49
  @hiera_config = hiera_config
50
50
  @max_compiles = max_compiles
51
+ @resource_types = resource_types
51
52
 
52
53
  @logger = Logging.logger[self]
53
54
  if modulepath && !modulepath.empty?
@@ -110,6 +111,22 @@ module Bolt
110
111
  compiler.evaluate_string('type PlanResult = Boltlib::PlanResult')
111
112
  end
112
113
 
114
+ # Register all resource types defined in $Boltdir/.resource_types as well as
115
+ # the built in types registered with the runtime_3_init method.
116
+ def register_resource_types(loaders)
117
+ static_loader = loaders.static_loader
118
+ static_loader.runtime_3_init
119
+ if File.directory?(@resource_types)
120
+ # Ruby 2.3 does not support Dir.children
121
+ (Dir.entries(@resource_types) - %w[. ..]).each do |resource_pp|
122
+ type_name_from_file = File.basename(resource_pp, '.pp').capitalize
123
+ typed_name = Puppet::Pops::Loader::TypedName.new(:type, type_name_from_file)
124
+ resource_type = Puppet::Pops::Types::TypeFactory.resource(type_name_from_file)
125
+ loaders.static_loader.set_entry(typed_name, resource_type)
126
+ end
127
+ end
128
+ end
129
+
113
130
  # Runs a block in a PAL script compiler configured for Bolt. Catches
114
131
  # exceptions thrown by the block and re-raises them ensuring they are
115
132
  # Bolt::Errors since the script compiler block will squash all exceptions.
@@ -119,6 +136,7 @@ module Bolt
119
136
  r = Puppet::Pal.in_tmp_environment('bolt', modulepath: @modulepath, facts: {}) do |pal|
120
137
  pal.with_script_compiler do |compiler|
121
138
  alias_types(compiler)
139
+ register_resource_types(Puppet.lookup(:loaders)) if @resource_types
122
140
  begin
123
141
  Puppet.override(yaml_plan_instantiator: Bolt::PAL::YamlPlan::Loader) do
124
142
  yield compiler
@@ -351,6 +369,16 @@ module Bolt
351
369
  end
352
370
  end
353
371
 
372
+ def generate_types
373
+ require 'puppet/face/generate'
374
+ in_bolt_compiler do
375
+ generator = Puppet::Generate::Type
376
+ inputs = generator.find_inputs(:pcore)
377
+ FileUtils.mkdir_p(@resource_types)
378
+ generator.generate(inputs, @resource_types, true)
379
+ end
380
+ end
381
+
354
382
  def run_task(task_name, targets, params, executor, inventory, description = nil)
355
383
  in_task_compiler(executor, inventory) do |compiler|
356
384
  params = params.merge('_bolt_api_call' => true, '_catch_errors' => true)
data/lib/bolt/plugin.rb CHANGED
@@ -134,7 +134,7 @@ module Bolt
134
134
  plugins
135
135
  end
136
136
 
137
- BUILTIN_PLUGINS = %w[task terraform pkcs7 prompt vault aws_inventory puppetdb].freeze
137
+ BUILTIN_PLUGINS = %w[task terraform pkcs7 prompt vault aws_inventory puppetdb azure_inventory].freeze
138
138
 
139
139
  attr_reader :pal, :plugin_context
140
140
 
@@ -193,7 +193,7 @@ module Bolt
193
193
  plugin = by_name(plugin_name)
194
194
  raise PluginError::Unknown, plugin_name unless plugin
195
195
  raise PluginError::UnsupportedHook.new(plugin_name, hook) unless plugin.hooks.include?(hook)
196
- @analytics.report_bundled_content("Plugin #{hook}", plugin_name) if BUILTIN_PLUGINS.include?(plugin_name)
196
+ @analytics.report_bundled_content("Plugin #{hook}", plugin_name)
197
197
 
198
198
  plugin.method(hook)
199
199
  end
@@ -36,7 +36,9 @@ module Bolt
36
36
  def validate_resolve_reference(opts)
37
37
  # TODO: Remove deprecation warning
38
38
  if opts.include?('encrypted-value')
39
- raise Bolt::ValidationError, "The 'encrypted-value' key is deprecated migrate to to 'encrypted_value'"
39
+ msg = "Parsing error: The 'encrypted-value' key is deprecated and can no longer be used. " \
40
+ "In your Bolt config files, please use 'encrypted_value' instead."
41
+ raise Bolt::ValidationError, msg
40
42
  end
41
43
  decode(opts['encrypted_value'])
42
44
  end