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

+12 -6
View File
@@ -1,9 +1,10 @@
import { json } from '@sveltejs/kit';
import type { RequestHandler } from './$types';
import { getEnvironments, createEnvironment, assignUserRole, getRoleByName, getEnvironmentPublicIps, setEnvironmentPublicIp, getEnvUpdateCheckSettings, getEnvironmentTimezone, type Environment } from '$lib/server/db';
import { getEnvironments, getEnvironmentByName, createEnvironment, assignUserRole, getRoleByName, getEnvironmentPublicIps, setEnvironmentPublicIp, getEnvUpdateCheckSettings, getEnvironmentTimezone, type Environment } from '$lib/server/db';
import { authorize } from '$lib/server/authorize';
import { refreshSubprocessEnvironments } from '$lib/server/subprocess-manager';
import { serializeLabels, parseLabels, MAX_LABELS } from '$lib/utils/label-colors';
import { cleanPem } from '$lib/utils/pem';
export const GET: RequestHandler = async ({ cookies }) => {
const auth = await authorize(cookies);
@@ -69,6 +70,12 @@ export const POST: RequestHandler = async ({ request, cookies }) => {
return json({ error: 'Name is required' }, { status: 400 });
}
// Check if environment with this name already exists
const existing = await getEnvironmentByName(data.name);
if (existing) {
return json({ error: 'An environment with this name already exists' }, { status: 409 });
}
// Host is required for direct and hawser-standard connections
const connectionType = data.connectionType || 'socket';
if ((connectionType === 'direct' || connectionType === 'hawser-standard') && !data.host) {
@@ -83,9 +90,9 @@ export const POST: RequestHandler = async ({ request, cookies }) => {
host: data.host,
port: data.port || 2375,
protocol: data.protocol || 'http',
tlsCa: data.tlsCa,
tlsCert: data.tlsCert,
tlsKey: data.tlsKey,
tlsCa: cleanPem(data.tlsCa),
tlsCert: cleanPem(data.tlsCert),
tlsKey: cleanPem(data.tlsKey),
tlsSkipVerify: data.tlsSkipVerify || false,
icon: data.icon || 'globe',
socketPath: data.socketPath || '/var/run/docker.sock',
@@ -124,7 +131,6 @@ export const POST: RequestHandler = async ({ request, cookies }) => {
return json(env);
} catch (error) {
console.error('Failed to create environment:', error);
const message = error instanceof Error ? error.message : 'Failed to create environment';
return json({ error: message }, { status: 500 });
return json({ error: 'Failed to create environment' }, { status: 500 });
}
};
+4 -3
View File
@@ -6,6 +6,7 @@ import { deleteGitStackFiles } from '$lib/server/git';
import { authorize } from '$lib/server/authorize';
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';
@@ -62,9 +63,9 @@ export const PUT: RequestHandler = async ({ params, request, cookies }) => {
host: data.host,
port: data.port,
protocol: data.protocol,
tlsCa: data.tlsCa,
tlsCert: data.tlsCert,
tlsKey: data.tlsKey,
tlsCa: cleanPem(data.tlsCa),
tlsCert: cleanPem(data.tlsCert),
tlsKey: cleanPem(data.tlsKey),
tlsSkipVerify: data.tlsSkipVerify,
icon: data.icon,
socketPath: data.socketPath,
+41 -37
View File
@@ -60,50 +60,54 @@ export const POST: RequestHandler = async ({ request }) => {
headers['X-Hawser-Token'] = config.hawserToken;
}
// For HTTPS with custom CA or skip verification, use subprocess to avoid Vite dev server TLS issues
if (protocol === 'https' && (config.tlsCa || config.tlsSkipVerify)) {
const fs = await import('node:fs');
let tempCaPath = '';
// For HTTPS with custom CA, client certs, or skip verification, use subprocess to avoid Vite dev server TLS issues
if (protocol === 'https' && (config.tlsCa || config.tlsCert || config.tlsSkipVerify)) {
// Clean PEM content (remove extra whitespace)
const cleanPem = (pem: string) => pem
.split('\n')
.map((line) => line.trim())
.filter((line) => line.length > 0)
.join('\n');
// Clean the certificate - remove leading/trailing whitespace from each line
let cleanedCa = '';
if (config.tlsCa && !config.tlsSkipVerify) {
cleanedCa = config.tlsCa
.split('\n')
.map((line) => line.trim())
.filter((line) => line.length > 0)
.join('\n');
tempCaPath = `/tmp/dockhand-ca-${Date.now()}.pem`;
fs.writeFileSync(tempCaPath, cleanedCa);
}
// Build Bun script that runs outside Vite's process (Vite interferes with TLS)
const tlsConfig = config.tlsSkipVerify
? `tls: { rejectUnauthorized: false }`
: `tls: { ca: await Bun.file('${tempCaPath}').text() }`;
// Pass config as base64-encoded JSON to avoid escaping issues
const tlsConfig = {
url: `https://${host}:${port}/info`,
headers,
tlsSkipVerify: config.tlsSkipVerify || false,
ca: config.tlsCa && !config.tlsSkipVerify ? cleanPem(config.tlsCa) : null,
cert: config.tlsCert ? cleanPem(config.tlsCert) : null,
key: config.tlsKey ? cleanPem(config.tlsKey) : null,
host
};
const configBase64 = Buffer.from(JSON.stringify(tlsConfig)).toString('base64');
// Inline script with config embedded (bun -e doesn't pass argv correctly)
const scriptContent = `
const response = await fetch('https://${host}:${port}/info', {
headers: ${JSON.stringify(headers)},
${tlsConfig}
});
const body = await response.text();
console.log(JSON.stringify({ status: response.status, body }));
const config = JSON.parse(Buffer.from('${configBase64}', 'base64').toString());
try {
const tls = {
sessionTimeout: 0,
servername: config.host,
rejectUnauthorized: !config.tlsSkipVerify
};
if (config.ca) tls.ca = [config.ca];
if (config.cert) tls.cert = [config.cert];
if (config.key) tls.key = config.key;
const response = await fetch(config.url, {
headers: config.headers,
tls,
keepalive: false
});
const body = await response.text();
console.log(JSON.stringify({ status: response.status, body }));
} catch (e) {
console.log(JSON.stringify({ error: e.message }));
}
`;
const scriptPath = `/tmp/dockhand-test-${Date.now()}.ts`;
fs.writeFileSync(scriptPath, scriptContent);
const proc = Bun.spawn(['bun', scriptPath], { stdout: 'pipe', stderr: 'pipe' });
const proc = Bun.spawn(['bun', '-e', scriptContent], { stdout: 'pipe', stderr: 'pipe' });
const output = await new Response(proc.stdout).text();
const stderr = await new Response(proc.stderr).text();
// Cleanup temp files
if (tempCaPath) {
try { fs.unlinkSync(tempCaPath); } catch {}
}
try { fs.unlinkSync(scriptPath); } catch {}
if (!output.trim()) {
throw new Error(stderr || 'Empty response from TLS test subprocess');
}