bolt 2.38.0 → 3.0.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 (66) hide show
  1. checksums.yaml +4 -4
  2. data/Puppetfile +17 -17
  3. data/bolt-modules/boltlib/lib/puppet/functions/apply_prep.rb +25 -0
  4. data/bolt-modules/boltlib/lib/puppet/functions/parallelize.rb +6 -8
  5. data/bolt-modules/boltlib/lib/puppet/functions/wait_until_available.rb +7 -3
  6. data/lib/bolt/analytics.rb +3 -2
  7. data/lib/bolt/applicator.rb +11 -1
  8. data/lib/bolt/bolt_option_parser.rb +3 -113
  9. data/lib/bolt/catalog.rb +10 -29
  10. data/lib/bolt/cli.rb +54 -155
  11. data/lib/bolt/config.rb +63 -269
  12. data/lib/bolt/config/options.rb +59 -97
  13. data/lib/bolt/config/transport/local.rb +1 -0
  14. data/lib/bolt/config/transport/options.rb +10 -2
  15. data/lib/bolt/config/transport/orch.rb +1 -0
  16. data/lib/bolt/config/transport/ssh.rb +0 -5
  17. data/lib/bolt/executor.rb +15 -5
  18. data/lib/bolt/inventory.rb +3 -2
  19. data/lib/bolt/inventory/group.rb +35 -12
  20. data/lib/bolt/inventory/inventory.rb +1 -1
  21. data/lib/bolt/logger.rb +115 -11
  22. data/lib/bolt/module.rb +10 -2
  23. data/lib/bolt/module_installer.rb +4 -2
  24. data/lib/bolt/module_installer/resolver.rb +65 -12
  25. data/lib/bolt/module_installer/specs/forge_spec.rb +8 -2
  26. data/lib/bolt/module_installer/specs/git_spec.rb +17 -2
  27. data/lib/bolt/outputter/human.rb +9 -5
  28. data/lib/bolt/outputter/json.rb +16 -16
  29. data/lib/bolt/outputter/rainbow.rb +3 -3
  30. data/lib/bolt/pal.rb +93 -14
  31. data/lib/bolt/pal/yaml_plan.rb +8 -2
  32. data/lib/bolt/pal/yaml_plan/evaluator.rb +7 -19
  33. data/lib/bolt/pal/yaml_plan/step.rb +3 -24
  34. data/lib/bolt/pal/yaml_plan/step/upload.rb +2 -2
  35. data/lib/bolt/pal/yaml_plan/transpiler.rb +6 -1
  36. data/lib/bolt/plugin.rb +3 -3
  37. data/lib/bolt/plugin/cache.rb +8 -8
  38. data/lib/bolt/plugin/module.rb +0 -23
  39. data/lib/bolt/plugin/puppet_connect_data.rb +77 -0
  40. data/lib/bolt/plugin/puppetdb.rb +1 -1
  41. data/lib/bolt/project.rb +54 -81
  42. data/lib/bolt/project_manager.rb +4 -3
  43. data/lib/bolt/project_manager/module_migrator.rb +6 -5
  44. data/lib/bolt/rerun.rb +1 -1
  45. data/lib/bolt/shell/bash.rb +1 -1
  46. data/lib/bolt/shell/bash/tmpdir.rb +4 -1
  47. data/lib/bolt/shell/powershell.rb +3 -4
  48. data/lib/bolt/shell/powershell/snippets.rb +9 -149
  49. data/lib/bolt/task.rb +1 -1
  50. data/lib/bolt/transport/docker/connection.rb +2 -2
  51. data/lib/bolt/transport/local.rb +1 -9
  52. data/lib/bolt/transport/orch/connection.rb +1 -1
  53. data/lib/bolt/transport/ssh.rb +1 -2
  54. data/lib/bolt/transport/ssh/connection.rb +1 -1
  55. data/lib/bolt/validator.rb +16 -15
  56. data/lib/bolt/version.rb +1 -1
  57. data/lib/bolt_server/config.rb +1 -1
  58. data/lib/bolt_server/schemas/partials/task.json +1 -1
  59. data/lib/bolt_server/transport_app.rb +3 -2
  60. data/libexec/bolt_catalog +1 -1
  61. data/modules/aggregate/plans/count.pp +21 -0
  62. data/modules/aggregate/plans/targets.pp +21 -0
  63. data/modules/puppet_connect/plans/test_input_data.pp +31 -0
  64. data/modules/puppetdb_fact/plans/init.pp +10 -0
  65. metadata +26 -17
  66. data/modules/aggregate/plans/nodes.pp +0 -36
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'bolt/config/options'
3
4
  require 'bolt/inventory/group'
4
5
  require 'bolt/inventory/inventory'
5
6
  require 'bolt/inventory/target'
@@ -18,12 +19,23 @@ module Bolt
18
19
  GROUP_KEYS = DATA_KEYS + %w[name groups targets]
19
20
  CONFIG_KEYS = Bolt::Config::INVENTORY_OPTIONS.keys
20
21
 
21
- def initialize(input, plugins)
22
+ def initialize(input, plugins, all_group: false)
22
23
  @logger = Bolt::Logger.logger(self)
23
24
  @plugins = plugins
24
25
 
25
26
  input = @plugins.resolve_top_level_references(input) if @plugins.reference?(input)
26
27
 
28
+ if all_group
29
+ if input.key?('name') && input['name'] != 'all'
30
+ Bolt::Logger.warn(
31
+ "top_level_group_name",
32
+ "Top-level group '#{input['name']}' cannot specify a name, using 'all' instead."
33
+ )
34
+ end
35
+
36
+ input = input.merge('name' => 'all')
37
+ end
38
+
27
39
  raise ValidationError.new("Group does not have a name", nil) unless input.key?('name')
28
40
 
29
41
  @name = @plugins.resolve_references(input['name'])
@@ -125,7 +137,7 @@ module Bolt
125
137
 
126
138
  unless (unexpected_keys = target.keys - TARGET_KEYS).empty?
127
139
  msg = "Found unexpected key(s) #{unexpected_keys.join(', ')} in target #{t_name}"
128
- @logger.warn(msg)
140
+ Bolt::Logger.warn("unknown_target_keys", msg)
129
141
  end
130
142
 
131
143
  validate_data_keys(target, t_name)
@@ -252,15 +264,7 @@ module Bolt
252
264
 
253
265
  unless (unexpected_keys = input.keys - GROUP_KEYS).empty?
254
266
  msg = "Found unexpected key(s) #{unexpected_keys.join(', ')} in group #{@name}"
255
- @logger.warn(msg)
256
- end
257
-
258
- Bolt::Util.walk_keys(input) do |key|
259
- if @plugins.reference?(key)
260
- raise ValidationError.new("Group keys cannot be specified as _plugin references", @name)
261
- else
262
- key
263
- end
267
+ Bolt::Logger.warn("unknown_group_keys", msg)
264
268
  end
265
269
  end
266
270
 
@@ -323,7 +327,26 @@ module Bolt
323
327
  'features' => @plugins.resolve_references(data.fetch('features', [])),
324
328
  'plugin_hooks' => @plugins.resolve_references(data.fetch('plugin_hooks', {}))
325
329
  }
330
+
326
331
  validate_data_keys(result, target)
332
+
333
+ Bolt::Config::Options::TRANSPORT_CONFIG.each_key do |transport|
334
+ next unless result['config'].key?(transport)
335
+ transport_config = result['config'][transport]
336
+ next unless transport_config.is_a?(Hash)
337
+ transport_config = Bolt::Util.postwalk_vals(transport_config) do |val|
338
+ if val.is_a?(Hash)
339
+ val = val.compact
340
+ val = nil if val.empty?
341
+ end
342
+ val
343
+ end
344
+ # the transport config is user-specified data so we
345
+ # still want to preserve it even if it exclusively
346
+ # contains nil-resolved keys
347
+ result['config'][transport] = transport_config || {}
348
+ end
349
+
327
350
  result['features'] = Set.new(result['features'].flatten)
328
351
  result
329
352
  end
@@ -348,7 +371,7 @@ module Bolt
348
371
  msg = +"Found unexpected key(s) #{unexpected_keys.join(', ')} in config for"
349
372
  msg << " target #{target} in" if target
350
373
  msg << " group #{@name}"
351
- @logger.warn(msg)
374
+ Bolt::Logger.warn("unknown_config_keys", msg)
352
375
  end
353
376
  end
354
377
  end
@@ -21,7 +21,7 @@ module Bolt
21
21
  @transport = transport
22
22
  @config = transports
23
23
  @plugins = plugins
24
- @groups = Group.new(@data.merge('name' => 'all'), plugins)
24
+ @groups = Group.new(@data, plugins, all_group: true)
25
25
  @group_lookup = {}
26
26
  @targets = {}
27
27
 
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,16 @@ 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?
76
92
  end
77
93
 
78
94
  # A helper to ensure the Logging library is always initialized with our
@@ -123,18 +139,106 @@ module Bolt
123
139
  Logging.reset
124
140
  end
125
141
 
126
- def self.warn_once(type, msg)
142
+ # The following methods are used in place of the Logging.logger
143
+ # methods of the same name when logging warning messages or logging
144
+ # any messages prior to the logger being configured. If the logger
145
+ # is not configured when any of these methods are called, the message
146
+ # will be added to a queue, otherwise they are logged immediately.
147
+ # The message queue is flushed by calling #flush_queue, which is
148
+ # called from Bolt::CLI after configuring the logger.
149
+ #
150
+ def self.warn(id, msg)
151
+ log(type: :warn, msg: "#{msg} [ID: #{id}]", id: id)
152
+ end
153
+
154
+ def self.warn_once(id, msg)
155
+ log(type: :warn_once, msg: "#{msg} [ID: #{id}]", id: id)
156
+ end
157
+
158
+ def self.deprecate(id, msg)
159
+ log(type: :deprecate, msg: "#{msg} [ID: #{id}]", id: id)
160
+ end
161
+
162
+ def self.deprecate_once(id, msg)
163
+ log(type: :deprecate_once, msg: "#{msg} [ID: #{id}]", id: id)
164
+ end
165
+
166
+ def self.debug(msg)
167
+ log(type: :debug, msg: msg)
168
+ end
169
+
170
+ def self.info(msg)
171
+ log(type: :info, msg: msg)
172
+ end
173
+
174
+ # Logs a message. If the logger has not been configured, this will queue
175
+ # the message to be logged later. Once the logger is configured, the
176
+ # queue will be flushed of all messages and new messages will be logged
177
+ # immediately.
178
+ #
179
+ # Logging with this method is controlled by a mutex, as the Bolt::Logger
180
+ # module is treated as a global singleton to allow multiple classes
181
+ # access to its methods.
182
+ #
183
+ private_class_method def self.log(type:, msg:, id: nil)
184
+ @mutex.synchronize do
185
+ if configured?
186
+ log_message(type: type, msg: msg, id: id)
187
+ else
188
+ @message_queue << { type: type, msg: msg, id: id }
189
+ end
190
+ end
191
+ end
192
+
193
+ # Logs all messages in the message queue and then flushes the queue.
194
+ #
195
+ def self.flush_queue
127
196
  @mutex.synchronize do
128
- @logger ||= Bolt::Logger.logger(self)
129
- if @warnings.add?(type)
130
- @logger.warn(msg)
197
+ @message_queue.each do |message|
198
+ log_message(message)
131
199
  end
200
+
201
+ @message_queue.clear
202
+ end
203
+ end
204
+
205
+ # Handles the actual logging of a message.
206
+ #
207
+ private_class_method def self.log_message(type:, msg:, id: nil)
208
+ case type
209
+ when :warn
210
+ do_warn(msg, id)
211
+ when :warn_once
212
+ do_warn_once(msg, id)
213
+ when :deprecate
214
+ do_deprecate(msg, id)
215
+ when :deprecate_once
216
+ do_deprecate_once(msg, id)
217
+ else
218
+ logger(self).send(type, msg)
132
219
  end
133
220
  end
134
221
 
135
- def self.deprecation_warning(type, msg)
136
- @analytics&.event('Warn', 'deprecation', label: type)
137
- warn_once(type, msg)
222
+ # The following methods do the actual warning.
223
+ #
224
+ private_class_method def self.do_warn(msg, id)
225
+ return if @disable_warnings.include?(id)
226
+ logger(self).warn(msg)
227
+ end
228
+
229
+ private_class_method def self.do_warn_once(msg, id)
230
+ return unless @warnings.add?(id)
231
+ do_warn(msg, id)
232
+ end
233
+
234
+ private_class_method def self.do_deprecate(msg, id)
235
+ @analytics&.event('Warn', 'deprecation', label: id)
236
+ do_warn(msg, id)
237
+ end
238
+
239
+ private_class_method def self.do_deprecate_once(msg, id)
240
+ @analytics&.event('Warn', 'deprecation', label: id)
241
+ do_warn_once(msg, id)
138
242
  end
139
243
  end
140
244
  end
data/lib/bolt/module.rb CHANGED
@@ -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
@@ -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
 
@@ -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, _config = {})
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')