220 lines
9.2 KiB
TypeScript
220 lines
9.2 KiB
TypeScript
import { describe, it, expect, vi } from "vitest";
|
|
import { ensureTenant } from "../../src/tenant-orchestrator.js";
|
|
|
|
function makeMockClients() {
|
|
const calls: { kind: string; name: string; namespace?: string; body?: unknown }[] = [];
|
|
function track(kind: string) {
|
|
return vi.fn(async (...args: unknown[]) => {
|
|
const arg = (args[0] ?? {}) as { name?: string; namespace?: string; body?: unknown };
|
|
calls.push({ kind, name: arg.name ?? "", namespace: arg.namespace, body: arg.body });
|
|
return { body: arg.body };
|
|
});
|
|
}
|
|
return {
|
|
calls,
|
|
core: {
|
|
createNamespace: track("Namespace"),
|
|
readNamespacedServiceAccount: vi.fn().mockRejectedValue({ code: 404 }),
|
|
createNamespacedServiceAccount: track("ServiceAccount"),
|
|
replaceNamespacedServiceAccount: track("ServiceAccountReplace"),
|
|
readNamespacedResourceQuota: vi.fn().mockRejectedValue({ code: 404 }),
|
|
createNamespacedResourceQuota: track("ResourceQuota"),
|
|
replaceNamespacedResourceQuota: track("ResourceQuotaReplace"),
|
|
readNamespacedLimitRange: vi.fn().mockRejectedValue({ code: 404 }),
|
|
createNamespacedLimitRange: track("LimitRange"),
|
|
replaceNamespacedLimitRange: track("LimitRangeReplace"),
|
|
readNamespace: vi.fn().mockRejectedValue({ code: 404 }),
|
|
replaceNamespace: track("NamespaceReplace"),
|
|
},
|
|
rbac: {
|
|
readNamespacedRole: vi.fn().mockRejectedValue({ code: 404 }),
|
|
createNamespacedRole: track("Role"),
|
|
replaceNamespacedRole: track("RoleReplace"),
|
|
readNamespacedRoleBinding: vi.fn().mockRejectedValue({ code: 404 }),
|
|
createNamespacedRoleBinding: track("RoleBinding"),
|
|
replaceNamespacedRoleBinding: track("RoleBindingReplace"),
|
|
},
|
|
networking: {
|
|
readNamespacedNetworkPolicy: vi.fn().mockRejectedValue({ code: 404 }),
|
|
createNamespacedNetworkPolicy: track("NetworkPolicy"),
|
|
replaceNamespacedNetworkPolicy: track("NetworkPolicyReplace"),
|
|
deleteNamespacedNetworkPolicy: vi.fn().mockRejectedValue({ code: 404 }),
|
|
},
|
|
custom: {
|
|
getNamespacedCustomObject: vi.fn().mockRejectedValue({ code: 404 }),
|
|
createNamespacedCustomObject: track("CiliumNetworkPolicy"),
|
|
replaceNamespacedCustomObject: track("CiliumNetworkPolicyReplace"),
|
|
deleteNamespacedCustomObject: vi.fn().mockRejectedValue({ code: 404 }),
|
|
},
|
|
};
|
|
}
|
|
|
|
describe("ensureTenant", () => {
|
|
const baseInput = {
|
|
namespace: "paperclip-acme",
|
|
companyId: "11111111-1111-1111-1111-111111111111",
|
|
paperclipServerNamespace: "paperclip",
|
|
serviceAccountAnnotations: {},
|
|
egressMode: "standard" as const,
|
|
egressAllowFqdns: ["api.anthropic.com"],
|
|
egressAllowCidrs: [] as string[],
|
|
resourceQuota: { pods: "20", requestsCpu: "5", requestsMemory: "20Gi", limitsCpu: "20", limitsMemory: "80Gi" },
|
|
};
|
|
|
|
it("creates all required resources in the correct order on a fresh tenant", async () => {
|
|
const clients = makeMockClients();
|
|
await ensureTenant(clients as never, baseInput);
|
|
const order = clients.calls.map((c) => c.kind);
|
|
expect(order).toEqual([
|
|
"Namespace",
|
|
"ServiceAccount",
|
|
"Role",
|
|
"RoleBinding",
|
|
"ResourceQuota",
|
|
"LimitRange",
|
|
"NetworkPolicy",
|
|
"NetworkPolicy",
|
|
]);
|
|
});
|
|
|
|
it("creates a CiliumNetworkPolicy instead of standard egress when egressMode=cilium", async () => {
|
|
const clients = makeMockClients();
|
|
await ensureTenant(clients as never, { ...baseInput, egressMode: "cilium" });
|
|
const cnpCall = clients.calls.find((c) => c.kind === "CiliumNetworkPolicy");
|
|
expect(cnpCall).toBeDefined();
|
|
const npCalls = clients.calls.filter((c) => c.kind === "NetworkPolicy");
|
|
expect(npCalls).toHaveLength(1);
|
|
expect((npCalls[0].body as { metadata: { name: string } }).metadata.name).toBe("paperclip-deny-all");
|
|
});
|
|
|
|
it("applies serviceAccountAnnotations to the ServiceAccount", async () => {
|
|
const clients = makeMockClients();
|
|
await ensureTenant(clients as never, {
|
|
...baseInput,
|
|
serviceAccountAnnotations: { "eks.amazonaws.com/role-arn": "arn:aws:iam::123:role/paperclip" },
|
|
});
|
|
const saCall = clients.calls.find((c) => c.kind === "ServiceAccount");
|
|
const sa = saCall!.body as { metadata: { annotations: Record<string, string> } };
|
|
expect(sa.metadata.annotations["eks.amazonaws.com/role-arn"]).toBe("arn:aws:iam::123:role/paperclip");
|
|
});
|
|
|
|
it("reconciles a namespace that already exists", async () => {
|
|
const clients = makeMockClients();
|
|
clients.core.readNamespace.mockResolvedValue({
|
|
metadata: {
|
|
name: baseInput.namespace,
|
|
resourceVersion: "rv-namespace",
|
|
labels: { "operator.example.com/team": "infra" },
|
|
},
|
|
});
|
|
await ensureTenant(clients as never, baseInput);
|
|
expect(clients.core.createNamespace).not.toHaveBeenCalled();
|
|
expect(clients.core.replaceNamespace).toHaveBeenCalledWith({
|
|
name: baseInput.namespace,
|
|
body: expect.objectContaining({
|
|
metadata: expect.objectContaining({
|
|
resourceVersion: "rv-namespace",
|
|
labels: expect.objectContaining({
|
|
"operator.example.com/team": "infra",
|
|
"paperclip.io/company-id": baseInput.companyId,
|
|
"paperclip.io/managed-by": "paperclip-k8s-plugin",
|
|
"pod-security.kubernetes.io/enforce": "restricted",
|
|
"pod-security.kubernetes.io/audit": "restricted",
|
|
"pod-security.kubernetes.io/warn": "restricted",
|
|
}),
|
|
}),
|
|
}),
|
|
});
|
|
});
|
|
|
|
it("reconciles existing managed resources with the latest desired manifests", async () => {
|
|
const clients = makeMockClients();
|
|
const existing = { metadata: { resourceVersion: "rv-1" } };
|
|
clients.core.readNamespace.mockResolvedValue({ metadata: { name: baseInput.namespace, resourceVersion: "rv-ns" } });
|
|
clients.core.readNamespacedServiceAccount.mockResolvedValue(existing);
|
|
clients.rbac.readNamespacedRole.mockResolvedValue(existing);
|
|
clients.rbac.readNamespacedRoleBinding.mockResolvedValue(existing);
|
|
clients.core.readNamespacedResourceQuota.mockResolvedValue(existing);
|
|
clients.core.readNamespacedLimitRange.mockResolvedValue(existing);
|
|
clients.networking.readNamespacedNetworkPolicy.mockResolvedValue(existing);
|
|
|
|
await ensureTenant(clients as never, {
|
|
...baseInput,
|
|
serviceAccountAnnotations: { "eks.amazonaws.com/role-arn": "arn:aws:iam::123:role/paperclip" },
|
|
resourceQuota: { ...baseInput.resourceQuota, pods: "25" },
|
|
});
|
|
|
|
expect(clients.core.replaceNamespacedServiceAccount).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
body: expect.objectContaining({
|
|
metadata: expect.objectContaining({
|
|
annotations: { "eks.amazonaws.com/role-arn": "arn:aws:iam::123:role/paperclip" },
|
|
resourceVersion: "rv-1",
|
|
}),
|
|
}),
|
|
}),
|
|
);
|
|
expect(clients.core.replaceNamespacedResourceQuota).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
body: expect.objectContaining({
|
|
metadata: expect.objectContaining({ resourceVersion: "rv-1" }),
|
|
spec: expect.objectContaining({ hard: expect.objectContaining({ pods: "25" }) }),
|
|
}),
|
|
}),
|
|
);
|
|
expect(clients.networking.replaceNamespacedNetworkPolicy).toHaveBeenCalled();
|
|
expect(clients.core.replaceNamespace).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
body: expect.objectContaining({
|
|
metadata: expect.objectContaining({
|
|
resourceVersion: "rv-ns",
|
|
labels: expect.objectContaining({
|
|
"pod-security.kubernetes.io/enforce": "restricted",
|
|
}),
|
|
}),
|
|
}),
|
|
}),
|
|
);
|
|
});
|
|
|
|
it("removes stale standard egress NetworkPolicy when cilium mode is selected", async () => {
|
|
const clients = makeMockClients();
|
|
await ensureTenant(clients as never, { ...baseInput, egressMode: "cilium" });
|
|
expect(clients.networking.deleteNamespacedNetworkPolicy).toHaveBeenCalledWith({
|
|
namespace: baseInput.namespace,
|
|
name: "paperclip-egress-allow",
|
|
});
|
|
});
|
|
|
|
it("handles concurrent first-run create conflicts by rereading and replacing managed resources", async () => {
|
|
const clients = makeMockClients();
|
|
const existing = { metadata: { resourceVersion: "rv-race" } };
|
|
clients.core.createNamespace.mockRejectedValueOnce({ code: 409 });
|
|
clients.core.readNamespace
|
|
.mockRejectedValueOnce({ code: 404 })
|
|
.mockResolvedValue({ metadata: { resourceVersion: "rv-namespace-race" } });
|
|
clients.core.readNamespacedServiceAccount
|
|
.mockRejectedValueOnce({ code: 404 })
|
|
.mockResolvedValue(existing);
|
|
clients.core.createNamespacedServiceAccount.mockRejectedValueOnce({ code: 409 });
|
|
|
|
await ensureTenant(clients as never, baseInput);
|
|
|
|
expect(clients.core.createNamespace).toHaveBeenCalled();
|
|
expect(clients.core.replaceNamespace).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
body: expect.objectContaining({
|
|
metadata: expect.objectContaining({ resourceVersion: "rv-namespace-race" }),
|
|
}),
|
|
}),
|
|
);
|
|
expect(clients.core.replaceNamespacedServiceAccount).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
body: expect.objectContaining({
|
|
metadata: expect.objectContaining({ resourceVersion: "rv-race" }),
|
|
}),
|
|
}),
|
|
);
|
|
});
|
|
});
|