This commit is contained in:
jarek committed 2026-01-01 16:00:34 +01:00
1 parent 5475112806
commit 242f8df49d
35 files changed
+1892 -1987

No files matched your search

+15 -2
View File
@@ -1,5 +1,5 @@
import { json } from '@sveltejs/kit';
import { listComposeStacks, deployStack, saveStackComposeFile } from '$lib/server/stacks';
import { listComposeStacks, deployStack, saveStackComposeFile, saveStackEnvVars } from '$lib/server/stacks';
import { EnvironmentNotFoundError } from '$lib/server/docker';
import { upsertStackSource, getStackSources } from '$lib/server/db';
import { authorize } from '$lib/server/authorize';
@@ -78,7 +78,7 @@ export const POST: RequestHandler = async ({ request, url, cookies }) => {
try {
const body = await request.json();
const { name, compose, start } = body;
const { name, compose, start, envVars } = body;
if (!name || typeof name !== 'string') {
return json({ error: 'Stack name is required' }, { status: 400 });
@@ -95,6 +95,11 @@ export const POST: RequestHandler = async ({ request, url, cookies }) => {
return json({ error: result.error }, { status: 400 });
}
// Save environment variables if provided (to both DB and .env file)
if (envVars && Array.isArray(envVars) && envVars.length > 0) {
await saveStackEnvVars(name, envVars, envIdNum);
}
// Record the stack as internally created
await upsertStackSource({
stackName: name,
@@ -105,6 +110,14 @@ export const POST: RequestHandler = async ({ request, url, cookies }) => {
return json({ success: true, started: false });
}
// Save environment variables BEFORE deploying so they're available during start
if (envVars && Array.isArray(envVars) && envVars.length > 0) {
// First ensure the stack directory exists by saving compose file
await saveStackComposeFile(name, compose, true);
// Save to both DB and .env file
await saveStackEnvVars(name, envVars, envIdNum);
}
// Deploy and start the stack
const result = await deployStack({
name,
+156 -8
View File
@@ -1,11 +1,96 @@
import { json } from '@sveltejs/kit';
import { getStackEnvVars, setStackEnvVars } from '$lib/server/db';
import { getStacksDir } from '$lib/server/stacks';
import { authorize } from '$lib/server/authorize';
import { existsSync } from 'node:fs';
import { join } from 'node:path';
import type { RequestHandler } from './$types';
/**
* Parse a .env file content into key-value pairs
*/
function parseEnvFile(content: string): Record<string, string> {
const result: Record<string, string> = {};
for (const line of content.split('\n')) {
const trimmed = line.trim();
// Skip empty lines and comments
if (!trimmed || trimmed.startsWith('#')) continue;
const eqIndex = trimmed.indexOf('=');
if (eqIndex > 0) {
const key = trimmed.substring(0, eqIndex).trim();
let value = trimmed.substring(eqIndex + 1);
// Remove surrounding quotes if present
if ((value.startsWith('"') && value.endsWith('"')) ||
(value.startsWith("'") && value.endsWith("'"))) {
value = value.slice(1, -1);
}
result[key] = value;
}
}
return result;
}
/**
* Merge new variables into existing .env file content.
* - Keeps comments and formatting
* - Updates values for existing keys
* - REMOVES keys that are not in newVars (user deleted them)
* - Appends new keys at the end
*/
function mergeEnvFileContent(
existingContent: string,
newVars: { key: string; value: string }[]
): string {
const newVarsMap = new Map(newVars.map(v => [v.key, v.value]));
const handledKeys = new Set<string>();
const lines = existingContent.split('\n');
const resultLines: string[] = [];
for (const line of lines) {
const trimmed = line.trim();
// Keep comments and blank lines as-is
if (!trimmed || trimmed.startsWith('#')) {
resultLines.push(line);
continue;
}
// Check if this is a variable line
const eqIndex = trimmed.indexOf('=');
if (eqIndex > 0) {
const key = trimmed.substring(0, eqIndex).trim();
if (newVarsMap.has(key)) {
// Update existing variable with new value from UI
resultLines.push(`${key}=${newVarsMap.get(key)}`);
handledKeys.add(key);
}
// If key not in newVarsMap, it was deleted - skip it (don't add to resultLines)
} else {
// Not a valid variable line, keep as-is
resultLines.push(line);
}
}
// Append any new variables that weren't in the original file
for (const v of newVars) {
if (!handledKeys.has(v.key)) {
resultLines.push(`${v.key}=${v.value}`);
}
}
// Ensure file ends with newline
let result = resultLines.join('\n');
if (!result.endsWith('\n')) {
result += '\n';
}
return result;
}
/**
* GET /api/stacks/[name]/env?env=X
* Get all environment variables for a stack.
* Merges variables from database with .env file (file values shown if different).
* Secrets are masked with '***' in the response.
*/
export const GET: RequestHandler = async ({ params, url, cookies }) => {
@@ -25,15 +110,52 @@ export const GET: RequestHandler = async ({ params, url, cookies }) => {
try {
const stackName = decodeURIComponent(params.name);
const variables = await getStackEnvVars(stackName, envIdNum, true);
return json({
variables: variables.map(v => ({
key: v.key,
value: v.value,
isSecret: v.isSecret
}))
});
// Get variables from database
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
const stacksDir = getStacksDir();
const envFilePath = join(stacksDir, stackName, '.env');
let fileVars: Record<string, string> = {};
if (existsSync(envFilePath)) {
try {
const content = await Bun.file(envFilePath).text();
fileVars = parseEnvFile(content);
} catch (e) {
// Ignore file read errors
}
}
// Merge: start with DB variables, add any new keys from file
const mergedKeys = new Set([...dbByKey.keys(), ...Object.keys(fileVars)]);
const variables: { key: string; value: string; isSecret: boolean }[] = [];
for (const key of mergedKeys) {
const dbVar = dbByKey.get(key);
const fileValue = fileVars[key];
if (dbVar) {
// Variable exists in DB
if (dbVar.isSecret) {
// Keep secret masked
variables.push({ key, value: dbVar.value, isSecret: true });
} else if (fileValue !== undefined && fileValue !== dbVar.value) {
// File has different value - use file value (user may have edited it)
variables.push({ key, value: fileValue, isSecret: false });
} else {
// Use DB value
variables.push({ key, value: dbVar.value, isSecret: false });
}
} else if (fileValue !== undefined) {
// Variable only in file - add it as non-secret
variables.push({ key, value: fileValue, isSecret: false });
}
}
return json({ variables });
} catch (error) {
console.error('Error getting stack env vars:', error);
return json({ error: 'Failed to get environment variables' }, { status: 500 });
@@ -114,6 +236,32 @@ export const PUT: RequestHandler = async ({ params, url, cookies, request }) =>
await setStackEnvVars(stackName, envIdNum, variablesToSave);
// Also write the .env file to the stack directory
// This allows users to see/edit variables outside of Dockhand
const stacksDir = getStacksDir();
const stackDir = join(stacksDir, stackName);
const envFilePath = join(stackDir, '.env');
// Only write if stack directory exists
if (existsSync(stackDir)) {
// Read existing file to preserve comments and formatting
let existingContent = '';
if (existsSync(envFilePath)) {
try {
existingContent = await Bun.file(envFilePath).text();
} catch {
// File read failed, start fresh
}
}
// Merge UI vars with existing file (preserves comments, keeps file vars)
const envContent = mergeEnvFileContent(
existingContent,
variablesToSave.map((v: { key: string; value: string }) => ({ key: v.key, value: v.value }))
);
await Bun.write(envFilePath, envContent);
}
return json({ success: true, count: variablesToSave.length });
} catch (error) {
console.error('Error setting stack env vars:', error);