Compose validate - a preflight linter & validator for compose files

This commit is contained in:
jarek committed 2026-08-19 14:26:45 +02:00
1 parent bc91e71075
commit 2584e52852
6 files changed
+62 -19

No files matched your search

@@ -10,24 +10,30 @@ import type { ValidateContext } from './types';
export interface CtxContainer {
name: string;
ports?: Array<{ PublicPort?: number }>;
labels?: Record<string, string>;
}
/**
* Given the env's containers/networks/volumes, produce the ValidateContext. Excludes
* THIS stack's own containers so a re-deploy isn't flagged as a self-collision (compose
* names its containers `<project>-<svc>-<n>`).
* THIS stack's own containers so a re-deploy isn't flagged as a self-collision. Match on
* the compose project LABEL first (authoritative - Docker stamps it on every compose
* container regardless of a custom container_name), then fall back to the default
* `<project>-<svc>-<n>` name prefix for containers that lack the label.
*/
export function deriveContextFromLists(
containers: CtxContainer[],
networkNames: string[],
volumeNames: string[],
selfStackName?: string
selfStackName?: string | null
): ValidateContext {
const usedHostPorts = new Set<number>();
const hostPortOwners = new Map<number, string>();
const existingContainerNames = new Set<string>();
for (const c of containers) {
const isSelf = !!selfStackName && c.name.startsWith(`${selfStackName}-`);
const isSelf =
!!selfStackName &&
(c.labels?.['com.docker.compose.project'] === selfStackName ||
c.name.startsWith(`${selfStackName}-`));
if (isSelf) continue;
existingContainerNames.add(c.name);
for (const p of c.ports || []) {
@@ -39,7 +45,7 @@ export function deriveContextFromLists(
}
}
return {
selfStackName,
selfStackName: selfStackName ?? undefined,
usedHostPorts,
hostPortOwners,
existingContainerNames,
+1 -1
View File
@@ -18,7 +18,7 @@ export { deriveContextFromLists } from './context-core';
*/
export async function buildValidateContext(
envId: number | null | undefined,
selfStackName?: string
selfStackName?: string | null
): Promise<ValidateContext> {
const [containers, networks, volumes] = await Promise.all([
listContainers(true, envId).catch(() => []),
@@ -48,7 +48,8 @@ export interface EffectiveComposeResult {
export async function renderEffectiveCompose(
stackName: string,
composeContent: string,
dockerHost?: string | null
dockerHost?: string | null,
envVars?: Record<string, string>
): Promise<EffectiveComposeResult> {
let dir: string | undefined;
try {
@@ -65,7 +66,7 @@ export async function renderEffectiveCompose(
// about - far better than the raw ${VAR} source.
const args = [...base.slice(1), '-f', file, 'config', '--format', 'json'];
const { code, stdout, stderr } = await execDocker(base[0], args, dir);
const { code, stdout, stderr } = await execDocker(base[0], args, dir, envVars);
if (code === 0) {
let effective: Record<string, unknown> | undefined;
@@ -135,7 +136,8 @@ export function parseConfigErrors(stderr: string): Finding[] {
function execDocker(
exe: string,
args: string[],
cwd: string
cwd: string,
envVars?: Record<string, string>
): Promise<{ code: number; stdout: string; stderr: string }> {
return new Promise((resolve, reject) => {
const outChunks: Buffer[] = [];
@@ -149,7 +151,10 @@ function execDocker(
cwd,
shell: false,
stdio: ['ignore', 'pipe', 'pipe'],
env: { PATH: process.env.PATH || '/usr/local/bin:/usr/bin:/bin' }
// The stack's env vars (incl. secrets) are injected so `${VAR}` interpolation
// resolves the same way it will at deploy time - otherwise config reports a
// spurious "VAR is not set" for every variable Dockhand supplies via shell env.
env: { PATH: process.env.PATH || '/usr/local/bin:/usr/bin:/bin', ...(envVars ?? {}) }
});
const timeout = setTimeout(() => {
+3 -2
View File
@@ -100,11 +100,12 @@ export async function runValidateWithConfig(
composeSource: string,
ctx: ValidateContext = {},
dockerHost?: string | null,
config: ValidateConfig = {}
config: ValidateConfig = {},
envVars?: Record<string, string>
): Promise<ValidateReport> {
const local = runValidate(composeSource, ctx, config);
const cfg = await renderEffectiveCompose(stackName, composeSource, dockerHost);
const cfg = await renderEffectiveCompose(stackName, composeSource, dockerHost, envVars);
// If config produced schema errors, merge them in. If the file already failed to
// parse locally, prefer config's precise schema message but keep both.