# Taskserv Validation Framework # Multi-level validation for taskservs before deployment use lib_provisioning * use utils.nu * use deps_validator.nu * use ../lib_provisioning/config/accessor.nu * # Validation levels const VALIDATION_LEVELS = { static: "Static validation (KCL, templates, scripts)" dependencies: "Dependency validation" prerequisites: "Server prerequisites validation" health: "Health check validation" all: "Complete validation (all levels)" } # Validate KCL schemas for taskserv def validate-kcl-schemas [ taskserv_name: string --verbose (-v) ]: nothing -> record { let taskservs_path = (get-taskservs-path) let kcl_path = ($taskservs_path | path join $taskserv_name "kcl") if not ($kcl_path | path exists) { return { valid: false level: "kcl" errors: [$"KCL directory not found: ($kcl_path)"] warnings: [] } } # Find all .k files let kcl_files = try { ls ($kcl_path | path join "*.k") | get name } catch { return { valid: false level: "kcl" errors: [$"No KCL files found in: ($kcl_path)"] warnings: [] } } if $verbose { _print $"Validating KCL schemas for (_ansi yellow_bold)($taskserv_name)(_ansi reset)..." } mut errors = [] mut warnings = [] for file in $kcl_files { if $verbose { _print $" Checking ($file | path basename)..." } let result = try { kcl run $file --format json | from json if $verbose { _print $" ✓ Valid" } null } catch { |err| let error_msg = $err.msg $errors = ($errors | append $"KCL error in ($file | path basename): ($error_msg)") if $verbose { _print $" ✗ Error: ($error_msg)" } null } } return { valid: (($errors | length) == 0) level: "kcl" files_checked: ($kcl_files | length) errors: $errors warnings: $warnings } } # Validate Jinja2 templates def validate-templates [ taskserv_name: string --verbose (-v) ]: nothing -> record { let taskservs_path = (get-taskservs-path) let default_path = ($taskservs_path | path join $taskserv_name "default") if not ($default_path | path exists) { return { valid: true level: "templates" files_checked: 0 errors: [] warnings: ["No default directory found, skipping template validation"] } } # Find all .j2 files let template_files = try { ls ($default_path | path join "**/*.j2") | get name } catch { return { valid: true level: "templates" files_checked: 0 errors: [] warnings: ["No templates found"] } } if $verbose { _print $"Validating templates for (_ansi yellow_bold)($taskserv_name)(_ansi reset)..." } mut errors = [] mut warnings = [] for file in $template_files { if $verbose { _print $" Checking ($file | path basename)..." } # Basic syntax check - just try to read and check for common issues let content = try { open $file } catch { $errors = ($errors | append $"Cannot read template: ($file | path basename)") continue } # Check for unclosed Jinja2 tags let open_blocks = ($content | str replace --all '\{\%.*?\%\}' '' | str replace --all '\{\{.*?\}\}' '') if ($open_blocks | str contains '{{') or ($open_blocks | str contains '{%') { $warnings = ($warnings | append $"Potential unclosed Jinja2 tags in: ($file | path basename)") } if $verbose { _print $" ✓ Basic syntax OK" } } return { valid: (($errors | length) == 0) level: "templates" files_checked: ($template_files | length) errors: $errors warnings: $warnings } } # Validate shell scripts def validate-scripts [ taskserv_name: string --verbose (-v) ]: nothing -> record { let taskservs_path = (get-taskservs-path) let default_path = ($taskservs_path | path join $taskserv_name "default") if not ($default_path | path exists) { return { valid: true level: "scripts" files_checked: 0 errors: [] warnings: ["No default directory found, skipping script validation"] } } # Find all .sh files let script_files = try { ls ($default_path | path join "**/*.sh") | get name } catch { return { valid: true level: "scripts" files_checked: 0 errors: [] warnings: ["No shell scripts found"] } } if $verbose { _print $"Validating scripts for (_ansi yellow_bold)($taskserv_name)(_ansi reset)..." } mut errors = [] mut warnings = [] # Check if shellcheck is available let has_shellcheck = (which shellcheck | length) > 0 if not $has_shellcheck { $warnings = ($warnings | append "shellcheck not available, skipping detailed script validation") } for file in $script_files { if $verbose { _print $" Checking ($file | path basename)..." } # Check if file is executable let is_executable = try { (ls -l $file | get mode | str contains "x") } catch { false } if not $is_executable { $warnings = ($warnings | append $"Script not executable: ($file | path basename)") } # Run shellcheck if available if $has_shellcheck { let result = try { ^shellcheck --severity=error $file if $verbose { _print $" ✓ shellcheck passed" } null } catch { |err| $errors = ($errors | append $"shellcheck error in ($file | path basename): ($err.msg)") if $verbose { _print $" ✗ shellcheck failed" } null } } else if $verbose { _print $" ⊘ shellcheck skipped" } } return { valid: (($errors | length) == 0) level: "scripts" files_checked: ($script_files | length) has_shellcheck: $has_shellcheck errors: $errors warnings: $warnings } } # Validate health check configuration def validate-health-check [ taskserv_name: string settings: record --verbose (-v) ]: nothing -> record { if $verbose { _print $"Validating health check for (_ansi yellow_bold)($taskserv_name)(_ansi reset)..." } let deps_validation = (validate-dependencies $taskserv_name $settings --verbose=false) if not $deps_validation.has_dependencies { return { valid: true level: "health" has_health_check: false errors: [] warnings: ["No health check configuration found"] } } let health_check = ($deps_validation.health_check | default null) if $health_check == null { return { valid: true level: "health" has_health_check: false errors: [] warnings: ["No health check configuration in dependencies"] } } mut errors = [] mut warnings = [] let endpoint = ($health_check | get -o endpoint | default "") let timeout = ($health_check | get -o timeout | default 30) let interval = ($health_check | get -o interval | default 10) if $endpoint == "" { $errors = ($errors | append "Health check endpoint is empty") } else { if not ($endpoint | str starts-with "http://") and not ($endpoint | str starts-with "https://") { $warnings = ($warnings | append "Health check endpoint should use http:// or https://") } if $verbose { _print $" Endpoint: ($endpoint)" _print $" Timeout: ($timeout)s" _print $" Interval: ($interval)s" } } if $timeout <= 0 { $errors = ($errors | append "Health check timeout must be positive") } if $interval <= 0 { $errors = ($errors | append "Health check interval must be positive") } return { valid: (($errors | length) == 0) level: "health" has_health_check: true endpoint: $endpoint timeout: $timeout interval: $interval errors: $errors warnings: $warnings } } # Main validation command export def "main validate" [ taskserv_name: string --infra (-i): string --settings (-s): string --level (-l): string = "all" --verbose (-v) --out: string ]: nothing -> nothing { if ($out | is-not-empty) { set-provisioning-out $out set-provisioning-no-terminal true } # Load settings let curr_settings = try { find_get_settings --infra $infra --settings $settings } catch { _print $"🛑 Failed to load settings" return } _print $"\n(_ansi cyan_bold)Taskserv Validation(_ansi reset)" _print $"Taskserv: (_ansi yellow_bold)($taskserv_name)(_ansi reset)" _print $"Level: ($level)\n" # Validate level parameter if $level not-in ["static", "dependencies", "prerequisites", "health", "all"] { _print $"🛑 Invalid level: ($level)" _print $"Valid levels: (($VALIDATION_LEVELS | columns | str join ', '))" return } mut all_results = [] # Static validation (KCL, templates, scripts) if $level in ["static", "all"] { let kcl_result = (validate-kcl-schemas $taskserv_name --verbose=$verbose) $all_results = ($all_results | append $kcl_result) let template_result = (validate-templates $taskserv_name --verbose=$verbose) $all_results = ($all_results | append $template_result) let script_result = (validate-scripts $taskserv_name --verbose=$verbose) $all_results = ($all_results | append $script_result) } # Dependencies validation if $level in ["dependencies", "all"] { let deps_result = (validate-dependencies $taskserv_name $curr_settings --verbose=$verbose) $all_results = ($all_results | append ($deps_result | insert level "dependencies")) if $verbose or not $deps_result.valid { print-validation-report $deps_result } } # Health check validation if $level in ["health", "all"] { let health_result = (validate-health-check $taskserv_name $curr_settings --verbose=$verbose) $all_results = ($all_results | append $health_result) } # Print summary _print $"\n(_ansi cyan_bold)Validation Summary(_ansi reset)" let total_errors = ($all_results | get errors | flatten | length) let total_warnings = ($all_results | get warnings | flatten | length) for result in $all_results { let level_name = $result.level let status = if $result.valid { $"(_ansi green_bold)✓(_ansi reset)" } else { $"(_ansi red_bold)✗(_ansi reset)" } let err_count = ($result.errors | length) let warn_count = ($result.warnings | length) _print $"($status) ($level_name): ($err_count) errors, ($warn_count) warnings" if $err_count > 0 { for err in $result.errors { _print $" (_ansi red)✗(_ansi reset) ($err)" } } if $warn_count > 0 and $verbose { for warn in $result.warnings { _print $" (_ansi yellow)⚠(_ansi reset) ($warn)" } } } _print $"\n(_ansi cyan_bold)Overall Status(_ansi reset)" if $total_errors == 0 { _print $"(_ansi green_bold)✓ VALID(_ansi reset) - ($total_warnings) warnings" } else { _print $"(_ansi red_bold)✗ INVALID(_ansi reset) - ($total_errors) errors, ($total_warnings) warnings" } } # Check dependencies command export def "main check-deps" [ taskserv_name: string --infra (-i): string --settings (-s): string --verbose (-v) ]: nothing -> nothing { let curr_settings = try { find_get_settings --infra $infra --settings $settings } catch { _print $"🛑 Failed to load settings" return } let validation = (validate-infra-dependencies $taskserv_name $curr_settings --verbose=$verbose) print-validation-report $validation } # List validation levels export def "main levels" []: nothing -> nothing { _print $"\n(_ansi cyan_bold)Available Validation Levels(_ansi reset)\n" for level in ($VALIDATION_LEVELS | transpose name description) { _print $"(_ansi yellow_bold)($level.name)(_ansi reset)" _print $" ($level.description)\n" } }