import type { V1Deployment, V1Pod } from "@kubernetes/client-node"; import { LABELS } from "../const"; import { ctx } from "./context"; import { apps, core, objectApi } from "./k8s"; const ManagedBySelector = `app.kubernetes.io/managed-by=${LABELS["app.kubernetes.io/managed-by"]}`; export function getProject() { return ctx().project; } export async function listDeployments(labelSelector?: string) { const { items } = await apps.listNamespacedDeployment({ namespace: getProject(), labelSelector, }); return items; } export function listManagedDeployments() { return listDeployments(ManagedBySelector); } export async function getDeployment(name: string): Promise { const deployment = await apps.readNamespacedDeployment({ namespace: getProject(), name, }); return deployment; } export async function listPodsForDeployment(name: string): Promise { const { items } = await core.listNamespacedPod({ namespace: getProject(), labelSelector: `app=${name}`, }); return items; } export async function getPodForDeployment(name: string): Promise { const pods = await listPodsForDeployment(name); const runningPod = pods.find((pod) => pod.status?.phase === "Running"); if (runningPod) return runningPod; const pod = pods[0]; if (!pod) throw new Error(`No pod found for deployment ${name}`); return pod; } export function getPodContainerName(pod: V1Pod): string { const container = pod.spec?.containers[0]?.name; if (!container) throw new Error(`No container found in pod ${pod.metadata?.name}`); return container; } export async function scaleDeployment(name: string, replicas: number) { return objectApi.patch({ apiVersion: "apps/v1", kind: "Deployment", metadata: { name, namespace: getProject(), }, spec: { replicas, }, }); } export async function restartDeployment(name: string) { return objectApi.patch({ apiVersion: "apps/v1", kind: "Deployment", metadata: { name, namespace: getProject(), }, spec: { template: { metadata: { annotations: { "kubectl.kubernetes.io/restartedAt": new Date().toISOString(), }, }, }, }, }); } function delay(ms: number) { return new Promise((resolve) => setTimeout(resolve, ms)); } export async function waitForDeploymentRollout( name: string, timeoutMs = 300000, ) { const startedAt = Date.now(); while (Date.now() - startedAt < timeoutMs) { const deployment = await getDeployment(name); const desiredReplicas = deployment.spec?.replicas ?? 1; const observedGeneration = deployment.status?.observedGeneration ?? 0; const generation = deployment.metadata?.generation ?? 0; const updatedReplicas = deployment.status?.updatedReplicas ?? 0; const availableReplicas = deployment.status?.availableReplicas ?? 0; const unavailableReplicas = deployment.status?.unavailableReplicas ?? 0; if ( observedGeneration >= generation && updatedReplicas === desiredReplicas && availableReplicas === desiredReplicas && unavailableReplicas === 0 ) { return; } await delay(2000); } throw new Error(`Timed out waiting for deployment ${name} rollout`); } export function throwWhen( condition: (arg: T) => arg is Narrowed, message?: string, ): (arg: T) => Exclude; export function throwWhen( condition: (arg: T) => unknown, message?: string, ): (arg: T) => T; export function throwWhen(condition: (arg: T) => unknown, message?: string) { return (arg: T) => { if (condition(arg)) throw new Error(message ?? "Condition matched"); return arg; }; } export function deepMerge(target: any, source: any): any { if ( typeof target !== "object" || target === null || typeof source !== "object" || source === null ) { return source; } const result = Array.isArray(target) ? [...target] : { ...target }; for (const key of Object.keys(source)) { const targetValue = result[key]; const sourceValue = source[key]; if (Array.isArray(targetValue) && Array.isArray(sourceValue)) { result[key] = [...targetValue, ...sourceValue]; // or use a custom strategy } else if ( targetValue && typeof targetValue === "object" && sourceValue && typeof sourceValue === "object" ) { result[key] = deepMerge(targetValue, sourceValue); } else { result[key] = sourceValue; } } return result; }