2025-10-07 10:32:04 +01:00

451 lines
13 KiB
Plaintext

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