This commit is contained in:
Jarek Krochmalski committed 2026-01-11 07:16:18 +01:00
1 parent bd7b832394
commit 7643807717
86 files changed
+3644 -1385

No files matched your search

+27 -17
View File
@@ -35,11 +35,9 @@ export const GET: RequestHandler = async ({ url, cookies }) => {
const existingNames = new Set(stacks.map((s) => s.name));
for (const source of stackSources) {
// Only add internal/git stacks that aren't already in the list
if (
!existingNames.has(source.stackName) &&
(source.sourceType === 'internal' || source.sourceType === 'git')
) {
// Add stacks from database that aren't already in the Docker list
// This includes internal, git, and external (adopted) stacks that are currently down
if (!existingNames.has(source.stackName)) {
stacks.push({
name: source.stackName,
containers: [],
@@ -78,7 +76,7 @@ export const POST: RequestHandler = async ({ request, url, cookies }) => {
try {
const body = await request.json();
const { name, compose, start, envVars, rawEnvContent } = body;
const { name, compose, start, envVars, rawEnvContent, composePath, envPath } = body;
if (!name || typeof name !== 'string') {
return json({ error: 'Stack name is required' }, { status: 400 });
@@ -90,7 +88,10 @@ export const POST: RequestHandler = async ({ request, url, cookies }) => {
// If start is false, only create the compose file without deploying
if (start === false) {
const result = await saveStackComposeFile(name, compose, true);
const result = await saveStackComposeFile(name, compose, true, envIdNum, {
composePath: composePath || undefined,
envPath: envPath || undefined
});
if (!result.success) {
return json({ error: result.error }, { status: 400 });
}
@@ -100,7 +101,7 @@ export const POST: RequestHandler = async ({ request, url, cookies }) => {
// - envVars: ALL vars → DB (secrets stored for shell injection, non-secrets for metadata)
if (rawEnvContent) {
// Write raw content to .env file (should NOT contain secrets)
await writeRawStackEnvFile(name, rawEnvContent);
await writeRawStackEnvFile(name, rawEnvContent, envIdNum, envPath || undefined);
}
// Save ALL vars to DB (secrets for shell injection at runtime)
if (envVars && Array.isArray(envVars) && envVars.length > 0) {
@@ -108,14 +109,16 @@ export const POST: RequestHandler = async ({ request, url, cookies }) => {
}
// Fallback: if no rawEnvContent, generate .env from non-secret vars
if (!rawEnvContent && envVars && Array.isArray(envVars) && envVars.length > 0) {
await saveStackEnvVars(name, envVars, envIdNum);
await saveStackEnvVars(name, envVars, envIdNum, envPath || undefined);
}
// Record the stack as internally created
// Record the stack as internally created with custom paths if provided
await upsertStackSource({
stackName: name,
environmentId: envIdNum,
sourceType: 'internal'
sourceType: 'internal',
composePath: composePath || undefined,
envPath: envPath || undefined
});
return json({ success: true, started: false });
@@ -124,13 +127,16 @@ export const POST: RequestHandler = async ({ request, url, cookies }) => {
// Save environment variables BEFORE deploying so they're available during start
if (rawEnvContent || (envVars && Array.isArray(envVars) && envVars.length > 0)) {
// First ensure the stack directory exists by saving compose file
await saveStackComposeFile(name, compose, true);
await saveStackComposeFile(name, compose, true, envIdNum, {
composePath: composePath || undefined,
envPath: envPath || undefined
});
// - rawEnvContent: non-secret vars with comments → .env file
// - envVars: ALL vars → DB (secrets stored for shell injection, non-secrets for metadata)
if (rawEnvContent) {
// Write raw content to .env file (should NOT contain secrets)
await writeRawStackEnvFile(name, rawEnvContent);
await writeRawStackEnvFile(name, rawEnvContent, envIdNum, envPath || undefined);
}
// Save ALL vars to DB (secrets for shell injection at runtime)
if (envVars && Array.isArray(envVars) && envVars.length > 0) {
@@ -138,7 +144,7 @@ export const POST: RequestHandler = async ({ request, url, cookies }) => {
}
// Fallback: if no rawEnvContent, generate .env from non-secret vars
if (!rawEnvContent && envVars && Array.isArray(envVars) && envVars.length > 0) {
await saveStackEnvVars(name, envVars, envIdNum);
await saveStackEnvVars(name, envVars, envIdNum, envPath || undefined);
}
}
@@ -146,18 +152,22 @@ export const POST: RequestHandler = async ({ request, url, cookies }) => {
const result = await deployStack({
name,
compose,
envId: envIdNum
envId: envIdNum,
composePath: composePath || undefined,
envPath: envPath || undefined
});
if (!result.success) {
return json({ error: result.error, output: result.output }, { status: 400 });
}
// Record the stack as internally created
// Record the stack as internally created with custom paths if provided
await upsertStackSource({
stackName: name,
environmentId: envIdNum,
sourceType: 'internal'
sourceType: 'internal',
composePath: composePath || undefined,
envPath: envPath || undefined
});
return json({ success: true, started: true, output: result.output });
+1 -4
View File
@@ -1,5 +1,5 @@
import { json } from '@sveltejs/kit';
import { removeStack, ExternalStackError, ComposeFileNotFoundError } from '$lib/server/stacks';
import { removeStack, ComposeFileNotFoundError } from '$lib/server/stacks';
import { authorize } from '$lib/server/authorize';
import { auditStack } from '$lib/server/audit';
import type { RequestHandler } from './$types';
@@ -34,9 +34,6 @@ export const DELETE: RequestHandler = async (event) => {
}
return json({ success: true });
} catch (error) {
if (error instanceof ExternalStackError) {
return json({ error: error.message }, { status: 400 });
}
if (error instanceof ComposeFileNotFoundError) {
return json({ error: error.message }, { status: 404 });
}
@@ -4,22 +4,36 @@ import { getStackComposeFile, deployStack, saveStackComposeFile } from '$lib/ser
import { authorize } from '$lib/server/authorize';
// GET /api/stacks/[name]/compose - Get compose file content
export const GET: RequestHandler = async ({ params, cookies }) => {
export const GET: RequestHandler = async ({ params, url, cookies }) => {
const auth = await authorize(cookies);
if (auth.authEnabled && !(await auth.can('stacks', 'view'))) {
return json({ error: 'Permission denied' }, { status: 403 });
}
const { name } = params;
const envId = url.searchParams.get('env');
const envIdNum = envId ? parseInt(envId) : undefined;
try {
const result = await getStackComposeFile(name);
const result = await getStackComposeFile(name, envIdNum);
if (!result.success) {
return json({ error: result.error }, { status: 404 });
// Return info about what's needed - unified response for all missing compose files
return json({
error: result.error,
needsFileLocation: result.needsFileLocation || false,
composePath: result.composePath,
envPath: result.envPath
}, { status: 404 });
}
return json({ content: result.content, stackDir: result.stackDir });
return json({
content: result.content,
stackDir: result.stackDir,
composePath: result.composePath,
envPath: result.envPath,
suggestedEnvPath: result.suggestedEnvPath
});
} catch (error: any) {
console.error(`Error getting compose file for stack ${name}:`, error);
return json({ error: error.message || 'Failed to get compose file' }, { status: 500 });
@@ -41,16 +55,29 @@ export const PUT: RequestHandler = async ({ params, request, url, cookies }) =>
try {
const body = await request.json();
const { content, restart = false } = body;
const { content, restart = false, composePath, envPath, moveFromDir, oldComposePath, oldEnvPath } = body;
if (!content || typeof content !== 'string') {
return json({ error: 'Compose file content is required' }, { status: 400 });
}
// Build options object for custom paths, move operation, and file renames
const pathOptions = (composePath || envPath !== undefined || moveFromDir || oldComposePath || oldEnvPath)
? { composePath, envPath, moveFromDir, oldComposePath, oldEnvPath }
: undefined;
let result;
if (restart) {
// Deploy with docker compose up -d --force-recreate
// Force recreate ensures env var changes are applied
// Note: deployStack uses requireComposeFile which will use saved paths
// Save paths first if provided
if (pathOptions) {
const saveResult = await saveStackComposeFile(name, content, false, envIdNum, pathOptions);
if (!saveResult.success) {
return json({ error: saveResult.error }, { status: 500 });
}
}
result = await deployStack({
name,
compose: content,
@@ -58,8 +85,8 @@ export const PUT: RequestHandler = async ({ params, request, url, cookies }) =>
forceRecreate: true
});
} else {
// Just save the file without restarting
result = await saveStackComposeFile(name, content);
// Just save the file without restarting (update operation, not create)
result = await saveStackComposeFile(name, content, false, envIdNum, pathOptions);
}
if (!result.success) {
+1 -4
View File
@@ -1,5 +1,5 @@
import { json } from '@sveltejs/kit';
import { downStack, ExternalStackError, ComposeFileNotFoundError } from '$lib/server/stacks';
import { downStack, ComposeFileNotFoundError } from '$lib/server/stacks';
import { authorize } from '$lib/server/authorize';
import { auditStack } from '$lib/server/audit';
import type { RequestHandler } from './$types';
@@ -42,9 +42,6 @@ export const POST: RequestHandler = async (event) => {
}
return json({ success: true, output: result.output });
} catch (error) {
if (error instanceof ExternalStackError) {
return json({ error: error.message }, { status: 400 });
}
if (error instanceof ComposeFileNotFoundError) {
return json({ error: error.message }, { status: 404 });
}
+32 -7
View File
@@ -1,9 +1,9 @@
import { json } from '@sveltejs/kit';
import { getStackEnvVars, setStackEnvVars } from '$lib/server/db';
import { getStacksDir } from '$lib/server/stacks';
import { getStackEnvVars, setStackEnvVars, getStackSource } from '$lib/server/db';
import { findStackDir } from '$lib/server/stacks';
import { authorize } from '$lib/server/authorize';
import { existsSync } from 'node:fs';
import { join } from 'node:path';
import { join, dirname } from 'node:path';
import type { RequestHandler } from './$types';
/**
@@ -61,12 +61,37 @@ export const GET: RequestHandler = async ({ params, url, cookies }) => {
const dbVariables = await getStackEnvVars(stackName, envIdNum, true);
const dbByKey = new Map(dbVariables.map(v => [v.key, v]));
// Try to read .env file from stack directory (only contains non-secrets)
const stacksDir = getStacksDir();
const envFilePath = join(stacksDir, stackName, '.env');
// Check if this stack has a custom compose path configured
const source = await getStackSource(stackName, envIdNum);
// Determine the env file path based on path resolution rules:
// - envPath = '' (empty string) → explicitly no env file
// - envPath = '/path/.env' → use custom path
// - envPath = null with composePath → suggest .env next to compose (but don't auto-load)
// - envPath = null without composePath → use default location
let envFilePath: string | null = null;
if (source?.envPath === '') {
// Empty string = explicitly no env file
envFilePath = null;
} else if (source?.envPath) {
// Custom env path specified
envFilePath = source.envPath;
} else if (source?.composePath) {
// Custom compose path but no env path - suggest .env next to compose
// For loading, check if it exists (but don't fail if it doesn't)
envFilePath = join(dirname(source.composePath), '.env');
} else {
// Default location - .env in stack directory
const stackDir = await findStackDir(stackName, envIdNum);
if (stackDir) {
envFilePath = join(stackDir, '.env');
}
}
let fileVars: Record<string, string> = {};
if (existsSync(envFilePath)) {
if (envFilePath && existsSync(envFilePath)) {
try {
const content = await Bun.file(envFilePath).text();
fileVars = parseEnvFile(content);
+59 -10
View File
@@ -1,8 +1,9 @@
import { json } from '@sveltejs/kit';
import { getStacksDir } from '$lib/server/stacks';
import { findStackDir, getStackDir } from '$lib/server/stacks';
import { getStackSource } from '$lib/server/db';
import { authorize } from '$lib/server/authorize';
import { existsSync, rmSync } from 'node:fs';
import { join } from 'node:path';
import { join, dirname } from 'node:path';
import type { RequestHandler } from './$types';
/**
@@ -26,11 +27,36 @@ export const GET: RequestHandler = async ({ params, url, cookies }) => {
try {
const stackName = decodeURIComponent(params.name);
const stacksDir = getStacksDir();
const envFilePath = join(stacksDir, stackName, '.env');
// Check if this stack has custom paths configured
const source = await getStackSource(stackName, envIdNum);
// Determine the env file path based on path resolution rules:
// - envPath = '' (empty string) → explicitly no env file
// - envPath = '/path/.env' → use custom path
// - envPath = null with composePath → suggest .env next to compose
// - envPath = null without composePath → use default location
let envFilePath: string | null = null;
if (source?.envPath === '') {
// Empty string = explicitly no env file
return json({ content: '', noEnvFile: true });
} else if (source?.envPath) {
// Custom env path specified
envFilePath = source.envPath;
} else if (source?.composePath) {
// Custom compose path but no env path - suggest .env next to compose
envFilePath = join(dirname(source.composePath), '.env');
} else {
// Default location - .env in stack directory
const stackDir = await findStackDir(stackName, envIdNum);
if (stackDir) {
envFilePath = join(stackDir, '.env');
}
}
let content = '';
if (existsSync(envFilePath)) {
if (envFilePath && existsSync(envFilePath)) {
try {
content = await Bun.file(envFilePath).text();
} catch {
@@ -73,12 +99,35 @@ export const PUT: RequestHandler = async ({ params, url, cookies, request }) =>
return json({ error: 'Invalid request body: content string required' }, { status: 400 });
}
const stacksDir = getStacksDir();
const stackDir = join(stacksDir, stackName);
const envFilePath = join(stackDir, '.env');
// Check if this stack has custom paths configured
const source = await getStackSource(stackName, envIdNum);
// Only write if stack directory exists
if (!existsSync(stackDir)) {
// Determine the env file path based on path resolution rules:
// - envPath = '' (empty string) → explicitly no env file, don't write
// - envPath = '/path/.env' → use custom path
// - envPath = null with composePath → suggest .env next to compose
// - envPath = null without composePath → use default location
let envFilePath: string | null = null;
if (source?.envPath === '') {
// Empty string = explicitly no env file - don't allow writes
return json({ success: true, noEnvFile: true });
} else if (source?.envPath) {
// Custom env path specified
envFilePath = source.envPath;
} else if (source?.composePath) {
// Custom compose path but no env path - suggest .env next to compose
envFilePath = join(dirname(source.composePath), '.env');
} else {
// Default location - .env in stack directory
const stackDir = await findStackDir(stackName, envIdNum);
if (stackDir) {
envFilePath = join(stackDir, '.env');
}
}
// Only write if we have a valid path
if (!envFilePath) {
return json({ error: 'Stack directory not found' }, { status: 404 });
}
@@ -1,5 +1,5 @@
import { json } from '@sveltejs/kit';
import { restartStack, ExternalStackError, ComposeFileNotFoundError } from '$lib/server/stacks';
import { restartStack, ComposeFileNotFoundError } from '$lib/server/stacks';
import { authorize } from '$lib/server/authorize';
import { auditStack } from '$lib/server/audit';
import type { RequestHandler } from './$types';
@@ -33,9 +33,6 @@ export const POST: RequestHandler = async (event) => {
}
return json({ success: true, output: result.output });
} catch (error) {
if (error instanceof ExternalStackError) {
return json({ error: error.message }, { status: 400 });
}
if (error instanceof ComposeFileNotFoundError) {
return json({ error: error.message }, { status: 404 });
}
@@ -1,5 +1,5 @@
import { json } from '@sveltejs/kit';
import { startStack, ExternalStackError, ComposeFileNotFoundError } from '$lib/server/stacks';
import { startStack, ComposeFileNotFoundError } from '$lib/server/stacks';
import { authorize } from '$lib/server/authorize';
import { auditStack } from '$lib/server/audit';
import type { RequestHandler } from './$types';
@@ -33,9 +33,6 @@ export const POST: RequestHandler = async (event) => {
}
return json({ success: true, output: result.output });
} catch (error) {
if (error instanceof ExternalStackError) {
return json({ error: error.message }, { status: 400 });
}
if (error instanceof ComposeFileNotFoundError) {
return json({ error: error.message }, { status: 404 });
}
+1 -4
View File
@@ -1,5 +1,5 @@
import { json } from '@sveltejs/kit';
import { stopStack, ExternalStackError, ComposeFileNotFoundError } from '$lib/server/stacks';
import { stopStack, ComposeFileNotFoundError } from '$lib/server/stacks';
import { authorize } from '$lib/server/authorize';
import { auditStack } from '$lib/server/audit';
import type { RequestHandler } from './$types';
@@ -33,9 +33,6 @@ export const POST: RequestHandler = async (event) => {
}
return json({ success: true, output: result.output });
} catch (error) {
if (error instanceof ExternalStackError) {
return json({ error: error.message }, { status: 400 });
}
if (error instanceof ComposeFileNotFoundError) {
return json({ error: error.message }, { status: 404 });
}
+2 -1
View File
@@ -18,10 +18,11 @@ export const GET: RequestHandler = async ({ url, cookies }) => {
const sources = await getStackSources(envIdNum);
// Convert to a map for easier lookup in the frontend
const sourceMap: Record<string, { sourceType: string; repository?: any }> = {};
const sourceMap: Record<string, { sourceType: string; composePath?: string | null; repository?: any }> = {};
for (const source of sources) {
sourceMap[source.stackName] = {
sourceType: source.sourceType,
composePath: source.composePath,
repository: source.repository
};
}