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.

checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f18043cfb3f91f5ce7fa45d339bc9aba13132d09592516f6ce02af8557bba712
4
- data.tar.gz: f8814932c524a3e753b775f719894950a1e57f93db9287b29dc0b966e9f70bf9
3
+ metadata.gz: 40fdd97c4150459a56f711d9ece1e1c0b57ff19c4e64ddb88459ea947bf4bbab
4
+ data.tar.gz: d2079c900a66e50d0bd9cccd17da37ab965686c6b6c9402b262f148ba2549c44
5
5
  SHA512:
6
- metadata.gz: 6cbc17086c5324dd743bfe073b3c7ebbba25e82fc3fe96af819aee84aa27d3b81d3a6ec2b1535816932a4717f5ca5c0aa792dfa80c9ce76f1cda8d10ae3edac9
7
- data.tar.gz: e8491955503d7e3b2a0c83faeea77b7fb85393fae307c644f62761116d07048b2396007e0b873ac6e98a82d6da3666a4e3c0f7413a75014560df56c98e236238
6
+ metadata.gz: bd59adfaa8bb6178de571eef84a08f124666ca94abb9a2a0550127ff105dc7c55e47ba789f9d09c1eae33a517dd4baaf95fce21543dc77bdedfb35c9472ffe88
7
+ data.tar.gz: 7f80fcb3d13a832338ab4da986d2ac25bdfbf67785f3fd863692cbd4eb018a30e7a687020c8d7c551ec1f58da9abefadbcd3976820c3d83a3c1cdfd9efb79f47
@@ -1,22 +1,55 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  Puppet::DataTypes.create_type('Target') do
4
- interface <<-PUPPET
5
- attributes => {
6
- uri => String[1],
7
- options => { type => Hash[String[1], Data], value => {} }
8
- },
9
- functions => {
10
- name => Callable[[], String[1]],
11
- host => Callable[[], Optional[String]],
12
- password => Callable[[], Optional[String[1]]],
13
- port => Callable[[], Optional[Integer]],
14
- protocol => Callable[[], Optional[String[1]]],
15
- user => Callable[[], Optional[String[1]]],
16
- }
17
- PUPPET
18
-
4
+ begin
5
+ inventory = Puppet.lookup(:bolt_inventory)
6
+ inventory_version = inventory.version
7
+ if inventory_version != 1
8
+ target_implementation_class = inventory.target_implementation_class
9
+ end
10
+ rescue Puppet::Context::UndefinedBindingError
11
+ inventory_version = 1
12
+ end
19
13
  load_file('bolt/target')
20
14
 
21
- implementation_class Bolt::Target
15
+ if inventory_version == 1
16
+ interface <<-PUPPET
17
+ attributes => {
18
+ uri => String[1],
19
+ options => { type => Hash[String[1], Data], value => {} }
20
+ },
21
+ functions => {
22
+ name => Callable[[], String[1]],
23
+ host => Callable[[], Optional[String]],
24
+ password => Callable[[], Optional[String[1]]],
25
+ port => Callable[[], Optional[Integer]],
26
+ protocol => Callable[[], Optional[String[1]]],
27
+ user => Callable[[], Optional[String[1]]],
28
+ }
29
+ PUPPET
30
+ implementation_class Bolt::Target
31
+ else
32
+ interface <<-PUPPET
33
+ attributes => {
34
+ uri => { type => Optional[String[1]], kind => given_or_derived },
35
+ name => { type => Optional[String[1]] , kind => given_or_derived },
36
+ target_alias => { type => Optional[Variant[String[1], Array[String[1]]]], kind => given_or_derived },
37
+ config => { type => Optional[Hash[String[1], Data]], kind => given_or_derived },
38
+ vars => { type => Optional[Hash[String[1], Data]], kind => given_or_derived },
39
+ facts => { type => Optional[Hash[String[1], Data]], kind => given_or_derived },
40
+ features => { type => Optional[Array[String[1]]], kind => given_or_derived },
41
+ plugin_hooks => { type => Optional[Hash[String[1], Data]], kind => given_or_derived }
42
+ },
43
+ functions => {
44
+ safe_name => Callable[[], String[1]],
45
+ host => Callable[[], Optional[String]],
46
+ password => Callable[[], Optional[String[1]]],
47
+ port => Callable[[], Optional[Integer]],
48
+ protocol => Callable[[], Optional[String[1]]],
49
+ user => Callable[[], Optional[String[1]]],
50
+ }
51
+ PUPPET
52
+
53
+ implementation_class target_implementation_class
54
+ end
22
55
  end
@@ -6,7 +6,10 @@ require 'bolt/task'
6
6
  # The results are returned as a list of hashes representing each resource.
7
7
  #
8
8
  # Requires the Puppet Agent be installed on the target, which can be accomplished with apply_prep
9
- # or by directly running the puppet_agent::install task.
9
+ # or by directly running the puppet_agent::install task. In order to be able to reference types without
10
+ # string quoting (for example `get_resources($target, Package)` instead of `get_resources($target, 'Package')`)
11
+ # run the command `bolt puppetfile generate-types` to generate type references in `$Boldir/.resource_types`.
12
+ #
10
13
  #
11
14
  # **NOTE:** Not available in apply block
12
15
  Puppet::Functions.create_function(:get_resources) do
@@ -16,7 +19,7 @@ Puppet::Functions.create_function(:get_resources) do
16
19
  # get_resources('target1,target2', [Package, File[/etc/puppetlabs]])
17
20
  dispatch :get_resources do
18
21
  param 'Boltlib::TargetSpec', :targets
19
- param 'Variant[String, Resource, Array[Variant[String, Resource]]]', :resources
22
+ param 'Variant[String, Type[Resource], Array[Variant[String, Type[Resource]]]]', :resources
20
23
  end
21
24
 
22
25
  def script_compiler
@@ -44,6 +47,10 @@ Puppet::Functions.create_function(:get_resources) do
44
47
  inventory = Puppet.lookup(:bolt_inventory)
45
48
 
46
49
  resources = [resources].flatten
50
+
51
+ # Stringify resource types to pass to task
52
+ resources.map! { |r| r.is_a?(String) ? r : r.to_s }
53
+
47
54
  resources.each do |resource|
48
55
  if resource !~ /^\w+$/ && resource !~ /^\w+\[.+\]$/
49
56
  raise Bolt::Error.new("#{resource} is not a valid resource type or type instance name", 'bolt/get-resources')
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bolt/error'
4
+
5
+ # Get a single target from inventory if it exists, otherwise create a new Target.
6
+ #
7
+ # **NOTE:** Calling `get_target` inside an `apply` block with a
8
+ # version 2 inventory creates a new Target object.
9
+ # `get_target('all')` returns an empty array.
10
+ # **NOTE:** Only compatible with inventory v2
11
+ Puppet::Functions.create_function(:get_target) do
12
+ # @param name A Target name.
13
+ # @return A single target, either new or from inventory.
14
+ # @example Create a new Target from a URI
15
+ # get_target('winrm://host2:54321')
16
+ # @example Get an existing Target from inventory
17
+ # get_target('existing-target')
18
+ dispatch :get_target do
19
+ param 'Boltlib::TargetSpec', :name
20
+ return_type 'Target'
21
+ end
22
+
23
+ def get_target(name)
24
+ inventory = Puppet.lookup(:bolt_inventory)
25
+ # Bolt executor not expected when invoked from apply block
26
+ executor = Puppet.lookup(:bolt_executor) { nil }
27
+ executor&.report_function_call(self.class.name)
28
+
29
+ unless inventory.version > 1
30
+ raise Puppet::ParseErrorWithIssue
31
+ .from_issue_and_stack(Bolt::PAL::Issues::UNSUPPORTED_INVENTORY_VERSION, action: 'get_target')
32
+ end
33
+
34
+ inventory.get_target(name)
35
+ end
36
+ end
@@ -83,6 +83,22 @@ Puppet::Functions.create_function(:run_plan, Puppet::Functions::InternalFunction
83
83
  executor.report_yaml_plan(closure.model.body)
84
84
  end
85
85
 
86
+ # If a TargetSpec parameter is passed, ensure it is in inventory
87
+ inventory = Puppet.lookup(:bolt_inventory)
88
+
89
+ if inventory.version > 1
90
+ param_types = closure.parameters.each_with_object({}) do |param, param_acc|
91
+ param_acc[param.name] = extract_parameter_types(param.type_expr).flatten
92
+ end
93
+ params.each do |param, value|
94
+ # Note the safe lookup operator is needed to handle case where a parameter is passed to a
95
+ # plan that the plan is not expecting
96
+ if param_types[param]&.include?('TargetSpec') || param_types[param]&.include?('Boltlib::TargetSpec')
97
+ inventory.get_targets(value)
98
+ end
99
+ end
100
+ end
101
+
86
102
  # wrap plan execution in logging messages
87
103
  executor.log_plan(plan_name) do
88
104
  result = nil
@@ -116,4 +132,30 @@ Puppet::Functions.create_function(:run_plan, Puppet::Functions::InternalFunction
116
132
  result
117
133
  end
118
134
  end
135
+
136
+ # Recursively examine the type_expr to build a list of types
137
+ def extract_parameter_types(type_expr)
138
+ # No type
139
+ if type_expr.nil?
140
+ []
141
+ # Multiple types to extract (ex. Variant[TargetSpec, String])
142
+ elsif defined?(type_expr.keys)
143
+ type_expr.keys.flat_map { |param| extract_parameter_types(param) }
144
+ # Store cased value
145
+ elsif defined?(type_expr.cased_value)
146
+ [type_expr.cased_value]
147
+ # Type alias, able to resolve alias
148
+ elsif defined?(type_expr.resolved_type.name)
149
+ [type_expr.resolved_type.name]
150
+ # Nested type alias, recurse
151
+ elsif defined?(type_expr.type)
152
+ extract_parameter_types(type_expr.type)
153
+ # Array conatins alias types
154
+ elsif defined?(type_expr.types)
155
+ type_expr.types.flat_map { |param| extract_parameter_types(param) }
156
+ # Each element can be handled by a resolver above
157
+ elsif defined?(type_expr.element_type)
158
+ extract_parameter_types(type_expr.element_type)
159
+ end
160
+ end
119
161
  end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bolt/error'
4
+
5
+ # Set configuration options on a target
6
+ #
7
+ # **NOTE:** Not available in apply block
8
+ # **NOTE:** Only compatible with inventory v2
9
+ Puppet::Functions.create_function(:set_config) do
10
+ # @param target The Target object to configure. See {get_targets}.
11
+ # @param key_or_key_path The configuration setting to update.
12
+ # @param value The configuration value
13
+ # @return The Target with the updated config
14
+ # @example Set the transport for a target
15
+ # set_config($target, 'transport', 'ssh')
16
+ # @example Set the ssh password
17
+ # set_config($target, ['ssh', 'password'], 'secret')
18
+ # @example Overwrite ssh config
19
+ # set_config($target, 'ssh', { user => 'me', password => 'secret' })
20
+ dispatch :set_config do
21
+ param 'Target', :target
22
+ param 'Variant[String, Array[String]]', :key_or_key_path
23
+ param 'Any', :value
24
+ return_type 'Target'
25
+ end
26
+
27
+ def set_config(target, key_or_key_path, value = true)
28
+ unless Puppet[:tasks]
29
+ raise Puppet::ParseErrorWithIssue
30
+ .from_issue_and_stack(Bolt::PAL::Issues::PLAN_OPERATION_NOT_SUPPORTED_WHEN_COMPILING, action: 'set_config')
31
+ end
32
+
33
+ inventory = Puppet.lookup(:bolt_inventory)
34
+ executor = Puppet.lookup(:bolt_executor)
35
+ executor.report_function_call(self.class.name)
36
+
37
+ unless inventory.version > 1
38
+ raise Puppet::ParseErrorWithIssue
39
+ .from_issue_and_stack(Bolt::PAL::Issues::UNSUPPORTED_INVENTORY_VERSION, action: 'set_config')
40
+ end
41
+
42
+ inventory.set_config(target, key_or_key_path, value)
43
+
44
+ target
45
+ end
46
+ end
@@ -22,6 +22,7 @@ Puppet::Functions.create_function(:set_feature) do
22
22
  param 'Target', :target
23
23
  param 'String', :feature
24
24
  optional_param 'Boolean', :value
25
+ return_type 'Target'
25
26
  end
26
27
 
27
28
  def set_feature(target, feature, value = true)
@@ -9,13 +9,14 @@ Puppet::Functions.create_function(:set_var) do
9
9
  # @param target The Target object to set the variable for. See {get_targets}.
10
10
  # @param key The key for the variable.
11
11
  # @param value The value of the variable.
12
- # @return [Undef]
12
+ # @return The target with the updated feature
13
13
  # @example Set a variable on a target
14
14
  # $target.set_var('ephemeral', true)
15
15
  dispatch :set_var do
16
16
  param 'Target', :target
17
17
  param 'String', :key
18
18
  param 'Data', :value
19
+ return_type 'Target'
19
20
  end
20
21
 
21
22
  def set_var(target, key, value)
@@ -28,6 +29,9 @@ Puppet::Functions.create_function(:set_var) do
28
29
  executor = Puppet.lookup(:bolt_executor)
29
30
  executor.report_function_call(self.class.name)
30
31
 
31
- inventory.set_var(target, key, value)
32
+ var_hash = { key => value }
33
+ inventory.set_var(target, var_hash)
34
+
35
+ target
32
36
  end
33
37
  end
@@ -107,7 +107,7 @@ module Bolt
107
107
  end
108
108
 
109
109
  def report_bundled_content(mode, name)
110
- if bundled_content[mode.split.first]&.include?(name)
110
+ if bundled_content[mode.split(' ').first]&.include?(name)
111
111
  event('Bundled Content', mode, label: name)
112
112
  end
113
113
  end
@@ -46,6 +46,25 @@ module Bolt
46
46
  end
47
47
  end
48
48
 
49
+ def self.invalid_report_error(result)
50
+ # These are the keys ApplyResult methods rely on.
51
+ expected_report_keys = %w[metrics resource_statuses status]
52
+ missing_keys = expected_report_keys.reject { |k| result.value.include?(k) }
53
+
54
+ unless missing_keys.empty?
55
+ if result['_output']
56
+ # rubocop:disable Metrics/LineLength
57
+ msg = "Report result contains an '_output' key. Catalog application may have printed extraneous output to stdout: #{result['_output']}"
58
+ # rubocop:enable Metrics/LineLength
59
+ else
60
+ msg = "Report did not contain all expected keys missing: #{missing_keys.join(' ,')}"
61
+ end
62
+
63
+ { 'msg' => msg,
64
+ 'kind' => 'bolt/invalid-report' }
65
+ end
66
+ end
67
+
49
68
  def self.from_task_result(result)
50
69
  if (puppet_missing = puppet_missing_error(result))
51
70
  new(result.target,
@@ -53,6 +72,10 @@ module Bolt
53
72
  report: result.value.reject { |k| k == '_error' })
54
73
  elsif !result.ok?
55
74
  new(result.target, error: result.error_hash)
75
+ elsif (invalid_report = invalid_report_error(result))
76
+ new(result.target,
77
+ error: invalid_report,
78
+ report: result.value.reject { |k| %w[_error _output].include?(k) })
56
79
  elsif (resource_error = resource_error(result))
57
80
  new(result.target,
58
81
  error: resource_error,
@@ -54,6 +54,9 @@ module Bolt
54
54
  when 'show-modules'
55
55
  { flags: OPTIONS[:global] + OPTIONS[:global_config_setters],
56
56
  banner: PUPPETFILE_SHOWMODULES_HELP }
57
+ when 'generate-types'
58
+ { flags: OPTIONS[:global] + OPTIONS[:global_config_setters],
59
+ banner: PUPPETFILE_GENERATETYPES_HELP }
57
60
  else
58
61
  { flags: OPTIONS[:global] + OPTIONS[:global_config_setters],
59
62
  banner: PUPPETFILE_HELP }
@@ -229,6 +232,7 @@ module Bolt
229
232
  Available actions are:
230
233
  install Install modules from a Puppetfile into a Boltdir
231
234
  show-modules List modules available to Bolt
235
+ generate-types Generate type references to register in Plans
232
236
 
233
237
  Install modules into the local Boltdir
234
238
  bolt puppetfile install
@@ -248,6 +252,18 @@ module Bolt
248
252
  PUPPETFILE_SHOWMODULES_HELP = <<~HELP
249
253
  Usage: bolt puppetfile show-modules
250
254
 
255
+ List modules available to Bolt
256
+ bolt puppetfile show-modules
257
+
258
+ Available options are:
259
+ HELP
260
+
261
+ PUPPETFILE_GENERATETYPES_HELP = <<~HELP
262
+ Usage: bolt puppetfile generate-types
263
+
264
+ Generate type references to register in Plans
265
+ bolt puppetfile generate-types
266
+
251
267
  Available options are:
252
268
  HELP
253
269
 
data/lib/bolt/boltdir.rb CHANGED
@@ -6,7 +6,8 @@ module Bolt
6
6
  class Boltdir
7
7
  BOLTDIR_NAME = 'Boltdir'
8
8
 
9
- attr_reader :path, :config_file, :inventory_file, :modulepath, :hiera_config, :puppetfile, :rerunfile, :type
9
+ attr_reader :path, :config_file, :inventory_file, :modulepath, :hiera_config,
10
+ :puppetfile, :rerunfile, :type, :resource_types
10
11
 
11
12
  def self.default_boltdir
12
13
  Boltdir.new(File.join('~', '.puppetlabs', 'bolt'), 'user')
@@ -37,6 +38,7 @@ module Bolt
37
38
  @hiera_config = @path + 'hiera.yaml'
38
39
  @puppetfile = @path + 'Puppetfile'
39
40
  @rerunfile = @path + '.rerun.json'
41
+ @resource_types = @path + '.resource_types'
40
42
  @type = type
41
43
  end
42
44
 
data/lib/bolt/cli.rb CHANGED
@@ -33,7 +33,7 @@ module Bolt
33
33
  'task' => %w[show run],
34
34
  'plan' => %w[show run convert],
35
35
  'file' => %w[upload],
36
- 'puppetfile' => %w[install show-modules],
36
+ 'puppetfile' => %w[install show-modules generate-types],
37
37
  'secret' => %w[encrypt decrypt createkeys],
38
38
  'inventory' => %w[show],
39
39
  'apply' => %w[] }.freeze
@@ -76,7 +76,6 @@ module Bolt
76
76
 
77
77
  def parse
78
78
  parser = BoltOptionParser.new(options)
79
-
80
79
  # This part aims to handle both `bolt <mode> --help` and `bolt help <mode>`.
81
80
  remaining = handle_parser_errors { parser.permute(@argv) } unless @argv.empty?
82
81
  if @argv.empty? || help?(remaining)
@@ -320,7 +319,11 @@ module Bolt
320
319
  when 'plan'
321
320
  code = run_plan(options[:object], options[:task_options], options[:target_args], options)
322
321
  when 'puppetfile'
323
- code = install_puppetfile(@config.puppetfile_config, @config.puppetfile, @config.modulepath)
322
+ if options[:action] == 'generate-types'
323
+ code = generate_types
324
+ elsif options[:action] == 'install'
325
+ code = install_puppetfile(@config.puppetfile_config, @config.puppetfile, @config.modulepath)
326
+ end
324
327
  when 'secret'
325
328
  code = Bolt::Secret.execute(plugins, outputter, options)
326
329
  when 'apply'
@@ -474,6 +477,12 @@ module Bolt
474
477
  outputter.print_module_list(pal.list_modules)
475
478
  end
476
479
 
480
+ def generate_types
481
+ # generate_types will surface a nice error with helpful message if it fails
482
+ pal.generate_types
483
+ 0
484
+ end
485
+
477
486
  def install_puppetfile(config, puppetfile, modulepath)
478
487
  require 'r10k/cli'
479
488
  require 'bolt/r10k_log_proxy'
@@ -495,6 +504,8 @@ module Bolt
495
504
 
496
505
  ok = install_action.call
497
506
  outputter.print_puppetfile_result(ok, puppetfile, moduledir)
507
+ # Automatically generate types after installing modules
508
+ pal.generate_types
498
509
 
499
510
  ok ? 0 : 1
500
511
  else
@@ -505,7 +516,10 @@ module Bolt
505
516
  end
506
517
 
507
518
  def pal
508
- @pal ||= Bolt::PAL.new(config.modulepath, config.hiera_config, config.compile_concurrency)
519
+ @pal ||= Bolt::PAL.new(config.modulepath,
520
+ config.hiera_config,
521
+ config.boltdir.resource_types,
522
+ config.compile_concurrency)
509
523
  end
510
524
 
511
525
  def convert_plan(plan)
@@ -544,9 +558,9 @@ module Bolt
544
558
  # We only need to enumerate bundled content when running a task or plan
545
559
  content = { 'Plan' => [],
546
560
  'Task' => [],
547
- 'Plugin' => %w[puppetdb pkcs7 prompt terraform task] }
561
+ 'Plugin' => Bolt::Plugin::BUILTIN_PLUGINS }
548
562
  if %w[plan task].include?(options[:subcommand]) && options[:action] == 'run'
549
- default_content = Bolt::PAL.new([], nil)
563
+ default_content = Bolt::PAL.new([], nil, nil)
550
564
  content['Plan'] = default_content.list_plans.each_with_object([]) do |iter, col|
551
565
  col << iter&.first
552
566
  end
data/lib/bolt/config.rb CHANGED
@@ -276,9 +276,7 @@ module Bolt
276
276
  raise Bolt::ValidationError, "Unsupported format: '#{@format}'"
277
277
  end
278
278
 
279
- if @hiera_config && !(File.file?(@hiera_config) && File.readable?(@hiera_config))
280
- raise Bolt::FileError, "Could not read hiera-config file #{@hiera_config}", @hiera_config
281
- end
279
+ Bolt::Util.validate_file('hiera-config', @hiera_config) if @hiera_config
282
280
 
283
281
  unless @transport.nil? || Bolt::TRANSPORTS.include?(@transport.to_sym)
284
282
  raise UnknownTransportError, @transport
@@ -1,19 +1,20 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'bolt/inventory/group'
4
+ require 'bolt/inventory/inventory2'
4
5
 
5
6
  module Bolt
6
7
  class Inventory
7
8
  class Group2
8
9
  attr_accessor :name, :targets, :aliases, :name_or_alias, :groups,
9
- :config, :rest, :facts, :vars, :features, :plugin_hooks
10
+ :config, :facts, :vars, :features, :plugin_hooks
10
11
 
11
12
  # THESE are duplicates with the old groups for now.
12
13
  # Regex used to validate group names and target aliases.
13
14
  NAME_REGEX = /\A[a-z0-9_][a-z0-9_-]*\Z/.freeze
14
15
 
15
16
  DATA_KEYS = %w[name config facts vars features plugin_hooks].freeze
16
- NODE_KEYS = DATA_KEYS + %w[alias uri]
17
+ TARGET_KEYS = DATA_KEYS + %w[alias uri]
17
18
  GROUP_KEYS = DATA_KEYS + %w[groups targets]
18
19
  CONFIG_KEYS = Bolt::TRANSPORTS.keys.map(&:to_s) + ['transport']
19
20
 
@@ -59,8 +60,10 @@ module Bolt
59
60
  groups = fetch_value(data, 'groups', Array)
60
61
 
61
62
  @targets = {}
63
+ # @target_objects = {}
62
64
  @aliases = {}
63
65
  @name_or_alias = []
66
+
64
67
  targets.each do |target|
65
68
  # If target is a string, it can refer to either a target name or
66
69
  # alias. Which can't be determined until all groups have been
@@ -152,6 +155,7 @@ module Bolt
152
155
  unless target.is_a?(Hash)
153
156
  raise ValidationError.new("Node entry must be a Hash, not #{target.class}", @name)
154
157
  end
158
+
155
159
  # This check prevents plugins from returning plugins
156
160
  raise ValidationError.new("Cannot set target with plugin", @name) if target.key?('_plugin')
157
161
  target.each do |k, v|
@@ -159,56 +163,57 @@ module Bolt
159
163
  validate_config_plugin(v, k, @name)
160
164
  end
161
165
 
162
- target['name'] ||= target['uri']
166
+ t_name = target['name'] || target['uri']
163
167
 
164
- if target['name'].nil? || target['name'].empty?
168
+ if t_name.nil? || t_name.empty?
165
169
  raise ValidationError.new("No name or uri for target: #{target}", @name)
166
170
  end
167
171
 
168
- if @targets.include?(target['name'])
172
+ unless t_name.ascii_only?
173
+ raise ValidationError.new("Target name must be ASCII characters: #{target}", @name)
174
+ end
175
+
176
+ if @targets.include?(t_name)
169
177
  @logger.warn("Ignoring duplicate target in #{@name}: #{target}")
170
178
  return
171
179
  end
172
180
 
173
- raise ValidationError.new("Node #{target} does not have a name", @name) unless target['name']
174
- @targets[target['name']] = target
175
-
176
- unless (unexpected_keys = target.keys - NODE_KEYS).empty?
177
- msg = "Found unexpected key(s) #{unexpected_keys.join(', ')} in target #{target['name']}"
181
+ unless (unexpected_keys = target.keys - TARGET_KEYS).empty?
182
+ msg = "Found unexpected key(s) #{unexpected_keys.join(', ')} in target #{t_name}"
178
183
  @logger.warn(msg)
179
184
  end
180
185
 
181
186
  unless target['config'].nil? || target['config'].is_a?(Hash)
182
- raise ValidationError.new("Invalid configuration for target: #{target['name']}", @name)
187
+ raise ValidationError.new("Invalid configuration for target: #{t_name}", @name)
183
188
  end
184
189
 
185
190
  config_keys = target['config']&.keys || []
186
191
  unless (unexpected_keys = config_keys - CONFIG_KEYS).empty?
187
- msg = "Found unexpected key(s) #{unexpected_keys.join(', ')} in config for target #{target['name']}"
192
+ msg = "Found unexpected key(s) #{unexpected_keys.join(', ')} in config for target #{t_name}"
188
193
  @logger.warn(msg)
189
194
  end
190
195
 
191
196
  target['config'] = config_only_plugin(target['config'])
192
197
 
193
- unless target.include?('alias')
194
- return
195
- end
196
-
197
- aliases = target['alias']
198
- aliases = [aliases] if aliases.is_a?(String)
199
- unless aliases.is_a?(Array)
200
- msg = "Alias entry on #{target['name']} must be a String or Array, not #{aliases.class}"
201
- raise ValidationError.new(msg, @name)
202
- end
198
+ if target.include?('alias')
199
+ aliases = target['alias']
200
+ aliases = [aliases] if aliases.is_a?(String)
201
+ unless aliases.is_a?(Array)
202
+ msg = "Alias entry on #{t_name} must be a String or Array, not #{aliases.class}"
203
+ raise ValidationError.new(msg, @name)
204
+ end
203
205
 
204
- aliases.each do |alia|
205
- raise ValidationError.new("Invalid alias #{alia}", @name) unless alia =~ NAME_REGEX
206
+ aliases.each do |alia|
207
+ raise ValidationError.new("Invalid alias #{alia}", @name) unless alia =~ NAME_REGEX
206
208
 
207
- if (found = @aliases[alia])
208
- raise ValidationError.new(alias_conflict(alia, found, target['name']), @name)
209
+ if (found = @aliases[alia])
210
+ raise ValidationError.new(alias_conflict(alia, found, t_name), @name)
211
+ end
212
+ @aliases[alia] = t_name
209
213
  end
210
- @aliases[alia] = target['name']
211
214
  end
215
+
216
+ @targets[t_name] = target
212
217
  end
213
218
 
214
219
  def lookup_targets(lookup)
@@ -307,21 +312,22 @@ module Bolt
307
312
 
308
313
  # Collect target names and aliases into a list used to validate that subgroups don't conflict.
309
314
  # Used names validate that previously used group names don't conflict with new target names/aliases.
310
- @targets.each_key do |n|
315
+ @targets.each do |t_name, t_data|
311
316
  # Require targets to be parseable as a Target.
312
317
  begin
313
- Target.new(n)
318
+ # Catch malformed URI here
319
+ Bolt::Inventory::Inventory2.parse_uri(t_data['uri'])
314
320
  rescue Bolt::ParseError => e
315
321
  @logger.debug(e)
316
- raise ValidationError.new("Invalid target name #{n}", @name)
322
+ raise ValidationError.new("Invalid target uri #{t_data['uri']}", @name)
317
323
  end
318
324
 
319
- raise ValidationError.new(group_target_conflict(n), @name) if used_names.include?(n)
320
- if aliased.include?(n)
321
- raise ValidationError.new(alias_target_conflict(n), @name)
325
+ raise ValidationError.new(group_target_conflict(t_name), @name) if used_names.include?(t_name)
326
+ if aliased.include?(t_name)
327
+ raise ValidationError.new(alias_target_conflict(t_name), @name)
322
328
  end
323
329
 
324
- target_names << n
330
+ target_names << t_name
325
331
  end
326
332
 
327
333
  @aliases.each do |n, target|