1.0.16
This commit is contained in:
1 parent
1cb47eaa9c
commit
48b9bde8ae
10 files changed
+291
-145
No files matched your search
+24
-10
@@ -549,16 +549,26 @@ export async function dockerFetch(
|
||||
if (config.type === 'https') {
|
||||
const tlsOptions: Record<string, unknown> = {};
|
||||
|
||||
// DISABLE TLS SESSION CACHING: Bun reuses TLS sessions across different hosts,
|
||||
// which causes client certificate mismatches in mTLS scenarios. By setting
|
||||
// sessionTimeout to 0, we force a fresh TLS handshake for every connection.
|
||||
tlsOptions.sessionTimeout = 0;
|
||||
// Detect if mutual TLS (client certificate authentication) is in use
|
||||
const isMtls = !!(config.cert && config.key);
|
||||
|
||||
// Set explicit servername for SNI - helps isolate TLS contexts per host
|
||||
if (isMtls) {
|
||||
// mTLS: Disable session caching to prevent Bun from reusing a TLS session
|
||||
// with wrong client certificates (pool key doesn't include certs)
|
||||
tlsOptions.sessionTimeout = 0;
|
||||
} else {
|
||||
// Non-mTLS HTTPS (CA-only or skip-verify): Allow short-lived session reuse.
|
||||
// Without this, every fetch allocates a new native TLS context in BoringSSL.
|
||||
// Native memory (mmap) is never returned to the OS, causing RSS to grow
|
||||
// continuously in long-running subprocesses (metrics, events).
|
||||
// 30s allows sessions to be reused within one metrics cycle, then expire.
|
||||
tlsOptions.sessionTimeout = 30;
|
||||
}
|
||||
|
||||
// Set explicit servername for SNI - isolates TLS contexts per host
|
||||
tlsOptions.servername = config.host;
|
||||
|
||||
// Load CA certificate (just this environment's CA, not composite)
|
||||
// The sessionTimeout=0 should prevent session reuse across hosts
|
||||
if (config.ca) {
|
||||
tlsOptions.ca = [config.ca];
|
||||
}
|
||||
@@ -581,10 +591,14 @@ export async function dockerFetch(
|
||||
if (Object.keys(tlsOptions).length > 0) {
|
||||
// @ts-ignore - Bun supports tls options with string certs
|
||||
finalOptions.tls = tlsOptions;
|
||||
// Force new connection for each request to prevent Bun from reusing
|
||||
// a TLS session with wrong client certificates (pool key doesn't include certs)
|
||||
// @ts-ignore - Bun supports keepalive option
|
||||
finalOptions.keepalive = false;
|
||||
if (isMtls) {
|
||||
// mTLS: Force new connection for each request to prevent Bun from
|
||||
// reusing a TLS session with wrong client certificates
|
||||
// @ts-ignore - Bun supports keepalive option
|
||||
finalOptions.keepalive = false;
|
||||
}
|
||||
// Non-mTLS: Use Bun's default keepalive (connection reuse) to avoid
|
||||
// allocating a new native TLS context per request
|
||||
}
|
||||
|
||||
// Optional verbose TLS debugging
|
||||
|
||||
+101
-2
@@ -765,6 +765,26 @@ interface ComposeCommandOptions {
|
||||
composeFileName?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a Docker Compose override file alongside the main compose file.
|
||||
* Docker Compose auto-discovers these when no -f flag is used, but when -f is required
|
||||
* we need to explicitly include the override file.
|
||||
*/
|
||||
function findComposeOverrideFile(stackDir: string, composeFileName: string): string | null {
|
||||
const overrideMap: Record<string, string[]> = {
|
||||
'compose.yaml': ['compose.override.yaml', 'compose.override.yml'],
|
||||
'compose.yml': ['compose.override.yaml', 'compose.override.yml'],
|
||||
'docker-compose.yaml': ['docker-compose.override.yaml', 'docker-compose.override.yml'],
|
||||
'docker-compose.yml': ['docker-compose.override.yaml', 'docker-compose.override.yml'],
|
||||
};
|
||||
const candidates = overrideMap[composeFileName] || [];
|
||||
for (const name of candidates) {
|
||||
const fullPath = join(stackDir, name);
|
||||
if (existsSync(fullPath)) return fullPath;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute a docker compose command locally via Bun.spawn.
|
||||
*
|
||||
@@ -910,7 +930,38 @@ async function executeLocalCompose(
|
||||
// Build command based on operation
|
||||
// If we have modified compose content (host path translation), use stdin instead of file
|
||||
const useStdin = finalComposeContent !== composeContent;
|
||||
const args = ['docker', 'compose', '-p', stackName, '-f', useStdin ? '-' : composeFile];
|
||||
const args = ['docker', 'compose', '-p', stackName];
|
||||
|
||||
// Temp file for path-translated override content (cleaned up in finally block)
|
||||
let tempOverridePath: string | undefined;
|
||||
|
||||
if (useStdin) {
|
||||
// Host path translation: must pipe modified content via stdin
|
||||
args.push('-f', '-');
|
||||
// Also include override file if it exists (needs path translation too)
|
||||
const overrideFile = findComposeOverrideFile(stackDir, basename(composeFile));
|
||||
if (overrideFile) {
|
||||
let overrideContent = await Bun.file(overrideFile).text();
|
||||
if (getHostDataDir()) {
|
||||
const rewrite = rewriteComposeVolumePaths(overrideContent, stackDir);
|
||||
if (rewrite.modified) overrideContent = rewrite.content;
|
||||
}
|
||||
tempOverridePath = join(stackDir, '.compose.override.translated.yaml');
|
||||
await Bun.write(tempOverridePath, overrideContent);
|
||||
args.push('-f', tempOverridePath);
|
||||
console.log(`${logPrefix} Including override file (path-translated): ${basename(overrideFile)}`);
|
||||
}
|
||||
} else if (customComposePath) {
|
||||
// Custom path (imported/adopted stacks): must use -f to point to non-standard location
|
||||
args.push('-f', composeFile);
|
||||
const overrideFile = findComposeOverrideFile(stackDir, basename(composeFile));
|
||||
if (overrideFile) {
|
||||
args.push('-f', overrideFile);
|
||||
console.log(`${logPrefix} Including override file: ${basename(overrideFile)}`);
|
||||
}
|
||||
}
|
||||
// else: internal stack without path translation - no -f needed.
|
||||
// Docker Compose auto-discovers compose.yaml + compose.override.yaml from cwd.
|
||||
|
||||
// Always auto-detect .env in compose directory (defaultEnvPath already defined above)
|
||||
if (existsSync(defaultEnvPath)) {
|
||||
@@ -1078,6 +1129,15 @@ async function executeLocalCompose(
|
||||
error: `Failed to run docker compose ${operation}: ${err.message}`
|
||||
};
|
||||
} finally {
|
||||
// Cleanup temp override file from host path translation
|
||||
if (tempOverridePath) {
|
||||
try {
|
||||
unlinkSync(tempOverridePath);
|
||||
} catch {
|
||||
// Ignore cleanup errors
|
||||
}
|
||||
}
|
||||
|
||||
// Cleanup TLS temp directory (always runs, even on exception)
|
||||
if (tlsCertDir) {
|
||||
activeTlsDirs.delete(tlsCertDir);
|
||||
@@ -1293,6 +1353,24 @@ async function executeComposeCommand(
|
||||
console.warn(`[Stack:${stackName}] Failed to read .env file at ${envPath}:`, err);
|
||||
}
|
||||
}
|
||||
|
||||
// Include compose override file if it exists alongside the compose file
|
||||
let hawserStackFiles = stackFiles;
|
||||
const composeDir = workingDir || (composePath ? dirname(composePath) : null);
|
||||
const composeBaseName = composePath ? basename(composePath) : 'compose.yaml';
|
||||
if (composeDir) {
|
||||
const overridePath = findComposeOverrideFile(composeDir, composeBaseName);
|
||||
if (overridePath) {
|
||||
try {
|
||||
const overrideContent = await Bun.file(overridePath).text();
|
||||
hawserStackFiles = { ...(hawserStackFiles || {}), [basename(overridePath)]: overrideContent };
|
||||
console.log(`[Stack:${stackName}] Including override file for Hawser: ${basename(overridePath)}`);
|
||||
} catch (err) {
|
||||
console.warn(`[Stack:${stackName}] Failed to read override file at ${overridePath}:`, err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return executeComposeViaHawser(
|
||||
operation,
|
||||
stackName,
|
||||
@@ -1302,7 +1380,7 @@ async function executeComposeCommand(
|
||||
secretVars,
|
||||
forceRecreate,
|
||||
removeVolumes,
|
||||
stackFiles,
|
||||
hawserStackFiles,
|
||||
serviceName,
|
||||
composeFileName
|
||||
);
|
||||
@@ -2037,6 +2115,27 @@ export async function deployStack(options: DeployStackOptions): Promise<StackOpe
|
||||
workingDir = await getStackDir(name, envId);
|
||||
console.log(`${logPrefix} Using internal stack directory:`, workingDir);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// For Hawser deployments: include compose and .env in stackFiles
|
||||
// Hawser writes files from the files map to disk at STACKS_DIR/{stackName}/
|
||||
if (!stackFiles) {
|
||||
stackFiles = {};
|
||||
}
|
||||
const composeFilename = actualComposePath ? basename(actualComposePath) : 'compose.yaml';
|
||||
if (!stackFiles[composeFilename]) {
|
||||
stackFiles[composeFilename] = compose;
|
||||
console.log(`${logPrefix} Added ${composeFilename} to stackFiles for Hawser (${compose.length} chars)`);
|
||||
}
|
||||
if (actualEnvPath && existsSync(actualEnvPath) && !stackFiles['.env']) {
|
||||
try {
|
||||
const envContent = await Bun.file(actualEnvPath).text();
|
||||
stackFiles['.env'] = envContent;
|
||||
console.log(`${logPrefix} Added .env to stackFiles for Hawser (${envContent.length} chars)`);
|
||||
} catch (err) {
|
||||
console.warn(`${logPrefix} Failed to read .env file at ${actualEnvPath}:`, err);
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`${logPrefix} Compose content length:`, compose.length, 'chars');
|
||||
|
||||
Reference in new issue
Block a user