bolt 1.15.0 → 1.16.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/bolt-modules/boltlib/lib/puppet/datatypes/target.rb +1 -1
- data/bolt-modules/boltlib/lib/puppet/functions/run_plan.rb +4 -4
- data/lib/bolt.rb +3 -0
- data/lib/bolt/analytics.rb +7 -2
- data/lib/bolt/applicator.rb +6 -2
- data/lib/bolt/bolt_option_parser.rb +4 -4
- data/lib/bolt/cli.rb +8 -4
- data/lib/bolt/config.rb +6 -6
- data/lib/bolt/executor.rb +2 -7
- data/lib/bolt/inventory.rb +37 -6
- data/lib/bolt/inventory/group2.rb +314 -0
- data/lib/bolt/inventory/inventory2.rb +261 -0
- data/lib/bolt/outputter/human.rb +3 -1
- data/lib/bolt/pal.rb +8 -7
- data/lib/bolt/puppetdb/client.rb +6 -5
- data/lib/bolt/target.rb +34 -14
- data/lib/bolt/task.rb +2 -2
- data/lib/bolt/transport/base.rb +2 -2
- data/lib/bolt/transport/docker.rb +1 -1
- data/lib/bolt/transport/docker/connection.rb +2 -0
- data/lib/bolt/transport/local.rb +9 -181
- data/lib/bolt/transport/local/shell.rb +202 -12
- data/lib/bolt/transport/local_windows.rb +203 -0
- data/lib/bolt/transport/orch.rb +6 -4
- data/lib/bolt/transport/orch/connection.rb +6 -2
- data/lib/bolt/transport/ssh.rb +10 -150
- data/lib/bolt/transport/ssh/connection.rb +15 -116
- data/lib/bolt/transport/sudoable.rb +163 -0
- data/lib/bolt/transport/sudoable/connection.rb +76 -0
- data/lib/bolt/transport/sudoable/tmpdir.rb +59 -0
- data/lib/bolt/transport/winrm.rb +4 -4
- data/lib/bolt/transport/winrm/connection.rb +1 -0
- data/lib/bolt/util.rb +2 -0
- data/lib/bolt/version.rb +1 -1
- data/lib/bolt_ext/puppetdb_inventory.rb +0 -1
- data/lib/bolt_server/transport_app.rb +3 -1
- data/lib/logging_extensions/logging.rb +13 -0
- data/lib/plan_executor/orch_client.rb +4 -0
- metadata +23 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: b815f57b892f3750786159c9251e03f34cae09c721a6c18a2ace76d7319324b3
|
4
|
+
data.tar.gz: 13d51985236eee75d10bd527a8fe4c0328b362175dc9313a84f519967c690d9a
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: f904cf6c228379b8f790ed6d102da43244cdc5b0fa8630f04758957e35c083ee9955d09b0d212c22ea5a9e07f989e72f73e88b07f99d984c9427a813386713f3
|
7
|
+
data.tar.gz: a755bc82082397a067d8a8bdbfc81f7687071bbb2aa993c3a95e0db24feeda4e28c7b34644cbb1a2195f3211918c9de7fafd8c232ce38acd7ce5c42cfd5359da
|
@@ -7,8 +7,8 @@ Puppet::DataTypes.create_type('Target') do
|
|
7
7
|
options => { type => Hash[String[1], Data], value => {} }
|
8
8
|
},
|
9
9
|
functions => {
|
10
|
-
host => Callable[[], String[1]],
|
11
10
|
name => Callable[[], String[1]],
|
11
|
+
host => Callable[[], Optional[String]],
|
12
12
|
password => Callable[[], Optional[String[1]]],
|
13
13
|
port => Callable[[], Optional[Integer]],
|
14
14
|
protocol => Callable[[], Optional[String[1]]],
|
@@ -82,11 +82,11 @@ Puppet::Functions.create_function(:run_plan, Puppet::Functions::InternalFunction
|
|
82
82
|
end
|
83
83
|
|
84
84
|
result
|
85
|
-
rescue Puppet::PreformattedError =>
|
86
|
-
if named_args['_catch_errors'] &&
|
87
|
-
result =
|
85
|
+
rescue Puppet::PreformattedError => e
|
86
|
+
if named_args['_catch_errors'] && e.cause.is_a?(Bolt::Error)
|
87
|
+
result = e.cause.to_puppet_error
|
88
88
|
else
|
89
|
-
raise
|
89
|
+
raise e
|
90
90
|
end
|
91
91
|
ensure
|
92
92
|
if run_as
|
data/lib/bolt.rb
CHANGED
data/lib/bolt/analytics.rb
CHANGED
@@ -2,9 +2,7 @@
|
|
2
2
|
|
3
3
|
require 'bolt/util'
|
4
4
|
require 'bolt/version'
|
5
|
-
require 'httpclient'
|
6
5
|
require 'json'
|
7
|
-
require 'locale'
|
8
6
|
require 'logging'
|
9
7
|
require 'securerandom'
|
10
8
|
|
@@ -62,17 +60,21 @@ module Bolt
|
|
62
60
|
|
63
61
|
class Client
|
64
62
|
attr_reader :user_id
|
63
|
+
attr_accessor :bundled_content
|
65
64
|
|
66
65
|
def initialize(user_id)
|
67
66
|
# lazy-load expensive gem code
|
68
67
|
require 'concurrent/configuration'
|
69
68
|
require 'concurrent/future'
|
69
|
+
require 'httpclient'
|
70
|
+
require 'locale'
|
70
71
|
|
71
72
|
@logger = Logging.logger[self]
|
72
73
|
@http = HTTPClient.new
|
73
74
|
@user_id = user_id
|
74
75
|
@executor = Concurrent.global_io_executor
|
75
76
|
@os = compute_os
|
77
|
+
@bundled_content = []
|
76
78
|
end
|
77
79
|
|
78
80
|
def screen_view(screen, **kwargs)
|
@@ -164,8 +166,11 @@ module Bolt
|
|
164
166
|
end
|
165
167
|
|
166
168
|
class NoopClient
|
169
|
+
attr_accessor :bundled_content
|
170
|
+
|
167
171
|
def initialize
|
168
172
|
@logger = Logging.logger[self]
|
173
|
+
@bundled_content = []
|
169
174
|
end
|
170
175
|
|
171
176
|
def screen_view(screen, **_kwargs)
|
data/lib/bolt/applicator.rb
CHANGED
@@ -4,7 +4,6 @@ require 'base64'
|
|
4
4
|
require 'find'
|
5
5
|
require 'json'
|
6
6
|
require 'logging'
|
7
|
-
require 'minitar'
|
8
7
|
require 'open3'
|
9
8
|
require 'bolt/error'
|
10
9
|
require 'bolt/task'
|
@@ -83,6 +82,7 @@ module Bolt
|
|
83
82
|
|
84
83
|
def compile(target, ast, plan_vars)
|
85
84
|
trusted = Puppet::Context::TrustedInformation.new('local', target.host, {})
|
85
|
+
facts = @inventory.facts(target).merge('bolt' => true)
|
86
86
|
|
87
87
|
catalog_input = {
|
88
88
|
code_ast: ast,
|
@@ -91,7 +91,7 @@ module Bolt
|
|
91
91
|
hiera_config: @hiera_config,
|
92
92
|
target: {
|
93
93
|
name: target.host,
|
94
|
-
facts:
|
94
|
+
facts: facts,
|
95
95
|
variables: @inventory.vars(target).merge(plan_vars),
|
96
96
|
trusted: trusted.to_h
|
97
97
|
},
|
@@ -225,6 +225,10 @@ module Bolt
|
|
225
225
|
end
|
226
226
|
|
227
227
|
def build_plugin_tarball
|
228
|
+
# lazy-load expensive gem code
|
229
|
+
require 'minitar'
|
230
|
+
require 'zlib'
|
231
|
+
|
228
232
|
start_time = Time.now
|
229
233
|
sio = StringIO.new
|
230
234
|
output = Minitar::Output.new(Zlib::GzipWriter.new(sio))
|
@@ -314,8 +314,8 @@ Usage: bolt apply <manifest.pp> [options]
|
|
314
314
|
def parse_params(params)
|
315
315
|
json = get_arg_input(params)
|
316
316
|
JSON.parse(json)
|
317
|
-
rescue JSON::ParserError =>
|
318
|
-
raise Bolt::CLIError, "Unable to parse --params value as JSON: #{
|
317
|
+
rescue JSON::ParserError => e
|
318
|
+
raise Bolt::CLIError, "Unable to parse --params value as JSON: #{e}"
|
319
319
|
end
|
320
320
|
|
321
321
|
def get_arg_input(value)
|
@@ -331,8 +331,8 @@ Usage: bolt apply <manifest.pp> [options]
|
|
331
331
|
|
332
332
|
def read_arg_file(file)
|
333
333
|
File.read(File.expand_path(file))
|
334
|
-
rescue StandardError =>
|
335
|
-
raise Bolt::FileError.new("Error attempting to read #{file}: #{
|
334
|
+
rescue StandardError => e
|
335
|
+
raise Bolt::FileError.new("Error attempting to read #{file}: #{e}", file)
|
336
336
|
end
|
337
337
|
end
|
338
338
|
end
|
data/lib/bolt/cli.rb
CHANGED
@@ -231,6 +231,7 @@ module Bolt
|
|
231
231
|
end
|
232
232
|
|
233
233
|
@analytics = Bolt::Analytics.build_client
|
234
|
+
@analytics.bundled_content = bundled_content
|
234
235
|
|
235
236
|
screen = "#{options[:subcommand]}_#{options[:action]}"
|
236
237
|
# submit a different screen for `bolt task show` and `bolt task show foo`
|
@@ -282,7 +283,7 @@ module Bolt
|
|
282
283
|
end
|
283
284
|
code = apply_manifest(options[:code], options[:targets], options[:object], options[:noop])
|
284
285
|
else
|
285
|
-
executor = Bolt::Executor.new(config.concurrency, @analytics, options[:noop]
|
286
|
+
executor = Bolt::Executor.new(config.concurrency, @analytics, options[:noop])
|
286
287
|
targets = options[:targets]
|
287
288
|
|
288
289
|
results = nil
|
@@ -373,7 +374,7 @@ module Bolt
|
|
373
374
|
params: params }
|
374
375
|
plan_context[:description] = options[:description] if options[:description]
|
375
376
|
|
376
|
-
executor = Bolt::Executor.new(config.concurrency, @analytics, options[:noop]
|
377
|
+
executor = Bolt::Executor.new(config.concurrency, @analytics, options[:noop])
|
377
378
|
executor.start_plan(plan_context)
|
378
379
|
result = pal.run_plan(plan_name, plan_arguments, executor, inventory, puppetdb_client)
|
379
380
|
|
@@ -386,7 +387,7 @@ module Bolt
|
|
386
387
|
def apply_manifest(code, targets, filename = nil, noop = false)
|
387
388
|
ast = pal.parse_manifest(code, filename)
|
388
389
|
|
389
|
-
executor = Bolt::Executor.new(config.concurrency, @analytics, noop
|
390
|
+
executor = Bolt::Executor.new(config.concurrency, @analytics, noop)
|
390
391
|
# Call start_plan just to enable plan_logging
|
391
392
|
executor.start_plan(nil)
|
392
393
|
|
@@ -469,7 +470,8 @@ module Bolt
|
|
469
470
|
end
|
470
471
|
|
471
472
|
def bundled_content
|
472
|
-
|
473
|
+
# We only need to enumerate bundled content when running a task or plan
|
474
|
+
if %w[plan task].include?(options[:subcommand]) && options[:action] == 'run'
|
473
475
|
default_content = Bolt::PAL.new([], nil)
|
474
476
|
plans = default_content.list_plans.each_with_object([]) do |iter, col|
|
475
477
|
col << iter&.first
|
@@ -478,6 +480,8 @@ module Bolt
|
|
478
480
|
col << iter&.first
|
479
481
|
end
|
480
482
|
plans.concat tasks
|
483
|
+
else
|
484
|
+
[]
|
481
485
|
end
|
482
486
|
end
|
483
487
|
end
|
data/lib/bolt/config.rb
CHANGED
@@ -1,24 +1,24 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require '
|
3
|
+
require 'etc'
|
4
4
|
require 'logging'
|
5
|
-
# limit the loaded portion of concurrent as its expensive
|
6
|
-
require 'concurrent/utility/processor_counter'
|
7
5
|
require 'pathname'
|
8
6
|
require 'bolt/boltdir'
|
9
7
|
require 'bolt/transport/ssh'
|
10
8
|
require 'bolt/transport/winrm'
|
11
9
|
require 'bolt/transport/orch'
|
12
10
|
require 'bolt/transport/local'
|
11
|
+
require 'bolt/transport/local_windows'
|
13
12
|
require 'bolt/transport/docker'
|
14
13
|
require 'bolt/transport/remote'
|
14
|
+
require 'bolt/util'
|
15
15
|
|
16
16
|
module Bolt
|
17
17
|
TRANSPORTS = {
|
18
18
|
ssh: Bolt::Transport::SSH,
|
19
19
|
winrm: Bolt::Transport::WinRM,
|
20
20
|
pcp: Bolt::Transport::Orch,
|
21
|
-
local: Bolt::Transport::Local,
|
21
|
+
local: Bolt::Util.windows? ? Bolt::Transport::LocalWindows : Bolt::Transport::Local,
|
22
22
|
docker: Bolt::Transport::Docker,
|
23
23
|
remote: Bolt::Transport::Remote
|
24
24
|
}.freeze
|
@@ -60,7 +60,7 @@ module Bolt
|
|
60
60
|
|
61
61
|
@boltdir = boltdir
|
62
62
|
@concurrency = 100
|
63
|
-
@compile_concurrency =
|
63
|
+
@compile_concurrency = Etc.nprocessors
|
64
64
|
@transport = 'ssh'
|
65
65
|
@format = 'human'
|
66
66
|
@puppetdb = {}
|
@@ -246,7 +246,7 @@ module Bolt
|
|
246
246
|
raise Bolt::ValidationError, 'Compile concurrency must be a positive integer'
|
247
247
|
end
|
248
248
|
|
249
|
-
compile_limit = 2 *
|
249
|
+
compile_limit = 2 * Etc.nprocessors
|
250
250
|
unless @compile_concurrency < compile_limit
|
251
251
|
raise Bolt::ValidationError, "Compilation is CPU-intensive, set concurrency less than #{compile_limit}"
|
252
252
|
end
|
data/lib/bolt/executor.rb
CHANGED
@@ -21,17 +21,13 @@ module Bolt
|
|
21
21
|
# https://makandracards.com/makandra/36011-ruby-do-not-mix-optional-and-keyword-arguments
|
22
22
|
def initialize(concurrency = 1,
|
23
23
|
analytics = Bolt::Analytics::NoopClient.new,
|
24
|
-
noop =
|
25
|
-
bundled_content: nil,
|
26
|
-
load_config: true)
|
24
|
+
noop = false)
|
27
25
|
|
28
26
|
# lazy-load expensive gem code
|
29
27
|
require 'concurrent'
|
30
28
|
|
31
29
|
@analytics = analytics
|
32
|
-
@bundled_content = bundled_content
|
33
30
|
@logger = Logging.logger[self]
|
34
|
-
@load_config = load_config
|
35
31
|
|
36
32
|
@transports = Bolt::TRANSPORTS.each_with_object({}) do |(key, val), coll|
|
37
33
|
coll[key.to_s] = if key == :remote
|
@@ -184,7 +180,7 @@ module Bolt
|
|
184
180
|
end
|
185
181
|
|
186
182
|
def report_bundled_content(mode, name)
|
187
|
-
if @bundled_content&.include?(name)
|
183
|
+
if @analytics.bundled_content&.include?(name)
|
188
184
|
@analytics&.event('Bundled Content', mode, label: name)
|
189
185
|
end
|
190
186
|
end
|
@@ -263,7 +259,6 @@ module Bolt
|
|
263
259
|
log_action(description, targets) do
|
264
260
|
notify = proc { |event| @notifier.notify(callback, event) if callback }
|
265
261
|
options = { '_run_as' => run_as }.merge(options) if run_as
|
266
|
-
options = options.merge('_load_config' => @load_config)
|
267
262
|
arguments['_task'] = task.name
|
268
263
|
|
269
264
|
results = batch_execute(targets) do |transport, batch|
|
data/lib/bolt/inventory.rb
CHANGED
@@ -3,8 +3,10 @@
|
|
3
3
|
require 'set'
|
4
4
|
require 'bolt/config'
|
5
5
|
require 'bolt/inventory/group'
|
6
|
+
require 'bolt/inventory/inventory2'
|
6
7
|
require 'bolt/target'
|
7
8
|
require 'bolt/util'
|
9
|
+
require 'yaml'
|
8
10
|
|
9
11
|
module Bolt
|
10
12
|
class Inventory
|
@@ -52,11 +54,23 @@ module Bolt
|
|
52
54
|
data = Bolt::Util.read_config_file(config.inventoryfile, config.default_inventoryfile, 'inventory')
|
53
55
|
end
|
54
56
|
|
55
|
-
inventory =
|
57
|
+
inventory = create_version(data, config)
|
56
58
|
inventory.validate
|
57
59
|
inventory
|
58
60
|
end
|
59
61
|
|
62
|
+
def self.create_version(data, config)
|
63
|
+
version = (data || {}).delete('version') { 1 }
|
64
|
+
case version
|
65
|
+
when 1
|
66
|
+
new(data, config)
|
67
|
+
when 2
|
68
|
+
Bolt::Inventory::Inventory2.new(data, config)
|
69
|
+
else
|
70
|
+
raise ValidationError, "Unsupported version #{version} specified in inventory"
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
60
74
|
def initialize(data, config = nil, target_vars: {}, target_facts: {}, target_features: {})
|
61
75
|
@logger = Logging.logger[self]
|
62
76
|
# Config is saved to add config options to targets
|
@@ -163,6 +177,21 @@ module Bolt
|
|
163
177
|
end
|
164
178
|
private :groups_in
|
165
179
|
|
180
|
+
# TODO: Possibly refactor this once inventory v2 is more stable
|
181
|
+
def self.localhost_defaults(data)
|
182
|
+
defaults = {
|
183
|
+
'config' => {
|
184
|
+
'transport' => 'local',
|
185
|
+
'local' => { 'interpreters' => { '.rb' => RbConfig.ruby } }
|
186
|
+
},
|
187
|
+
'features' => ['puppet-agent']
|
188
|
+
}
|
189
|
+
data = Bolt::Util.deep_merge(defaults, data)
|
190
|
+
# If features is an empty array deep_merge won't add the puppet-agent
|
191
|
+
data['features'] << 'puppet-agent' if data['features'].empty?
|
192
|
+
data
|
193
|
+
end
|
194
|
+
|
166
195
|
# Pass a target to get_targets for a public version of this
|
167
196
|
# Should this reconfigure configured targets?
|
168
197
|
def update_target(target)
|
@@ -174,10 +203,7 @@ module Bolt
|
|
174
203
|
data['config'] = {}
|
175
204
|
end
|
176
205
|
|
177
|
-
|
178
|
-
data['config']['transport'] = 'local' if target.name == 'localhost'
|
179
|
-
end
|
180
|
-
|
206
|
+
data = self.class.localhost_defaults(data) if target.name == 'localhost'
|
181
207
|
# These should only get set from the inventory if they have not yet
|
182
208
|
# been instantiated
|
183
209
|
set_vars_from_hash(target.name, data['vars']) unless @target_vars[target.name]
|
@@ -224,6 +250,11 @@ module Bolt
|
|
224
250
|
end
|
225
251
|
private :resolve_name
|
226
252
|
|
253
|
+
def create_target(data)
|
254
|
+
Target.new(data)
|
255
|
+
end
|
256
|
+
private :create_target
|
257
|
+
|
227
258
|
def expand_targets(targets)
|
228
259
|
if targets.is_a? Bolt::Target
|
229
260
|
targets.inventory = self
|
@@ -235,7 +266,7 @@ module Bolt
|
|
235
266
|
targets.split(/[[:space:],]+/).reject(&:empty?).map do |name|
|
236
267
|
ts = resolve_name(name)
|
237
268
|
ts.map do |t|
|
238
|
-
target =
|
269
|
+
target = create_target(t)
|
239
270
|
target.inventory = self
|
240
271
|
target
|
241
272
|
end
|
@@ -0,0 +1,314 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'bolt/inventory/group'
|
4
|
+
|
5
|
+
module Bolt
|
6
|
+
class Inventory
|
7
|
+
class Group2
|
8
|
+
attr_accessor :name, :nodes, :aliases, :name_or_alias, :groups, :config, :rest, :facts, :vars, :features
|
9
|
+
|
10
|
+
# THESE are duplicates with the old groups for now.
|
11
|
+
# Regex used to validate group names and target aliases.
|
12
|
+
NAME_REGEX = /\A[a-z0-9_][a-z0-9_-]*\Z/.freeze
|
13
|
+
|
14
|
+
DATA_KEYS = %w[name config facts vars features].freeze
|
15
|
+
NODE_KEYS = DATA_KEYS + %w[alias uri]
|
16
|
+
GROUP_KEYS = DATA_KEYS + %w[groups nodes]
|
17
|
+
CONFIG_KEYS = Bolt::TRANSPORTS.keys.map(&:to_s) + ['transport']
|
18
|
+
|
19
|
+
def initialize(data)
|
20
|
+
@logger = Logging.logger[self]
|
21
|
+
|
22
|
+
raise ValidationError.new("Expected group to be a Hash, not #{data.class}", nil) unless data.is_a?(Hash)
|
23
|
+
raise ValidationError.new("Group does not have a name", nil) unless data.key?('name')
|
24
|
+
|
25
|
+
@name = data['name']
|
26
|
+
raise ValidationError.new("Group name must be a String, not #{@name.inspect}", nil) unless @name.is_a?(String)
|
27
|
+
raise ValidationError.new("Invalid group name #{@name}", @name) unless @name =~ NAME_REGEX
|
28
|
+
|
29
|
+
unless (unexpected_keys = data.keys - GROUP_KEYS).empty?
|
30
|
+
msg = "Found unexpected key(s) #{unexpected_keys.join(', ')} in group #{@name}"
|
31
|
+
@logger.warn(msg)
|
32
|
+
end
|
33
|
+
|
34
|
+
@vars = fetch_value(data, 'vars', Hash)
|
35
|
+
@facts = fetch_value(data, 'facts', Hash)
|
36
|
+
@features = fetch_value(data, 'features', Array)
|
37
|
+
@config = fetch_value(data, 'config', Hash)
|
38
|
+
|
39
|
+
unless (unexpected_keys = @config.keys - CONFIG_KEYS).empty?
|
40
|
+
msg = "Found unexpected key(s) #{unexpected_keys.join(', ')} in config for group #{@name}"
|
41
|
+
@logger.warn(msg)
|
42
|
+
end
|
43
|
+
|
44
|
+
nodes = fetch_value(data, 'nodes', Array)
|
45
|
+
groups = fetch_value(data, 'groups', Array)
|
46
|
+
|
47
|
+
@nodes = {}
|
48
|
+
@aliases = {}
|
49
|
+
nodes.reject { |node| node.is_a?(String) }.each do |node|
|
50
|
+
unless node.is_a?(Hash)
|
51
|
+
raise ValidationError.new("Node entry must be a String or Hash, not #{node.class}", @name)
|
52
|
+
end
|
53
|
+
|
54
|
+
node['name'] ||= node['uri']
|
55
|
+
|
56
|
+
if node['name'].nil? || node['name'].empty?
|
57
|
+
raise ValidationError.new("No name or uri for node: #{node}", @name)
|
58
|
+
end
|
59
|
+
|
60
|
+
if @nodes.include?(node['name'])
|
61
|
+
@logger.warn("Ignoring duplicate node in #{@name}: #{node}")
|
62
|
+
next
|
63
|
+
end
|
64
|
+
|
65
|
+
raise ValidationError.new("Node #{node} does not have a name", @name) unless node['name']
|
66
|
+
@nodes[node['name']] = node
|
67
|
+
|
68
|
+
unless (unexpected_keys = node.keys - NODE_KEYS).empty?
|
69
|
+
msg = "Found unexpected key(s) #{unexpected_keys.join(', ')} in node #{node['name']}"
|
70
|
+
@logger.warn(msg)
|
71
|
+
end
|
72
|
+
config_keys = node['config']&.keys || []
|
73
|
+
unless (unexpected_keys = config_keys - CONFIG_KEYS).empty?
|
74
|
+
msg = "Found unexpected key(s) #{unexpected_keys.join(', ')} in config for node #{node['name']}"
|
75
|
+
@logger.warn(msg)
|
76
|
+
end
|
77
|
+
|
78
|
+
next unless node.include?('alias')
|
79
|
+
|
80
|
+
aliases = node['alias']
|
81
|
+
aliases = [aliases] if aliases.is_a?(String)
|
82
|
+
unless aliases.is_a?(Array)
|
83
|
+
msg = "Alias entry on #{node['name']} must be a String or Array, not #{aliases.class}"
|
84
|
+
raise ValidationError.new(msg, @name)
|
85
|
+
end
|
86
|
+
|
87
|
+
aliases.each do |alia|
|
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
|
95
|
+
end
|
96
|
+
|
97
|
+
# If node is a string, it can refer to either a node name or alias. Which can't be determined
|
98
|
+
# until all groups have been resolved, and requires a depth-first traversal to categorize them.
|
99
|
+
@name_or_alias = nodes.select { |node| node.is_a?(String) }
|
100
|
+
|
101
|
+
@groups = groups.map { |g| Group2.new(g) }
|
102
|
+
end
|
103
|
+
|
104
|
+
def node_data(node_name)
|
105
|
+
if (data = @nodes[node_name])
|
106
|
+
{ 'config' => data['config'] || {},
|
107
|
+
'vars' => data['vars'] || {},
|
108
|
+
'facts' => data['facts'] || {},
|
109
|
+
'features' => data['features'] || [],
|
110
|
+
# This allows us to determine if a node was found?
|
111
|
+
'name' => data['name'] || nil,
|
112
|
+
'uri' => data['uri'] || nil,
|
113
|
+
# groups come from group_data
|
114
|
+
'groups' => [] }
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
def data_merge(data1, data2)
|
119
|
+
if data2.nil? || data1.nil?
|
120
|
+
return data2 || data1
|
121
|
+
end
|
122
|
+
|
123
|
+
{
|
124
|
+
'config' => Bolt::Util.deep_merge(data1['config'], data2['config']),
|
125
|
+
'name' => data1['name'] || data2['name'],
|
126
|
+
'uri' => data1['uri'] || data2['uri'],
|
127
|
+
# Shallow merge instead of deep merge so that vars with a hash value
|
128
|
+
# are assigned a new hash, rather than merging the existing value
|
129
|
+
# with the value meant to replace it
|
130
|
+
'vars' => data1['vars'].merge(data2['vars']),
|
131
|
+
'facts' => Bolt::Util.deep_merge(data1['facts'], data2['facts']),
|
132
|
+
'features' => data1['features'] | data2['features'],
|
133
|
+
'groups' => data2['groups'] + data1['groups']
|
134
|
+
}
|
135
|
+
end
|
136
|
+
|
137
|
+
private def fetch_value(data, key, type)
|
138
|
+
value = data.fetch(key, type.new)
|
139
|
+
unless value.is_a?(type)
|
140
|
+
raise ValidationError.new("Expected #{key} to be of type #{type}, not #{value.class}", @name)
|
141
|
+
end
|
142
|
+
value
|
143
|
+
end
|
144
|
+
|
145
|
+
def resolve_aliases(aliases, node_names)
|
146
|
+
@name_or_alias.each do |name_or_alias|
|
147
|
+
# If an alias is found, insert the name into this group. Otherwise use the name as a new node's uri.
|
148
|
+
if node_names.include?(name_or_alias)
|
149
|
+
@nodes[name_or_alias] = { 'name' => name_or_alias }
|
150
|
+
elsif (node_name = aliases[name_or_alias])
|
151
|
+
if @nodes.include?(node_name)
|
152
|
+
@logger.warn("Ignoring duplicate node in #{@name}: #{node_name}")
|
153
|
+
else
|
154
|
+
@nodes[node_name] = { 'name' => node_name }
|
155
|
+
end
|
156
|
+
else
|
157
|
+
node_name = name_or_alias
|
158
|
+
|
159
|
+
if @nodes.include?(node_name)
|
160
|
+
@logger.warn("Ignoring duplicate node in #{@name}: #{node_name}")
|
161
|
+
else
|
162
|
+
@nodes[node_name] = { 'uri' => node_name }
|
163
|
+
end
|
164
|
+
end
|
165
|
+
end
|
166
|
+
|
167
|
+
@groups.each { |g| g.resolve_aliases(aliases, node_names) }
|
168
|
+
end
|
169
|
+
|
170
|
+
private def alias_conflict(name, node1, node2)
|
171
|
+
"Alias #{name} refers to multiple targets: #{node1} and #{node2}"
|
172
|
+
end
|
173
|
+
|
174
|
+
private def group_alias_conflict(name)
|
175
|
+
"Group #{name} conflicts with alias of the same name"
|
176
|
+
end
|
177
|
+
|
178
|
+
private def group_node_conflict(name)
|
179
|
+
"Group #{name} conflicts with node of the same name"
|
180
|
+
end
|
181
|
+
|
182
|
+
private def alias_node_conflict(name)
|
183
|
+
"Node name #{name} conflicts with alias of the same name"
|
184
|
+
end
|
185
|
+
|
186
|
+
def validate(used_names = Set.new, node_names = Set.new, aliased = {}, depth = 0)
|
187
|
+
# Test if this group name conflicts with anything used before.
|
188
|
+
raise ValidationError.new("Tried to redefine group #{@name}", @name) if used_names.include?(@name)
|
189
|
+
raise ValidationError.new(group_node_conflict(@name), @name) if node_names.include?(@name)
|
190
|
+
raise ValidationError.new(group_alias_conflict(@name), @name) if aliased.include?(@name)
|
191
|
+
|
192
|
+
used_names << @name
|
193
|
+
|
194
|
+
# Collect node names and aliases into a list used to validate that subgroups don't conflict.
|
195
|
+
# Used names validate that previously used group names don't conflict with new node names/aliases.
|
196
|
+
@nodes.each_key do |n|
|
197
|
+
# Require nodes to be parseable as a Target.
|
198
|
+
begin
|
199
|
+
Target.new(n)
|
200
|
+
rescue Bolt::ParseError => e
|
201
|
+
@logger.debug(e)
|
202
|
+
raise ValidationError.new("Invalid node name #{n}", @name)
|
203
|
+
end
|
204
|
+
|
205
|
+
raise ValidationError.new(group_node_conflict(n), @name) if used_names.include?(n)
|
206
|
+
if aliased.include?(n)
|
207
|
+
raise ValidationError.new(alias_node_conflict(n), @name)
|
208
|
+
end
|
209
|
+
|
210
|
+
node_names << n
|
211
|
+
end
|
212
|
+
|
213
|
+
@aliases.each do |n, target|
|
214
|
+
raise ValidationError.new(group_alias_conflict(n), @name) if used_names.include?(n)
|
215
|
+
if node_names.include?(n)
|
216
|
+
raise ValidationError.new(alias_node_conflict(n), @name)
|
217
|
+
end
|
218
|
+
|
219
|
+
if aliased.include?(n)
|
220
|
+
raise ValidationError.new(alias_conflict(n, target, aliased[n]), @name)
|
221
|
+
end
|
222
|
+
|
223
|
+
aliased[n] = target
|
224
|
+
end
|
225
|
+
|
226
|
+
@groups.each do |g|
|
227
|
+
begin
|
228
|
+
g.validate(used_names, node_names, aliased, depth + 1)
|
229
|
+
rescue ValidationError => e
|
230
|
+
e.add_parent(@name)
|
231
|
+
raise e
|
232
|
+
end
|
233
|
+
end
|
234
|
+
|
235
|
+
nil
|
236
|
+
end
|
237
|
+
|
238
|
+
# The data functions below expect and return nil or a hash of the schema
|
239
|
+
# { 'config' => Hash , 'vars' => Hash, 'facts' => Hash, 'features' => Array, groups => Array }
|
240
|
+
def data_for(node_name)
|
241
|
+
data_merge(group_collect(node_name), node_collect(node_name))
|
242
|
+
end
|
243
|
+
|
244
|
+
def group_data
|
245
|
+
{ 'config' => @config,
|
246
|
+
'vars' => @vars,
|
247
|
+
'facts' => @facts,
|
248
|
+
'features' => @features,
|
249
|
+
'groups' => [@name] }
|
250
|
+
end
|
251
|
+
|
252
|
+
def empty_data
|
253
|
+
{ 'config' => {},
|
254
|
+
'vars' => {},
|
255
|
+
'facts' => {},
|
256
|
+
'features' => [],
|
257
|
+
'groups' => [] }
|
258
|
+
end
|
259
|
+
|
260
|
+
# Returns all nodes contained within the group, which includes nodes from subgroups.
|
261
|
+
def node_names
|
262
|
+
@groups.inject(local_node_names) do |acc, g|
|
263
|
+
acc.merge(g.node_names)
|
264
|
+
end
|
265
|
+
end
|
266
|
+
|
267
|
+
# Returns a mapping of aliases to nodes contained within the group, which includes subgroups.
|
268
|
+
def node_aliases
|
269
|
+
@groups.inject(@aliases) do |acc, g|
|
270
|
+
acc.merge(g.node_aliases)
|
271
|
+
end
|
272
|
+
end
|
273
|
+
|
274
|
+
# Return a mapping of group names to group.
|
275
|
+
def collect_groups
|
276
|
+
@groups.inject(name => self) do |acc, g|
|
277
|
+
acc.merge(g.collect_groups)
|
278
|
+
end
|
279
|
+
end
|
280
|
+
|
281
|
+
def local_node_names
|
282
|
+
Set.new(@nodes.keys)
|
283
|
+
end
|
284
|
+
private :local_node_names
|
285
|
+
|
286
|
+
def node_collect(node_name)
|
287
|
+
data = @groups.inject(nil) do |acc, g|
|
288
|
+
if (d = g.node_collect(node_name))
|
289
|
+
data_merge(d, acc)
|
290
|
+
else
|
291
|
+
acc
|
292
|
+
end
|
293
|
+
end
|
294
|
+
data_merge(node_data(node_name), data)
|
295
|
+
end
|
296
|
+
|
297
|
+
def group_collect(node_name)
|
298
|
+
data = @groups.inject(nil) do |acc, g|
|
299
|
+
if (d = g.data_for(node_name))
|
300
|
+
data_merge(d, acc)
|
301
|
+
else
|
302
|
+
acc
|
303
|
+
end
|
304
|
+
end
|
305
|
+
|
306
|
+
if data
|
307
|
+
data_merge(group_data, data)
|
308
|
+
elsif @nodes.include?(node_name)
|
309
|
+
group_data
|
310
|
+
end
|
311
|
+
end
|
312
|
+
end
|
313
|
+
end
|
314
|
+
end
|