bolt 2.28.0 → 2.33.1

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 (80) hide show
  1. checksums.yaml +4 -4
  2. data/Puppetfile +15 -14
  3. data/bolt-modules/boltlib/lib/puppet/functions/download_file.rb +1 -1
  4. data/bolt-modules/boltlib/lib/puppet/functions/facts.rb +6 -0
  5. data/bolt-modules/boltlib/lib/puppet/functions/puppetdb_query.rb +2 -2
  6. data/bolt-modules/boltlib/lib/puppet/functions/run_command.rb +1 -1
  7. data/bolt-modules/boltlib/lib/puppet/functions/run_script.rb +1 -1
  8. data/bolt-modules/boltlib/lib/puppet/functions/run_task.rb +1 -1
  9. data/bolt-modules/boltlib/lib/puppet/functions/run_task_with.rb +1 -1
  10. data/bolt-modules/boltlib/lib/puppet/functions/upload_file.rb +1 -1
  11. data/bolt-modules/boltlib/lib/puppet/functions/write_file.rb +2 -2
  12. data/bolt-modules/out/lib/puppet/functions/out/message.rb +44 -1
  13. data/bolt-modules/prompt/lib/puppet/functions/prompt.rb +3 -0
  14. data/guides/logging.txt +18 -0
  15. data/guides/module.txt +19 -0
  16. data/guides/modulepath.txt +25 -0
  17. data/lib/bolt/bolt_option_parser.rb +50 -28
  18. data/lib/bolt/catalog.rb +1 -1
  19. data/lib/bolt/cli.rb +159 -112
  20. data/lib/bolt/config.rb +13 -1
  21. data/lib/bolt/config/modulepath.rb +30 -0
  22. data/lib/bolt/config/options.rb +38 -9
  23. data/lib/bolt/config/transport/options.rb +2 -2
  24. data/lib/bolt/error.rb +4 -0
  25. data/lib/bolt/executor.rb +13 -13
  26. data/lib/bolt/inventory.rb +10 -9
  27. data/lib/bolt/logger.rb +26 -19
  28. data/lib/bolt/module_installer.rb +198 -0
  29. data/lib/bolt/{puppetfile → module_installer}/installer.rb +3 -2
  30. data/lib/bolt/module_installer/puppetfile.rb +117 -0
  31. data/lib/bolt/module_installer/puppetfile/forge_module.rb +54 -0
  32. data/lib/bolt/module_installer/puppetfile/git_module.rb +37 -0
  33. data/lib/bolt/module_installer/puppetfile/module.rb +26 -0
  34. data/lib/bolt/module_installer/resolver.rb +76 -0
  35. data/lib/bolt/module_installer/specs.rb +93 -0
  36. data/lib/bolt/module_installer/specs/forge_spec.rb +84 -0
  37. data/lib/bolt/module_installer/specs/git_spec.rb +178 -0
  38. data/lib/bolt/outputter.rb +2 -45
  39. data/lib/bolt/outputter/human.rb +78 -18
  40. data/lib/bolt/outputter/json.rb +22 -7
  41. data/lib/bolt/outputter/logger.rb +2 -2
  42. data/lib/bolt/pal.rb +55 -45
  43. data/lib/bolt/pal/yaml_plan.rb +4 -2
  44. data/lib/bolt/pal/yaml_plan/evaluator.rb +23 -1
  45. data/lib/bolt/pal/yaml_plan/loader.rb +14 -9
  46. data/lib/bolt/plugin.rb +1 -1
  47. data/lib/bolt/plugin/module.rb +1 -1
  48. data/lib/bolt/project.rb +32 -22
  49. data/lib/bolt/project_migrator.rb +80 -0
  50. data/lib/bolt/project_migrator/base.rb +39 -0
  51. data/lib/bolt/project_migrator/config.rb +67 -0
  52. data/lib/bolt/project_migrator/inventory.rb +67 -0
  53. data/lib/bolt/project_migrator/modules.rb +200 -0
  54. data/lib/bolt/result.rb +23 -11
  55. data/lib/bolt/shell/bash.rb +15 -9
  56. data/lib/bolt/shell/powershell.rb +11 -6
  57. data/lib/bolt/transport/base.rb +18 -18
  58. data/lib/bolt/transport/docker.rb +23 -6
  59. data/lib/bolt/transport/orch.rb +23 -14
  60. data/lib/bolt/transport/remote.rb +2 -2
  61. data/lib/bolt/transport/simple.rb +6 -6
  62. data/lib/bolt/transport/ssh/connection.rb +1 -1
  63. data/lib/bolt/util.rb +41 -0
  64. data/lib/bolt/version.rb +1 -1
  65. data/lib/bolt_server/acl.rb +2 -2
  66. data/lib/bolt_server/base_config.rb +3 -3
  67. data/lib/bolt_server/schemas/partials/task.json +17 -2
  68. data/lib/bolt_server/transport_app.rb +93 -13
  69. data/lib/bolt_spec/bolt_context.rb +4 -2
  70. data/lib/bolt_spec/plans.rb +1 -1
  71. data/lib/bolt_spec/plans/action_stubs/command_stub.rb +1 -1
  72. data/lib/bolt_spec/plans/action_stubs/script_stub.rb +1 -1
  73. data/lib/bolt_spec/plans/mock_executor.rb +6 -6
  74. data/lib/bolt_spec/run.rb +1 -1
  75. metadata +31 -12
  76. data/lib/bolt/project_migrate.rb +0 -138
  77. data/lib/bolt/puppetfile.rb +0 -160
  78. data/lib/bolt/puppetfile/module.rb +0 -66
  79. data/lib/bolt_server/pe/pal.rb +0 -67
  80. data/modules/secure_env_vars/plans/init.pp +0 -20
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bolt/config'
4
+
5
+ module Bolt
6
+ class Config
7
+ class Modulepath
8
+ BOLTLIB_PATH = File.expand_path('../../../bolt-modules', __dir__)
9
+ MODULES_PATH = File.expand_path('../../../modules', __dir__)
10
+
11
+ # The user_modulepath only includes the original modulepath and is used during pluginsync.
12
+ # We don't want to pluginsync any of the content from BOLT_MODULES since that content
13
+ # includes core modules that can conflict with modules installed with an agent.
14
+ attr_reader :user_modulepath
15
+
16
+ def initialize(user_modulepath, boltlib_path: BOLTLIB_PATH, builtin_content_path: MODULES_PATH)
17
+ @user_modulepath = Array(user_modulepath).flatten
18
+ @boltlib_path = Array(boltlib_path).flatten
19
+ @builtin_content_path = Array(builtin_content_path).flatten
20
+ end
21
+
22
+ # The full_modulepath includes both the BOLTLIB
23
+ # path and the MODULES_PATH to ensure bolt functions and
24
+ # built-in content are available in the compliler
25
+ def full_modulepath
26
+ @boltlib_path + @user_modulepath + @builtin_content_path
27
+ end
28
+ end
29
+ end
30
+ end
@@ -233,17 +233,46 @@ module Bolt
233
233
  "install` command.",
234
234
  type: Array,
235
235
  items: {
236
- type: Hash,
237
- required: ["name"],
238
- properties: {
239
- "name" => {
240
- description: "The name of the module.",
241
- type: String
236
+ type: [Hash, String],
237
+ oneOf: [
238
+ {
239
+ required: ["name"],
240
+ properties: {
241
+ "name" => {
242
+ description: "The name of the module.",
243
+ type: String
244
+ },
245
+ "version_requirement" => {
246
+ description: "The version requirement for the module. Accepts a specific version (1.2.3), version "\
247
+ "shorthand (1.2.x), or a version range (>= 1.2.0).",
248
+ type: String
249
+ }
250
+ }
251
+ },
252
+ {
253
+ required: %w[git ref],
254
+ properties: {
255
+ "git" => {
256
+ description: "The URL to the public git repository.",
257
+ type: String
258
+ },
259
+ "ref" => {
260
+ description: "The git reference to check out. Can be either a branch, tag, or commit SHA.",
261
+ type: String
262
+ }
263
+ }
242
264
  }
243
- }
265
+ ]
244
266
  },
245
267
  _plugin: false,
246
- _example: [{ "name" => "puppetlabs-mysql" }, { "name" => "puppetlabs-apache" }]
268
+ _example: [
269
+ "puppetlabs-facts",
270
+ { "name" => "puppetlabs-mysql" },
271
+ { "name" => "puppetlabs-apache", "version_requirement" => "5.5.0" },
272
+ { "name" => "puppetlabs-puppetdb", "version_requirement" => "7.x" },
273
+ { "name" => "puppetlabs-firewall", "version_requirement" => ">= 1.0.0 < 3.0.0" },
274
+ { "git" => "https://github.com/puppetlabs/puppetlabs-apt", "ref" => "7.6.0" }
275
+ ]
247
276
  },
248
277
  "name" => {
249
278
  description: "The name of the Bolt project. When this option is configured, the project is considered a "\
@@ -318,7 +347,7 @@ module Bolt
318
347
  "server_urls" => {
319
348
  description: "An array containing the PuppetDB host to connect to. Include the protocol `https` "\
320
349
  "and the port, which is usually `8081`. For example, "\
321
- "`https://my-master.example.com:8081`.",
350
+ "`https://my-puppetdb-server.com:8081`.",
322
351
  type: Array,
323
352
  _example: ["https://puppet.example.com:8081"]
324
353
  },
@@ -357,7 +357,7 @@ module Bolt
357
357
  description: "The URL of the host used for API requests.",
358
358
  format: "uri",
359
359
  _plugin: true,
360
- _example: "https://api.example.com"
360
+ _example: "https://api.example.com:<port>"
361
361
  },
362
362
  "shell-command" => {
363
363
  type: String,
@@ -374,7 +374,7 @@ module Bolt
374
374
  },
375
375
  "ssh-command" => {
376
376
  type: [Array, String],
377
- description: "The command and flags to use when SSHing. This option is used when you need support for "\
377
+ description: "The command and options to use when SSHing. This option is used when you need support for "\
378
378
  "features or algorithms that are not supported by the net-ssh Ruby library. **This option "\
379
379
  "is experimental.** You can read more about this option in [Native SSH "\
380
380
  "transport](experimental_features.md#native-ssh-transport).",
@@ -24,6 +24,10 @@ module Bolt
24
24
  h
25
25
  end
26
26
 
27
+ def add_filelineno(details)
28
+ @details.merge!(details) unless @details['file']
29
+ end
30
+
27
31
  def to_json(opts = nil)
28
32
  to_h.to_json(opts)
29
33
  end
@@ -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)
@@ -253,33 +253,33 @@ module Bolt
253
253
  result
254
254
  end
255
255
 
256
- def run_command(targets, command, options = {})
256
+ def run_command(targets, command, options = {}, position = [])
257
257
  description = options.fetch(:description, "command '#{command}'")
258
258
  log_action(description, targets) do
259
259
  options[:run_as] = run_as if run_as && !options.key?(:run_as)
260
260
 
261
261
  batch_execute(targets) do |transport, batch|
262
262
  with_node_logging("Running command '#{command}'", batch) do
263
- transport.batch_command(batch, command, options, &method(:publish_event))
263
+ transport.batch_command(batch, command, options, position, &method(:publish_event))
264
264
  end
265
265
  end
266
266
  end
267
267
  end
268
268
 
269
- def run_script(targets, script, arguments, options = {})
269
+ def run_script(targets, script, arguments, options = {}, position = [])
270
270
  description = options.fetch(:description, "script #{script}")
271
271
  log_action(description, targets) do
272
272
  options[:run_as] = run_as if run_as && !options.key?(:run_as)
273
273
 
274
274
  batch_execute(targets) do |transport, batch|
275
275
  with_node_logging("Running script #{script} with '#{arguments.to_json}'", batch) do
276
- transport.batch_script(batch, script, arguments, options, &method(:publish_event))
276
+ transport.batch_script(batch, script, arguments, options, position, &method(:publish_event))
277
277
  end
278
278
  end
279
279
  end
280
280
  end
281
281
 
282
- def run_task(targets, task, arguments, options = {})
282
+ def run_task(targets, task, arguments, options = {}, position = [])
283
283
  description = options.fetch(:description, "task #{task.name}")
284
284
  log_action(description, targets) do
285
285
  options[:run_as] = run_as if run_as && !options.key?(:run_as)
@@ -287,13 +287,13 @@ module Bolt
287
287
 
288
288
  batch_execute(targets) do |transport, batch|
289
289
  with_node_logging("Running task #{task.name} with '#{arguments.to_json}'", batch) do
290
- transport.batch_task(batch, task, arguments, options, &method(:publish_event))
290
+ transport.batch_task(batch, task, arguments, options, position, &method(:publish_event))
291
291
  end
292
292
  end
293
293
  end
294
294
  end
295
295
 
296
- def run_task_with(target_mapping, task, options = {})
296
+ def run_task_with(target_mapping, task, options = {}, position = [])
297
297
  targets = target_mapping.keys
298
298
  description = options.fetch(:description, "task #{task.name}")
299
299
 
@@ -303,26 +303,26 @@ module Bolt
303
303
 
304
304
  batch_execute(targets) do |transport, batch|
305
305
  with_node_logging("Running task #{task.name}'", batch) do
306
- transport.batch_task_with(batch, task, target_mapping, options, &method(:publish_event))
306
+ transport.batch_task_with(batch, task, target_mapping, options, position, &method(:publish_event))
307
307
  end
308
308
  end
309
309
  end
310
310
  end
311
311
 
312
- def upload_file(targets, source, destination, options = {})
312
+ def upload_file(targets, source, destination, options = {}, position = [])
313
313
  description = options.fetch(:description, "file upload from #{source} to #{destination}")
314
314
  log_action(description, targets) do
315
315
  options[:run_as] = run_as if run_as && !options.key?(:run_as)
316
316
 
317
317
  batch_execute(targets) do |transport, batch|
318
318
  with_node_logging("Uploading file #{source} to #{destination}", batch) do
319
- transport.batch_upload(batch, source, destination, options, &method(:publish_event))
319
+ transport.batch_upload(batch, source, destination, options, position, &method(:publish_event))
320
320
  end
321
321
  end
322
322
  end
323
323
  end
324
324
 
325
- def download_file(targets, source, destination, options = {})
325
+ def download_file(targets, source, destination, options = {}, position = [])
326
326
  description = options.fetch(:description, "file download from #{source} to #{destination}")
327
327
 
328
328
  begin
@@ -337,7 +337,7 @@ module Bolt
337
337
 
338
338
  batch_execute(targets) do |transport, batch|
339
339
  with_node_logging("Downloading file #{source} to #{destination}", batch) do
340
- transport.batch_download(batch, source, destination, options, &method(:publish_event))
340
+ transport.batch_download(batch, source, destination, options, position, &method(:publish_event))
341
341
  end
342
342
  end
343
343
  end
@@ -56,16 +56,17 @@ module Bolt
56
56
  rescue Psych::Exception
57
57
  raise Bolt::ParseError, "Could not parse inventory from $#{ENVIRONMENT_VAR}"
58
58
  end
59
+ elsif config.inventoryfile
60
+ data = Bolt::Util.read_yaml_hash(config.inventoryfile, 'inventory')
61
+ logger.debug("Loaded inventory from #{config.inventoryfile}")
59
62
  else
60
- data = if config.inventoryfile
61
- Bolt::Util.read_yaml_hash(config.inventoryfile, 'inventory')
62
- else
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
66
- end
67
- # This avoids rubocop complaining about identical conditionals
68
- logger.debug("Loaded inventory from #{config.inventoryfile}") if config.inventoryfile
63
+ data = Bolt::Util.read_optional_yaml_hash(config.default_inventoryfile, 'inventory')
64
+
65
+ if config.default_inventoryfile.exist?
66
+ logger.debug("Loaded inventory from #{config.default_inventoryfile}")
67
+ else
68
+ logger.debug("Tried to load inventory from #{config.default_inventoryfile}, but the file does not exist")
69
+ end
69
70
  end
70
71
 
71
72
  # Resolve plugin references from transport config
@@ -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,20 +15,25 @@ 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)
@@ -115,14 +124,12 @@ module Bolt
115
124
  end
116
125
 
117
126
  def self.warn_once(type, msg)
118
- @mutex.synchronize {
119
- @warnings ||= []
127
+ @mutex.synchronize do
120
128
  @logger ||= Bolt::Logger.logger(self)
121
- unless @warnings.include?(type)
129
+ if @warnings.add?(type)
122
130
  @logger.warn(msg)
123
- @warnings << type
124
131
  end
125
- }
132
+ end
126
133
  end
127
134
 
128
135
  def self.deprecation_warning(type, msg)
@@ -0,0 +1,198 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bolt/error'
4
+ require 'bolt/logger'
5
+ require 'bolt/module_installer/installer'
6
+ require 'bolt/module_installer/puppetfile'
7
+ require 'bolt/module_installer/resolver'
8
+ require 'bolt/module_installer/specs'
9
+
10
+ module Bolt
11
+ class ModuleInstaller
12
+ def initialize(outputter, pal)
13
+ @outputter = outputter
14
+ @pal = pal
15
+ @logger = Bolt::Logger.logger(self)
16
+ end
17
+
18
+ # Adds a single module to the project.
19
+ #
20
+ def add(name, specs, puppetfile_path, moduledir, config_path)
21
+ project_specs = Specs.new(specs)
22
+
23
+ # Exit early if project config already includes a spec with this name.
24
+ if project_specs.include?(name)
25
+ @outputter.print_message(
26
+ "Project configuration file #{config_path} already includes specification with name "\
27
+ "#{name}. Nothing to do."
28
+ )
29
+ return true
30
+ end
31
+
32
+ @outputter.print_message("Adding module #{name} to project\n\n")
33
+
34
+ # Generate the specs to resolve from. If a Puppetfile exists, parse it and
35
+ # convert the modules to specs. Otherwise, use the project specs.
36
+ resolve_specs = if puppetfile_path.exist?
37
+ existing_puppetfile = Puppetfile.parse(puppetfile_path)
38
+ existing_puppetfile.assert_satisfies(project_specs)
39
+ Specs.from_puppetfile(existing_puppetfile)
40
+ else
41
+ project_specs
42
+ end
43
+
44
+ # Resolve module dependencies. Attempt to first resolve with resolve
45
+ # specss. If that fails, fall back to resolving from project specs.
46
+ # This prevents Bolt from modifying installed modules unless there is
47
+ # a version conflict.
48
+ @outputter.print_action_step("Resolving module dependencies, this may take a moment")
49
+
50
+ begin
51
+ resolve_specs.add_specs('name' => name)
52
+ puppetfile = Resolver.new.resolve(resolve_specs)
53
+ rescue Bolt::Error
54
+ project_specs.add_specs('name' => name)
55
+ puppetfile = Resolver.new.resolve(project_specs)
56
+ end
57
+
58
+ # Display the diff between the existing Puppetfile and the new Puppetfile.
59
+ print_puppetfile_diff(existing_puppetfile, puppetfile)
60
+
61
+ # Add the module to the project configuration.
62
+ @outputter.print_action_step("Updating project configuration file at #{config_path}")
63
+
64
+ data = Bolt::Util.read_yaml_hash(config_path, 'project')
65
+ data['modules'] ||= []
66
+ data['modules'] << name
67
+
68
+ begin
69
+ File.write(config_path, data.to_yaml)
70
+ rescue SystemCallError => e
71
+ raise Bolt::FileError.new(
72
+ "Unable to update project configuration file: #{e.message}",
73
+ config
74
+ )
75
+ end
76
+
77
+ # Write the Puppetfile.
78
+ @outputter.print_action_step("Writing Puppetfile at #{puppetfile_path}")
79
+ puppetfile.write(puppetfile_path, moduledir)
80
+
81
+ # Install the modules.
82
+ install_puppetfile(puppetfile_path, moduledir)
83
+ end
84
+
85
+ # Outputs a diff of an old Puppetfile and a new Puppetfile.
86
+ #
87
+ def print_puppetfile_diff(old, new)
88
+ # Build hashes mapping the module name to the module object. This makes it
89
+ # a little easier to determine which modules have been added, removed, or
90
+ # modified.
91
+ old = (old&.modules || []).each_with_object({}) do |mod, acc|
92
+ next unless mod.type == :forge
93
+ acc[mod.full_name] = mod
94
+ end
95
+
96
+ new = new.modules.each_with_object({}) do |mod, acc|
97
+ next unless mod.type == :forge
98
+ acc[mod.full_name] = mod
99
+ end
100
+
101
+ # New modules are those present in new but not in old.
102
+ added = new.reject { |full_name, _mod| old.include?(full_name) }.values
103
+
104
+ if added.any?
105
+ diff = "Adding the following modules:\n"
106
+ added.each { |mod| diff += "#{mod.full_name} #{mod.version}\n" }
107
+ @outputter.print_action_step(diff)
108
+ end
109
+
110
+ # Upgraded modules are those that have a newer version in new than old.
111
+ upgraded = new.select do |full_name, mod|
112
+ if old.include?(full_name)
113
+ mod.version > old[full_name].version
114
+ end
115
+ end.keys
116
+
117
+ if upgraded.any?
118
+ diff = "Upgrading the following modules:\n"
119
+ upgraded.each { |full_name| diff += "#{full_name} #{old[full_name].version} to #{new[full_name].version}\n" }
120
+ @outputter.print_action_step(diff)
121
+ end
122
+
123
+ # Downgraded modules are those that have an older version in new than old.
124
+ downgraded = new.select do |full_name, mod|
125
+ if old.include?(full_name)
126
+ mod.version < old[full_name].version
127
+ end
128
+ end.keys
129
+
130
+ if downgraded.any?
131
+ diff = "Downgrading the following modules: \n"
132
+ downgraded.each { |full_name| diff += "#{full_name} #{old[full_name].version} to #{new[full_name].version}\n" }
133
+ @outputter.print_action_step(diff)
134
+ end
135
+
136
+ # Removed modules are those present in old but not in new.
137
+ removed = old.reject { |full_name, _mod| new.include?(full_name) }.values
138
+
139
+ if removed.any?
140
+ diff = "Removing the following modules:\n"
141
+ removed.each { |mod| diff += "#{mod.full_name} #{mod.version}\n" }
142
+ @outputter.print_action_step(diff)
143
+ end
144
+ end
145
+
146
+ # Installs a project's module dependencies.
147
+ #
148
+ def install(specs, path, moduledir, force: false, resolve: true)
149
+ @outputter.print_message("Installing project modules\n\n")
150
+
151
+ if resolve != false
152
+ specs = Specs.new(specs)
153
+
154
+ # If forcibly installing or if there is no Puppetfile, resolve
155
+ # and write a Puppetfile.
156
+ if force || !path.exist?
157
+ @outputter.print_action_step("Resolving module dependencies, this may take a moment")
158
+ puppetfile = Resolver.new.resolve(specs)
159
+
160
+ # We get here either through 'bolt module install' which uses the
161
+ # managed modulepath (which isn't configurable) or through bolt
162
+ # project init --modules, which uses the default modulepath. This
163
+ # should be safe to assume that if `.modules/` is the moduledir the
164
+ # user is using the new workflow
165
+ @outputter.print_action_step("Writing Puppetfile at #{path}")
166
+ if moduledir.basename.to_s == '.modules'
167
+ puppetfile.write(path, moduledir)
168
+ else
169
+ puppetfile.write(path)
170
+ end
171
+ # If not forcibly installing and there is a Puppetfile, assert
172
+ # that it satisfies the specs.
173
+ else
174
+ puppetfile = Puppetfile.parse(path)
175
+ puppetfile.assert_satisfies(specs)
176
+ end
177
+ end
178
+
179
+ # Install the modules.
180
+ install_puppetfile(path, moduledir)
181
+ end
182
+
183
+ # Installs the Puppetfile and generates types.
184
+ #
185
+ def install_puppetfile(path, moduledir, config = {})
186
+ @outputter.print_action_step("Syncing modules from #{path} to #{moduledir}")
187
+ ok = Installer.new(config).install(path, moduledir)
188
+
189
+ # Automatically generate types after installing modules
190
+ @outputter.print_action_step("Generating type references")
191
+ @pal.generate_types
192
+
193
+ @outputter.print_puppetfile_result(ok, path, moduledir)
194
+
195
+ ok
196
+ end
197
+ end
198
+ end