optional stacks_dir flat layout for local stacks

This commit is contained in:
Jarek Krochmalski committed 2026-09-02 15:52:44 +02:00
1 parent 3fa75c9702
commit 002f53ce78
5 files changed
+92 -403

No files matched your search

+3 -23
View File
@@ -1,6 +1,6 @@
import { json } from '@sveltejs/kit';
import { join } from 'path';
import { getStackDir, isHawserConnection } from '$lib/server/stacks';
import { getStackDir } from '$lib/server/stacks';
import { getEnvironment } from '$lib/server/db';
import type { RequestHandler } from './$types';
@@ -17,15 +17,6 @@ import type { RequestHandler } from './$types';
* resp-200: {stackDir:string!, composePath:string!, envPath:string!, source:string!}
* resp-200-example: {"stackDir":"/data/stacks/prod/web","composePath":"/data/stacks/prod/web/compose.yaml","envPath":"/data/stacks/prod/web/.env","source":"default"}
* resp-400: Stack name is required
*
* Query params:
* - name: Stack name (required)
* - env: Environment ID (optional)
* - location: Custom base location path (optional)
*
* If location is provided, path will be: {location}/{envName}/{stackName}/
* Otherwise uses Dockhand's default via getStackDir (flat STACKS_DIR/<stackName>/
* for local envs when STACKS_DIR is set, else $DATA_DIR/stacks/<envName>/<stackName>/).
*/
export const GET: RequestHandler = async ({ url }) => {
const stackName = url.searchParams.get('name');
@@ -38,7 +29,6 @@ export const GET: RequestHandler = async ({ url }) => {
}
let stackDir: string;
let source = 'default';
if (location) {
// Custom location: {location}/{envName}/{stackName}/
@@ -54,23 +44,13 @@ export const GET: RequestHandler = async ({ url }) => {
}
} else {
// Dockhand default location
if (envIdNum) {
const env = await getEnvironment(envIdNum);
if (env && isHawserConnection(env) && env.hawserStacksDir) {
stackDir = join(env.hawserStacksDir, stackName);
source = 'hawser';
} else {
stackDir = await getStackDir(stackName, envIdNum);
}
} else {
stackDir = await getStackDir(stackName, envIdNum);
}
stackDir = await getStackDir(stackName, envIdNum);
}
return json({
stackDir,
composePath: `${stackDir}/compose.yaml`,
envPath: `${stackDir}/.env`,
source
source: 'default'
});
};
+11 -24
View File
@@ -1,9 +1,8 @@
import { json } from '@sveltejs/kit';
import type { RequestHandler } from './$types';
import { getStackSources, getEnvironment } from '$lib/server/db';
import { countStackEnvVars, resolveStackSourceDisplayPathsForEnv, buildStackPathHintsMap } from '$lib/server/stacks';
import { getStackSources } from '$lib/server/db';
import { countStackEnvVars } from '$lib/server/stacks';
import { authorize } from '$lib/server/authorize';
import { listContainers } from '$lib/server/docker';
/**
* @openapi
@@ -26,19 +25,7 @@ export const GET: RequestHandler = async ({ url, cookies }) => {
try {
const sources = await getStackSources(envIdNum);
const envIds = [...new Set(sources.map((s) => s.environmentId).filter((id): id is number => id != null))];
const envs = await Promise.all(envIds.map((id) => getEnvironment(id)));
const envMap = new Map(envs.filter((e) => e !== undefined).map((e) => [e.id, e]));
const hintEnvIds = [...new Set(sources.map((s) => s.environmentId ?? null))];
const hintMaps = await Promise.all(
hintEnvIds.map(async (id) => {
const containers = await listContainers(true, id).catch(() => []);
return { id, map: buildStackPathHintsMap(containers) };
})
);
const hintsByEnv = new Map(hintMaps.map((h) => [String(h.id ?? 'null'), h.map]));
// Convert to a map for easier lookup in the frontend
const sourceMap: Record<
string,
{
@@ -50,6 +37,11 @@ export const GET: RequestHandler = async ({ url, cookies }) => {
envVarCount?: number;
}
> = {};
// Count env vars server-side (one local read per stack) so the list badge does
// not need a /env fetch per stack. GET /env resolves its env param as
// `envId ? parseInt : null`, so pass the SAME null-when-absent value (not
// undefined - getStackEnvVars scopes differently for null vs undefined) so the
// count matches that endpoint's variable list exactly.
const countEnvId = envIdNum ?? null;
const counts = await Promise.all(
sources.map((s) =>
@@ -58,21 +50,16 @@ export const GET: RequestHandler = async ({ url, cookies }) => {
: Promise.resolve(0)
)
);
for (const [i, source] of sources.entries()) {
const resolved = await resolveStackSourceDisplayPathsForEnv(
source,
source.environmentId != null ? envMap.get(source.environmentId) ?? null : null,
hintsByEnv.get(String(source.environmentId ?? 'null'))?.get(source.stackName) ?? null
);
sources.forEach((source, i) => {
sourceMap[source.stackName] = {
sourceType: source.sourceType,
composePath: resolved.composePath,
composePath: source.composePath,
repository: source.repository,
secretProviderId: source.secretProviderId,
icon: source.icon ?? null,
envVarCount: counts[i],
};
}
});
return json(sourceMap);
} catch (error) {
@@ -706,20 +706,18 @@
<p>
The environment
<code class="text-xs bg-muted px-1 py-0.5 rounded">{deleteEnvTarget.name}</code>
and the following directories will be permanently removed from the Dockhand host
(managed local stacks under <code class="text-xs">STACKS_DIR</code>, when configured, are removed per stack name):
and the following directories will be permanently removed from the Dockhand host:
</p>
<div class="space-y-1 text-xs font-mono bg-muted/40 rounded-md p-3 border overflow-x-auto">
<div class="flex items-center gap-2 whitespace-nowrap">
<Trash2 class="w-3.5 h-3.5 shrink-0 text-destructive" />
<code class="whitespace-nowrap">$DATA_DIR/stacks/{deleteEnvTarget.name}/</code>
<span class="text-muted-foreground shrink-0">(staging)</span>
</div>
<div class="flex items-center gap-2 whitespace-nowrap">
<Trash2 class="w-3.5 h-3.5 shrink-0 text-destructive" />
<code class="whitespace-nowrap">$DATA_DIR/git-repos/{deleteEnvTarget.name}/</code>
</div>
</div>
<p class="text-muted-foreground text-xs">
Shared git repository clones under <code class="text-xs">$DATA_DIR/git-repos/&lt;repoName&gt;/</code>
are not removed when an environment is deleted.
</p>
{#if deleteCountsLoading}
<p class="flex items-center gap-2">
<RefreshCw class="w-3.5 h-3.5 animate-spin" />