bolt 2.30.0 → 2.34.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 (77) hide show
  1. checksums.yaml +4 -4
  2. data/Puppetfile +12 -12
  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 +6 -1
  18. data/lib/bolt/cli.rb +82 -142
  19. data/lib/bolt/config/modulepath.rb +30 -0
  20. data/lib/bolt/config/options.rb +31 -13
  21. data/lib/bolt/config/transport/options.rb +2 -2
  22. data/lib/bolt/error.rb +13 -3
  23. data/lib/bolt/executor.rb +24 -12
  24. data/lib/bolt/inventory.rb +10 -9
  25. data/lib/bolt/inventory/group.rb +2 -1
  26. data/lib/bolt/module_installer.rb +117 -91
  27. data/lib/bolt/{puppetfile → module_installer}/installer.rb +3 -2
  28. data/lib/bolt/module_installer/puppetfile.rb +117 -0
  29. data/lib/bolt/module_installer/puppetfile/forge_module.rb +54 -0
  30. data/lib/bolt/module_installer/puppetfile/git_module.rb +37 -0
  31. data/lib/bolt/module_installer/puppetfile/module.rb +26 -0
  32. data/lib/bolt/module_installer/resolver.rb +76 -0
  33. data/lib/bolt/module_installer/specs.rb +93 -0
  34. data/lib/bolt/module_installer/specs/forge_spec.rb +85 -0
  35. data/lib/bolt/module_installer/specs/git_spec.rb +179 -0
  36. data/lib/bolt/outputter.rb +0 -47
  37. data/lib/bolt/outputter/human.rb +46 -16
  38. data/lib/bolt/outputter/json.rb +17 -8
  39. data/lib/bolt/pal.rb +52 -40
  40. data/lib/bolt/pal/yaml_plan.rb +4 -2
  41. data/lib/bolt/pal/yaml_plan/evaluator.rb +23 -1
  42. data/lib/bolt/pal/yaml_plan/loader.rb +14 -9
  43. data/lib/bolt/plan_creator.rb +160 -0
  44. data/lib/bolt/plugin.rb +2 -2
  45. data/lib/bolt/project.rb +6 -11
  46. data/lib/bolt/project_migrator.rb +1 -1
  47. data/lib/bolt/project_migrator/base.rb +2 -2
  48. data/lib/bolt/project_migrator/config.rb +5 -4
  49. data/lib/bolt/project_migrator/inventory.rb +3 -3
  50. data/lib/bolt/project_migrator/modules.rb +23 -21
  51. data/lib/bolt/puppetdb/config.rb +5 -5
  52. data/lib/bolt/result.rb +23 -11
  53. data/lib/bolt/shell/bash.rb +14 -8
  54. data/lib/bolt/shell/powershell.rb +12 -7
  55. data/lib/bolt/task/run.rb +1 -1
  56. data/lib/bolt/transport/base.rb +18 -18
  57. data/lib/bolt/transport/docker.rb +23 -6
  58. data/lib/bolt/transport/orch.rb +26 -17
  59. data/lib/bolt/transport/remote.rb +3 -3
  60. data/lib/bolt/transport/simple.rb +6 -6
  61. data/lib/bolt/transport/ssh/connection.rb +1 -1
  62. data/lib/bolt/util.rb +5 -0
  63. data/lib/bolt/version.rb +1 -1
  64. data/lib/bolt_server/file_cache.rb +2 -0
  65. data/lib/bolt_server/schemas/partials/task.json +17 -2
  66. data/lib/bolt_server/transport_app.rb +92 -12
  67. data/lib/bolt_spec/bolt_context.rb +4 -2
  68. data/lib/bolt_spec/plans.rb +1 -1
  69. data/lib/bolt_spec/plans/action_stubs/command_stub.rb +1 -1
  70. data/lib/bolt_spec/plans/action_stubs/script_stub.rb +1 -1
  71. data/lib/bolt_spec/plans/mock_executor.rb +5 -5
  72. data/lib/bolt_spec/run.rb +1 -1
  73. metadata +24 -9
  74. data/lib/bolt/puppetfile.rb +0 -142
  75. data/lib/bolt/puppetfile/module.rb +0 -90
  76. data/lib/bolt_server/pe/pal.rb +0 -67
  77. 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
@@ -234,26 +234,44 @@ module Bolt
234
234
  type: Array,
235
235
  items: {
236
236
  type: [Hash, String],
237
- required: ["name"],
238
- properties: {
239
- "name" => {
240
- description: "The name of the module.",
241
- type: 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
+ }
242
251
  },
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
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
+ }
247
264
  }
248
- }
265
+ ]
249
266
  },
250
267
  _plugin: false,
251
268
  _example: [
252
- { "name" => "puppetlabs-mysql" },
253
269
  "puppetlabs-facts",
270
+ { "name" => "puppetlabs-mysql" },
254
271
  { "name" => "puppetlabs-apache", "version_requirement" => "5.5.0" },
255
272
  { "name" => "puppetlabs-puppetdb", "version_requirement" => "7.x" },
256
- { "name" => "puppetlabs-firewall", "version_requirement" => ">= 1.0.0 < 3.0.0" }
273
+ { "name" => "puppetlabs-firewall", "version_requirement" => ">= 1.0.0 < 3.0.0" },
274
+ { "git" => "https://github.com/puppetlabs/puppetlabs-apt", "ref" => "7.6.0" }
257
275
  ]
258
276
  },
259
277
  "name" => {
@@ -329,7 +347,7 @@ module Bolt
329
347
  "server_urls" => {
330
348
  description: "An array containing the PuppetDB host to connect to. Include the protocol `https` "\
331
349
  "and the port, which is usually `8081`. For example, "\
332
- "`https://my-master.example.com:8081`.",
350
+ "`https://my-puppetdb-server.com:8081`.",
333
351
  type: Array,
334
352
  _example: ["https://puppet.example.com:8081"]
335
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).",
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'bolt/util'
4
+
3
5
  module Bolt
4
6
  class Error < RuntimeError
5
7
  attr_reader :kind, :details, :issue_code, :error_code
@@ -24,6 +26,10 @@ module Bolt
24
26
  h
25
27
  end
26
28
 
29
+ def add_filelineno(details)
30
+ @details.merge!(details) unless @details['file']
31
+ end
32
+
27
33
  def to_json(opts = nil)
28
34
  to_h.to_json(opts)
29
35
  end
@@ -33,13 +39,17 @@ module Bolt
33
39
  end
34
40
 
35
41
  def self.unknown_task(task)
36
- new("Could not find a task named \"#{task}\". For a list of available tasks, run \"bolt task show\"",
37
- 'bolt/unknown-task')
42
+ command = Bolt::Util.powershell? ? "Get-BoltTask" : "bolt task show"
43
+ new(
44
+ "Could not find a task named '#{task}'. For a list of available tasks, run '#{command}'.",
45
+ 'bolt/unknown-task'
46
+ )
38
47
  end
39
48
 
40
49
  def self.unknown_plan(plan)
50
+ command = Bolt::Util.powershell? ? "Get-BoltPlan" : "bolt plan show"
41
51
  new(
42
- "Could not find a plan named \"#{plan}\". For a list of available plans, run \"bolt plan show\"",
52
+ "Could not find a plan named '#{plan}'. For a list of available plans, run '#{command}'.",
43
53
  'bolt/unknown-plan'
44
54
  )
45
55
  end
@@ -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,25 @@ 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_minimal_logging(targets, task, arguments, options = {})
297
+ description = options.fetch(:description, "task #{task.name}")
298
+ log_action(description, targets) do
299
+ options[:run_as] = run_as if run_as && !options.key?(:run_as)
300
+ arguments['_task'] = task.name
301
+
302
+ batch_execute(targets) do |transport, batch|
303
+ transport.batch_task(batch, task, arguments, options, [], &method(:publish_event))
304
+ end
305
+ end
306
+ end
307
+
308
+ def run_task_with(target_mapping, task, options = {}, position = [])
297
309
  targets = target_mapping.keys
298
310
  description = options.fetch(:description, "task #{task.name}")
299
311
 
@@ -303,26 +315,26 @@ module Bolt
303
315
 
304
316
  batch_execute(targets) do |transport, batch|
305
317
  with_node_logging("Running task #{task.name}'", batch) do
306
- transport.batch_task_with(batch, task, target_mapping, options, &method(:publish_event))
318
+ transport.batch_task_with(batch, task, target_mapping, options, position, &method(:publish_event))
307
319
  end
308
320
  end
309
321
  end
310
322
  end
311
323
 
312
- def upload_file(targets, source, destination, options = {})
324
+ def upload_file(targets, source, destination, options = {}, position = [])
313
325
  description = options.fetch(:description, "file upload from #{source} to #{destination}")
314
326
  log_action(description, targets) do
315
327
  options[:run_as] = run_as if run_as && !options.key?(:run_as)
316
328
 
317
329
  batch_execute(targets) do |transport, batch|
318
330
  with_node_logging("Uploading file #{source} to #{destination}", batch) do
319
- transport.batch_upload(batch, source, destination, options, &method(:publish_event))
331
+ transport.batch_upload(batch, source, destination, options, position, &method(:publish_event))
320
332
  end
321
333
  end
322
334
  end
323
335
  end
324
336
 
325
- def download_file(targets, source, destination, options = {})
337
+ def download_file(targets, source, destination, options = {}, position = [])
326
338
  description = options.fetch(:description, "file download from #{source} to #{destination}")
327
339
 
328
340
  begin
@@ -337,7 +349,7 @@ module Bolt
337
349
 
338
350
  batch_execute(targets) do |transport, batch|
339
351
  with_node_logging("Downloading file #{source} to #{destination}", batch) do
340
- transport.batch_download(batch, source, destination, options, &method(:publish_event))
352
+ transport.batch_download(batch, source, destination, options, position, &method(:publish_event))
341
353
  end
342
354
  end
343
355
  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
@@ -241,10 +241,11 @@ module Bolt
241
241
  end
242
242
 
243
243
  if input.key?('nodes')
244
+ command = Bolt::Util.powershell? ? 'Update-BoltProject' : 'bolt project migrate'
244
245
  msg = <<~MSG.chomp
245
246
  Found 'nodes' key in group #{@name}. This looks like a v1 inventory file, which is
246
247
  no longer supported by Bolt. Migrate to a v2 inventory file automatically using
247
- 'bolt project migrate'.
248
+ '#{command}'.
248
249
  MSG
249
250
  raise ValidationError.new(msg, nil)
250
251
  end
@@ -2,6 +2,10 @@
2
2
 
3
3
  require 'bolt/error'
4
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'
5
9
 
6
10
  module Bolt
7
11
  class ModuleInstaller
@@ -13,36 +17,53 @@ module Bolt
13
17
 
14
18
  # Adds a single module to the project.
15
19
  #
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."
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
+ )
27
29
  return true
28
30
  end
29
31
 
30
- # If the Puppetfile exists, make sure it's managed by Bolt.
31
- if puppetfile_path.exist?
32
- assert_managed_puppetfile(puppetfile, puppetfile_path)
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)
33
56
  end
34
57
 
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)
58
+ # Display the diff between the existing Puppetfile and the new Puppetfile.
59
+ print_puppetfile_diff(existing_puppetfile, puppetfile)
39
60
 
40
61
  # Add the module to the project configuration.
41
- @outputter.print_message "Updating project configuration file at #{config_path}"
62
+ @outputter.print_action_step("Updating project configuration file at #{config_path}")
42
63
 
43
64
  data = Bolt::Util.read_yaml_hash(config_path, 'project')
44
65
  data['modules'] ||= []
45
- data['modules'] << { 'name' => new_module.title }
66
+ data['modules'] << name.tr('-', '/')
46
67
 
47
68
  begin
48
69
  File.write(config_path, data.to_yaml)
@@ -54,76 +75,104 @@ module Bolt
54
75
  end
55
76
 
56
77
  # Write the Puppetfile.
57
- @outputter.print_message "Writing Puppetfile at #{puppetfile_path}"
78
+ @outputter.print_action_step("Writing Puppetfile at #{puppetfile_path}")
58
79
  puppetfile.write(puppetfile_path, moduledir)
59
80
 
60
81
  # Install the modules.
61
82
  install_puppetfile(puppetfile_path, moduledir)
62
83
  end
63
84
 
64
- # Creates a new Puppetfile that includes the new module and its dependencies.
85
+ # Outputs a diff of an old Puppetfile and a new Puppetfile.
65
86
  #
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."
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
83
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)
84
134
  end
85
135
 
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
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
93
144
  end
94
145
 
95
146
  # Installs a project's module dependencies.
96
147
  #
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)
148
+ def install(specs, path, moduledir, force: false, resolve: true)
149
+ @outputter.print_message("Installing project modules\n\n")
150
+
109
151
  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
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)
115
159
 
116
- @outputter.print_message "Writing Puppetfile at #{path}"
117
160
  # We get here either through 'bolt module install' which uses the
118
161
  # managed modulepath (which isn't configurable) or through bolt
119
162
  # project init --modules, which uses the default modulepath. This
120
163
  # should be safe to assume that if `.modules/` is the moduledir the
121
164
  # user is using the new workflow
122
- if moduledir.basename == '.modules'
165
+ @outputter.print_action_step("Writing Puppetfile at #{path}")
166
+ if moduledir.basename.to_s == '.modules'
123
167
  puppetfile.write(path, moduledir)
124
168
  else
125
169
  puppetfile.write(path)
126
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)
127
176
  end
128
177
  end
129
178
 
@@ -134,39 +183,16 @@ module Bolt
134
183
  # Installs the Puppetfile and generates types.
135
184
  #
136
185
  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)
186
+ @outputter.print_action_step("Syncing modules from #{path} to #{moduledir}")
187
+ ok = Installer.new(config).install(path, moduledir)
141
188
 
142
189
  # Automatically generate types after installing modules
190
+ @outputter.print_action_step("Generating type references")
143
191
  @pal.generate_types
144
192
 
145
193
  @outputter.print_puppetfile_result(ok, path, moduledir)
146
194
 
147
195
  ok
148
196
  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
197
  end
172
198
  end