diff --git a/src/app/settings/cluster/actions.ts b/src/app/settings/cluster/actions.ts new file mode 100644 index 0000000..601bc6f --- /dev/null +++ b/src/app/settings/cluster/actions.ts @@ -0,0 +1,16 @@ +'use server' + +import { ServiceException } from "@/model/service.exception.model"; +import { ProfilePasswordChangeModel, profilePasswordChangeZodModel } from "@/model/update-password.model"; +import userService from "@/server/services/user.service"; +import { getAuthUserSession, saveFormAction, simpleAction } from "@/server/utils/action-wrapper.utils"; +import { TotpModel, totpZodModel } from "@/model/update-password.model copy"; +import { SuccessActionResult } from "@/model/server-action-error-return.model"; +import clusterService from "@/server/services/node.service"; + +export const setNodeStatus = async (nodeName: string, schedulable: boolean) => + simpleAction(async () => { + await getAuthUserSession(); + await clusterService.setNodeStatus(nodeName, schedulable); + return new SuccessActionResult(undefined, 'Successfully updated node status.'); + }); diff --git a/src/app/settings/cluster/nodeInfo.tsx b/src/app/settings/cluster/nodeInfo.tsx index 509f126..24b063f 100644 --- a/src/app/settings/cluster/nodeInfo.tsx +++ b/src/app/settings/cluster/nodeInfo.tsx @@ -3,11 +3,28 @@ import { NodeInfoModel } from "@/model/node-info.model"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { Code } from "@/components/custom/code"; +import { Toast } from "@/lib/toast.utils"; +import { setNodeStatus } from "./actions"; +import { Button } from "@/components/ui/button"; +import { useConfirmDialog } from "@/lib/zustand.states"; +import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"; export default async function NodeInfo({ nodeInfos }: { nodeInfos: NodeInfoModel[] }) { - return ( + const { openDialog } = useConfirmDialog(); + const setNodeStatusClick = async (nodeName: string, schedulable: boolean) => { + const confirmation = await openDialog({ + title: 'Update Node Status', + description: `Do you really want to ${schedulable ? 'activate' : 'deactivate'} Node ${nodeName}? ${!schedulable ? 'This will stop all running containers on this node and moves the workload to the other nodes. Future workloads won\'t be scheduled on this node.' : ''}`, + yesButton: 'Yes', + noButton: 'cancel' + }); + if (confirmation) { + Toast.fromAction(() => setNodeStatus(nodeName, schedulable)); + } + } + return ( Nodes @@ -18,38 +35,73 @@ export default async function NodeInfo({ nodeInfos }: { nodeInfos: NodeInfoModel {nodeInfos.map((nodeInfo, index) => (
-

+

Node {index + 1}

-
-
+
+
+ + +
CPU
+ +

{nodeInfo.pidStatusText}

+
+
+
+ + + +
RAM
+
+ +

{nodeInfo.memoryStatusText}

+
+
+
+ + + +
Disk
+
+ +

{nodeInfo.diskStatusText}

+
+
+
+ + +
+
Name: {nodeInfo.name}
+
+ Schedulable: + + + + {nodeInfo.schedulable ? 'Yes' : 'No'} + + +

{nodeInfo.schedulable ? 'Node is ready to run containers.' : 'Node ist deactivated. All containers will be scheduled on other nodes.'}

+
+
+
+
IP: {nodeInfo.ip}
-
- CPU Cores: {nodeInfo.cpuCapacity} -
-
- Memory: {nodeInfo.ramCapacity} -
-
- OS: {nodeInfo.os} | {nodeInfo.architecture} -
-
-
-
-
-
-
-
+
+ Spec: {nodeInfo.cpuCapacity} CPU Cores, {nodeInfo.ramCapacity} Memory
+ OS: {nodeInfo.os} | {nodeInfo.architecture}
Kernel Version: {nodeInfo.kernelVersion}
Container Runtime Version: {nodeInfo.containerRuntimeVersion}
Kube Proxy Version: {nodeInfo.kubeProxyVersion}
Kubelet Version: {nodeInfo.kubeletVersion}
+
+ +
))}
diff --git a/src/model/node-info.model.ts b/src/model/node-info.model.ts index eb141d1..4675f66 100644 --- a/src/model/node-info.model.ts +++ b/src/model/node-info.model.ts @@ -1,4 +1,5 @@ import { stringToNumber, stringToOptionalNumber } from "@/lib/zod.utils"; +import { pid } from "process"; import { z } from "zod"; export const nodeInfoZodModel = z.object({ @@ -13,6 +14,13 @@ export const nodeInfoZodModel = z.object({ kernelVersion: z.string(), kubeProxyVersion: z.string(), kubeletVersion: z.string(), + memoryOk: z.boolean(), + diskOk: z.boolean(), + pidOk: z.boolean(), + schedulable: z.boolean(), + memoryStatusText: z.string().optional(), + diskStatusText: z.string().optional(), + pidStatusText: z.string().optional(), }) export type NodeInfoModel = z.infer; \ No newline at end of file diff --git a/src/server/services/node.service.ts b/src/server/services/node.service.ts index 444ca92..16d0f22 100644 --- a/src/server/services/node.service.ts +++ b/src/server/services/node.service.ts @@ -1,25 +1,63 @@ +import { spec } from "node:test/reporters"; import k3s from "../adapter/kubernetes-api.adapter"; import { NodeInfoModel } from "@/model/node-info.model"; +import { Tags } from "../utils/cache-tag-generator.utils"; +import { revalidateTag, unstable_cache } from "next/cache"; class ClusterService { async getNodeInfo(): Promise { - const nodeReturnInfo = await k3s.core.listNode(); - return nodeReturnInfo.body.items.map((node) => { - return { - name: node.metadata?.name!, - status: node.status?.conditions?.filter((condition) => condition.type === 'Ready')[0].status!, - os: node.status?.nodeInfo?.osImage!, - architecture: node.status?.nodeInfo?.architecture!, - cpuCapacity: node.status?.capacity?.cpu!, - ramCapacity: node.status?.capacity?.memory!, - ip: node.status?.addresses?.filter((address) => address.type === 'InternalIP')[0].address!, - kernelVersion: node.status?.nodeInfo?.kernelVersion!, - containerRuntimeVersion: node.status?.nodeInfo?.containerRuntimeVersion!, - kubeProxyVersion: node.status?.nodeInfo?.kubeProxyVersion!, - kubeletVersion: node.status?.nodeInfo?.kubeletVersion!, + return await unstable_cache(async () => { + const nodeReturnInfo = await k3s.core.listNode(); + return nodeReturnInfo.body.items.map((node) => { + return { + name: node.metadata?.name!, + status: node.status?.conditions?.filter((condition) => condition.type === 'Ready')[0].status!, + os: node.status?.nodeInfo?.osImage!, + architecture: node.status?.nodeInfo?.architecture!, + cpuCapacity: node.status?.capacity?.cpu!, + ramCapacity: node.status?.capacity?.memory!, + ip: node.status?.addresses?.filter((address) => address.type === 'InternalIP')[0].address!, + kernelVersion: node.status?.nodeInfo?.kernelVersion!, + containerRuntimeVersion: node.status?.nodeInfo?.containerRuntimeVersion!, + kubeProxyVersion: node.status?.nodeInfo?.kubeProxyVersion!, + kubeletVersion: node.status?.nodeInfo?.kubeletVersion!, + + memoryOk: node.status?.conditions?.filter((condition) => condition.type === 'MemoryPressure')[0].status === 'False', + memoryStatusText: node.status?.conditions?.filter((condition) => condition.type === 'MemoryPressure')[0].message, + diskOk: node.status?.conditions?.filter((condition) => condition.type === 'DiskPressure')[0].status === 'False', + diskStatusText: node.status?.conditions?.filter((condition) => condition.type === 'DiskPressure')[0].message, + pidOk: node.status?.conditions?.filter((condition) => condition.type === 'PIDPressure')[0].status === 'False', + pidStatusText: node.status?.conditions?.filter((condition) => condition.type === 'PIDPressure')[0].message, + schedulable: !node.spec?.unschedulable + } + }); + }, + [Tags.nodeInfos()], { + revalidate: 10, + tags: [Tags.nodeInfos()] + })(); + + } + + async setNodeStatus(nodeName: string, schedulable: boolean) { + try { + await k3s.core.patchNode(nodeName, { "spec": { "unschedulable": schedulable ? null : true } }, undefined, undefined, undefined, undefined, undefined, { + headers: { 'Content-Type': 'application/strategic-merge-patch+json' }, + }); + + if (!schedulable) { + // delete all pods on node + const pods = await k3s.core.listPodForAllNamespaces(); + for (const pod of pods.body.items) { + if (pod.spec?.nodeName === nodeName) { + await k3s.core.deleteNamespacedPod(pod.metadata?.name!, pod.metadata?.namespace!); + } + } } - }); + } finally { + revalidateTag(Tags.nodeInfos()); + } } } diff --git a/src/server/utils/cache-tag-generator.utils.ts b/src/server/utils/cache-tag-generator.utils.ts index 14912bb..cfd1e48 100644 --- a/src/server/utils/cache-tag-generator.utils.ts +++ b/src/server/utils/cache-tag-generator.utils.ts @@ -19,4 +19,8 @@ export class Tags { static appBuilds(appId: string) { return `app-build-${appId}`; } + + static nodeInfos() { + return `node-infos`; + } } \ No newline at end of file