Module: Rzo::App::ConfigValidation

Included in:
Subcommand
Defined in:
lib/rzo/app/config_validation.rb

Overview

Mix-in module providing configuration validation methods and safety checking. The goal is to provide useful feedback to the end user in the situation where ~/.rizzo.yaml is configured to point at directories which do not exist, have missing keys, etc... rubocop:disable Metrics/ModuleLength

Defined Under Namespace

Classes: Issue

Constant Summary collapse

RZO_PERSONAL_CONFIG_SCHEMA =

Rizzo configuration schema for the personal configuration file at ~/.rizzo.yaml. Minimum necessary to load the complete configuration from all control repositories.

{
  '$schema' => 'http://json-schema.org/draft/schema#',
  title: 'Personal Configuration',
  description: 'Rizzo personal configuration file',
  type: 'object',
  properties: {
    defaults: {
      type: 'object',
    },
    control_repos: {
      type: 'array',
      items: { type: 'string' },
      uniqueItems: true,
    },
  },
  required: ['control_repos'],
}.freeze
RZO_REPO_CONFIG_SCHEMA =

Rizzo complete configuration schema. This should move to a JSON file outside the code.

{
  type: 'object',
  required: %w[defaults control_repos puppetmaster],
  properties: {
    defaults: {
      type: 'object',
      required: ['bootstrap_repo_path'],
      properties: {
        bootstrap_repo_path: {
          type: 'string',
          pattern: '^([a-zA-Z]:){0,1}(/[^/]+)+$',
        },
      },
    },
    puppetmaster: {
      type: 'object',
      required: %w[name modulepath synced_folders],
      properties: {
        name: {
          type: 'array',
          items: { type: 'string' },
        },
        modulepath: {
          type: 'array',
          items: { type: 'string' },
        },
        synced_folders: {
          '$schema' => 'http://json-schema.org/draft/schema#',
          type: 'object',
          properties: {
            '/' => {},
            patternProperties: {
              '^(/[^/]+)+$' => {},
            },
            additionalProperties: false,
            required: ['/'],
          }
        },
      },
    },
    control_repos: {
      type: 'array',
      items: { type: 'string' },
      uniqueItems: true,
    },
    nodes: {
      type: 'array',
      items: {
        type: 'object',
        required: %w[name hostname ip],
        properties: {
          name: { type: 'string' },
          hostname: { type: 'string' },
          ip: { type: 'string' },
          memory: {
            type: 'string',
            pattern: '^[0-9]+$'
          },
          forwarded_ports: {
            type: 'array',
            items: {
              type: 'object',
              required: %w[guest host],
              properties: {
                guest: {
                  type: 'string',
                  pattern: '^[0-9]+$',
                },
                host: {
                  type: 'string',
                  pattern: '^[0-9]+$',
                },
              },
            },
          },
        },
      },
      uniqueItems: true,
    }
  },
}.freeze
CHECKS_PERSONAL_CONFIG =

The checks to execute, in order. Each method must return nil if there are no issues found. Otherwise, the check should return either one, or an array of Issue instances.

i[validate_personal_schema validate_control_repos].freeze
CHECKS_REPO_CONFIG =
i[validate_schema validate_defaults_key validate_control_repos].freeze

Instance Method Summary collapse

Instance Method Details

#compute_issues(checks, config) ⇒ Array<Issue>

Compute Issues given a config map (base or complete), and an Array of methods to execute.

Parameters:

  • checks (Array<Symbol>)

    the method identifiers to execute, passing config. These methods must return nil (no issue found), an Issue instance, or Array for multiple issues found.

  • config (Hash)

    the config hash, either a base configuration or a fully merged configuration.

Returns:

  • (Array<Issue>)

    Array of issue instances, or an empty array if no issues found with the config.



150
151
152
153
154
155
156
157
158
159
# File 'lib/rzo/app/config_validation.rb', line 150

def compute_issues(checks, config)
  ctx = self
  checks.each_with_object([]) do |mth, ary|
    debug "Checking config for #{mth} issues"
    if issue = ctx.send(mth, config)
      # May get back an Array<Issue> or one Issue
      ary.concat([*issue])
    end
  end
end

#validate_complete_config!(config) ⇒ Object

Validate a complete loaded configuration. This is distinct from a base configuration in that the YAML files in each control repository have already been merged, in order, on top of the base configuration originating at ~/.rizzo.yaml. This implements safety checking. These methods are expected to execute within the context of a Rzo::App::Subcommand instance, therefore log methods and the parsed configuration are assumed to be available.

The approach is to collect an Array of Issue instances. If issues are found, control is handed off to validate_inform! to inform the user of the issues and potentially abort the program.

Parameters:

  • config (Hash)

    the config hash, fully merged by load_config!



190
191
192
193
194
195
196
197
# File 'lib/rzo/app/config_validation.rb', line 190

def validate_complete_config!(config)
  issues = compute_issues(CHECKS_REPO_CONFIG, config)
  if issues.empty?
    debug 'No issues detected with the complete, merged configuration.'
  else
    validate_inform!(issues)
  end
end

#validate_control_repos(config) ⇒ Issue?

Validate the top level "control_repos" key, which should have a value of Array where each string value is a fully qualified path.

Returns:

  • (Issue, nil)

    Issue found, or nil if no issues found.



242
243
244
245
246
247
248
249
250
251
252
253
# File 'lib/rzo/app/config_validation.rb', line 242

def validate_control_repos(config)
  if repos = config['control_repos']
    return Issue.new('Top level key "control_repos" must have an Array value') unless repos.is_a? Array
  else
    return Issue.new('Top level key "control_repos" is not specified.  It must be an Array of paths to your control repos.')
  end
  repos.each_with_object([]) do |pth, ary|
    if issue = validate_existence(pth, '#/control_repos')
      ary << issue
    end
  end
end

#validate_defaults_key(config) ⇒ Issue?

Validate the configuration has a top level key named "defaults" and the value is a Hash map. rubocop:disable Metrics/MethodLength

Returns:

  • (Issue, nil)

    Issue found, or nil if no issues found.



220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
# File 'lib/rzo/app/config_validation.rb', line 220

def validate_defaults_key(config)
  if defaults = config['defaults']
    return Issue.new('Top level key "defaults" must have a Hash value') unless defaults.is_a? Hash
  else
    return Issue.new('Configuration does not contain top level "defaults" key')
  end
  if pth = defaults['bootstrap_repo_path']
    return Issue.new('#/defaults/bootstrap_repo_path is not a String') unless pth.is_a? String
  else
    return Issue.new 'Configuration "defaults" value does not contain a '\
      '"bootstrap_repo_path" key.  For example, '\
      '{"defaults":{"bootstrap_repo_path":"/tmp/foo"}}'
  end
  validate_existence(pth, '#/defaults/bootstrap_repo_path value of ')
end

#validate_existence(path, prefix = '') ⇒ Issue, ...

Given a string, validate it's a fully qualified path, readable, and a git directory.

Returns:

  • (Issue, Array<Issue>, nil)

    nil if no issues found, or one or more Issue instances.



261
262
263
264
265
266
267
268
# File 'lib/rzo/app/config_validation.rb', line 261

def validate_existence(path, prefix = '')
  pn = Pathname.new(path)
  git = pn + '.git'
  return Issue.new("#{prefix}#{pn} is not an absolute path.  It must be fully qualified, not relative") unless pn.absolute?
  return Issue.new("#{prefix}#{pn} is not a directory.  Has it been cloned?") unless pn.directory?
  return Issue.new("#{prefix}#{pn} is not readable.  Are permissions correct?") unless pn.readable?
  return Issue.new("#{prefix}#{git} does not exist.  Has #{git.dirname} been cloned properly?") unless git.directory?
end

#validate_inform!(issues) ⇒ Object

Inform the user about issues found and exit the program. The top level exception handler is not expected to display much information on validation errors. This method is expected to provide the helpful guidance.

least a key named :message

Parameters:

  • issues (Array<Issue>)

    Array of issues. Each hash must have at



293
294
295
296
297
298
299
300
301
302
# File 'lib/rzo/app/config_validation.rb', line 293

def validate_inform!(issues)
  if opts[:validate]
    msg = "Validation issues found with #{opts[:config]}"
    exc = ErrorAndExit.new(msg, 2)
    exc.log_fatal = issues.each_with_object([]) { |i, a| a << i.to_s }
    raise exc
  else
    issues.each { |i| log.warn(i.to_s) }
  end
end

#validate_personal_config!(config) ⇒ Object

Validate a personal configuration, typically originating from ~/.rizzo.yaml. This configuration is necessary to build a complete control repo configuration using the top level control repo. This validation focuses on the minimum necessary configuration to bootstrap the complete configuration, primarily the repo locations and existence.



167
168
169
170
171
172
173
174
# File 'lib/rzo/app/config_validation.rb', line 167

def validate_personal_config!(config)
  issues = compute_issues(CHECKS_PERSONAL_CONFIG, config)
  if issues.empty?
    debug 'No issues detected with the personal configuration.'
  else
    validate_inform!(issues)
  end
end

#validate_personal_schema(config) ⇒ Issue, ...

Validate the personal configuration, focus on ensuring the rest of the configuration can load properly.

Returns:

  • (Issue, Array<Issue>, nil)

    nil if no issues found, or one or more Issue instances.



276
277
278
279
280
281
282
283
284
# File 'lib/rzo/app/config_validation.rb', line 276

def validate_personal_schema(config)
  if JSON::Validator.validate(RZO_PERSONAL_CONFIG_SCHEMA, config)
    debug 'No schema violations found in personal configuration file.'
    return nil
  else
    err_msgs = JSON::Validator.fully_validate(RZO_PERSONAL_CONFIG_SCHEMA, config)
    return err_msgs.map { |msg| Issue.new("Personal config problem: #{msg}") }
  end
end

#validate_schema(config) ⇒ Issue?

Validate using json-schema

Returns:

  • (Issue, nil)

    Issue found, or nil if no issues found.



204
205
206
207
208
209
210
211
212
# File 'lib/rzo/app/config_validation.rb', line 204

def validate_schema(config)
  if JSON::Validator.validate(RZO_REPO_CONFIG_SCHEMA, config)
    debug 'No schema violations found in loaded config.'
    return nil
  else
    err_msgs = JSON::Validator.fully_validate(RZO_REPO_CONFIG_SCHEMA, config)
    return err_msgs.map { |msg| Issue.new("Schema violation: #{msg}") }
  end
end