direct remote environments - remote stacks dir configurable, for putting the whole stack onto the remote
This commit is contained in:
19 files changed
+2316
-506
No files matched your search
@@ -1,6 +1,7 @@
|
||||
import { json } from '@sveltejs/kit';
|
||||
import type { RequestHandler } from './$types';
|
||||
import { authorize } from '$lib/server/authorize';
|
||||
import { requireBackups } from '$lib/server/backups/route-guards';
|
||||
import { runRestore } from '$lib/server/backups';
|
||||
import { validateRestoreRequest, type RestoreMode } from '$lib/server/backups/validate';
|
||||
import { createJobResponse } from '$lib/server/sse';
|
||||
@@ -10,9 +11,8 @@ import { guardSnapshotEnvAccess } from '$lib/server/backups/route-guards';
|
||||
export const POST: RequestHandler = async (event) => {
|
||||
const { request, cookies } = event;
|
||||
const auth = await authorize(cookies);
|
||||
if (auth.authEnabled && !await auth.can('backups', 'manage')) {
|
||||
return json({ error: 'Permission denied' }, { status: 403 });
|
||||
}
|
||||
const rbacDenied = await requireBackups(auth, 'manage');
|
||||
if (rbacDenied) return rbacDenied;
|
||||
|
||||
const body = await request.json();
|
||||
const mode: RestoreMode = body.mode === 'in-place' ? 'in-place' : 'new-location';
|
||||
@@ -25,6 +25,9 @@ export const POST: RequestHandler = async (event) => {
|
||||
// The client sends false to bring the stack up without them. Only a stack redeploy
|
||||
// consumes this; container restores ignore it.
|
||||
const restoreSecrets = body.restoreSecrets !== false;
|
||||
// New-location STACK restore: skip the captured compose/.env (data-only restore),
|
||||
// and/or ADOPT the restored stack into Dockhand afterwards (opt-in). Both default off.
|
||||
const skipStackFiles = body.skipStackFiles === true;
|
||||
|
||||
// Authorization gates run BEFORE request validation, so an unauthorized caller
|
||||
// never learns anything about the request shape.
|
||||
@@ -76,6 +79,7 @@ export const POST: RequestHandler = async (event) => {
|
||||
postRestore,
|
||||
volumeDestinations,
|
||||
restoreSecrets,
|
||||
skipStackFiles,
|
||||
},
|
||||
access,
|
||||
// Stream progress to the client (the restore modal's log). Without this the
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
import { json } from '@sveltejs/kit';
|
||||
import type { RequestHandler } from './$types';
|
||||
import { authorize } from '$lib/server/authorize';
|
||||
import { requireBackups } from '$lib/server/backups/route-guards';
|
||||
import { probeStackDir } from '$lib/server/backups';
|
||||
|
||||
// GET /api/backup/stack-dir-listing?target=<stack>&env=<id>
|
||||
// Probe the TARGET host and list the actual contents of the stack directory, so the
|
||||
// create/edit backup dialog can show the resolved host path + let the user pick which
|
||||
// stack-dir entries to back up. Returns { kind:'listed', hostPath, entries } or
|
||||
// { kind:'unknown', reason } (never throws for an operational failure).
|
||||
export const GET: RequestHandler = async ({ url, cookies }) => {
|
||||
const auth = await authorize(cookies);
|
||||
const denied = await requireBackups(auth, 'view');
|
||||
if (denied) return denied;
|
||||
|
||||
const targetName = url.searchParams.get('target');
|
||||
if (!targetName) return json({ error: 'target is required' }, { status: 400 });
|
||||
const envParam = url.searchParams.get('env');
|
||||
const environmentId = envParam ? parseInt(envParam) : null;
|
||||
|
||||
if (environmentId != null && auth.isEnterprise && !await auth.canAccessEnvironment(environmentId)) {
|
||||
return json({ error: 'Environment access denied' }, { status: 403 });
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await probeStackDir(targetName, environmentId);
|
||||
return json(result);
|
||||
} catch (e) {
|
||||
return json({ kind: 'unknown', reason: e instanceof Error ? e.message : 'failed to probe stack directory' });
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,31 @@
|
||||
import { json } from '@sveltejs/kit';
|
||||
import type { RequestHandler } from './$types';
|
||||
import { authorize } from '$lib/server/authorize';
|
||||
import { requireBackups } from '$lib/server/backups/route-guards';
|
||||
import { previewStackBackupPath } from '$lib/server/backups';
|
||||
|
||||
// GET /api/backup/stack-path?target=<stack>&env=<id>
|
||||
// Preview WHERE a stack's directory will be captured from on the host (the resolved host
|
||||
// bind path), or `unknown` with a reason - so the create-schedule dialog can show it up front
|
||||
// and warn before scheduling a backup that would hard-fail the stack-dir probe.
|
||||
export const GET: RequestHandler = async ({ url, cookies }) => {
|
||||
const auth = await authorize(cookies);
|
||||
const denied = await requireBackups(auth, 'view');
|
||||
if (denied) return denied;
|
||||
|
||||
const targetName = url.searchParams.get('target');
|
||||
if (!targetName) return json({ error: 'target is required' }, { status: 400 });
|
||||
const envParam = url.searchParams.get('env');
|
||||
const environmentId = envParam ? parseInt(envParam) : null;
|
||||
|
||||
if (environmentId != null && auth.isEnterprise && !await auth.canAccessEnvironment(environmentId)) {
|
||||
return json({ error: 'Environment access denied' }, { status: 403 });
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await previewStackBackupPath(targetName, environmentId);
|
||||
return json(result);
|
||||
} catch (e) {
|
||||
return json({ kind: 'unknown', reason: e instanceof Error ? e.message : 'failed to resolve stack path' });
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,53 @@
|
||||
import { json } from '@sveltejs/kit';
|
||||
import type { RequestHandler } from './$types';
|
||||
import { authorize } from '$lib/server/authorize';
|
||||
import { getEnvironment, getEnvSetting, setEnvSetting } from '$lib/server/db';
|
||||
|
||||
// Per-env "remote stacks directory" for `direct` environments. A direct daemon has no
|
||||
// agent and no shared filesystem with Dockhand, so relative bind files (./config.yaml,
|
||||
// ./data) never reach it. When this is set, Dockhand stages the stack files onto the
|
||||
// remote host under this dir before `docker compose up`. Empty/unset = current behavior.
|
||||
// Stored via the generic per-env settings store (no schema column).
|
||||
|
||||
export const GET: RequestHandler = async ({ params, cookies }) => {
|
||||
const auth = await authorize(cookies);
|
||||
if (auth.authEnabled && !(await auth.can('environments', 'view'))) {
|
||||
return json({ error: 'Permission denied' }, { status: 403 });
|
||||
}
|
||||
const id = parseInt(params.id);
|
||||
const env = await getEnvironment(id);
|
||||
if (!env) return json({ error: 'Environment not found' }, { status: 404 });
|
||||
|
||||
const stored = await getEnvSetting('remote_stacks_dir', id);
|
||||
const remoteStacksDir = stored && typeof stored === 'string' && stored.trim() !== '' ? stored : null;
|
||||
return json({ remoteStacksDir });
|
||||
};
|
||||
|
||||
export const POST: RequestHandler = async ({ params, request, cookies }) => {
|
||||
const auth = await authorize(cookies);
|
||||
if (auth.authEnabled && !(await auth.can('environments', 'edit'))) {
|
||||
return json({ error: 'Permission denied' }, { status: 403 });
|
||||
}
|
||||
const id = parseInt(params.id);
|
||||
const env = await getEnvironment(id);
|
||||
if (!env) return json({ error: 'Environment not found' }, { status: 404 });
|
||||
|
||||
const data = await request.json().catch(() => ({}));
|
||||
const raw = data?.remoteStacksDir;
|
||||
|
||||
// null / '' clears the setting (reverts to current behavior).
|
||||
if (raw === null || raw === undefined || raw === '') {
|
||||
await setEnvSetting('remote_stacks_dir', '', id);
|
||||
return json({ success: true, remoteStacksDir: null });
|
||||
}
|
||||
if (typeof raw !== 'string') {
|
||||
return json({ error: 'remoteStacksDir must be a string or null' }, { status: 400 });
|
||||
}
|
||||
// Must be an absolute path on the remote host; reject relative or traversal-y input.
|
||||
const value = raw.trim();
|
||||
if (!value.startsWith('/') || value.includes('..')) {
|
||||
return json({ error: 'remoteStacksDir must be an absolute path with no ".."' }, { status: 400 });
|
||||
}
|
||||
await setEnvSetting('remote_stacks_dir', value, id);
|
||||
return json({ success: true, remoteStacksDir: value });
|
||||
};
|
||||
Reference in new issue
Block a user