This commit is contained in:
jarek committed 2026-02-09 10:15:21 +01:00
1 parent 1cb47eaa9c
commit 48b9bde8ae
10 files changed
+291 -145

No files matched your search

@@ -36,6 +36,7 @@ interface DockerClientConfig {
ca?: string;
cert?: string;
key?: string;
skipVerify?: boolean;
hawserToken?: string;
environmentId?: number;
}
@@ -62,6 +63,7 @@ async function getDockerConfig(envId?: number | null): Promise<DockerClientConfi
ca: env.tlsCa || undefined,
cert: env.tlsCert || undefined,
key: env.tlsKey || undefined,
skipVerify: env.tlsSkipVerify || undefined,
hawserToken: env.connectionType === 'hawser-standard' ? env.hawserToken || undefined : undefined
};
}
@@ -288,14 +290,15 @@ export const GET: RequestHandler = async ({ params, url, cookies }) => {
const fetchOpts: any = { headers: inspectHeaders };
if (config.type === 'https') {
fetchOpts.tls = {
sessionTimeout: 0, // Disable TLS session caching for mTLS
sessionTimeout: 0,
servername: config.host,
rejectUnauthorized: true
rejectUnauthorized: !config.skipVerify
};
if (config.ca) fetchOpts.tls.ca = [config.ca];
if (config.cert) fetchOpts.tls.cert = [config.cert];
if (config.key) fetchOpts.tls.key = config.key;
fetchOpts.keepalive = false;
if (process.env.DEBUG_TLS) fetchOpts.verbose = true;
}
inspectResponse = await fetch(inspectUrl, fetchOpts);
}
@@ -355,14 +358,15 @@ export const GET: RequestHandler = async ({ params, url, cookies }) => {
};
if (config.type === 'https') {
fetchOpts.tls = {
sessionTimeout: 0, // Disable TLS session caching for mTLS
sessionTimeout: 0,
servername: config.host,
rejectUnauthorized: true
rejectUnauthorized: !config.skipVerify
};
if (config.ca) fetchOpts.tls.ca = [config.ca];
if (config.cert) fetchOpts.tls.cert = [config.cert];
if (config.key) fetchOpts.tls.key = config.key;
fetchOpts.keepalive = false;
if (process.env.DEBUG_TLS) fetchOpts.verbose = true;
}
response = await fetch(logsUrl, fetchOpts);
}
@@ -314,6 +314,11 @@ async function getEnvironmentStatsProgressive(
});
return images;
})
.catch(() => {
envStats.loading!.images = false;
onPartialUpdate({ id: env.id, loading: { ...envStats.loading! } });
return [];
});
const networksPromise = withTimeout(listNetworks(env.id).catch(() => []), 10000, [])
@@ -328,6 +333,11 @@ async function getEnvironmentStatsProgressive(
});
return networks;
})
.catch(() => {
envStats.loading!.networks = false;
onPartialUpdate({ id: env.id, loading: { ...envStats.loading! } });
return [];
});
const stacksPromise = withTimeout(listComposeStacks(env.id).catch(() => []), 10000, [])
@@ -345,6 +355,11 @@ async function getEnvironmentStatsProgressive(
});
return stacks;
})
.catch(() => {
envStats.loading!.stacks = false;
onPartialUpdate({ id: env.id, loading: { ...envStats.loading! } });
return [];
});
// PHASE 3: Disk usage (slow - includes volumes) - uses cache for better performance
@@ -390,6 +405,12 @@ async function getEnvironmentStatsProgressive(
});
return diskUsage;
})
.catch(() => {
envStats.loading!.volumes = false;
envStats.loading!.diskUsage = false;
onPartialUpdate({ id: env.id, loading: { ...envStats.loading! } });
return null;
});
// PHASE 4: Top containers (slow - requires per-container stats)
@@ -436,10 +457,14 @@ async function getEnvironmentStatsProgressive(
});
return envStats.topContainers;
}).catch(() => {
envStats.loading!.topContainers = false;
onPartialUpdate({ id: env.id, loading: { ...envStats.loading! } });
return [];
});
// Wait for all to complete
await Promise.all([
await Promise.allSettled([
containersPromise,
imagesPromise,
networksPromise,
@@ -572,7 +597,7 @@ export const GET: RequestHandler = async ({ cookies }) => {
});
// Wait for all to complete
await Promise.all(promises);
await Promise.allSettled(promises);
// Send done event and close
if (!controllerClosed) {
@@ -1,7 +1,7 @@
import { json } from '@sveltejs/kit';
import type { RequestHandler } from './$types';
import { getEnvironment, updateEnvironment } from '$lib/server/db';
import { getDockerInfo } from '$lib/server/docker';
import { getDockerInfo, getHawserInfo } from '$lib/server/docker';
import { edgeConnections, isEdgeConnected } from '$lib/server/hawser';
export const POST: RequestHandler = async ({ params }) => {
@@ -75,28 +75,15 @@ export const POST: RequestHandler = async ({ params }) => {
// For Hawser Standard mode, fetch Hawser info (Edge mode handled above with early return)
let hawserInfo = null;
if (env.connectionType === 'hawser-standard') {
// Standard mode: fetch via HTTP
try {
const protocol = env.useTls ? 'https' : 'http';
const headers: Record<string, string> = {};
if (env.hawserToken) {
headers['X-Hawser-Token'] = env.hawserToken;
}
const hawserResp = await fetch(`${protocol}://${env.host}:${env.port || 2376}/_hawser/info`, {
headers,
signal: AbortSignal.timeout(5000)
});
if (hawserResp.ok) {
hawserInfo = await hawserResp.json();
// Save hawser info to database
if (hawserInfo?.hawserVersion) {
await updateEnvironment(id, {
hawserVersion: hawserInfo.hawserVersion,
hawserAgentId: hawserInfo.agentId,
hawserAgentName: hawserInfo.agentName,
hawserLastSeen: new Date().toISOString()
});
}
hawserInfo = await getHawserInfo(id);
if (hawserInfo?.hawserVersion) {
await updateEnvironment(id, {
hawserVersion: hawserInfo.hawserVersion,
hawserAgentId: hawserInfo.agentId,
hawserAgentName: hawserInfo.agentName,
hawserLastSeen: new Date().toISOString()
});
}
} catch {
// Hawser info fetch failed, continue without it
+61 -75
View File
@@ -14,6 +14,39 @@ interface TestConnectionRequest {
hawserToken?: string;
}
function cleanPem(pem: string): string {
return pem
.split('\n')
.map((line) => line.trim())
.filter((line) => line.length > 0)
.join('\n');
}
function buildTlsOptions(config: TestConnectionRequest): Record<string, any> | undefined {
const protocol = config.protocol || 'http';
if (protocol !== 'https') return undefined;
const tls: Record<string, any> = {
sessionTimeout: 0,
servername: config.host
};
if (config.tlsSkipVerify) {
tls.rejectUnauthorized = false;
} else {
tls.rejectUnauthorized = true;
if (config.tlsCa) {
tls.ca = [cleanPem(config.tlsCa)];
}
}
if (config.tlsCert) {
tls.cert = [cleanPem(config.tlsCert)];
}
if (config.tlsKey) {
tls.key = cleanPem(config.tlsKey);
}
return tls;
}
/**
* Test Docker connection with provided configuration (without saving to database)
*/
@@ -55,78 +88,23 @@ export const POST: RequestHandler = async ({ request }) => {
'Content-Type': 'application/json'
};
// Add Hawser token if present
if (config.connectionType === 'hawser-standard' && config.hawserToken) {
headers['X-Hawser-Token'] = config.hawserToken;
}
// 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');
const fetchOptions: any = {
headers,
signal: AbortSignal.timeout(10000),
keepalive: false
};
// 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 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 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();
if (!output.trim()) {
throw new Error(stderr || 'Empty response from TLS test subprocess');
}
const result = JSON.parse(output.trim());
if (result.error) {
throw new Error(result.error);
}
response = new Response(result.body, {
status: result.status,
headers: { 'Content-Type': 'application/json' }
});
} else {
response = await fetch(url, {
headers,
signal: AbortSignal.timeout(10000)
});
const tls = buildTlsOptions(config);
if (tls) {
fetchOptions.tls = tls;
if (process.env.DEBUG_TLS) fetchOptions.verbose = true;
}
response = await fetch(url, fetchOptions);
}
if (!response.ok) {
@@ -141,17 +119,25 @@ try {
if (config.connectionType === 'hawser-standard' && config.host) {
try {
const protocol = config.protocol || 'http';
const headers: Record<string, string> = {};
const hawserHeaders: Record<string, string> = {};
if (config.hawserToken) {
headers['X-Hawser-Token'] = config.hawserToken;
hawserHeaders['X-Hawser-Token'] = config.hawserToken;
}
const hawserResp = await fetch(
`${protocol}://${config.host}:${config.port || 2375}/_hawser/info`,
{
headers,
signal: AbortSignal.timeout(5000)
}
);
const hawserUrl = `${protocol}://${config.host}:${config.port || 2375}/_hawser/info`;
const fetchOptions: any = {
headers: hawserHeaders,
signal: AbortSignal.timeout(5000),
keepalive: false
};
const tls = buildTlsOptions(config);
if (tls) {
fetchOptions.tls = tls;
if (process.env.DEBUG_TLS) fetchOptions.verbose = true;
}
const hawserResp = await fetch(hawserUrl, fetchOptions);
if (hawserResp.ok) {
hawserInfo = await hawserResp.json();
}
+27 -9
View File
@@ -36,6 +36,7 @@ interface DockerClientConfig {
ca?: string;
cert?: string;
key?: string;
skipVerify?: boolean;
hawserToken?: string;
environmentId?: number;
}
@@ -62,6 +63,7 @@ async function getDockerConfig(envId?: number | null): Promise<DockerClientConfi
ca: env.tlsCa || undefined,
cert: env.tlsCert || undefined,
key: env.tlsKey || undefined,
skipVerify: env.tlsSkipVerify || undefined,
hawserToken: env.connectionType === 'hawser-standard' ? env.hawserToken || undefined : undefined
};
}
@@ -432,13 +434,21 @@ export const GET: RequestHandler = async ({ url, cookies }) => {
if (config.hawserToken) inspectHeaders['X-Hawser-Token'] = config.hawserToken;
// Build fetch options - only include tls for HTTPS
const fetchOptions: RequestInit & { tls?: unknown } = {
const fetchOptions: any = {
headers: inspectHeaders,
signal: AbortSignal.timeout(30000) // 30 second timeout for inspect
signal: AbortSignal.timeout(30000)
};
if (config.type === 'https' && config.ca) {
// @ts-ignore - Bun TLS option
fetchOptions.tls = { ca: config.ca, cert: config.cert, key: config.key };
if (config.type === 'https') {
fetchOptions.tls = {
sessionTimeout: 0,
servername: config.host,
rejectUnauthorized: !config.skipVerify
};
if (config.ca) fetchOptions.tls.ca = [config.ca];
if (config.cert) fetchOptions.tls.cert = [config.cert];
if (config.key) fetchOptions.tls.key = config.key;
fetchOptions.keepalive = false;
if (process.env.DEBUG_TLS) fetchOptions.verbose = true;
}
inspectResponse = await fetch(inspectUrl, fetchOptions);
@@ -470,13 +480,21 @@ export const GET: RequestHandler = async ({ url, cookies }) => {
// For logs streaming, use the cleanup abort controller without a timeout
// (the stream needs to stay open indefinitely)
const fetchOptions: RequestInit & { tls?: unknown } = {
const fetchOptions: any = {
headers: logsHeaders,
signal: abortController.signal
};
if (config.type === 'https' && config.ca) {
// @ts-ignore - Bun TLS option
fetchOptions.tls = { ca: config.ca, cert: config.cert, key: config.key };
if (config.type === 'https') {
fetchOptions.tls = {
sessionTimeout: 0,
servername: config.host,
rejectUnauthorized: !config.skipVerify
};
if (config.ca) fetchOptions.tls.ca = [config.ca];
if (config.cert) fetchOptions.tls.cert = [config.cert];
if (config.key) fetchOptions.tls.key = config.key;
fetchOptions.keepalive = false;
if (process.env.DEBUG_TLS) fetchOptions.verbose = true;
}
logsResponse = await fetch(logsUrl, fetchOptions);