fix(plugin): reconcile kubernetes namespace labels

This commit is contained in:
Dotta
2026-05-12 18:01:11 -05:00
committed by Chris Farhood
parent b248acd46c
commit 94fc81266f
2 changed files with 79 additions and 20 deletions
@@ -41,34 +41,48 @@ export async function ensureTenant(clients: KubeClients, input: EnsureTenantInpu
} }
async function ensureNamespace(clients: KubeClients, input: EnsureTenantInput): Promise<void> { async function ensureNamespace(clients: KubeClients, input: EnsureTenantInput): Promise<void> {
const manifest = buildNamespaceManifest(input);
try { try {
await clients.core.readNamespace({ name: input.namespace }); const existing = await clients.core.readNamespace({ name: input.namespace });
await clients.core.replaceNamespace({
name: input.namespace,
body: withResourceVersion(buildNamespaceManifest(input, existing), existing) as never,
});
return; return;
} catch (err) { } catch (err) {
if (!isNotFound(err)) throw err; if (!isNotFound(err)) throw err;
} }
try { try {
await clients.core.createNamespace({ await clients.core.createNamespace({ body: manifest });
body: {
apiVersion: "v1",
kind: "Namespace",
metadata: {
name: input.namespace,
labels: {
"paperclip.io/company-id": input.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",
},
},
},
});
} catch (err) { } catch (err) {
if (!isAlreadyExists(err)) throw err; if (!isAlreadyExists(err)) throw err;
const existing = await clients.core.readNamespace({ name: input.namespace });
await clients.core.replaceNamespace({
name: input.namespace,
body: withResourceVersion(buildNamespaceManifest(input, existing), existing) as never,
});
} }
} }
function buildNamespaceManifest(input: EnsureTenantInput, existing?: unknown): Record<string, unknown> {
const existingLabels = (existing as { metadata?: { labels?: Record<string, string> } })?.metadata?.labels ?? {};
return {
apiVersion: "v1",
kind: "Namespace",
metadata: {
name: input.namespace,
labels: {
...existingLabels,
"paperclip.io/company-id": input.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",
},
},
};
}
async function ensureServiceAccount(clients: KubeClients, input: EnsureTenantInput): Promise<void> { async function ensureServiceAccount(clients: KubeClients, input: EnsureTenantInput): Promise<void> {
const manifest = { const manifest = {
apiVersion: "v1", apiVersion: "v1",
@@ -24,6 +24,7 @@ function makeMockClients() {
createNamespacedLimitRange: track("LimitRange"), createNamespacedLimitRange: track("LimitRange"),
replaceNamespacedLimitRange: track("LimitRangeReplace"), replaceNamespacedLimitRange: track("LimitRangeReplace"),
readNamespace: vi.fn().mockRejectedValue({ code: 404 }), readNamespace: vi.fn().mockRejectedValue({ code: 404 }),
replaceNamespace: track("NamespaceReplace"),
}, },
rbac: { rbac: {
readNamespacedRole: vi.fn().mockRejectedValue({ code: 404 }), readNamespacedRole: vi.fn().mockRejectedValue({ code: 404 }),
@@ -97,17 +98,39 @@ describe("ensureTenant", () => {
expect(sa.metadata.annotations["eks.amazonaws.com/role-arn"]).toBe("arn:aws:iam::123:role/paperclip"); expect(sa.metadata.annotations["eks.amazonaws.com/role-arn"]).toBe("arn:aws:iam::123:role/paperclip");
}); });
it("does not recreate a namespace that already exists", async () => { it("reconciles a namespace that already exists", async () => {
const clients = makeMockClients(); const clients = makeMockClients();
clients.core.readNamespace.mockResolvedValue({ body: { metadata: { name: baseInput.namespace } } }); clients.core.readNamespace.mockResolvedValue({
metadata: {
name: baseInput.namespace,
resourceVersion: "rv-namespace",
labels: { "operator.example.com/team": "infra" },
},
});
await ensureTenant(clients as never, baseInput); await ensureTenant(clients as never, baseInput);
expect(clients.core.createNamespace).not.toHaveBeenCalled(); 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 () => { it("reconciles existing managed resources with the latest desired manifests", async () => {
const clients = makeMockClients(); const clients = makeMockClients();
const existing = { metadata: { resourceVersion: "rv-1" } }; const existing = { metadata: { resourceVersion: "rv-1" } };
clients.core.readNamespace.mockResolvedValue({ metadata: { name: baseInput.namespace } }); clients.core.readNamespace.mockResolvedValue({ metadata: { name: baseInput.namespace, resourceVersion: "rv-ns" } });
clients.core.readNamespacedServiceAccount.mockResolvedValue(existing); clients.core.readNamespacedServiceAccount.mockResolvedValue(existing);
clients.rbac.readNamespacedRole.mockResolvedValue(existing); clients.rbac.readNamespacedRole.mockResolvedValue(existing);
clients.rbac.readNamespacedRoleBinding.mockResolvedValue(existing); clients.rbac.readNamespacedRoleBinding.mockResolvedValue(existing);
@@ -140,6 +163,18 @@ describe("ensureTenant", () => {
}), }),
); );
expect(clients.networking.replaceNamespacedNetworkPolicy).toHaveBeenCalled(); 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 () => { it("removes stale standard egress NetworkPolicy when cilium mode is selected", async () => {
@@ -155,6 +190,9 @@ describe("ensureTenant", () => {
const clients = makeMockClients(); const clients = makeMockClients();
const existing = { metadata: { resourceVersion: "rv-race" } }; const existing = { metadata: { resourceVersion: "rv-race" } };
clients.core.createNamespace.mockRejectedValueOnce({ code: 409 }); clients.core.createNamespace.mockRejectedValueOnce({ code: 409 });
clients.core.readNamespace
.mockRejectedValueOnce({ code: 404 })
.mockResolvedValue({ metadata: { resourceVersion: "rv-namespace-race" } });
clients.core.readNamespacedServiceAccount clients.core.readNamespacedServiceAccount
.mockRejectedValueOnce({ code: 404 }) .mockRejectedValueOnce({ code: 404 })
.mockResolvedValue(existing); .mockResolvedValue(existing);
@@ -163,6 +201,13 @@ describe("ensureTenant", () => {
await ensureTenant(clients as never, baseInput); await ensureTenant(clients as never, baseInput);
expect(clients.core.createNamespace).toHaveBeenCalled(); 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(clients.core.replaceNamespacedServiceAccount).toHaveBeenCalledWith(
expect.objectContaining({ expect.objectContaining({
body: expect.objectContaining({ body: expect.objectContaining({