feat: topology, and improves handling

This commit is contained in:
2026-02-17 19:17:13 +07:00
parent e8dbefde43
commit d14f043e7c
145 changed files with 4213 additions and 2861 deletions
@@ -0,0 +1,40 @@
import { getErrorMessage } from "@minikura/shared/errors";
import { logger } from "../../../infrastructure/logger";
export abstract class BaseK8sOperations {
protected namespace: string;
constructor(namespace: string) {
this.namespace = namespace;
}
protected async executeOperation<T>(
operation: () => Promise<T>,
errorContext: string
): Promise<T> {
try {
return await operation();
} catch (error: unknown) {
const errorMessage = getErrorMessage(error);
logger.error({ err: error, context: errorContext }, "K8s operation failed");
throw new Error(`${errorContext}: ${errorMessage}`);
}
}
protected async executeOperationSafe<T>(
operation: () => Promise<T>,
defaultValue: T,
errorContext: string
): Promise<T> {
try {
return await operation();
} catch (error: unknown) {
logger.error({ err: error, context: errorContext }, "K8s operation failed (safe mode)");
return defaultValue;
}
}
getNamespace(): string {
return this.namespace;
}
}
@@ -0,0 +1,39 @@
import type * as k8s from "@kubernetes/client-node";
import { BaseK8sOperations } from "./base.operations";
export class ClusterOperations extends BaseK8sOperations {
constructor(
private coreApi: k8s.CoreV1Api,
private customObjectsApi: k8s.CustomObjectsApi,
namespace: string
) {
super(namespace);
}
async listNodes() {
return this.executeOperation(
() => this.coreApi.listNode().then((r) => r.items),
"Failed to fetch nodes"
);
}
async getNodeMetrics() {
return this.executeOperation(
() =>
this.customObjectsApi.listClusterCustomObject({
group: "metrics.k8s.io",
version: "v1beta1",
plural: "nodes",
}),
"Failed to fetch node metrics"
);
}
async listConfigMaps(namespace?: string) {
const ns = namespace || this.namespace;
return this.executeOperation(
() => this.coreApi.listNamespacedConfigMap({ namespace: ns }).then((r) => r.items),
"Failed to fetch configmaps"
);
}
}
@@ -0,0 +1,55 @@
import type * as k8s from "@kubernetes/client-node";
import type { CustomResourceSummary } from "@minikura/api";
import { getAge } from "@minikura/shared/errors";
import { BaseK8sOperations } from "./base.operations";
interface CustomResourceItem {
kind?: string;
metadata?: {
name?: string;
namespace?: string;
creationTimestamp?: string;
labels?: Record<string, string>;
};
spec?: Record<string, unknown>;
status?: { phase?: string; [key: string]: unknown };
}
interface CustomResourceList {
items?: CustomResourceItem[];
}
export class CustomResourceOperations extends BaseK8sOperations {
constructor(
private customObjectsApi: k8s.CustomObjectsApi,
namespace: string
) {
super(namespace);
}
async listCustomResources(
group: string,
version: string,
plural: string
): Promise<CustomResourceSummary[]> {
return this.executeOperation(async () => {
const response = await this.customObjectsApi.listNamespacedCustomObject({
group,
version,
namespace: this.namespace,
plural,
});
const items = (response as unknown as CustomResourceList).items || [];
return items.map((item) => ({
name: item.metadata?.name ?? "",
namespace: item.metadata?.namespace ?? this.namespace,
age: getAge(item.metadata?.creationTimestamp),
labels: item.metadata?.labels,
spec: item.spec ?? {},
status: item.status ?? {},
}));
}, `Failed to fetch custom resources (${plural})`);
}
}
@@ -0,0 +1,6 @@
export { BaseK8sOperations } from "./base.operations";
export { ClusterOperations } from "./cluster.operations";
export { CustomResourceOperations } from "./custom-resource.operations";
export { NetworkOperations } from "./network.operations";
export { PodOperations } from "./pod.operations";
export { WorkloadOperations } from "./workload.operations";
@@ -0,0 +1,89 @@
import type * as k8s from "@kubernetes/client-node";
import { BaseK8sOperations } from "./base.operations";
export class NetworkOperations extends BaseK8sOperations {
constructor(
private coreApi: k8s.CoreV1Api,
private networkingApi: k8s.NetworkingV1Api,
namespace: string
) {
super(namespace);
}
async listServices() {
return this.executeOperation(
() => this.coreApi.listNamespacedService({ namespace: this.namespace }).then((r) => r.items),
"Failed to fetch services"
);
}
async listIngresses() {
return this.executeOperation(
() =>
this.networkingApi
.listNamespacedIngress({ namespace: this.namespace })
.then((r) => r.items),
"Failed to fetch ingresses"
);
}
async getServiceInfo(serviceName: string) {
return this.executeOperation(
() => this.coreApi.readNamespacedService({ name: serviceName, namespace: this.namespace }),
`Failed to fetch service info for ${serviceName}`
);
}
async getServerConnectionInfo(serviceName: string) {
return this.executeOperation(async () => {
const service = await this.coreApi.readNamespacedService({
name: serviceName,
namespace: this.namespace,
});
const serviceType = service.spec?.type || "ClusterIP";
const ports = service.spec?.ports || [];
let host: string;
let externalHost: string | undefined;
switch (serviceType) {
case "LoadBalancer": {
const ingress = service.status?.loadBalancer?.ingress?.[0];
host =
ingress?.hostname ||
ingress?.ip ||
`${serviceName}.${this.namespace}.svc.cluster.local`;
externalHost = ingress?.hostname || ingress?.ip;
break;
}
case "NodePort":
host = `${serviceName}.${this.namespace}.svc.cluster.local`;
externalHost = "<node-ip>";
break;
default:
host = `${serviceName}.${this.namespace}.svc.cluster.local`;
externalHost = undefined;
}
const portMappings = ports.map((port) => ({
name: port.name,
port: port.port,
targetPort: port.targetPort,
nodePort: port.nodePort,
protocol: port.protocol || "TCP",
}));
return {
serviceName,
namespace: this.namespace,
serviceType,
internalHost: host,
externalHost,
ports: portMappings,
};
}, `Failed to fetch connection info for service ${serviceName}`);
}
}
@@ -0,0 +1,72 @@
import type * as k8s from "@kubernetes/client-node";
import { BaseK8sOperations } from "./base.operations";
export class PodOperations extends BaseK8sOperations {
constructor(
private coreApi: k8s.CoreV1Api,
namespace: string
) {
super(namespace);
}
async listPods() {
return this.executeOperation(
() => this.coreApi.listNamespacedPod({ namespace: this.namespace }).then((r) => r.items),
"Failed to fetch pods"
);
}
async listPodsByLabel(labelSelector: string) {
return this.executeOperation(
() =>
this.coreApi
.listNamespacedPod({ namespace: this.namespace, labelSelector })
.then((r) => r.items),
"Failed to fetch pods by label"
);
}
async getPodInfo(podName: string) {
return this.executeOperation(
() => this.coreApi.readNamespacedPod({ name: podName, namespace: this.namespace }),
`Failed to fetch pod info for ${podName}`
);
}
async getPodLogs(
podName: string,
options?: {
container?: string;
tailLines?: number;
timestamps?: boolean;
sinceSeconds?: number;
}
): Promise<string> {
return this.executeOperation(
() =>
this.coreApi.readNamespacedPodLog({
name: podName,
namespace: this.namespace,
container: options?.container,
tailLines: options?.tailLines,
timestamps: options?.timestamps,
sinceSeconds: options?.sinceSeconds,
}),
`Failed to fetch logs for pod ${podName}`
);
}
async getPodMetrics(customObjectsApi: k8s.CustomObjectsApi, namespace?: string) {
const ns = namespace || this.namespace;
return this.executeOperation(
() =>
customObjectsApi.listNamespacedCustomObject({
group: "metrics.k8s.io",
version: "v1beta1",
namespace: ns,
plural: "pods",
}),
"Failed to fetch pod metrics"
);
}
}
@@ -0,0 +1,27 @@
import type * as k8s from "@kubernetes/client-node";
import { BaseK8sOperations } from "./base.operations";
export class WorkloadOperations extends BaseK8sOperations {
constructor(
private appsApi: k8s.AppsV1Api,
namespace: string
) {
super(namespace);
}
async listDeployments() {
return this.executeOperation(
() =>
this.appsApi.listNamespacedDeployment({ namespace: this.namespace }).then((r) => r.items),
"Failed to fetch deployments"
);
}
async listStatefulSets() {
return this.executeOperation(
() =>
this.appsApi.listNamespacedStatefulSet({ namespace: this.namespace }).then((r) => r.items),
"Failed to fetch statefulsets"
);
}
}