bolt 2.24.0 → 2.28.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 (50) hide show
  1. checksums.yaml +4 -4
  2. data/Puppetfile +1 -1
  3. data/bolt-modules/boltlib/lib/puppet/datatypes/result.rb +2 -1
  4. data/bolt-modules/boltlib/lib/puppet/functions/download_file.rb +1 -1
  5. data/bolt-modules/dir/lib/puppet/functions/dir/children.rb +1 -1
  6. data/lib/bolt/analytics.rb +7 -3
  7. data/lib/bolt/applicator.rb +21 -21
  8. data/lib/bolt/bolt_option_parser.rb +75 -7
  9. data/lib/bolt/catalog.rb +4 -2
  10. data/lib/bolt/cli.rb +126 -147
  11. data/lib/bolt/config.rb +56 -26
  12. data/lib/bolt/config/options.rb +24 -2
  13. data/lib/bolt/executor.rb +1 -1
  14. data/lib/bolt/inventory.rb +8 -1
  15. data/lib/bolt/inventory/group.rb +1 -1
  16. data/lib/bolt/inventory/inventory.rb +1 -1
  17. data/lib/bolt/inventory/target.rb +1 -1
  18. data/lib/bolt/logger.rb +9 -2
  19. data/lib/bolt/outputter/logger.rb +1 -1
  20. data/lib/bolt/pal.rb +21 -10
  21. data/lib/bolt/pal/yaml_plan/evaluator.rb +1 -1
  22. data/lib/bolt/plugin/puppetdb.rb +1 -1
  23. data/lib/bolt/project.rb +63 -17
  24. data/lib/bolt/puppetdb/client.rb +1 -1
  25. data/lib/bolt/puppetdb/config.rb +1 -1
  26. data/lib/bolt/puppetfile.rb +160 -0
  27. data/lib/bolt/puppetfile/installer.rb +43 -0
  28. data/lib/bolt/puppetfile/module.rb +66 -0
  29. data/lib/bolt/r10k_log_proxy.rb +1 -1
  30. data/lib/bolt/rerun.rb +2 -2
  31. data/lib/bolt/result.rb +23 -0
  32. data/lib/bolt/shell.rb +1 -1
  33. data/lib/bolt/task.rb +1 -1
  34. data/lib/bolt/transport/base.rb +5 -5
  35. data/lib/bolt/transport/docker/connection.rb +1 -1
  36. data/lib/bolt/transport/local/connection.rb +1 -1
  37. data/lib/bolt/transport/ssh.rb +1 -1
  38. data/lib/bolt/transport/ssh/connection.rb +1 -1
  39. data/lib/bolt/transport/ssh/exec_connection.rb +1 -1
  40. data/lib/bolt/transport/winrm.rb +1 -1
  41. data/lib/bolt/transport/winrm/connection.rb +1 -1
  42. data/lib/bolt/util.rb +11 -11
  43. data/lib/bolt/version.rb +1 -1
  44. data/lib/bolt_server/base_config.rb +1 -1
  45. data/lib/bolt_server/config.rb +1 -1
  46. data/lib/bolt_server/file_cache.rb +12 -12
  47. data/lib/bolt_server/transport_app.rb +125 -26
  48. data/lib/bolt_spec/bolt_context.rb +4 -4
  49. data/lib/bolt_spec/run.rb +3 -0
  50. metadata +9 -12
@@ -19,7 +19,7 @@ module Bolt
19
19
  class Config
20
20
  include Bolt::Config::Options
21
21
 
22
- attr_reader :config_files, :warnings, :data, :transports, :project, :modified_concurrency, :deprecations
22
+ attr_reader :config_files, :logs, :data, :transports, :project, :modified_concurrency, :deprecations
23
23
 
24
24
  BOLT_CONFIG_NAME = 'bolt.yaml'
25
25
  BOLT_DEFAULTS_NAME = 'bolt-defaults.yaml'
@@ -32,16 +32,19 @@ module Bolt
32
32
  end
33
33
 
34
34
  def self.from_project(project, overrides = {})
35
+ logs = []
35
36
  conf = if project.project_file == project.config_file
36
37
  project.data
37
38
  else
38
- Bolt::Util.read_optional_yaml_hash(project.config_file, 'config')
39
+ c = Bolt::Util.read_optional_yaml_hash(project.config_file, 'config')
40
+ logs << { debug: "Loaded configuration from #{project.config_file}" } if File.exist?(project.config_file)
41
+ c
39
42
  end
40
43
 
41
44
  data = load_defaults(project).push(
42
45
  filepath: project.config_file,
43
46
  data: conf,
44
- warnings: [],
47
+ logs: logs,
45
48
  deprecations: []
46
49
  )
47
50
 
@@ -50,17 +53,20 @@ module Bolt
50
53
 
51
54
  def self.from_file(configfile, overrides = {})
52
55
  project = Bolt::Project.create_project(Pathname.new(configfile).expand_path.dirname)
56
+ logs = []
53
57
 
54
58
  conf = if project.project_file == project.config_file
55
59
  project.data
56
60
  else
57
- Bolt::Util.read_yaml_hash(configfile, 'config')
61
+ c = Bolt::Util.read_yaml_hash(configfile, 'config')
62
+ logs << { debug: "Loaded configuration from #{configfile}" }
63
+ c
58
64
  end
59
65
 
60
66
  data = load_defaults(project).push(
61
- filepath: project.config_file,
67
+ filepath: configfile,
62
68
  data: conf,
63
- warnings: [],
69
+ logs: logs,
64
70
  deprecations: []
65
71
  )
66
72
 
@@ -90,13 +96,13 @@ module Bolt
90
96
  def self.load_bolt_defaults_yaml(dir)
91
97
  filepath = dir + BOLT_DEFAULTS_NAME
92
98
  data = Bolt::Util.read_yaml_hash(filepath, 'config')
93
- warnings = []
99
+ logs = [{ debug: "Loaded configuration from #{filepath}" }]
94
100
 
95
101
  # Warn if 'bolt.yaml' detected in same directory.
96
102
  if File.exist?(bolt_yaml = dir + BOLT_CONFIG_NAME)
97
- warnings.push(
98
- msg: "Detected multiple configuration files: ['#{bolt_yaml}', '#{filepath}']. '#{bolt_yaml}' "\
99
- "will be ignored."
103
+ logs.push(
104
+ warn: "Detected multiple configuration files: ['#{bolt_yaml}', '#{filepath}']. '#{bolt_yaml}' "\
105
+ "will be ignored."
100
106
  )
101
107
  end
102
108
 
@@ -105,9 +111,9 @@ module Bolt
105
111
 
106
112
  if project_config.any?
107
113
  data.reject! { |key, _| project_config.include?(key) }
108
- warnings.push(
109
- msg: "Unsupported project configuration detected in '#{filepath}': #{project_config.keys}. "\
110
- "Project configuration should be set in 'bolt-project.yaml'."
114
+ logs.push(
115
+ warn: "Unsupported project configuration detected in '#{filepath}': #{project_config.keys}. "\
116
+ "Project configuration should be set in 'bolt-project.yaml'."
111
117
  )
112
118
  end
113
119
 
@@ -116,10 +122,10 @@ module Bolt
116
122
 
117
123
  if transport_config.any?
118
124
  data.reject! { |key, _| transport_config.include?(key) }
119
- warnings.push(
120
- msg: "Unsupported inventory configuration detected in '#{filepath}': #{transport_config.keys}. "\
121
- "Transport configuration should be set under the 'inventory-config' option or "\
122
- "in 'inventory.yaml'."
125
+ logs.push(
126
+ warn: "Unsupported inventory configuration detected in '#{filepath}': #{transport_config.keys}. "\
127
+ "Transport configuration should be set under the 'inventory-config' option or "\
128
+ "in 'inventory.yaml'."
123
129
  )
124
130
  end
125
131
 
@@ -142,7 +148,7 @@ module Bolt
142
148
  data = data.merge(data.delete('inventory-config'))
143
149
  end
144
150
 
145
- { filepath: filepath, data: data, warnings: warnings, deprecations: [] }
151
+ { filepath: filepath, data: data, logs: logs, deprecations: [] }
146
152
  end
147
153
 
148
154
  # Loads a 'bolt.yaml' file, the legacy configuration file. There's no special munging needed
@@ -150,11 +156,12 @@ module Bolt
150
156
  def self.load_bolt_yaml(dir)
151
157
  filepath = dir + BOLT_CONFIG_NAME
152
158
  data = Bolt::Util.read_yaml_hash(filepath, 'config')
159
+ logs = [{ debug: "Loaded configuration from #{filepath}" }]
153
160
  deprecations = [{ type: 'Using bolt.yaml for system configuration',
154
161
  msg: "Configuration file #{filepath} is deprecated and will be removed in a future version "\
155
162
  "of Bolt. Use '#{dir + BOLT_DEFAULTS_NAME}' instead." }]
156
163
 
157
- { filepath: filepath, data: data, warnings: [], deprecations: deprecations }
164
+ { filepath: filepath, data: data, logs: logs, deprecations: deprecations }
158
165
  end
159
166
 
160
167
  def self.load_defaults(project)
@@ -187,13 +194,13 @@ module Bolt
187
194
  unless config_data.is_a?(Array)
188
195
  config_data = [{ filepath: project.config_file,
189
196
  data: config_data,
190
- warnings: [],
197
+ logs: [],
191
198
  deprecations: [] }]
192
199
  end
193
200
 
194
- @logger = Logging.logger[self]
201
+ @logger = Bolt::Logger.logger(self)
195
202
  @project = project
196
- @warnings = @project.warnings.dup
203
+ @logs = @project.logs.dup
197
204
  @deprecations = @project.deprecations.dup
198
205
  @transports = {}
199
206
  @config_files = []
@@ -204,7 +211,7 @@ module Bolt
204
211
  'compile-concurrency' => Etc.nprocessors,
205
212
  'concurrency' => default_concurrency,
206
213
  'format' => 'human',
207
- 'log' => { 'console' => {}, 'bolt-debug.log' => { 'level' => 'debug', 'append' => false } },
214
+ 'log' => { 'console' => {} },
208
215
  'plugin_hooks' => {},
209
216
  'plugins' => {},
210
217
  'puppetdb' => {},
@@ -213,8 +220,15 @@ module Bolt
213
220
  'transport' => 'ssh'
214
221
  }
215
222
 
223
+ if project.path.directory?
224
+ default_data['log']['bolt-debug.log'] = {
225
+ 'level' => 'debug',
226
+ 'append' => false
227
+ }
228
+ end
229
+
216
230
  loaded_data = config_data.each_with_object([]) do |data, acc|
217
- @warnings.concat(data[:warnings]) if data[:warnings].any?
231
+ @logs.concat(data[:logs]) if data[:logs].any?
218
232
  @deprecations.concat(data[:deprecations]) if data[:deprecations].any?
219
233
 
220
234
  if data[:data].any?
@@ -330,10 +344,26 @@ module Bolt
330
344
  end
331
345
 
332
346
  private def update_logs(logs)
347
+ begin
348
+ if logs['bolt-debug.log'] && logs['bolt-debug.log'] != 'disable'
349
+ FileUtils.touch(File.expand_path('bolt-debug.log', @project.path))
350
+ end
351
+ rescue StandardError
352
+ logs.delete('bolt-debug.log')
353
+ end
354
+
333
355
  logs.each_with_object({}) do |(key, val), acc|
334
- next unless val.is_a?(Hash)
356
+ # Remove any disabled logs
357
+ next if val == 'disable'
335
358
 
336
359
  name = normalize_log(key)
360
+
361
+ # But otherwise it has to be a Hash
362
+ unless val.is_a?(Hash)
363
+ raise Bolt::ValidationError,
364
+ "config of log #{name} must be a Hash, received #{val.class} #{val.inspect}"
365
+ end
366
+
337
367
  acc[name] = val.slice('append', 'level')
338
368
  .transform_keys(&:to_sym)
339
369
 
@@ -358,7 +388,7 @@ module Bolt
358
388
  def validate
359
389
  if @data['future']
360
390
  msg = "Configuration option 'future' no longer exposes future behavior."
361
- @warnings << { option: 'future', msg: msg }
391
+ @logs << { warn: msg }
362
392
  end
363
393
 
364
394
  keys = OPTIONS.keys - %w[plugins plugin_hooks puppetdb]
@@ -176,7 +176,9 @@ module Bolt
176
176
  description: "A map of configuration for the logfile output. Under `log`, you can configure log options "\
177
177
  "for `console` and add configuration for individual log files, such as "\
178
178
  "`~/.puppetlabs/bolt/debug.log`. Individual log files must be valid filepaths. If the log "\
179
- "file does not exist, then Bolt will create it before logging information.",
179
+ "file does not exist, then Bolt will create it before logging information. Set the value to "\
180
+ "`disable` to remove a log file defined at an earlier level of the config hierarchy. By "\
181
+ "default, Bolt logs to a bolt-debug.log file in the Bolt project directory.",
180
182
  type: Hash,
181
183
  properties: {
182
184
  "console" => {
@@ -194,7 +196,8 @@ module Bolt
194
196
  },
195
197
  additionalProperties: {
196
198
  description: "Configuration for the logfile output.",
197
- type: Hash,
199
+ type: [String, Hash],
200
+ enum: ['disable'],
198
201
  properties: {
199
202
  "append" => {
200
203
  description: "Whether to append output to an existing log file.",
@@ -224,6 +227,24 @@ module Bolt
224
227
  _example: ["~/.puppetlabs/bolt/modules", "~/.puppetlabs/bolt/site-modules"],
225
228
  _default: ["project/modules", "project/site-modules", "project/site"]
226
229
  },
230
+ "modules" => {
231
+ description: "A list of module dependencies for the project. Each dependency is a map of data specifying "\
232
+ "the module to install. To install the project's module dependencies, run the `bolt module "\
233
+ "install` command.",
234
+ type: Array,
235
+ items: {
236
+ type: Hash,
237
+ required: ["name"],
238
+ properties: {
239
+ "name" => {
240
+ description: "The name of the module.",
241
+ type: String
242
+ }
243
+ }
244
+ },
245
+ _plugin: false,
246
+ _example: [{ "name" => "puppetlabs-mysql" }, { "name" => "puppetlabs-apache" }]
247
+ },
227
248
  "name" => {
228
249
  description: "The name of the Bolt project. When this option is configured, the project is considered a "\
229
250
  "[Bolt project](experimental_features.md#bolt-projects), allowing Bolt to load content from "\
@@ -473,6 +494,7 @@ module Bolt
473
494
  inventoryfile
474
495
  log
475
496
  modulepath
497
+ modules
476
498
  name
477
499
  plans
478
500
  plugin_hooks
@@ -40,7 +40,7 @@ module Bolt
40
40
  require 'concurrent'
41
41
 
42
42
  @analytics = analytics
43
- @logger = Logging.logger[self]
43
+ @logger = Bolt::Logger.logger(self)
44
44
 
45
45
  @transports = Bolt::TRANSPORTS.each_with_object({}) do |(key, val), coll|
46
46
  coll[key.to_s] = if key == :remote
@@ -46,10 +46,13 @@ module Bolt
46
46
  end
47
47
 
48
48
  def self.from_config(config, plugins)
49
+ logger = Bolt::Logger.logger(self)
50
+
49
51
  if ENV.include?(ENVIRONMENT_VAR)
50
52
  begin
51
53
  data = YAML.safe_load(ENV[ENVIRONMENT_VAR])
52
54
  raise Bolt::ParseError, "Could not parse inventory from $#{ENVIRONMENT_VAR}" unless data.is_a?(Hash)
55
+ logger.debug("Loaded inventory from environment variable #{ENVIRONMENT_VAR}")
53
56
  rescue Psych::Exception
54
57
  raise Bolt::ParseError, "Could not parse inventory from $#{ENVIRONMENT_VAR}"
55
58
  end
@@ -57,8 +60,12 @@ module Bolt
57
60
  data = if config.inventoryfile
58
61
  Bolt::Util.read_yaml_hash(config.inventoryfile, 'inventory')
59
62
  else
60
- Bolt::Util.read_optional_yaml_hash(config.default_inventoryfile, 'inventory')
63
+ i = Bolt::Util.read_optional_yaml_hash(config.default_inventoryfile, 'inventory')
64
+ logger.debug("Loaded inventory from #{config.default_inventoryfile}") if i
65
+ i
61
66
  end
67
+ # This avoids rubocop complaining about identical conditionals
68
+ logger.debug("Loaded inventory from #{config.inventoryfile}") if config.inventoryfile
62
69
  end
63
70
 
64
71
  # Resolve plugin references from transport config
@@ -19,7 +19,7 @@ module Bolt
19
19
  CONFIG_KEYS = Bolt::Config::INVENTORY_OPTIONS.keys
20
20
 
21
21
  def initialize(input, plugins)
22
- @logger = Logging.logger[self]
22
+ @logger = Bolt::Logger.logger(self)
23
23
  @plugins = plugins
24
24
 
25
25
  input = @plugins.resolve_top_level_references(input) if @plugins.reference?(input)
@@ -16,7 +16,7 @@ module Bolt
16
16
 
17
17
  # TODO: Pass transport config instead of config object
18
18
  def initialize(data, transport, transports, plugins)
19
- @logger = Logging.logger[self]
19
+ @logger = Bolt::Logger.logger(self)
20
20
  @data = data || {}
21
21
  @transport = transport
22
22
  @config = transports
@@ -13,7 +13,7 @@ module Bolt
13
13
  raise Bolt::Inventory::ValidationError.new("Target must have either a name or uri", nil)
14
14
  end
15
15
 
16
- @logger = Logging.logger[inventory]
16
+ @logger = Bolt::Logger.logger(inventory)
17
17
 
18
18
  # If the target isn't mentioned by any groups, it won't have a uri or
19
19
  # name and we will use the target_name as both
@@ -28,7 +28,7 @@ module Bolt
28
28
  end
29
29
 
30
30
  def self.configure(destinations, color)
31
- root_logger = Logging.logger[:root]
31
+ root_logger = Bolt::Logger.logger(:root)
32
32
 
33
33
  root_logger.add_appenders Logging.appenders.stderr(
34
34
  'console',
@@ -66,6 +66,13 @@ module Bolt
66
66
  end
67
67
  end
68
68
 
69
+ # A helper to ensure the Logging library is always initialized with our
70
+ # custom log levels before retrieving a Logger instance.
71
+ def self.logger(name)
72
+ initialize_logging
73
+ Logging.logger[name]
74
+ end
75
+
69
76
  def self.analytics=(analytics)
70
77
  @analytics = analytics
71
78
  end
@@ -110,7 +117,7 @@ module Bolt
110
117
  def self.warn_once(type, msg)
111
118
  @mutex.synchronize {
112
119
  @warnings ||= []
113
- @logger ||= Logging.logger[self]
120
+ @logger ||= Bolt::Logger.logger(self)
114
121
  unless @warnings.include?(type)
115
122
  @logger.warn(msg)
116
123
  @warnings << type
@@ -7,7 +7,7 @@ module Bolt
7
7
  class Logger < Bolt::Outputter
8
8
  def initialize(verbose, trace)
9
9
  super(false, verbose, trace)
10
- @logger = Logging.logger[self]
10
+ @logger = Bolt::Logger.logger(self)
11
11
  end
12
12
 
13
13
  def handle_event(event)
@@ -65,7 +65,7 @@ module Bolt
65
65
  @resource_types = resource_types
66
66
  @project = project
67
67
 
68
- @logger = Logging.logger[self]
68
+ @logger = Bolt::Logger.logger(self)
69
69
  if modulepath && !modulepath.empty?
70
70
  @logger.debug("Loading modules from #{@modulepath.join(File::PATH_SEPARATOR)}")
71
71
  end
@@ -76,7 +76,7 @@ module Bolt
76
76
  # Puppet logging is global so this is class method to avoid confusion
77
77
  def self.configure_logging
78
78
  Puppet::Util::Log.destinations.clear
79
- Puppet::Util::Log.newdestination(Logging.logger['Puppet'])
79
+ Puppet::Util::Log.newdestination(Bolt::Logger.logger('Puppet'))
80
80
  # Defer all log level decisions to the Logging library by telling Puppet
81
81
  # to log everything
82
82
  Puppet.settings[:log_level] = 'debug'
@@ -141,6 +141,19 @@ module Bolt
141
141
  end
142
142
  end
143
143
 
144
+ def detect_project_conflict(project, environment)
145
+ return unless project && project.load_as_module?
146
+ # The environment modulepath has stripped out non-existent directories,
147
+ # so we don't need to check for them
148
+ modules = environment.modulepath.flat_map do |path|
149
+ Dir.children(path).select { |name| Puppet::Module.is_module_directory?(name, path) }
150
+ end
151
+ if modules.include?(project.name)
152
+ Bolt::Logger.warn_once("project shadows module",
153
+ "The project '#{project.name}' shadows an existing module of the same name")
154
+ end
155
+ end
156
+
144
157
  # Runs a block in a PAL script compiler configured for Bolt. Catches
145
158
  # exceptions thrown by the block and re-raises them ensuring they are
146
159
  # Bolt::Errors since the script compiler block will squash all exceptions.
@@ -149,15 +162,13 @@ module Bolt
149
162
  setup
150
163
  r = Puppet::Pal.in_tmp_environment('bolt', modulepath: @modulepath, facts: {}) do |pal|
151
164
  # Only load the project if it a) exists, b) has a name it can be loaded with
152
- bolt_project = @project if @project&.name
153
- # Puppet currently won't receive the project unless it is a named project. Since
154
- # the download_file plan function needs access to the project path, add it to the
155
- # context.
156
- bolt_project_data = @project
157
- Puppet.override(bolt_project: bolt_project,
158
- bolt_project_data: bolt_project_data,
165
+ Puppet.override(bolt_project: @project,
159
166
  yaml_plan_instantiator: Bolt::PAL::YamlPlan::Loader) do
160
- pal.with_script_compiler do |compiler|
167
+ # Because this has the side effect of loading and caching the list
168
+ # of modules, it must happen *after* we have overridden
169
+ # bolt_project or the project will be ignored
170
+ detect_project_conflict(@project, Puppet.lookup(:environments).get('bolt'))
171
+ pal.with_script_compiler(set_local_facts: false) do |compiler|
161
172
  alias_types(compiler)
162
173
  register_resource_types(Puppet.lookup(:loaders)) if @resource_types
163
174
  begin
@@ -7,7 +7,7 @@ module Bolt
7
7
  class YamlPlan
8
8
  class Evaluator
9
9
  def initialize(analytics = Bolt::Analytics::NoopClient.new)
10
- @logger = Logging.logger[self]
10
+ @logger = Bolt::Logger.logger(self)
11
11
  @analytics = analytics
12
12
  @evaluator = Puppet::Pops::Parser::EvaluatingParser.new
13
13
  end
@@ -19,7 +19,7 @@ module Bolt
19
19
  def initialize(config:, context:)
20
20
  pdb_config = Bolt::PuppetDB::Config.load_config(config, context.boltdir)
21
21
  @puppetdb_client = Bolt::PuppetDB::Client.new(pdb_config)
22
- @logger = Logging.logger[self]
22
+ @logger = Bolt::Logger.logger(self)
23
23
  end
24
24
 
25
25
  def name
@@ -17,37 +17,53 @@ module Bolt
17
17
  }.freeze
18
18
 
19
19
  attr_reader :path, :data, :config_file, :inventory_file, :modulepath, :hiera_config,
20
- :puppetfile, :rerunfile, :type, :resource_types, :warnings, :project_file,
20
+ :puppetfile, :rerunfile, :type, :resource_types, :logs, :project_file,
21
21
  :deprecations, :downloads, :plans_path
22
22
 
23
- def self.default_project
24
- create_project(File.expand_path(File.join('~', '.puppetlabs', 'bolt')), 'user')
23
+ def self.default_project(logs = [])
24
+ create_project(File.expand_path(File.join('~', '.puppetlabs', 'bolt')), 'user', logs)
25
25
  # If homedir isn't defined use the system config path
26
26
  rescue ArgumentError
27
- create_project(Bolt::Config.system_path, 'system')
27
+ create_project(Bolt::Config.system_path, 'system', logs)
28
28
  end
29
29
 
30
30
  # Search recursively up the directory hierarchy for the Project. Look for a
31
31
  # directory called Boltdir or a file called bolt.yaml (for a control repo
32
32
  # type Project). Otherwise, repeat the check on each directory up the
33
33
  # hierarchy, falling back to the default if we reach the root.
34
- def self.find_boltdir(dir)
34
+ def self.find_boltdir(dir, logs = [])
35
35
  dir = Pathname.new(dir)
36
36
 
37
37
  if (dir + BOLTDIR_NAME).directory?
38
- create_project(dir + BOLTDIR_NAME, 'embedded')
38
+ create_project(dir + BOLTDIR_NAME, 'embedded', logs)
39
39
  elsif (dir + 'bolt.yaml').file? || (dir + 'bolt-project.yaml').file?
40
- create_project(dir, 'local')
40
+ create_project(dir, 'local', logs)
41
41
  elsif dir.root?
42
- default_project
42
+ default_project(logs)
43
43
  else
44
- find_boltdir(dir.parent)
44
+ logs << { debug: "Did not detect Boltdir, bolt.yaml, or bolt-project.yaml at '#{dir}'. "\
45
+ "This directory won't be loaded as a project." }
46
+ find_boltdir(dir.parent, logs)
45
47
  end
46
48
  end
47
49
 
48
- def self.create_project(path, type = 'option')
50
+ def self.create_project(path, type = 'option', logs = [])
49
51
  fullpath = Pathname.new(path).expand_path
50
52
 
53
+ if type == 'user'
54
+ begin
55
+ # This is already expanded if the type is user
56
+ FileUtils.mkdir_p(path)
57
+ rescue StandardError
58
+ logs << { warn: "Could not create default project at #{path}. Continuing without a writeable project. "\
59
+ "Log and rerun files will not be written." }
60
+ end
61
+ end
62
+
63
+ if type == 'option' && !File.directory?(path)
64
+ raise Bolt::Error.new("Could not find project at #{path}", "bolt/project-error")
65
+ end
66
+
51
67
  if !Bolt::Util.windows? && type != 'environment' && fullpath.world_writable?
52
68
  raise Bolt::Error.new(
53
69
  "Project directory '#{fullpath}' is world-writable which poses a security risk. Set "\
@@ -58,15 +74,18 @@ module Bolt
58
74
 
59
75
  project_file = File.join(fullpath, 'bolt-project.yaml')
60
76
  data = Bolt::Util.read_optional_yaml_hash(File.expand_path(project_file), 'project')
61
- new(data, path, type)
77
+ default = type =~ /user|system/ ? 'default ' : ''
78
+ exist = File.exist?(File.expand_path(project_file))
79
+ logs << { info: "Loaded #{default}project from '#{fullpath}'" } if exist
80
+ new(data, path, type, logs)
62
81
  end
63
82
 
64
- def initialize(raw_data, path, type = 'option')
83
+ def initialize(raw_data, path, type = 'option', logs = [])
65
84
  @path = Pathname.new(path).expand_path
66
85
 
67
86
  @project_file = @path + 'bolt-project.yaml'
68
87
 
69
- @warnings = []
88
+ @logs = logs
70
89
  @deprecations = []
71
90
  if (@path + 'bolt.yaml').file? && project_file?
72
91
  msg = "Project-level configuration in bolt.yaml is deprecated if using bolt-project.yaml. "\
@@ -88,7 +107,7 @@ module Bolt
88
107
  tc = Bolt::Config::INVENTORY_OPTIONS.keys & raw_data.keys
89
108
  if tc.any?
90
109
  msg = "Transport configuration isn't supported in bolt-project.yaml. Ignoring keys #{tc}"
91
- @warnings << { msg: msg }
110
+ @logs << { warn: msg }
92
111
  end
93
112
 
94
113
  @data = raw_data.reject { |k, _| Bolt::Config::INVENTORY_OPTIONS.include?(k) }
@@ -98,7 +117,7 @@ module Bolt
98
117
  @config_file = if (Bolt::Config::BOLT_OPTIONS & @data.keys).any?
99
118
  if (@path + 'bolt.yaml').file?
100
119
  msg = "bolt-project.yaml contains valid config keys, bolt.yaml will be ignored"
101
- @warnings << { msg: msg }
120
+ @logs << { warn: msg }
102
121
  end
103
122
  @project_file
104
123
  else
@@ -114,7 +133,9 @@ module Bolt
114
133
  # This API is used to prepend the project as a module to Puppet's internal
115
134
  # module_references list. CHANGE AT YOUR OWN RISK
116
135
  def to_h
117
- { path: @path.to_s, name: name }
136
+ { path: @path.to_s,
137
+ name: name,
138
+ load_as_module?: load_as_module? }
118
139
  end
119
140
 
120
141
  def eql?(other)
@@ -126,6 +147,10 @@ module Bolt
126
147
  @project_file.file?
127
148
  end
128
149
 
150
+ def load_as_module?
151
+ !name.nil?
152
+ end
153
+
129
154
  def name
130
155
  @data['name']
131
156
  end
@@ -138,6 +163,10 @@ module Bolt
138
163
  @data['plans']
139
164
  end
140
165
 
166
+ def modules
167
+ @data['modules']
168
+ end
169
+
141
170
  def validate
142
171
  if name
143
172
  if name !~ Bolt::Module::MODULE_NAME_REGEX
@@ -151,7 +180,7 @@ module Bolt
151
180
  end
152
181
  else
153
182
  message = "No project name is specified in bolt-project.yaml. Project-level content will not be available."
154
- @warnings << { msg: message }
183
+ @logs << { warn: message }
155
184
  end
156
185
 
157
186
  %w[tasks plans].each do |conf|
@@ -159,6 +188,23 @@ module Bolt
159
188
  raise Bolt::ValidationError, "'#{conf}' in bolt-project.yaml must be an array"
160
189
  end
161
190
  end
191
+
192
+ if @data['modules']
193
+ unless @data['modules'].is_a?(Array)
194
+ raise Bolt::ValidationError, "'modules' in bolt-project.yaml must be an array"
195
+ end
196
+
197
+ @data['modules'].each do |mod|
198
+ next if mod.is_a?(Hash)
199
+ raise Bolt::ValidationError, "Module declaration #{mod.inspect} must be a hash"
200
+ end
201
+
202
+ unknown_keys = data['modules'].flat_map(&:keys).uniq - ['name']
203
+ if unknown_keys.any?
204
+ @logs << { warn: "Module declarations in bolt-project.yaml only support a name key. Ignoring "\
205
+ "unsupported keys: #{unknown_keys.join(', ')}." }
206
+ end
207
+ end
162
208
  end
163
209
 
164
210
  def check_deprecated_file