479 lines
17 KiB
Text
479 lines
17 KiB
Text
# 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"
|
|
}
|