134 lines
4.1 KiB
TypeScript
134 lines
4.1 KiB
TypeScript
import { afterEach, describe, expect, mock, spyOn, test } from "bun:test";
|
|
import type { KubernetesObject } from "@kubernetes/client-node";
|
|
import { applyResource, getResourceKey, sortResources } from "../../lib/apply";
|
|
import { objectApi } from "../../lib/k8s";
|
|
|
|
afterEach(() => mock.restore());
|
|
|
|
function resource(kind: string, name = kind): KubernetesObject {
|
|
return { apiVersion: "v1", kind, metadata: { name, namespace: "project" } };
|
|
}
|
|
|
|
describe("resource ordering", () => {
|
|
test("sorts creation dependencies in deterministic order", () => {
|
|
const input = [
|
|
resource("Database"),
|
|
resource("IngressRoute"),
|
|
resource("Deployment"),
|
|
resource("GarageKey"),
|
|
resource("HorizontalPodAutoscaler"),
|
|
resource("Service"),
|
|
resource("GarageBucket"),
|
|
resource("ConfigMap"),
|
|
resource("Secret"),
|
|
resource("PersistentVolumeClaim"),
|
|
resource("StorageClass"),
|
|
resource("RoleBinding"),
|
|
resource("ClusterRoleBinding"),
|
|
resource("Role"),
|
|
resource("ClusterRole"),
|
|
resource("ServiceAccount"),
|
|
resource("Namespace"),
|
|
resource("Ingress"),
|
|
];
|
|
expect(sortResources(input).map((item) => item.kind)).toEqual([
|
|
"Namespace",
|
|
"GarageBucket",
|
|
"GarageKey",
|
|
"ServiceAccount",
|
|
"ClusterRole",
|
|
"Role",
|
|
"ClusterRoleBinding",
|
|
"RoleBinding",
|
|
"StorageClass",
|
|
"PersistentVolumeClaim",
|
|
"Secret",
|
|
"ConfigMap",
|
|
"Service",
|
|
"Deployment",
|
|
"HorizontalPodAutoscaler",
|
|
"Ingress",
|
|
"IngressRoute",
|
|
"Database",
|
|
]);
|
|
expect(input[0]?.kind).toBe("Database");
|
|
});
|
|
|
|
test("preserves order for unknown resource kinds", () => {
|
|
expect(
|
|
sortResources([resource("First"), resource("Second")]).map(
|
|
(item) => item.kind,
|
|
),
|
|
).toEqual(["First", "Second"]);
|
|
});
|
|
|
|
test("builds keys with kind, namespace, and name", () => {
|
|
expect(getResourceKey(resource("Secret", "app-env"))).toBe(
|
|
"Secret:project:app-env",
|
|
);
|
|
expect(
|
|
getResourceKey({
|
|
apiVersion: "v1",
|
|
kind: "Namespace",
|
|
metadata: { name: "app" },
|
|
}),
|
|
).toBe("Namespace::app");
|
|
});
|
|
});
|
|
|
|
describe("resource apply", () => {
|
|
test("does not send server-managed fields from a resource read from Kubernetes", async () => {
|
|
const returned = {
|
|
apiVersion: "postgresql.cnpg.io/v1",
|
|
kind: "Database",
|
|
metadata: {
|
|
name: "orders",
|
|
namespace: "database",
|
|
labels: { "kuber.dev/project": "demo" },
|
|
annotations: { "example.dev/setting": "enabled" },
|
|
managedFields: [{ manager: "kube-controller-manager" }],
|
|
resourceVersion: "42",
|
|
uid: "database-uid",
|
|
creationTimestamp: new Date("2026-09-08T00:00:00Z"),
|
|
generation: 3,
|
|
selfLink:
|
|
"/apis/postgresql.cnpg.io/v1/namespaces/database/databases/orders",
|
|
},
|
|
spec: { name: "orders" },
|
|
status: { phase: "Ready" },
|
|
} as KubernetesObject;
|
|
const patch = spyOn(objectApi, "patch").mockImplementation(
|
|
async (resource) => resource as never,
|
|
);
|
|
|
|
await applyResource(returned);
|
|
|
|
expect(patch).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
metadata: {
|
|
name: "orders",
|
|
namespace: "database",
|
|
labels: { "kuber.dev/project": "demo" },
|
|
annotations: { "example.dev/setting": "enabled" },
|
|
},
|
|
spec: { name: "orders" },
|
|
}),
|
|
undefined,
|
|
undefined,
|
|
"kuber",
|
|
true,
|
|
"application/apply-patch+yaml",
|
|
);
|
|
const sent = patch.mock.calls[0]?.[0] as typeof returned;
|
|
expect(sent.metadata).not.toHaveProperty("managedFields");
|
|
expect(sent.metadata).not.toHaveProperty("resourceVersion");
|
|
expect(sent.metadata).not.toHaveProperty("uid");
|
|
expect(sent.metadata).not.toHaveProperty("creationTimestamp");
|
|
expect(sent.metadata).not.toHaveProperty("generation");
|
|
expect(sent.metadata).not.toHaveProperty("selfLink");
|
|
expect(sent).not.toHaveProperty("status");
|
|
expect(returned.metadata).toHaveProperty("managedFields");
|
|
expect(returned).toHaveProperty("status");
|
|
});
|
|
});
|