mirror of
https://github.com/khoaliber/dockhand.git
synced 2026-03-06 21:29:05 +00:00
Initial commit
This commit is contained in:
131
routes/api/stacks/+server.ts
Normal file
131
routes/api/stacks/+server.ts
Normal file
@@ -0,0 +1,131 @@
|
||||
import { json } from '@sveltejs/kit';
|
||||
import { listComposeStacks, deployStack, saveStackComposeFile } from '$lib/server/stacks';
|
||||
import { EnvironmentNotFoundError } from '$lib/server/docker';
|
||||
import { upsertStackSource, getStackSources } from '$lib/server/db';
|
||||
import { authorize } from '$lib/server/authorize';
|
||||
import type { RequestHandler } from './$types';
|
||||
|
||||
export const GET: RequestHandler = async ({ url, cookies }) => {
|
||||
const auth = await authorize(cookies);
|
||||
|
||||
const envId = url.searchParams.get('env');
|
||||
const envIdNum = envId ? parseInt(envId) : undefined;
|
||||
|
||||
// Permission check with environment context
|
||||
if (auth.authEnabled && !(await auth.can('stacks', 'view', envIdNum))) {
|
||||
return json({ error: 'Permission denied' }, { status: 403 });
|
||||
}
|
||||
|
||||
// Environment access check (enterprise only)
|
||||
if (envIdNum && auth.isEnterprise && !(await auth.canAccessEnvironment(envIdNum))) {
|
||||
return json({ error: 'Access denied to this environment' }, { status: 403 });
|
||||
}
|
||||
|
||||
// Early return if no environment specified
|
||||
if (!envIdNum) {
|
||||
return json([]);
|
||||
}
|
||||
|
||||
try {
|
||||
const stacks = await listComposeStacks(envIdNum);
|
||||
|
||||
// Add stacks from database that are internally managed but don't have containers yet
|
||||
// (created with "Create" button, not "Create & Start")
|
||||
const stackSources = await getStackSources(envIdNum);
|
||||
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')
|
||||
) {
|
||||
stacks.push({
|
||||
name: source.stackName,
|
||||
containers: [],
|
||||
containerDetails: [],
|
||||
status: 'created' as any
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return json(stacks);
|
||||
} catch (error) {
|
||||
if (error instanceof EnvironmentNotFoundError) {
|
||||
return json({ error: 'Environment not found' }, { status: 404 });
|
||||
}
|
||||
console.error('Error listing compose stacks:', error);
|
||||
// Return empty array instead of error to allow UI to load
|
||||
return json([]);
|
||||
}
|
||||
};
|
||||
|
||||
export const POST: RequestHandler = async ({ request, url, cookies }) => {
|
||||
const auth = await authorize(cookies);
|
||||
|
||||
const envId = url.searchParams.get('env');
|
||||
const envIdNum = envId ? parseInt(envId) : undefined;
|
||||
|
||||
// Permission check with environment context
|
||||
if (auth.authEnabled && !(await auth.can('stacks', 'create', envIdNum))) {
|
||||
return json({ error: 'Permission denied' }, { status: 403 });
|
||||
}
|
||||
|
||||
// Environment access check (enterprise only)
|
||||
if (envIdNum && auth.isEnterprise && !(await auth.canAccessEnvironment(envIdNum))) {
|
||||
return json({ error: 'Access denied to this environment' }, { status: 403 });
|
||||
}
|
||||
|
||||
try {
|
||||
const body = await request.json();
|
||||
const { name, compose, start } = body;
|
||||
|
||||
if (!name || typeof name !== 'string') {
|
||||
return json({ error: 'Stack name is required' }, { status: 400 });
|
||||
}
|
||||
|
||||
if (!compose || typeof compose !== 'string') {
|
||||
return json({ error: 'Compose file content is required' }, { status: 400 });
|
||||
}
|
||||
|
||||
// If start is false, only create the compose file without deploying
|
||||
if (start === false) {
|
||||
const result = await saveStackComposeFile(name, compose, true);
|
||||
if (!result.success) {
|
||||
return json({ error: result.error }, { status: 400 });
|
||||
}
|
||||
|
||||
// Record the stack as internally created
|
||||
await upsertStackSource({
|
||||
stackName: name,
|
||||
environmentId: envIdNum,
|
||||
sourceType: 'internal'
|
||||
});
|
||||
|
||||
return json({ success: true, started: false });
|
||||
}
|
||||
|
||||
// Deploy and start the stack
|
||||
const result = await deployStack({
|
||||
name,
|
||||
compose,
|
||||
envId: envIdNum
|
||||
});
|
||||
|
||||
if (!result.success) {
|
||||
return json({ error: result.error, output: result.output }, { status: 400 });
|
||||
}
|
||||
|
||||
// Record the stack as internally created
|
||||
await upsertStackSource({
|
||||
stackName: name,
|
||||
environmentId: envIdNum,
|
||||
sourceType: 'internal'
|
||||
});
|
||||
|
||||
return json({ success: true, started: true, output: result.output });
|
||||
} catch (error: any) {
|
||||
console.error('Error creating compose stack:', error);
|
||||
return json({ error: error.message || 'Failed to create stack' }, { status: 500 });
|
||||
}
|
||||
};
|
||||
46
routes/api/stacks/[name]/+server.ts
Normal file
46
routes/api/stacks/[name]/+server.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import { json } from '@sveltejs/kit';
|
||||
import { removeStack, ExternalStackError, ComposeFileNotFoundError } from '$lib/server/stacks';
|
||||
import { authorize } from '$lib/server/authorize';
|
||||
import { auditStack } from '$lib/server/audit';
|
||||
import type { RequestHandler } from './$types';
|
||||
|
||||
export const DELETE: RequestHandler = async (event) => {
|
||||
const { params, url, cookies } = event;
|
||||
const auth = await authorize(cookies);
|
||||
|
||||
const force = url.searchParams.get('force') === 'true';
|
||||
const envId = url.searchParams.get('env');
|
||||
const envIdNum = envId ? parseInt(envId) : undefined;
|
||||
|
||||
// Permission check with environment context
|
||||
if (auth.authEnabled && !(await auth.can('stacks', 'remove', envIdNum))) {
|
||||
return json({ error: 'Permission denied' }, { status: 403 });
|
||||
}
|
||||
|
||||
// Environment access check (enterprise only)
|
||||
if (envIdNum && auth.isEnterprise && !(await auth.canAccessEnvironment(envIdNum))) {
|
||||
return json({ error: 'Access denied to this environment' }, { status: 403 });
|
||||
}
|
||||
|
||||
try {
|
||||
const stackName = decodeURIComponent(params.name);
|
||||
const result = await removeStack(stackName, envIdNum, force);
|
||||
|
||||
// Audit log
|
||||
await auditStack(event, 'delete', stackName, envIdNum, { force });
|
||||
|
||||
if (!result.success) {
|
||||
return json({ success: false, error: result.error }, { status: 400 });
|
||||
}
|
||||
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 });
|
||||
}
|
||||
console.error('Error removing compose stack:', error);
|
||||
return json({ error: 'Failed to remove compose stack' }, { status: 500 });
|
||||
}
|
||||
};
|
||||
72
routes/api/stacks/[name]/compose/+server.ts
Normal file
72
routes/api/stacks/[name]/compose/+server.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
import { json } from '@sveltejs/kit';
|
||||
import type { RequestHandler } from './$types';
|
||||
import { getStackComposeFile, deployStack, saveStackComposeFile } from '$lib/server/stacks';
|
||||
import { authorize } from '$lib/server/authorize';
|
||||
|
||||
// GET /api/stacks/[name]/compose - Get compose file content
|
||||
export const GET: RequestHandler = async ({ params, cookies }) => {
|
||||
const auth = await authorize(cookies);
|
||||
if (auth.authEnabled && !(await auth.can('stacks', 'view'))) {
|
||||
return json({ error: 'Permission denied' }, { status: 403 });
|
||||
}
|
||||
|
||||
const { name } = params;
|
||||
|
||||
try {
|
||||
const result = await getStackComposeFile(name);
|
||||
|
||||
if (!result.success) {
|
||||
return json({ error: result.error }, { status: 404 });
|
||||
}
|
||||
|
||||
return json({ content: result.content });
|
||||
} 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 });
|
||||
}
|
||||
};
|
||||
|
||||
// PUT /api/stacks/[name]/compose - Update compose file content
|
||||
export const PUT: RequestHandler = async ({ params, request, url, cookies }) => {
|
||||
const auth = await authorize(cookies);
|
||||
|
||||
const { name } = params;
|
||||
const envId = url.searchParams.get('env');
|
||||
const envIdNum = envId ? parseInt(envId) : undefined;
|
||||
|
||||
// Permission check with environment context
|
||||
if (auth.authEnabled && !(await auth.can('stacks', 'edit', envIdNum))) {
|
||||
return json({ error: 'Permission denied' }, { status: 403 });
|
||||
}
|
||||
|
||||
try {
|
||||
const body = await request.json();
|
||||
const { content, restart = false } = body;
|
||||
|
||||
if (!content || typeof content !== 'string') {
|
||||
return json({ error: 'Compose file content is required' }, { status: 400 });
|
||||
}
|
||||
|
||||
let result;
|
||||
if (restart) {
|
||||
// Deploy with docker compose up -d (only recreates changed services)
|
||||
result = await deployStack({
|
||||
name,
|
||||
compose: content,
|
||||
envId: envIdNum
|
||||
});
|
||||
} else {
|
||||
// Just save the file without restarting
|
||||
result = await saveStackComposeFile(name, content);
|
||||
}
|
||||
|
||||
if (!result.success) {
|
||||
return json({ error: result.error }, { status: 500 });
|
||||
}
|
||||
|
||||
return json({ success: true });
|
||||
} catch (error: any) {
|
||||
console.error(`Error updating compose file for stack ${name}:`, error);
|
||||
return json({ error: error.message || 'Failed to update compose file' }, { status: 500 });
|
||||
}
|
||||
};
|
||||
54
routes/api/stacks/[name]/down/+server.ts
Normal file
54
routes/api/stacks/[name]/down/+server.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
import { json } from '@sveltejs/kit';
|
||||
import { downStack, ExternalStackError, ComposeFileNotFoundError } from '$lib/server/stacks';
|
||||
import { authorize } from '$lib/server/authorize';
|
||||
import { auditStack } from '$lib/server/audit';
|
||||
import type { RequestHandler } from './$types';
|
||||
|
||||
export const POST: RequestHandler = async (event) => {
|
||||
const { params, url, cookies, request } = event;
|
||||
const auth = await authorize(cookies);
|
||||
|
||||
const envId = url.searchParams.get('env');
|
||||
const envIdNum = envId ? parseInt(envId) : undefined;
|
||||
|
||||
// Permission check with environment context
|
||||
if (auth.authEnabled && !(await auth.can('stacks', 'stop', envIdNum))) {
|
||||
return json({ error: 'Permission denied' }, { status: 403 });
|
||||
}
|
||||
|
||||
// Environment access check (enterprise only)
|
||||
if (envIdNum && auth.isEnterprise && !(await auth.canAccessEnvironment(envIdNum))) {
|
||||
return json({ error: 'Access denied to this environment' }, { status: 403 });
|
||||
}
|
||||
|
||||
try {
|
||||
// Parse body for optional removeVolumes flag
|
||||
let removeVolumes = false;
|
||||
try {
|
||||
const body = await request.json();
|
||||
removeVolumes = body.removeVolumes === true;
|
||||
} catch {
|
||||
// No body or invalid JSON - use defaults
|
||||
}
|
||||
|
||||
const stackName = decodeURIComponent(params.name);
|
||||
const result = await downStack(stackName, envIdNum, removeVolumes);
|
||||
|
||||
// Audit log
|
||||
await auditStack(event, 'down', stackName, envIdNum, { removeVolumes });
|
||||
|
||||
if (!result.success) {
|
||||
return json({ success: false, error: result.error }, { status: 400 });
|
||||
}
|
||||
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 });
|
||||
}
|
||||
console.error('Error downing compose stack:', error);
|
||||
return json({ error: 'Failed to down compose stack' }, { status: 500 });
|
||||
}
|
||||
};
|
||||
122
routes/api/stacks/[name]/env/+server.ts
vendored
Normal file
122
routes/api/stacks/[name]/env/+server.ts
vendored
Normal file
@@ -0,0 +1,122 @@
|
||||
import { json } from '@sveltejs/kit';
|
||||
import { getStackEnvVars, setStackEnvVars } from '$lib/server/db';
|
||||
import { authorize } from '$lib/server/authorize';
|
||||
import type { RequestHandler } from './$types';
|
||||
|
||||
/**
|
||||
* GET /api/stacks/[name]/env?env=X
|
||||
* Get all environment variables for a stack.
|
||||
* Secrets are masked with '***' in the response.
|
||||
*/
|
||||
export const GET: RequestHandler = async ({ params, url, cookies }) => {
|
||||
const auth = await authorize(cookies);
|
||||
const envId = url.searchParams.get('env');
|
||||
const envIdNum = envId ? parseInt(envId) : null;
|
||||
|
||||
// Permission check with environment context
|
||||
if (auth.authEnabled && !await auth.can('stacks', 'view', envIdNum ?? undefined)) {
|
||||
return json({ error: 'Permission denied' }, { status: 403 });
|
||||
}
|
||||
|
||||
// Environment access check (enterprise only)
|
||||
if (envIdNum && auth.isEnterprise && !await auth.canAccessEnvironment(envIdNum)) {
|
||||
return json({ error: 'Access denied to this environment' }, { status: 403 });
|
||||
}
|
||||
|
||||
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
|
||||
}))
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error getting stack env vars:', error);
|
||||
return json({ error: 'Failed to get environment variables' }, { status: 500 });
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* PUT /api/stacks/[name]/env?env=X
|
||||
* Set/replace all environment variables for a stack.
|
||||
* Body: { variables: [{ key, value, isSecret? }] }
|
||||
*
|
||||
* Note: For secrets, if the value is '***' (the masked placeholder), the original
|
||||
* secret value from the database is preserved instead of overwriting with '***'.
|
||||
*/
|
||||
export const PUT: RequestHandler = async ({ params, url, cookies, request }) => {
|
||||
const auth = await authorize(cookies);
|
||||
const envId = url.searchParams.get('env');
|
||||
const envIdNum = envId ? parseInt(envId) : null;
|
||||
|
||||
// Permission check with environment context
|
||||
if (auth.authEnabled && !await auth.can('stacks', 'edit', envIdNum ?? undefined)) {
|
||||
return json({ error: 'Permission denied' }, { status: 403 });
|
||||
}
|
||||
|
||||
// Environment access check (enterprise only)
|
||||
if (envIdNum && auth.isEnterprise && !await auth.canAccessEnvironment(envIdNum)) {
|
||||
return json({ error: 'Access denied to this environment' }, { status: 403 });
|
||||
}
|
||||
|
||||
try {
|
||||
const stackName = decodeURIComponent(params.name);
|
||||
const body = await request.json();
|
||||
|
||||
if (!body.variables || !Array.isArray(body.variables)) {
|
||||
return json({ error: 'Invalid request body: variables array required' }, { status: 400 });
|
||||
}
|
||||
|
||||
// Validate variables
|
||||
for (const v of body.variables) {
|
||||
if (!v.key || typeof v.key !== 'string') {
|
||||
return json({ error: 'Invalid variable: key is required and must be a string' }, { status: 400 });
|
||||
}
|
||||
if (typeof v.value !== 'string') {
|
||||
return json({ error: `Invalid variable "${v.key}": value must be a string` }, { status: 400 });
|
||||
}
|
||||
// Validate key format (env var naming convention)
|
||||
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(v.key)) {
|
||||
return json({ error: `Invalid variable name "${v.key}": must start with a letter or underscore and contain only alphanumeric characters and underscores` }, { status: 400 });
|
||||
}
|
||||
}
|
||||
|
||||
// Check if any secrets have the masked placeholder '***'
|
||||
// If so, we need to preserve their original values from the database
|
||||
const secretsWithMaskedValue = body.variables.filter(
|
||||
(v: { key: string; value: string; isSecret?: boolean }) =>
|
||||
v.isSecret && v.value === '***'
|
||||
);
|
||||
|
||||
let variablesToSave = body.variables;
|
||||
|
||||
if (secretsWithMaskedValue.length > 0) {
|
||||
// Get existing variables (unmasked) to preserve secret values
|
||||
const existingVars = await getStackEnvVars(stackName, envIdNum, false);
|
||||
const existingByKey = new Map(existingVars.map(v => [v.key, v]));
|
||||
|
||||
// Replace masked secrets with their original values
|
||||
variablesToSave = body.variables.map((v: { key: string; value: string; isSecret?: boolean }) => {
|
||||
if (v.isSecret && v.value === '***') {
|
||||
const existing = existingByKey.get(v.key);
|
||||
if (existing && existing.isSecret) {
|
||||
// Preserve the original secret value
|
||||
return { ...v, value: existing.value };
|
||||
}
|
||||
}
|
||||
return v;
|
||||
});
|
||||
}
|
||||
|
||||
await setStackEnvVars(stackName, envIdNum, variablesToSave);
|
||||
|
||||
return json({ success: true, count: variablesToSave.length });
|
||||
} catch (error) {
|
||||
console.error('Error setting stack env vars:', error);
|
||||
return json({ error: 'Failed to set environment variables' }, { status: 500 });
|
||||
}
|
||||
};
|
||||
148
routes/api/stacks/[name]/env/validate/+server.ts
vendored
Normal file
148
routes/api/stacks/[name]/env/validate/+server.ts
vendored
Normal file
@@ -0,0 +1,148 @@
|
||||
import { json } from '@sveltejs/kit';
|
||||
import { getStackEnvVars } from '$lib/server/db';
|
||||
import { getStackComposeFile } from '$lib/server/stacks';
|
||||
import { authorize } from '$lib/server/authorize';
|
||||
import type { RequestHandler } from './$types';
|
||||
|
||||
interface ValidationResult {
|
||||
valid: boolean;
|
||||
required: string[];
|
||||
optional: string[];
|
||||
defined: string[];
|
||||
missing: string[];
|
||||
unused: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract environment variables from compose YAML content.
|
||||
* Matches ${VAR_NAME} and ${VAR_NAME:-default} patterns.
|
||||
* Returns { required: [...], optional: [...] }
|
||||
*/
|
||||
function extractComposeVars(yaml: string): { required: string[]; optional: string[] } {
|
||||
const required: string[] = [];
|
||||
const optional: string[] = [];
|
||||
|
||||
// Match ${VAR_NAME} (required) and ${VAR_NAME:-default} or ${VAR_NAME-default} (optional)
|
||||
const regex = /\$\{([A-Za-z_][A-Za-z0-9_]*)(?:(:-?)[^}]*)?\}/g;
|
||||
let match;
|
||||
|
||||
while ((match = regex.exec(yaml)) !== null) {
|
||||
const varName = match[1];
|
||||
const hasDefault = match[2] !== undefined;
|
||||
|
||||
if (hasDefault) {
|
||||
if (!optional.includes(varName) && !required.includes(varName)) {
|
||||
optional.push(varName);
|
||||
}
|
||||
} else {
|
||||
// Move from optional to required if we find a non-default usage
|
||||
const optIdx = optional.indexOf(varName);
|
||||
if (optIdx !== -1) {
|
||||
optional.splice(optIdx, 1);
|
||||
}
|
||||
if (!required.includes(varName)) {
|
||||
required.push(varName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Also match $VAR_NAME (simple variable substitution)
|
||||
const simpleRegex = /\$([A-Za-z_][A-Za-z0-9_]*)(?![{A-Za-z0-9_])/g;
|
||||
while ((match = simpleRegex.exec(yaml)) !== null) {
|
||||
const varName = match[1];
|
||||
if (!required.includes(varName) && !optional.includes(varName)) {
|
||||
required.push(varName);
|
||||
}
|
||||
}
|
||||
|
||||
return { required: required.sort(), optional: optional.sort() };
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /api/stacks/[name]/env/validate?env=X
|
||||
* Validate stack environment variables against compose file requirements.
|
||||
* Can use saved compose file or accept compose content in body.
|
||||
* Body (optional): { compose: "yaml content..." }
|
||||
*/
|
||||
export const POST: RequestHandler = async ({ params, url, cookies, request }) => {
|
||||
const auth = await authorize(cookies);
|
||||
const envId = url.searchParams.get('env');
|
||||
const envIdNum = envId ? parseInt(envId) : null;
|
||||
|
||||
// Permission check with environment context
|
||||
if (auth.authEnabled && !await auth.can('stacks', 'view', envIdNum ?? undefined)) {
|
||||
return json({ error: 'Permission denied' }, { status: 403 });
|
||||
}
|
||||
|
||||
// Environment access check (enterprise only)
|
||||
if (envIdNum && auth.isEnterprise && !await auth.canAccessEnvironment(envIdNum)) {
|
||||
return json({ error: 'Access denied to this environment' }, { status: 403 });
|
||||
}
|
||||
|
||||
try {
|
||||
const stackName = decodeURIComponent(params.name);
|
||||
let composeContent: string | null = null;
|
||||
let providedVariables: string[] | null = null;
|
||||
|
||||
// Check if compose content and/or variables are provided in body
|
||||
const contentType = request.headers.get('content-type');
|
||||
if (contentType?.includes('application/json')) {
|
||||
try {
|
||||
const body = await request.json();
|
||||
if (body.compose && typeof body.compose === 'string') {
|
||||
composeContent = body.compose;
|
||||
}
|
||||
// Accept variables from UI for validation (overrides DB lookup)
|
||||
if (Array.isArray(body.variables)) {
|
||||
providedVariables = body.variables.filter((v: unknown) => typeof v === 'string');
|
||||
}
|
||||
} catch {
|
||||
// Ignore JSON parse errors - will try to load from file
|
||||
}
|
||||
}
|
||||
|
||||
// If no compose in body, try to load from saved file
|
||||
if (!composeContent) {
|
||||
const savedCompose = await getStackComposeFile(stackName);
|
||||
if (savedCompose.success && savedCompose.content) {
|
||||
composeContent = savedCompose.content;
|
||||
}
|
||||
}
|
||||
|
||||
if (!composeContent) {
|
||||
return json({ error: 'No compose content provided and no saved compose file found' }, { status: 400 });
|
||||
}
|
||||
|
||||
// Extract variables from compose
|
||||
const { required, optional } = extractComposeVars(composeContent);
|
||||
|
||||
// Get defined variables - either from request body or database
|
||||
let defined: string[];
|
||||
if (providedVariables !== null) {
|
||||
// Use provided variables from UI
|
||||
defined = providedVariables.sort();
|
||||
} else {
|
||||
// Fall back to database
|
||||
const envVars = await getStackEnvVars(stackName, envIdNum, false);
|
||||
defined = envVars.map(v => v.key).sort();
|
||||
}
|
||||
|
||||
// Calculate missing and unused
|
||||
const missing = required.filter(v => !defined.includes(v));
|
||||
const unused = defined.filter(v => !required.includes(v) && !optional.includes(v));
|
||||
|
||||
const result: ValidationResult = {
|
||||
valid: missing.length === 0,
|
||||
required,
|
||||
optional,
|
||||
defined,
|
||||
missing,
|
||||
unused
|
||||
};
|
||||
|
||||
return json(result);
|
||||
} catch (error) {
|
||||
console.error('Error validating stack env vars:', error);
|
||||
return json({ error: 'Failed to validate environment variables' }, { status: 500 });
|
||||
}
|
||||
};
|
||||
45
routes/api/stacks/[name]/restart/+server.ts
Normal file
45
routes/api/stacks/[name]/restart/+server.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import { json } from '@sveltejs/kit';
|
||||
import { restartStack, ExternalStackError, ComposeFileNotFoundError } from '$lib/server/stacks';
|
||||
import { authorize } from '$lib/server/authorize';
|
||||
import { auditStack } from '$lib/server/audit';
|
||||
import type { RequestHandler } from './$types';
|
||||
|
||||
export const POST: RequestHandler = async (event) => {
|
||||
const { params, url, cookies } = event;
|
||||
const auth = await authorize(cookies);
|
||||
|
||||
const envId = url.searchParams.get('env');
|
||||
const envIdNum = envId ? parseInt(envId) : undefined;
|
||||
|
||||
// Permission check with environment context
|
||||
if (auth.authEnabled && !(await auth.can('stacks', 'restart', envIdNum))) {
|
||||
return json({ error: 'Permission denied' }, { status: 403 });
|
||||
}
|
||||
|
||||
// Environment access check (enterprise only)
|
||||
if (envIdNum && auth.isEnterprise && !(await auth.canAccessEnvironment(envIdNum))) {
|
||||
return json({ error: 'Access denied to this environment' }, { status: 403 });
|
||||
}
|
||||
|
||||
try {
|
||||
const stackName = decodeURIComponent(params.name);
|
||||
const result = await restartStack(stackName, envIdNum);
|
||||
|
||||
// Audit log
|
||||
await auditStack(event, 'restart', stackName, envIdNum);
|
||||
|
||||
if (!result.success) {
|
||||
return json({ success: false, error: result.error }, { status: 400 });
|
||||
}
|
||||
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 });
|
||||
}
|
||||
console.error('Error restarting compose stack:', error);
|
||||
return json({ error: 'Failed to restart compose stack' }, { status: 500 });
|
||||
}
|
||||
};
|
||||
45
routes/api/stacks/[name]/start/+server.ts
Normal file
45
routes/api/stacks/[name]/start/+server.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import { json } from '@sveltejs/kit';
|
||||
import { startStack, ExternalStackError, ComposeFileNotFoundError } from '$lib/server/stacks';
|
||||
import { authorize } from '$lib/server/authorize';
|
||||
import { auditStack } from '$lib/server/audit';
|
||||
import type { RequestHandler } from './$types';
|
||||
|
||||
export const POST: RequestHandler = async (event) => {
|
||||
const { params, url, cookies } = event;
|
||||
const auth = await authorize(cookies);
|
||||
|
||||
const envId = url.searchParams.get('env');
|
||||
const envIdNum = envId ? parseInt(envId) : undefined;
|
||||
|
||||
// Permission check with environment context
|
||||
if (auth.authEnabled && !(await auth.can('stacks', 'start', envIdNum))) {
|
||||
return json({ error: 'Permission denied' }, { status: 403 });
|
||||
}
|
||||
|
||||
// Environment access check (enterprise only)
|
||||
if (envIdNum && auth.isEnterprise && !(await auth.canAccessEnvironment(envIdNum))) {
|
||||
return json({ error: 'Access denied to this environment' }, { status: 403 });
|
||||
}
|
||||
|
||||
try {
|
||||
const stackName = decodeURIComponent(params.name);
|
||||
const result = await startStack(stackName, envIdNum);
|
||||
|
||||
// Audit log
|
||||
await auditStack(event, 'start', stackName, envIdNum);
|
||||
|
||||
if (!result.success) {
|
||||
return json({ success: false, error: result.error }, { status: 400 });
|
||||
}
|
||||
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 });
|
||||
}
|
||||
console.error('Error starting compose stack:', error);
|
||||
return json({ error: 'Failed to start compose stack' }, { status: 500 });
|
||||
}
|
||||
};
|
||||
45
routes/api/stacks/[name]/stop/+server.ts
Normal file
45
routes/api/stacks/[name]/stop/+server.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import { json } from '@sveltejs/kit';
|
||||
import { stopStack, ExternalStackError, ComposeFileNotFoundError } from '$lib/server/stacks';
|
||||
import { authorize } from '$lib/server/authorize';
|
||||
import { auditStack } from '$lib/server/audit';
|
||||
import type { RequestHandler } from './$types';
|
||||
|
||||
export const POST: RequestHandler = async (event) => {
|
||||
const { params, url, cookies } = event;
|
||||
const auth = await authorize(cookies);
|
||||
|
||||
const envId = url.searchParams.get('env');
|
||||
const envIdNum = envId ? parseInt(envId) : undefined;
|
||||
|
||||
// Permission check with environment context
|
||||
if (auth.authEnabled && !(await auth.can('stacks', 'stop', envIdNum))) {
|
||||
return json({ error: 'Permission denied' }, { status: 403 });
|
||||
}
|
||||
|
||||
// Environment access check (enterprise only)
|
||||
if (envIdNum && auth.isEnterprise && !(await auth.canAccessEnvironment(envIdNum))) {
|
||||
return json({ error: 'Access denied to this environment' }, { status: 403 });
|
||||
}
|
||||
|
||||
try {
|
||||
const stackName = decodeURIComponent(params.name);
|
||||
const result = await stopStack(stackName, envIdNum);
|
||||
|
||||
// Audit log
|
||||
await auditStack(event, 'stop', stackName, envIdNum);
|
||||
|
||||
if (!result.success) {
|
||||
return json({ success: false, error: result.error }, { status: 400 });
|
||||
}
|
||||
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 });
|
||||
}
|
||||
console.error('Error stopping compose stack:', error);
|
||||
return json({ error: 'Failed to stop compose stack' }, { status: 500 });
|
||||
}
|
||||
};
|
||||
34
routes/api/stacks/sources/+server.ts
Normal file
34
routes/api/stacks/sources/+server.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import { json } from '@sveltejs/kit';
|
||||
import type { RequestHandler } from './$types';
|
||||
import { getStackSources } from '$lib/server/db';
|
||||
import { authorize } from '$lib/server/authorize';
|
||||
|
||||
export const GET: RequestHandler = async ({ url, cookies }) => {
|
||||
const auth = await authorize(cookies);
|
||||
|
||||
const envId = url.searchParams.get('env');
|
||||
const envIdNum = envId ? parseInt(envId) : undefined;
|
||||
|
||||
// Permission check with environment context
|
||||
if (auth.authEnabled && !await auth.can('stacks', 'view', envIdNum)) {
|
||||
return json({ error: 'Permission denied' }, { status: 403 });
|
||||
}
|
||||
|
||||
try {
|
||||
const sources = await getStackSources(envIdNum);
|
||||
|
||||
// Convert to a map for easier lookup in the frontend
|
||||
const sourceMap: Record<string, { sourceType: string; repository?: any }> = {};
|
||||
for (const source of sources) {
|
||||
sourceMap[source.stackName] = {
|
||||
sourceType: source.sourceType,
|
||||
repository: source.repository
|
||||
};
|
||||
}
|
||||
|
||||
return json(sourceMap);
|
||||
} catch (error) {
|
||||
console.error('Failed to get stack sources:', error);
|
||||
return json({ error: 'Failed to get stack sources' }, { status: 500 });
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user