# Mode Configuration Validator # Validates mode configurations against Nickel schemas and runtime requirements # utils/logging star-import was dead — dropped (ADR-025 Phase 3 Layer 2). # Validate complete mode configuration export def validate-mode-config [ mode_name: string # Mode to validate --strict # Strict validation (fail on warnings) ] -> record { log-info $"Validating mode configuration: ($mode_name)" let config_file = get-mode-file $mode_name if not ($config_file | path exists) { error make { msg: $"Mode configuration file not found: ($config_file)" } } let config = open $config_file mut errors = [] mut warnings = [] # Validate structure let struct_result = validate-structure $config $errors = ($errors | append $struct_result.errors) $warnings = ($warnings | append $struct_result.warnings) # Validate authentication let auth_result = validate-authentication-config $config.authentication $errors = ($errors | append $auth_result.errors) $warnings = ($warnings | append $auth_result.warnings) # Validate services let service_result = validate-services-config $config.services $errors = ($errors | append $service_result.errors) $warnings = ($warnings | append $service_result.warnings) # Validate extensions let ext_result = validate-extensions-config $config.extensions $errors = ($errors | append $ext_result.errors) $warnings = ($warnings | append $ext_result.warnings) # Validate workspaces let ws_result = validate-workspace-config $config.workspaces $errors = ($errors | append $ws_result.errors) $warnings = ($warnings | append $ws_result.warnings) # Validate security let sec_result = validate-security-config $config.security $errors = ($errors | append $sec_result.errors) $warnings = ($warnings | append $sec_result.warnings) # Determine success let has_errors = (($errors | length) > 0) let has_warnings = (($warnings | length) > 0) let success = if $strict { not $has_errors and not $has_warnings } else { not $has_errors } { mode: $mode_name success: $success errors: $errors warnings: $warnings error_count: ($errors | length) warning_count: ($warnings | length) } } # Validate mode structure def validate-structure [config: record] -> record { mut errors = [] mut warnings = [] # Required top-level fields let required = ["mode", "description", "authentication", "services", "extensions", "workspaces", "security"] for field in $required { if not ($field in $config) { $errors = ($errors | append $"Missing required field: ($field)") } } # Validate mode name if "mode" in $config { let valid_modes = ["solo", "multi-user", "cicd", "enterprise"] if not ($config.mode in $valid_modes) { $errors = ($errors | append $"Invalid mode: ($config.mode). Must be one of: ($valid_modes | str join ', ')") } } {errors: $errors, warnings: $warnings} } # Validate authentication configuration def validate-authentication-config [auth: record] -> record { mut errors = [] mut warnings = [] if not ("type" in $auth) { $errors = ($errors | append "Authentication type not specified") return {errors: $errors, warnings: $warnings} } let valid_types = ["none", "token", "mtls", "oauth", "kms"] if not ($auth.type in $valid_types) { $errors = ($errors | append $"Invalid authentication type: ($auth.type)") } # Type-specific validation match $auth.type { "token" => { if not ("token_path" in $auth) { $errors = ($errors | append "Token path required for token authentication") } } "mtls" => { if not ("mtls" in $auth) { $errors = ($errors | append "mTLS configuration required") } else { let mtls = $auth.mtls let required_certs = ["client_cert_path", "client_key_path", "ca_cert_path"] for cert in $required_certs { if not ($cert in $mtls) { $errors = ($errors | append $"Missing mTLS field: ($cert)") } } } } "oauth" => { if not ("oauth" in $auth) { $errors = ($errors | append "OAuth configuration required") } else { let oauth = $auth.oauth if not ("provider_url" in $oauth) { $errors = ($errors | append "OAuth provider_url required") } if not ("client_id" in $oauth) { $errors = ($errors | append "OAuth client_id required") } } } } # SSH key storage validation if "ssh_key_storage" in $auth { let valid_storage = ["local", "kms", "vault"] if not ($auth.ssh_key_storage in $valid_storage) { $errors = ($errors | append $"Invalid ssh_key_storage: ($auth.ssh_key_storage)") } if ($auth.ssh_key_storage == "kms") and not ("kms" in $auth) { $warnings = ($warnings | append "SSH key storage set to KMS but no KMS configuration provided") } } {errors: $errors, warnings: $warnings} } # Validate services configuration def validate-services-config [services: record] -> record { mut errors = [] mut warnings = [] # Required services let required_services = ["orchestrator", "oci-registry"] for service in $required_services { if not ($service in $services) { $errors = ($errors | append $"Missing required service: ($service)") } else { let svc = $services | get $service if not ("deployment" in $svc) { $errors = ($errors | append $"Service ($service) missing 'deployment' field") } else { let valid_deployments = ["local", "remote", "k8s", "disabled"] if not ($svc.deployment in $valid_deployments) { $errors = ($errors | append $"Invalid deployment type for ($service): ($svc.deployment)") } # Deployment-specific validation match $svc.deployment { "local" => { if not ("data_dir" in $svc) and not ("local_config" in $svc) { $warnings = ($warnings | append $"Local service ($service) should specify data_dir") } if not ("port" in $svc) { $warnings = ($warnings | append $"Local service ($service) should specify port") } } "remote" => { if not ("endpoint" in $svc) and not ("remote_config" in $svc) { $errors = ($errors | append $"Remote service ($service) must specify endpoint") } } "k8s" => { if not ("namespace" in $svc) and not ("k8s_config" in $svc) { $errors = ($errors | append $"K8s service ($service) must specify namespace") } if not ("deployment_name" in $svc) and not ("k8s_config" in $svc) { $errors = ($errors | append $"K8s service ($service) must specify deployment_name") } } } } } } # Validate OCI registry specific fields if "oci-registry" in $services { let oci = $services."oci-registry" if $oci.deployment != "disabled" { if not ("type" in $oci) { $errors = ($errors | append "OCI registry type not specified") } else { let valid_types = ["distribution", "zot", "harbor", "artifactory"] if not ($oci.type in $valid_types) { $errors = ($errors | append $"Invalid OCI registry type: ($oci.type)") } } if not ("endpoint" in $oci) { $errors = ($errors | append "OCI registry endpoint not specified") } if "namespaces" in $oci { let ns = $oci.namespaces let required_ns = ["extensions", "nickel_packages", "platform_images", "test_images"] for n in $required_ns { if not ($n in $ns) { $warnings = ($warnings | append $"OCI registry namespace missing: ($n)") } } } else { $warnings = ($warnings | append "OCI registry namespaces not configured") } } } {errors: $errors, warnings: $warnings} } # Validate extensions configuration def validate-extensions-config [extensions: record] -> record { mut errors = [] mut warnings = [] if not ("source" in $extensions) { $errors = ($errors | append "Extension source not specified") return {errors: $errors, warnings: $warnings} } let valid_sources = ["local", "gitea", "oci", "mixed"] if not ($extensions.source in $valid_sources) { $errors = ($errors | append $"Invalid extension source: ($extensions.source)") } # Source-specific validation match $extensions.source { "local" => { if not ("local_path" in $extensions) { $warnings = ($warnings | append "Local extension source should specify local_path") } } "gitea" => { if not ("gitea_config" in $extensions) { $errors = ($errors | append "Gitea configuration required for gitea source") } } "oci" => { if not ("oci_registry" in $extensions) { $errors = ($errors | append "OCI registry configuration required for oci source") } else { if not $extensions.oci_registry.enabled { $warnings = ($warnings | append "OCI source selected but OCI registry not enabled") } } } } {errors: $errors, warnings: $warnings} } # Validate workspace configuration def validate-workspace-config [workspaces: record] -> record { mut errors = [] mut warnings = [] if not ("locking" in $workspaces) { $errors = ($errors | append "Workspace locking policy not specified") return {errors: $errors, warnings: $warnings} } let valid_locking = ["disabled", "enabled", "required"] if not ($workspaces.locking in $valid_locking) { $errors = ($errors | append $"Invalid locking policy: ($workspaces.locking)") } if ($workspaces.locking in ["enabled", "required"]) { if not ("lock_provider" in $workspaces) { $errors = ($errors | append "Lock provider required when locking is enabled") } else { let valid_providers = ["gitea", "etcd", "redis", "filesystem"] if not ($workspaces.lock_provider in $valid_providers) { $errors = ($errors | append $"Invalid lock provider: ($workspaces.lock_provider)") } } } if not ("git_integration" in $workspaces) { $warnings = ($warnings | append "Git integration policy not specified") } else { let valid_git = ["disabled", "optional", "required"] if not ($workspaces.git_integration in $valid_git) { $errors = ($errors | append $"Invalid git integration: ($workspaces.git_integration)") } } if not ("isolation" in $workspaces) { $warnings = ($warnings | append "Workspace isolation not specified, using default") } {errors: $errors, warnings: $warnings} } # Validate security configuration def validate-security-config [security: record] -> record { mut errors = [] mut warnings = [] # Check encryption settings if not ("encryption_at_rest" in $security) { $warnings = ($warnings | append "Encryption at rest not specified") } if not ("encryption_in_transit" in $security) { $warnings = ($warnings | append "Encryption in transit not specified") } # Validate DNS modification if "dns_modification" in $security { let valid_dns = ["none", "coredns", "system"] if not ($security.dns_modification in $valid_dns) { $errors = ($errors | append $"Invalid dns_modification: ($security.dns_modification)") } } # Validate audit logging if "audit_logging" in $security and $security.audit_logging { if not ("audit_log_path" in $security) { $errors = ($errors | append "Audit log path required when audit logging enabled") } } # Validate secret provider if "secret_provider" in $security { let sp = $security.secret_provider if not ("type" in $sp) { $errors = ($errors | append "Secret provider type not specified") } else { let valid_types = ["sops", "kms"] if not ($sp.type in $valid_types) { $errors = ($errors | append $"Invalid secret provider type: ($sp.type)") } # Type-specific validation if ($sp.type == "sops") and not ("age_key_file" in $sp) { $warnings = ($warnings | append "SOPS configured but age_key_file not specified") } if ($sp.type == "kms") and not ("server_url" in $sp) { $errors = ($errors | append "KMS server_url required for KMS secret provider") } } } {errors: $errors, warnings: $warnings} } # Check runtime requirements for mode export def check-runtime-requirements [ mode_name: string ] -> record { let config_file = get-mode-file $mode_name let config = open $config_file mut checks = [] # Check for required binaries match $config.services.orchestrator.deployment { "local" => { let orch_check = (which orchestrator | length) > 0 $checks = ($checks | append { check: "Orchestrator binary" required: true available: $orch_check message: if $orch_check { "Found" } else { "Not found in PATH" } }) } } # Check for OCI registry tools if $config.services.oci-registry.deployment == "local" { let registry_type = $config.services.oci-registry.type let bin_check = (which $registry_type | length) > 0 $checks = ($checks | append { check: $"OCI registry binary \(($registry_type))" required: false available: $bin_check message: if $bin_check { "Found" } else { $"($registry_type) not found in PATH" } }) } # Check for authentication requirements match $config.authentication.type { "token" => { let token_path = $config.authentication.token_path | str replace "~" $env.HOME let token_exists = ($token_path | path exists) $checks = ($checks | append { check: "Authentication token" required: true available: $token_exists message: if $token_exists { $"Found at ($token_path)" } else { $"Not found at ($token_path)" } }) } "mtls" => { if "mtls" in $config.authentication { let cert_path = $config.authentication.mtls.client_cert_path let cert_exists = ($cert_path | path exists) $checks = ($checks | append { check: "mTLS client certificate" required: true available: $cert_exists message: if $cert_exists { $"Found at ($cert_path)" } else { $"Not found at ($cert_path)" } }) } } } # Check for Kubernetes (if using k8s deployment) if ($config.services.orchestrator.deployment == "k8s") { let kubectl_check = (which kubectl | length) > 0 $checks = ($checks | append { check: "kubectl" required: true available: $kubectl_check message: if $kubectl_check { "Found" } else { "Not found in PATH" } }) if $kubectl_check { let cluster_check = (do { kubectl cluster-info } | complete).exit_code == 0 $checks = ($checks | append { check: "Kubernetes cluster access" required: true available: $cluster_check message: if $cluster_check { "Connected" } else { "Cannot connect to cluster" } }) } } let all_required_met = ($checks | where required | all {|c| $c.available }) { mode: $mode_name all_requirements_met: $all_required_met checks: $checks } } # Helper: Get mode file path def get-mode-file [mode: string] -> string { let modes_dir = $env.PROVISIONING_PROJECT_ROOT? | default (pwd) | path join "workspace" "config" "modes" $modes_dir | path join $"($mode).yaml" }