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, ServiceAccount: 3, ClusterRole: 4, Role: 5, ClusterRoleBinding: 6, RoleBinding: 7, StorageClass: 8, PersistentVolumeClaim: 9, Secret: 10, ConfigMap: 11, Service: 12, Deployment: 13, HorizontalPodAutoscaler: 14, Ingress: 15, IngressRoute: 16, } 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: "autoscaling/v2", kind: "HorizontalPodAutoscaler" }, { 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(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 { 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( resource: T, ): Promise { return objectApi.patch( resource, undefined, undefined, FIELD_MANAGER, true, PatchStrategy.ServerSideApply, ); } export async function applyResources( resources: T[], ): Promise { const applied: T[] = []; for (const resource of sortResources(resources)) { applied.push(await applyResource(resource)); } return applied; } export async function deleteResource( resource: KubernetesObject, ): Promise { await objectApi.delete(resource); } export async function deleteResources( resources: KubernetesObject[], ): Promise { for (const resource of sortResources(resources).reverse()) { await deleteResource(resource); } } export async function listManagedResources( namespace: string, ): Promise { 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 { 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))); }