import type * as k8s from "@kubernetes/client-node"; import type { DeploymentInfo, K8sConfigMapSummary, K8sIngressSummary, K8sNodeSummary, K8sServiceInfo, K8sServicePort, K8sServiceSummary, PodDetails, PodInfo, StatefulSetInfo, } from "@minikura/api"; import { getAge } from "@minikura/shared/errors"; export class K8sResources { constructor( private readonly coreApi: k8s.CoreV1Api, private readonly appsApi: k8s.AppsV1Api, private readonly networkingApi: k8s.NetworkingV1Api, private readonly namespace: string ) {} async listPods(): Promise { const response = await this.coreApi.listNamespacedPod({ namespace: this.namespace }); return response.items.map((pod) => mapPodInfo(pod)); } async listPodsByLabel(labelSelector: string): Promise { const response = await this.coreApi.listNamespacedPod({ namespace: this.namespace, labelSelector, }); return response.items.map((pod) => ({ ...mapPodInfo(pod), containers: pod.spec?.containers?.map((container) => container.name ?? "") || [], })); } async getPodInfo(podName: string): Promise { const response = await this.coreApi.readNamespacedPod({ name: podName, namespace: this.namespace, }); const pod = response; return { ...mapPodInfo(pod), containers: pod.spec?.containers?.map((container) => container.name ?? "") || [], ip: pod.status?.podIP, conditions: pod.status?.conditions?.map((condition) => ({ type: condition.type, status: condition.status, lastTransitionTime: condition.lastTransitionTime ? condition.lastTransitionTime.toISOString() : undefined, })) || [], containerStatuses: pod.status?.containerStatuses?.map((status) => ({ name: status.name, ready: status.ready, restartCount: status.restartCount, state: status.state ? { waiting: status.state.waiting ? { reason: status.state.waiting.reason, message: status.state.waiting.message, } : undefined, running: status.state.running ? { startedAt: status.state.running.startedAt, } : undefined, terminated: status.state.terminated ? { reason: status.state.terminated.reason, exitCode: status.state.terminated.exitCode, finishedAt: status.state.terminated.finishedAt, } : undefined, } : undefined, })) || [], }; } async listDeployments(): Promise { const response = await this.appsApi.listNamespacedDeployment({ namespace: this.namespace }); return response.items.map((deployment) => ({ name: deployment.metadata?.name ?? "", namespace: deployment.metadata?.namespace, ready: `${deployment.status?.readyReplicas ?? 0}/${deployment.status?.replicas ?? 0}`, desired: deployment.status?.replicas ?? 0, current: deployment.status?.replicas ?? 0, updated: deployment.status?.updatedReplicas ?? 0, upToDate: deployment.status?.updatedReplicas ?? 0, available: deployment.status?.availableReplicas ?? 0, age: getAge(deployment.metadata?.creationTimestamp), labels: deployment.metadata?.labels, })); } async listStatefulSets(): Promise { const response = await this.appsApi.listNamespacedStatefulSet({ namespace: this.namespace }); return response.items.map((statefulSet) => ({ name: statefulSet.metadata?.name ?? "", namespace: statefulSet.metadata?.namespace, ready: `${statefulSet.status?.readyReplicas ?? 0}/${statefulSet.spec?.replicas ?? 0}`, desired: statefulSet.spec?.replicas ?? 0, current: statefulSet.status?.currentReplicas ?? 0, updated: statefulSet.status?.updatedReplicas ?? 0, age: getAge(statefulSet.metadata?.creationTimestamp), labels: statefulSet.metadata?.labels, })); } async listServices(): Promise { const response = await this.coreApi.listNamespacedService({ namespace: this.namespace }); return response.items.map((service) => { const ports = service.spec?.ports ?? []; const portSummary = ports .map((port) => `${port.port}${port.nodePort ? `:${port.nodePort}` : ""}/${port.protocol}`) .join(", "); return { name: service.metadata?.name ?? "", namespace: service.metadata?.namespace, type: service.spec?.type, clusterIP: service.spec?.clusterIP ?? null, externalIP: service.status?.loadBalancer?.ingress?.[0]?.ip || service.spec?.externalIPs?.join(", ") || "", ports: portSummary, age: getAge(service.metadata?.creationTimestamp), labels: service.metadata?.labels, }; }); } async listConfigMaps(): Promise { const response = await this.coreApi.listNamespacedConfigMap({ namespace: this.namespace }); return response.items.map((configMap) => ({ name: configMap.metadata?.name ?? "", namespace: configMap.metadata?.namespace, data: Object.keys(configMap.data ?? {}).length, age: getAge(configMap.metadata?.creationTimestamp), labels: configMap.metadata?.labels, })); } async listIngresses(): Promise { const response = await this.networkingApi.listNamespacedIngress({ namespace: this.namespace }); return response.items.map((ingress) => { const hosts = ingress.spec?.rules ?.map((rule) => rule.host) .filter((host): host is string => Boolean(host)) .join(", ") || ""; const addresses = ingress.status?.loadBalancer?.ingress ?.map((item) => item.ip || item.hostname) .filter((entry): entry is string => Boolean(entry)) .join(", ") || ""; return { name: ingress.metadata?.name ?? "", namespace: ingress.metadata?.namespace, className: ingress.spec?.ingressClassName ?? null, hosts, address: addresses, age: getAge(ingress.metadata?.creationTimestamp), labels: ingress.metadata?.labels, }; }); } async getServiceInfo(serviceName: string): Promise { const response = await this.coreApi.readNamespacedService({ name: serviceName, namespace: this.namespace, }); const service = response; const ports: K8sServicePort[] = service.spec?.ports?.map((port) => ({ name: port.name ?? null, protocol: port.protocol ?? null, port: port.port, targetPort: port.targetPort, nodePort: port.nodePort ?? null, })) || []; return { name: service.metadata?.name, namespace: service.metadata?.namespace, type: service.spec?.type, clusterIP: service.spec?.clusterIP ?? null, externalIPs: service.spec?.externalIPs || [], loadBalancerIP: service.status?.loadBalancer?.ingress?.[0]?.ip || null, loadBalancerHostname: service.status?.loadBalancer?.ingress?.[0]?.hostname || null, ports, selector: service.spec?.selector, }; } async listNodes(): Promise { const response = await this.coreApi.listNode(); return response.items.map((node) => { const labels = node.metadata?.labels ?? {}; const roles = Object.keys(labels) .filter((label) => label.startsWith("node-role.kubernetes.io/")) .map((label) => label.replace("node-role.kubernetes.io/", "")) .join(","); const addresses = node.status?.addresses ?? []; const internalIP = addresses.find((address) => address.type === "InternalIP")?.address; const externalIP = addresses.find((address) => address.type === "ExternalIP")?.address; const hostname = addresses.find((address) => address.type === "Hostname")?.address; const readyCondition = node.status?.conditions?.find( (condition) => condition.type === "Ready" ); return { name: node.metadata?.name, status: readyCondition?.status === "True" ? "Ready" : "NotReady", roles: roles || "", age: getAge(node.metadata?.creationTimestamp), version: node.status?.nodeInfo?.kubeletVersion, internalIP, externalIP, hostname, }; }); } } function mapPodInfo(pod: k8s.V1Pod): PodInfo { const containerStatuses = pod.status?.containerStatuses ?? []; const readyCount = containerStatuses.filter((status) => status.ready).length; const totalCount = containerStatuses.length; const restarts = containerStatuses.reduce( (accumulator, status) => accumulator + (status.restartCount ?? 0), 0 ); return { name: pod.metadata?.name ?? "", namespace: pod.metadata?.namespace, status: pod.status?.phase ?? "Unknown", ready: `${readyCount}/${totalCount}`, restarts, age: getAge(pod.metadata?.creationTimestamp), labels: pod.metadata?.labels, nodeName: pod.spec?.nodeName, }; }