bolt 2.31.0 → 2.35.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 (72) hide show
  1. checksums.yaml +4 -4
  2. data/Puppetfile +7 -7
  3. data/bolt-modules/boltlib/lib/puppet/functions/catch_errors.rb +1 -3
  4. data/bolt-modules/boltlib/lib/puppet/functions/download_file.rb +17 -6
  5. data/bolt-modules/boltlib/lib/puppet/functions/facts.rb +6 -0
  6. data/bolt-modules/boltlib/lib/puppet/functions/parallelize.rb +56 -0
  7. data/bolt-modules/boltlib/lib/puppet/functions/puppetdb_query.rb +2 -2
  8. data/bolt-modules/boltlib/lib/puppet/functions/run_command.rb +24 -6
  9. data/bolt-modules/boltlib/lib/puppet/functions/run_script.rb +27 -8
  10. data/bolt-modules/boltlib/lib/puppet/functions/run_task.rb +21 -1
  11. data/bolt-modules/boltlib/lib/puppet/functions/run_task_with.rb +18 -1
  12. data/bolt-modules/boltlib/lib/puppet/functions/upload_file.rb +24 -6
  13. data/bolt-modules/out/lib/puppet/functions/out/message.rb +44 -1
  14. data/bolt-modules/prompt/lib/puppet/functions/prompt.rb +3 -0
  15. data/guides/logging.txt +18 -0
  16. data/guides/module.txt +19 -0
  17. data/guides/modulepath.txt +25 -0
  18. data/lib/bolt/bolt_option_parser.rb +6 -1
  19. data/lib/bolt/cli.rb +70 -144
  20. data/lib/bolt/config/options.rb +35 -17
  21. data/lib/bolt/config/transport/options.rb +1 -1
  22. data/lib/bolt/error.rb +37 -3
  23. data/lib/bolt/executor.rb +111 -13
  24. data/lib/bolt/inventory/group.rb +2 -1
  25. data/lib/bolt/module_installer.rb +71 -115
  26. data/lib/bolt/{puppetfile → module_installer}/installer.rb +3 -2
  27. data/lib/bolt/module_installer/puppetfile.rb +117 -0
  28. data/lib/bolt/module_installer/puppetfile/forge_module.rb +54 -0
  29. data/lib/bolt/module_installer/puppetfile/git_module.rb +37 -0
  30. data/lib/bolt/module_installer/puppetfile/module.rb +26 -0
  31. data/lib/bolt/module_installer/resolver.rb +76 -0
  32. data/lib/bolt/module_installer/specs.rb +93 -0
  33. data/lib/bolt/module_installer/specs/forge_spec.rb +85 -0
  34. data/lib/bolt/module_installer/specs/git_spec.rb +179 -0
  35. data/lib/bolt/outputter.rb +0 -47
  36. data/lib/bolt/outputter/human.rb +23 -11
  37. data/lib/bolt/outputter/json.rb +1 -1
  38. data/lib/bolt/pal.rb +48 -30
  39. data/lib/bolt/pal/yaml_plan.rb +11 -2
  40. data/lib/bolt/pal/yaml_plan/evaluator.rb +23 -1
  41. data/lib/bolt/pal/yaml_plan/loader.rb +14 -9
  42. data/lib/bolt/plan_creator.rb +160 -0
  43. data/lib/bolt/plugin.rb +1 -1
  44. data/lib/bolt/project.rb +5 -10
  45. data/lib/bolt/project_migrator/config.rb +2 -1
  46. data/lib/bolt/project_migrator/inventory.rb +2 -2
  47. data/lib/bolt/project_migrator/modules.rb +10 -8
  48. data/lib/bolt/puppetdb/client.rb +3 -2
  49. data/lib/bolt/puppetdb/config.rb +8 -6
  50. data/lib/bolt/result.rb +23 -11
  51. data/lib/bolt/shell/bash.rb +11 -6
  52. data/lib/bolt/shell/powershell.rb +12 -7
  53. data/lib/bolt/task/run.rb +1 -1
  54. data/lib/bolt/transport/base.rb +18 -18
  55. data/lib/bolt/transport/docker.rb +23 -6
  56. data/lib/bolt/transport/orch.rb +23 -19
  57. data/lib/bolt/transport/orch/connection.rb +10 -3
  58. data/lib/bolt/transport/remote.rb +3 -3
  59. data/lib/bolt/transport/simple.rb +6 -6
  60. data/lib/bolt/util.rb +5 -0
  61. data/lib/bolt/version.rb +1 -1
  62. data/lib/bolt/yarn.rb +23 -0
  63. data/lib/bolt_server/file_cache.rb +2 -0
  64. data/lib/bolt_server/schemas/partials/task.json +17 -2
  65. data/lib/bolt_server/transport_app.rb +38 -7
  66. data/lib/bolt_spec/plans/action_stubs/command_stub.rb +1 -1
  67. data/lib/bolt_spec/plans/action_stubs/script_stub.rb +1 -1
  68. data/lib/bolt_spec/plans/mock_executor.rb +9 -6
  69. metadata +25 -8
  70. data/lib/bolt/puppetfile.rb +0 -149
  71. data/lib/bolt/puppetfile/module.rb +0 -93
  72. data/modules/secure_env_vars/plans/init.pp +0 -20
@@ -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:8143"
361
361
  },
362
362
  "shell-command" => {
363
363
  type: String,
@@ -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
@@ -80,6 +90,20 @@ module Bolt
80
90
  end
81
91
  end
82
92
 
93
+ class ParallelFailure < Bolt::Error
94
+ def initialize(results, failed_indices)
95
+ details = {
96
+ 'action' => 'parallelize',
97
+ 'failed_indices' => failed_indices,
98
+ 'results' => results
99
+ }
100
+ message = "Plan aborted: parallel block failed on #{failed_indices.length} target"
101
+ message += "s" unless failed_indices.length == 1
102
+ super(message, 'bolt/parallel-failure', details)
103
+ @error_code = 2
104
+ end
105
+ end
106
+
83
107
  class PlanFailure < Error
84
108
  def initialize(*args)
85
109
  super(*args)
@@ -121,6 +145,16 @@ module Bolt
121
145
  end
122
146
  end
123
147
 
148
+ class InvalidParallelResult < Error
149
+ def initialize(result_str, file, line)
150
+ super("Parallel block returned an invalid result: #{result_str}",
151
+ 'bolt/invalid-plan-result',
152
+ { 'file' => file,
153
+ 'line' => line,
154
+ 'result_string' => result_str })
155
+ end
156
+ end
157
+
124
158
  class ValidationError < Bolt::Error
125
159
  def initialize(msg)
126
160
  super(msg, 'bolt/validation-error')
@@ -17,6 +17,7 @@ require 'bolt/transport/orch'
17
17
  require 'bolt/transport/local'
18
18
  require 'bolt/transport/docker'
19
19
  require 'bolt/transport/remote'
20
+ require 'bolt/yarn'
20
21
 
21
22
  module Bolt
22
23
  TRANSPORTS = {
@@ -29,7 +30,7 @@ module Bolt
29
30
  }.freeze
30
31
 
31
32
  class Executor
32
- attr_reader :noop, :transports
33
+ attr_reader :noop, :transports, :in_parallel
33
34
  attr_accessor :run_as
34
35
 
35
36
  def initialize(concurrency = 1,
@@ -60,6 +61,7 @@ module Bolt
60
61
 
61
62
  @noop = noop
62
63
  @run_as = nil
64
+ @in_parallel = false
63
65
  @pool = if concurrency > 0
64
66
  Concurrent::ThreadPoolExecutor.new(name: 'exec', max_threads: concurrency)
65
67
  else
@@ -84,6 +86,14 @@ module Bolt
84
86
  self
85
87
  end
86
88
 
89
+ def unsubscribe(subscriber, types = nil)
90
+ if types.nil? || types.sort == @subscribers[subscriber]&.sort
91
+ @subscribers.delete(subscriber)
92
+ elsif @subscribers[subscriber].is_a?(Array)
93
+ @subscribers[subscriber] = @subscribers[subscriber] - types
94
+ end
95
+ end
96
+
87
97
  def publish_event(event)
88
98
  @subscribers.each do |subscriber, types|
89
99
  # If types isn't set or if the subscriber is subscribed to
@@ -253,33 +263,33 @@ module Bolt
253
263
  result
254
264
  end
255
265
 
256
- def run_command(targets, command, options = {})
266
+ def run_command(targets, command, options = {}, position = [])
257
267
  description = options.fetch(:description, "command '#{command}'")
258
268
  log_action(description, targets) do
259
269
  options[:run_as] = run_as if run_as && !options.key?(:run_as)
260
270
 
261
271
  batch_execute(targets) do |transport, batch|
262
272
  with_node_logging("Running command '#{command}'", batch) do
263
- transport.batch_command(batch, command, options, &method(:publish_event))
273
+ transport.batch_command(batch, command, options, position, &method(:publish_event))
264
274
  end
265
275
  end
266
276
  end
267
277
  end
268
278
 
269
- def run_script(targets, script, arguments, options = {})
279
+ def run_script(targets, script, arguments, options = {}, position = [])
270
280
  description = options.fetch(:description, "script #{script}")
271
281
  log_action(description, targets) do
272
282
  options[:run_as] = run_as if run_as && !options.key?(:run_as)
273
283
 
274
284
  batch_execute(targets) do |transport, batch|
275
285
  with_node_logging("Running script #{script} with '#{arguments.to_json}'", batch) do
276
- transport.batch_script(batch, script, arguments, options, &method(:publish_event))
286
+ transport.batch_script(batch, script, arguments, options, position, &method(:publish_event))
277
287
  end
278
288
  end
279
289
  end
280
290
  end
281
291
 
282
- def run_task(targets, task, arguments, options = {})
292
+ def run_task(targets, task, arguments, options = {}, position = [])
283
293
  description = options.fetch(:description, "task #{task.name}")
284
294
  log_action(description, targets) do
285
295
  options[:run_as] = run_as if run_as && !options.key?(:run_as)
@@ -287,13 +297,25 @@ module Bolt
287
297
 
288
298
  batch_execute(targets) do |transport, batch|
289
299
  with_node_logging("Running task #{task.name} with '#{arguments.to_json}'", batch) do
290
- transport.batch_task(batch, task, arguments, options, &method(:publish_event))
300
+ transport.batch_task(batch, task, arguments, options, position, &method(:publish_event))
291
301
  end
292
302
  end
293
303
  end
294
304
  end
295
305
 
296
- def run_task_with(target_mapping, task, options = {})
306
+ def run_task_with_minimal_logging(targets, task, arguments, options = {})
307
+ description = options.fetch(:description, "task #{task.name}")
308
+ log_action(description, targets) do
309
+ options[:run_as] = run_as if run_as && !options.key?(:run_as)
310
+ arguments['_task'] = task.name
311
+
312
+ batch_execute(targets) do |transport, batch|
313
+ transport.batch_task(batch, task, arguments, options, [], &method(:publish_event))
314
+ end
315
+ end
316
+ end
317
+
318
+ def run_task_with(target_mapping, task, options = {}, position = [])
297
319
  targets = target_mapping.keys
298
320
  description = options.fetch(:description, "task #{task.name}")
299
321
 
@@ -303,26 +325,26 @@ module Bolt
303
325
 
304
326
  batch_execute(targets) do |transport, batch|
305
327
  with_node_logging("Running task #{task.name}'", batch) do
306
- transport.batch_task_with(batch, task, target_mapping, options, &method(:publish_event))
328
+ transport.batch_task_with(batch, task, target_mapping, options, position, &method(:publish_event))
307
329
  end
308
330
  end
309
331
  end
310
332
  end
311
333
 
312
- def upload_file(targets, source, destination, options = {})
334
+ def upload_file(targets, source, destination, options = {}, position = [])
313
335
  description = options.fetch(:description, "file upload from #{source} to #{destination}")
314
336
  log_action(description, targets) do
315
337
  options[:run_as] = run_as if run_as && !options.key?(:run_as)
316
338
 
317
339
  batch_execute(targets) do |transport, batch|
318
340
  with_node_logging("Uploading file #{source} to #{destination}", batch) do
319
- transport.batch_upload(batch, source, destination, options, &method(:publish_event))
341
+ transport.batch_upload(batch, source, destination, options, position, &method(:publish_event))
320
342
  end
321
343
  end
322
344
  end
323
345
  end
324
346
 
325
- def download_file(targets, source, destination, options = {})
347
+ def download_file(targets, source, destination, options = {}, position = [])
326
348
  description = options.fetch(:description, "file download from #{source} to #{destination}")
327
349
 
328
350
  begin
@@ -337,7 +359,7 @@ module Bolt
337
359
 
338
360
  batch_execute(targets) do |transport, batch|
339
361
  with_node_logging("Downloading file #{source} to #{destination}", batch) do
340
- transport.batch_download(batch, source, destination, options, &method(:publish_event))
362
+ transport.batch_download(batch, source, destination, options, position, &method(:publish_event))
341
363
  end
342
364
  end
343
365
  end
@@ -347,6 +369,82 @@ module Bolt
347
369
  plan.call_by_name_with_scope(scope, params, true)
348
370
  end
349
371
 
372
+ def create_yarn(scope, block, object, index)
373
+ fiber = Fiber.new do
374
+ # Create the new scope
375
+ newscope = Puppet::Parser::Scope.new(scope.compiler)
376
+ local = Puppet::Parser::Scope::LocalScope.new
377
+
378
+ # Compress the current scopes into a single vars hash to add to the new scope
379
+ current_scope = scope.effective_symtable(true)
380
+ until current_scope.nil?
381
+ current_scope.instance_variable_get(:@symbols)&.each_pair { |k, v| local[k] = v }
382
+ current_scope = current_scope.parent
383
+ end
384
+ newscope.push_ephemerals([local])
385
+
386
+ begin
387
+ result = catch(:return) do
388
+ args = { block.parameters[0][1].to_s => object }
389
+ block.closure.call_by_name_with_scope(newscope, args, true)
390
+ end
391
+
392
+ # If we got a return from the block, get it's value
393
+ # Otherwise the result is the last line from the block
394
+ result = result.value if result.is_a?(Puppet::Pops::Evaluator::Return)
395
+
396
+ # Validate the result is a PlanResult
397
+ unless Puppet::Pops::Types::TypeParser.singleton.parse('Boltlib::PlanResult').instance?(result)
398
+ raise Bolt::InvalidParallelResult.new(result.to_s, *Puppet::Pops::PuppetStack.top_of_stack)
399
+ end
400
+
401
+ result
402
+ rescue Puppet::PreformattedError => e
403
+ if e.cause.is_a?(Bolt::Error)
404
+ e.cause
405
+ else
406
+ raise e
407
+ end
408
+ end
409
+ end
410
+
411
+ Bolt::Yarn.new(fiber, index)
412
+ end
413
+
414
+ def handle_event(event)
415
+ case event[:type]
416
+ when :node_result
417
+ @thread_completed = true
418
+ end
419
+ end
420
+
421
+ def round_robin(skein)
422
+ subscribe(self, [:node_result])
423
+ results = Array.new(skein.length)
424
+ @in_parallel = true
425
+
426
+ until skein.empty?
427
+ @thread_completed = false
428
+ r = nil
429
+
430
+ skein.each do |yarn|
431
+ if yarn.alive?
432
+ r = yarn.resume
433
+ else
434
+ results[yarn.index] = yarn.value
435
+ skein.delete(yarn)
436
+ end
437
+ end
438
+
439
+ next unless r == 'unfinished'
440
+ sleep(0.1) until @thread_completed || skein.empty?
441
+ end
442
+
443
+ @in_parallel = false
444
+ unsubscribe(self, [:node_result])
445
+ results
446
+ end
447
+
350
448
  class TimeoutError < RuntimeError; end
351
449
 
352
450
  def wait_until_available(targets,
@@ -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,45 +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
- @outputter.print_message("Adding module #{name} to project\n\n")
20
-
21
- # If the project configuration file already includes this module,
22
- # exit early.
23
- puppetfile = Bolt::Puppetfile.new(modules)
24
- new_module = Bolt::Puppetfile::Module.from_hash('name' => name)
25
-
26
- if puppetfile.modules.include?(new_module)
27
- @outputter.print_action_step(
28
- "Project configuration file #{config_path} already 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."
29
28
  )
30
29
  return true
31
30
  end
32
31
 
33
- # If the Puppetfile exists, make sure it's managed by Bolt.
34
- if puppetfile_path.exist?
35
- assert_managed_puppetfile(puppetfile, puppetfile_path)
36
- existing = Bolt::Puppetfile.parse(puppetfile_path)
37
- else
38
- existing = Bolt::Puppetfile.new
39
- end
32
+ @outputter.print_message("Adding module #{name} to project\n\n")
40
33
 
41
- # Create a Puppetfile object that includes the new module and its
42
- # dependencies. We error early here so we don't add the new module to the
43
- # project config or modify the Puppetfile.
44
- puppetfile = add_new_module_to_puppetfile(new_module, modules, puppetfile_path)
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
45
57
 
46
58
  # Display the diff between the existing Puppetfile and the new Puppetfile.
47
- print_puppetfile_diff(existing, puppetfile)
59
+ print_puppetfile_diff(existing_puppetfile, puppetfile)
48
60
 
49
61
  # Add the module to the project configuration.
50
62
  @outputter.print_action_step("Updating project configuration file at #{config_path}")
51
63
 
52
64
  data = Bolt::Util.read_yaml_hash(config_path, 'project')
53
65
  data['modules'] ||= []
54
- data['modules'] << { 'name' => new_module.title }
66
+ data['modules'] << name.tr('-', '/')
55
67
 
56
68
  begin
57
69
  File.write(config_path, data.to_yaml)
@@ -70,130 +82,97 @@ module Bolt
70
82
  install_puppetfile(puppetfile_path, moduledir)
71
83
  end
72
84
 
73
- # Creates a new Puppetfile that includes the new module and its dependencies.
74
- #
75
- private def add_new_module_to_puppetfile(new_module, modules, path)
76
- @outputter.print_action_step("Resolving module dependencies, this may take a moment")
77
-
78
- # If there is an existing Puppetfile, add the new module and attempt
79
- # to resolve. This will not update the versions of any installed modules.
80
- if path.exist?
81
- puppetfile = Bolt::Puppetfile.parse(path)
82
- puppetfile.add_modules(new_module)
83
-
84
- begin
85
- puppetfile.resolve
86
- return puppetfile
87
- rescue Bolt::Error
88
- @logger.debug "Unable to find a version of #{new_module} compatible "\
89
- "with installed modules. Attempting to re-resolve modules "\
90
- "from project configuration; some versions of installed "\
91
- "modules may change."
92
- end
93
- end
94
-
95
- # If there is not an existing Puppetfile, or resolving with pinned
96
- # modules fails, resolve all of the module declarations with the new
97
- # module.
98
- puppetfile = Bolt::Puppetfile.new(modules)
99
- puppetfile.add_modules(new_module)
100
- puppetfile.resolve
101
- puppetfile
102
- end
103
-
104
85
  # Outputs a diff of an old Puppetfile and a new Puppetfile.
105
86
  #
106
87
  def print_puppetfile_diff(old, new)
107
- # Build hashes mapping the module title to the module object. This makes it
88
+ # Build hashes mapping the module name to the module object. This makes it
108
89
  # a little easier to determine which modules have been added, removed, or
109
90
  # modified.
110
- old = old.modules.each_with_object({}) do |mod, acc|
111
- acc[mod.title] = mod
91
+ old = (old&.modules || []).each_with_object({}) do |mod, acc|
92
+ next unless mod.type == :forge
93
+ acc[mod.full_name] = mod
112
94
  end
113
95
 
114
96
  new = new.modules.each_with_object({}) do |mod, acc|
115
- acc[mod.title] = mod
97
+ next unless mod.type == :forge
98
+ acc[mod.full_name] = mod
116
99
  end
117
100
 
118
101
  # New modules are those present in new but not in old.
119
- added = new.reject { |title, _mod| old.include?(title) }.values
102
+ added = new.reject { |full_name, _mod| old.include?(full_name) }.values
120
103
 
121
104
  if added.any?
122
105
  diff = "Adding the following modules:\n"
123
- added.each { |mod| diff += "#{mod.title} #{mod.version}\n" }
106
+ added.each { |mod| diff += "#{mod.full_name} #{mod.version}\n" }
124
107
  @outputter.print_action_step(diff)
125
108
  end
126
109
 
127
110
  # Upgraded modules are those that have a newer version in new than old.
128
- upgraded = new.select do |title, mod|
129
- if old.include?(title)
130
- SemanticPuppet::Version.parse(mod.version) > SemanticPuppet::Version.parse(old[title].version)
111
+ upgraded = new.select do |full_name, mod|
112
+ if old.include?(full_name)
113
+ mod.version > old[full_name].version
131
114
  end
132
115
  end.keys
133
116
 
134
117
  if upgraded.any?
135
118
  diff = "Upgrading the following modules:\n"
136
- upgraded.each { |title| diff += "#{title} #{old[title].version} to #{new[title].version}\n" }
119
+ upgraded.each { |full_name| diff += "#{full_name} #{old[full_name].version} to #{new[full_name].version}\n" }
137
120
  @outputter.print_action_step(diff)
138
121
  end
139
122
 
140
123
  # Downgraded modules are those that have an older version in new than old.
141
- downgraded = new.select do |title, mod|
142
- if old.include?(title)
143
- SemanticPuppet::Version.parse(mod.version) < SemanticPuppet::Version.parse(old[title].version)
124
+ downgraded = new.select do |full_name, mod|
125
+ if old.include?(full_name)
126
+ mod.version < old[full_name].version
144
127
  end
145
128
  end.keys
146
129
 
147
130
  if downgraded.any?
148
131
  diff = "Downgrading the following modules: \n"
149
- downgraded.each { |title| diff += "#{title} #{old[title].version} to #{new[title].version}\n" }
132
+ downgraded.each { |full_name| diff += "#{full_name} #{old[full_name].version} to #{new[full_name].version}\n" }
150
133
  @outputter.print_action_step(diff)
151
134
  end
152
135
 
153
136
  # Removed modules are those present in old but not in new.
154
- removed = old.reject { |title, _mod| new.include?(title) }.values
137
+ removed = old.reject { |full_name, _mod| new.include?(full_name) }.values
155
138
 
156
139
  if removed.any?
157
140
  diff = "Removing the following modules:\n"
158
- removed.each { |mod| diff += "#{mod.title} #{mod.version}\n" }
141
+ removed.each { |mod| diff += "#{mod.full_name} #{mod.version}\n" }
159
142
  @outputter.print_action_step(diff)
160
143
  end
161
144
  end
162
145
 
163
146
  # Installs a project's module dependencies.
164
147
  #
165
- def install(modules, path, moduledir, force: false, resolve: true)
166
- require 'bolt/puppetfile'
167
-
148
+ def install(specs, path, moduledir, force: false, resolve: true)
168
149
  @outputter.print_message("Installing project modules\n\n")
169
150
 
170
- puppetfile = Bolt::Puppetfile.new(modules)
171
-
172
- # If the Puppetfile exists, check if it includes specs for each declared
173
- # module, erroring if there are any missing. Otherwise, resolve the
174
- # module dependencies and write a new Puppetfile. Users can forcibly
175
- # overwrite an existing Puppetfile with the '--force' option, or opt to
176
- # install the Puppetfile as-is with --no-resolve.
177
- #
178
- # This is just if resolve is not false (nil should default to true)
179
151
  if resolve != false
180
- if path.exist? && !force
181
- assert_managed_puppetfile(puppetfile, path)
182
- else
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?
183
157
  @outputter.print_action_step("Resolving module dependencies, this may take a moment")
184
- puppetfile.resolve
158
+ puppetfile = Resolver.new.resolve(specs)
185
159
 
186
- @outputter.print_action_step("Writing Puppetfile at #{path}")
187
160
  # We get here either through 'bolt module install' which uses the
188
161
  # managed modulepath (which isn't configurable) or through bolt
189
162
  # project init --modules, which uses the default modulepath. This
190
163
  # should be safe to assume that if `.modules/` is the moduledir the
191
164
  # user is using the new workflow
165
+ @outputter.print_action_step("Writing Puppetfile at #{path}")
192
166
  if moduledir.basename.to_s == '.modules'
193
167
  puppetfile.write(path, moduledir)
194
168
  else
195
169
  puppetfile.write(path)
196
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)
197
176
  end
198
177
  end
199
178
 
@@ -204,39 +183,16 @@ module Bolt
204
183
  # Installs the Puppetfile and generates types.
205
184
  #
206
185
  def install_puppetfile(path, moduledir, config = {})
207
- require 'bolt/puppetfile/installer'
208
-
209
186
  @outputter.print_action_step("Syncing modules from #{path} to #{moduledir}")
210
- ok = Bolt::Puppetfile::Installer.new(config).install(path, moduledir)
187
+ ok = Installer.new(config).install(path, moduledir)
211
188
 
212
189
  # Automatically generate types after installing modules
190
+ @outputter.print_action_step("Generating type references")
213
191
  @pal.generate_types
214
192
 
215
193
  @outputter.print_puppetfile_result(ok, path, moduledir)
216
194
 
217
195
  ok
218
196
  end
219
-
220
- # Asserts that an existing Puppetfile is managed by Bolt.
221
- #
222
- private def assert_managed_puppetfile(puppetfile, path)
223
- existing_puppetfile = Bolt::Puppetfile.parse(path)
224
-
225
- unless existing_puppetfile.modules.superset? puppetfile.modules
226
- missing_modules = puppetfile.modules - existing_puppetfile.modules
227
-
228
- message = <<~MESSAGE.chomp
229
- Puppetfile #{path} is missing specifications for the following
230
- module declarations:
231
-
232
- #{missing_modules.map(&:to_hash).to_yaml.lines.drop(1).join.chomp}
233
-
234
- This may not be a Puppetfile managed by Bolt. To forcibly overwrite the
235
- Puppetfile, run 'bolt module install --force'.
236
- MESSAGE
237
-
238
- raise Bolt::Error.new(message, 'bolt/missing-module-specs')
239
- end
240
- end
241
197
  end
242
198
  end