/** * Value Extraction Testing * * Extracts values from generated Terraform and Ansible files to verify * that variable substitution happened correctly. */ import { readFile } from 'node:fs/promises'; import * as yaml from 'yaml'; export interface ExtractedValues { terraform: { vmid?: number; hostname?: string; ip?: string; gateway?: string; vlan?: number; cores?: number; memory?: number; storage?: string; rootfs_size?: string; }; ansible: { dns_primary?: string; dns_fallback?: string; gateway?: string; }; } export interface ValueExtractionResult { pass: boolean; extracted: ExtractedValues; expected: ExtractedValues; mismatches: Array<{ path: string; expected: unknown; actual: unknown; }>; } /** * Extract values from Terraform main.tf file * * @param terraformPath - Path to main.tf file * @returns Extracted Terraform values */ export async function extractTerraformValues( terraformPath: string, ): Promise { const content = await readFile(terraformPath, 'utf-8'); const values: ExtractedValues['terraform'] = {}; // Extract vmid (integer) const vmidMatch = content.match(/vmid\s*=\s*(\d+)/); if (vmidMatch) { values.vmid = Number.parseInt(vmidMatch[1] || '0', 10); } // Extract hostname (string) const hostnameMatch = content.match(/hostname\s*=\s*"([^"]+)"/); if (hostnameMatch) { values.hostname = hostnameMatch[1]; } // Extract IP address from network block const ipMatch = content.match(/ip\s*=\s*"([^"]+)"/); if (ipMatch) { values.ip = ipMatch[1]; } // Extract gateway from network block const gwMatch = content.match(/gw\s*=\s*"([^"]+)"/); if (gwMatch) { values.gateway = gwMatch[1]; } // Extract VLAN tag (integer) const tagMatch = content.match(/tag\s*=\s*(\d+)/); if (tagMatch) { values.vlan = Number.parseInt(tagMatch[1] || '0', 10); } // Extract cores (integer) const coresMatch = content.match(/cores\s*=\s*(\d+)/); if (coresMatch) { values.cores = Number.parseInt(coresMatch[1] || '0', 10); } // Extract memory (integer) const memoryMatch = content.match(/memory\s*=\s*(\d+)/); if (memoryMatch) { values.memory = Number.parseInt(memoryMatch[1] || '0', 10); } // Extract storage from rootfs block const storageMatch = content.match(/rootfs\s*{[^}]*storage\s*=\s*"([^"]+)"/s); if (storageMatch) { values.storage = storageMatch[1]; } // Extract rootfs size const sizeMatch = content.match(/rootfs\s*{[^}]*size\s*=\s*"([^"]+)"/s); if (sizeMatch) { values.rootfs_size = sizeMatch[1]; } return values; } /** * Extract values from Ansible inventory files * * Now that templates use Ansible Jinja2 syntax, actual values are stored * in inventory YAML files, not hardcoded in templates. * * @param ansibleDir - Path to ansible directory (containing inventory/) * @returns Extracted Ansible values */ export async function extractAnsibleValues( ansibleDir: string, ): Promise { const values: ExtractedValues['ansible'] = {}; try { // Read group_vars/all.yml which contains system config const groupVarsPath = `${ansibleDir}/inventory/group_vars/all.yml`; const groupVarsContent = await readFile(groupVarsPath, 'utf-8'); const groupVars = yaml.parse(groupVarsContent) as Record; // Extract system config values if (typeof groupVars.dns_primary === 'string') { values.dns_primary = groupVars.dns_primary; } if (typeof groupVars.dns_fallback === 'string') { values.dns_fallback = groupVars.dns_fallback; } if (typeof groupVars.routing_internal_gateway === 'string') { values.gateway = groupVars.routing_internal_gateway; } } catch (error) { // If inventory files don't exist, return empty values console.error('Failed to extract Ansible values from inventory:', error); } return values; } /** * Compare extracted values against expected test values * * @param generatedDir - Path to generated output directory * @param expectedValues - Expected values from test-values.yml * @returns Comparison result */ export async function compareExtractedValues( generatedDir: string, expectedValues: { terraform: ExtractedValues['terraform']; ansible: ExtractedValues['ansible']; }, ): Promise { const mismatches: ValueExtractionResult['mismatches'] = []; // Extract Terraform values const terraformPath = `${generatedDir}/terraform/main.tf`; const extractedTerraform = await extractTerraformValues(terraformPath); // Extract Ansible values from inventory const ansibleDir = `${generatedDir}/ansible`; const extractedAnsible = await extractAnsibleValues(ansibleDir); // Compare Terraform values for (const [key, expectedValue] of Object.entries(expectedValues.terraform)) { const actualValue = extractedTerraform[key as keyof typeof extractedTerraform]; if (actualValue !== expectedValue) { mismatches.push({ path: `terraform.${key}`, expected: expectedValue, actual: actualValue, }); } } // Compare Ansible values for (const [key, expectedValue] of Object.entries(expectedValues.ansible)) { const actualValue = extractedAnsible[key as keyof typeof extractedAnsible]; if (actualValue !== expectedValue) { mismatches.push({ path: `ansible.${key}`, expected: expectedValue, actual: actualValue, }); } } return { pass: mismatches.length === 0, extracted: { terraform: extractedTerraform, ansible: extractedAnsible, }, expected: expectedValues, mismatches, }; } /** * Format value extraction mismatches for display * * @param result - Value extraction result * @returns Formatted string */ export function formatValueMismatches(result: ValueExtractionResult): string { if (result.pass) { return 'All values match expected configuration'; } const lines: string[] = []; for (const mismatch of result.mismatches) { lines.push(`❌ ${mismatch.path}`); lines.push(` Expected: ${JSON.stringify(mismatch.expected)}`); lines.push(` Actual: ${JSON.stringify(mismatch.actual)}`); lines.push(''); } return lines.join('\n'); }