78 lines
4.2 KiB
TypeScript
78 lines
4.2 KiB
TypeScript
import { json } from '@sveltejs/kit';
|
|
import { authorize } from '$lib/server/authorize';
|
|
import { runValidateWithConfig } from '$lib/server/compose-validate';
|
|
import { buildValidateContext } from '$lib/server/compose-validate/context';
|
|
import { getEnvironment } from '$lib/server/db';
|
|
import type { ValidateConfig } from '$lib/server/compose-validate/types';
|
|
import type { RequestHandler } from './$types';
|
|
|
|
/**
|
|
* The daemon `docker compose config` should validate against. `config` renders the
|
|
* effective compose locally (it does NOT dial the daemon), so this only matters for
|
|
* consistency: local/socket -> default socket; direct TCP -> that host. Hawser/edge
|
|
* agents are remote, so we skip config for them (null) and fall back to local rules.
|
|
*/
|
|
async function resolveDockerHostForConfig(envIdNum: number | undefined): Promise<string | null> {
|
|
if (!envIdNum) return null; // no env -> local socket
|
|
const env = await getEnvironment(envIdNum).catch(() => undefined);
|
|
if (!env) return null;
|
|
const type = env.connectionType || 'socket';
|
|
if (type === 'socket') return null;
|
|
if (type === 'direct' && env.host) {
|
|
const proto = env.protocol === 'https' ? 'https' : 'tcp';
|
|
return `${proto}://${env.host}:${env.port || 2375}`;
|
|
}
|
|
return null; // hawser-standard / hawser-edge: no local TCP host, config renders locally anyway
|
|
}
|
|
|
|
/**
|
|
* Validate a compose file (Compose Validate): a preflight linter that surfaces
|
|
* problems - port collisions, exposure, typos, undefined network/volume refs, latest
|
|
* tags, docker.sock mounts - before `docker compose up`, with the line of each finding
|
|
* for editor markers. Context-aware rules (cross-stack port collisions, missing
|
|
* external networks/volumes, container-name collisions) use the target environment's
|
|
* live containers/networks/volumes.
|
|
*
|
|
* @openapi
|
|
* summary: Run the Compose Validate preflight linter on a compose file and return findings with severities and line numbers (requires the 'view' permission)
|
|
* path: name:string! Stack name (used to exclude the stack's own containers from cross-stack collision checks)
|
|
* query: env:integer Environment ID for the context-aware checks (from GET /api/environments)
|
|
* body: {compose:string!, config:{disabled:[string], severity:object}}
|
|
* body-example: {"compose":"services:\n web:\n image: nginx:latest\n ports: [\"8080:80\"]\n"}
|
|
* resp-200: {findings:[{ruleId:string!, severity:string!, message:string!, hint:string, service:string, line:integer, fix:object, fixDescription:string}]!, counts:{error:integer!, warn:integer!, info:integer!}!}
|
|
* resp-200-example: {"findings":[{"ruleId":"LATEST_TAG","severity":"warn","message":"\"web\" uses `:latest` (nginx:latest)","hint":"Pin a version tag for reproducible deploys and to enable newer-version detection.","service":"web","line":3}],"counts":{"error":0,"warn":1,"info":0}}
|
|
* resp-400: compose content is required
|
|
* resp-403: Permission denied
|
|
*/
|
|
export const POST: RequestHandler = async ({ cookies, url, params, request }) => {
|
|
const auth = await authorize(cookies);
|
|
const envId = url.searchParams.get('env');
|
|
const envIdNum = envId ? parseInt(envId, 10) : undefined;
|
|
|
|
if (auth.authEnabled && !(await auth.can('stacks', 'view', envIdNum))) {
|
|
return json({ error: 'Permission denied' }, { status: 403 });
|
|
}
|
|
|
|
const body = (await request.json().catch(() => ({}))) as {
|
|
compose?: string;
|
|
config?: ValidateConfig;
|
|
};
|
|
const compose = typeof body.compose === 'string' ? body.compose : '';
|
|
if (!compose.trim()) {
|
|
return json({ error: 'compose content is required' }, { status: 400 });
|
|
}
|
|
|
|
const stackName = decodeURIComponent(params.name);
|
|
|
|
// Best-effort live context; degrades to an empty context if the env is unreachable
|
|
// (context-aware rules then simply produce nothing - the linter still runs).
|
|
const ctx = await buildValidateContext(envIdNum ?? null, stackName).catch(() => ({}));
|
|
|
|
// `docker compose config` gives the authoritative schema/syntax verdict; the local
|
|
// rule catalog adds the context-aware checks config can't do. Config degrades to a
|
|
// pure local lint if the daemon/CLI is unreachable, so this never blocks.
|
|
const dockerHost = await resolveDockerHostForConfig(envIdNum);
|
|
const report = await runValidateWithConfig(stackName, compose, ctx, dockerHost, body.config ?? {});
|
|
return json(report);
|
|
};
|