bolt 2.40.1 → 3.0.1

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/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 +58 -157
  11. data/lib/bolt/config.rb +62 -239
  12. data/lib/bolt/config/options.rb +58 -97
  13. data/lib/bolt/config/transport/local.rb +1 -0
  14. data/lib/bolt/config/transport/options.rb +8 -1
  15. data/lib/bolt/config/transport/orch.rb +1 -0
  16. data/lib/bolt/executor.rb +15 -5
  17. data/lib/bolt/inventory.rb +3 -2
  18. data/lib/bolt/inventory/group.rb +35 -4
  19. data/lib/bolt/inventory/inventory.rb +1 -1
  20. data/lib/bolt/logger.rb +115 -11
  21. data/lib/bolt/module.rb +10 -2
  22. data/lib/bolt/module_installer.rb +4 -2
  23. data/lib/bolt/module_installer/resolver.rb +65 -12
  24. data/lib/bolt/module_installer/specs/forge_spec.rb +8 -2
  25. data/lib/bolt/module_installer/specs/git_spec.rb +17 -2
  26. data/lib/bolt/outputter/human.rb +9 -5
  27. data/lib/bolt/outputter/json.rb +16 -16
  28. data/lib/bolt/outputter/rainbow.rb +3 -3
  29. data/lib/bolt/pal.rb +93 -14
  30. data/lib/bolt/pal/yaml_plan.rb +8 -2
  31. data/lib/bolt/pal/yaml_plan/evaluator.rb +7 -19
  32. data/lib/bolt/pal/yaml_plan/step.rb +3 -24
  33. data/lib/bolt/pal/yaml_plan/step/upload.rb +2 -2
  34. data/lib/bolt/pal/yaml_plan/transpiler.rb +6 -1
  35. data/lib/bolt/plugin.rb +3 -3
  36. data/lib/bolt/plugin/cache.rb +7 -7
  37. data/lib/bolt/plugin/module.rb +0 -23
  38. data/lib/bolt/plugin/puppet_connect_data.rb +77 -0
  39. data/lib/bolt/plugin/puppetdb.rb +1 -1
  40. data/lib/bolt/project.rb +54 -81
  41. data/lib/bolt/project_manager.rb +4 -3
  42. data/lib/bolt/project_manager/module_migrator.rb +6 -5
  43. data/lib/bolt/rerun.rb +1 -1
  44. data/lib/bolt/shell/bash.rb +1 -1
  45. data/lib/bolt/shell/bash/tmpdir.rb +4 -1
  46. data/lib/bolt/shell/powershell.rb +3 -4
  47. data/lib/bolt/shell/powershell/snippets.rb +9 -149
  48. data/lib/bolt/task.rb +1 -1
  49. data/lib/bolt/transport/docker/connection.rb +2 -2
  50. data/lib/bolt/transport/local.rb +1 -9
  51. data/lib/bolt/transport/orch/connection.rb +1 -1
  52. data/lib/bolt/transport/ssh.rb +1 -2
  53. data/lib/bolt/transport/ssh/connection.rb +1 -1
  54. data/lib/bolt/validator.rb +2 -2
  55. data/lib/bolt/version.rb +1 -1
  56. data/lib/bolt_server/config.rb +1 -1
  57. data/lib/bolt_server/schemas/partials/task.json +1 -1
  58. data/lib/bolt_server/transport_app.rb +3 -2
  59. data/libexec/bolt_catalog +1 -1
  60. data/modules/aggregate/plans/count.pp +21 -0
  61. data/modules/aggregate/plans/targets.pp +21 -0
  62. data/modules/puppet_connect/plans/test_input_data.pp +31 -0
  63. data/modules/puppetdb_fact/plans/init.pp +10 -0
  64. metadata +27 -18
  65. data/modules/aggregate/plans/nodes.pp +0 -36
@@ -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')
@@ -32,8 +32,8 @@ module Bolt
32
32
  end
33
33
 
34
34
  def start_spin
35
- return unless @spin
36
- @spin = true
35
+ return unless @spin && @stream.isatty && !@spinning
36
+ @spinning = true
37
37
  @spin_thread = Thread.new do
38
38
  loop do
39
39
  sleep(0.1)
@@ -43,9 +43,9 @@ module Bolt
43
43
  end
44
44
 
45
45
  def stop_spin
46
- return unless @spin
46
+ return unless @spin && @stream.isatty && @spinning
47
+ @spinning = false
47
48
  @spin_thread.terminate
48
- @spin = false
49
49
  @stream.print("\b")
50
50
  end
51
51
 
@@ -81,6 +81,10 @@ module Bolt
81
81
  print_plan_start(event)
82
82
  when :plan_finish
83
83
  print_plan_finish(event)
84
+ when :start_spin
85
+ start_spin
86
+ when :stop_spin
87
+ stop_spin
84
88
  end
85
89
  end
86
90
  end
@@ -388,7 +392,7 @@ module Bolt
388
392
 
389
393
  def print_target_info(targets)
390
394
  @stream.puts ::JSON.pretty_generate(
391
- "targets": targets.map(&:detail)
395
+ targets: targets.map(&:detail)
392
396
  )
393
397
  count = "#{targets.count} target#{'s' unless targets.count == 1}"
394
398
  @stream.puts colorize(:green, count)
@@ -95,38 +95,38 @@ module Bolt
95
95
  end
96
96
 
97
97
  def print_puppetfile_result(success, puppetfile, moduledir)
98
- @stream.puts({ "success": success,
99
- "puppetfile": puppetfile,
100
- "moduledir": moduledir.to_s }.to_json)
98
+ @stream.puts({ success: success,
99
+ puppetfile: puppetfile,
100
+ moduledir: moduledir.to_s }.to_json)
101
101
  end
102
102
 
103
103
  def print_targets(target_list, inventoryfile)
104
104
  @stream.puts ::JSON.pretty_generate(
105
- "inventory": {
106
- "targets": target_list[:inventory].map(&:name),
107
- "count": target_list[:inventory].count,
108
- "file": inventoryfile.to_s
105
+ inventory: {
106
+ targets: target_list[:inventory].map(&:name),
107
+ count: target_list[:inventory].count,
108
+ file: inventoryfile.to_s
109
109
  },
110
- "adhoc": {
111
- "targets": target_list[:adhoc].map(&:name),
112
- "count": target_list[:adhoc].count
110
+ adhoc: {
111
+ targets: target_list[:adhoc].map(&:name),
112
+ count: target_list[:adhoc].count
113
113
  },
114
- "targets": target_list.values.flatten.map(&:name),
115
- "count": target_list.values.flatten.count
114
+ targets: target_list.values.flatten.map(&:name),
115
+ count: target_list.values.flatten.count
116
116
  )
117
117
  end
118
118
 
119
119
  def print_target_info(targets)
120
120
  @stream.puts ::JSON.pretty_generate(
121
- "targets": targets.map(&:detail),
122
- "count": targets.count
121
+ targets: targets.map(&:detail),
122
+ count: targets.count
123
123
  )
124
124
  end
125
125
 
126
126
  def print_groups(groups)
127
127
  count = groups.count
128
- @stream.puts({ "groups": groups,
129
- "count": count }.to_json)
128
+ @stream.puts({ groups: groups,
129
+ count: count }.to_json)
130
130
  end
131
131
 
132
132
  def fatal_error(err)