bolt 1.49.0 → 2.0.0
Sign up to get free protection for your applications and to get access to all the features.
Potentially problematic release.
This version of bolt might be problematic. Click here for more details.
- checksums.yaml +4 -4
- data/Puppetfile +6 -6
- data/bolt-modules/boltlib/lib/puppet/datatypes/target.rb +24 -45
- data/bolt-modules/boltlib/lib/puppet/functions/add_facts.rb +3 -3
- data/bolt-modules/boltlib/lib/puppet/functions/add_to_group.rb +1 -1
- data/bolt-modules/boltlib/lib/puppet/functions/apply_prep.rb +10 -12
- data/bolt-modules/boltlib/lib/puppet/functions/catch_errors.rb +1 -1
- data/bolt-modules/boltlib/lib/puppet/functions/fail_plan.rb +3 -3
- data/bolt-modules/boltlib/lib/puppet/functions/get_resources.rb +5 -4
- data/bolt-modules/boltlib/lib/puppet/functions/get_target.rb +1 -3
- data/bolt-modules/boltlib/lib/puppet/functions/get_targets.rb +1 -2
- data/bolt-modules/boltlib/lib/puppet/functions/puppetdb_fact.rb +2 -2
- data/bolt-modules/boltlib/lib/puppet/functions/remove_from_group.rb +2 -2
- data/bolt-modules/boltlib/lib/puppet/functions/resolve_references.rb +1 -1
- data/bolt-modules/boltlib/lib/puppet/functions/run_command.rb +7 -3
- data/bolt-modules/boltlib/lib/puppet/functions/run_plan.rb +15 -31
- data/bolt-modules/boltlib/lib/puppet/functions/run_script.rb +9 -5
- data/bolt-modules/boltlib/lib/puppet/functions/run_task.rb +9 -3
- data/bolt-modules/boltlib/lib/puppet/functions/set_config.rb +4 -3
- data/bolt-modules/boltlib/lib/puppet/functions/set_feature.rb +6 -6
- data/bolt-modules/boltlib/lib/puppet/functions/set_var.rb +2 -2
- data/bolt-modules/boltlib/lib/puppet/functions/upload_file.rb +7 -3
- data/bolt-modules/boltlib/lib/puppet/functions/wait_until_available.rb +6 -2
- data/bolt-modules/boltlib/lib/puppet/functions/without_default_logging.rb +2 -2
- data/bolt-modules/ctrl/lib/puppet/functions/ctrl/do_until.rb +2 -1
- data/bolt-modules/file/lib/puppet/functions/file/exists.rb +2 -1
- data/bolt-modules/file/lib/puppet/functions/file/join.rb +1 -0
- data/bolt-modules/file/lib/puppet/functions/file/read.rb +1 -0
- data/bolt-modules/file/lib/puppet/functions/file/readable.rb +2 -1
- data/bolt-modules/out/lib/puppet/functions/out/message.rb +1 -1
- data/bolt-modules/system/lib/puppet/functions/system/env.rb +1 -0
- data/lib/bolt/applicator.rb +70 -118
- data/lib/bolt/apply_target.rb +1 -1
- data/lib/bolt/bolt_option_parser.rb +21 -37
- data/lib/bolt/catalog.rb +5 -22
- data/lib/bolt/catalog/logging.rb +1 -1
- data/lib/bolt/cli.rb +33 -44
- data/lib/bolt/config.rb +15 -18
- data/lib/bolt/error.rb +2 -2
- data/lib/bolt/executor.rb +32 -40
- data/lib/bolt/inventory.rb +9 -367
- data/lib/bolt/inventory/group.rb +293 -182
- data/lib/bolt/inventory/{inventory2.rb → inventory.rb} +25 -14
- data/lib/bolt/inventory/target.rb +1 -1
- data/lib/bolt/module.rb +4 -4
- data/lib/bolt/outputter/human.rb +11 -6
- data/lib/bolt/outputter/json.rb +3 -11
- data/lib/bolt/pal.rb +1 -2
- data/lib/bolt/pal/yaml_plan/step/resources.rb +1 -1
- data/lib/bolt/plugin.rb +1 -1
- data/lib/bolt/plugin/module.rb +19 -36
- data/lib/bolt/plugin/prompt.rb +2 -4
- data/lib/bolt/puppetdb/config.rb +1 -3
- data/lib/bolt/result.rb +3 -6
- data/lib/bolt/secret/base.rb +0 -6
- data/lib/bolt/target.rb +8 -219
- data/lib/bolt/transport/local/shell.rb +9 -13
- data/lib/bolt/transport/orch.rb +3 -5
- data/lib/bolt/transport/ssh.rb +1 -0
- data/lib/bolt/transport/ssh/connection.rb +1 -4
- data/lib/bolt/transport/winrm/connection.rb +1 -1
- data/lib/bolt/util.rb +2 -8
- data/lib/bolt/version.rb +1 -1
- data/lib/bolt_server/transport_app.rb +35 -17
- data/lib/bolt_spec/plans.rb +8 -2
- data/libexec/bolt_catalog +19 -5
- metadata +4 -8
- data/exe/bolt-inventory-pdb +0 -13
- data/lib/bolt/inventory/group2.rb +0 -403
- data/lib/bolt_ext/puppetdb_inventory.rb +0 -129
@@ -164,15 +164,13 @@ module Bolt
|
|
164
164
|
|
165
165
|
# Read from out and err
|
166
166
|
ready_read&.each do |stream|
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
rescue EOFError
|
175
|
-
end
|
167
|
+
# Check for sudo prompt
|
168
|
+
read_streams[stream] << if use_sudo
|
169
|
+
check_sudo(stream, inp, t.pid, options[:stdin])
|
170
|
+
else
|
171
|
+
stream.readpartial(CHUNK_SIZE)
|
172
|
+
end
|
173
|
+
rescue EOFError
|
176
174
|
end
|
177
175
|
|
178
176
|
# select will either return an empty array if there are no
|
@@ -204,10 +202,8 @@ module Bolt
|
|
204
202
|
# Read any remaining data in the pipe. Do not wait for
|
205
203
|
# EOF in case the pipe is inherited by a child process.
|
206
204
|
read_streams.each do |stream, _|
|
207
|
-
|
208
|
-
|
209
|
-
rescue Errno::EAGAIN, EOFError
|
210
|
-
end
|
205
|
+
loop { read_streams[stream] << stream.read_nonblock(CHUNK_SIZE) }
|
206
|
+
rescue Errno::EAGAIN, EOFError
|
211
207
|
end
|
212
208
|
result_output.stdout << read_streams[out]
|
213
209
|
result_output.stderr << read_streams[err]
|
data/lib/bolt/transport/orch.rb
CHANGED
@@ -56,11 +56,9 @@ module Bolt
|
|
56
56
|
def finish_plan(result)
|
57
57
|
if result.is_a? Bolt::PlanResult
|
58
58
|
@connections.each_value do |conn|
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
@logger.debug("Failed to finish plan on #{conn.key}: #{e.message}")
|
63
|
-
end
|
59
|
+
conn.finish_plan(result)
|
60
|
+
rescue StandardError => e
|
61
|
+
@logger.debug("Failed to finish plan on #{conn.key}: #{e.message}")
|
64
62
|
end
|
65
63
|
end
|
66
64
|
end
|
data/lib/bolt/transport/ssh.rb
CHANGED
@@ -19,6 +19,7 @@ module Bolt
|
|
19
19
|
"`py` both map to a task executable `task.py`) and the extension is case "\
|
20
20
|
"sensitive. When a target's name is `localhost`, Ruby tasks run with the "\
|
21
21
|
"Bolt Ruby interpreter by default.",
|
22
|
+
"load-config" => "Whether to load system SSH configuration.",
|
22
23
|
"password" => "Login password.",
|
23
24
|
"port" => "Connection port.",
|
24
25
|
"private-key" => "Either the path to the private key file to use for authentication, or a "\
|
@@ -34,10 +34,7 @@ module Bolt
|
|
34
34
|
@transport_logger = transport_logger
|
35
35
|
@logger.debug("Initializing ssh connection to #{@target.safe_name}")
|
36
36
|
|
37
|
-
@sudo_password = @target.options['sudo-password']
|
38
|
-
# rubocop:disable Style/GlobalVars
|
39
|
-
@sudo_password ||= @target.password if $future
|
40
|
-
# rubocop:enable Style/GlobalVars
|
37
|
+
@sudo_password = @target.options['sudo-password'] || @target.password
|
41
38
|
|
42
39
|
if target.options['private-key']&.instance_of?(String)
|
43
40
|
begin
|
@@ -276,7 +276,7 @@ module Bolt
|
|
276
276
|
if Dir.exist?(source)
|
277
277
|
tree.open_directory(directory: dest, write: true, disposition: ::RubySMB::Dispositions::FILE_OPEN_IF)
|
278
278
|
|
279
|
-
|
279
|
+
Dir.children(source).each do |child|
|
280
280
|
child_dest = dest + '\\' + child
|
281
281
|
write_remote_file_smb_recursive(tree, File.join(source, child), child_dest)
|
282
282
|
end
|
data/lib/bolt/util.rb
CHANGED
@@ -104,12 +104,6 @@ module Bolt
|
|
104
104
|
hash1.merge(hash2, &recursive_merge)
|
105
105
|
end
|
106
106
|
|
107
|
-
def map_vals(hash)
|
108
|
-
hash.each_with_object({}) do |(k, v), acc|
|
109
|
-
acc[k] = yield(v)
|
110
|
-
end
|
111
|
-
end
|
112
|
-
|
113
107
|
# Accepts a Data object and returns a copy with all hash keys
|
114
108
|
# modified by block. use &:to_s to stringify keys or &:to_sym to symbolize them
|
115
109
|
def walk_keys(data, &block)
|
@@ -131,7 +125,7 @@ module Bolt
|
|
131
125
|
def walk_vals(data, skip_top = false, &block)
|
132
126
|
data = yield(data) unless skip_top
|
133
127
|
if data.is_a? Hash
|
134
|
-
|
128
|
+
data.transform_values { |v| walk_vals(v, &block) }
|
135
129
|
elsif data.is_a? Array
|
136
130
|
data.map { |v| walk_vals(v, &block) }
|
137
131
|
else
|
@@ -144,7 +138,7 @@ module Bolt
|
|
144
138
|
# parents.
|
145
139
|
def postwalk_vals(data, skip_top = false, &block)
|
146
140
|
new_data = if data.is_a? Hash
|
147
|
-
|
141
|
+
data.transform_values { |v| postwalk_vals(v, &block) }
|
148
142
|
elsif data.is_a? Array
|
149
143
|
data.map { |v| postwalk_vals(v, &block) }
|
150
144
|
else
|
data/lib/bolt/version.rb
CHANGED
@@ -186,17 +186,15 @@ module BoltServer
|
|
186
186
|
[400, '`environment` is a required argument']
|
187
187
|
else
|
188
188
|
@pal_mutex.synchronize do
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
[400, e.to_json]
|
199
|
-
end
|
189
|
+
pal = BoltServer::PE::PAL.new({}, environment)
|
190
|
+
yield pal
|
191
|
+
rescue Puppet::Environments::EnvironmentNotFound
|
192
|
+
[400, {
|
193
|
+
"class" => 'bolt/unknown-environment',
|
194
|
+
"message" => "Environment #{environment} not found"
|
195
|
+
}.to_json]
|
196
|
+
rescue Bolt::Error => e
|
197
|
+
[400, e.to_json]
|
200
198
|
end
|
201
199
|
end
|
202
200
|
end
|
@@ -255,14 +253,23 @@ module BoltServer
|
|
255
253
|
'load-config' => false
|
256
254
|
}
|
257
255
|
|
258
|
-
opts = defaults.merge(target_hash
|
256
|
+
opts = defaults.merge(target_hash).merge(overrides)
|
259
257
|
|
260
258
|
if opts['private-key-content']
|
261
259
|
private_key_content = opts.delete('private-key-content')
|
262
260
|
opts['private-key'] = { 'key-data' => private_key_content }
|
263
261
|
end
|
264
262
|
|
265
|
-
|
263
|
+
data = {
|
264
|
+
'uri' => target_hash['hostname'],
|
265
|
+
'config' => {
|
266
|
+
'transport' => 'ssh',
|
267
|
+
'ssh' => opts
|
268
|
+
}
|
269
|
+
}
|
270
|
+
|
271
|
+
inventory = Bolt::Inventory.empty
|
272
|
+
Bolt::Target.from_hash(data, inventory)
|
266
273
|
end
|
267
274
|
|
268
275
|
post '/ssh/:action' do
|
@@ -286,12 +293,23 @@ module BoltServer
|
|
286
293
|
end
|
287
294
|
|
288
295
|
def make_winrm_target(target_hash)
|
289
|
-
|
290
|
-
'
|
296
|
+
defaults = {
|
297
|
+
'ssl' => false,
|
298
|
+
'ssl-verify' => false
|
299
|
+
}
|
300
|
+
|
301
|
+
opts = defaults.merge(target_hash)
|
302
|
+
|
303
|
+
data = {
|
304
|
+
'uri' => target_hash['hostname'],
|
305
|
+
'config' => {
|
306
|
+
'transport' => 'winrm',
|
307
|
+
'winrm' => opts
|
308
|
+
}
|
291
309
|
}
|
292
310
|
|
293
|
-
|
294
|
-
Bolt::Target.
|
311
|
+
inventory = Bolt::Inventory.empty
|
312
|
+
Bolt::Target.from_hash(data, inventory)
|
295
313
|
end
|
296
314
|
|
297
315
|
post '/winrm/:action' do
|
data/lib/bolt_spec/plans.rb
CHANGED
@@ -185,10 +185,16 @@ module BoltSpec
|
|
185
185
|
end
|
186
186
|
|
187
187
|
# Provided as a class so expectations can be placed on it.
|
188
|
-
class MockPuppetDBClient
|
188
|
+
class MockPuppetDBClient
|
189
|
+
attr_reader :config
|
190
|
+
|
191
|
+
def initialize(config)
|
192
|
+
@config = config
|
193
|
+
end
|
194
|
+
end
|
189
195
|
|
190
196
|
def puppetdb_client
|
191
|
-
@puppetdb_client ||= MockPuppetDBClient.new
|
197
|
+
@puppetdb_client ||= MockPuppetDBClient.new(Bolt::PuppetDB::Config.new({}))
|
192
198
|
end
|
193
199
|
|
194
200
|
def pal
|
data/libexec/bolt_catalog
CHANGED
@@ -41,12 +41,26 @@ elsif command == "compile"
|
|
41
41
|
else
|
42
42
|
JSON.parse(STDIN.read)
|
43
43
|
end
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
catalog
|
44
|
+
begin
|
45
|
+
catalog = Bolt::Catalog.new.compile_catalog(request)
|
46
|
+
# This seems to be a string in ruby 2.3
|
47
|
+
if catalog.is_a?(String)
|
48
|
+
catalog = JSON.parse(catalog)
|
49
|
+
end
|
50
|
+
puts JSON.pretty_generate(catalog)
|
51
|
+
rescue Puppet::PreformattedError => e
|
52
|
+
message = if e.cause
|
53
|
+
location_info = Puppet::Util::Errors.error_location_with_space(e.file, e.line, e.pos)
|
54
|
+
"#{e.cause.message}#{location_info}"
|
55
|
+
else
|
56
|
+
e.message
|
57
|
+
end
|
58
|
+
puts({ message: message }.to_json)
|
59
|
+
exit 1
|
60
|
+
rescue StandardError => e
|
61
|
+
puts({ message: e.message }.to_json)
|
62
|
+
exit 1
|
48
63
|
end
|
49
|
-
puts JSON.pretty_generate(catalog)
|
50
64
|
else
|
51
65
|
puts "USAGE: run 'bolt_catalog compile' with a request on STDIN " \
|
52
66
|
"or 'bolt_catalog parse manifest.pp' to generate JSON AST"
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: bolt
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version:
|
4
|
+
version: 2.0.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Puppet
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2020-02-
|
11
|
+
date: 2020-02-19 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: addressable
|
@@ -343,7 +343,6 @@ email:
|
|
343
343
|
- puppet@puppet.com
|
344
344
|
executables:
|
345
345
|
- bolt
|
346
|
-
- bolt-inventory-pdb
|
347
346
|
extensions: []
|
348
347
|
extra_rdoc_files: []
|
349
348
|
files:
|
@@ -388,7 +387,6 @@ files:
|
|
388
387
|
- bolt-modules/out/lib/puppet/functions/out/message.rb
|
389
388
|
- bolt-modules/system/lib/puppet/functions/system/env.rb
|
390
389
|
- exe/bolt
|
391
|
-
- exe/bolt-inventory-pdb
|
392
390
|
- lib/bolt.rb
|
393
391
|
- lib/bolt/analytics.rb
|
394
392
|
- lib/bolt/applicator.rb
|
@@ -405,8 +403,7 @@ files:
|
|
405
403
|
- lib/bolt/executor.rb
|
406
404
|
- lib/bolt/inventory.rb
|
407
405
|
- lib/bolt/inventory/group.rb
|
408
|
-
- lib/bolt/inventory/
|
409
|
-
- lib/bolt/inventory/inventory2.rb
|
406
|
+
- lib/bolt/inventory/inventory.rb
|
410
407
|
- lib/bolt/inventory/target.rb
|
411
408
|
- lib/bolt/logger.rb
|
412
409
|
- lib/bolt/module.rb
|
@@ -473,7 +470,6 @@ files:
|
|
473
470
|
- lib/bolt/util.rb
|
474
471
|
- lib/bolt/util/puppet_log_level.rb
|
475
472
|
- lib/bolt/version.rb
|
476
|
-
- lib/bolt_ext/puppetdb_inventory.rb
|
477
473
|
- lib/bolt_server/acl.rb
|
478
474
|
- lib/bolt_server/base_config.rb
|
479
475
|
- lib/bolt_server/config.rb
|
@@ -528,7 +524,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
528
524
|
requirements:
|
529
525
|
- - "~>"
|
530
526
|
- !ruby/object:Gem::Version
|
531
|
-
version: '2.
|
527
|
+
version: '2.5'
|
532
528
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
533
529
|
requirements:
|
534
530
|
- - ">="
|
data/exe/bolt-inventory-pdb
DELETED
@@ -1,403 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require 'bolt/inventory/group'
|
4
|
-
require 'bolt/inventory/inventory2'
|
5
|
-
require 'bolt/inventory/target'
|
6
|
-
|
7
|
-
module Bolt
|
8
|
-
class Inventory
|
9
|
-
class Group2
|
10
|
-
attr_accessor :name, :groups
|
11
|
-
|
12
|
-
# Regex used to validate group names and target aliases.
|
13
|
-
NAME_REGEX = /\A[a-z0-9_][a-z0-9_-]*\Z/.freeze
|
14
|
-
|
15
|
-
DATA_KEYS = %w[config facts vars features plugin_hooks].freeze
|
16
|
-
TARGET_KEYS = DATA_KEYS + %w[name alias uri]
|
17
|
-
GROUP_KEYS = DATA_KEYS + %w[name groups targets]
|
18
|
-
CONFIG_KEYS = Bolt::TRANSPORTS.keys.map(&:to_s) + ['transport']
|
19
|
-
|
20
|
-
def initialize(input, plugins)
|
21
|
-
@logger = Logging.logger[self]
|
22
|
-
@plugins = plugins
|
23
|
-
|
24
|
-
input = @plugins.resolve_top_level_references(input) if @plugins.reference?(input)
|
25
|
-
|
26
|
-
raise ValidationError.new("Group does not have a name", nil) unless input.key?('name')
|
27
|
-
|
28
|
-
@name = @plugins.resolve_references(input['name'])
|
29
|
-
|
30
|
-
raise ValidationError.new("Group name must be a String, not #{@name.inspect}", nil) unless @name.is_a?(String)
|
31
|
-
raise ValidationError.new("Invalid group name #{@name}", @name) unless @name =~ NAME_REGEX
|
32
|
-
|
33
|
-
validate_group_input(input)
|
34
|
-
|
35
|
-
@input = input
|
36
|
-
|
37
|
-
validate_data_keys(@input)
|
38
|
-
|
39
|
-
targets = @plugins.resolve_top_level_references(input.fetch('targets', []))
|
40
|
-
|
41
|
-
@unresolved_targets = {}
|
42
|
-
@resolved_targets = {}
|
43
|
-
|
44
|
-
@aliases = {}
|
45
|
-
@string_targets = []
|
46
|
-
|
47
|
-
Array(targets).each do |target|
|
48
|
-
# If target is a string, it can either be trivially defining a target
|
49
|
-
# or it could be a name/alias of a target defined in another group.
|
50
|
-
# We can't tell the difference until all groups have been resolved,
|
51
|
-
# so we store the string on its own here and process it later.
|
52
|
-
if target.is_a?(String)
|
53
|
-
@string_targets << target
|
54
|
-
# Handle plugins at this level so that lookups cannot trigger recursive lookups
|
55
|
-
elsif target.is_a?(Hash)
|
56
|
-
add_target_definition(target)
|
57
|
-
else
|
58
|
-
raise ValidationError.new("Target entry must be a String or Hash, not #{target.class}", @name)
|
59
|
-
end
|
60
|
-
end
|
61
|
-
|
62
|
-
groups = input.fetch('groups', [])
|
63
|
-
# 'groups' can be a _plugin reference, in which case we want to resolve
|
64
|
-
# it. That can itself return a reference, so we want to keep resolving
|
65
|
-
# them until we have a value. We don't just use resolve_references
|
66
|
-
# though, since that will resolve any nested references and we want to
|
67
|
-
# leave it to the group to do that lazily.
|
68
|
-
groups = @plugins.resolve_top_level_references(groups)
|
69
|
-
|
70
|
-
@groups = Array(groups).map { |g| Group2.new(g, plugins) }
|
71
|
-
end
|
72
|
-
|
73
|
-
def target_data(target_name)
|
74
|
-
if @unresolved_targets.key?(target_name)
|
75
|
-
target = @unresolved_targets.delete(target_name)
|
76
|
-
resolved_data = resolve_data_keys(target, target_name).merge(
|
77
|
-
'name' => target['name'],
|
78
|
-
'uri' => target['uri'],
|
79
|
-
'alias' => target['alias'],
|
80
|
-
# groups come from group_data
|
81
|
-
'groups' => []
|
82
|
-
)
|
83
|
-
@resolved_targets[target_name] = resolved_data
|
84
|
-
else
|
85
|
-
@resolved_targets[target_name]
|
86
|
-
end
|
87
|
-
end
|
88
|
-
|
89
|
-
def all_target_names
|
90
|
-
@unresolved_targets.keys + @resolved_targets.keys
|
91
|
-
end
|
92
|
-
|
93
|
-
def add_target_definition(target)
|
94
|
-
# This check ensures target lookup plugins do not returns bare strings.
|
95
|
-
# Remove it if we decide to allows task plugins to return string Target
|
96
|
-
# names.
|
97
|
-
unless target.is_a?(Hash)
|
98
|
-
raise ValidationError.new("Target entry must be a Hash, not #{target.class}", @name)
|
99
|
-
end
|
100
|
-
|
101
|
-
target['name'] = @plugins.resolve_references(target['name']) if target.key?('name')
|
102
|
-
target['uri'] = @plugins.resolve_references(target['uri']) if target.key?('uri')
|
103
|
-
target['alias'] = @plugins.resolve_references(target['alias']) if target.key?('alias')
|
104
|
-
|
105
|
-
t_name = target['name'] || target['uri']
|
106
|
-
|
107
|
-
if t_name.nil? || t_name.empty?
|
108
|
-
raise ValidationError.new("No name or uri for target: #{target}", @name)
|
109
|
-
end
|
110
|
-
|
111
|
-
unless t_name.is_a? String
|
112
|
-
raise ValidationError.new("Target name must be a String, not #{t_name.class}", @name)
|
113
|
-
end
|
114
|
-
|
115
|
-
unless t_name.ascii_only?
|
116
|
-
raise ValidationError.new("Target name must be ASCII characters: #{target}", @name)
|
117
|
-
end
|
118
|
-
|
119
|
-
if local_targets.include?(t_name)
|
120
|
-
@logger.warn("Ignoring duplicate target in #{@name}: #{target}")
|
121
|
-
return
|
122
|
-
end
|
123
|
-
|
124
|
-
unless (unexpected_keys = target.keys - TARGET_KEYS).empty?
|
125
|
-
msg = "Found unexpected key(s) #{unexpected_keys.join(', ')} in target #{t_name}"
|
126
|
-
@logger.warn(msg)
|
127
|
-
end
|
128
|
-
|
129
|
-
validate_data_keys(target, t_name)
|
130
|
-
|
131
|
-
if target.include?('alias')
|
132
|
-
aliases = target['alias']
|
133
|
-
aliases = [aliases] if aliases.is_a?(String)
|
134
|
-
unless aliases.is_a?(Array)
|
135
|
-
msg = "Alias entry on #{t_name} must be a String or Array, not #{aliases.class}"
|
136
|
-
raise ValidationError.new(msg, @name)
|
137
|
-
end
|
138
|
-
|
139
|
-
insert_alia(t_name, aliases)
|
140
|
-
end
|
141
|
-
|
142
|
-
@unresolved_targets[t_name] = target
|
143
|
-
end
|
144
|
-
|
145
|
-
def remove_target(target)
|
146
|
-
@resolved_targets.delete(target.name)
|
147
|
-
@unresolved_targets.delete(target.name)
|
148
|
-
end
|
149
|
-
|
150
|
-
def add_target(target)
|
151
|
-
@resolved_targets[target.name] = { 'name' => target.name }
|
152
|
-
end
|
153
|
-
|
154
|
-
def insert_alia(target_name, aliases)
|
155
|
-
aliases.each do |alia|
|
156
|
-
raise ValidationError.new("Invalid alias #{alia}", @name) unless alia =~ NAME_REGEX
|
157
|
-
|
158
|
-
if (found = @aliases[alia])
|
159
|
-
raise ValidationError.new(alias_conflict(alia, found, target_name), @name)
|
160
|
-
end
|
161
|
-
@aliases[alia] = target_name
|
162
|
-
end
|
163
|
-
end
|
164
|
-
|
165
|
-
def clear_alia(target_name)
|
166
|
-
@aliases.reject! { |_alias, name| name == target_name }
|
167
|
-
end
|
168
|
-
|
169
|
-
def data_merge(data1, data2)
|
170
|
-
if data2.nil? || data1.nil?
|
171
|
-
return data2 || data1
|
172
|
-
end
|
173
|
-
|
174
|
-
{
|
175
|
-
'config' => Bolt::Util.deep_merge(data1['config'], data2['config']),
|
176
|
-
'name' => data1['name'] || data2['name'],
|
177
|
-
'uri' => data1['uri'] || data2['uri'],
|
178
|
-
# Collect all aliases across all groups for each target uri
|
179
|
-
'alias' => [*data1['alias'], *data2['alias']],
|
180
|
-
# Shallow merge instead of deep merge so that vars with a hash value
|
181
|
-
# are assigned a new hash, rather than merging the existing value
|
182
|
-
# with the value meant to replace it
|
183
|
-
'vars' => data1['vars'].merge(data2['vars']),
|
184
|
-
'facts' => Bolt::Util.deep_merge(data1['facts'], data2['facts']),
|
185
|
-
'features' => data1['features'] | data2['features'],
|
186
|
-
'plugin_hooks' => data1['plugin_hooks'].merge(data2['plugin_hooks']),
|
187
|
-
'groups' => data2['groups'] + data1['groups']
|
188
|
-
}
|
189
|
-
end
|
190
|
-
|
191
|
-
def resolve_string_targets(aliases, known_targets)
|
192
|
-
@string_targets.each do |string_target|
|
193
|
-
# If this is the name of a target defined elsewhere, then insert the
|
194
|
-
# target into this group as just a name. Otherwise, add a new target
|
195
|
-
# with the string as the URI.
|
196
|
-
if known_targets.include?(string_target)
|
197
|
-
@unresolved_targets[string_target] = { 'name' => string_target }
|
198
|
-
# If this is an alias for an existing target, then add it to this group
|
199
|
-
elsif (canonical_name = aliases[string_target])
|
200
|
-
if local_targets.include?(canonical_name)
|
201
|
-
@logger.warn("Ignoring duplicate target in #{@name}: #{canonical_name}")
|
202
|
-
else
|
203
|
-
@unresolved_targets[canonical_name] = { 'name' => canonical_name }
|
204
|
-
end
|
205
|
-
# If it's not the name or alias of an existing target, then make a
|
206
|
-
# new target using the string as the URI
|
207
|
-
elsif local_targets.include?(string_target)
|
208
|
-
@logger.warn("Ignoring duplicate target in #{@name}: #{string_target}")
|
209
|
-
else
|
210
|
-
@unresolved_targets[string_target] = { 'uri' => string_target }
|
211
|
-
end
|
212
|
-
end
|
213
|
-
|
214
|
-
@groups.each { |g| g.resolve_string_targets(aliases, known_targets) }
|
215
|
-
end
|
216
|
-
|
217
|
-
private def alias_conflict(name, target1, target2)
|
218
|
-
"Alias #{name} refers to multiple targets: #{target1} and #{target2}"
|
219
|
-
end
|
220
|
-
|
221
|
-
private def group_alias_conflict(name)
|
222
|
-
"Group #{name} conflicts with alias of the same name"
|
223
|
-
end
|
224
|
-
|
225
|
-
private def group_target_conflict(name)
|
226
|
-
"Group #{name} conflicts with target of the same name"
|
227
|
-
end
|
228
|
-
|
229
|
-
private def alias_target_conflict(name)
|
230
|
-
"Target name #{name} conflicts with alias of the same name"
|
231
|
-
end
|
232
|
-
|
233
|
-
def validate_group_input(input)
|
234
|
-
raise ValidationError.new("Expected group to be a Hash, not #{input.class}", nil) unless input.is_a?(Hash)
|
235
|
-
|
236
|
-
# DEPRECATION : remove this before finalization
|
237
|
-
if input.key?('target-lookups')
|
238
|
-
msg = "'target-lookups' are no longer a separate key. Merge 'target-lookups' and 'targets' lists and replace 'plugin' with '_plugin'" # rubocop:disable Layout/LineLength
|
239
|
-
raise ValidationError.new(msg, @name)
|
240
|
-
end
|
241
|
-
|
242
|
-
unless (unexpected_keys = input.keys - GROUP_KEYS).empty?
|
243
|
-
msg = "Found unexpected key(s) #{unexpected_keys.join(', ')} in group #{@name}"
|
244
|
-
@logger.warn(msg)
|
245
|
-
end
|
246
|
-
|
247
|
-
Bolt::Util.walk_keys(input) do |key|
|
248
|
-
if @plugins.reference?(key)
|
249
|
-
raise ValidationError.new("Group keys cannot be specified as _plugin references", @name)
|
250
|
-
else
|
251
|
-
key
|
252
|
-
end
|
253
|
-
end
|
254
|
-
end
|
255
|
-
|
256
|
-
def validate(used_group_names = Set.new, used_target_names = Set.new, used_aliases = {})
|
257
|
-
# Test if this group name conflicts with anything used before.
|
258
|
-
raise ValidationError.new("Tried to redefine group #{@name}", @name) if used_group_names.include?(@name)
|
259
|
-
raise ValidationError.new(group_target_conflict(@name), @name) if used_target_names.include?(@name)
|
260
|
-
raise ValidationError.new(group_alias_conflict(@name), @name) if used_aliases.include?(@name)
|
261
|
-
|
262
|
-
used_group_names << @name
|
263
|
-
|
264
|
-
# Collect target names and aliases into a list used to validate that subgroups don't conflict.
|
265
|
-
# Used names validate that previously used group names don't conflict with new target names/aliases.
|
266
|
-
@unresolved_targets.merge(@resolved_targets).each do |t_name, t_data|
|
267
|
-
# Require targets to be parseable as a Target.
|
268
|
-
begin
|
269
|
-
# Catch malformed URI here
|
270
|
-
Bolt::Inventory::Target.parse_uri(t_data['uri'])
|
271
|
-
rescue Bolt::ParseError => e
|
272
|
-
@logger.debug(e)
|
273
|
-
raise ValidationError.new("Invalid target uri #{t_data['uri']}", @name)
|
274
|
-
end
|
275
|
-
|
276
|
-
raise ValidationError.new(group_target_conflict(t_name), @name) if used_group_names.include?(t_name)
|
277
|
-
if used_aliases.include?(t_name)
|
278
|
-
raise ValidationError.new(alias_target_conflict(t_name), @name)
|
279
|
-
end
|
280
|
-
|
281
|
-
used_target_names << t_name
|
282
|
-
end
|
283
|
-
|
284
|
-
@aliases.each do |n, target|
|
285
|
-
raise ValidationError.new(group_alias_conflict(n), @name) if used_group_names.include?(n)
|
286
|
-
if used_target_names.include?(n)
|
287
|
-
raise ValidationError.new(alias_target_conflict(n), @name)
|
288
|
-
end
|
289
|
-
|
290
|
-
if used_aliases.include?(n)
|
291
|
-
raise ValidationError.new(alias_conflict(n, target, used_aliases[n]), @name)
|
292
|
-
end
|
293
|
-
|
294
|
-
used_aliases[n] = target
|
295
|
-
end
|
296
|
-
|
297
|
-
@groups.each do |g|
|
298
|
-
begin
|
299
|
-
g.validate(used_group_names, used_target_names, used_aliases)
|
300
|
-
rescue ValidationError => e
|
301
|
-
e.add_parent(@name)
|
302
|
-
raise e
|
303
|
-
end
|
304
|
-
end
|
305
|
-
|
306
|
-
nil
|
307
|
-
end
|
308
|
-
|
309
|
-
def resolve_data_keys(data, target = nil)
|
310
|
-
result = {
|
311
|
-
'config' => @plugins.resolve_references(data.fetch('config', {})),
|
312
|
-
'vars' => @plugins.resolve_references(data.fetch('vars', {})),
|
313
|
-
'facts' => @plugins.resolve_references(data.fetch('facts', {})),
|
314
|
-
'features' => @plugins.resolve_references(data.fetch('features', [])),
|
315
|
-
'plugin_hooks' => @plugins.resolve_references(data.fetch('plugin_hooks', {}))
|
316
|
-
}
|
317
|
-
validate_data_keys(result, target)
|
318
|
-
result['features'] = Set.new(result['features'].flatten)
|
319
|
-
result
|
320
|
-
end
|
321
|
-
|
322
|
-
def validate_data_keys(data, target = nil)
|
323
|
-
{
|
324
|
-
'config' => Hash,
|
325
|
-
'vars' => Hash,
|
326
|
-
'facts' => Hash,
|
327
|
-
'features' => Array,
|
328
|
-
'plugin_hooks' => Hash
|
329
|
-
}.each do |key, expected_type|
|
330
|
-
next if !data.key?(key) || data[key].is_a?(expected_type) || @plugins.reference?(data[key])
|
331
|
-
|
332
|
-
msg = +"Expected #{key} to be of type #{expected_type}, not #{data[key].class}"
|
333
|
-
msg << " for target #{target}" if target
|
334
|
-
raise ValidationError.new(msg, @name)
|
335
|
-
end
|
336
|
-
unless @plugins.reference?(data['config'])
|
337
|
-
unexpected_keys = data.fetch('config', {}).keys - CONFIG_KEYS
|
338
|
-
if unexpected_keys.any?
|
339
|
-
msg = +"Found unexpected key(s) #{unexpected_keys.join(', ')} in config for"
|
340
|
-
msg << " target #{target} in" if target
|
341
|
-
msg << " group #{@name}"
|
342
|
-
@logger.warn(msg)
|
343
|
-
end
|
344
|
-
end
|
345
|
-
end
|
346
|
-
|
347
|
-
def group_data
|
348
|
-
@group_data ||= resolve_data_keys(@input).merge('groups' => [@name])
|
349
|
-
end
|
350
|
-
|
351
|
-
# Returns targets contained directly within the group, ignoring subgroups
|
352
|
-
def local_targets
|
353
|
-
Set.new(@unresolved_targets.keys) + Set.new(@resolved_targets.keys)
|
354
|
-
end
|
355
|
-
|
356
|
-
# Returns all targets contained within the group, which includes targets from subgroups.
|
357
|
-
def all_targets
|
358
|
-
@groups.inject(local_targets) do |acc, g|
|
359
|
-
acc.merge(g.all_targets)
|
360
|
-
end
|
361
|
-
end
|
362
|
-
|
363
|
-
# Returns a mapping of aliases to targets contained within the group, which includes subgroups.
|
364
|
-
def target_aliases
|
365
|
-
@groups.inject(@aliases) do |acc, g|
|
366
|
-
acc.merge(g.target_aliases)
|
367
|
-
end
|
368
|
-
end
|
369
|
-
|
370
|
-
# Return a mapping of group names to group.
|
371
|
-
def collect_groups
|
372
|
-
@groups.inject(name => self) do |acc, g|
|
373
|
-
acc.merge(g.collect_groups)
|
374
|
-
end
|
375
|
-
end
|
376
|
-
|
377
|
-
def target_collect(target_name)
|
378
|
-
child_data = @groups.map { |group| group.target_collect(target_name) }
|
379
|
-
# Data from earlier groups wins
|
380
|
-
child_result = child_data.inject do |acc, group_data|
|
381
|
-
data_merge(group_data, acc)
|
382
|
-
end
|
383
|
-
# Children override the parent
|
384
|
-
data_merge(target_data(target_name), child_result)
|
385
|
-
end
|
386
|
-
|
387
|
-
def group_collect(target_name)
|
388
|
-
child_data = @groups.map { |group| group.group_collect(target_name) }
|
389
|
-
# Data from earlier groups wins
|
390
|
-
child_result = child_data.inject do |acc, group_data|
|
391
|
-
data_merge(group_data, acc)
|
392
|
-
end
|
393
|
-
|
394
|
-
# If this group has the target or one of the child groups has the
|
395
|
-
# target, return the data, otherwise return nil
|
396
|
-
if child_result || local_targets.include?(target_name)
|
397
|
-
# Children override the parent
|
398
|
-
data_merge(group_data, child_result)
|
399
|
-
end
|
400
|
-
end
|
401
|
-
end
|
402
|
-
end
|
403
|
-
end
|