168 lines
4.2 KiB
TypeScript
168 lines
4.2 KiB
TypeScript
import { PatchStrategy } from "@kubernetes/client-node";
|
|
import type { KubernetesObject } from "@kubernetes/client-node";
|
|
import { LABELS } from "../const";
|
|
import { objectApi } from "./k8s";
|
|
|
|
const FIELD_MANAGER = "kuber";
|
|
|
|
const ResourceOrder = {
|
|
Namespace: 0,
|
|
GarageBucket: 1,
|
|
GarageKey: 2,
|
|
PersistentVolumeClaim: 3,
|
|
Secret: 4,
|
|
ConfigMap: 5,
|
|
Service: 6,
|
|
Deployment: 7,
|
|
Ingress: 8,
|
|
IngressRoute: 9,
|
|
} as const;
|
|
|
|
const ManagedResources = [
|
|
{ apiVersion: "v1", kind: "PersistentVolumeClaim" },
|
|
{ apiVersion: "v1", kind: "Secret" },
|
|
{ apiVersion: "v1", kind: "ConfigMap" },
|
|
{ apiVersion: "v1", kind: "Service" },
|
|
{ apiVersion: "apps/v1", kind: "Deployment" },
|
|
{ apiVersion: "networking.k8s.io/v1", kind: "Ingress" },
|
|
{ apiVersion: "traefik.io/v1alpha1", kind: "IngressRoute" },
|
|
] as const;
|
|
|
|
const ManagedBySelector = `app.kubernetes.io/managed-by=${LABELS["app.kubernetes.io/managed-by"]}`;
|
|
const ManagedByLabel = LABELS["app.kubernetes.io/managed-by"];
|
|
|
|
function getResourceOrder(resource: KubernetesObject): number {
|
|
return ResourceOrder[resource.kind as keyof typeof ResourceOrder] ?? 999;
|
|
}
|
|
|
|
export function sortResources<T extends KubernetesObject>(resources: T[]): T[] {
|
|
return [...resources].sort(
|
|
(left, right) => getResourceOrder(left) - getResourceOrder(right),
|
|
);
|
|
}
|
|
|
|
export function getResourceKey(resource: KubernetesObject): string {
|
|
return `${resource.kind}:${resource.metadata?.namespace ?? ""}:${resource.metadata?.name ?? ""}`;
|
|
}
|
|
|
|
export async function getNamespaceManagementStatus(
|
|
namespace: string,
|
|
): Promise<"missing" | "managed" | "external"> {
|
|
try {
|
|
const existing = await objectApi.read({
|
|
apiVersion: "v1",
|
|
kind: "Namespace",
|
|
metadata: { name: namespace },
|
|
});
|
|
|
|
return existing.metadata?.labels?.["app.kubernetes.io/managed-by"] ===
|
|
ManagedByLabel
|
|
? "managed"
|
|
: "external";
|
|
} catch (error) {
|
|
if (
|
|
error &&
|
|
typeof error === "object" &&
|
|
"code" in error &&
|
|
error.code === 404
|
|
) {
|
|
return "missing";
|
|
}
|
|
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
export async function assertManagedNamespace(namespace: string): Promise<void> {
|
|
const status = await getNamespaceManagementStatus(namespace);
|
|
if (status === "external") {
|
|
throw new Error(
|
|
`Namespace ${namespace} is not managed by kuber. Refusing to mutate it.`,
|
|
);
|
|
}
|
|
}
|
|
|
|
export async function applyResource<T extends KubernetesObject>(
|
|
resource: T,
|
|
): Promise<T> {
|
|
return objectApi.patch(
|
|
resource,
|
|
undefined,
|
|
undefined,
|
|
FIELD_MANAGER,
|
|
true,
|
|
PatchStrategy.ServerSideApply,
|
|
);
|
|
}
|
|
|
|
export async function applyResources<T extends KubernetesObject>(
|
|
resources: T[],
|
|
): Promise<T[]> {
|
|
const applied: T[] = [];
|
|
|
|
for (const resource of sortResources(resources)) {
|
|
applied.push(await applyResource(resource));
|
|
}
|
|
|
|
return applied;
|
|
}
|
|
|
|
export async function deleteResource(
|
|
resource: KubernetesObject,
|
|
): Promise<void> {
|
|
await objectApi.delete(resource);
|
|
}
|
|
|
|
export async function deleteResources(
|
|
resources: KubernetesObject[],
|
|
): Promise<void> {
|
|
for (const resource of sortResources(resources).reverse()) {
|
|
await deleteResource(resource);
|
|
}
|
|
}
|
|
|
|
export async function listManagedResources(
|
|
namespace: string,
|
|
): Promise<KubernetesObject[]> {
|
|
const resources = await Promise.all(
|
|
ManagedResources.map(async ({ apiVersion, kind }) => {
|
|
const list = await objectApi.list(
|
|
apiVersion,
|
|
kind,
|
|
namespace,
|
|
undefined,
|
|
undefined,
|
|
undefined,
|
|
undefined,
|
|
ManagedBySelector,
|
|
);
|
|
|
|
return list.items.map((item) => ({
|
|
...item,
|
|
apiVersion: item.apiVersion ?? apiVersion,
|
|
kind: item.kind ?? kind,
|
|
metadata: {
|
|
...item.metadata,
|
|
namespace: item.metadata?.namespace ?? namespace,
|
|
},
|
|
}));
|
|
}),
|
|
);
|
|
|
|
return resources.flat();
|
|
}
|
|
|
|
export async function getStaleResources(
|
|
namespace: string,
|
|
desiredResources: KubernetesObject[],
|
|
): Promise<KubernetesObject[]> {
|
|
const desired = new Set(
|
|
desiredResources
|
|
.filter((resource) => resource.kind !== "Namespace")
|
|
.map(getResourceKey),
|
|
);
|
|
|
|
const existing = await listManagedResources(namespace);
|
|
return existing.filter((resource) => !desired.has(getResourceKey(resource)));
|
|
}
|