This commit is contained in:
jarek committed 2026-03-02 10:41:42 +01:00
1 parent bc00bbfe5c
commit 1066ce9eb1
8 files changed
+175 -120

No files matched your search

+1 -36
View File
@@ -14,6 +14,7 @@ import { createHash } from 'node:crypto';
import type { Environment } from './db';
import { getStackEnvVarsAsRecord } from './db';
import { isSystemContainer } from './scheduler/tasks/update-utils';
import { deepDiff } from '../utils/diff.js';
/**
* Custom error for when an environment is not found.
@@ -1664,42 +1665,6 @@ export async function createContainer(options: CreateContainerOptions, envId?: n
return { id: result.Id, start: () => startContainer(result.Id, envId) };
}
/**
* Deep-diff two objects recursively, returning all paths that differ.
*/
export function deepDiff(a: any, b: any, path = ''): string[] {
const diffs: string[] = [];
if (a === b) return diffs;
if (a === null || b === null || typeof a !== typeof b) {
diffs.push(`${path}: ${JSON.stringify(a)} → ${JSON.stringify(b)}`);
return diffs;
}
if (typeof a !== 'object') {
if (a !== b) diffs.push(`${path}: ${JSON.stringify(a)} → ${JSON.stringify(b)}`);
return diffs;
}
if (Array.isArray(a) || Array.isArray(b)) {
const aStr = JSON.stringify(a);
const bStr = JSON.stringify(b);
if (aStr !== bStr) diffs.push(`${path}: ${aStr} → ${bStr}`);
return diffs;
}
const allKeys = Array.from(new Set([...Object.keys(a), ...Object.keys(b)]));
for (const key of allKeys) {
const childPath = path ? `${path}.${key}` : key;
if (!(key in a)) {
diffs.push(`${childPath}: <missing> → ${JSON.stringify(b[key])}`);
} else if (!(key in b)) {
diffs.push(`${childPath}: ${JSON.stringify(a[key])} → <missing>`);
} else {
diffs.push(...deepDiff(a[key], b[key], childPath));
}
}
return diffs;
}
/**
* Recreate a container using full Config/HostConfig passthrough from inspect data.
+32
View File
@@ -138,6 +138,10 @@ const stackLocks = new Map<string, Promise<void>>();
// Track active TLS temp directories for cleanup on unexpected process exit
const activeTlsDirs = new Set<string>();
// Cache of envId → daemon max API version (e.g. "1.43")
// Populated lazily to avoid CLI/daemon version mismatch on older Docker hosts (e.g. Synology)
const dockerApiVersionCache = new Map<string, string>();
// Register cleanup handlers once at module load
if (typeof process !== 'undefined') {
const cleanupTlsDirs = () => {
@@ -153,6 +157,25 @@ if (typeof process !== 'undefined') {
process.on('SIGTERM', () => { cleanupTlsDirs(); process.exit(143); });
}
/**
* Fetch and cache the Docker daemon's maximum supported API version for a given environment.
* Used to set DOCKER_API_VERSION when spawning docker compose, preventing version mismatch
* errors on older Docker hosts (e.g. Synology DSM).
*/
async function getDockerApiVersionForCli(envId: number | null | undefined): Promise<string | undefined> {
const key = String(envId ?? 'local');
if (dockerApiVersionCache.has(key)) return dockerApiVersionCache.get(key);
try {
const { getDockerVersion } = await import('./docker.js');
const version = await getDockerVersion(envId);
const apiVersion: string | undefined = version?.ApiVersion;
if (apiVersion) dockerApiVersionCache.set(key, apiVersion);
return apiVersion;
} catch {
return undefined;
}
}
/**
* Execute a function with exclusive lock on a stack.
* Prevents race conditions when multiple operations target the same stack.
@@ -909,6 +932,15 @@ async function executeLocalCompose(
spawnEnv.DOCKER_HOST = process.env.DOCKER_HOST;
}
// Auto-cap Docker CLI API version to the daemon's max supported version.
// This fixes compatibility with older Docker daemons (e.g. Synology DSM) that
// reject newer client versions. DOCKER_API_VERSION env var overrides this if set.
const daemonApiVersion = process.env.DOCKER_API_VERSION
?? await getDockerApiVersionForCli(envId);
if (daemonApiVersion) {
spawnEnv.DOCKER_API_VERSION = daemonApiVersion;
}
// Check if .env file exists on disk (for legacy support decision)
const defaultEnvPath = join(stackDir, '.env');
const hasEnvFile = existsSync(defaultEnvPath) || (customEnvPath && existsSync(customEnvPath));