forked from farhoodlabs/paperclip
fix(plugin): reconcile kubernetes namespace labels
This commit is contained in:
@@ -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",
|
||||||
|
|||||||
+48
-3
@@ -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({
|
||||||
|
|||||||
Reference in New Issue
Block a user