This commit is contained in:
jarek committed 2026-01-28 07:34:12 +01:00
1 parent a88d3d5788
commit 8f3a7eb435
96 files changed
+5235 -1870

No files matched your search

+10 -3
View File
@@ -1,7 +1,8 @@
import { json } from '@sveltejs/kit';
import type { RequestHandler } from './$types';
import { getEnvironments, getEnvironmentByName, createEnvironment, assignUserRole, getRoleByName, getEnvironmentPublicIps, setEnvironmentPublicIp, getEnvUpdateCheckSettings, getEnvironmentTimezone, type Environment } from '$lib/server/db';
import { getEnvironments, getEnvironmentByName, createEnvironment, assignUserRole, getRoleByName, getEnvironmentPublicIps, setEnvironmentPublicIp, getEnvUpdateCheckSettings, getEnvironmentTimezone, getImagePruneSettings, type Environment } from '$lib/server/db';
import { authorize } from '$lib/server/authorize';
import { auditEnvironment } from '$lib/server/audit';
import { refreshSubprocessEnvironments } from '$lib/server/subprocess-manager';
import { serializeLabels, parseLabels, MAX_LABELS } from '$lib/utils/label-colors';
import { cleanPem } from '$lib/utils/pem';
@@ -36,16 +37,18 @@ export const GET: RequestHandler = async ({ cookies }) => {
}
}
// Parse labels from JSON string to array, add public IPs, update check settings, and timezone
// Parse labels from JSON string to array, add public IPs, update check settings, image prune settings, and timezone
const envWithParsedLabels = await Promise.all(environments.map(async env => {
const updateSettings = updateCheckSettingsMap.get(env.id);
const timezone = await getEnvironmentTimezone(env.id);
const imagePruneSettings = await getImagePruneSettings(env.id);
return {
...env,
labels: parseLabels(env.labels as string | null),
publicIp: publicIps[env.id.toString()] || null,
updateCheckEnabled: updateSettings?.enabled || false,
updateCheckAutoUpdate: updateSettings?.autoUpdate || false,
imagePruneEnabled: imagePruneSettings?.enabled || false,
timezone
};
}));
@@ -57,7 +60,8 @@ export const GET: RequestHandler = async ({ cookies }) => {
}
};
export const POST: RequestHandler = async ({ request, cookies }) => {
export const POST: RequestHandler = async (event) => {
const { request, cookies } = event;
const auth = await authorize(cookies);
if (auth.authEnabled && !await auth.can('environments', 'create')) {
return json({ error: 'Permission denied' }, { status: 403 });
@@ -128,6 +132,9 @@ export const POST: RequestHandler = async ({ request, cookies }) => {
}
}
// Audit log
await auditEnvironment(event, 'create', env.id, env.name);
return json(env);
} catch (error) {
console.error('Failed to create environment:', error);
+36 -4
View File
@@ -1,14 +1,16 @@
import { json } from '@sveltejs/kit';
import type { RequestHandler } from './$types';
import { getEnvironment, updateEnvironment, deleteEnvironment, getEnvironmentPublicIps, setEnvironmentPublicIp, deleteEnvironmentPublicIp, deleteEnvUpdateCheckSettings, getGitStacksForEnvironmentOnly, deleteGitStack } from '$lib/server/db';
import { getEnvironment, updateEnvironment, deleteEnvironment, getEnvironmentPublicIps, setEnvironmentPublicIp, deleteEnvironmentPublicIp, deleteEnvUpdateCheckSettings, deleteImagePruneSettings, getGitStacksForEnvironmentOnly, deleteGitStack } from '$lib/server/db';
import { clearDockerClientCache } from '$lib/server/docker';
import { deleteGitStackFiles } from '$lib/server/git';
import { authorize } from '$lib/server/authorize';
import { auditEnvironment } from '$lib/server/audit';
import { refreshSubprocessEnvironments } from '$lib/server/subprocess-manager';
import { serializeLabels, parseLabels, MAX_LABELS } from '$lib/utils/label-colors';
import { cleanPem } from '$lib/utils/pem';
import { unregisterSchedule } from '$lib/server/scheduler';
import { closeEdgeConnection } from '$lib/server/hawser';
import { computeAuditDiff } from '$lib/utils/diff';
export const GET: RequestHandler = async ({ params, cookies }) => {
const auth = await authorize(cookies);
@@ -40,7 +42,8 @@ export const GET: RequestHandler = async ({ params, cookies }) => {
}
};
export const PUT: RequestHandler = async ({ params, request, cookies }) => {
export const PUT: RequestHandler = async (event) => {
const { params, request, cookies } = event;
const auth = await authorize(cookies);
if (auth.authEnabled && !await auth.can('environments', 'edit')) {
return json({ error: 'Permission denied' }, { status: 403 });
@@ -48,6 +51,13 @@ export const PUT: RequestHandler = async ({ params, request, cookies }) => {
try {
const id = parseInt(params.id);
// Get old values before update for diff
const oldEnv = await getEnvironment(id);
if (!oldEnv) {
return json({ error: 'Environment not found' }, { status: 404 });
}
const data = await request.json();
// Clear cached Docker client before updating
@@ -95,6 +105,14 @@ export const PUT: RequestHandler = async ({ params, request, cookies }) => {
const publicIps = await getEnvironmentPublicIps();
const publicIp = publicIps[id.toString()] || null;
// Compute diff for audit (exclude sensitive TLS fields)
const diff = computeAuditDiff(oldEnv, env, {
excludeFields: ['tlsCa', 'tlsCert', 'tlsKey', 'hawserToken', 'labels']
});
// Audit log
await auditEnvironment(event, 'update', env.id, env.name, diff);
// Parse labels from JSON string to array
return json({
...env,
@@ -107,7 +125,8 @@ export const PUT: RequestHandler = async ({ params, request, cookies }) => {
}
};
export const DELETE: RequestHandler = async ({ params, cookies }) => {
export const DELETE: RequestHandler = async (event) => {
const { params, cookies } = event;
const auth = await authorize(cookies);
if (auth.authEnabled && !await auth.can('environments', 'delete')) {
return json({ error: 'Permission denied' }, { status: 403 });
@@ -116,6 +135,12 @@ export const DELETE: RequestHandler = async ({ params, cookies }) => {
try {
const id = parseInt(params.id);
// Get environment name before deletion for audit log
const env = await getEnvironment(id);
if (!env) {
return json({ error: 'Environment not found' }, { status: 404 });
}
// Close Edge connection if this is a Hawser Edge environment
// This rejects any pending requests and closes the WebSocket
closeEdgeConnection(id);
@@ -131,7 +156,7 @@ export const DELETE: RequestHandler = async ({ params, cookies }) => {
unregisterSchedule(stack.id, 'git_stack_sync');
}
// Delete git stack files from filesystem
deleteGitStackFiles(stack.id);
await deleteGitStackFiles(stack.id, stack.stackName, stack.environmentId);
// Delete git stack from database
await deleteGitStack(stack.id);
}
@@ -149,9 +174,16 @@ export const DELETE: RequestHandler = async ({ params, cookies }) => {
await deleteEnvUpdateCheckSettings(id);
unregisterSchedule(id, 'env_update_check');
// Clean up image prune settings and unregister schedule
await deleteImagePruneSettings(id);
unregisterSchedule(id, 'image_prune');
// Notify subprocesses to stop collecting from deleted environment
refreshSubprocessEnvironments();
// Audit log
await auditEnvironment(event, 'delete', id, env.name);
return json({ success: true });
} catch (error) {
console.error('Failed to delete environment:', error);
@@ -0,0 +1,121 @@
import { json } from '@sveltejs/kit';
import type { RequestHandler } from './$types';
import { authorize } from '$lib/server/authorize';
import {
getImagePruneSettings,
setImagePruneSettings,
getEnvironment
} from '$lib/server/db';
import { registerSchedule, unregisterSchedule, triggerImagePrune } from '$lib/server/scheduler';
/**
* Get image prune settings for an environment.
*/
export const GET: RequestHandler = async ({ params, cookies }) => {
const auth = await authorize(cookies);
if (auth.authEnabled && !await auth.can('environments', 'view')) {
return json({ error: 'Permission denied' }, { status: 403 });
}
try {
const id = parseInt(params.id);
// Verify environment exists
const env = await getEnvironment(id);
if (!env) {
return json({ error: 'Environment not found' }, { status: 404 });
}
const settings = await getImagePruneSettings(id);
return json({
settings: settings || {
enabled: false,
cronExpression: '0 3 * * 0', // Default: 3 AM Sunday
pruneMode: 'dangling'
}
});
} catch (error) {
console.error('Failed to get image prune settings:', error);
return json({ error: 'Failed to get image prune settings' }, { status: 500 });
}
};
/**
* Save image prune settings for an environment.
*/
export const POST: RequestHandler = async ({ params, request, cookies }) => {
const auth = await authorize(cookies);
if (auth.authEnabled && !await auth.can('environments', 'edit')) {
return json({ error: 'Permission denied' }, { status: 403 });
}
try {
const id = parseInt(params.id);
// Verify environment exists
const env = await getEnvironment(id);
if (!env) {
return json({ error: 'Environment not found' }, { status: 404 });
}
const data = await request.json();
// Get existing settings to preserve lastPruned and lastResult
const existingSettings = await getImagePruneSettings(id);
const settings = {
enabled: data.enabled ?? false,
cronExpression: data.cronExpression || '0 3 * * 0',
pruneMode: data.pruneMode || 'dangling',
lastPruned: existingSettings?.lastPruned,
lastResult: existingSettings?.lastResult
};
// Save settings to database
await setImagePruneSettings(id, settings);
// Register or unregister schedule based on enabled state
if (settings.enabled) {
await registerSchedule(id, 'image_prune', id);
} else {
unregisterSchedule(id, 'image_prune');
}
return json({ success: true, settings });
} catch (error) {
console.error('Failed to save image prune settings:', error);
return json({ error: 'Failed to save image prune settings' }, { status: 500 });
}
};
/**
* Manually trigger image prune for an environment.
*/
export const PUT: RequestHandler = async ({ params, cookies }) => {
const auth = await authorize(cookies);
if (auth.authEnabled && !await auth.can('environments', 'edit')) {
return json({ error: 'Permission denied' }, { status: 403 });
}
try {
const id = parseInt(params.id);
// Verify environment exists
const env = await getEnvironment(id);
if (!env) {
return json({ error: 'Environment not found' }, { status: 404 });
}
const result = await triggerImagePrune(id);
if (!result.success) {
return json({ error: result.error }, { status: 400 });
}
return json({ success: true });
} catch (error) {
console.error('Failed to trigger image prune:', error);
return json({ error: 'Failed to trigger image prune' }, { status: 500 });
}
};