import { json } from '@sveltejs/kit'; import { getStackEnvVars } from '$lib/server/db'; import { getStackComposeFile } from '$lib/server/stacks'; import { authorize } from '$lib/server/authorize'; import type { RequestHandler } from './$types'; interface ValidationResult { valid: boolean; required: string[]; optional: string[]; defined: string[]; missing: string[]; unused: string[]; } /** * Extract environment variables from compose YAML content. * Matches ${VAR_NAME} and ${VAR_NAME:-default} patterns. * Returns { required: [...], optional: [...] } */ function extractComposeVars(yaml: string): { required: string[]; optional: string[] } { const required: string[] = []; const optional: string[] = []; // Match ${VAR_NAME} (required) and ${VAR_NAME:-default} or ${VAR_NAME-default} (optional) const regex = /\$\{([A-Za-z_][A-Za-z0-9_]*)(?:(:-?)[^}]*)?\}/g; let match; while ((match = regex.exec(yaml)) !== null) { const varName = match[1]; const hasDefault = match[2] !== undefined; if (hasDefault) { if (!optional.includes(varName) && !required.includes(varName)) { optional.push(varName); } } else { // Move from optional to required if we find a non-default usage const optIdx = optional.indexOf(varName); if (optIdx !== -1) { optional.splice(optIdx, 1); } if (!required.includes(varName)) { required.push(varName); } } } // Also match $VAR_NAME (simple variable substitution) const simpleRegex = /\$([A-Za-z_][A-Za-z0-9_]*)(?![{A-Za-z0-9_])/g; while ((match = simpleRegex.exec(yaml)) !== null) { const varName = match[1]; if (!required.includes(varName) && !optional.includes(varName)) { required.push(varName); } } return { required: required.sort(), optional: optional.sort() }; } /** * POST /api/stacks/[name]/env/validate?env=X * Validate stack environment variables against compose file requirements. * Can use saved compose file or accept compose content in body. * Body (optional): { compose: "yaml content..." } */ export const POST: RequestHandler = async ({ params, url, cookies, request }) => { const auth = await authorize(cookies); const envId = url.searchParams.get('env'); const envIdNum = envId ? parseInt(envId) : null; // Permission check with environment context if (auth.authEnabled && !await auth.can('stacks', 'view', envIdNum ?? undefined)) { return json({ error: 'Permission denied' }, { status: 403 }); } // Environment access check (enterprise only) if (envIdNum && auth.isEnterprise && !await auth.canAccessEnvironment(envIdNum)) { return json({ error: 'Access denied to this environment' }, { status: 403 }); } try { const stackName = decodeURIComponent(params.name); let composeContent: string | null = null; let providedVariables: string[] | null = null; // Check if compose content and/or variables are provided in body const contentType = request.headers.get('content-type'); if (contentType?.includes('application/json')) { try { const body = await request.json(); if (body.compose && typeof body.compose === 'string') { composeContent = body.compose; } // Accept variables from UI for validation (overrides DB lookup) if (Array.isArray(body.variables)) { providedVariables = body.variables.filter((v: unknown) => typeof v === 'string'); } } catch { // Ignore JSON parse errors - will try to load from file } } // If no compose in body, try to load from saved file if (!composeContent) { const savedCompose = await getStackComposeFile(stackName); if (savedCompose.success && savedCompose.content) { composeContent = savedCompose.content; } } if (!composeContent) { return json({ error: 'No compose content provided and no saved compose file found' }, { status: 400 }); } // Extract variables from compose const { required, optional } = extractComposeVars(composeContent); // Get defined variables - either from request body or database let defined: string[]; if (providedVariables !== null) { // Use provided variables from UI defined = providedVariables.sort(); } else { // Fall back to database const envVars = await getStackEnvVars(stackName, envIdNum, false); defined = envVars.map(v => v.key).sort(); } // Calculate missing and unused const missing = required.filter(v => !defined.includes(v)); const unused = defined.filter(v => !required.includes(v) && !optional.includes(v)); const result: ValidationResult = { valid: missing.length === 0, required, optional, defined, missing, unused }; return json(result); } catch (error) { console.error('Error validating stack env vars:', error); return json({ error: 'Failed to validate environment variables' }, { status: 500 }); } };