bolt 2.42.0 → 3.3.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 (90) hide show
  1. checksums.yaml +4 -4
  2. data/Puppetfile +21 -19
  3. data/bolt-modules/boltlib/lib/puppet/functions/add_facts.rb +1 -1
  4. data/bolt-modules/boltlib/lib/puppet/functions/apply_prep.rb +25 -0
  5. data/bolt-modules/boltlib/lib/puppet/functions/parallelize.rb +6 -8
  6. data/bolt-modules/boltlib/lib/puppet/functions/run_plan.rb +2 -2
  7. data/bolt-modules/boltlib/lib/puppet/functions/run_script.rb +27 -5
  8. data/bolt-modules/boltlib/lib/puppet/functions/upload_file.rb +1 -1
  9. data/bolt-modules/boltlib/lib/puppet/functions/wait_until_available.rb +7 -3
  10. data/bolt-modules/file/lib/puppet/functions/file/read.rb +3 -2
  11. data/lib/bolt/analytics.rb +3 -2
  12. data/lib/bolt/applicator.rb +11 -1
  13. data/lib/bolt/apply_result.rb +1 -1
  14. data/lib/bolt/bolt_option_parser.rb +9 -116
  15. data/lib/bolt/catalog.rb +10 -29
  16. data/lib/bolt/cli.rb +90 -154
  17. data/lib/bolt/config.rb +66 -239
  18. data/lib/bolt/config/options.rb +79 -102
  19. data/lib/bolt/config/transport/local.rb +1 -0
  20. data/lib/bolt/config/transport/lxd.rb +21 -0
  21. data/lib/bolt/config/transport/options.rb +9 -2
  22. data/lib/bolt/config/transport/orch.rb +1 -0
  23. data/lib/bolt/executor.rb +23 -6
  24. data/lib/bolt/inventory.rb +1 -1
  25. data/lib/bolt/inventory/group.rb +7 -4
  26. data/lib/bolt/logger.rb +123 -11
  27. data/lib/bolt/module_installer.rb +6 -4
  28. data/lib/bolt/module_installer/puppetfile.rb +2 -2
  29. data/lib/bolt/module_installer/resolver.rb +59 -14
  30. data/lib/bolt/module_installer/specs/forge_spec.rb +10 -4
  31. data/lib/bolt/module_installer/specs/git_spec.rb +19 -4
  32. data/lib/bolt/outputter/human.rb +56 -17
  33. data/lib/bolt/outputter/json.rb +16 -16
  34. data/lib/bolt/outputter/rainbow.rb +3 -3
  35. data/lib/bolt/pal.rb +95 -15
  36. data/lib/bolt/pal/yaml_plan.rb +9 -4
  37. data/lib/bolt/pal/yaml_plan/evaluator.rb +5 -153
  38. data/lib/bolt/pal/yaml_plan/step.rb +91 -52
  39. data/lib/bolt/pal/yaml_plan/step/command.rb +16 -16
  40. data/lib/bolt/pal/yaml_plan/step/download.rb +15 -16
  41. data/lib/bolt/pal/yaml_plan/step/eval.rb +11 -11
  42. data/lib/bolt/pal/yaml_plan/step/message.rb +13 -4
  43. data/lib/bolt/pal/yaml_plan/step/plan.rb +19 -15
  44. data/lib/bolt/pal/yaml_plan/step/resources.rb +82 -21
  45. data/lib/bolt/pal/yaml_plan/step/script.rb +32 -17
  46. data/lib/bolt/pal/yaml_plan/step/task.rb +19 -16
  47. data/lib/bolt/pal/yaml_plan/step/upload.rb +16 -17
  48. data/lib/bolt/pal/yaml_plan/transpiler.rb +2 -1
  49. data/lib/bolt/plan_creator.rb +1 -1
  50. data/lib/bolt/plugin.rb +2 -2
  51. data/lib/bolt/plugin/cache.rb +7 -7
  52. data/lib/bolt/plugin/module.rb +0 -23
  53. data/lib/bolt/plugin/puppet_connect_data.rb +77 -0
  54. data/lib/bolt/plugin/puppetdb.rb +1 -1
  55. data/lib/bolt/project.rb +54 -81
  56. data/lib/bolt/project_manager.rb +5 -4
  57. data/lib/bolt/project_manager/module_migrator.rb +7 -6
  58. data/lib/bolt/rerun.rb +1 -1
  59. data/lib/bolt/result.rb +6 -1
  60. data/lib/bolt/shell.rb +16 -0
  61. data/lib/bolt/shell/bash.rb +57 -25
  62. data/lib/bolt/shell/bash/tmpdir.rb +6 -3
  63. data/lib/bolt/shell/powershell.rb +33 -10
  64. data/lib/bolt/shell/powershell/snippets.rb +37 -150
  65. data/lib/bolt/task.rb +2 -2
  66. data/lib/bolt/transport/base.rb +0 -9
  67. data/lib/bolt/transport/docker.rb +1 -125
  68. data/lib/bolt/transport/docker/connection.rb +86 -161
  69. data/lib/bolt/transport/local.rb +1 -9
  70. data/lib/bolt/transport/lxd.rb +26 -0
  71. data/lib/bolt/transport/lxd/connection.rb +99 -0
  72. data/lib/bolt/transport/orch/connection.rb +1 -1
  73. data/lib/bolt/transport/ssh.rb +1 -2
  74. data/lib/bolt/transport/ssh/connection.rb +2 -2
  75. data/lib/bolt/transport/winrm/connection.rb +1 -1
  76. data/lib/bolt/validator.rb +2 -2
  77. data/lib/bolt/version.rb +1 -1
  78. data/lib/bolt_server/config.rb +1 -1
  79. data/lib/bolt_server/transport_app.rb +61 -32
  80. data/lib/bolt_spec/bolt_context.rb +9 -4
  81. data/lib/bolt_spec/plans.rb +1 -109
  82. data/lib/bolt_spec/plans/action_stubs.rb +1 -1
  83. data/lib/bolt_spec/plans/mock_executor.rb +4 -0
  84. data/libexec/bolt_catalog +1 -1
  85. data/modules/aggregate/plans/count.pp +21 -0
  86. data/modules/aggregate/plans/targets.pp +21 -0
  87. data/modules/puppet_connect/plans/test_input_data.pp +67 -0
  88. data/modules/puppetdb_fact/plans/init.pp +10 -0
  89. metadata +13 -9
  90. data/modules/aggregate/plans/nodes.pp +0 -36
@@ -97,7 +97,7 @@ module Bolt
97
97
 
98
98
  Bolt::Validator.new.tap do |validator|
99
99
  validator.validate(data, schema, source)
100
- validator.warnings.each { |warning| logger.warn(warning) }
100
+ validator.warnings.each { |warning| Bolt::Logger.warn(warning[:id], warning[:msg]) }
101
101
  end
102
102
 
103
103
  inventory = create_version(data, config.transport, config.transports, plugins)
@@ -27,7 +27,10 @@ module Bolt
27
27
 
28
28
  if all_group
29
29
  if input.key?('name') && input['name'] != 'all'
30
- @logger.warn("Top-level group '#{input['name']}' cannot specify a name, using 'all' instead.")
30
+ Bolt::Logger.warn(
31
+ "top_level_group_name",
32
+ "Top-level group '#{input['name']}' cannot specify a name, using 'all' instead."
33
+ )
31
34
  end
32
35
 
33
36
  input = input.merge('name' => 'all')
@@ -134,7 +137,7 @@ module Bolt
134
137
 
135
138
  unless (unexpected_keys = target.keys - TARGET_KEYS).empty?
136
139
  msg = "Found unexpected key(s) #{unexpected_keys.join(', ')} in target #{t_name}"
137
- @logger.warn(msg)
140
+ Bolt::Logger.warn("unknown_target_keys", msg)
138
141
  end
139
142
 
140
143
  validate_data_keys(target, t_name)
@@ -261,7 +264,7 @@ module Bolt
261
264
 
262
265
  unless (unexpected_keys = input.keys - GROUP_KEYS).empty?
263
266
  msg = "Found unexpected key(s) #{unexpected_keys.join(', ')} in group #{@name}"
264
- @logger.warn(msg)
267
+ Bolt::Logger.warn("unknown_group_keys", msg)
265
268
  end
266
269
  end
267
270
 
@@ -368,7 +371,7 @@ module Bolt
368
371
  msg = +"Found unexpected key(s) #{unexpected_keys.join(', ')} in config for"
369
372
  msg << " target #{target} in" if target
370
373
  msg << " group #{@name}"
371
- @logger.warn(msg)
374
+ Bolt::Logger.warn("unknown_config_keys", msg)
372
375
  end
373
376
  end
374
377
  end
data/lib/bolt/logger.rb CHANGED
@@ -4,9 +4,15 @@ 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
7
+ LEVELS = %w[trace debug info warn error fatal].freeze
8
+
9
+ # This module is treated as a global singleton so that multiple classes
10
+ # in Bolt can log warnings with IDs. Access to the following variables
11
+ # are controlled by a mutex.
12
+ @mutex = Mutex.new
13
+ @warnings = Set.new
14
+ @disable_warnings = Set.new
15
+ @message_queue = []
10
16
 
11
17
  # This method provides a single point-of-entry to setup logging for both
12
18
  # the CLI and for tests. This is necessary because we define custom log
@@ -36,7 +42,7 @@ module Bolt
36
42
  end
37
43
  end
38
44
 
39
- def self.configure(destinations, color)
45
+ def self.configure(destinations, color, disable_warnings = nil)
40
46
  root_logger = Bolt::Logger.logger(:root)
41
47
 
42
48
  root_logger.add_appenders Logging.appenders.stderr(
@@ -73,6 +79,24 @@ module Bolt
73
79
 
74
80
  appender.level = params[:level] if params[:level]
75
81
  end
82
+
83
+ # Set the list of disabled warnings and mark the logger as configured.
84
+ # Log all messages in the message queue and flush the queue.
85
+ if disable_warnings
86
+ @mutex.synchronize { @disable_warnings = disable_warnings }
87
+ end
88
+ end
89
+
90
+ def self.configured?
91
+ Logging.logger[:root].appenders.any?
92
+ end
93
+
94
+ def self.stream
95
+ @stream
96
+ end
97
+
98
+ def self.stream=(stream)
99
+ @stream = stream
76
100
  end
77
101
 
78
102
  # A helper to ensure the Logging library is always initialized with our
@@ -123,18 +147,106 @@ module Bolt
123
147
  Logging.reset
124
148
  end
125
149
 
126
- def self.warn_once(type, msg)
150
+ # The following methods are used in place of the Logging.logger
151
+ # methods of the same name when logging warning messages or logging
152
+ # any messages prior to the logger being configured. If the logger
153
+ # is not configured when any of these methods are called, the message
154
+ # will be added to a queue, otherwise they are logged immediately.
155
+ # The message queue is flushed by calling #flush_queue, which is
156
+ # called from Bolt::CLI after configuring the logger.
157
+ #
158
+ def self.warn(id, msg)
159
+ log(type: :warn, msg: "#{msg} [ID: #{id}]", id: id)
160
+ end
161
+
162
+ def self.warn_once(id, msg)
163
+ log(type: :warn_once, msg: "#{msg} [ID: #{id}]", id: id)
164
+ end
165
+
166
+ def self.deprecate(id, msg)
167
+ log(type: :deprecate, msg: "#{msg} [ID: #{id}]", id: id)
168
+ end
169
+
170
+ def self.deprecate_once(id, msg)
171
+ log(type: :deprecate_once, msg: "#{msg} [ID: #{id}]", id: id)
172
+ end
173
+
174
+ def self.debug(msg)
175
+ log(type: :debug, msg: msg)
176
+ end
177
+
178
+ def self.info(msg)
179
+ log(type: :info, msg: msg)
180
+ end
181
+
182
+ # Logs a message. If the logger has not been configured, this will queue
183
+ # the message to be logged later. Once the logger is configured, the
184
+ # queue will be flushed of all messages and new messages will be logged
185
+ # immediately.
186
+ #
187
+ # Logging with this method is controlled by a mutex, as the Bolt::Logger
188
+ # module is treated as a global singleton to allow multiple classes
189
+ # access to its methods.
190
+ #
191
+ private_class_method def self.log(type:, msg:, id: nil)
127
192
  @mutex.synchronize do
128
- @logger ||= Bolt::Logger.logger(self)
129
- if @warnings.add?(type)
130
- @logger.warn(msg)
193
+ if configured?
194
+ log_message(type: type, msg: msg, id: id)
195
+ else
196
+ @message_queue << { type: type, msg: msg, id: id }
131
197
  end
132
198
  end
133
199
  end
134
200
 
135
- def self.deprecation_warning(type, msg)
136
- @analytics&.event('Warn', 'deprecation', label: type)
137
- warn_once(type, msg)
201
+ # Logs all messages in the message queue and then flushes the queue.
202
+ #
203
+ def self.flush_queue
204
+ @mutex.synchronize do
205
+ @message_queue.each do |message|
206
+ log_message(message)
207
+ end
208
+
209
+ @message_queue.clear
210
+ end
211
+ end
212
+
213
+ # Handles the actual logging of a message.
214
+ #
215
+ private_class_method def self.log_message(type:, msg:, id: nil)
216
+ case type
217
+ when :warn
218
+ do_warn(msg, id)
219
+ when :warn_once
220
+ do_warn_once(msg, id)
221
+ when :deprecate
222
+ do_deprecate(msg, id)
223
+ when :deprecate_once
224
+ do_deprecate_once(msg, id)
225
+ else
226
+ logger(self).send(type, msg)
227
+ end
228
+ end
229
+
230
+ # The following methods do the actual warning.
231
+ #
232
+ private_class_method def self.do_warn(msg, id)
233
+ return if @disable_warnings.include?(id)
234
+ logger(self).warn(msg)
235
+ end
236
+
237
+ private_class_method def self.do_warn_once(msg, id)
238
+ return unless @warnings.add?(id)
239
+ do_warn(msg, id)
240
+ end
241
+
242
+ private_class_method def self.do_deprecate(msg, id)
243
+ @analytics&.event('Warn', 'deprecation', label: id)
244
+ do_warn(msg, id)
245
+ end
246
+
247
+ private_class_method def self.do_deprecate_once(msg, id)
248
+ @analytics&.event('Warn', 'deprecation', label: id)
249
+ do_warn_once(msg, id)
138
250
  end
139
251
  end
140
252
  end
@@ -45,7 +45,7 @@ module Bolt
45
45
  # specss. If that fails, fall back to resolving from project specs.
46
46
  # This prevents Bolt from modifying installed modules unless there is
47
47
  # a version conflict.
48
- @outputter.print_action_step("Resolving module dependencies, this may take a moment")
48
+ @outputter.print_action_step("Resolving module dependencies, this might take a moment")
49
49
 
50
50
  @outputter.start_spin
51
51
  begin
@@ -156,7 +156,7 @@ module Bolt
156
156
  # If forcibly installing or if there is no Puppetfile, resolve
157
157
  # and write a Puppetfile.
158
158
  if force || !path.exist?
159
- @outputter.print_action_step("Resolving module dependencies, this may take a moment")
159
+ @outputter.print_action_step("Resolving module dependencies, this might take a moment")
160
160
 
161
161
  # This doesn't use the block as it's more testable to just mock *_spin
162
162
  @outputter.start_spin
@@ -195,8 +195,10 @@ module Bolt
195
195
  @outputter.stop_spin
196
196
 
197
197
  # Automatically generate types after installing modules
198
- @outputter.print_action_step("Generating type references")
199
- @pal.generate_types
198
+ if ok
199
+ @outputter.print_action_step("Generating type references")
200
+ @pal.generate_types(cache: true)
201
+ end
200
202
 
201
203
  @outputter.print_puppetfile_result(ok, path, moduledir)
202
204
 
@@ -36,7 +36,7 @@ module Bolt
36
36
  raise Bolt::ValidationError, <<~MSG
37
37
  Unable to parse Puppetfile #{path}:
38
38
  #{parsed.validation_errors.join("\n\n")}.
39
- This may not be a Puppetfile managed by Bolt.
39
+ This Puppetfile might not be managed by Bolt.
40
40
  MSG
41
41
  end
42
42
 
@@ -106,7 +106,7 @@ module Bolt
106
106
 
107
107
  #{unsatisfied_specs.map(&:to_hash).to_yaml.lines.drop(1).join.chomp}
108
108
 
109
- This may not be a Puppetfile managed by Bolt. To forcibly overwrite the
109
+ This Puppetfile might not be managed by Bolt. To forcibly overwrite the
110
110
  Puppetfile, run '#{command}'.
111
111
  MESSAGE
112
112
 
@@ -13,10 +13,15 @@ module Bolt
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.
@@ -47,20 +52,40 @@ module Bolt
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,16 +93,36 @@ module Bolt
68
93
  end
69
94
  end
70
95
 
71
- # Create the Puppetfile object.
72
- Bolt::ModuleInstaller::Puppetfile.new(modules)
73
- end
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
74
107
 
75
- private def spec_searcher_config(config)
76
- PuppetfileResolver::SpecSearchers::Configuration.new.tap do |obj|
77
- obj.forge.proxy = config.dig('forge', 'proxy') || config.dig('proxy')
78
- obj.git.proxy = config.dig('proxy')
79
- obj.forge.forge_api = config.dig('forge', 'baseurl')
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
80
123
  end
124
+
125
+ Bolt::ModuleInstaller::Puppetfile.new(modules)
81
126
  end
82
127
  end
83
128
  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)
@@ -33,8 +39,8 @@ module Bolt
33
39
  unless (match = name.match(NAME_REGEX))
34
40
  raise Bolt::ValidationError,
35
41
  "Invalid name for Forge module specification: #{name}. Name must match "\
36
- "'owner/name'. Owner segment may only include letters or digits. Name "\
37
- "segment must start with a lowercase letter and may only include lowercase "\
42
+ "'owner/name'. Owner segment can only include letters or digits. Name "\
43
+ "segment must start with a lowercase letter and can only include lowercase "\
38
44
  "letters, digits, and underscores."
39
45
  end
40
46
 
@@ -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
@@ -36,8 +49,8 @@ module Bolt
36
49
  unless (match = name.match(NAME_REGEX))
37
50
  raise Bolt::ValidationError,
38
51
  "Invalid name for Git module specification: #{name}. Name must match "\
39
- "'name' or 'owner/name'. Owner segment may only include letters or digits. "\
40
- "Name segment must start with a lowercase letter and may only include "\
52
+ "'name' or 'owner/name'. Owner segment can only include letters or digits. "\
53
+ "Name segment must start with a lowercase letter and can only include "\
41
54
  "lowercase letters, digits, and underscores."
42
55
  end
43
56
 
@@ -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')