provisioning-core/nulib/domain/config/loader/validator.nu

357 lines
12 KiB
Text
Raw Permalink Normal View History

# Module: Configuration Validator
# Purpose: Validates configuration structure, paths, data types, semantic rules, and file existence.
# Dependencies: loader_core for get-config-value
# Configuration Validation Functions
# Validates configuration structure, paths, data types, semantic rules, and files
# Validate configuration structure - checks required sections exist
export def validate-config-structure [
config: record
] {
let required_sections = ["core", "paths", "debug", "sops"]
mut errors = []
mut warnings = []
for section in $required_sections {
let section_value = ($config | get -o $section | default null)
if ($section_value | is-empty) {
$errors = ($errors | append {
type: "missing_section",
severity: "error",
section: $section,
message: $"Missing required configuration section: ($section)"
})
}
}
{
valid: (($errors | length) == 0),
errors: $errors,
warnings: $warnings
}
}
# Validate path values - checks paths exist and are absolute
export def validate-path-values [
config: record
] {
let required_paths = ["base", "providers", "taskservs", "clusters"]
mut errors = []
mut warnings = []
let paths = ($config | get -o paths | default {})
for path_name in $required_paths {
let path_value = ($paths | get -o $path_name | default null)
if ($path_value | is-empty) {
$errors = ($errors | append {
type: "missing_path",
severity: "error",
path: $path_name,
message: $"Missing required path: paths.($path_name)"
})
} else {
# Check if path is absolute
if not ($path_value | str starts-with "/") {
$warnings = ($warnings | append {
type: "relative_path",
severity: "warning",
path: $path_name,
value: $path_value,
message: $"Path paths.($path_name) should be absolute, got: ($path_value)"
})
}
# Check if base path exists (critical for system operation)
if $path_name == "base" {
if not ($path_value | path exists) {
$errors = ($errors | append {
type: "path_not_exists",
severity: "error",
path: $path_name,
value: $path_value,
message: $"Base path does not exist: ($path_value)"
})
}
}
}
}
{
valid: (($errors | length) == 0),
errors: $errors,
warnings: $warnings
}
}
# Validate data types - checks configuration values have correct types
export def validate-data-types [
config: record
] {
mut errors = []
mut warnings = []
# Validate core.version follows semantic versioning pattern
let core_version = ($config | get -o core.version | default null)
if ($core_version | is-not-empty) {
let version_pattern = "^\\d+\\.\\d+\\.\\d+(-.+)?$"
let version_parts = ($core_version | split row ".")
if (($version_parts | length) < 3) {
$errors = ($errors | append {
type: "invalid_version",
severity: "error",
field: "core.version",
value: $core_version,
message: $"core.version must follow semantic versioning format, got: ($core_version)"
})
}
}
# Validate debug.enabled is boolean
let debug_enabled = ($config | get -o debug.enabled | default null)
if ($debug_enabled | is-not-empty) {
if (($debug_enabled | describe) != "bool") {
$errors = ($errors | append {
type: "invalid_type",
severity: "error",
field: "debug.enabled",
value: $debug_enabled,
expected: "bool",
actual: ($debug_enabled | describe),
message: $"debug.enabled must be boolean, got: ($debug_enabled | describe)"
})
}
}
# Validate debug.metadata is boolean
let debug_metadata = ($config | get -o debug.metadata | default null)
if ($debug_metadata | is-not-empty) {
if (($debug_metadata | describe) != "bool") {
$errors = ($errors | append {
type: "invalid_type",
severity: "error",
field: "debug.metadata",
value: $debug_metadata,
expected: "bool",
actual: ($debug_metadata | describe),
message: $"debug.metadata must be boolean, got: ($debug_metadata | describe)"
})
}
}
# Validate sops.use_sops is boolean
let sops_use = ($config | get -o sops.use_sops | default null)
if ($sops_use | is-not-empty) {
if (($sops_use | describe) != "bool") {
$errors = ($errors | append {
type: "invalid_type",
severity: "error",
field: "sops.use_sops",
value: $sops_use,
expected: "bool",
actual: ($sops_use | describe),
message: $"sops.use_sops must be boolean, got: ($sops_use | describe)"
})
}
}
{
valid: (($errors | length) == 0),
errors: $errors,
warnings: $warnings
}
}
# Validate semantic rules - business logic validation
export def validate-semantic-rules [
config: record
] {
mut errors = []
mut warnings = []
# Validate provider configuration
let providers = ($config | get -o providers | default {})
let default_provider = ($providers | get -o default | default null)
if ($default_provider | is-not-empty) {
let valid_providers = ["aws", "upcloud", "local"]
if not ($default_provider in $valid_providers) {
$errors = ($errors | append {
type: "invalid_provider",
severity: "error",
field: "providers.default",
value: $default_provider,
valid_options: $valid_providers,
message: $"Invalid default provider: ($default_provider). Valid options: ($valid_providers | str join ', ')"
})
}
}
# Validate log level
let log_level = ($config | get -o debug.log_level | default null)
if ($log_level | is-not-empty) {
let valid_levels = ["trace", "debug", "info", "warn", "error"]
if not ($log_level in $valid_levels) {
$warnings = ($warnings | append {
type: "invalid_log_level",
severity: "warning",
field: "debug.log_level",
value: $log_level,
valid_options: $valid_levels,
message: $"Invalid log level: ($log_level). Valid options: ($valid_levels | str join ', ')"
})
}
}
# Validate output format
let output_format = ($config | get -o output.format | default null)
if ($output_format | is-not-empty) {
let valid_formats = ["json", "yaml", "toml", "text"]
if not ($output_format in $valid_formats) {
$warnings = ($warnings | append {
type: "invalid_output_format",
severity: "warning",
field: "output.format",
value: $output_format,
valid_options: $valid_formats,
message: $"Invalid output format: ($output_format). Valid options: ($valid_formats | str join ', ')"
})
}
}
{
valid: (($errors | length) == 0),
errors: $errors,
warnings: $warnings
}
}
# Validate file existence - checks referenced files exist
export def validate-file-existence [
config: record
] {
mut errors = []
mut warnings = []
# Check SOPS configuration file
let sops_config = ($config | get -o sops.config_path | default null)
if ($sops_config | is-not-empty) {
if not ($sops_config | path exists) {
$warnings = ($warnings | append {
type: "missing_sops_config",
severity: "warning",
field: "sops.config_path",
value: $sops_config,
message: $"SOPS config file not found: ($sops_config)"
})
}
}
# Check SOPS key files
let key_paths = ($config | get -o sops.key_search_paths | default [])
mut found_key = false
for key_path in $key_paths {
let expanded_path = ($key_path | str replace "~" $env.HOME)
if ($expanded_path | path exists) {
$found_key = true
break
}
}
if not $found_key and ($key_paths | length) > 0 {
$warnings = ($warnings | append {
type: "missing_sops_keys",
severity: "warning",
field: "sops.key_search_paths",
value: $key_paths,
message: $"No SOPS key files found in search paths: ($key_paths | str join ', ')"
})
}
# Check critical configuration files
let settings_file = ($config | get -o paths.files.settings | default null)
if ($settings_file | is-not-empty) {
if not ($settings_file | path exists) {
$errors = ($errors | append {
type: "missing_settings_file",
severity: "error",
field: "paths.files.settings",
value: $settings_file,
message: $"Settings file not found: ($settings_file)"
})
}
}
{
valid: (($errors | length) == 0),
errors: $errors,
warnings: $warnings
}
}
# Enhanced main validation function
export def validate-config [
config: record
--detailed = false # Show detailed validation results
--strict = false # Treat warnings as errors
] {
# Run all validation checks
let structure_result = (validate-config-structure $config)
let paths_result = (validate-path-values $config)
let types_result = (validate-data-types $config)
let semantic_result = (validate-semantic-rules $config)
let files_result = (validate-file-existence $config)
# Combine all results
let all_errors = (
$structure_result.errors | append $paths_result.errors | append $types_result.errors |
append $semantic_result.errors | append $files_result.errors
)
let all_warnings = (
$structure_result.warnings | append $paths_result.warnings | append $types_result.warnings |
append $semantic_result.warnings | append $files_result.warnings
)
let has_errors = ($all_errors | length) > 0
let has_warnings = ($all_warnings | length) > 0
# In strict mode, treat warnings as errors
let final_valid = if $strict {
not $has_errors and not $has_warnings
} else {
not $has_errors
}
# Throw error if validation fails and not in detailed mode
if not $detailed and not $final_valid {
let error_messages = ($all_errors | each { |err| $err.message })
let warning_messages = if $strict { ($all_warnings | each { |warn| $warn.message }) } else { [] }
let combined_messages = ($error_messages | append $warning_messages)
error make {
msg: ($combined_messages | str join "; ")
}
}
# Return detailed results
{
valid: $final_valid,
errors: $all_errors,
warnings: $all_warnings,
summary: {
total_errors: ($all_errors | length),
total_warnings: ($all_warnings | length),
checks_run: 5,
structure_valid: $structure_result.valid,
paths_valid: $paths_result.valid,
types_valid: $types_result.valid,
semantic_valid: $semantic_result.valid,
files_valid: $files_result.valid
}
}
}