# 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 } } }