import { existsSync } from 'node:fs'; import { join } from 'node:path'; import { AnsibleInventorySchema, parseJsonWithValidation } from '../validation/schemas'; /** * Validation result structure */ export interface ValidationResult { success: boolean; stdout?: string; stderr?: string; error?: string; details?: unknown; } /** * Check if ansible-lint is available * * Policy function (Rule 10.1) - validation only * * @returns True if ansible-lint is installed */ export function isAnsibleLintAvailable(): boolean { const result = Bun.spawnSync(['which', 'ansible-lint'], { stdout: 'pipe', stderr: 'pipe', }); return result.exitCode === 0; } /** * Check if ansible-playbook is available * * Policy function - validation only * * @returns True if ansible-playbook is installed */ export function isAnsiblePlaybookAvailable(): boolean { const result = Bun.spawnSync(['which', 'ansible-playbook'], { stdout: 'pipe', stderr: 'pipe', }); return result.exitCode === 0; } /** * Check if ansible-inventory is available * * Policy function - validation only * * @returns True if ansible-inventory is installed */ export function isAnsibleInventoryAvailable(): boolean { const result = Bun.spawnSync(['which', 'ansible-inventory'], { stdout: 'pipe', stderr: 'pipe', }); return result.exitCode === 0; } /** * Validate generated Ansible with ansible-lint * * Execution function (Rule 10.1) - spawns external process * * Fails on BOTH warnings and errors (strict mode) * * @param ansiblePath - Path to ansible directory containing playbook * @param vaultPasswordFile - Optional path to vault password file * @returns Validation result */ export async function validateWithAnsibleLint( ansiblePath: string, vaultPasswordFile?: string, ): Promise { // Check if ansible-lint is available if (!isAnsibleLintAvailable()) { return { success: false, error: 'ansible-lint command not found. Please install ansible-lint: pip install ansible-lint', }; } // Check if path exists if (!existsSync(ansiblePath)) { return { success: false, error: `Ansible path does not exist: ${ansiblePath}`, }; } // Check if playbook exists const playbookPath = join(ansiblePath, 'playbook.yml'); if (!existsSync(playbookPath)) { return { success: false, error: `Playbook not found: ${playbookPath}`, }; } // Run ansible-lint // Note: ansible-lint exits with non-zero on warnings AND errors const args = ['ansible-lint', playbookPath]; const env = { ...process.env }; // If vault password file provided, set environment variable for ansible-vault if (vaultPasswordFile) { env.ANSIBLE_VAULT_PASSWORD_FILE = vaultPasswordFile; } const result = Bun.spawnSync(args, { stdout: 'pipe', stderr: 'pipe', cwd: ansiblePath, env, }); const stdout = result.stdout ? new TextDecoder().decode(result.stdout) : ''; const stderr = result.stderr ? new TextDecoder().decode(result.stderr) : ''; if (result.exitCode !== 0) { return { success: false, error: `ansible-lint found issues (exit code ${result.exitCode})`, stdout, stderr, }; } return { success: true, stdout, }; } /** * Validate Ansible playbook syntax * * Execution function - spawns external process * * @param playbookPath - Path to playbook.yml * @param inventoryPath - Path to inventory directory or file * @param vaultPasswordFile - Optional path to vault password file * @returns Validation result */ export async function validatePlaybookSyntax( playbookPath: string, inventoryPath: string, vaultPasswordFile?: string, ): Promise { // Check if ansible-playbook is available if (!isAnsiblePlaybookAvailable()) { return { success: false, error: 'ansible-playbook command not found. Please install Ansible: https://docs.ansible.com/ansible/latest/installation_guide/intro_installation.html', }; } // Check if files exist if (!existsSync(playbookPath)) { return { success: false, error: `Playbook not found: ${playbookPath}`, }; } if (!existsSync(inventoryPath)) { return { success: false, error: `Inventory not found: ${inventoryPath}`, }; } // Run ansible-playbook --syntax-check const env = { ...process.env }; // If vault password file provided, set environment variable for ansible-vault if (vaultPasswordFile) { env.ANSIBLE_VAULT_PASSWORD_FILE = vaultPasswordFile; } const result = Bun.spawnSync( ['ansible-playbook', '--syntax-check', '-i', inventoryPath, playbookPath], { stdout: 'pipe', stderr: 'pipe', env, }, ); const stdout = result.stdout ? new TextDecoder().decode(result.stdout) : ''; const stderr = result.stderr ? new TextDecoder().decode(result.stderr) : ''; if (result.exitCode !== 0) { return { success: false, error: `Syntax check failed (exit code ${result.exitCode})`, stdout, stderr, }; } return { success: true, stdout, }; } /** * Validate Ansible inventory structure * * Execution function - spawns external process * * @param inventoryPath - Path to inventory directory or file * @returns Validation result */ export async function validateInventory(inventoryPath: string): Promise { // Check if ansible-inventory is available if (!isAnsibleInventoryAvailable()) { return { success: false, error: 'ansible-inventory command not found. Please install Ansible.', }; } // Check if inventory exists if (!existsSync(inventoryPath)) { return { success: false, error: `Inventory not found: ${inventoryPath}`, }; } // Run ansible-inventory --list const result = Bun.spawnSync(['ansible-inventory', '-i', inventoryPath, '--list'], { stdout: 'pipe', stderr: 'pipe', }); const stdout = result.stdout ? new TextDecoder().decode(result.stdout) : ''; const stderr = result.stderr ? new TextDecoder().decode(result.stderr) : ''; if (result.exitCode !== 0) { return { success: false, error: `Inventory parsing failed (exit code ${result.exitCode})`, stdout, stderr, }; } // Try to parse and validate JSON output try { const inventory = parseJsonWithValidation(stdout, AnsibleInventorySchema, 'Ansible inventory'); return { success: true, stdout, details: inventory, }; } catch (error) { return { success: false, error: 'Failed to parse inventory JSON output', stdout, stderr, details: error, }; } }