import type { Machine } from '@/types/infrastructure'; import { z } from 'zod'; /** * Zod schema for Terraform output values * Terraform can return plain values or wrapped in { value, type, sensitive } * Validates external data from terraform output -json (Rule 3.7) */ const TerraformOutputValueSchema = z.union([ z.string(), z.number(), z.object({ value: z.union([z.string(), z.number()]), type: z.string().optional(), sensitive: z.boolean().optional(), }), ]); /** * Zod schema for Digital Ocean Terraform outputs * Expects droplet_ip and droplet_id outputs from Terraform */ const DigitalOceanOutputSchema = z.object({ droplet_ip: TerraformOutputValueSchema, droplet_id: TerraformOutputValueSchema, }); /** * Extract infrastructure properties from a machine. * Translates from TypeScript camelCase to dot notation user-facing format. * * @param machine - Machine record from database * @returns Infrastructure properties with dot notation keys */ export function extractMachineProperties(machine: Machine): Record { return { 'ip.primary': machine.ipAddress, hostname: machine.hostname, id: machine.id, }; } /** * Proxmox provider configuration shape */ export interface ProxmoxProviderConfig { default_target_node: string; lxc_template: string; storage: string; /** * Cloud-init VM template to clone for `requires.system.type: vm` modules — the * VM analogue of `lxc_template`. Optional: only Proxmox services that host VM * modules configure it. A VM module declares `vm_template` as a *required* * infrastructure var, so a service missing it fails loudly at resolution. */ vm_template?: string; } /** * Extract infrastructure properties from IPAM allocation (Proxmox). * Used when container service allocates IP via IPAM before Terraform runs. * * @param vmid - Proxmox VMID allocated by IPAM * @param containerIp - Container IP allocated by IPAM * @param hostname - Container hostname * @param providerConfig - Proxmox provider configuration from container service * @returns Infrastructure properties with dot notation keys */ export function extractProxmoxProperties( vmid: number, containerIp: string, hostname: string, providerConfig: ProxmoxProviderConfig, ): Record { return { 'ip.primary': containerIp, hostname: hostname, id: vmid.toString(), target_node: providerConfig.default_target_node, lxc_template: providerConfig.lxc_template, storage: providerConfig.storage, // VM clone source — present only when the service configures it. VM modules // declare `vm_template` as a required infrastructure var (resolution errors // if absent); LXC modules never reference it. Omitted (not empty) when unset // so the resolver's required/optional handling stays correct. ...(providerConfig.vm_template ? { vm_template: providerConfig.vm_template } : {}), }; } /** * Extract infrastructure properties from Terraform outputs (Digital Ocean). * Parses Terraform's JSON output format which can wrap values in objects. * Validates output structure before extraction (Rule 3.7). * * @param outputs - Terraform output JSON (from `terraform output -json`) * @param hostname - Container hostname * @returns Infrastructure properties with dot notation keys */ export function extractTerraformProperties( outputs: Record, hostname: string, ): Record { // Validate structure matches Digital Ocean expectations let validated: z.infer; try { validated = DigitalOceanOutputSchema.parse(outputs); } catch (error) { if (error instanceof z.ZodError) { // Check for missing top-level fields (completely absent from outputs) const topLevelErrors = error.errors.filter((e) => e.path.length === 1); const hasUndefinedFields = topLevelErrors.some((e) => { if (e.code === 'invalid_union') { // For union errors, check if the field is actually undefined const fieldName = e.path[0] as string; return !(fieldName in outputs); // Field is truly missing } return e.code === 'invalid_type' && e.received === 'undefined'; }); if (hasUndefinedFields) { throw new Error( 'Missing required Terraform outputs: droplet_ip and droplet_id must be defined', ); } // Invalid format (field exists but doesn't match expected types) throw new Error(`Invalid Terraform output format: ${JSON.stringify(outputs)}`); } throw error; } // Terraform output format can be: // { "droplet_ip": "203.0.113.42" } OR // { "droplet_ip": { "value": "203.0.113.42", "type": "string", "sensitive": false } } const unwrapOutput = (value: unknown): string => { if (typeof value === 'string') { return value; } if (typeof value === 'number') { return value.toString(); } if (typeof value === 'object' && value !== null && 'value' in value) { const wrapped = value as { value: unknown }; if (typeof wrapped.value === 'string') { return wrapped.value; } if (typeof wrapped.value === 'number') { return wrapped.value.toString(); } } throw new Error(`Invalid Terraform output format: ${JSON.stringify(value)}`); }; const dropletIp = unwrapOutput(validated.droplet_ip); const dropletId = unwrapOutput(validated.droplet_id); return { 'ip.primary': dropletIp, hostname: hostname, id: dropletId, }; }