Files
dockhand-upstream/src/routes/api/containers/[id]/inspect/+server.ts
T
2026-06-06 16:18:05 +02:00

74 lines
2.8 KiB
TypeScript

import { json } from '@sveltejs/kit';
import type { RequestHandler } from './$types';
import { inspectContainer, inspectImage } from '$lib/server/docker';
import { getSecretKeysToMask } from '$lib/server/db';
import { getStackComposeFile } from '$lib/server/stacks';
import { authorize } from '$lib/server/authorize';
import { validateDockerIdParam } from '$lib/server/docker-validation';
import {
detectImageEnvDivergence,
detectImageLabelDivergence
} from '$lib/server/container-image-divergence';
export const GET: RequestHandler = async ({ params, url, cookies }) => {
const invalid = validateDockerIdParam(params.id, 'container');
if (invalid) return invalid;
const auth = await authorize(cookies);
const envId = url.searchParams.get('env');
const envIdNum = envId ? parseInt(envId) : undefined;
// Permission check with environment context
if (auth.authEnabled && !await auth.can('containers', 'inspect', envIdNum)) {
return json({ error: 'Permission denied' }, { status: 403 });
}
try {
const containerData = await inspectContainer(params.id, envIdNum);
// Compute env/label divergence BEFORE masking, so the comparison
// uses real values. Failure to inspect the image is non-fatal —
// the field is omitted in that case.
let divergence: { env: string[]; labels: string[] } | undefined;
try {
const imageRef = containerData.Config?.Image;
if (imageRef) {
const imageData: any = await inspectImage(imageRef, envIdNum);
const imageEnv: string[] = imageData?.Config?.Env || [];
const imageLabels: Record<string, string> = imageData?.Config?.Labels || {};
divergence = {
env: detectImageEnvDivergence(containerData.Config?.Env || [], imageEnv),
labels: detectImageLabelDivergence(containerData.Config?.Labels, imageLabels)
};
}
} catch {
// image not present / not pullable / etc — drop the field
}
// Mask secret env vars for containers belonging to a Compose stack.
// Uses compose file parsing to detect interpolation (e.g., MYSQL_PASSWORD=${db_secret}).
const stackName = containerData.Config?.Labels?.['com.docker.compose.project'];
if (stackName && Array.isArray(containerData.Config?.Env)) {
const composeResult = await getStackComposeFile(stackName, envIdNum).catch(() => null);
const secretKeys = await getSecretKeysToMask(stackName, envIdNum, composeResult?.content);
if (secretKeys.size > 0) {
containerData.Config.Env = containerData.Config.Env.map((entry: string) => {
const eqIdx = entry.indexOf('=');
if (eqIdx === -1) return entry;
const key = entry.substring(0, eqIdx);
if (secretKeys.has(key)) {
return `${key}=***`;
}
return entry;
});
}
}
return json({ ...containerData, divergence });
} catch (error) {
console.error('Failed to inspect container:', error);
return json({ error: 'Failed to inspect container' }, { status: 500 });
}
};