updating a compose file over the API now saves it, not just deploys it
This commit is contained in:
2 files changed
+40
-17
No files matched your search
@@ -0,0 +1,15 @@
|
|||||||
|
/**
|
||||||
|
* The ONE formula for a stack's folder under a base dir on a host: `<base>/<stack>`.
|
||||||
|
* Deploy staging (write) and backup (read) share it so they can't diverge (#1383 / #1240).
|
||||||
|
* Pure + dependency-free, so both sides can import it.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/** Trim + drop trailing slashes: `/data/stacks/` -> `/data/stacks`. */
|
||||||
|
export function normalizeBaseDir(base: string): string {
|
||||||
|
return base.trim().replace(/\/+$/, '');
|
||||||
|
}
|
||||||
|
|
||||||
|
/** `<normalized base>/<stackName>`. Name is used verbatim - do NOT slugify (both sides use it raw). */
|
||||||
|
export function stackDirIn(base: string, stackName: string): string {
|
||||||
|
return `${normalizeBaseDir(base)}/${stackName}`;
|
||||||
|
}
|
||||||
@@ -70,22 +70,36 @@ export const PUT: RequestHandler = async ({ params, request, url, cookies }) =>
|
|||||||
return json({ error: 'secretProviderId must be a number or null' }, { status: 400 });
|
return json({ error: 'secretProviderId must be a number or null' }, { status: 400 });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Binding a secret provider resolves its secrets into the container at deploy;
|
||||||
|
// require the secrets permission so a stacks-only user can't exfiltrate a
|
||||||
|
// provider's secrets by binding it and reading the container env.
|
||||||
|
if (
|
||||||
|
typeof secretProviderId === 'number' &&
|
||||||
|
auth.authEnabled &&
|
||||||
|
!(await auth.can('secrets', 'view', envIdNum))
|
||||||
|
) {
|
||||||
|
return json({ error: 'Permission denied: binding a secret provider requires the secrets permission' }, { status: 403 });
|
||||||
|
}
|
||||||
|
|
||||||
// Build options object for custom paths, move operation, file renames, and secret provider binding
|
// Build options object for custom paths, move operation, file renames, and secret provider binding
|
||||||
const pathOptions = (composePath || envPath !== undefined || moveFromDir || oldComposePath || oldEnvPath || secretProviderId !== undefined)
|
const pathOptions = (composePath || envPath !== undefined || moveFromDir || oldComposePath || oldEnvPath || secretProviderId !== undefined)
|
||||||
? { composePath, envPath, moveFromDir, oldComposePath, oldEnvPath, secretProviderId }
|
? { composePath, envPath, moveFromDir, oldComposePath, oldEnvPath, secretProviderId }
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
|
// Persist the submitted content on EVERY accepted PUT, whether or not path fields came
|
||||||
|
// along. Gating this on pathOptions left Dockhand's stored copy stale while restart:true
|
||||||
|
// deployed the new content - a later GET then served the old copy, silently reverting the
|
||||||
|
// change on the next read/edit/deploy round-trip (#1383). saveStackComposeFile handles a
|
||||||
|
// possibly-undefined pathOptions fine (the non-restart branch already relied on that).
|
||||||
|
const saveResult = await saveStackComposeFile(name, content, false, envIdNum, pathOptions);
|
||||||
|
if (!saveResult.success) {
|
||||||
|
return json({ error: saveResult.error }, { status: 500 });
|
||||||
|
}
|
||||||
|
|
||||||
if (restart) {
|
if (restart) {
|
||||||
// Deploy with docker compose up -d --force-recreate
|
// Deploy with docker compose up -d --force-recreate.
|
||||||
// Force recreate ensures env var changes are applied
|
// Force recreate ensures env var changes are applied.
|
||||||
// Save paths first if provided
|
// Get authoritative paths from DB/filesystem for deploy (now reflects the saved content).
|
||||||
if (pathOptions) {
|
|
||||||
const saveResult = await saveStackComposeFile(name, content, false, envIdNum, pathOptions);
|
|
||||||
if (!saveResult.success) {
|
|
||||||
return json({ error: saveResult.error }, { status: 500 });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Get authoritative paths from DB/filesystem for deploy
|
|
||||||
const composeInfo = await getStackComposeFile(name, envIdNum);
|
const composeInfo = await getStackComposeFile(name, envIdNum);
|
||||||
|
|
||||||
// Deploy via SSE to keep connection alive during long operations
|
// Deploy via SSE to keep connection alive during long operations
|
||||||
@@ -112,13 +126,7 @@ export const PUT: RequestHandler = async ({ params, request, url, cookies }) =>
|
|||||||
}, request);
|
}, request);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Just save the file without restarting (update operation, not create)
|
// No restart: the content is already persisted above.
|
||||||
const result = await saveStackComposeFile(name, content, false, envIdNum, pathOptions);
|
|
||||||
|
|
||||||
if (!result.success) {
|
|
||||||
return json({ error: result.error }, { status: 500 });
|
|
||||||
}
|
|
||||||
|
|
||||||
return json({ success: true });
|
return json({ success: true });
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error(`Error updating compose file for stack ${name}:`, error);
|
console.error(`Error updating compose file for stack ${name}:`, error);
|
||||||
|
|||||||
Reference in new issue
Block a user