bolt 2.37.0 → 2.44.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 (65) hide show
  1. checksums.yaml +4 -4
  2. data/Puppetfile +17 -17
  3. data/bolt-modules/boltlib/lib/puppet/functions/parallelize.rb +6 -8
  4. data/lib/bolt/analytics.rb +3 -2
  5. data/lib/bolt/applicator.rb +11 -1
  6. data/lib/bolt/bolt_option_parser.rb +20 -13
  7. data/lib/bolt/catalog.rb +10 -29
  8. data/lib/bolt/cli.rb +58 -40
  9. data/lib/bolt/config.rb +134 -119
  10. data/lib/bolt/config/options.rb +142 -77
  11. data/lib/bolt/config/transport/base.rb +2 -2
  12. data/lib/bolt/config/transport/local.rb +1 -0
  13. data/lib/bolt/config/transport/options.rb +18 -68
  14. data/lib/bolt/config/transport/orch.rb +1 -0
  15. data/lib/bolt/config/transport/ssh.rb +0 -5
  16. data/lib/bolt/executor.rb +15 -5
  17. data/lib/bolt/inventory.rb +26 -0
  18. data/lib/bolt/inventory/group.rb +35 -12
  19. data/lib/bolt/inventory/inventory.rb +1 -1
  20. data/lib/bolt/inventory/options.rb +130 -0
  21. data/lib/bolt/inventory/target.rb +10 -11
  22. data/lib/bolt/logger.rb +114 -10
  23. data/lib/bolt/module.rb +10 -2
  24. data/lib/bolt/module_installer.rb +25 -15
  25. data/lib/bolt/module_installer/resolver.rb +65 -12
  26. data/lib/bolt/module_installer/specs/forge_spec.rb +8 -2
  27. data/lib/bolt/module_installer/specs/git_spec.rb +17 -2
  28. data/lib/bolt/outputter.rb +19 -5
  29. data/lib/bolt/outputter/human.rb +24 -1
  30. data/lib/bolt/outputter/json.rb +1 -1
  31. data/lib/bolt/outputter/logger.rb +1 -1
  32. data/lib/bolt/outputter/rainbow.rb +12 -1
  33. data/lib/bolt/pal.rb +93 -14
  34. data/lib/bolt/pal/yaml_plan.rb +8 -2
  35. data/lib/bolt/pal/yaml_plan/evaluator.rb +2 -2
  36. data/lib/bolt/pal/yaml_plan/transpiler.rb +6 -1
  37. data/lib/bolt/plugin.rb +3 -3
  38. data/lib/bolt/plugin/cache.rb +8 -8
  39. data/lib/bolt/plugin/module.rb +1 -1
  40. data/lib/bolt/plugin/puppet_connect_data.rb +35 -0
  41. data/lib/bolt/plugin/puppetdb.rb +2 -2
  42. data/lib/bolt/project.rb +76 -50
  43. data/lib/bolt/project_manager.rb +2 -0
  44. data/lib/bolt/project_manager/config_migrator.rb +9 -1
  45. data/lib/bolt/project_manager/module_migrator.rb +2 -0
  46. data/lib/bolt/puppetdb/client.rb +8 -0
  47. data/lib/bolt/rerun.rb +1 -1
  48. data/lib/bolt/shell/bash.rb +1 -1
  49. data/lib/bolt/shell/bash/tmpdir.rb +4 -1
  50. data/lib/bolt/shell/powershell.rb +7 -5
  51. data/lib/bolt/target.rb +4 -0
  52. data/lib/bolt/task.rb +1 -1
  53. data/lib/bolt/transport/docker/connection.rb +2 -2
  54. data/lib/bolt/transport/local.rb +13 -0
  55. data/lib/bolt/transport/orch/connection.rb +1 -1
  56. data/lib/bolt/transport/ssh.rb +1 -2
  57. data/lib/bolt/transport/ssh/connection.rb +1 -1
  58. data/lib/bolt/validator.rb +227 -0
  59. data/lib/bolt/version.rb +1 -1
  60. data/lib/bolt_server/config.rb +1 -1
  61. data/lib/bolt_server/schemas/partials/task.json +1 -1
  62. data/lib/bolt_server/transport_app.rb +28 -27
  63. data/libexec/bolt_catalog +1 -1
  64. metadata +27 -11
  65. data/lib/bolt/config/validator.rb +0 -231
@@ -6,8 +6,14 @@ module Bolt
6
6
  CONTENT_NAME_REGEX = /\A[a-z][a-z0-9_]*(::[a-z][a-z0-9_]*)*\z/.freeze
7
7
  MODULE_NAME_REGEX = /\A[a-z][a-z0-9_]*\z/.freeze
8
8
 
9
- def self.discover(modulepath)
10
- modulepath.each_with_object({}) do |path, mods|
9
+ def self.discover(modulepath, project)
10
+ mods = {}
11
+
12
+ if project.load_as_module?
13
+ mods[project.name] = Bolt::Module.new(project.name, project.path.to_s)
14
+ end
15
+
16
+ modulepath.each do |path|
11
17
  next unless File.exist?(path) && File.directory?(path)
12
18
  Dir.children(path)
13
19
  .map { |dir| File.join(path, dir) }
@@ -20,6 +26,8 @@ module Bolt
20
26
  end
21
27
  end
22
28
  end
29
+
30
+ mods
23
31
  end
24
32
 
25
33
  attr_reader :name, :path
@@ -17,14 +17,14 @@ module Bolt
17
17
 
18
18
  # Adds a single module to the project.
19
19
  #
20
- def add(name, specs, puppetfile_path, moduledir, config_path)
20
+ def add(name, specs, puppetfile_path, moduledir, project_file, config)
21
21
  project_specs = Specs.new(specs)
22
22
 
23
23
  # Exit early if project config already includes a spec with this name.
24
24
  if project_specs.include?(name)
25
25
  @outputter.print_message(
26
- "Project configuration file #{config_path} already includes specification with name "\
27
- "#{name}. Nothing to do."
26
+ "Project configuration file #{project_file} already includes specification "\
27
+ "with name #{name}. Nothing to do."
28
28
  )
29
29
  return true
30
30
  end
@@ -47,30 +47,32 @@ module Bolt
47
47
  # a version conflict.
48
48
  @outputter.print_action_step("Resolving module dependencies, this may take a moment")
49
49
 
50
+ @outputter.start_spin
50
51
  begin
51
52
  resolve_specs.add_specs('name' => name)
52
- puppetfile = Resolver.new.resolve(resolve_specs)
53
+ puppetfile = Resolver.new.resolve(resolve_specs, config)
53
54
  rescue Bolt::Error
54
55
  project_specs.add_specs('name' => name)
55
- puppetfile = Resolver.new.resolve(project_specs)
56
+ puppetfile = Resolver.new.resolve(project_specs, config)
56
57
  end
58
+ @outputter.stop_spin
57
59
 
58
60
  # Display the diff between the existing Puppetfile and the new Puppetfile.
59
61
  print_puppetfile_diff(existing_puppetfile, puppetfile)
60
62
 
61
63
  # Add the module to the project configuration.
62
- @outputter.print_action_step("Updating project configuration file at #{config_path}")
64
+ @outputter.print_action_step("Updating project configuration file at #{project_file}")
63
65
 
64
- data = Bolt::Util.read_yaml_hash(config_path, 'project')
66
+ data = Bolt::Util.read_yaml_hash(project_file, 'project')
65
67
  data['modules'] ||= []
66
68
  data['modules'] << name.tr('-', '/')
67
69
 
68
70
  begin
69
- File.write(config_path, data.to_yaml)
71
+ File.write(project_file, data.to_yaml)
70
72
  rescue SystemCallError => e
71
73
  raise Bolt::FileError.new(
72
74
  "Unable to update project configuration file: #{e.message}",
73
- config
75
+ project_file
74
76
  )
75
77
  end
76
78
 
@@ -79,7 +81,7 @@ module Bolt
79
81
  puppetfile.write(puppetfile_path, moduledir)
80
82
 
81
83
  # Install the modules.
82
- install_puppetfile(puppetfile_path, moduledir)
84
+ install_puppetfile(puppetfile_path, moduledir, config)
83
85
  end
84
86
 
85
87
  # Outputs a diff of an old Puppetfile and a new Puppetfile.
@@ -145,7 +147,7 @@ module Bolt
145
147
 
146
148
  # Installs a project's module dependencies.
147
149
  #
148
- def install(specs, path, moduledir, force: false, resolve: true)
150
+ def install(specs, path, moduledir, config = {}, force: false, resolve: true)
149
151
  @outputter.print_message("Installing project modules\n\n")
150
152
 
151
153
  if resolve != false
@@ -155,7 +157,11 @@ module Bolt
155
157
  # and write a Puppetfile.
156
158
  if force || !path.exist?
157
159
  @outputter.print_action_step("Resolving module dependencies, this may take a moment")
158
- puppetfile = Resolver.new.resolve(specs)
160
+
161
+ # This doesn't use the block as it's more testable to just mock *_spin
162
+ @outputter.start_spin
163
+ puppetfile = Resolver.new.resolve(specs, config)
164
+ @outputter.stop_spin
159
165
 
160
166
  # We get here either through 'bolt module install' which uses the
161
167
  # managed modulepath (which isn't configurable) or through bolt
@@ -177,18 +183,22 @@ module Bolt
177
183
  end
178
184
 
179
185
  # Install the modules.
180
- install_puppetfile(path, moduledir)
186
+ install_puppetfile(path, moduledir, config)
181
187
  end
182
188
 
183
189
  # Installs the Puppetfile and generates types.
184
190
  #
185
191
  def install_puppetfile(path, moduledir, config = {})
186
192
  @outputter.print_action_step("Syncing modules from #{path} to #{moduledir}")
193
+ @outputter.start_spin
187
194
  ok = Installer.new(config).install(path, moduledir)
195
+ @outputter.stop_spin
188
196
 
189
197
  # Automatically generate types after installing modules
190
- @outputter.print_action_step("Generating type references")
191
- @pal.generate_types
198
+ if ok
199
+ @outputter.print_action_step("Generating type references")
200
+ @pal.generate_types(cache: true)
201
+ end
192
202
 
193
203
  @outputter.print_puppetfile_result(ok, path, moduledir)
194
204
 
@@ -9,14 +9,19 @@ module Bolt
9
9
  class Resolver
10
10
  # Resolves module specs and returns a Puppetfile object.
11
11
  #
12
- def resolve(specs)
12
+ def resolve(specs, config = {})
13
13
  require 'puppetfile-resolver'
14
14
 
15
15
  # Build the document model from the specs.
16
- document = PuppetfileResolver::Puppetfile::Document.new('')
16
+ document = PuppetfileResolver::Puppetfile::Document.new('')
17
+ unresolved = []
17
18
 
18
19
  specs.specs.each do |spec|
19
- document.add_module(spec.to_resolver_module)
20
+ if spec.resolve
21
+ document.add_module(spec.to_resolver_module)
22
+ else
23
+ unresolved << spec
24
+ end
20
25
  end
21
26
 
22
27
  # Make sure the document model is valid.
@@ -38,29 +43,49 @@ module Bolt
38
43
  # raised by puppetfile-resolver and re-raising them as Bolt errors.
39
44
  begin
40
45
  result = resolver.resolve(
41
- cache: nil,
42
- ui: nil,
43
- module_paths: [],
44
- allow_missing_modules: false
46
+ cache: nil,
47
+ ui: nil,
48
+ allow_missing_modules: false,
49
+ spec_searcher_configuration: spec_searcher_config(config)
45
50
  )
46
51
  rescue StandardError => e
47
52
  raise Bolt::Error.new(e.message, 'bolt/module-resolver-error')
48
53
  end
49
54
 
50
- # Convert the specs returned from the resolver into Bolt module objects.
51
- modules = result.specifications.values.each_with_object([]) do |mod, acc|
55
+ # Create the Puppetfile object.
56
+ generate_puppetfile(specs, result.specifications.values, unresolved)
57
+ end
58
+
59
+ # Creates a puppetfile-resolver config object.
60
+ #
61
+ private def spec_searcher_config(config)
62
+ PuppetfileResolver::SpecSearchers::Configuration.new.tap do |obj|
63
+ obj.forge.proxy = config.dig('forge', 'proxy') || config.dig('proxy')
64
+ obj.git.proxy = config.dig('proxy')
65
+ obj.forge.forge_api = config.dig('forge', 'baseurl')
66
+ end
67
+ end
68
+
69
+ # Creates a Puppetfile object with Module objects created from resolved and
70
+ # unresolved specs.
71
+ #
72
+ private def generate_puppetfile(specs, resolved, unresolved)
73
+ modules = []
74
+
75
+ # Convert the resolved specs into Bolt module objects.
76
+ resolved.each do |mod|
52
77
  # Skip over anything that isn't a module spec, such as a Puppet spec.
53
78
  next unless mod.is_a? PuppetfileResolver::Models::ModuleSpecification
54
79
 
55
80
  case mod.origin
56
81
  when :forge
57
- acc << Bolt::ModuleInstaller::Puppetfile::ForgeModule.new(
82
+ modules << Bolt::ModuleInstaller::Puppetfile::ForgeModule.new(
58
83
  "#{mod.owner}/#{mod.name}",
59
84
  mod.version.to_s
60
85
  )
61
86
  when :git
62
87
  spec = specs.specs.find { |s| s.name == mod.name }
63
- acc << Bolt::ModuleInstaller::Puppetfile::GitModule.new(
88
+ modules << Bolt::ModuleInstaller::Puppetfile::GitModule.new(
64
89
  spec.name,
65
90
  spec.git,
66
91
  spec.sha
@@ -68,7 +93,35 @@ module Bolt
68
93
  end
69
94
  end
70
95
 
71
- # Create the Puppetfile object.
96
+ # Error if there are any name conflicts between unresolved specs and
97
+ # resolved modules. r10k will error if a Puppetfile includes duplicate
98
+ # names, but we error early here to provide a more helpful message.
99
+ if (name_conflicts = modules.map(&:name) & unresolved.map(&:name)).any?
100
+ raise Bolt::Error.new(
101
+ "Detected unresolved module specifications with the same name as a resolved module "\
102
+ "dependency: #{name_conflicts.join(', ')}. Either remove the unresolved module specification "\
103
+ "or set the module with the conflicting dependency to not resolve.",
104
+ "bolt/module-name-conflict-error"
105
+ )
106
+ end
107
+
108
+ # Convert the unresolved specs into Bolt module objects.
109
+ unresolved.each do |spec|
110
+ case spec.type
111
+ when :forge
112
+ modules << Bolt::ModuleInstaller::Puppetfile::ForgeModule.new(
113
+ spec.full_name,
114
+ spec.version_requirement
115
+ )
116
+ when :git
117
+ modules << Bolt::ModuleInstaller::Puppetfile::GitModule.new(
118
+ spec.name,
119
+ spec.git,
120
+ spec.ref
121
+ )
122
+ end
123
+ end
124
+
72
125
  Bolt::ModuleInstaller::Puppetfile.new(modules)
73
126
  end
74
127
  end
@@ -13,14 +13,20 @@ module Bolt
13
13
  class ForgeSpec
14
14
  NAME_REGEX = %r{\A[a-zA-Z0-9]+[-/](?<name>[a-z][a-z0-9_]*)\z}.freeze
15
15
  REQUIRED_KEYS = Set.new(%w[name]).freeze
16
- KNOWN_KEYS = Set.new(%w[name version_requirement]).freeze
16
+ KNOWN_KEYS = Set.new(%w[name resolve version_requirement]).freeze
17
17
 
18
- attr_reader :full_name, :name, :semantic_version, :type
18
+ attr_reader :full_name, :name, :resolve, :semantic_version, :type, :version_requirement
19
19
 
20
20
  def initialize(init_hash)
21
+ @resolve = init_hash.key?('resolve') ? init_hash['resolve'] : true
21
22
  @full_name, @name = parse_name(init_hash['name'])
22
23
  @version_requirement, @semantic_version = parse_version_requirement(init_hash['version_requirement'])
23
24
  @type = :forge
25
+
26
+ unless @resolve == true || @resolve == false
27
+ raise Bolt::ValidationError,
28
+ "Option 'resolve' for module spec #{@full_name} must be a Boolean"
29
+ end
24
30
  end
25
31
 
26
32
  def self.implements?(hash)
@@ -13,18 +13,31 @@ module Bolt
13
13
  class GitSpec
14
14
  NAME_REGEX = %r{\A(?:[a-zA-Z0-9]+[-/])?(?<name>[a-z][a-z0-9_]*)\z}.freeze
15
15
  REQUIRED_KEYS = Set.new(%w[git ref]).freeze
16
+ KNOWN_KEYS = Set.new(%w[git name ref resolve]).freeze
16
17
 
17
- attr_reader :git, :ref, :type
18
+ attr_reader :git, :ref, :resolve, :type
18
19
 
19
20
  def initialize(init_hash)
21
+ @resolve = init_hash.key?('resolve') ? init_hash['resolve'] : true
20
22
  @name = parse_name(init_hash['name'])
21
23
  @git, @repo = parse_git(init_hash['git'])
22
24
  @ref = init_hash['ref']
23
25
  @type = :git
26
+
27
+ if @name.nil? && @resolve == false
28
+ raise Bolt::ValidationError,
29
+ "Missing name for Git module specification: #{@git}. Git module specifications "\
30
+ "must include a 'name' key when 'resolve' is false."
31
+ end
32
+
33
+ unless @resolve == true || @resolve == false
34
+ raise Bolt::ValidationError,
35
+ "Option 'resolve' for module spec #{@git} must be a Boolean"
36
+ end
24
37
  end
25
38
 
26
39
  def self.implements?(hash)
27
- REQUIRED_KEYS == hash.keys.to_set
40
+ KNOWN_KEYS.superset?(hash.keys.to_set) && REQUIRED_KEYS.subset?(hash.keys.to_set)
28
41
  end
29
42
 
30
43
  # Parses the name into owner and name segments, and formats the full
@@ -47,6 +60,8 @@ module Bolt
47
60
  # Gets the repo from the git URL.
48
61
  #
49
62
  private def parse_git(git)
63
+ return [git, nil] unless @resolve
64
+
50
65
  repo = if git.start_with?('git@github.com:')
51
66
  git.split('git@github.com:').last.split('.git').first
52
67
  elsif git.start_with?('https://github.com')
@@ -2,24 +2,25 @@
2
2
 
3
3
  module Bolt
4
4
  class Outputter
5
- def self.for_format(format, color, verbose, trace)
5
+ def self.for_format(format, color, verbose, trace, spin)
6
6
  case format
7
7
  when 'human'
8
- Bolt::Outputter::Human.new(color, verbose, trace)
8
+ Bolt::Outputter::Human.new(color, verbose, trace, spin)
9
9
  when 'json'
10
- Bolt::Outputter::JSON.new(color, verbose, trace)
10
+ Bolt::Outputter::JSON.new(color, verbose, trace, false)
11
11
  when 'rainbow'
12
- Bolt::Outputter::Rainbow.new(color, verbose, trace)
12
+ Bolt::Outputter::Rainbow.new(color, verbose, trace, spin)
13
13
  when nil
14
14
  raise "Cannot use outputter before parsing."
15
15
  end
16
16
  end
17
17
 
18
- def initialize(color, verbose, trace, stream = $stdout)
18
+ def initialize(color, verbose, trace, spin, stream = $stdout)
19
19
  @color = color
20
20
  @verbose = verbose
21
21
  @trace = trace
22
22
  @stream = stream
23
+ @spin = spin
23
24
  end
24
25
 
25
26
  def indent(indent, string)
@@ -34,6 +35,19 @@ module Bolt
34
35
  def print_error
35
36
  raise NotImplementedError, "print_error() must be implemented by the outputter class"
36
37
  end
38
+
39
+ def start_spin; end
40
+
41
+ def stop_spin; end
42
+
43
+ def spin
44
+ start_spin
45
+ begin
46
+ yield
47
+ ensure
48
+ stop_spin
49
+ end
50
+ end
37
51
  end
38
52
  end
39
53
 
@@ -14,12 +14,13 @@ module Bolt
14
14
 
15
15
  def print_head; end
16
16
 
17
- def initialize(color, verbose, trace, stream = $stdout)
17
+ def initialize(color, verbose, trace, spin, stream = $stdout)
18
18
  super
19
19
  # Plans and without_default_logging() calls can both be nested, so we
20
20
  # track each of them with a "stack" consisting of an integer.
21
21
  @plan_depth = 0
22
22
  @disable_depth = 0
23
+ @pinwheel = %w[- \\ | /]
23
24
  end
24
25
 
25
26
  def colorize(color, string)
@@ -30,6 +31,24 @@ module Bolt
30
31
  end
31
32
  end
32
33
 
34
+ def start_spin
35
+ return unless @spin && @stream.isatty && !@spinning
36
+ @spinning = true
37
+ @spin_thread = Thread.new do
38
+ loop do
39
+ sleep(0.1)
40
+ @stream.print(colorize(:cyan, @pinwheel.rotate!.first + "\b"))
41
+ end
42
+ end
43
+ end
44
+
45
+ def stop_spin
46
+ return unless @spin && @stream.isatty && @spinning
47
+ @spinning = false
48
+ @spin_thread.terminate
49
+ @stream.print("\b")
50
+ end
51
+
33
52
  def remove_trail(string)
34
53
  string.sub(/\s\z/, '')
35
54
  end
@@ -62,6 +81,10 @@ module Bolt
62
81
  print_plan_start(event)
63
82
  when :plan_finish
64
83
  print_plan_finish(event)
84
+ when :start_spin
85
+ start_spin
86
+ when :stop_spin
87
+ stop_spin
65
88
  end
66
89
  end
67
90
  end
@@ -3,7 +3,7 @@
3
3
  module Bolt
4
4
  class Outputter
5
5
  class JSON < Bolt::Outputter
6
- def initialize(color, verbose, trace, stream = $stdout)
6
+ def initialize(color, verbose, trace, spin, stream = $stdout)
7
7
  super
8
8
  @items_open = false
9
9
  @object_open = false
@@ -6,7 +6,7 @@ module Bolt
6
6
  class Outputter
7
7
  class Logger < Bolt::Outputter
8
8
  def initialize(verbose, trace)
9
- super(false, verbose, trace)
9
+ super(false, verbose, trace, false)
10
10
  @logger = Bolt::Logger.logger(self)
11
11
  end
12
12
 
@@ -5,7 +5,7 @@ require 'bolt/pal'
5
5
  module Bolt
6
6
  class Outputter
7
7
  class Rainbow < Bolt::Outputter::Human
8
- def initialize(color, verbose, trace, stream = $stdout)
8
+ def initialize(color, verbose, trace, spin, stream = $stdout)
9
9
  begin
10
10
  require 'paint'
11
11
  if Bolt::Util.windows?
@@ -62,6 +62,17 @@ module Bolt
62
62
  end
63
63
  end
64
64
 
65
+ def start_spin
66
+ return unless @spin && @stream.isatty && !@spinning
67
+ @spinning = true
68
+ @spin_thread = Thread.new do
69
+ loop do
70
+ sleep(0.1)
71
+ @stream.print(colorize(:rainbow, @pinwheel.rotate!.first + "\b"))
72
+ end
73
+ end
74
+ end
75
+
65
76
  def print_summary(results, elapsed_time = nil)
66
77
  ok_set = results.ok_set
67
78
  unless ok_set.empty?