optional stacks_dir flat layout for local stacks
This commit is contained in:
5 files changed
+92
-403
No files matched your search
+15
-54
@@ -26,7 +26,6 @@ import { resolveNanoCpusConflict, resolvePodmanUsernsMode } from './hostconfig-r
|
||||
import { parseImageReference } from './registry/image-ref';
|
||||
export { parseImageReference } from './registry/image-ref';
|
||||
import { rebaseEnvOntoImage, rebaseLabelsOntoImage, rebaseCommand, describeEnvRebase, describeLabelRebase, type ImageEnvLabels } from './container-env-merge';
|
||||
import { db, environments, eq } from './db/drizzle.js';
|
||||
import { encodeRegistryAuth, fetchRegistryToken, isSafeRegistryHost } from './registry-auth';
|
||||
import { classifyManifest, type ArtifactKind } from './semver/manifest-artifact';
|
||||
import { isSystemContainer, classifyEmptyDigestImage, localDigestIsIndexChild } from './scheduler/tasks/update-utils';
|
||||
@@ -4093,47 +4092,23 @@ export async function dockerPing(envId: number): Promise<boolean> {
|
||||
}
|
||||
}
|
||||
|
||||
export interface HawserAgentInfo {
|
||||
/**
|
||||
* Get Hawser agent info (for hawser-standard mode)
|
||||
* Returns agent info including uptime
|
||||
*/
|
||||
export async function getHawserInfo(envId: number): Promise<{
|
||||
agentId: string;
|
||||
agentName: string;
|
||||
dockerVersion: string;
|
||||
hawserVersion: string;
|
||||
mode: string;
|
||||
uptime: number;
|
||||
/** Hawser agent STACKS_DIR — remote host path where compose stacks are written */
|
||||
stacksDir?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Hawser agent info (for hawser-standard mode)
|
||||
* Returns agent info including uptime
|
||||
*/
|
||||
export async function getHawserInfo(envId: number): Promise<HawserAgentInfo | null> {
|
||||
return getHawserInfoCached(envId);
|
||||
}
|
||||
|
||||
const HAWSER_INFO_TTL_MS = 60_000;
|
||||
const HAWSER_INFO_NEGATIVE_TTL_MS = 3_000;
|
||||
const hawserInfoCache = new Map<number, { info: HawserAgentInfo | null; expiresAt: number }>();
|
||||
|
||||
/**
|
||||
* Memoized wrapper around the /_hawser/info agent round-trip. Stacks listing
|
||||
* resolves display paths per stack, so a single list request would otherwise
|
||||
* hit the agent once per stack (N+1).
|
||||
*/
|
||||
async function getHawserInfoCached(envId: number): Promise<HawserAgentInfo | null> {
|
||||
const cached = hawserInfoCache.get(envId);
|
||||
if (cached && cached.expiresAt > Date.now()) {
|
||||
return cached.info;
|
||||
}
|
||||
|
||||
let info: HawserAgentInfo | null = null;
|
||||
} | null> {
|
||||
for (let attempt = 0; attempt < 2; attempt++) {
|
||||
try {
|
||||
const response = await dockerFetch('/_hawser/info', {}, envId);
|
||||
if (response.ok) {
|
||||
info = await response.json();
|
||||
break;
|
||||
return await response.json();
|
||||
}
|
||||
await drainResponse(response);
|
||||
console.warn(`[Hawser] Info endpoint returned ${response.status} for env ${envId}`);
|
||||
@@ -4142,27 +4117,7 @@ async function getHawserInfoCached(envId: number): Promise<HawserAgentInfo | nul
|
||||
console.warn(`[Hawser] Failed to fetch info for env ${envId} (attempt ${attempt + 1}): ${msg}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Persist the remote stacks dir so display-path resolution survives agent
|
||||
// restarts and doesn't depend on container runtime state (labels) or a
|
||||
// live info round-trip per request.
|
||||
if (info?.stacksDir) {
|
||||
try {
|
||||
await db
|
||||
.update(environments)
|
||||
.set({ hawserStacksDir: info.stacksDir })
|
||||
.where(eq(environments.id, envId));
|
||||
} catch (error) {
|
||||
const msg = error instanceof Error ? error.message : String(error);
|
||||
console.warn(`[Hawser] Failed to persist stacksDir for env ${envId}: ${msg}`);
|
||||
}
|
||||
}
|
||||
|
||||
hawserInfoCache.set(envId, {
|
||||
info,
|
||||
expiresAt: Date.now() + (info ? HAWSER_INFO_TTL_MS : HAWSER_INFO_NEGATIVE_TTL_MS)
|
||||
});
|
||||
return info;
|
||||
return null;
|
||||
}
|
||||
|
||||
// Volume operations
|
||||
@@ -5072,9 +5027,15 @@ export async function runContainerWithStreaming(options: {
|
||||
HostConfig: {
|
||||
Binds: options.binds || [],
|
||||
AutoRemove: false,
|
||||
// No log rotation: a helper's stdout IS its result, read via /logs after it
|
||||
// exits. An empty Config inherits the daemon's default rotation (Synology sets
|
||||
// max-size=10m,max-file=3), and a scanner writing a large JSON report line by
|
||||
// line blows past the window - the head rotates away and /logs returns a
|
||||
// truncated, mid-document buffer (#1496). One big file, no rotation; the log
|
||||
// dies with the ephemeral container so it never accumulates on disk.
|
||||
LogConfig: {
|
||||
Type: 'json-file',
|
||||
Config: {}
|
||||
Config: { 'max-file': '1', 'max-size': '1g' }
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
+58
-295
@@ -9,6 +9,7 @@ import { existsSync, mkdirSync, rmSync, readdirSync, cpSync, statSync, unlinkSyn
|
||||
import { join, resolve, dirname, basename, isAbsolute, normalize as pathNormalize, sep as pathSep } from 'node:path';
|
||||
import { spawn as nodeSpawn } from 'node:child_process';
|
||||
import type { ChildProcess } from 'node:child_process';
|
||||
import { redactSecretVars } from './secret-redact';
|
||||
import {
|
||||
applyFileDeletions,
|
||||
hashDirFiles,
|
||||
@@ -18,13 +19,7 @@ import {
|
||||
type DeletionApplyResult,
|
||||
type DeletionSkipReason
|
||||
} from './git-deletions';
|
||||
import {
|
||||
remapComposeContentsFromRemoteToStaging,
|
||||
remapComposeContentsFromStagingToRemote,
|
||||
remapPathsFromRemoteToStaging,
|
||||
remapPathsFromStagingToRemote
|
||||
} from './stacks-display-paths';
|
||||
import { isPathUnderRoot } from './path-utils';
|
||||
import { buildComposeOperationArgs, shouldRunSeparateBuildStep } from './compose-args';
|
||||
import { findStackNameCollision, moveStackFilePathCrossDevice, resolveStackDirForLayout } from './stack-path-utils';
|
||||
import { db, environments, eq } from './db/drizzle.js';
|
||||
import { isAllowedStackFilename } from './stack-filename';
|
||||
@@ -661,234 +656,6 @@ export function validateStacksDirAtStartup(): void {
|
||||
console.log(`[StacksDir] Using STACKS_DIR=${resolved}`);
|
||||
}
|
||||
|
||||
type HawserEnvLike = { connectionType?: string | null; hawserStacksDir?: string | null };
|
||||
|
||||
async function resolveHawserStackDirPair(
|
||||
stackName: string,
|
||||
environmentId: number,
|
||||
env?: HawserEnvLike | null,
|
||||
hints?: { workingDir: string | null; configFiles: string[] | null } | null
|
||||
): Promise<{ stagingStackDir: string; remoteStackDir: string } | null> {
|
||||
const resolvedEnv = env ?? (await getEnvironment(environmentId));
|
||||
if (!isHawserConnection(resolvedEnv)) return null;
|
||||
|
||||
const stagingStackDir = resolve(
|
||||
(await findStackDir(stackName, environmentId)) ?? (await getStackDir(stackName, environmentId))
|
||||
);
|
||||
|
||||
let remoteStackDir: string | null = null;
|
||||
const pathHints = hints ?? (await getStackPathHints(stackName, environmentId));
|
||||
if (pathHints.workingDir) {
|
||||
remoteStackDir = pathHints.workingDir;
|
||||
} else if (pathHints.configFiles?.length) {
|
||||
remoteStackDir = dirname(pathHints.configFiles[0]);
|
||||
}
|
||||
|
||||
// DB-persisted remote stacksDir (survives agent restarts)
|
||||
if (!remoteStackDir && resolvedEnv?.hawserStacksDir) {
|
||||
remoteStackDir = join(resolvedEnv.hawserStacksDir, stackName);
|
||||
}
|
||||
|
||||
if (!remoteStackDir) {
|
||||
const { getHawserInfo } = await import('./docker.js');
|
||||
const info = await getHawserInfo(environmentId);
|
||||
if (info?.stacksDir) {
|
||||
remoteStackDir = join(info.stacksDir, stackName);
|
||||
try {
|
||||
await db
|
||||
.update(environments)
|
||||
.set({ hawserStacksDir: info.stacksDir })
|
||||
.where(eq(environments.id, environmentId));
|
||||
} catch (error) {
|
||||
const msg = error instanceof Error ? error.message : String(error);
|
||||
console.warn(`[StacksDir] Failed to persist stacksDir for env ${environmentId}: ${msg}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!remoteStackDir) return null;
|
||||
return { stagingStackDir, remoteStackDir: resolve(remoteStackDir) };
|
||||
}
|
||||
|
||||
/**
|
||||
* For Hawser environments, compose paths in the DB point at Dockhand's staging copy
|
||||
* ($DATA_DIR/stacks/<env>/<stack>/). Deployed files live on the remote host under the
|
||||
* Hawser agent's STACKS_DIR/<stackName>/.
|
||||
*/
|
||||
export async function remapHawserStagingDisplayPaths(
|
||||
stackName: string,
|
||||
environmentId: number | null | undefined,
|
||||
paths: { composePath: string | null; composePaths: string[] },
|
||||
env?: HawserEnvLike | null,
|
||||
hints?: { workingDir: string | null; configFiles: string[] | null } | null
|
||||
): Promise<{ composePath: string | null; composePaths: string[] }> {
|
||||
if (!paths.composePath && paths.composePaths.length === 0) return paths;
|
||||
if (environmentId == null) return paths;
|
||||
|
||||
const pathList =
|
||||
paths.composePaths.length > 0
|
||||
? paths.composePaths
|
||||
: paths.composePath
|
||||
? [paths.composePath]
|
||||
: [];
|
||||
|
||||
const pair = await resolveHawserStackDirPair(stackName, environmentId, env, hints);
|
||||
if (!pair) return paths;
|
||||
|
||||
const { stagingStackDir, remoteStackDir } = pair;
|
||||
const usesStaging = pathList.some((p) => {
|
||||
const resolved = resolve(p);
|
||||
return isPathUnderRoot(resolved, stagingStackDir) || isManagedStagingDir(dirname(resolved));
|
||||
});
|
||||
if (!usesStaging) return paths;
|
||||
|
||||
const remapped = remapPathsFromStagingToRemote(stagingStackDir, remoteStackDir, pathList);
|
||||
return {
|
||||
composePath: remapped[0] ?? null,
|
||||
composePaths: remapped
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Remap composeContents keys from staging paths to Hawser remote display paths.
|
||||
*/
|
||||
export async function remapHawserStagingDisplayComposeContents(
|
||||
stackName: string,
|
||||
environmentId: number | null | undefined,
|
||||
composeContents: Record<string, string> | null | undefined,
|
||||
env?: HawserEnvLike | null,
|
||||
hints?: { workingDir: string | null; configFiles: string[] | null } | null
|
||||
): Promise<Record<string, string> | null | undefined> {
|
||||
if (!composeContents || environmentId == null) return composeContents;
|
||||
|
||||
const pair = await resolveHawserStackDirPair(stackName, environmentId, env, hints);
|
||||
if (!pair) return composeContents;
|
||||
|
||||
return remapComposeContentsFromStagingToRemote(
|
||||
pair.stagingStackDir,
|
||||
pair.remoteStackDir,
|
||||
composeContents
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert Hawser display paths from the UI back to Dockhand staging paths for disk writes.
|
||||
*/
|
||||
export async function unmapHawserDisplayComposeOptionsToStaging(
|
||||
stackName: string,
|
||||
environmentId: number | null | undefined,
|
||||
options: {
|
||||
composePath?: string;
|
||||
composePaths?: string[] | null;
|
||||
composeContents?: Record<string, string>;
|
||||
envPath?: string;
|
||||
moveFromDir?: string;
|
||||
oldComposePath?: string;
|
||||
oldEnvPath?: string;
|
||||
}
|
||||
): Promise<typeof options> {
|
||||
if (!options || environmentId == null) return options;
|
||||
|
||||
const pair = await resolveHawserStackDirPair(stackName, environmentId);
|
||||
if (!pair) return options;
|
||||
|
||||
const { stagingStackDir, remoteStackDir } = pair;
|
||||
const result = { ...options };
|
||||
|
||||
if (options.composePath) {
|
||||
result.composePath = remapPathsFromRemoteToStaging(stagingStackDir, remoteStackDir, [
|
||||
options.composePath
|
||||
])[0];
|
||||
}
|
||||
if (options.composePaths) {
|
||||
result.composePaths = remapPathsFromRemoteToStaging(
|
||||
stagingStackDir,
|
||||
remoteStackDir,
|
||||
options.composePaths
|
||||
);
|
||||
}
|
||||
if (options.composeContents) {
|
||||
result.composeContents = remapComposeContentsFromRemoteToStaging(
|
||||
stagingStackDir,
|
||||
remoteStackDir,
|
||||
options.composeContents
|
||||
);
|
||||
}
|
||||
if (options.envPath) {
|
||||
result.envPath = remapPathsFromRemoteToStaging(stagingStackDir, remoteStackDir, [
|
||||
options.envPath
|
||||
])[0];
|
||||
}
|
||||
if (options.oldComposePath) {
|
||||
result.oldComposePath = remapPathsFromRemoteToStaging(stagingStackDir, remoteStackDir, [
|
||||
options.oldComposePath
|
||||
])[0];
|
||||
}
|
||||
if (options.oldEnvPath) {
|
||||
result.oldEnvPath = remapPathsFromRemoteToStaging(stagingStackDir, remoteStackDir, [
|
||||
options.oldEnvPath
|
||||
])[0];
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve stack source compose paths for UI display, including Hawser remote remapping.
|
||||
*/
|
||||
export async function resolveStackSourceDisplayPathsForEnv(
|
||||
source: {
|
||||
stackName: string;
|
||||
environmentId?: number | null;
|
||||
sourceType: string;
|
||||
composePath?: string | null;
|
||||
gitStack?: { contextDir?: string | null; composePath?: string } | null;
|
||||
},
|
||||
env?: HawserEnvLike | null,
|
||||
hints?: { workingDir: string | null; configFiles: string[] | null } | null
|
||||
): Promise<{ composePath: string | null; composePaths: string[] }> {
|
||||
const base = resolveStackSourceDisplayPaths(source);
|
||||
return remapHawserStagingDisplayPaths(source.stackName, source.environmentId, base, env, hints);
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a map of compose project name -> path hints from a pre-fetched container
|
||||
* list, so a stacks listing doesn't trigger one container listing per stack.
|
||||
*/
|
||||
export function buildStackPathHintsMap(
|
||||
containers: any[]
|
||||
): Map<string, { workingDir: string | null; configFiles: string[] | null }> {
|
||||
const map = new Map<string, { workingDir: string | null; configFiles: string[] | null }>();
|
||||
for (const container of containers) {
|
||||
const labels = container.labels || {};
|
||||
const project = labels['com.docker.compose.project'];
|
||||
if (!project || map.has(project)) continue;
|
||||
const workingDir = labels['com.docker.compose.project.working_dir'] || null;
|
||||
const configFilesRaw = labels['com.docker.compose.project.config_files'] || null;
|
||||
const configFiles = configFilesRaw ? configFilesRaw.split(',').map((f: string) => f.trim()) : null;
|
||||
map.set(project, { workingDir, configFiles });
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve stack source compose paths to absolute on-disk paths for UI display.
|
||||
* Git stacks store repo-relative paths in the DB; external/adopted stacks use absolute paths.
|
||||
*/
|
||||
export function resolveStackSourceDisplayPaths(
|
||||
source: {
|
||||
sourceType: string;
|
||||
composePath?: string | null;
|
||||
gitStack?: { contextDir?: string | null; composePath?: string } | null;
|
||||
}
|
||||
): { composePath: string | null; composePaths: string[] } {
|
||||
if (!source.composePath) {
|
||||
return { composePath: null, composePaths: [] };
|
||||
}
|
||||
const path = isAbsolute(source.composePath) ? source.composePath : resolve(source.composePath);
|
||||
return { composePath: path, composePaths: [path] };
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// COMPOSE FILE MANAGEMENT
|
||||
// =============================================================================
|
||||
@@ -1393,7 +1160,7 @@ function findComposeOverrideFile(stackDir: string, composeFileName: string): str
|
||||
* @param customComposePath - Optional path to existing compose file (for imported stacks, skips writing)
|
||||
*/
|
||||
async function executeLocalCompose(
|
||||
operation: 'up' | 'down' | 'stop' | 'start' | 'restart' | 'pull',
|
||||
operation: 'up' | 'down' | 'stop' | 'start' | 'restart' | 'pull' | 'build',
|
||||
stackName: string,
|
||||
composeContent: string,
|
||||
dockerHost?: string,
|
||||
@@ -1652,39 +1419,7 @@ async function executeLocalCompose(
|
||||
console.log(`${logPrefix} [HostPath] Using stdin for compose content (paths translated)`);
|
||||
}
|
||||
|
||||
switch (operation) {
|
||||
case 'up':
|
||||
args.push('up', '-d', '--remove-orphans');
|
||||
if (forceRecreate) args.push('--force-recreate');
|
||||
if (build) args.push('--build');
|
||||
if (build && noBuildCache) args.push('--no-cache');
|
||||
if (pullPolicy) args.push('--pull', pullPolicy);
|
||||
// If targeting a specific service, only update that service
|
||||
if (serviceName) {
|
||||
args.push(serviceName);
|
||||
}
|
||||
break;
|
||||
case 'down':
|
||||
args.push('down', '--remove-orphans');
|
||||
if (removeVolumes) args.push('--volumes');
|
||||
break;
|
||||
case 'stop':
|
||||
args.push('stop');
|
||||
break;
|
||||
case 'start':
|
||||
args.push('start');
|
||||
break;
|
||||
case 'restart':
|
||||
args.push('restart');
|
||||
break;
|
||||
case 'pull':
|
||||
args.push('pull');
|
||||
// If targeting a specific service, pull only that service
|
||||
if (serviceName) {
|
||||
args.push(serviceName);
|
||||
}
|
||||
break;
|
||||
}
|
||||
args.push(...buildComposeOperationArgs(operation, { forceRecreate, removeVolumes, build, noBuildCache, pullPolicy, serviceName }));
|
||||
|
||||
const commandStr = args.join(' ');
|
||||
|
||||
@@ -1774,10 +1509,13 @@ async function executeLocalCompose(
|
||||
command: commandStr
|
||||
};
|
||||
} else {
|
||||
// stderr can echo an interpolated secret value (e.g. a failing
|
||||
// command containing ${DB_PASSWORD}); redact before it can reach a
|
||||
// notification channel, the DB errorMessage, or the client.
|
||||
return {
|
||||
success: false,
|
||||
output: stdout,
|
||||
error: stderr || `docker compose ${operation} exited with code ${code}`,
|
||||
output: redactSecretVars(stdout, secretVars),
|
||||
error: redactSecretVars(stderr, secretVars) || `docker compose ${operation} exited with code ${code}`,
|
||||
command: commandStr
|
||||
};
|
||||
}
|
||||
@@ -1789,7 +1527,7 @@ async function executeLocalCompose(
|
||||
return {
|
||||
success: false,
|
||||
output: '',
|
||||
error: `Failed to run docker compose ${operation}: ${err.message}`,
|
||||
error: redactSecretVars(`Failed to run docker compose ${operation}: ${err.message}`, secretVars),
|
||||
command: commandStr
|
||||
};
|
||||
} finally {
|
||||
@@ -1822,7 +1560,7 @@ async function executeLocalCompose(
|
||||
* @param secretVars - Secret environment variables (injected via shell env on Hawser, NEVER in .env)
|
||||
*/
|
||||
async function executeComposeViaHawser(
|
||||
operation: 'up' | 'down' | 'stop' | 'start' | 'restart' | 'pull',
|
||||
operation: 'up' | 'down' | 'stop' | 'start' | 'restart' | 'pull' | 'build',
|
||||
stackName: string,
|
||||
composeContent: string,
|
||||
envId: number,
|
||||
@@ -1993,10 +1731,12 @@ async function executeComposeViaHawser(
|
||||
deletion
|
||||
};
|
||||
} else {
|
||||
// The agent's stderr can echo an interpolated secret value; redact before
|
||||
// it reaches a notification channel, the DB errorMessage, or the client.
|
||||
return {
|
||||
success: false,
|
||||
output: result.output || '',
|
||||
error: result.error || `Compose ${operation} failed`,
|
||||
output: redactSecretVars(result.output || '', secretVars),
|
||||
error: redactSecretVars(result.error || `Compose ${operation} failed`, secretVars),
|
||||
deletion
|
||||
};
|
||||
}
|
||||
@@ -2008,7 +1748,7 @@ async function executeComposeViaHawser(
|
||||
output: '',
|
||||
error: isStringLength
|
||||
? `Stack files too large to send via Hawser. The repository may contain large binary files. Consider using a .dockerignore or moving large files out of the compose directory.`
|
||||
: `Failed to ${operation} via Hawser: ${err.message}`
|
||||
: redactSecretVars(`Failed to ${operation} via Hawser: ${err.message}`, secretVars)
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -2020,7 +1760,7 @@ async function executeComposeViaHawser(
|
||||
* @param secretVars - Secret environment variables (from DB, injected via shell env)
|
||||
*/
|
||||
async function executeComposeCommand(
|
||||
operation: 'up' | 'down' | 'stop' | 'start' | 'restart' | 'pull',
|
||||
operation: 'up' | 'down' | 'stop' | 'start' | 'restart' | 'pull' | 'build',
|
||||
options: ComposeCommandOptions,
|
||||
composeContent: string,
|
||||
envVars?: Record<string, string>,
|
||||
@@ -2901,6 +2641,12 @@ export async function removeStack(
|
||||
removeVolumes = false,
|
||||
deleteFiles = true
|
||||
): Promise<StackOperationResult> {
|
||||
// Reject a name that isn't a plain stack name BEFORE any path construction. A
|
||||
// traversal name (e.g. "..") would make getStackDir resolve to DATA_DIR's parent and
|
||||
// rmSync it; the create/deploy paths already enforce this same shape.
|
||||
if (!/^[a-z0-9][a-z0-9_-]*$/.test(stackName)) {
|
||||
return { success: false, error: 'Invalid stack name' };
|
||||
}
|
||||
return withStackLock(stackName, async () => {
|
||||
// Get compose file (may not exist for external stacks)
|
||||
const composeResult = await getStackComposeFile(stackName, envId);
|
||||
@@ -3053,7 +2799,10 @@ export async function removeStack(
|
||||
// (Don't delete default-path files when an adopted stack has custom path outside DATA_DIR)
|
||||
if (!stackDir && !stackSource?.composePath) {
|
||||
const defaultDir = await findStackDir(stackName, envId) || await getStackDir(stackName, envId);
|
||||
if (existsSync(defaultDir)) {
|
||||
// Same #675 guard as the composePath branch: only a strict subdir of a managed
|
||||
// stacks root whose basename is the stack name is deletable. Never DATA_DIR or a parent.
|
||||
const deletableRoots = [getDefaultStacksDir(), ...(isStacksDirEnvSet() ? [getLocalStacksDir()] : [])];
|
||||
if (deletableRoots.some((root) => isDeletableStackDir(defaultDir, root, stackName)) && existsSync(defaultDir)) {
|
||||
stackDir = defaultDir;
|
||||
}
|
||||
}
|
||||
@@ -3380,27 +3129,41 @@ export async function deployStack(options: DeployStackOptions): Promise<StackOpe
|
||||
// so no override file is needed - only pass secrets for shell injection.
|
||||
const isGitStack = !!sourceDir;
|
||||
|
||||
const cmdOptions: ComposeCommandOptions = {
|
||||
stackName: name,
|
||||
envId,
|
||||
forceRecreate,
|
||||
build,
|
||||
noBuildCache,
|
||||
pullPolicy,
|
||||
stackFiles,
|
||||
workingDir,
|
||||
composePath: actualComposePath,
|
||||
envPath: actualEnvPath,
|
||||
useOverrideFile: isGitStack,
|
||||
// Pass compose filename for Hawser (extracted from path or provided explicitly)
|
||||
composeFileName: composeFileName || (actualComposePath ? basename(actualComposePath) : undefined),
|
||||
filesToDelete
|
||||
};
|
||||
const composeEnvVars = isGitStack ? dbNonSecretVars : undefined;
|
||||
|
||||
// `--no-cache` is a `build` flag, not an `up` flag (#1479). When a no-cache
|
||||
// rebuild is requested, run a separate `docker compose build --no-cache` first,
|
||||
// then a plain `up`. Skipped on Hawser (its agent has no build op) - the up below
|
||||
// then omits --build for a no-cache request, so nothing crashes there.
|
||||
const deployEnv = envId ? await getEnvironment(envId) : null;
|
||||
if (shouldRunSeparateBuildStep(build, noBuildCache, deployEnv?.connectionType)) {
|
||||
console.log(`${logPrefix} Running separate 'build --no-cache' step before up...`);
|
||||
const buildResult = await executeComposeCommand('build', cmdOptions, compose, composeEnvVars, secretVars);
|
||||
if (!buildResult.success) return buildResult;
|
||||
}
|
||||
|
||||
console.log(`${logPrefix} Calling executeComposeCommand...`);
|
||||
const result = await executeComposeCommand(
|
||||
'up',
|
||||
{
|
||||
stackName: name,
|
||||
envId,
|
||||
forceRecreate,
|
||||
build,
|
||||
noBuildCache,
|
||||
pullPolicy,
|
||||
stackFiles,
|
||||
workingDir,
|
||||
composePath: actualComposePath,
|
||||
envPath: actualEnvPath,
|
||||
useOverrideFile: isGitStack,
|
||||
// Pass compose filename for Hawser (extracted from path or provided explicitly)
|
||||
composeFileName: composeFileName || (actualComposePath ? basename(actualComposePath) : undefined),
|
||||
filesToDelete
|
||||
},
|
||||
cmdOptions,
|
||||
compose,
|
||||
isGitStack ? dbNonSecretVars : undefined,
|
||||
composeEnvVars,
|
||||
secretVars
|
||||
);
|
||||
console.log(`${logPrefix} ========================================`);
|
||||
|
||||
Reference in new issue
Block a user