tell direct-remote users to redeploy when a stack backup can't find its files

This commit is contained in:
Jarek Krochmalski committed 2026-09-12 16:24:13 +02:00
1 parent 5512357a74
commit 61cd945705
5 files changed
+111 -14

No files matched your search

+4 -9
View File
@@ -41,7 +41,7 @@ export interface MetadataFile {
* @param resticArgs the full restic argv (starting with 'backup') — already
* tag/exclude/flag-assembled by the caller.
*/
import type { StackDirProbeHint } from './stackdir-plan';
import { stackDirProbeFixHint, type StackDirProbeHint } from './stackdir-plan';
export function buildBackupScript(resticArgs: string[], stackDirProbe?: { volumeKey: string; composeFileName: string; label?: string; hostPath?: string; hint?: StackDirProbeHint }): string {
// restic runs backgrounded so the SIGINT-forwarding trap can reach it on cancel
@@ -63,19 +63,14 @@ export function buildBackupScript(resticArgs: string[], stackDirProbe?: { volume
if (stackDirProbe) {
const composePath = shellQuote(`/volumes/${stackDirProbe.volumeKey}/${stackDirProbe.composeFileName}`);
const label = stackDirProbe.label ? ` ${stackDirProbe.label}` : '';
// The compose is missing under the host dir we bind-mounted. For a remote env (hawser or a
// user-set path) tell the operator to point "Remote stack path (for backup)" at the real host
// path; for a local stack a redeploy stages the files.
// The compose is missing under the host dir we bind-mounted. The fix depends on WHY
// (path wrong vs not-deployed-there-yet vs local) - stackDirProbeFixHint decides.
const hint = stackDirProbe.hint;
const remote = hint?.kind === 'hawser-defaulted' || hint?.kind === 'user-set';
const where = remote ? hint.hostPath : stackDirProbe.hostPath;
// "on <env>" when we know the environment name, else a plain "on the host".
const on = remote && hint.envName ? ` on ${hint.envName}` : ` on the host`;
// Name the EXACT settings location so the operator can go straight there.
const settingsLoc = remote && hint.envName ? `Settings > Environments > ${hint.envName}` : `Settings > Environments`;
const fix = remote
? ` Set "Remote stack path (for backup)" in ${settingsLoc} to the real host path where this stack's files live.`
: ` Redeploy the stack to stage its files there.`;
const fix = stackDirProbeFixHint(hint, where);
const at = where ? ` in ${where}` : '';
// Build the whole message as ONE string and shell-quote it so the operator-facing text
// (which contains quotes, parens, and `>` - e.g. `"Remote stack path (for backup)"` and
+2 -1
View File
@@ -311,7 +311,8 @@ async function planStackDirVolume(
// HOST-side "Remote stack path (for backup)" - NOT to redeploy.
const probeHint: StackDirProbeHint =
isHawser && remoteStacksDirDefaulted ? { kind: 'hawser-defaulted', hostPath, envName }
: (isHawser || directRemote) && remoteStacksDir ? { kind: 'user-set', hostPath, envName }
: (isHawser || directRemote) && remoteStacksDir
? { kind: 'user-set', transport: isHawser ? 'hawser' : 'direct', hostPath, envName }
: { kind: 'local' };
return {
+30 -1
View File
@@ -211,9 +211,38 @@ export function isLocalDaemon(connectionType: string | null, envTcpHost: string
// wrong. `local`: a socket/local-daemon stack -> redeploy staged its files.
export type StackDirProbeHint =
| { kind: 'hawser-defaulted'; hostPath: string; envName: string | null }
| { kind: 'user-set'; hostPath: string; envName: string | null }
// `transport` distinguishes how the files reach the host: a hawser agent keeps them
// under its own STACKS_DIR (the path just needs to be the right HOST mapping), while a
// direct-remote env has Dockhand COPY them there on `up` - so a direct env whose path is
// set but empty needs a redeploy, not a path change.
| { kind: 'user-set'; transport: 'hawser' | 'direct'; hostPath: string; envName: string | null }
| { kind: 'local' };
/**
* The operator-facing fix line for a STACKDIR PROBE FAILED, tailored to why the compose
* is missing under the mounted host dir. Pure so it can be unit-tested (the message is the
* only actionable thing the user sees when a stack backup can't find its files).
*/
export function stackDirProbeFixHint(hint: StackDirProbeHint | undefined, hostPath: string | undefined): string {
const envName = hint && 'envName' in hint ? hint.envName : null;
const settingsLoc = envName ? `Settings > Environments > ${envName}` : `Settings > Environments`;
const at = hostPath ? ` at ${hostPath}` : '';
// A direct-remote env stages the files on deploy: if the path is set but the folder is
// empty, the stack simply hasn't been deployed there yet - a redeploy stages it. Naming a
// path change here (as the other branches do) is the wrong advice and sends the user in
// circles when the path is already correct.
if (hint?.kind === 'user-set' && hint.transport === 'direct') {
return ` The Remote stack path is set, but this stack's files aren't on ${envName ?? 'the host'} yet - redeploy the stack so Dockhand stages them${at}. (down/start/restart don't copy; it must be a deploy.)`;
}
// Hawser (defaulted or user-set): the agent keeps stacks under its own dir, so an empty
// probe means the configured HOST path doesn't map to where the agent actually writes.
if (hint?.kind === 'hawser-defaulted' || hint?.kind === 'user-set') {
return ` Set "Remote stack path (for backup)" in ${settingsLoc} to the real host path where this stack's files live.`;
}
// Local stack: a redeploy stages the files into the managed stack dir.
return ` Redeploy the stack to stage its files there.`;
}
/** Result of resolving the candidate host stack folder (before the runtime probe). */
export type HostStackDirResolution =
| { kind: 'candidate'; hostPath: string; composeFile: string; source: string }