# Enhanced Check Mode for Taskservs # Provides dry-run capabilities with detailed validation and preview # REMOVED: use lib_provisioning * - causes circular import use utils.nu * use deps_validator.nu * use validate.nu * use ../lib_provisioning/config/accessor.nu * use ../lib_provisioning/utils/ssh.nu [scp_to, ssh_cmd] # Preview taskserv configuration generation def preview-config-generation [ taskserv_name: string taskserv_profile: string settings: record server: record --verbose (-v) ] { let taskservs_path = (get-taskservs-path) let taskserv_dir = (find-taskserv-dir $taskservs_path $taskserv_name) let profile_path = if ($taskserv_dir | is-not-empty) { $taskserv_dir | path join $taskserv_profile } else { "" } if not ($profile_path | path exists) { return { valid: false errors: [$"Profile path not found: ($profile_path)"] warnings: [] files: [] } } # Find all template files let template_files = (glob ($profile_path | path join "**/*.j2")) # Find shell scripts let script_files = (glob ($profile_path | path join "**/*.sh")) # Find other config files let config_files = (do -i { ls $profile_path | where type == "file" | where name !~ ".j2$" | where name !~ ".sh$" | get name } | default []) mut preview_files = [] # Preview templates for tpl in $template_files { let dest_name = ($tpl | path basename | str replace ".j2" "") $preview_files = ($preview_files | append { type: "template" source: ($tpl | path relative-to $profile_path) destination: $dest_name action: "render and upload" }) } # Preview scripts for script in $script_files { $preview_files = ($preview_files | append { type: "script" source: ($script | path basename) destination: ($script | path basename) action: "upload and execute" }) } # Preview config files for cfg in $config_files { $preview_files = ($preview_files | append { type: "config" source: ($cfg | path basename) destination: ($cfg | path basename) action: "upload" }) } return { valid: true errors: [] warnings: [] files: $preview_files total_files: ($preview_files | length) } } # Check prerequisites on target server (without actually connecting in check mode) def check-prerequisites [ taskserv_name: string server: record settings: record check_mode: bool ] { mut checks = [] # Check if server is accessible (in check mode, just validate config) if $check_mode { $checks = ($checks | append { check: "Server accessibility" status: "skipped" message: "Check mode - SSH not tested" }) } else { # In real mode, this would test SSH connection $checks = ($checks | append { check: "Server accessibility" status: "pending" message: "Would test SSH connection" }) } # Check if required directories exist (preview only in check mode) let required_dirs = ["/tmp", "/etc", "/usr/local/bin"] for dir in $required_dirs { $checks = ($checks | append { check: $"Directory ($dir)" status: "info" message: $"Would verify directory exists" }) } # Check if required commands are available let required_commands = ["bash", "systemctl"] for cmd in $required_commands { $checks = ($checks | append { check: $"Command ($cmd)" status: "info" message: $"Would verify command is available" }) } return { checks: $checks total_checks: ($checks | length) } } # Enhanced check mode handler export def run-check-mode [ taskserv_name: string taskserv_profile: string settings: record server: record --verbose (-v) ] { _print $"\n(_ansi cyan_bold)Check Mode: ($taskserv_name)(_ansi reset) on (_ansi green_bold)($server.hostname)(_ansi reset)" mut results = { taskserv: $taskserv_name profile: $taskserv_profile server: $server.hostname validations: [] overall_valid: true } # 1. Static validation _print $"\n(_ansi yellow)→ Running static validation...(_ansi reset)" let static_validation = (run-static-validation $taskserv_name --verbose=$verbose) let static_valid = ( $static_validation.nickel.valid and $static_validation.templates.valid and $static_validation.scripts.valid ) if $static_valid { _print $" (_ansi green)✓ Static validation passed(_ansi reset)" } else { _print $" (_ansi red)✗ Static validation failed(_ansi reset)" $results.overall_valid = false } $results.validations = ($results.validations | append { level: "static" valid: $static_valid details: $static_validation }) # 2. Dependency validation _print $"\n(_ansi yellow)→ Checking dependencies...(_ansi reset)" let deps_validation = (validate-dependencies $taskserv_name $settings --verbose=$verbose) if $deps_validation.valid { _print $" (_ansi green)✓ Dependencies OK(_ansi reset)" if ($deps_validation.warnings | default [] | length) > 0 { _print $" Warnings: (($deps_validation.warnings | str join ', '))" } } else { _print $" (_ansi red)✗ Dependency issues found(_ansi reset)" for err in ($deps_validation.errors | default []) { _print $" (_ansi red)✗(_ansi reset) ($err)" } $results.overall_valid = false } $results.validations = ($results.validations | append { level: "dependencies" valid: $deps_validation.valid details: $deps_validation }) # 3. Preview configuration generation _print $"\n(_ansi yellow)→ Previewing configuration generation...(_ansi reset)" let config_preview = (preview-config-generation $taskserv_name $taskserv_profile $settings $server --verbose=$verbose) if $config_preview.valid { _print $" (_ansi green)✓ Configuration preview generated(_ansi reset)" _print $" Files to process: ($config_preview.total_files)" if $verbose and ($config_preview.files | length) > 0 { _print $"\n Files to be deployed:" for file in $config_preview.files { _print $" ($file.type): ($file.source) → ($file.destination)" } } } else { _print $" (_ansi red)✗ Configuration preview failed(_ansi reset)" $results.overall_valid = false } $results.validations = ($results.validations | append { level: "configuration" valid: $config_preview.valid details: $config_preview }) # 4. Prerequisites check _print $"\n(_ansi yellow)→ Checking prerequisites...(_ansi reset)" let prereq_check = (check-prerequisites $taskserv_name $server $settings true) let mode_label = "(preview mode)" _print $" (_ansi blue)ℹ(_ansi reset) Prerequisite checks ($mode_label):" for check in $prereq_check.checks { let icon = match $check.status { "passed" => $"(_ansi green)✓(_ansi reset)" "failed" => $"(_ansi red)✗(_ansi reset)" "info" => $"(_ansi blue)ℹ(_ansi reset)" "skipped" => $"(_ansi yellow)⊘(_ansi reset)" _ => "•" } _print $" ($icon) ($check.check): ($check.message)" } $results.validations = ($results.validations | append { level: "prerequisites" valid: true details: $prereq_check }) # Summary _print $"\n(_ansi cyan_bold)Check Mode Summary(_ansi reset)" if $results.overall_valid { _print $"(_ansi green_bold)✓ All validations passed(_ansi reset)" _print $"\n💡 Taskserv can be deployed with: (_ansi cyan)provisioning taskserv create ($taskserv_name)(_ansi reset)" } else { _print $"(_ansi red_bold)✗ Validation failed(_ansi reset)" _print $"\n🛑 Fix the errors above before deploying" } return $results } # Print detailed check mode report export def print-check-report [ results: record --format: string = "text" ] { match $format { "json" => { $results | to json } "yaml" => { $results | to yaml } _ => { # Text format already printed by run-check-mode null } } } # Upload taskserv scripts to server for inspection WITHOUT executing them. # defs must include: settings, server, taskserv, ip (real), taskserv_dir, taskserv_profile export def run-upload-inspection [ defs: record --verbose (-v) ]: nothing -> record { let name = $defs.taskserv.name let check_dir = $"/tmp/prvng-check/($name)" let ip = $defs.ip let profile_path = ($defs.taskserv_dir | path join $defs.taskserv_profile) _print $"\n(_ansi cyan_bold)Upload Inspection: ($name)(_ansi reset) → (_ansi green_bold)($defs.server.hostname)(_ansi reset) [($ip)]" if not ($profile_path | path exists) { _print $" (_ansi red)✗(_ansi reset) Profile path not found: ($profile_path)" return { valid: false check_dir: $check_dir uploaded_files: [] syntax_ok: false errors: [$"Profile path not found: ($profile_path)"] } } # Enumerate local files to report let file_list = (do -i { ls $profile_path | where type == "file" | get name } | default []) # Pack profile dir into local temp tar let tar_path = $"/tmp/prvng-check-($name).tar.gz" let pack_result = (do { ^tar -C $profile_path -czf $tar_path . } | complete) if $pack_result.exit_code != 0 { _print $" (_ansi red)✗(_ansi reset) Failed to pack: ($pack_result.stderr)" return { valid: false, check_dir: $check_dir, uploaded_files: [], syntax_ok: false, errors: ["Pack failed"] } } # SSH: create inspection directory if not (ssh_cmd $defs.settings $defs.server false $"mkdir -p ($check_dir)" $ip) { rm -f $tar_path _print $" (_ansi red)✗(_ansi reset) SSH connection failed — cannot create ($check_dir)" return { valid: false, check_dir: $check_dir, uploaded_files: [], syntax_ok: false, errors: ["SSH mkdir failed"] } } # SCP: upload tar to /tmp on server if not (scp_to $defs.settings $defs.server [$tar_path] "/tmp" $ip) { rm -f $tar_path _print $" (_ansi red)✗(_ansi reset) SCP upload failed" return { valid: false, check_dir: $check_dir, uploaded_files: [], syntax_ok: false, errors: ["SCP failed"] } } rm -f $tar_path # SSH: extract bundle into check_dir — no execute let extract_cmd = $"cd ($check_dir) && tar -xzf /tmp/prvng-check-($name).tar.gz && rm -f /tmp/prvng-check-($name).tar.gz" if not (ssh_cmd $defs.settings $defs.server false $extract_cmd $ip) { _print $" (_ansi red)✗(_ansi reset) Extraction on server failed" return { valid: false, check_dir: $check_dir, uploaded_files: ($file_list | each { |f| $f | path basename }), syntax_ok: false, errors: ["Extract failed"] } } # SSH: bash -n syntax check on all uploaded .sh files (no execution) let syntax_cmd = $"find ($check_dir) -name '*.sh' -exec bash -n \\{\\} \\;" let syntax_ok = (ssh_cmd $defs.settings $defs.server false $syntax_cmd $ip) let basenames = ($file_list | each { |f| $f | path basename }) if $verbose { _print $" Files uploaded from ($profile_path):" for f in $basenames { _print $" ($f)" } } let syntax_label = if $syntax_ok { $"(_ansi green)✓(_ansi reset) bash -n syntax OK" } else { $"(_ansi red)✗(_ansi reset) Syntax errors found — see SSH output above" } _print $" (_ansi green)✓(_ansi reset) Uploaded to (_ansi cyan)($check_dir)(_ansi reset) — not executed" _print $" ($syntax_label)" _print $" Inspect : (_ansi blue)ssh ($defs.server.installer_user)@($ip) ls -la ($check_dir)/(_ansi reset)" _print $" Cleanup : (_ansi blue)ssh ($defs.server.installer_user)@($ip) rm -rf ($check_dir)(_ansi reset)" { valid: $syntax_ok check_dir: $check_dir server: $defs.server.hostname ip: $ip syntax_ok: $syntax_ok uploaded_files: $basenames errors: (if $syntax_ok { [] } else { ["Script syntax errors detected remotely"] }) } }