bolt 2.35.0 → 2.36.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 (36) hide show
  1. checksums.yaml +4 -4
  2. data/bolt-modules/boltlib/lib/puppet/datatypes/applyresult.rb +1 -0
  3. data/lib/bolt/analytics.rb +27 -8
  4. data/lib/bolt/apply_result.rb +3 -3
  5. data/lib/bolt/bolt_option_parser.rb +38 -15
  6. data/lib/bolt/cli.rb +13 -87
  7. data/lib/bolt/config.rb +131 -52
  8. data/lib/bolt/config/options.rb +42 -4
  9. data/lib/bolt/config/transport/base.rb +10 -19
  10. data/lib/bolt/config/transport/local.rb +0 -7
  11. data/lib/bolt/config/transport/ssh.rb +8 -14
  12. data/lib/bolt/config/validator.rb +231 -0
  13. data/lib/bolt/executor.rb +5 -17
  14. data/lib/bolt/outputter/rainbow.rb +1 -1
  15. data/lib/bolt/plugin.rb +0 -7
  16. data/lib/bolt/project.rb +30 -36
  17. data/lib/bolt/project_manager.rb +199 -0
  18. data/lib/bolt/{project_migrator/config.rb → project_manager/config_migrator.rb} +41 -4
  19. data/lib/bolt/{project_migrator/inventory.rb → project_manager/inventory_migrator.rb} +3 -3
  20. data/lib/bolt/{project_migrator/base.rb → project_manager/migrator.rb} +2 -2
  21. data/lib/bolt/{project_migrator/modules.rb → project_manager/module_migrator.rb} +3 -3
  22. data/lib/bolt/puppetdb/config.rb +1 -2
  23. data/lib/bolt/shell/bash.rb +1 -1
  24. data/lib/bolt/task/run.rb +1 -1
  25. data/lib/bolt/transport/ssh/exec_connection.rb +6 -2
  26. data/lib/bolt/util.rb +14 -7
  27. data/lib/bolt/version.rb +1 -1
  28. data/lib/bolt_server/base_config.rb +3 -1
  29. data/lib/bolt_server/config.rb +3 -1
  30. data/lib/bolt_server/schemas/partials/task.json +2 -2
  31. data/lib/bolt_server/transport_app.rb +5 -5
  32. data/libexec/apply_catalog.rb +1 -1
  33. data/libexec/custom_facts.rb +1 -1
  34. data/libexec/query_resources.rb +1 -1
  35. metadata +8 -13
  36. data/lib/bolt/project_migrator.rb +0 -80
@@ -104,6 +104,21 @@ module Bolt
104
104
  # files and is not used by Bolt to actually set default values.
105
105
  OPTIONS = {
106
106
  "apply_settings" => {
107
+ description: "A map of Puppet settings to use when applying Puppet code using the `apply` "\
108
+ "plan function or the `bolt apply` command.",
109
+ type: Hash,
110
+ properties: {
111
+ "show_diff" => {
112
+ description: "Whether to log and report a contextual diff.",
113
+ type: [TrueClass, FalseClass],
114
+ _example: true,
115
+ _default: false
116
+ }
117
+ },
118
+ _plugin: false,
119
+ _deprecation: "This option will be removed in Bolt 3.0. Use `apply-settings` instead."
120
+ },
121
+ "apply-settings" => {
107
122
  description: "A map of Puppet settings to use when applying Puppet code using the `apply` "\
108
123
  "plan function or the `bolt apply` command.",
109
124
  type: Hash,
@@ -169,6 +184,9 @@ module Bolt
169
184
  "files](inventory_file_v2.md).",
170
185
  type: String,
171
186
  _plugin: false,
187
+ _deprecation: "This option will be removed in Bolt 3.0. Use the `--inventoryfile` command-line option "\
188
+ "to use a non-default inventory file or move the file contents to `inventory.yaml` in the "\
189
+ "project directory.",
172
190
  _example: "~/.puppetlabs/bolt/inventory.yaml",
173
191
  _default: "project/inventory.yaml"
174
192
  },
@@ -183,7 +201,8 @@ module Bolt
183
201
  properties: {
184
202
  "console" => {
185
203
  description: "Configuration for logs output to the console.",
186
- type: Hash,
204
+ type: [String, Hash],
205
+ enum: ['disable'],
187
206
  properties: {
188
207
  "level" => {
189
208
  description: "The type of information to log.",
@@ -276,8 +295,8 @@ module Bolt
276
295
  },
277
296
  "name" => {
278
297
  description: "The name of the Bolt project. When this option is configured, the project is considered a "\
279
- "[Bolt project](experimental_features.md#bolt-projects), allowing Bolt to load content from "\
280
- "the project directory as though it were a module.",
298
+ "[Bolt project](projects.md), allowing Bolt to load content from the project directory "\
299
+ "as though it were a module.",
281
300
  type: String,
282
301
  _plugin: false,
283
302
  _example: "myproject"
@@ -294,6 +313,16 @@ module Bolt
294
313
  _example: ["myproject", "myproject::foo", "myproject::bar", "myproject::deploy::*"]
295
314
  },
296
315
  "plugin_hooks" => {
316
+ description: "A map of [plugin hooks](writing_plugins.md#hooks) and which plugins a hook should use. "\
317
+ "The only configurable plugin hook is `puppet_library`, which can use two possible plugins: "\
318
+ "[`puppet_agent`](https://github.com/puppetlabs/puppetlabs-puppet_agent#puppet_agentinstall) "\
319
+ "and [`task`](using_plugins.md#task).",
320
+ type: Hash,
321
+ _plugin: true,
322
+ _example: { "puppet_library" => { "plugin" => "puppet_agent", "version" => "6.15.0", "_run_as" => "root" } },
323
+ _deprecation: "This option will be removed in Bolt 3.0. Use `plugin-hooks` instead."
324
+ },
325
+ "plugin-hooks" => {
297
326
  description: "A map of [plugin hooks](writing_plugins.md#hooks) and which plugins a hook should use. "\
298
327
  "The only configurable plugin hook is `puppet_library`, which can use two possible plugins: "\
299
328
  "[`puppet_agent`](https://github.com/puppetlabs/puppetlabs-puppet_agent#puppet_agentinstall) "\
@@ -307,7 +336,11 @@ module Bolt
307
336
  "its value is a map of configuration data. Configurable options are specified by the plugin. "\
308
337
  "Read more about configuring plugins in [Using plugins](using_plugins.md#configuring-plugins).",
309
338
  type: Hash,
310
- _plugin: true,
339
+ additionalProperties: {
340
+ type: Hash,
341
+ _plugin: true
342
+ },
343
+ _plugin: false,
311
344
  _example: { "pkcs7" => { "keysize" => 1024 } }
312
345
  },
313
346
  "puppetdb" => {
@@ -480,6 +513,7 @@ module Bolt
480
513
 
481
514
  # Options that are available in a bolt.yaml file
482
515
  BOLT_OPTIONS = %w[
516
+ apply-settings
483
517
  apply_settings
484
518
  color
485
519
  compile-concurrency
@@ -489,6 +523,7 @@ module Bolt
489
523
  inventoryfile
490
524
  log
491
525
  modulepath
526
+ plugin-hooks
492
527
  plugin_hooks
493
528
  plugins
494
529
  puppetdb
@@ -505,6 +540,7 @@ module Bolt
505
540
  format
506
541
  inventory-config
507
542
  log
543
+ plugin-hooks
508
544
  plugin_hooks
509
545
  plugins
510
546
  puppetdb
@@ -514,6 +550,7 @@ module Bolt
514
550
 
515
551
  # Options that are available in a bolt-project.yaml file
516
552
  BOLT_PROJECT_OPTIONS = %w[
553
+ apply-settings
517
554
  apply_settings
518
555
  color
519
556
  compile-concurrency
@@ -526,6 +563,7 @@ module Bolt
526
563
  modules
527
564
  name
528
565
  plans
566
+ plugin-hooks
529
567
  plugin_hooks
530
568
  plugins
531
569
  puppetdb
@@ -2,6 +2,7 @@
2
2
 
3
3
  require 'bolt/error'
4
4
  require 'bolt/util'
5
+ require 'bolt/config/validator'
5
6
  require 'bolt/config/transport/options'
6
7
 
7
8
  module Bolt
@@ -90,6 +91,14 @@ module Bolt
90
91
  self::OPTIONS
91
92
  end
92
93
 
94
+ def self.schema
95
+ {
96
+ type: Hash,
97
+ properties: self::TRANSPORT_OPTIONS.slice(*self::OPTIONS),
98
+ _plugin: true
99
+ }
100
+ end
101
+
93
102
  private def defaults
94
103
  unless defined? self.class::DEFAULTS
95
104
  raise NotImplementedError,
@@ -116,25 +125,7 @@ module Bolt
116
125
 
117
126
  # Validation defaults to just asserting the option types
118
127
  private def validate
119
- assert_type
120
- end
121
-
122
- # Validates that each option is the correct type. Types are loaded from the TRANSPORT_OPTIONS hash.
123
- private def assert_type
124
- @config.each_pair do |opt, val|
125
- types = Array(TRANSPORT_OPTIONS.dig(opt, :type)).compact
126
-
127
- next if val.nil? || types.empty? || types.include?(val.class)
128
-
129
- # Ruby doesn't have a Boolean class, so add it to the types list if TrueClass or FalseClass
130
- # are present.
131
- if types.include?(TrueClass) || types.include?(FalseClass)
132
- types = types - [TrueClass, FalseClass] + ['Boolean']
133
- end
134
-
135
- raise Bolt::ValidationError,
136
- "#{opt} must be of type #{types.join(', ')}; received #{val.class} #{val.inspect} "
137
- end
128
+ Bolt::Config::Validator.new.validate(@config.compact, self.class.schema.fetch(:properties))
138
129
  end
139
130
  end
140
131
  end
@@ -29,13 +29,6 @@ module Bolt
29
29
  if @config['interpreters']
30
30
  @config['interpreters'] = normalize_interpreters(@config['interpreters'])
31
31
  end
32
-
33
- if (run_as_cmd = @config['run-as-command'])
34
- unless run_as_cmd.all? { |n| n.is_a?(String) }
35
- raise Bolt::ValidationError,
36
- "run-as-command must be an Array of Strings, received #{run_as_cmd.class} #{run_as_cmd.inspect}"
37
- end
38
- end
39
32
  end
40
33
  end
41
34
  end
@@ -73,6 +73,14 @@ module Bolt
73
73
  %w[ssh-command native-ssh].concat(OPTIONS)
74
74
  end
75
75
 
76
+ def self.schema
77
+ {
78
+ type: Hash,
79
+ properties: self::TRANSPORT_OPTIONS.slice(*(self::OPTIONS + self::NATIVE_OPTIONS)),
80
+ _plugin: true
81
+ }
82
+ end
83
+
76
84
  private def filter(unfiltered)
77
85
  # Because we filter before merging config together it's impossible to
78
86
  # know whether both ssh-command *and* native-ssh will be specified
@@ -87,12 +95,6 @@ module Bolt
87
95
  super
88
96
 
89
97
  if (key_opt = @config['private-key'])
90
- unless key_opt.instance_of?(String) || (key_opt.instance_of?(Hash) && key_opt.include?('key-data'))
91
- raise Bolt::ValidationError,
92
- "private-key option must be a path to a private key file or a Hash containing the 'key-data', "\
93
- "received #{key_opt.class} #{key_opt}"
94
- end
95
-
96
98
  if key_opt.instance_of?(String)
97
99
  @config['private-key'] = File.expand_path(key_opt, @project)
98
100
 
@@ -114,14 +116,6 @@ module Bolt
114
116
  "Unsupported login-shell #{@config['login-shell']}. Supported shells are #{LOGIN_SHELLS.join(', ')}"
115
117
  end
116
118
 
117
- %w[encryption-algorithms host-key-algorithms kex-algorithms mac-algorithms run-as-command].each do |opt|
118
- next unless @config.key?(opt)
119
- unless @config[opt].all? { |n| n.is_a?(String) }
120
- raise Bolt::ValidationError,
121
- "#{opt} must be an Array of Strings, received #{@config[opt].inspect}"
122
- end
123
- end
124
-
125
119
  if @config['login-shell'] == 'powershell'
126
120
  %w[tty run-as].each do |key|
127
121
  if @config[key]
@@ -0,0 +1,231 @@
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 Config
10
+ class Validator
11
+ attr_reader :deprecations, :warnings
12
+
13
+ def initialize
14
+ @errors = []
15
+ @deprecations = []
16
+ @warnings = []
17
+ @path = []
18
+ end
19
+
20
+ # This is the entry method for validating data against the schema.
21
+ # It loops over each key-value pair in the data hash and validates
22
+ # the value against the relevant schema definition.
23
+ #
24
+ def validate(data, schema, location = nil)
25
+ @location = location
26
+
27
+ validate_keys(data.keys, schema.keys)
28
+
29
+ data.each_pair do |key, value|
30
+ next unless schema.key?(key)
31
+
32
+ @path.push(key)
33
+
34
+ check_deprecated(key, schema[key], location)
35
+ validate_value(value, schema[key])
36
+ ensure
37
+ @path.pop
38
+ end
39
+
40
+ raise_error
41
+ end
42
+
43
+ # Adds a warning if the given option is deprecated.
44
+ #
45
+ def check_deprecated(key, definition, location)
46
+ if definition.key?(:_deprecation)
47
+ message = "Option '#{path}' "
48
+ message += "at #{location} " if location
49
+ message += "is deprecated. #{definition[:_deprecation]}"
50
+ @deprecations << { option: key, message: message }
51
+ end
52
+ end
53
+
54
+ # Raises a ValidationError if there are any errors. All error messages
55
+ # created during validation are concatenated into a single error
56
+ # message.
57
+ #
58
+ private def raise_error
59
+ return unless @errors.any?
60
+
61
+ message = "Invalid configuration"
62
+ message += " at #{@location}" if @location
63
+ message += ":\n"
64
+ message += @errors.map { |error| "\s\s#{error}" }.join("\n")
65
+
66
+ raise Bolt::ValidationError, message
67
+ end
68
+
69
+ # Validate an individual value. This performs validation that is
70
+ # common to all values, including type validation. After validating
71
+ # the value's type, the value is passed off to an individual
72
+ # validation method for the value's type.
73
+ #
74
+ private def validate_value(value, definition)
75
+ return if plugin_reference?(value, definition)
76
+ return unless valid_type?(value, definition)
77
+
78
+ case value
79
+ when Hash
80
+ validate_hash(value, definition)
81
+ when Array
82
+ validate_array(value, definition)
83
+ when String
84
+ validate_string(value, definition)
85
+ when Numeric
86
+ validate_number(value, definition)
87
+ end
88
+ end
89
+
90
+ # Validates a hash value, logging errors for any validations that fail.
91
+ # This will enumerate each key-value pair in the hash and validate each
92
+ # value individually.
93
+ #
94
+ private def validate_hash(value, definition)
95
+ properties = definition[:properties] ? definition[:properties].keys : []
96
+
97
+ if definition[:properties] && definition[:additionalProperties].nil?
98
+ validate_keys(value.keys, properties)
99
+ end
100
+
101
+ if definition[:required] && (definition[:required] - value.keys).any?
102
+ missing = definition[:required] - value.keys
103
+ @errors << "Value at '#{path}' is missing required keys #{missing.join(', ')}"
104
+ end
105
+
106
+ value.each_pair do |key, val|
107
+ @path.push(key)
108
+
109
+ if properties.include?(key)
110
+ validate_value(val, definition[:properties][key])
111
+ elsif definition[:additionalProperties]
112
+ validate_value(val, definition[:additionalProperties])
113
+ end
114
+ ensure
115
+ @path.pop
116
+ end
117
+ end
118
+
119
+ # Validates an array value, logging errors for any validations that fail.
120
+ # This will enumerate the items in the array and validate each item
121
+ # individually.
122
+ #
123
+ private def validate_array(value, definition)
124
+ if definition[:uniqueItems] && value.size != value.uniq.size
125
+ @errors << "Value at '#{path}' must not include duplicate elements"
126
+ return
127
+ end
128
+
129
+ return unless definition.key?(:items)
130
+
131
+ value.each_with_index do |item, index|
132
+ @path.push(index)
133
+ validate_value(item, definition[:items])
134
+ ensure
135
+ @path.pop
136
+ end
137
+ end
138
+
139
+ # Validates a string value, logging errors for any validations that fail.
140
+ #
141
+ private def validate_string(value, definition)
142
+ if definition.key?(:enum) && !definition[:enum].include?(value)
143
+ message = "Value at '#{path}' must be "
144
+ message += "one of " if definition[:enum].count > 1
145
+ message += definition[:enum].join(', ')
146
+ multitype_error(message, value, definition)
147
+ end
148
+ end
149
+
150
+ # Validates a numeric value, logging errors for any validations that fail.
151
+ #
152
+ private def validate_number(value, definition)
153
+ if definition.key?(:minimum) && value < definition[:minimum]
154
+ @errors << "Value at '#{path}' must be a minimum of #{definition[:minimum]}"
155
+ end
156
+ end
157
+
158
+ # Adds warnings for unknown config options.
159
+ #
160
+ private def validate_keys(keys, known_keys)
161
+ (keys - known_keys).each do |key|
162
+ message = "Unknown option '#{key}'"
163
+ message += " at '#{path}'" if @path.any?
164
+ message += " at #{@location}" if @location
165
+ message += "."
166
+ @warnings << message
167
+ end
168
+ end
169
+
170
+ # Returns true if a value is a plugin reference. This also validates whether
171
+ # a value can be a plugin reference in the first place. If the value is a
172
+ # plugin reference but cannot be one according to the schema, then this will
173
+ # log an error.
174
+ #
175
+ private def plugin_reference?(value, definition)
176
+ if value.is_a?(Hash) && value.key?('_plugin')
177
+ unless definition[:_plugin]
178
+ @errors << "Value at '#{path}' is a plugin reference, which is unsupported at "\
179
+ "this location"
180
+ end
181
+
182
+ true
183
+ else
184
+ false
185
+ end
186
+ end
187
+
188
+ # Asserts the type for each option against the type specified in the schema
189
+ # definition. The schema definition can specify multiple valid types, so the
190
+ # value needs to only match one of the types to be valid. Returns early if
191
+ # there is no type in the definition (in practice this shouldn't happen, but
192
+ # this will safeguard against any dev mistakes).
193
+ #
194
+ private def valid_type?(value, definition)
195
+ return unless definition.key?(:type)
196
+
197
+ types = Array(definition[:type])
198
+
199
+ if types.include?(value.class)
200
+ true
201
+ else
202
+ if types.include?(TrueClass) || types.include?(FalseClass)
203
+ types = types - [TrueClass, FalseClass] + ['Boolean']
204
+ end
205
+
206
+ @errors << "Value at '#{path}' must be of type #{types.join(' or ')}"
207
+
208
+ false
209
+ end
210
+ end
211
+
212
+ # Adds an error that includes additional helpful information for values
213
+ # that accept multiple types.
214
+ #
215
+ private def multitype_error(message, value, definition)
216
+ if Array(definition[:type]).count > 1
217
+ types = Array(definition[:type]) - [value.class]
218
+ message += " or must be of type #{types.join(' or ')}"
219
+ end
220
+
221
+ @errors << message
222
+ end
223
+
224
+ # Returns the formatted path for the key.
225
+ #
226
+ private def path
227
+ @path.join('.')
228
+ end
229
+ end
230
+ end
231
+ end