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
@@ -103,7 +103,9 @@ module Bolt
103
103
  # early here and not initialize the project if the modules cannot be
104
104
  # resolved and installed.
105
105
  if modules
106
+ @outputter.start_spin
106
107
  Bolt::ModuleInstaller.new(@outputter, @pal).install(modules, puppetfile, moduledir)
108
+ @outputter.stop_spin
107
109
  end
108
110
 
109
111
  data = { 'name' => project_name }
@@ -72,7 +72,15 @@ module Bolt
72
72
  data = Bolt::Util.read_yaml_hash(project_file, 'config')
73
73
  modified = false
74
74
 
75
- [%w[apply_settings apply-settings], %w[plugin_hooks plugin-hooks]].each do |old, new|
75
+ # Keys to update. The first element is the old key, while the second is
76
+ # the key update it to.
77
+ to_update = [
78
+ %w[apply_settings apply-settings],
79
+ %w[puppetfile module-install],
80
+ %w[plugin_hooks plugin-hooks]
81
+ ]
82
+
83
+ to_update.each do |old, new|
76
84
  next unless data.key?(old)
77
85
 
78
86
  if data.key?(new)
@@ -61,6 +61,7 @@ module Bolt
61
61
  # Create specs to resolve from
62
62
  specs = Bolt::ModuleInstaller::Specs.new(modules.map(&:to_hash))
63
63
 
64
+ @outputter.start_spin
64
65
  # Attempt to resolve dependencies
65
66
  begin
66
67
  @outputter.print_message('')
@@ -72,6 +73,7 @@ module Bolt
72
73
  end
73
74
 
74
75
  migrate_managed_modules(puppetfile, puppetfile_path, managed_moduledir)
76
+ @outputter.stop_spin
75
77
 
76
78
  # Move remaining modules to 'modules'
77
79
  consolidate_modules(modulepath)
@@ -18,6 +18,7 @@ module Bolt
18
18
  def query_certnames(query)
19
19
  return [] unless query
20
20
 
21
+ @logger.debug("Querying certnames")
21
22
  results = make_query(query)
22
23
 
23
24
  if results&.first && !results.first&.key?('certname')
@@ -34,6 +35,8 @@ module Bolt
34
35
  certnames.uniq!
35
36
  name_query = certnames.map { |c| ["=", "certname", c] }
36
37
  name_query.insert(0, "or")
38
+
39
+ @logger.debug("Querying certnames")
37
40
  result = make_query(name_query, 'inventory')
38
41
 
39
42
  result&.each_with_object({}) do |node, coll|
@@ -52,6 +55,8 @@ module Bolt
52
55
  facts_query.insert(0, "or")
53
56
 
54
57
  query = ['and', name_query, facts_query]
58
+
59
+ @logger.debug("Querying certnames")
55
60
  result = make_query(query, 'fact-contents')
56
61
  result.map! { |h| h.delete_if { |k, _v| %w[environment name].include?(k) } }
57
62
  result.group_by { |c| c['certname'] }
@@ -63,11 +68,13 @@ module Bolt
63
68
  url += "/#{path}" if path
64
69
 
65
70
  begin
71
+ @logger.debug("Sending PuppetDB query to #{url}")
66
72
  response = http_client.post(url, body: body, header: headers)
67
73
  rescue StandardError => e
68
74
  raise Bolt::PuppetDBFailoverError, "Failed to query PuppetDB: #{e}"
69
75
  end
70
76
 
77
+ @logger.debug("Got response code #{response.code} from PuppetDB")
71
78
  if response.code != 200
72
79
  msg = "Failed to query PuppetDB: #{response.body}"
73
80
  if response.code == 400
@@ -92,6 +99,7 @@ module Bolt
92
99
  return @http if @http
93
100
  # lazy-load expensive gem code
94
101
  require 'httpclient'
102
+ @logger.trace("Creating HTTP Client")
95
103
  @http = HTTPClient.new
96
104
  @http.ssl_config.set_client_cert_file(@config.cert, @config.key) if @config.cert
97
105
  @http.ssl_config.add_trust_ca(@config.cacert)
@@ -49,7 +49,7 @@ module Bolt
49
49
  end
50
50
  end
51
51
  rescue StandardError => e
52
- Bolt::Logger.warn_once('unwriteable_file', "Failed to save result to #{@path}: #{e.message}")
52
+ Bolt::Logger.warn_once("unwriteable_file", "Failed to save result to #{@path}: #{e.message}")
53
53
  end
54
54
  end
55
55
  end
@@ -299,7 +299,7 @@ module Bolt
299
299
  if target.options['cleanup']
300
300
  dir.delete
301
301
  else
302
- @logger.warn("Skipping cleanup of tmpdir #{dir}")
302
+ Bolt::Logger.warn("skip_cleanup", "Skipping cleanup of tmpdir #{dir}")
303
303
  end
304
304
  end
305
305
  end
@@ -48,7 +48,10 @@ module Bolt
48
48
  def delete
49
49
  result = @shell.execute(['rm', '-rf', @path], sudoable: true, run_as: @owner)
50
50
  if result.exit_code != 0
51
- @logger.warn("Failed to clean up tmpdir '#{@path}': #{result.stderr.string}")
51
+ Bolt::Logger.warn(
52
+ "fail_cleanup",
53
+ "Failed to clean up tmpdir '#{@path}': #{result.stderr.string}"
54
+ )
52
55
  end
53
56
  # For testing
54
57
  result.stderr.string
@@ -27,7 +27,7 @@ module Bolt
27
27
  "bolt-debug.log or run with '--log-level debug' to see the full "\
28
28
  "list of targets with PowerShell 2."
29
29
 
30
- Bolt::Logger.deprecation_warning("PowerShell 2", msg)
30
+ Bolt::Logger.deprecate_once("powershell_2", msg)
31
31
  @logger.debug("Detected PowerShell 2 on #{target}.")
32
32
  end
33
33
  end
@@ -163,7 +163,7 @@ module Bolt
163
163
  if target.options['cleanup']
164
164
  rmdir(@tmpdir)
165
165
  else
166
- @logger.warn("Skipping cleanup of tmpdir '#{@tmpdir}'")
166
+ Bolt::Logger.warn("Skipping cleanup of tmpdir '#{@tmpdir}'", "skip_cleanup")
167
167
  end
168
168
  end
169
169
  end
@@ -193,7 +193,8 @@ module Bolt
193
193
  def run_command(command, options = {}, position = [])
194
194
  command = [*env_declarations(options[:env_vars]), command].join("\r\n") if options[:env_vars]
195
195
 
196
- output = execute(command)
196
+ wrap_command = conn.is_a?(Bolt::Transport::Local::Connection)
197
+ output = execute(command, wrap_command)
197
198
  Bolt::Result.for_command(target,
198
199
  output.stdout.string,
199
200
  output.stderr.string,
@@ -284,8 +285,9 @@ module Bolt
284
285
  end
285
286
  end
286
287
 
287
- def execute(command)
288
- if conn.max_command_length && command.length > conn.max_command_length
288
+ def execute(command, wrap_command = false)
289
+ if (conn.max_command_length && command.length > conn.max_command_length) ||
290
+ wrap_command
289
291
  return with_tmpdir do |dir|
290
292
  command += "\r\nif (!$?) { if($LASTEXITCODE) { exit $LASTEXITCODE } else { exit 1 } }"
291
293
  script_file = File.join(dir, "#{SecureRandom.uuid}_wrapper.ps1")
@@ -80,6 +80,10 @@ module Bolt
80
80
  inventory_target.resources
81
81
  end
82
82
 
83
+ def set_local_defaults
84
+ inventory_target.set_local_defaults
85
+ end
86
+
83
87
  # rubocop:disable Naming/AccessorMethodName
84
88
  def set_resource(resource)
85
89
  inventory_target.set_resource(resource)
@@ -149,7 +149,7 @@ module Bolt
149
149
  if unknown_keys.any?
150
150
  msg = "Metadata for task '#{@name}' contains unknown keys: #{unknown_keys.join(', ')}."
151
151
  msg += " This could be a typo in the task metadata or may result in incorrect behavior."
152
- @logger.warn(msg)
152
+ Bolt::Logger.warn("unknown_task_metadata_keys", msg)
153
153
  end
154
154
  end
155
155
  end
@@ -140,10 +140,10 @@ module Bolt
140
140
  if @target.options['cleanup']
141
141
  _, stderr, exitcode = execute('rm', '-rf', dir, {})
142
142
  if exitcode != 0
143
- @logger.warn("Failed to clean up tmpdir '#{dir}': #{stderr}")
143
+ Bolt::Logger.warn("fail_cleanup", "Failed to clean up tmpdir '#{dir}': #{stderr}")
144
144
  end
145
145
  else
146
- @logger.warn("Skipping cleanup of tmpdir '#{dir}'")
146
+ Bolt::Logger.warn("skip_cleanup", "Skipping cleanup of tmpdir '#{dir}'")
147
147
  end
148
148
  end
149
149
  end
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'bolt/logger'
3
4
  require 'bolt/transport/simple'
4
5
 
5
6
  module Bolt
@@ -10,6 +11,18 @@ module Bolt
10
11
  end
11
12
 
12
13
  def with_connection(target)
14
+ if target.transport_config['bundled-ruby'] || target.name == 'localhost'
15
+ target.set_local_defaults
16
+ end
17
+
18
+ if target.name != 'localhost' &&
19
+ !target.transport_config.key?('bundled-ruby')
20
+ msg = "The local transport will default to using Bolt's Ruby interpreter and "\
21
+ "setting the 'puppet-agent' feature in Bolt 3.0. Enable or disable these "\
22
+ "defaults by setting 'bundled-ruby' in the local transport config."
23
+ Bolt::Logger.warn_once("local_default_config", msg)
24
+ end
25
+
13
26
  yield Connection.new(target)
14
27
  end
15
28
  end
@@ -21,7 +21,7 @@ module Bolt
21
21
 
22
22
  @logger = logger
23
23
  @key = self.class.get_key(opts)
24
- client_opts = opts.slice('token-file', 'cacert', 'job-poll-interval', 'job-poll-timeout')
24
+ client_opts = opts.slice('token-file', 'cacert', 'job-poll-interval', 'job-poll-timeout', 'read-timeout')
25
25
 
26
26
  if opts['service-url']
27
27
  uri = Addressable::URI.parse(opts['service-url'])
@@ -23,8 +23,7 @@ module Bolt
23
23
 
24
24
  def with_connection(target)
25
25
  if target.transport_config['ssh-command'] && !target.transport_config['native-ssh']
26
- Bolt::Logger.warn_once("ssh-command and native-ssh conflict",
27
- "native-ssh must be true to use ssh-command")
26
+ Bolt::Logger.warn_once("native_ssh_disabled", "native-ssh must be true to use ssh-command")
28
27
  end
29
28
 
30
29
  conn = if target.transport_config['native-ssh']
@@ -34,7 +34,7 @@ module Bolt
34
34
  begin
35
35
  Bolt::Util.validate_file('ssh key', target.options['private-key'])
36
36
  rescue Bolt::FileError => e
37
- @logger.warn(e.msg)
37
+ Bolt::Logger.warn("invalid_ssh_key", e.msg)
38
38
  end
39
39
  end
40
40
  end
@@ -0,0 +1,227 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bolt/error'
4
+
5
+ # This class validates config against a schema, raising an error that includes
6
+ # details about any invalid configuration.
7
+ #
8
+ module Bolt
9
+ class Validator
10
+ attr_reader :deprecations, :warnings
11
+
12
+ def initialize
13
+ @errors = []
14
+ @deprecations = []
15
+ @warnings = []
16
+ @path = []
17
+ end
18
+
19
+ # This is the entry method for validating data against the schema.
20
+ #
21
+ def validate(data, schema, location = nil)
22
+ @schema = schema
23
+ @location = location
24
+
25
+ validate_value(data, schema)
26
+
27
+ raise_error
28
+ end
29
+
30
+ # Raises a ValidationError if there are any errors. All error messages
31
+ # created during validation are concatenated into a single error
32
+ # message.
33
+ #
34
+ private def raise_error
35
+ return unless @errors.any?
36
+
37
+ message = "Invalid configuration"
38
+ message += " at #{@location}" if @location
39
+ message += ":\n"
40
+ message += @errors.map { |error| "\s\s#{error}" }.join("\n")
41
+
42
+ raise Bolt::ValidationError, message
43
+ end
44
+
45
+ # Validate an individual value. This performs validation that is
46
+ # common to all values, including type validation. After validating
47
+ # the value's type, the value is passed off to an individual
48
+ # validation method for the value's type.
49
+ #
50
+ private def validate_value(value, definition, plugin_supported = false)
51
+ definition = @schema.dig(:definitions, definition[:_ref]) if definition[:_ref]
52
+ plugin_supported = definition[:_plugin] if definition.key?(:_plugin)
53
+
54
+ return if plugin_reference?(value, plugin_supported)
55
+ return unless valid_type?(value, definition)
56
+
57
+ case value
58
+ when Hash
59
+ validate_hash(value, definition, plugin_supported)
60
+ when Array
61
+ validate_array(value, definition, plugin_supported)
62
+ when String
63
+ validate_string(value, definition)
64
+ when Numeric
65
+ validate_number(value, definition)
66
+ end
67
+ end
68
+
69
+ # Validates a hash value, logging errors for any validations that fail.
70
+ # This will enumerate each key-value pair in the hash and validate each
71
+ # value individually.
72
+ #
73
+ private def validate_hash(value, definition, plugin_supported)
74
+ properties = definition[:properties] ? definition[:properties].keys : []
75
+
76
+ if definition[:properties] && definition[:additionalProperties].nil?
77
+ validate_keys(value.keys, properties)
78
+ end
79
+
80
+ if definition[:required] && (definition[:required] - value.keys).any?
81
+ missing = definition[:required] - value.keys
82
+ @errors << "Value at '#{path}' is missing required keys #{missing.join(', ')}"
83
+ end
84
+
85
+ value.each_pair do |key, val|
86
+ @path.push(key)
87
+
88
+ if properties.include?(key)
89
+ check_deprecated(key, definition[:properties][key])
90
+ validate_value(val, definition[:properties][key], plugin_supported)
91
+ elsif definition[:additionalProperties].is_a?(Hash)
92
+ validate_value(val, definition[:additionalProperties], plugin_supported)
93
+ end
94
+ ensure
95
+ @path.pop
96
+ end
97
+ end
98
+
99
+ # Validates an array value, logging errors for any validations that fail.
100
+ # This will enumerate the items in the array and validate each item
101
+ # individually.
102
+ #
103
+ private def validate_array(value, definition, plugin_supported)
104
+ if definition[:uniqueItems] && value.size != value.uniq.size
105
+ @errors << "Value at '#{path}' must not include duplicate elements"
106
+ return
107
+ end
108
+
109
+ return unless definition.key?(:items)
110
+
111
+ value.each_with_index do |item, index|
112
+ @path.push(index)
113
+ validate_value(item, definition[:items], plugin_supported)
114
+ ensure
115
+ @path.pop
116
+ end
117
+ end
118
+
119
+ # Validates a string value, logging errors for any validations that fail.
120
+ #
121
+ private def validate_string(value, definition)
122
+ if definition.key?(:enum) && !definition[:enum].include?(value)
123
+ message = "Value at '#{path}' must be "
124
+ message += "one of " if definition[:enum].count > 1
125
+ message += definition[:enum].join(', ')
126
+ multitype_error(message, value, definition)
127
+ end
128
+ end
129
+
130
+ # Validates a numeric value, logging errors for any validations that fail.
131
+ #
132
+ private def validate_number(value, definition)
133
+ if definition.key?(:minimum) && value < definition[:minimum]
134
+ @errors << "Value at '#{path}' must be a minimum of #{definition[:minimum]}"
135
+ end
136
+
137
+ if definition.key?(:maximum) && value > definition[:maximum]
138
+ @errors << "Value at '#{path}' must be a maximum of #{definition[:maximum]}"
139
+ end
140
+ end
141
+
142
+ # Adds warnings for unknown config options.
143
+ #
144
+ private def validate_keys(keys, known_keys)
145
+ (keys - known_keys).each do |key|
146
+ message = "Unknown option '#{key}'"
147
+ message += " at '#{path}'" if @path.any?
148
+ message += " at #{@location}" if @location
149
+ message += "."
150
+ @warnings << { id: 'unknown_option', msg: message }
151
+ end
152
+ end
153
+
154
+ # Adds a warning if the given option is deprecated.
155
+ #
156
+ private def check_deprecated(key, definition)
157
+ definition = @schema.dig(:definitions, definition[:_ref]) if definition[:_ref]
158
+
159
+ if definition.key?(:_deprecation)
160
+ message = "Option '#{path}' "
161
+ message += "at #{@location} " if @location
162
+ message += "is deprecated. #{definition[:_deprecation]}"
163
+ @deprecations << { id: "#{key}_option", msg: message }
164
+ end
165
+ end
166
+
167
+ # Returns true if a value is a plugin reference. This also validates whether
168
+ # a value can be a plugin reference in the first place. If the value is a
169
+ # plugin reference but cannot be one according to the schema, then this will
170
+ # log an error.
171
+ #
172
+ private def plugin_reference?(value, plugin_supported)
173
+ if value.is_a?(Hash) && value.key?('_plugin')
174
+ unless plugin_supported
175
+ @errors << "Value at '#{path}' is a plugin reference, which is unsupported at "\
176
+ "this location"
177
+ end
178
+
179
+ true
180
+ else
181
+ false
182
+ end
183
+ end
184
+
185
+ # Asserts the type for each option against the type specified in the schema
186
+ # definition. The schema definition can specify multiple valid types, so the
187
+ # value needs to only match one of the types to be valid. Returns early if
188
+ # there is no type in the definition (in practice this shouldn't happen, but
189
+ # this will safeguard against any dev mistakes).
190
+ #
191
+ private def valid_type?(value, definition)
192
+ return unless definition.key?(:type)
193
+
194
+ types = Array(definition[:type])
195
+
196
+ if types.include?(value.class)
197
+ true
198
+ else
199
+ if types.include?(TrueClass) || types.include?(FalseClass)
200
+ types = types - [TrueClass, FalseClass] + ['Boolean']
201
+ end
202
+
203
+ @errors << "Value at '#{path}' must be of type #{types.join(' or ')}"
204
+
205
+ false
206
+ end
207
+ end
208
+
209
+ # Adds an error that includes additional helpful information for values
210
+ # that accept multiple types.
211
+ #
212
+ private def multitype_error(message, value, definition)
213
+ if Array(definition[:type]).count > 1
214
+ types = Array(definition[:type]) - [value.class]
215
+ message += " or must be of type #{types.join(' or ')}"
216
+ end
217
+
218
+ @errors << message
219
+ end
220
+
221
+ # Returns the formatted path for the key.
222
+ #
223
+ private def path
224
+ @path.join('.')
225
+ end
226
+ end
227
+ end