bolt 2.25.0 → 2.30.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 (63) hide show
  1. checksums.yaml +4 -4
  2. data/Puppetfile +4 -3
  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 +116 -26
  9. data/lib/bolt/catalog.rb +5 -3
  10. data/lib/bolt/cli.rb +194 -185
  11. data/lib/bolt/config.rb +61 -26
  12. data/lib/bolt/config/options.rb +35 -2
  13. data/lib/bolt/executor.rb +2 -2
  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 +35 -21
  19. data/lib/bolt/module_installer.rb +172 -0
  20. data/lib/bolt/outputter.rb +4 -0
  21. data/lib/bolt/outputter/human.rb +53 -11
  22. data/lib/bolt/outputter/json.rb +7 -1
  23. data/lib/bolt/outputter/logger.rb +3 -3
  24. data/lib/bolt/pal.rb +29 -20
  25. data/lib/bolt/pal/yaml_plan/evaluator.rb +1 -1
  26. data/lib/bolt/plugin/module.rb +1 -1
  27. data/lib/bolt/plugin/puppetdb.rb +1 -1
  28. data/lib/bolt/project.rb +89 -28
  29. data/lib/bolt/project_migrator.rb +80 -0
  30. data/lib/bolt/project_migrator/base.rb +39 -0
  31. data/lib/bolt/project_migrator/config.rb +67 -0
  32. data/lib/bolt/project_migrator/inventory.rb +67 -0
  33. data/lib/bolt/project_migrator/modules.rb +198 -0
  34. data/lib/bolt/puppetdb/client.rb +1 -1
  35. data/lib/bolt/puppetdb/config.rb +1 -1
  36. data/lib/bolt/puppetfile.rb +142 -0
  37. data/lib/bolt/puppetfile/installer.rb +43 -0
  38. data/lib/bolt/puppetfile/module.rb +90 -0
  39. data/lib/bolt/r10k_log_proxy.rb +1 -1
  40. data/lib/bolt/rerun.rb +2 -2
  41. data/lib/bolt/result.rb +23 -0
  42. data/lib/bolt/shell.rb +1 -1
  43. data/lib/bolt/shell/bash.rb +1 -1
  44. data/lib/bolt/task.rb +1 -1
  45. data/lib/bolt/transport/base.rb +5 -5
  46. data/lib/bolt/transport/docker/connection.rb +1 -1
  47. data/lib/bolt/transport/local/connection.rb +1 -1
  48. data/lib/bolt/transport/ssh.rb +1 -1
  49. data/lib/bolt/transport/ssh/connection.rb +1 -1
  50. data/lib/bolt/transport/ssh/exec_connection.rb +1 -1
  51. data/lib/bolt/transport/winrm.rb +1 -1
  52. data/lib/bolt/transport/winrm/connection.rb +1 -1
  53. data/lib/bolt/util.rb +52 -11
  54. data/lib/bolt/version.rb +1 -1
  55. data/lib/bolt_server/acl.rb +2 -2
  56. data/lib/bolt_server/base_config.rb +3 -3
  57. data/lib/bolt_server/config.rb +1 -1
  58. data/lib/bolt_server/file_cache.rb +12 -12
  59. data/lib/bolt_server/transport_app.rb +125 -26
  60. data/lib/bolt_spec/bolt_context.rb +4 -4
  61. data/lib/bolt_spec/plans/mock_executor.rb +1 -1
  62. metadata +15 -13
  63. data/lib/bolt/project_migrate.rb +0 -138
@@ -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 = []
@@ -221,7 +228,7 @@ module Bolt
221
228
  end
222
229
 
223
230
  loaded_data = config_data.each_with_object([]) do |data, acc|
224
- @warnings.concat(data[:warnings]) if data[:warnings].any?
231
+ @logs.concat(data[:logs]) if data[:logs].any?
225
232
  @deprecations.concat(data[:deprecations]) if data[:deprecations].any?
226
233
 
227
234
  if data[:data].any?
@@ -337,10 +344,26 @@ module Bolt
337
344
  end
338
345
 
339
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
+
340
355
  logs.each_with_object({}) do |(key, val), acc|
341
- next unless val.is_a?(Hash)
356
+ # Remove any disabled logs
357
+ next if val == 'disable'
342
358
 
343
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
+
344
367
  acc[name] = val.slice('append', 'level')
345
368
  .transform_keys(&:to_sym)
346
369
 
@@ -365,7 +388,13 @@ module Bolt
365
388
  def validate
366
389
  if @data['future']
367
390
  msg = "Configuration option 'future' no longer exposes future behavior."
368
- @warnings << { option: 'future', msg: msg }
391
+ @logs << { warn: msg }
392
+ end
393
+
394
+ if @project.modules && @data['modulepath']&.include?(@project.managed_moduledir.to_s)
395
+ raise Bolt::ValidationError,
396
+ "Found invalid path in modulepath: #{@project.managed_moduledir}. This path "\
397
+ "is automatically appended to the modulepath and cannot be configured."
369
398
  end
370
399
 
371
400
  keys = OPTIONS.keys - %w[plugins plugin_hooks puppetdb]
@@ -422,7 +451,13 @@ module Bolt
422
451
  end
423
452
 
424
453
  def modulepath
425
- @data['modulepath'] || @project.modulepath
454
+ path = @data['modulepath'] || @project.modulepath
455
+
456
+ if @project.modules
457
+ path + [@project.managed_moduledir.to_s]
458
+ else
459
+ path
460
+ end
426
461
  end
427
462
 
428
463
  def modulepath=(value)
@@ -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,35 @@ 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, String],
237
+ required: ["name"],
238
+ properties: {
239
+ "name" => {
240
+ description: "The name of the module.",
241
+ type: String
242
+ },
243
+ "version_requirement" => {
244
+ description: "The version requirement for the module. Accepts a specific version (1.2.3), version "\
245
+ "shorthand (1.2.x), or a version range (>= 1.2.0).",
246
+ type: String
247
+ }
248
+ }
249
+ },
250
+ _plugin: false,
251
+ _example: [
252
+ { "name" => "puppetlabs-mysql" },
253
+ "puppetlabs-facts",
254
+ { "name" => "puppetlabs-apache", "version_requirement" => "5.5.0" },
255
+ { "name" => "puppetlabs-puppetdb", "version_requirement" => "7.x" },
256
+ { "name" => "puppetlabs-firewall", "version_requirement" => ">= 1.0.0 < 3.0.0" }
257
+ ]
258
+ },
227
259
  "name" => {
228
260
  description: "The name of the Bolt project. When this option is configured, the project is considered a "\
229
261
  "[Bolt project](experimental_features.md#bolt-projects), allowing Bolt to load content from "\
@@ -473,6 +505,7 @@ module Bolt
473
505
  inventoryfile
474
506
  log
475
507
  modulepath
508
+ modules
476
509
  name
477
510
  plans
478
511
  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
@@ -227,7 +227,7 @@ module Bolt
227
227
  data[:resource_mean] = sum / resource_counts.length
228
228
  end
229
229
 
230
- @analytics&.event('Apply', 'ast', data)
230
+ @analytics&.event('Apply', 'ast', **data)
231
231
  end
232
232
 
233
233
  def report_yaml_plan(plan)
@@ -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
@@ -4,6 +4,10 @@ require 'logging'
4
4
 
5
5
  module Bolt
6
6
  module Logger
7
+ LEVELS = %w[trace debug info notice warn error fatal].freeze
8
+ @mutex = Mutex.new
9
+ @warnings = Set.new
10
+
7
11
  # This method provides a single point-of-entry to setup logging for both
8
12
  # the CLI and for tests. This is necessary because we define custom log
9
13
  # levels which create corresponding methods on the logger instances;
@@ -11,24 +15,29 @@ module Bolt
11
15
  # will fail.
12
16
  def self.initialize_logging
13
17
  # Initialization isn't idempotent and will result in warnings about const
14
- # redefs, so skip it if it's already been initialized
15
- return if Logging.initialized?
16
-
17
- Logging.init :trace, :debug, :info, :notice, :warn, :error, :fatal, :any
18
- @mutex = Mutex.new
19
-
20
- Logging.color_scheme(
21
- 'bolt',
22
- lines: {
23
- warn: :yellow,
24
- error: :red,
25
- fatal: %i[white on_red]
26
- }
27
- )
18
+ # redefs, so skip it if the log levels we expect are present. If it's
19
+ # already been initialized with an insufficient set of levels, go ahead
20
+ # and call init anyway or we'll have failures when calling log methods
21
+ # for missing levels.
22
+ unless levels & LEVELS == LEVELS
23
+ Logging.init(*LEVELS)
24
+ end
25
+
26
+ # As above, only create the color scheme if we haven't already created it.
27
+ unless Logging.color_scheme('bolt')
28
+ Logging.color_scheme(
29
+ 'bolt',
30
+ lines: {
31
+ warn: :yellow,
32
+ error: :red,
33
+ fatal: %i[white on_red]
34
+ }
35
+ )
36
+ end
28
37
  end
29
38
 
30
39
  def self.configure(destinations, color)
31
- root_logger = Logging.logger[:root]
40
+ root_logger = Bolt::Logger.logger(:root)
32
41
 
33
42
  root_logger.add_appenders Logging.appenders.stderr(
34
43
  'console',
@@ -66,6 +75,13 @@ module Bolt
66
75
  end
67
76
  end
68
77
 
78
+ # A helper to ensure the Logging library is always initialized with our
79
+ # custom log levels before retrieving a Logger instance.
80
+ def self.logger(name)
81
+ initialize_logging
82
+ Logging.logger[name]
83
+ end
84
+
69
85
  def self.analytics=(analytics)
70
86
  @analytics = analytics
71
87
  end
@@ -108,14 +124,12 @@ module Bolt
108
124
  end
109
125
 
110
126
  def self.warn_once(type, msg)
111
- @mutex.synchronize {
112
- @warnings ||= []
113
- @logger ||= Logging.logger[self]
114
- unless @warnings.include?(type)
127
+ @mutex.synchronize do
128
+ @logger ||= Bolt::Logger.logger(self)
129
+ if @warnings.add?(type)
115
130
  @logger.warn(msg)
116
- @warnings << type
117
131
  end
118
- }
132
+ end
119
133
  end
120
134
 
121
135
  def self.deprecation_warning(type, msg)
@@ -0,0 +1,172 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bolt/error'
4
+ require 'bolt/logger'
5
+
6
+ module Bolt
7
+ class ModuleInstaller
8
+ def initialize(outputter, pal)
9
+ @outputter = outputter
10
+ @pal = pal
11
+ @logger = Bolt::Logger.logger(self)
12
+ end
13
+
14
+ # Adds a single module to the project.
15
+ #
16
+ def add(name, modules, puppetfile_path, moduledir, config_path)
17
+ require 'bolt/puppetfile'
18
+
19
+ # If the project configuration file already includes this module,
20
+ # exit early.
21
+ puppetfile = Bolt::Puppetfile.new(modules)
22
+ new_module = Bolt::Puppetfile::Module.from_hash('name' => name)
23
+
24
+ if puppetfile.modules.include?(new_module)
25
+ @outputter.print_message "Project configuration file #{config_path} already "\
26
+ "includes module #{new_module}. Nothing to do."
27
+ return true
28
+ end
29
+
30
+ # If the Puppetfile exists, make sure it's managed by Bolt.
31
+ if puppetfile_path.exist?
32
+ assert_managed_puppetfile(puppetfile, puppetfile_path)
33
+ end
34
+
35
+ # Create a Puppetfile object that includes the new module and its
36
+ # dependencies. We error early here so we don't add the new module to the
37
+ # project config or modify the Puppetfile.
38
+ puppetfile = add_new_module_to_puppetfile(new_module, modules, puppetfile_path)
39
+
40
+ # Add the module to the project configuration.
41
+ @outputter.print_message "Updating project configuration file at #{config_path}"
42
+
43
+ data = Bolt::Util.read_yaml_hash(config_path, 'project')
44
+ data['modules'] ||= []
45
+ data['modules'] << { 'name' => new_module.title }
46
+
47
+ begin
48
+ File.write(config_path, data.to_yaml)
49
+ rescue SystemCallError => e
50
+ raise Bolt::FileError.new(
51
+ "Unable to update project configuration file: #{e.message}",
52
+ config
53
+ )
54
+ end
55
+
56
+ # Write the Puppetfile.
57
+ @outputter.print_message "Writing Puppetfile at #{puppetfile_path}"
58
+ puppetfile.write(puppetfile_path, moduledir)
59
+
60
+ # Install the modules.
61
+ install_puppetfile(puppetfile_path, moduledir)
62
+ end
63
+
64
+ # Creates a new Puppetfile that includes the new module and its dependencies.
65
+ #
66
+ private def add_new_module_to_puppetfile(new_module, modules, path)
67
+ @outputter.print_message "Resolving module dependencies, this may take a moment"
68
+
69
+ # If there is an existing Puppetfile, add the new module and attempt
70
+ # to resolve. This will not update the versions of any installed modules.
71
+ if path.exist?
72
+ puppetfile = Bolt::Puppetfile.parse(path)
73
+ puppetfile.add_modules(new_module)
74
+
75
+ begin
76
+ puppetfile.resolve
77
+ return puppetfile
78
+ rescue Bolt::Error
79
+ @logger.debug "Unable to find a version of #{new_module} compatible "\
80
+ "with installed modules. Attempting to re-resolve modules "\
81
+ "from project configuration; some versions of installed "\
82
+ "modules may change."
83
+ end
84
+ end
85
+
86
+ # If there is not an existing Puppetfile, or resolving with pinned
87
+ # modules fails, resolve all of the module declarations with the new
88
+ # module.
89
+ puppetfile = Bolt::Puppetfile.new(modules)
90
+ puppetfile.add_modules(new_module)
91
+ puppetfile.resolve
92
+ puppetfile
93
+ end
94
+
95
+ # Installs a project's module dependencies.
96
+ #
97
+ def install(modules, path, moduledir, force: false, resolve: true)
98
+ require 'bolt/puppetfile'
99
+
100
+ puppetfile = Bolt::Puppetfile.new(modules)
101
+
102
+ # If the Puppetfile exists, check if it includes specs for each declared
103
+ # module, erroring if there are any missing. Otherwise, resolve the
104
+ # module dependencies and write a new Puppetfile. Users can forcibly
105
+ # overwrite an existing Puppetfile with the '--force' option, or opt to
106
+ # install the Puppetfile as-is with --no-resolve.
107
+ #
108
+ # This is just if resolve is not false (nil should default to true)
109
+ if resolve != false
110
+ if path.exist? && !force
111
+ assert_managed_puppetfile(puppetfile, path)
112
+ else
113
+ @outputter.print_message "Resolving module dependencies, this may take a moment"
114
+ puppetfile.resolve
115
+
116
+ @outputter.print_message "Writing Puppetfile at #{path}"
117
+ # We get here either through 'bolt module install' which uses the
118
+ # managed modulepath (which isn't configurable) or through bolt
119
+ # project init --modules, which uses the default modulepath. This
120
+ # should be safe to assume that if `.modules/` is the moduledir the
121
+ # user is using the new workflow
122
+ if moduledir.basename == '.modules'
123
+ puppetfile.write(path, moduledir)
124
+ else
125
+ puppetfile.write(path)
126
+ end
127
+ end
128
+ end
129
+
130
+ # Install the modules.
131
+ install_puppetfile(path, moduledir)
132
+ end
133
+
134
+ # Installs the Puppetfile and generates types.
135
+ #
136
+ def install_puppetfile(path, moduledir, config = {})
137
+ require 'bolt/puppetfile/installer'
138
+
139
+ @outputter.print_message "Syncing modules from #{path} to #{moduledir}"
140
+ ok = Bolt::Puppetfile::Installer.new(config).install(path, moduledir)
141
+
142
+ # Automatically generate types after installing modules
143
+ @pal.generate_types
144
+
145
+ @outputter.print_puppetfile_result(ok, path, moduledir)
146
+
147
+ ok
148
+ end
149
+
150
+ # Asserts that an existing Puppetfile is managed by Bolt.
151
+ #
152
+ private def assert_managed_puppetfile(puppetfile, path)
153
+ existing_puppetfile = Bolt::Puppetfile.parse(path)
154
+
155
+ unless existing_puppetfile.modules.superset? puppetfile.modules
156
+ missing_modules = puppetfile.modules - existing_puppetfile.modules
157
+
158
+ message = <<~MESSAGE.chomp
159
+ Puppetfile #{path} is missing specifications for the following
160
+ module declarations:
161
+
162
+ #{missing_modules.map(&:to_hash).to_yaml.lines.drop(1).join.chomp}
163
+
164
+ This may not be a Puppetfile managed by Bolt. To forcibly overwrite the
165
+ Puppetfile, run 'bolt module install --force'.
166
+ MESSAGE
167
+
168
+ raise Bolt::Error.new(message, 'bolt/missing-module-specs')
169
+ end
170
+ end
171
+ end
172
+ end