217 lines
5.0 KiB
TypeScript
217 lines
5.0 KiB
TypeScript
import assert from "node:assert/strict";
|
|
import test from "node:test";
|
|
|
|
import {
|
|
clearContainerMetricsCacheForTest,
|
|
getProjectEnvRuntimeCandidatePaths,
|
|
invalidateContainerMetricsCache,
|
|
parseStoredVolumeMounts,
|
|
readContainerMetricsWithCache,
|
|
resolveContainerPathToHostMountPath,
|
|
} from "../../src/server/routes/containers";
|
|
|
|
const stats = {
|
|
cpuPercent: 12.5,
|
|
memoryPercent: 32.1,
|
|
pids: 4,
|
|
memory: {
|
|
raw: "128MiB / 512MiB",
|
|
used: "128MiB",
|
|
limit: "512MiB",
|
|
usedBytes: 134217728,
|
|
limitBytes: 536870912,
|
|
},
|
|
network: {
|
|
raw: "1kB / 2kB",
|
|
read: "1kB",
|
|
write: "2kB",
|
|
totalBytes: null,
|
|
readBytes: null,
|
|
writeBytes: null,
|
|
},
|
|
io: {
|
|
raw: "3kB / 4kB",
|
|
read: "3kB",
|
|
write: "4kB",
|
|
totalBytes: null,
|
|
readBytes: null,
|
|
writeBytes: null,
|
|
},
|
|
};
|
|
|
|
function readMetrics(loadStats: () => Promise<typeof stats>) {
|
|
return readContainerMetricsWithCache({
|
|
organizationId: "org-1",
|
|
serverId: "server-1",
|
|
containerId: "container-1",
|
|
containerName: "app",
|
|
containerStatus: "RUNNING",
|
|
dockerId: "abc123",
|
|
runtimeId: "abc123",
|
|
loadStats,
|
|
});
|
|
}
|
|
|
|
test("container metrics cache dedupes concurrent samples and reuses short-lived results", async () => {
|
|
clearContainerMetricsCacheForTest();
|
|
let loadCount = 0;
|
|
let resolveStats: (value: typeof stats) => void = () => undefined;
|
|
|
|
const loadStats = async () => {
|
|
loadCount += 1;
|
|
return new Promise<typeof stats>((resolve) => {
|
|
resolveStats = resolve;
|
|
});
|
|
};
|
|
|
|
const first = readMetrics(loadStats);
|
|
const second = readMetrics(loadStats);
|
|
|
|
assert.equal(loadCount, 1);
|
|
resolveStats(stats);
|
|
|
|
const [firstResult, secondResult] = await Promise.all([first, second]);
|
|
|
|
assert.deepEqual(firstResult.stats, stats);
|
|
assert.deepEqual(secondResult.stats, stats);
|
|
assert.equal(loadCount, 1);
|
|
|
|
const cachedResult = await readMetrics(loadStats);
|
|
|
|
assert.deepEqual(cachedResult.stats, stats);
|
|
assert.equal(loadCount, 1);
|
|
});
|
|
|
|
test("container metrics cache invalidation forces a fresh sample", async () => {
|
|
clearContainerMetricsCacheForTest();
|
|
let loadCount = 0;
|
|
const loadStats = async () => {
|
|
loadCount += 1;
|
|
return stats;
|
|
};
|
|
|
|
await readMetrics(loadStats);
|
|
await readMetrics(loadStats);
|
|
assert.equal(loadCount, 1);
|
|
|
|
invalidateContainerMetricsCache({
|
|
organizationId: "org-1",
|
|
serverId: "server-1",
|
|
containerId: "container-1",
|
|
});
|
|
|
|
await readMetrics(loadStats);
|
|
assert.equal(loadCount, 2);
|
|
});
|
|
|
|
test("container metrics cache does not retain failed samples", async () => {
|
|
clearContainerMetricsCacheForTest();
|
|
let loadCount = 0;
|
|
const loadStats = async () => {
|
|
loadCount += 1;
|
|
if (loadCount === 1) {
|
|
throw new Error("docker stats failed");
|
|
}
|
|
return stats;
|
|
};
|
|
|
|
await assert.rejects(() => readMetrics(loadStats), /docker stats failed/);
|
|
const result = await readMetrics(loadStats);
|
|
|
|
assert.deepEqual(result.stats, stats);
|
|
assert.equal(loadCount, 2);
|
|
});
|
|
|
|
test("project env path resolver maps container paths to the most specific host mount", () => {
|
|
const mounts = [
|
|
{
|
|
Source: "/srv/apps/example",
|
|
Destination: "/app",
|
|
RW: true,
|
|
},
|
|
{
|
|
Source: "/srv/apps/example/storage",
|
|
Destination: "/app/storage",
|
|
RW: true,
|
|
},
|
|
];
|
|
|
|
assert.equal(
|
|
resolveContainerPathToHostMountPath("/app/.env", mounts),
|
|
"/srv/apps/example/.env",
|
|
);
|
|
assert.equal(
|
|
resolveContainerPathToHostMountPath("/app/storage/.env", mounts),
|
|
"/srv/apps/example/storage/.env",
|
|
);
|
|
assert.equal(
|
|
resolveContainerPathToHostMountPath("/workspace/.env", mounts),
|
|
null,
|
|
);
|
|
});
|
|
|
|
test("project env path resolver can use stored container volume bindings", () => {
|
|
const mounts = parseStoredVolumeMounts([
|
|
"/srv/laravel:/bitnami/laravel",
|
|
"named-volume:/ignored",
|
|
"/srv/read-only:/readonly:ro",
|
|
]);
|
|
|
|
assert.deepEqual(mounts, [
|
|
{
|
|
Source: "/srv/laravel",
|
|
Destination: "/bitnami/laravel",
|
|
RW: true,
|
|
},
|
|
{
|
|
Source: "/srv/read-only",
|
|
Destination: "/readonly",
|
|
RW: false,
|
|
},
|
|
]);
|
|
assert.equal(
|
|
resolveContainerPathToHostMountPath("/bitnami/laravel/.env", mounts),
|
|
"/srv/laravel/.env",
|
|
);
|
|
assert.equal(mounts[0]?.Destination, "/bitnami/laravel");
|
|
});
|
|
|
|
test("project env runtime candidates prefer inspect paths before common fallbacks", () => {
|
|
const mounts = parseStoredVolumeMounts([
|
|
"/srv/project:/runtime/project",
|
|
]);
|
|
const paths = getProjectEnvRuntimeCandidatePaths(
|
|
{
|
|
Config: {
|
|
WorkingDir: "/runtime/project",
|
|
},
|
|
},
|
|
mounts,
|
|
);
|
|
|
|
assert.deepEqual(paths.slice(0, 4), [
|
|
"/runtime/project/.env",
|
|
"/app/.env",
|
|
"/workspace/.env",
|
|
"/var/www/html/.env",
|
|
]);
|
|
});
|
|
|
|
test("project env runtime candidates prefer the main process working directory", () => {
|
|
const paths = getProjectEnvRuntimeCandidatePaths(
|
|
{
|
|
Config: {
|
|
WorkingDir: "/image/default",
|
|
},
|
|
},
|
|
[],
|
|
["/app"],
|
|
);
|
|
|
|
assert.deepEqual(paths.slice(0, 3), [
|
|
"/app/.env",
|
|
"/image/default/.env",
|
|
"/workspace/.env",
|
|
]);
|
|
});
|