mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-05 00:51:37 +00:00
* fix: add Kubernetes resource-only teardown * fix(k8s): preserve shared namespaces on teardown --------- Co-authored-by: Peter Steinberger <steipete@macos.shared>
186 lines
6.7 KiB
TypeScript
186 lines
6.7 KiB
TypeScript
import { spawnSync } from "node:child_process";
|
|
import { chmodSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
import path from "node:path";
|
|
import { afterEach, describe, expect, it } from "vitest";
|
|
import { useAutoCleanupTempDirTracker } from "../helpers/temp-dir.js";
|
|
|
|
const tempDirs = useAutoCleanupTempDirTracker(afterEach);
|
|
|
|
function writeExecutable(filePath: string, contents: string): void {
|
|
writeFileSync(filePath, contents);
|
|
chmodSync(filePath, 0o755);
|
|
}
|
|
|
|
function runWithStubbedKubectl(
|
|
args: string[],
|
|
namespace: string,
|
|
options: {
|
|
deleteKustomizeStatus?: number;
|
|
deleteNamespaceStatus?: number;
|
|
deleteSecretStatus?: number;
|
|
} = {},
|
|
) {
|
|
const root = tempDirs.make("openclaw-k8s-delete-");
|
|
const binDir = path.join(root, "bin");
|
|
const logPath = path.join(root, "kubectl.log");
|
|
mkdirSync(binDir);
|
|
|
|
writeExecutable(
|
|
path.join(binDir, "kubectl"),
|
|
`#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
printf '%s\\n' "$*" >> "$OPENCLAW_KUBECTL_LOG"
|
|
if [[ "$1" == "delete" && "$2" == "-k" ]]; then
|
|
exit "\${OPENCLAW_KUBECTL_DELETE_KUSTOMIZE_STATUS:-0}"
|
|
fi
|
|
if [[ "$1" == "delete" && "$2" == "namespace" ]]; then
|
|
exit "\${OPENCLAW_KUBECTL_DELETE_NAMESPACE_STATUS:-0}"
|
|
fi
|
|
if [[ "$1" == "delete" && "$2" == "secret" ]]; then
|
|
exit "\${OPENCLAW_KUBECTL_DELETE_SECRET_STATUS:-0}"
|
|
fi
|
|
if [[ "$1" == "get" && "$2" == "namespace" ]]; then
|
|
exit 99
|
|
fi
|
|
case "$1" in
|
|
cluster-info|delete) exit 0 ;;
|
|
*) exit 99 ;;
|
|
esac
|
|
`,
|
|
);
|
|
writeExecutable(path.join(binDir, "openssl"), "#!/usr/bin/env bash\nexit 0\n");
|
|
|
|
const result = spawnSync("bash", ["scripts/k8s/deploy.sh", ...args], {
|
|
cwd: process.cwd(),
|
|
encoding: "utf8",
|
|
env: {
|
|
...process.env,
|
|
OPENCLAW_KUBECTL_DELETE_KUSTOMIZE_STATUS: String(options.deleteKustomizeStatus ?? 0),
|
|
OPENCLAW_KUBECTL_DELETE_NAMESPACE_STATUS: String(options.deleteNamespaceStatus ?? 0),
|
|
OPENCLAW_KUBECTL_DELETE_SECRET_STATUS: String(options.deleteSecretStatus ?? 0),
|
|
OPENCLAW_KUBECTL_LOG: logPath,
|
|
OPENCLAW_NAMESPACE: namespace,
|
|
PATH: `${binDir}:${process.env.PATH ?? ""}`,
|
|
},
|
|
});
|
|
|
|
return {
|
|
calls: readFileSync(logPath, "utf8").trim().split("\n"),
|
|
output: `${result.stdout}\n${result.stderr}`,
|
|
result,
|
|
};
|
|
}
|
|
|
|
describe("scripts/k8s/deploy.sh", () => {
|
|
function runDeleteResourcesWithStubbedKubectl(
|
|
namespace: string,
|
|
options: {
|
|
deleteKustomizeStatus?: number;
|
|
} = {},
|
|
) {
|
|
return runWithStubbedKubectl(["--delete-resources"], namespace, options);
|
|
}
|
|
|
|
it("keeps the default namespace delete mode as a full namespace teardown", () => {
|
|
const { calls, output, result } = runWithStubbedKubectl(["--delete"], "openclaw");
|
|
|
|
expect(result.status, output).toBe(0);
|
|
expect(output).toContain("Deleting namespace 'openclaw' and all resources");
|
|
expect(calls).toEqual(["cluster-info", "delete namespace openclaw --ignore-not-found"]);
|
|
});
|
|
|
|
it("keeps a custom namespace and unrelated workloads when the legacy delete mode is used", () => {
|
|
const { calls, output, result } = runWithStubbedKubectl(["--delete"], "my-namespace");
|
|
|
|
expect(result.status, output).toBe(0);
|
|
expect(output).toContain("Deleting OpenClaw resources from namespace 'my-namespace'");
|
|
expect(calls).toEqual([
|
|
"cluster-info",
|
|
`delete -k ${path.resolve("scripts/k8s/manifests")} -n my-namespace --ignore-not-found`,
|
|
"delete secret openclaw-secrets -n my-namespace --ignore-not-found",
|
|
]);
|
|
expect(calls).not.toContain("delete namespace my-namespace --ignore-not-found");
|
|
expect(calls).not.toContain("get namespace my-namespace");
|
|
});
|
|
|
|
it("deletes OpenClaw resources without deleting the namespace", () => {
|
|
const { calls, output, result } = runDeleteResourcesWithStubbedKubectl("my-namespace");
|
|
|
|
expect(result.status, output).toBe(0);
|
|
expect(output).toContain("Deleting OpenClaw resources from namespace 'my-namespace'");
|
|
|
|
expect(calls).toEqual([
|
|
"cluster-info",
|
|
`delete -k ${path.resolve("scripts/k8s/manifests")} -n my-namespace --ignore-not-found`,
|
|
"delete secret openclaw-secrets -n my-namespace --ignore-not-found",
|
|
]);
|
|
expect(calls).not.toContain("delete namespace my-namespace --ignore-not-found");
|
|
expect(calls).not.toContain("get namespace my-namespace");
|
|
});
|
|
|
|
it("surfaces resource delete failures instead of reporting teardown success", () => {
|
|
const { calls, output, result } = runDeleteResourcesWithStubbedKubectl("restricted-namespace", {
|
|
deleteKustomizeStatus: 17,
|
|
});
|
|
|
|
expect(result.status, output).toBe(17);
|
|
expect(output).toContain("Deleting OpenClaw resources from namespace 'restricted-namespace'");
|
|
expect(calls).toEqual([
|
|
"cluster-info",
|
|
`delete -k ${path.resolve("scripts/k8s/manifests")} -n restricted-namespace --ignore-not-found`,
|
|
]);
|
|
expect(output).not.toContain("Done.");
|
|
});
|
|
|
|
it("stops custom namespace teardown when deleting managed manifests fails", () => {
|
|
const { calls, output, result } = runWithStubbedKubectl(["--delete"], "shared-namespace", {
|
|
deleteKustomizeStatus: 17,
|
|
});
|
|
|
|
expect(result.status, output).toBe(17);
|
|
expect(calls).toEqual([
|
|
"cluster-info",
|
|
`delete -k ${path.resolve("scripts/k8s/manifests")} -n shared-namespace --ignore-not-found`,
|
|
]);
|
|
expect(output).not.toContain("Done.");
|
|
});
|
|
|
|
it.each(["--delete", "--delete-resources"])(
|
|
"surfaces generated Secret deletion failures for %s",
|
|
(mode) => {
|
|
const { calls, output, result } = runWithStubbedKubectl([mode], "shared-namespace", {
|
|
deleteSecretStatus: 23,
|
|
});
|
|
|
|
expect(result.status, output).toBe(23);
|
|
expect(calls).toEqual([
|
|
"cluster-info",
|
|
`delete -k ${path.resolve("scripts/k8s/manifests")} -n shared-namespace --ignore-not-found`,
|
|
"delete secret openclaw-secrets -n shared-namespace --ignore-not-found",
|
|
]);
|
|
expect(output).not.toContain("Done.");
|
|
},
|
|
);
|
|
|
|
it("surfaces namespace deletion failures instead of reporting teardown success", () => {
|
|
const { calls, output, result } = runWithStubbedKubectl(["--delete-namespace"], "shared", {
|
|
deleteNamespaceStatus: 29,
|
|
});
|
|
|
|
expect(result.status, output).toBe(29);
|
|
expect(calls).toEqual(["cluster-info", "delete namespace shared --ignore-not-found"]);
|
|
expect(output).not.toContain("Done.");
|
|
});
|
|
|
|
it("deletes the namespace only when the explicit namespace teardown mode is selected", () => {
|
|
const { calls, output, result } = runWithStubbedKubectl(
|
|
["--delete-namespace"],
|
|
"shared-namespace",
|
|
);
|
|
|
|
expect(result.status, output).toBe(0);
|
|
expect(output).toContain("Deleting namespace 'shared-namespace' and all resources");
|
|
expect(calls).toEqual(["cluster-info", "delete namespace shared-namespace --ignore-not-found"]);
|
|
});
|
|
});
|