mirror of
https://github.com/biersoeckli/QuickStack.git
synced 2026-01-01 17:20:14 -06:00
added functionality to activate and deactivate nodes
This commit is contained in:
16
src/app/settings/cluster/actions.ts
Normal file
16
src/app/settings/cluster/actions.ts
Normal file
@@ -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.');
|
||||
});
|
||||
@@ -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 (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Nodes</CardTitle>
|
||||
@@ -18,38 +35,73 @@ export default async function NodeInfo({ nodeInfos }: { nodeInfos: NodeInfoModel
|
||||
|
||||
{nodeInfos.map((nodeInfo, index) => (
|
||||
<div key={index} className="space-y-4 rounded-lg border">
|
||||
<h3 className={(nodeInfo.status ? 'bg-green-200 text-green-700' : 'bg-red-200 text-red-700') + ' p-4 rounded-t-lg font-semibold text-xl text-center'}>
|
||||
<h3 className={(nodeInfo.status && nodeInfo.schedulable ? 'bg-green-200 text-green-700' : 'bg-red-200 text-red-700') + ' p-4 rounded-t-lg font-semibold text-xl text-center'}>
|
||||
Node {index + 1}
|
||||
</h3>
|
||||
<div className="space-y-2 px-4 pb-4">
|
||||
<div>
|
||||
<div className="space-y-2 px-4 pb-2">
|
||||
<div className="flex justify-center gap-4">
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild><div className={(nodeInfo.pidOk ? 'bg-green-200 text-green-700' : 'bg-red-200 text-red-700') + ' px-3 py-1.5 rounded cursor-pointer'}>CPU</div></TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p className="max-w-[350px]">{nodeInfo.pidStatusText}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<div className={(nodeInfo.memoryOk ? 'bg-green-200 text-green-700' : 'bg-red-200 text-red-700') + ' px-3 py-1.5 rounded cursor-pointer'}>RAM</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p className="max-w-[350px]">{nodeInfo.memoryStatusText}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<div className={(nodeInfo.diskOk ? 'bg-green-200 text-green-700' : 'bg-red-200 text-red-700') + ' px-3 py-1.5 rounded cursor-pointer'}>Disk</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p className="max-w-[350px]">{nodeInfo.diskStatusText}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
|
||||
|
||||
</div>
|
||||
<div className="pt-2">
|
||||
<span className="font-semibold">Name:</span> <Code>{nodeInfo.name}</Code>
|
||||
</div>
|
||||
<div>
|
||||
<span className="font-semibold">Schedulable:</span>
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<span className={nodeInfo.schedulable ? 'text-green-500 font-semibold' : 'text-red-500 font-semibold'}> {nodeInfo.schedulable ? 'Yes' : 'No'}</span>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p className="max-w-[350px]">{nodeInfo.schedulable ? 'Node is ready to run containers.' : 'Node ist deactivated. All containers will be scheduled on other nodes.'}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</div>
|
||||
<div>
|
||||
<span className="font-semibold">IP:</span> <Code>{nodeInfo.ip}</Code>
|
||||
</div>
|
||||
<div>
|
||||
<span className="font-semibold">CPU Cores:</span> {nodeInfo.cpuCapacity}
|
||||
</div>
|
||||
<div>
|
||||
<span className="font-semibold">Memory:</span> {nodeInfo.ramCapacity}
|
||||
</div>
|
||||
<div>
|
||||
<span className="font-semibold">OS:</span> {nodeInfo.os} | {nodeInfo.architecture}
|
||||
</div>
|
||||
<div>
|
||||
</div>
|
||||
<div>
|
||||
</div>
|
||||
<div>
|
||||
</div>
|
||||
<div className="text-xs text-slate-500">
|
||||
<div className="text-xs text-slate-500 pt-2">
|
||||
<span className="font-semibold">Spec:</span> {nodeInfo.cpuCapacity} CPU Cores, {nodeInfo.ramCapacity} Memory<br />
|
||||
<span className="font-semibold">OS:</span> {nodeInfo.os} | {nodeInfo.architecture}<br />
|
||||
<span className="font-semibold">Kernel Version:</span> {nodeInfo.kernelVersion}<br />
|
||||
<span className="font-semibold">Container Runtime Version:</span> {nodeInfo.containerRuntimeVersion}<br />
|
||||
<span className="font-semibold">Kube Proxy Version:</span> {nodeInfo.kubeProxyVersion}<br />
|
||||
<span className="font-semibold">Kubelet Version:</span> {nodeInfo.kubeletVersion}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex px-4 pb-4 gap-4">
|
||||
<Button onClick={() => setNodeStatusClick(nodeInfo.name, !nodeInfo.schedulable)} variant="outline">{nodeInfo.schedulable ? 'Deactivate' : 'Activate'} Node</Button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -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<typeof nodeInfoZodModel>;
|
||||
@@ -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<NodeInfoModel[]> {
|
||||
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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -19,4 +19,8 @@ export class Tags {
|
||||
static appBuilds(appId: string) {
|
||||
return `app-build-${appId}`;
|
||||
}
|
||||
|
||||
static nodeInfos() {
|
||||
return `node-infos`;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user