bolt 2.26.0 → 2.31.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 (51) hide show
  1. checksums.yaml +4 -4
  2. data/Puppetfile +13 -12
  3. data/bolt-modules/boltlib/lib/puppet/functions/write_file.rb +2 -2
  4. data/lib/bolt/analytics.rb +4 -0
  5. data/lib/bolt/applicator.rb +19 -18
  6. data/lib/bolt/bolt_option_parser.rb +112 -22
  7. data/lib/bolt/catalog.rb +1 -1
  8. data/lib/bolt/cli.rb +210 -174
  9. data/lib/bolt/config.rb +22 -2
  10. data/lib/bolt/config/modulepath.rb +30 -0
  11. data/lib/bolt/config/options.rb +30 -0
  12. data/lib/bolt/config/transport/options.rb +1 -1
  13. data/lib/bolt/executor.rb +1 -1
  14. data/lib/bolt/inventory.rb +11 -10
  15. data/lib/bolt/logger.rb +26 -19
  16. data/lib/bolt/module_installer.rb +242 -0
  17. data/lib/bolt/outputter.rb +4 -0
  18. data/lib/bolt/outputter/human.rb +77 -17
  19. data/lib/bolt/outputter/json.rb +21 -6
  20. data/lib/bolt/outputter/logger.rb +2 -2
  21. data/lib/bolt/pal.rb +46 -25
  22. data/lib/bolt/plugin.rb +1 -1
  23. data/lib/bolt/plugin/module.rb +1 -1
  24. data/lib/bolt/project.rb +62 -12
  25. data/lib/bolt/project_migrator.rb +80 -0
  26. data/lib/bolt/project_migrator/base.rb +39 -0
  27. data/lib/bolt/project_migrator/config.rb +67 -0
  28. data/lib/bolt/project_migrator/inventory.rb +67 -0
  29. data/lib/bolt/project_migrator/modules.rb +198 -0
  30. data/lib/bolt/puppetfile.rb +149 -0
  31. data/lib/bolt/puppetfile/installer.rb +43 -0
  32. data/lib/bolt/puppetfile/module.rb +93 -0
  33. data/lib/bolt/rerun.rb +1 -1
  34. data/lib/bolt/result.rb +15 -0
  35. data/lib/bolt/shell/bash.rb +4 -3
  36. data/lib/bolt/transport/base.rb +4 -4
  37. data/lib/bolt/transport/ssh/connection.rb +1 -1
  38. data/lib/bolt/util.rb +51 -10
  39. data/lib/bolt/version.rb +1 -1
  40. data/lib/bolt_server/acl.rb +2 -2
  41. data/lib/bolt_server/base_config.rb +3 -3
  42. data/lib/bolt_server/config.rb +1 -1
  43. data/lib/bolt_server/file_cache.rb +11 -11
  44. data/lib/bolt_server/transport_app.rb +206 -27
  45. data/lib/bolt_spec/bolt_context.rb +8 -6
  46. data/lib/bolt_spec/plans.rb +1 -1
  47. data/lib/bolt_spec/plans/mock_executor.rb +1 -1
  48. data/lib/bolt_spec/run.rb +1 -1
  49. metadata +14 -6
  50. data/lib/bolt/project_migrate.rb +0 -138
  51. data/lib/bolt_server/pe/pal.rb +0 -67
@@ -35,6 +35,10 @@ module Bolt
35
35
  raise NotImplementedError, "print_message() must be implemented by the outputter class"
36
36
  end
37
37
 
38
+ def print_error
39
+ raise NotImplementedError, "print_error() must be implemented by the outputter class"
40
+ end
41
+
38
42
  def stringify(message)
39
43
  formatted = format_message(message)
40
44
  if formatted.is_a?(Hash) || formatted.is_a?(Array)
@@ -5,9 +5,12 @@ require 'bolt/pal'
5
5
  module Bolt
6
6
  class Outputter
7
7
  class Human < Bolt::Outputter
8
- COLORS = { red: "31",
9
- green: "32",
10
- yellow: "33" }.freeze
8
+ COLORS = {
9
+ red: "31",
10
+ green: "32",
11
+ yellow: "33",
12
+ cyan: "36"
13
+ }.freeze
11
14
 
12
15
  def print_head; end
13
16
 
@@ -31,6 +34,10 @@ module Bolt
31
34
  string.sub(/\s\z/, '')
32
35
  end
33
36
 
37
+ def wrap(string, width = 80)
38
+ string.gsub(/(.{1,#{width}})(\s+|\Z)/, "\\1\n")
39
+ end
40
+
34
41
  def handle_event(event)
35
42
  case event[:type]
36
43
  when :enable_default_output
@@ -48,9 +55,9 @@ module Bolt
48
55
  when :node_result
49
56
  print_result(event[:result]) if @verbose
50
57
  when :step_start
51
- print_step_start(event) if plan_logging?
58
+ print_step_start(**event) if plan_logging?
52
59
  when :step_finish
53
- print_step_finish(event) if plan_logging?
60
+ print_step_finish(**event) if plan_logging?
54
61
  when :plan_start
55
62
  print_plan_start(event)
56
63
  when :plan_finish
@@ -188,7 +195,7 @@ module Bolt
188
195
  @stream.puts total_msg
189
196
  end
190
197
 
191
- def print_table(results)
198
+ def print_table(results, padding_left = 0, padding_right = 3)
192
199
  # lazy-load expensive gem code
193
200
  require 'terminal-table'
194
201
 
@@ -198,8 +205,8 @@ module Bolt
198
205
  border_x: '',
199
206
  border_y: '',
200
207
  border_i: '',
201
- padding_left: 0,
202
- padding_right: 3,
208
+ padding_left: padding_left,
209
+ padding_right: padding_right,
203
210
  border_top: false,
204
211
  border_bottom: false
205
212
  }
@@ -241,7 +248,7 @@ module Bolt
241
248
  task_info << "MODULE:\n"
242
249
 
243
250
  path = task.files.first['path'].chomp("/tasks/#{task.files.first['name']}")
244
- task_info << if path.start_with?(Bolt::PAL::MODULES_PATH)
251
+ task_info << if path.start_with?(Bolt::Config::Modulepath::MODULES_PATH)
245
252
  "built-in module"
246
253
  else
247
254
  path
@@ -271,7 +278,7 @@ module Bolt
271
278
  plan_info << "MODULE:\n"
272
279
 
273
280
  path = plan['module']
274
- plan_info << if path.start_with?(Bolt::PAL::MODULES_PATH)
281
+ plan_info << if path.start_with?(Bolt::Config::Modulepath::MODULES_PATH)
275
282
  "built-in module"
276
283
  else
277
284
  path
@@ -299,9 +306,9 @@ module Bolt
299
306
  def print_module_list(module_list)
300
307
  module_list.each do |path, modules|
301
308
  if (mod = modules.find { |m| m[:internal_module_group] })
302
- @stream.puts(mod[:internal_module_group])
309
+ @stream.puts(colorize(:cyan, mod[:internal_module_group]))
303
310
  else
304
- @stream.puts(path)
311
+ @stream.puts(colorize(:cyan, path))
305
312
  end
306
313
 
307
314
  if modules.empty?
@@ -317,17 +324,35 @@ module Bolt
317
324
  [m[:name], version]
318
325
  end
319
326
 
320
- print_table(module_info)
327
+ print_table(module_info, 2, 1)
321
328
  end
322
329
 
323
330
  @stream.write("\n")
324
331
  end
325
332
  end
326
333
 
327
- def print_targets(targets)
328
- count = "#{targets.count} target#{'s' unless targets.count == 1}"
329
- @stream.puts targets.map(&:name).join("\n")
330
- @stream.puts colorize(:green, count)
334
+ def print_targets(target_list, inventoryfile)
335
+ adhoc = colorize(:yellow, "(Not found in inventory file)")
336
+
337
+ targets = []
338
+ targets += target_list[:inventory].map { |target| [target.name, nil] }
339
+ targets += target_list[:adhoc].map { |target| [target.name, adhoc] }
340
+
341
+ if targets.any?
342
+ print_table(targets, 0, 2)
343
+ @stream.puts
344
+ end
345
+
346
+ @stream.puts "INVENTORY FILE:"
347
+ if inventoryfile.exist?
348
+ @stream.puts inventoryfile
349
+ else
350
+ @stream.puts wrap("Tried to load inventory from #{inventoryfile}, but the file does not exist")
351
+ end
352
+
353
+ @stream.puts "\nTARGET COUNT:"
354
+ @stream.puts "#{targets.count} total, #{target_list[:inventory].count} from inventory, "\
355
+ "#{target_list[:adhoc].count} adhoc"
331
356
  end
332
357
 
333
358
  def print_target_info(targets)
@@ -394,6 +419,41 @@ module Bolt
394
419
  @stream.puts(message)
395
420
  end
396
421
 
422
+ def print_error(message)
423
+ @stream.puts(colorize(:red, message))
424
+ end
425
+
426
+ def print_prompt(prompt)
427
+ @stream.print(colorize(:cyan, indent(4, prompt)))
428
+ end
429
+
430
+ def print_prompt_error(message)
431
+ @stream.puts(colorize(:red, indent(4, message)))
432
+ end
433
+
434
+ def print_action_step(step)
435
+ first, *remaining = wrap(step, 76).lines
436
+
437
+ first = indent(2, "→ #{first}")
438
+ remaining = remaining.map { |line| indent(4, line) }
439
+ step = [first, *remaining, "\n"].join
440
+
441
+ @stream.puts(step)
442
+ end
443
+
444
+ def print_action_error(error)
445
+ # Running everything through 'wrap' messes with newlines. Separating
446
+ # into lines and wrapping each individually ensures separate errors are
447
+ # distinguishable.
448
+ first, *remaining = error.lines
449
+ first = colorize(:red, indent(2, "→ #{wrap(first, 76)}"))
450
+ wrapped = remaining.map { |l| wrap(l) }
451
+ to_print = wrapped.map { |line| colorize(:red, indent(4, line)) }
452
+ step = [first, *to_print, "\n"].join
453
+
454
+ @stream.puts(step)
455
+ end
456
+
397
457
  def duration_to_string(duration)
398
458
  hrs = (duration / 3600).floor
399
459
  mins = ((duration % 3600) / 60).floor
@@ -48,7 +48,7 @@ module Bolt
48
48
 
49
49
  def print_task_info(task)
50
50
  path = task.files.first['path'].chomp("/tasks/#{task.files.first['name']}")
51
- module_dir = if path.start_with?(Bolt::PAL::MODULES_PATH)
51
+ module_dir = if path.start_with?(Bolt::Config::Modulepath::MODULES_PATH)
52
52
  "built-in module"
53
53
  else
54
54
  path
@@ -62,7 +62,7 @@ module Bolt
62
62
 
63
63
  def print_plan_info(plan)
64
64
  path = plan.delete('module')
65
- plan['module_dir'] = if path.start_with?(Bolt::PAL::MODULES_PATH)
65
+ plan['module_dir'] = if path.start_with?(Bolt::Config::Modulepath::MODULES_PATH)
66
66
  "built-in module"
67
67
  else
68
68
  path
@@ -97,13 +97,22 @@ module Bolt
97
97
  def print_puppetfile_result(success, puppetfile, moduledir)
98
98
  @stream.puts({ "success": success,
99
99
  "puppetfile": puppetfile,
100
- "moduledir": moduledir }.to_json)
100
+ "moduledir": moduledir.to_s }.to_json)
101
101
  end
102
102
 
103
- def print_targets(targets)
103
+ def print_targets(target_list, inventoryfile)
104
104
  @stream.puts ::JSON.pretty_generate(
105
- "targets": targets.map(&:name),
106
- "count": targets.count
105
+ "inventory": {
106
+ "targets": target_list[:inventory].map(&:name),
107
+ "count": target_list[:inventory].count,
108
+ "file": inventoryfile.to_s
109
+ },
110
+ "adhoc": {
111
+ "targets": target_list[:adhoc].map(&:name),
112
+ "count": target_list[:adhoc].count
113
+ },
114
+ "targets": target_list.values.flatten.map(&:name),
115
+ "count": target_list.values.flatten.count
107
116
  )
108
117
  end
109
118
 
@@ -135,6 +144,12 @@ module Bolt
135
144
  def print_message(message)
136
145
  $stderr.puts(message)
137
146
  end
147
+ alias print_error print_message
148
+
149
+ def print_action_step(step)
150
+ $stderr.puts(step)
151
+ end
152
+ alias print_action_error print_action_step
138
153
  end
139
154
  end
140
155
  end
@@ -13,9 +13,9 @@ module Bolt
13
13
  def handle_event(event)
14
14
  case event[:type]
15
15
  when :step_start
16
- log_step_start(event)
16
+ log_step_start(**event)
17
17
  when :step_finish
18
- log_step_finish(event)
18
+ log_step_finish(**event)
19
19
  when :plan_start
20
20
  log_plan_start(event)
21
21
  when :plan_finish
@@ -5,18 +5,16 @@ require 'bolt/executor'
5
5
  require 'bolt/error'
6
6
  require 'bolt/plan_result'
7
7
  require 'bolt/util'
8
+ require 'bolt/config/modulepath'
8
9
  require 'etc'
9
10
 
10
11
  module Bolt
11
12
  class PAL
12
- BOLTLIB_PATH = File.expand_path('../../bolt-modules', __dir__)
13
- MODULES_PATH = File.expand_path('../../modules', __dir__)
14
-
15
13
  # PALError is used to convert errors from executing puppet code into
16
14
  # Bolt::Errors
17
15
  class PALError < Bolt::Error
18
16
  def self.from_preformatted_error(err)
19
- if err.cause&.is_a? Bolt::Error
17
+ if err.cause.is_a? Bolt::Error
20
18
  err.cause
21
19
  else
22
20
  from_error(err)
@@ -29,15 +27,12 @@ module Bolt
29
27
  message = err.cause ? err.cause.message : err.message
30
28
 
31
29
  # Provide the location of an error if it came from a plan
32
- details = if defined?(err.file) && err.file
33
- { file: err.file,
34
- line: err.line,
35
- column: err.pos }.compact
36
- else
37
- {}
38
- end
30
+ details = {}
31
+ details[:file] = err.file if defined?(err.file)
32
+ details[:line] = err.line if defined?(err.line)
33
+ details[:column] = err.pos if defined?(err.pos)
39
34
 
40
- e = new(message, details)
35
+ e = new(message, details.compact)
41
36
 
42
37
  e.set_backtrace(err.backtrace)
43
38
  e
@@ -48,16 +43,16 @@ module Bolt
48
43
  end
49
44
  end
50
45
 
51
- attr_reader :modulepath, :user_modulepath
52
-
53
46
  def initialize(modulepath, hiera_config, resource_types, max_compiles = Etc.nprocessors,
54
47
  trusted_external = nil, apply_settings = {}, project = nil)
48
+ unless modulepath.is_a?(Bolt::Config::Modulepath)
49
+ msg = "Type error in PAL: modulepath must be a Bolt::Config::Modulepath"
50
+ raise Bolt::Error.new(msg, "bolt/execution-error")
51
+ end
55
52
  # Nothing works without initialized this global state. Reinitializing
56
53
  # is safe and in practice only happens in tests
57
54
  self.class.load_puppet
58
-
59
- @user_modulepath = modulepath
60
- @modulepath = [BOLTLIB_PATH, *modulepath, MODULES_PATH]
55
+ @modulepath = modulepath
61
56
  @hiera_config = hiera_config
62
57
  @trusted_external = trusted_external
63
58
  @apply_settings = apply_settings
@@ -66,13 +61,21 @@ module Bolt
66
61
  @project = project
67
62
 
68
63
  @logger = Bolt::Logger.logger(self)
69
- if modulepath && !modulepath.empty?
70
- @logger.debug("Loading modules from #{@modulepath.join(File::PATH_SEPARATOR)}")
64
+ unless user_modulepath.empty?
65
+ @logger.debug("Loading modules from #{full_modulepath.join(File::PATH_SEPARATOR)}")
71
66
  end
72
67
 
73
68
  @loaded = false
74
69
  end
75
70
 
71
+ def full_modulepath
72
+ @modulepath.full_modulepath
73
+ end
74
+
75
+ def user_modulepath
76
+ @modulepath.user_modulepath
77
+ end
78
+
76
79
  # Puppet logging is global so this is class method to avoid confusion
77
80
  def self.configure_logging
78
81
  Puppet::Util::Log.destinations.clear
@@ -141,16 +144,33 @@ module Bolt
141
144
  end
142
145
  end
143
146
 
147
+ def detect_project_conflict(project, environment)
148
+ return unless project && project.load_as_module?
149
+ # The environment modulepath has stripped out non-existent directories,
150
+ # so we don't need to check for them
151
+ modules = environment.modulepath.flat_map do |path|
152
+ Dir.children(path).select { |name| Puppet::Module.is_module_directory?(name, path) }
153
+ end
154
+ if modules.include?(project.name)
155
+ Bolt::Logger.warn_once("project shadows module",
156
+ "The project '#{project.name}' shadows an existing module of the same name")
157
+ end
158
+ end
159
+
144
160
  # Runs a block in a PAL script compiler configured for Bolt. Catches
145
161
  # exceptions thrown by the block and re-raises them ensuring they are
146
162
  # Bolt::Errors since the script compiler block will squash all exceptions.
147
163
  def in_bolt_compiler
148
164
  # TODO: If we always call this inside a bolt_executor we can remove this here
149
165
  setup
150
- r = Puppet::Pal.in_tmp_environment('bolt', modulepath: @modulepath, facts: {}) do |pal|
166
+ r = Puppet::Pal.in_tmp_environment('bolt', modulepath: full_modulepath, facts: {}) do |pal|
151
167
  # Only load the project if it a) exists, b) has a name it can be loaded with
152
168
  Puppet.override(bolt_project: @project,
153
169
  yaml_plan_instantiator: Bolt::PAL::YamlPlan::Loader) do
170
+ # Because this has the side effect of loading and caching the list
171
+ # of modules, it must happen *after* we have overridden
172
+ # bolt_project or the project will be ignored
173
+ detect_project_conflict(@project, Puppet.lookup(:environments).get('bolt'))
154
174
  pal.with_script_compiler(set_local_facts: false) do |compiler|
155
175
  alias_types(compiler)
156
176
  register_resource_types(Puppet.lookup(:loaders)) if @resource_types
@@ -198,11 +218,11 @@ module Bolt
198
218
  apply_executor: applicator || Applicator.new(
199
219
  inventory,
200
220
  executor,
201
- @modulepath,
221
+ full_modulepath,
202
222
  # Skip syncing built-in plugins, since we vendor some Puppet 6
203
223
  # versions of "core" types, which are already present on the agent,
204
224
  # but may cause issues on Puppet 5 agents.
205
- @user_modulepath,
225
+ user_modulepath,
206
226
  @project,
207
227
  pdb_client,
208
228
  @hiera_config,
@@ -424,13 +444,14 @@ module Bolt
424
444
  # Returns a mapping of all modules available to the Bolt compiler
425
445
  #
426
446
  # @return [Hash{String => Array<Hash{Symbol => String,nil}>}]
427
- # A hash that associates each directory on the module path with an array
447
+ # A hash that associates each directory on the modulepath with an array
428
448
  # containing a hash of information for each module in that directory.
429
449
  # The information hash provides the name, version, and a string
430
450
  # indicating whether the module belongs to an internal module group.
431
451
  def list_modules
432
- internal_module_groups = { BOLTLIB_PATH => 'Plan Language Modules',
433
- MODULES_PATH => 'Packaged Modules' }
452
+ internal_module_groups = { Bolt::Config::Modulepath::BOLTLIB_PATH => 'Plan Language Modules',
453
+ Bolt::Config::Modulepath::MODULES_PATH => 'Packaged Modules',
454
+ @project.managed_moduledir.to_s => 'Project Dependencies' }
434
455
 
435
456
  in_bolt_compiler do
436
457
  # NOTE: Can replace map+to_h with transform_values when Ruby 2.4
@@ -168,7 +168,7 @@ module Bolt
168
168
  end
169
169
 
170
170
  def modules
171
- @modules ||= Bolt::Module.discover(@pal.modulepath)
171
+ @modules ||= Bolt::Module.discover(@pal.full_modulepath)
172
172
  end
173
173
 
174
174
  def add_plugin(plugin)
@@ -16,7 +16,7 @@ module Bolt
16
16
  mod = modules[name]
17
17
  if mod&.plugin?
18
18
  opts[:mod] = mod
19
- plugin = Bolt::Plugin::Module.new(opts)
19
+ plugin = Bolt::Plugin::Module.new(**opts)
20
20
  plugin.setup
21
21
  plugin
22
22
  else
@@ -16,9 +16,10 @@ module Bolt
16
16
  "These tasks are included in `bolt task show` output"
17
17
  }.freeze
18
18
 
19
- attr_reader :path, :data, :config_file, :inventory_file, :modulepath, :hiera_config,
19
+ attr_reader :path, :data, :config_file, :inventory_file, :hiera_config,
20
20
  :puppetfile, :rerunfile, :type, :resource_types, :logs, :project_file,
21
- :deprecations, :downloads, :plans_path
21
+ :deprecations, :downloads, :plans_path, :modulepath, :managed_moduledir,
22
+ :backup_dir
22
23
 
23
24
  def self.default_project(logs = [])
24
25
  create_project(File.expand_path(File.join('~', '.puppetlabs', 'bolt')), 'user', logs)
@@ -50,6 +51,20 @@ module Bolt
50
51
  def self.create_project(path, type = 'option', logs = [])
51
52
  fullpath = Pathname.new(path).expand_path
52
53
 
54
+ if type == 'user'
55
+ begin
56
+ # This is already expanded if the type is user
57
+ FileUtils.mkdir_p(path)
58
+ rescue StandardError
59
+ logs << { warn: "Could not create default project at #{path}. Continuing without a writeable project. "\
60
+ "Log and rerun files will not be written." }
61
+ end
62
+ end
63
+
64
+ if type == 'option' && !File.directory?(path)
65
+ raise Bolt::Error.new("Could not find project at #{path}", "bolt/project-error")
66
+ end
67
+
53
68
  if !Bolt::Util.windows? && type != 'environment' && fullpath.world_writable?
54
69
  raise Bolt::Error.new(
55
70
  "Project directory '#{fullpath}' is world-writable which poses a security risk. Set "\
@@ -80,15 +95,16 @@ module Bolt
80
95
  @deprecations << { type: 'Using bolt.yaml for project configuration', msg: msg }
81
96
  end
82
97
 
83
- @inventory_file = @path + 'inventory.yaml'
84
- @modulepath = [(@path + 'modules').to_s, (@path + 'site-modules').to_s, (@path + 'site').to_s]
85
- @hiera_config = @path + 'hiera.yaml'
86
- @puppetfile = @path + 'Puppetfile'
87
- @rerunfile = @path + '.rerun.json'
88
- @resource_types = @path + '.resource_types'
89
- @type = type
90
- @downloads = @path + 'downloads'
91
- @plans_path = @path + 'plans'
98
+ @inventory_file = @path + 'inventory.yaml'
99
+ @hiera_config = @path + 'hiera.yaml'
100
+ @puppetfile = @path + 'Puppetfile'
101
+ @rerunfile = @path + '.rerun.json'
102
+ @resource_types = @path + '.resource_types'
103
+ @type = type
104
+ @downloads = @path + 'downloads'
105
+ @plans_path = @path + 'plans'
106
+ @managed_moduledir = @path + '.modules'
107
+ @backup_dir = @path + '.bolt-bak'
92
108
 
93
109
  tc = Bolt::Config::INVENTORY_OPTIONS.keys & raw_data.keys
94
110
  if tc.any?
@@ -98,6 +114,14 @@ module Bolt
98
114
 
99
115
  @data = raw_data.reject { |k, _| Bolt::Config::INVENTORY_OPTIONS.include?(k) }
100
116
 
117
+ # If the 'modules' key is present in the project configuration file,
118
+ # use the new, shorter modulepath.
119
+ @modulepath = if @data.key?('modules')
120
+ [(@path + 'modules').to_s]
121
+ else
122
+ [(@path + 'modules').to_s, (@path + 'site-modules').to_s, (@path + 'site').to_s]
123
+ end
124
+
101
125
  # Once bolt.yaml deprecation is removed, this attribute should be removed
102
126
  # and replaced with .project_file in lib/bolt/config.rb
103
127
  @config_file = if (Bolt::Config::BOLT_OPTIONS & @data.keys).any?
@@ -149,6 +173,16 @@ module Bolt
149
173
  @data['plans']
150
174
  end
151
175
 
176
+ def modules
177
+ @modules ||= @data['modules']&.map do |mod|
178
+ if mod.is_a?(String)
179
+ { 'name' => mod }
180
+ else
181
+ mod
182
+ end
183
+ end
184
+ end
185
+
152
186
  def validate
153
187
  if name
154
188
  if name !~ Bolt::Module::MODULE_NAME_REGEX
@@ -156,7 +190,7 @@ module Bolt
156
190
  Invalid project name '#{name}' in bolt-project.yaml; project name must begin with a lowercase letter
157
191
  and can include lowercase letters, numbers, and underscores.
158
192
  ERROR_STRING
159
- elsif Dir.children(Bolt::PAL::BOLTLIB_PATH).include?(name)
193
+ elsif Dir.children(Bolt::Config::Modulepath::BOLTLIB_PATH).include?(name)
160
194
  raise Bolt::ValidationError, "The project '#{name}' will not be loaded. The project name conflicts "\
161
195
  "with a built-in Bolt module of the same name."
162
196
  end
@@ -170,6 +204,22 @@ module Bolt
170
204
  raise Bolt::ValidationError, "'#{conf}' in bolt-project.yaml must be an array"
171
205
  end
172
206
  end
207
+
208
+ if @data['modules']
209
+ unless @data['modules'].is_a?(Array)
210
+ raise Bolt::ValidationError, "'modules' in bolt-project.yaml must be an array"
211
+ end
212
+
213
+ @data['modules'].each do |mod|
214
+ next if (mod.is_a?(Hash) && mod.key?('name')) || mod.is_a?(String)
215
+ raise Bolt::ValidationError, "Module declaration #{mod.inspect} must be a hash with a name key"
216
+ end
217
+
218
+ unknown_keys = modules.flat_map(&:keys).uniq - %w[name version_requirement]
219
+ if unknown_keys.any?
220
+ @logs << { warn: "Ignoring unknown keys in module declarations: #{unknown_keys.join(', ')}." }
221
+ end
222
+ end
173
223
  end
174
224
 
175
225
  def check_deprecated_file