Skip to content

Commit 5218c66

Browse files
authored
Merge pull request puppetlabs#2345 from beechtom/2337/config-validator
(puppetlabsGH-2337) Validate config using schemas
2 parents e38fffb + e85e530 commit 5218c66

19 files changed

Lines changed: 715 additions & 186 deletions

lib/bolt/bolt_option_parser.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -891,12 +891,12 @@ def initialize(options)
891891
if ENV.include?(Bolt::Inventory::ENVIRONMENT_VAR)
892892
raise Bolt::CLIError, "Cannot pass inventory file when #{Bolt::Inventory::ENVIRONMENT_VAR} is set"
893893
end
894-
@options[:inventoryfile] = Pathname.new(File.expand_path(path))
894+
@options[:inventoryfile] = File.expand_path(path)
895895
end
896896
define('--puppetfile PATH',
897897
'Specify a Puppetfile to use when installing modules. (default: ~/.puppetlabs/bolt/Puppetfile)',
898898
'Modules are installed in the current project.') do |path|
899-
@options[:puppetfile_path] = Pathname.new(File.expand_path(path))
899+
@options[:puppetfile_path] = File.expand_path(path)
900900
end
901901
define('--[no-]save-rerun', 'Whether to update the rerun file after this command.') do |save|
902902
@options[:'save-rerun'] = save

lib/bolt/config.rb

Lines changed: 23 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
require 'bolt/logger'
88
require 'bolt/util'
99
require 'bolt/config/options'
10+
require 'bolt/config/validator'
1011

1112
module Bolt
1213
class UnknownTransportError < Bolt::Error
@@ -73,6 +74,16 @@ def self.from_file(configfile, overrides = {})
7374
new(project, data, overrides)
7475
end
7576

77+
def self.defaults_schema
78+
base = OPTIONS.slice(*BOLT_DEFAULTS_OPTIONS)
79+
base['inventory-config'][:properties] = TRANSPORT_CONFIG.transform_values(&:schema)
80+
base
81+
end
82+
83+
def self.bolt_schema
84+
OPTIONS.slice(*BOLT_OPTIONS).merge(TRANSPORT_CONFIG.transform_values(&:schema))
85+
end
86+
7687
def self.system_path
7788
# Lazy-load expensive gem code
7889
require 'win32/dir' if Bolt::Util.windows?
@@ -106,6 +117,10 @@ def self.load_bolt_defaults_yaml(dir)
106117
)
107118
end
108119

120+
# Validate the config against the schema. This will raise a single error
121+
# with all validation errors.
122+
Validator.new.validate(data, defaults_schema, filepath)
123+
109124
# Remove project-specific config such as hiera-config, etc.
110125
project_config = data.slice(*(BOLT_PROJECT_OPTIONS - BOLT_DEFAULTS_OPTIONS))
111126

@@ -161,6 +176,10 @@ def self.load_bolt_yaml(dir)
161176
msg: "Configuration file #{filepath} is deprecated and will be removed in a future version "\
162177
"of Bolt. Use '#{dir + BOLT_DEFAULTS_NAME}' instead." }]
163178

179+
# Validate the config against the schema. This will raise a single error
180+
# with all validation errors.
181+
Validator.new.validate(data, bolt_schema, filepath)
182+
164183
{ filepath: filepath, data: data, logs: logs, deprecations: deprecations }
165184
end
166185

@@ -271,7 +290,7 @@ def normalize_overrides(options)
271290

272291
# Set console log to debug if in debug mode
273292
if options[:debug]
274-
overrides['log'] = { 'console' => { 'level' => :debug } }
293+
overrides['log'] = { 'console' => { 'level' => 'debug' } }
275294
end
276295

277296
if options[:puppetfile_path]
@@ -280,6 +299,9 @@ def normalize_overrides(options)
280299

281300
overrides['trace'] = opts['trace'] if opts.key?('trace')
282301

302+
# Validate the overrides
303+
Validator.new.validate(overrides, OPTIONS, 'command line')
304+
283305
overrides
284306
end
285307

@@ -397,35 +419,11 @@ def validate
397419
"is automatically appended to the modulepath and cannot be configured."
398420
end
399421

400-
keys = OPTIONS.keys - %w[plugins plugin_hooks puppetdb]
401-
keys.each do |key|
402-
next unless Bolt::Util.references?(@data[key])
403-
valid_keys = TRANSPORT_CONFIG.keys + %w[plugins plugin_hooks puppetdb]
404-
raise Bolt::ValidationError,
405-
"Found unsupported key _plugin in config setting #{key}. Plugins are only available in "\
406-
"#{valid_keys.join(', ')}."
407-
end
408-
409-
unless concurrency.is_a?(Integer) && concurrency > 0
410-
raise Bolt::ValidationError,
411-
"Concurrency must be a positive Integer, received #{concurrency.class} #{concurrency}"
412-
end
413-
414-
unless compile_concurrency.is_a?(Integer) && compile_concurrency > 0
415-
raise Bolt::ValidationError,
416-
"Compile concurrency must be a positive Integer, received #{compile_concurrency.class} "\
417-
"#{compile_concurrency}"
418-
end
419-
420422
compile_limit = 2 * Etc.nprocessors
421423
unless compile_concurrency < compile_limit
422424
raise Bolt::ValidationError, "Compilation is CPU-intensive, set concurrency less than #{compile_limit}"
423425
end
424426

425-
unless %w[human json rainbow].include? format
426-
raise Bolt::ValidationError, "Unsupported format: '#{format}'"
427-
end
428-
429427
Bolt::Util.validate_file('hiera-config', @data['hiera-config']) if @data['hiera-config']
430428
Bolt::Util.validate_file('trusted-external-command', trusted_external) if trusted_external
431429

lib/bolt/config/options.rb

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -307,7 +307,11 @@ module Options
307307
"its value is a map of configuration data. Configurable options are specified by the plugin. "\
308308
"Read more about configuring plugins in [Using plugins](using_plugins.md#configuring-plugins).",
309309
type: Hash,
310-
_plugin: true,
310+
additionalProperties: {
311+
type: Hash,
312+
_plugin: true
313+
},
314+
_plugin: false,
311315
_example: { "pkcs7" => { "keysize" => 1024 } }
312316
},
313317
"puppetdb" => {

lib/bolt/config/transport/base.rb

Lines changed: 10 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
require 'bolt/error'
44
require 'bolt/util'
5+
require 'bolt/config/validator'
56
require 'bolt/config/transport/options'
67

78
module Bolt
@@ -90,6 +91,14 @@ def self.options
9091
self::OPTIONS
9192
end
9293

94+
def self.schema
95+
{
96+
type: Hash,
97+
properties: self::TRANSPORT_OPTIONS.slice(*self::OPTIONS),
98+
_plugin: true
99+
}
100+
end
101+
93102
private def defaults
94103
unless defined? self.class::DEFAULTS
95104
raise NotImplementedError,
@@ -116,25 +125,7 @@ def self.options
116125

117126
# Validation defaults to just asserting the option types
118127
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))
138129
end
139130
end
140131
end

lib/bolt/config/transport/local.rb

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -29,13 +29,6 @@ def self.options
2929
if @config['interpreters']
3030
@config['interpreters'] = normalize_interpreters(@config['interpreters'])
3131
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
3932
end
4033
end
4134
end

lib/bolt/config/transport/ssh.rb

Lines changed: 8 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,14 @@ def self.options
7373
%w[ssh-command native-ssh].concat(OPTIONS)
7474
end
7575

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+
7684
private def filter(unfiltered)
7785
# Because we filter before merging config together it's impossible to
7886
# know whether both ssh-command *and* native-ssh will be specified
@@ -87,12 +95,6 @@ def self.options
8795
super
8896

8997
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-
9698
if key_opt.instance_of?(String)
9799
@config['private-key'] = File.expand_path(key_opt, @project)
98100

@@ -114,14 +116,6 @@ def self.options
114116
"Unsupported login-shell #{@config['login-shell']}. Supported shells are #{LOGIN_SHELLS.join(', ')}"
115117
end
116118

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-
125119
if @config['login-shell'] == 'powershell'
126120
%w[tty run-as].each do |key|
127121
if @config[key]

0 commit comments

Comments
 (0)