mirror of
https://github.com/YuzuZensai/Minikura.git
synced 2026-09-14 03:09:50 +00:00
✨ feat: topology, and improves handling
This commit is contained in:
@@ -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"
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user