provisioning-core/nulib/domain/mode/validator.nu

480 lines
17 KiB
Text
Raw Permalink Normal View History

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