451 lines
13 KiB
Plaintext
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"
|
|
}
|
|
}
|