diff --git a/cli/src/commands/cluster.test.ts b/cli/src/commands/cluster.test.ts index e049dd39160..70f502cf02c 100644 --- a/cli/src/commands/cluster.test.ts +++ b/cli/src/commands/cluster.test.ts @@ -241,9 +241,20 @@ describe("cluster commands", () => { const cmd = createClusterCommand(m); const code = await cmd.run(["remove", "c-1"]); expect(code).toBe(0); + expect(m.clusterConnections.get).toHaveBeenCalledWith("c-1"); expect(m.clusterConnections.delete).toHaveBeenCalledWith("c-1"); }); + it("remove: returns non-zero when the connection does not exist", async () => { + const m = mocks(); + (m.clusterConnections.get as any).mockResolvedValue(null); + const cmd = createClusterCommand(m); + const code = await cmd.run(["remove", "missing"]); + expect(code).not.toBe(0); + expect(m.clusterConnections.delete).not.toHaveBeenCalled(); + expect(out.join("\n")).toMatch(/not found/i); + }); + it("remove: returns non-zero when id is missing", async () => { const m = mocks(); const cmd = createClusterCommand(m); diff --git a/cli/src/commands/cluster.ts b/cli/src/commands/cluster.ts index d5f9f4f653e..386509f16ed 100644 --- a/cli/src/commands/cluster.ts +++ b/cli/src/commands/cluster.ts @@ -295,6 +295,11 @@ async function cmdRemove(argv: string[], deps: ClusterCommandDeps): Promise"); return 2; } + const existing = await deps.clusterConnections.get(id); + if (!existing) { + deps.print(`Cluster connection ${id} not found`); + return 1; + } await deps.clusterConnections.delete(id); deps.print(`Deleted cluster connection ${id}`); return 0; diff --git a/packages/adapters/kubernetes-execution/src/orchestrator/cilium-network-policy.ts b/packages/adapters/kubernetes-execution/src/orchestrator/cilium-network-policy.ts index f5517babd70..3422ecf9b15 100644 --- a/packages/adapters/kubernetes-execution/src/orchestrator/cilium-network-policy.ts +++ b/packages/adapters/kubernetes-execution/src/orchestrator/cilium-network-policy.ts @@ -65,16 +65,25 @@ export async function applyCiliumNetworkPolicy(client: KubernetesApiClient, p: C await client.request("GET", itemPath); await client.request("PUT", itemPath, p); } catch (err: unknown) { - const is404 = - /\b404\b/.test(String(err)) || - (typeof err === "object" && err !== null && (err as Record)["statusCode"] === 404) || - (typeof err === "object" && err !== null && - typeof (err as Record)["response"] === "object" && - ((err as Record)["response"] as Record)?.["statusCode"] === 404); - if (is404) { + if (isNotFoundError(err)) { await client.request("POST", collectionPath, p); return; } throw err; } } + +function isNotFoundError(err: unknown): boolean { + if (typeof err === "object" && err !== null) { + const record = err as Record; + if (record.statusCode === 404) return true; + if ( + typeof record.response === "object" && + record.response !== null && + (record.response as Record).statusCode === 404 + ) { + return true; + } + } + return /\bfailed 404:/.test(String(err)); +} diff --git a/packages/adapters/kubernetes-execution/test/unit/cilium-network-policy.test.ts b/packages/adapters/kubernetes-execution/test/unit/cilium-network-policy.test.ts index fdf4b52c284..fe4289a2178 100644 --- a/packages/adapters/kubernetes-execution/test/unit/cilium-network-policy.test.ts +++ b/packages/adapters/kubernetes-execution/test/unit/cilium-network-policy.test.ts @@ -1,5 +1,8 @@ -import { describe, it, expect } from "vitest"; -import { buildCiliumAgentEgressPolicy } from "../../src/orchestrator/cilium-network-policy.js"; +import { describe, it, expect, vi } from "vitest"; +import { + applyCiliumNetworkPolicy, + buildCiliumAgentEgressPolicy, +} from "../../src/orchestrator/cilium-network-policy.js"; describe("buildCiliumAgentEgressPolicy", () => { it("merges adapter and tenant FQDN allowlists, deduplicated and sorted", () => { @@ -65,4 +68,28 @@ describe("buildCiliumAgentEgressPolicy", () => { }); expect(p.spec.egress).toEqual([]); }); + + it("creates the policy when the custom client reports failed 404", async () => { + const request = vi.fn() + .mockRejectedValueOnce(new Error("failed 404: not found")) + .mockResolvedValueOnce({}); + await applyCiliumNetworkPolicy({ request } as any, buildCiliumAgentEgressPolicy({ + namespace: "paperclip-x", companyId: "c-1", companySlug: "x", + adapterAllowFqdns: [], tenantAllowFqdns: [], controlPlaneSelector: null, + })); + expect(request).toHaveBeenLastCalledWith( + "POST", + "/apis/cilium.io/v2/namespaces/paperclip-x/ciliumnetworkpolicies", + expect.any(Object), + ); + }); + + it("does not treat unrelated 404 text as not found", async () => { + const request = vi.fn().mockRejectedValueOnce(new Error("backend error code 40442")); + await expect(applyCiliumNetworkPolicy({ request } as any, buildCiliumAgentEgressPolicy({ + namespace: "paperclip-x", companyId: "c-1", companySlug: "x", + adapterAllowFqdns: [], tenantAllowFqdns: [], controlPlaneSelector: null, + }))).rejects.toThrow("40442"); + expect(request).toHaveBeenCalledTimes(1); + }); });