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.

Files changed (70) hide show
  1. checksums.yaml +4 -4
  2. data/Puppetfile +6 -6
  3. data/bolt-modules/boltlib/lib/puppet/datatypes/target.rb +24 -45
  4. data/bolt-modules/boltlib/lib/puppet/functions/add_facts.rb +3 -3
  5. data/bolt-modules/boltlib/lib/puppet/functions/add_to_group.rb +1 -1
  6. data/bolt-modules/boltlib/lib/puppet/functions/apply_prep.rb +10 -12
  7. data/bolt-modules/boltlib/lib/puppet/functions/catch_errors.rb +1 -1
  8. data/bolt-modules/boltlib/lib/puppet/functions/fail_plan.rb +3 -3
  9. data/bolt-modules/boltlib/lib/puppet/functions/get_resources.rb +5 -4
  10. data/bolt-modules/boltlib/lib/puppet/functions/get_target.rb +1 -3
  11. data/bolt-modules/boltlib/lib/puppet/functions/get_targets.rb +1 -2
  12. data/bolt-modules/boltlib/lib/puppet/functions/puppetdb_fact.rb +2 -2
  13. data/bolt-modules/boltlib/lib/puppet/functions/remove_from_group.rb +2 -2
  14. data/bolt-modules/boltlib/lib/puppet/functions/resolve_references.rb +1 -1
  15. data/bolt-modules/boltlib/lib/puppet/functions/run_command.rb +7 -3
  16. data/bolt-modules/boltlib/lib/puppet/functions/run_plan.rb +15 -31
  17. data/bolt-modules/boltlib/lib/puppet/functions/run_script.rb +9 -5
  18. data/bolt-modules/boltlib/lib/puppet/functions/run_task.rb +9 -3
  19. data/bolt-modules/boltlib/lib/puppet/functions/set_config.rb +4 -3
  20. data/bolt-modules/boltlib/lib/puppet/functions/set_feature.rb +6 -6
  21. data/bolt-modules/boltlib/lib/puppet/functions/set_var.rb +2 -2
  22. data/bolt-modules/boltlib/lib/puppet/functions/upload_file.rb +7 -3
  23. data/bolt-modules/boltlib/lib/puppet/functions/wait_until_available.rb +6 -2
  24. data/bolt-modules/boltlib/lib/puppet/functions/without_default_logging.rb +2 -2
  25. data/bolt-modules/ctrl/lib/puppet/functions/ctrl/do_until.rb +2 -1
  26. data/bolt-modules/file/lib/puppet/functions/file/exists.rb +2 -1
  27. data/bolt-modules/file/lib/puppet/functions/file/join.rb +1 -0
  28. data/bolt-modules/file/lib/puppet/functions/file/read.rb +1 -0
  29. data/bolt-modules/file/lib/puppet/functions/file/readable.rb +2 -1
  30. data/bolt-modules/out/lib/puppet/functions/out/message.rb +1 -1
  31. data/bolt-modules/system/lib/puppet/functions/system/env.rb +1 -0
  32. data/lib/bolt/applicator.rb +70 -118
  33. data/lib/bolt/apply_target.rb +1 -1
  34. data/lib/bolt/bolt_option_parser.rb +21 -37
  35. data/lib/bolt/catalog.rb +5 -22
  36. data/lib/bolt/catalog/logging.rb +1 -1
  37. data/lib/bolt/cli.rb +33 -44
  38. data/lib/bolt/config.rb +15 -18
  39. data/lib/bolt/error.rb +2 -2
  40. data/lib/bolt/executor.rb +32 -40
  41. data/lib/bolt/inventory.rb +9 -367
  42. data/lib/bolt/inventory/group.rb +293 -182
  43. data/lib/bolt/inventory/{inventory2.rb → inventory.rb} +25 -14
  44. data/lib/bolt/inventory/target.rb +1 -1
  45. data/lib/bolt/module.rb +4 -4
  46. data/lib/bolt/outputter/human.rb +11 -6
  47. data/lib/bolt/outputter/json.rb +3 -11
  48. data/lib/bolt/pal.rb +1 -2
  49. data/lib/bolt/pal/yaml_plan/step/resources.rb +1 -1
  50. data/lib/bolt/plugin.rb +1 -1
  51. data/lib/bolt/plugin/module.rb +19 -36
  52. data/lib/bolt/plugin/prompt.rb +2 -4
  53. data/lib/bolt/puppetdb/config.rb +1 -3
  54. data/lib/bolt/result.rb +3 -6
  55. data/lib/bolt/secret/base.rb +0 -6
  56. data/lib/bolt/target.rb +8 -219
  57. data/lib/bolt/transport/local/shell.rb +9 -13
  58. data/lib/bolt/transport/orch.rb +3 -5
  59. data/lib/bolt/transport/ssh.rb +1 -0
  60. data/lib/bolt/transport/ssh/connection.rb +1 -4
  61. data/lib/bolt/transport/winrm/connection.rb +1 -1
  62. data/lib/bolt/util.rb +2 -8
  63. data/lib/bolt/version.rb +1 -1
  64. data/lib/bolt_server/transport_app.rb +35 -17
  65. data/lib/bolt_spec/plans.rb +8 -2
  66. data/libexec/bolt_catalog +19 -5
  67. metadata +4 -8
  68. data/exe/bolt-inventory-pdb +0 -13
  69. data/lib/bolt/inventory/group2.rb +0 -403
  70. 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
- begin
168
- # Check for sudo prompt
169
- read_streams[stream] << if use_sudo
170
- check_sudo(stream, inp, t.pid, options[:stdin])
171
- else
172
- stream.readpartial(CHUNK_SIZE)
173
- end
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
- begin
208
- loop { read_streams[stream] << stream.read_nonblock(CHUNK_SIZE) }
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]
@@ -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
- begin
60
- conn.finish_plan(result)
61
- rescue StandardError => e
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
@@ -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
- (Dir.entries(source) - ['.', '..']).each do |child|
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
@@ -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
- map_vals(data) { |v| walk_vals(v, &block) }
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
- map_vals(data) { |v| postwalk_vals(v, &block) }
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Bolt
4
- VERSION = '1.49.0'
4
+ VERSION = '2.0.0'
5
5
  end
@@ -186,17 +186,15 @@ module BoltServer
186
186
  [400, '`environment` is a required argument']
187
187
  else
188
188
  @pal_mutex.synchronize do
189
- begin
190
- pal = BoltServer::PE::PAL.new({}, environment)
191
- yield pal
192
- rescue Puppet::Environments::EnvironmentNotFound
193
- [400, {
194
- "class" => 'bolt/unknown-environment',
195
- "message" => "Environment #{environment} not found"
196
- }.to_json]
197
- rescue Bolt::Error => e
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.clone).merge(overrides)
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
- Bolt::Target.new(target_hash['hostname'], opts)
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
- overrides = {
290
- 'protocol' => 'winrm'
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
- opts = target_hash.clone.merge(overrides)
294
- Bolt::Target.new(target_hash['hostname'], opts)
311
+ inventory = Bolt::Inventory.empty
312
+ Bolt::Target.from_hash(data, inventory)
295
313
  end
296
314
 
297
315
  post '/winrm/:action' do
@@ -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; end
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
@@ -41,12 +41,26 @@ elsif command == "compile"
41
41
  else
42
42
  JSON.parse(STDIN.read)
43
43
  end
44
- catalog = Bolt::Catalog.new.compile_catalog(request)
45
- # This seems to be a string in ruby 2.3
46
- if catalog.is_a?(String)
47
- catalog = JSON.parse(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: 1.49.0
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-10 00:00:00.000000000 Z
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/group2.rb
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.3'
527
+ version: '2.5'
532
528
  required_rubygems_version: !ruby/object:Gem::Requirement
533
529
  requirements:
534
530
  - - ">="
@@ -1,13 +0,0 @@
1
- #!/usr/bin/env ruby
2
- # frozen_string_literal: true
3
-
4
- require 'bolt_ext/puppetdb_inventory'
5
-
6
- begin
7
- Bolt::PuppetDBInventory::CLI.new(ARGV).run
8
- exit 0
9
- rescue StandardError => e
10
- warn "Error: #{e}"
11
- warn e.backtrace if @trace
12
- exit 1
13
- end
@@ -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