159 lines
5.8 KiB
JavaScript
159 lines
5.8 KiB
JavaScript
import assert from "node:assert/strict";
|
|
import test from "node:test";
|
|
|
|
import {
|
|
assertReadonlyKubectlArgs,
|
|
buildDevRuntimeHotfixAuditReport,
|
|
parseArgs
|
|
} from "./dev-runtime-hotfix-audit.mjs";
|
|
|
|
test("plan mode emits a bounded read-only plan and runs no kubectl commands", async () => {
|
|
let commandCount = 0;
|
|
const report = await buildDevRuntimeHotfixAuditReport(parseArgs(["--plan"]), {
|
|
runCommand: async () => {
|
|
commandCount += 1;
|
|
throw new Error("plan mode must not execute commands");
|
|
}
|
|
});
|
|
|
|
assert.equal(commandCount, 0);
|
|
assert.equal(report.mode, "plan");
|
|
assert.equal(report.safety.k8sWriteAttempted, false);
|
|
assert.deepEqual(report.classifications, [
|
|
"source-artifact-expected",
|
|
"unknown-needs-manual-readonly-check"
|
|
]);
|
|
assert.ok(report.commandPlan.readonly.every((command) => command.includes("KUBECONFIG=/etc/rancher/k3s/k3s.yaml")));
|
|
assert.ok(report.commandPlan.forbiddenDefaultOperations.some((command) => command.includes("kubectl patch")));
|
|
});
|
|
|
|
test("collect-readonly classifies active ConfigMap mount and pod marker without write verbs", async () => {
|
|
const commands = [];
|
|
const report = await buildDevRuntimeHotfixAuditReport(parseArgs(["--collect-readonly"]), {
|
|
runCommand: async ({ args, displayCommand }) => {
|
|
commands.push(displayCommand);
|
|
return fixtureResult(args);
|
|
}
|
|
});
|
|
|
|
assert.equal(report.mode, "collect-readonly");
|
|
assert.equal(report.safety.k8sWriteAttempted, false);
|
|
assert.ok(report.classifications.includes("hotfix-configmap-present"));
|
|
assert.ok(report.classifications.includes("deployment-mounts-hotfix"));
|
|
assert.ok(report.classifications.includes("pod-loads-hotfix-marker"));
|
|
assert.ok(report.classifications.includes("rollback-required"));
|
|
assert.equal(report.conclusion.status, "hotfix-detected");
|
|
assert.ok(commands.length > 0);
|
|
assertNoWriteVerbs(commands);
|
|
});
|
|
|
|
test("collect-readonly reports no-hotfix-detected when ConfigMap and mount are absent", async () => {
|
|
const report = await buildDevRuntimeHotfixAuditReport(parseArgs(["--collect-readonly"]), {
|
|
runCommand: async ({ args }) => fixtureResult(args, { noHotfix: true })
|
|
});
|
|
|
|
assert.deepEqual(report.classifications, [
|
|
"source-artifact-expected",
|
|
"no-hotfix-detected"
|
|
]);
|
|
assert.equal(report.conclusion.status, "clear");
|
|
});
|
|
|
|
test("collect-readonly keeps unknown classification when D601 node cannot be proven", async () => {
|
|
const report = await buildDevRuntimeHotfixAuditReport(parseArgs(["--collect-readonly"]), {
|
|
runCommand: async ({ args }) => {
|
|
if (args.includes("nodes")) {
|
|
return { ok: false, exitCode: 1, stdout: "", stderr: "unable to connect to the server" };
|
|
}
|
|
return fixtureResult(args);
|
|
}
|
|
});
|
|
|
|
assert.ok(report.classifications.includes("unknown-needs-manual-readonly-check"));
|
|
assert.notEqual(report.conclusion.status, "clear");
|
|
});
|
|
|
|
test("kubectl write verbs are rejected by the audit helper", () => {
|
|
assert.throws(() => assertReadonlyKubectlArgs(["-n", "hwlab-dev", "patch", "deployment", "hwlab-cloud-api"]), /forbidden/u);
|
|
assert.throws(() => assertReadonlyKubectlArgs(["-n", "hwlab-dev", "rollout", "restart", "deployment", "hwlab-cloud-api"]), /forbidden/u);
|
|
});
|
|
|
|
function fixtureResult(args, options = {}) {
|
|
const text = args.join(" ");
|
|
if (text.includes("get nodes")) {
|
|
return okJson({ items: [{ metadata: { name: "d601" } }] });
|
|
}
|
|
if (text.includes("get configmap")) {
|
|
if (options.noHotfix) {
|
|
return { ok: false, exitCode: 1, stdout: "", stderr: "Error from server (NotFound): configmaps \"hwlab-cloud-api-code-agent-hotfix\" not found" };
|
|
}
|
|
return okJson({ data: { "code-agent-chat.ts": "// runtime-hotfix-pc-gateway-shell" } });
|
|
}
|
|
if (text.includes("get deployment")) {
|
|
return okJson(deploymentFixture({ noHotfix: options.noHotfix }));
|
|
}
|
|
if (text.includes("get pods")) {
|
|
return okJson({
|
|
items: [{
|
|
metadata: { name: "hwlab-cloud-api-abc", creationTimestamp: "2026-05-24T00:00:00Z" },
|
|
status: { phase: "Running" },
|
|
spec: { nodeName: "d601" }
|
|
}]
|
|
});
|
|
}
|
|
if (text.includes("exec") && text.includes("grep")) {
|
|
return options.noHotfix
|
|
? { ok: false, exitCode: 1, stdout: "", stderr: "" }
|
|
: { ok: true, exitCode: 0, stdout: "1:runtime-hotfix-pc-gateway-shell\n", stderr: "" };
|
|
}
|
|
if (text.includes("exec") && text.includes("node --check")) {
|
|
return { ok: true, exitCode: 0, stdout: "", stderr: "" };
|
|
}
|
|
throw new Error(`unexpected fixture command: ${text}`);
|
|
}
|
|
|
|
function deploymentFixture({ noHotfix = false } = {}) {
|
|
return {
|
|
metadata: { generation: 9 },
|
|
spec: {
|
|
template: {
|
|
metadata: {
|
|
annotations: noHotfix ? {} : {
|
|
"hwlab.pikastech.local/pc-gateway-shell-hotfix": "runtime-hotfix"
|
|
}
|
|
},
|
|
spec: {
|
|
volumes: noHotfix ? [] : [{
|
|
name: "code-agent-hotfix",
|
|
configMap: {
|
|
name: "hwlab-cloud-api-code-agent-hotfix",
|
|
items: [{ key: "code-agent-chat.ts", path: "code-agent-chat.ts" }]
|
|
}
|
|
}],
|
|
containers: [{
|
|
name: "hwlab-cloud-api",
|
|
image: "127.0.0.1:5000/hwlab/hwlab-cloud-api:dev",
|
|
volumeMounts: noHotfix ? [] : [{
|
|
name: "code-agent-hotfix",
|
|
mountPath: "/app/internal/cloud/code-agent-chat.ts",
|
|
subPath: "code-agent-chat.ts",
|
|
readOnly: true
|
|
}]
|
|
}]
|
|
}
|
|
}
|
|
}
|
|
};
|
|
}
|
|
|
|
function okJson(value) {
|
|
return { ok: true, exitCode: 0, stdout: JSON.stringify(value), stderr: "" };
|
|
}
|
|
|
|
function assertNoWriteVerbs(commands) {
|
|
const forbidden = /\bkubectl\s+(?:apply|patch|rollout|delete|create|replace|scale|set|edit|annotate|label)\b/u;
|
|
for (const command of commands) {
|
|
assert.doesNotMatch(command, forbidden, command);
|
|
}
|
|
}
|