Files
kuber/lib/apply.ts
T

176 lines
4.4 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,
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<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)));
}