direct remote environments - remote stacks dir configurable, for putting the whole stack onto the remote

This commit is contained in:
jarek committed 2026-08-09 10:31:27 +02:00
1 parent 86ef3f743b
commit bd06b340c5
19 files changed
+2316 -506

No files matched your search

+28 -5
View File
@@ -27,9 +27,9 @@ export interface MetadataFile {
* The metadata files (metadata.json, and for stacks the compose/.env and the
* whole stackfiles/ tree) are NOT written by this script — they are streamed into
* the container's /metadata directory via the Docker put-archive API BEFORE the
* container starts (see restic.ts runInHelper). Embedding their base64 inline in
* the Cmd used to blow the kernel ARG_MAX for large stack files ("argument list
* too long"); put-archive has no such limit.
* container starts (see restic.ts runInHelper). Inlining their base64 in the Cmd
* would hit the kernel ARG_MAX for large stack files ("argument list too long");
* put-archive has no such limit.
*
* So the script only:
* 1. ensures /metadata exists (put-archive already created it, but be defensive
@@ -41,7 +41,7 @@ export interface MetadataFile {
* @param resticArgs the full restic argv (starting with 'backup') — already
* tag/exclude/flag-assembled by the caller.
*/
export function buildBackupScript(resticArgs: string[]): string {
export function buildBackupScript(resticArgs: string[], stackDirProbe?: { volumeKey: string; composeFileName: string; label?: string; hostPath?: string }): string {
// restic runs backgrounded so the SIGINT-forwarding trap can reach it on cancel
// (SIGINT lets restic release its repo lock; a SIGKILL orphans it and hangs the
// next backup on --retry-lock). The kill-0 re-wait loop only fires when a signal
@@ -49,7 +49,30 @@ export function buildBackupScript(resticArgs: string[]): string {
// real code (0/3) for the marker.
const restic = `restic ${resticArgs.map(shellQuote).join(' ')}`;
const step = `${restic} & __rpid=$!; trap 'kill -INT $__rpid 2>/dev/null' INT TERM; wait $__rpid; __rc=$?; while kill -0 $__rpid 2>/dev/null; do wait $__rpid; __rc=$?; done; ( exit $__rc )`;
return `mkdir -p /metadata; set +e; ${finishScript(step)}`;
// STACK-DIR VOLUME PROBE (100% runtime guard for the 'volume' capture mode). The plan
// decided the target daemon is local and bind-mounted the stack dir at
// /volumes/<volumeKey>. If that mount is a phantom (a remote daemon Docker
// auto-created an EMPTY dir for), the compose file will be MISSING under it — backing
// that up would be a silent-empty snapshot. So before restic runs, assert the compose
// is visible; if not, exit non-zero (the finishScript marker turns this into a hard
// backup failure), never a silent success.
let probe = '';
if (stackDirProbe) {
const composePath = shellQuote(`/volumes/${stackDirProbe.volumeKey}/${stackDirProbe.composeFileName}`);
const label = stackDirProbe.label ? ` ${stackDirProbe.label}` : '';
// Name the REAL host path we mounted (<remote_stacks_dir>/<stack>) so the operator knows
// exactly where to look, and what to do: redeploy the stack to stage its files there.
const hostHint = stackDirProbe.hostPath
? ` The stack folder is empty or missing at ${stackDirProbe.hostPath} on the Docker host - redeploy the stack to stage its files there.`
: '';
probe =
`if [ ! -f ${composePath} ]; then ` +
`echo "[backup] $(date -u +%Y-%m-%dT%H:%M:%SZ)${label} STACKDIR PROBE FAILED: the stack's compose (${stackDirProbe.composeFileName}) is not present under the mounted stack dir.${hostHint} Refusing to write a silent-empty snapshot." 1>&2; ` +
`exit 1; fi; `;
}
return `mkdir -p /metadata; set +e; ${probe}${finishScript(step)}`;
}
/**