From da7a694703bd6280cb44e31c9a16740866079202 Mon Sep 17 00:00:00 2001 From: MohamedElmdary Date: Mon, 3 Nov 2025 13:57:51 +0200 Subject: [PATCH 1/4] Add info alert to tell user how to have a high ava cluster --- frontend/kubecloud/src/components/deploy/Step1DefineVMs.vue | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/frontend/kubecloud/src/components/deploy/Step1DefineVMs.vue b/frontend/kubecloud/src/components/deploy/Step1DefineVMs.vue index 3058d7bd..5a156d74 100644 --- a/frontend/kubecloud/src/components/deploy/Step1DefineVMs.vue +++ b/frontend/kubecloud/src/components/deploy/Step1DefineVMs.vue @@ -61,6 +61,11 @@ Add Master + + + To achieve high availability in your cluster, consider having at least three master nodes. + +

No master nodes configured

From ee770d6a0c34ee8ccba187aff76b534d6d625d6f Mon Sep 17 00:00:00 2001 From: MohamedElmdary Date: Thu, 6 Nov 2025 00:11:37 +0200 Subject: [PATCH 2/4] Add repair without error handling as it has issue related to size of reserved node --- backend/notification-config-example.json | 2 +- frontend/kubecloud/public/env.js | 6 + .../dashboard/ManageClusterView.vue | 117 ++++++++++++++++++ frontend/kubecloud/src/utils/userService.ts | 14 +++ 4 files changed, 138 insertions(+), 1 deletion(-) create mode 100644 frontend/kubecloud/public/env.js diff --git a/backend/notification-config-example.json b/backend/notification-config-example.json index fcb69d9a..8040bb3c 100644 --- a/backend/notification-config-example.json +++ b/backend/notification-config-example.json @@ -1,5 +1,5 @@ { - "email_templates_dir_path": "../internal/templates/notifications", + "email_templates_dir_path": "./internal/templates/notifications", "template_types": { "deployment": { "default": { diff --git a/frontend/kubecloud/public/env.js b/frontend/kubecloud/public/env.js new file mode 100644 index 00000000..4ed8dac0 --- /dev/null +++ b/frontend/kubecloud/public/env.js @@ -0,0 +1,6 @@ +window.__ENV__ = { + VITE_API_BASE_URL: 'http://localhost:8080/api', + VITE_NETWORK: 'dev', + VITE_STRIPE_PUBLISHABLE_KEY: + 'pk_test_51Rdp72C4WqB88qsawZgMYCdQo9t6AMBJktvBAwvca8qYHwDobDihxmesYs3oisucfGmk7n0FmLBPdizSTCjlwPcu005CEA2vQ8', +} diff --git a/frontend/kubecloud/src/components/dashboard/ManageClusterView.vue b/frontend/kubecloud/src/components/dashboard/ManageClusterView.vue index f356e986..62a19683 100644 --- a/frontend/kubecloud/src/components/dashboard/ManageClusterView.vue +++ b/frontend/kubecloud/src/components/dashboard/ManageClusterView.vue @@ -105,6 +105,20 @@ {{ node.contract_id || '-' }} + + + + Repair Node + + +
+ + + + Repair {{ nodeToRepair }} node + + + +

Pick a node in order to replace the old one

+ +
+ + + + + Cancel + + + Repair + + +
+
+ @@ -185,13 +233,21 @@ import { useClusterStore } from '../../stores/clusters' import { useNotificationStore } from '../../stores/notifications' import { useKubeconfig } from '../../composables/useKubeconfig' import { api } from '../../utils/api' +import NodeSelect from '../ui/NodeSelect.vue'; +import { useNodes } from '../../composables/useNodes'; +import { getAvailableCPU, getAvailableRAM, getAvailableStorage, getTotalCPU } from '../../utils/nodeNormalizer'; import { formatDate } from '../../utils/dateUtils' import { userService } from '@/utils/userService' import { useUserStore } from '@/stores/user' +import useNodeStoragePool from '@/composables/useNodeStoragePool' const userStore = useUserStore() +const addFormNodeId = ref(null); +const { nodes, loading: nodesLoading, fetchNodes } = useNodes() +onMounted(fetchNodes) + const haveEnoughBalance = computed(() => { return userStore.netBalance >= 5 }) @@ -210,6 +266,63 @@ const loading = ref(true) const notFound = ref(false) const deleteConfirmDialog = ref(false); const nodeToDelete = ref(''); +const nodeToRepair = ref(''); + +const repairing = ref(false) +async function repairNode(oldNodeName: string, newNode: number) { + const name = cluster.value?.cluster.name + const node = filteredNodes.value.find(n => n.original_name === oldNodeName) + if (!name || !node) { + console.warn('cluster or it\'s name not found', cluster.value) + return + } + + repairing.value = true + + const { data } = await userService.removeNodeFromDeployment(name, oldNodeName) + await new Promise(res => setTimeout(res, 5000)) + if (await userService.waitTaskTocomplete((data as any).task_id)) { + const {data: d } = await userService.addNodeToDeployment(name, { + name: name, + nodes: [ + { + name: oldNodeName, + type: node.type, + node_id: newNode, + cpu: node.cpu, + memory: node.memory, + root_size: node.root_size, + disk_size: node.disk_size, + env_vars: node.env_vars, + } + ] + }) + await userService.waitTaskTocomplete((d as any).task_id) + } + repairing.value = false +} + +const { validateNodeStoragePool, createStoragePoolError, failedToCheckStoragePoolError } = useNodeStoragePool() +const nodeValidationError = ref('') +const validatingNode = ref(false) + +async function validateNode(nodeId: number | null) { +try { + nodeValidationError.value = '' + validatingNode.value = true + if (!nodeId || !nodes.value.find((node) => node.nodeId === nodeId)) return + const isValid = await validateNodeStoragePool(/* addFormStorage.value */ 25, nodeId) + if (!isValid) { + nodeValidationError.value = createStoragePoolError(nodeId) + return + } +} catch (error) { + console.error(error) + nodeValidationError.value = failedToCheckStoragePoolError().message +} finally { + validatingNode.value = false +} +} const projectName = computed(() => route.params.id?.toString() || '') const cluster = computed(() => @@ -222,6 +335,10 @@ const filteredNodes = computed(() => { } return [] }) +const filteredNodesMap = computed(() => filteredNodes.value.reduce((r, n) => { + r[n.original_name] = n.node_id + return r +}, {} as {[key: string]: number})) const totalCPU = computed(() => { return filteredNodes.value.length diff --git a/frontend/kubecloud/src/utils/userService.ts b/frontend/kubecloud/src/utils/userService.ts index c8dc4229..e5f62b7f 100644 --- a/frontend/kubecloud/src/utils/userService.ts +++ b/frontend/kubecloud/src/utils/userService.ts @@ -311,6 +311,20 @@ export class UserService { return response.data.data } + async waitTaskTocomplete(id: string): Promise { + const res = await api.get>(`/v1/workflow/${id}`) + if (res.data.data === "failed") { + return false + } + + if (res.data.data === "completed") { + return true + } + + await new Promise(res => setTimeout(res, 5000)) + return this.waitTaskTocomplete(id) + } + private async trackNodeStatus(nodeId: number, targetStatus: "rented" | "rentable", maxAttempts: number = 20, interval: number = 5000) { await new Promise((resolve, reject) => { let attempts = 0 From 4b998706bc1206eef05f938004c3039ee886eda8 Mon Sep 17 00:00:00 2001 From: MohamedElmdary Date: Thu, 6 Nov 2025 12:03:07 +0200 Subject: [PATCH 3/4] add delay before wait task --- .../kubecloud/src/components/dashboard/ManageClusterView.vue | 1 + 1 file changed, 1 insertion(+) diff --git a/frontend/kubecloud/src/components/dashboard/ManageClusterView.vue b/frontend/kubecloud/src/components/dashboard/ManageClusterView.vue index 62a19683..5c58869f 100644 --- a/frontend/kubecloud/src/components/dashboard/ManageClusterView.vue +++ b/frontend/kubecloud/src/components/dashboard/ManageClusterView.vue @@ -297,6 +297,7 @@ async function repairNode(oldNodeName: string, newNode: number) { } ] }) + await new Promise(res => setTimeout(res, 5000)) await userService.waitTaskTocomplete((d as any).task_id) } repairing.value = false From c399438e9cf14bb85cbb13fd9df239ddadce3f83 Mon Sep 17 00:00:00 2001 From: MohamedElmdary Date: Thu, 6 Nov 2025 12:50:16 +0200 Subject: [PATCH 4/4] clean up --- backend/notification-config-example.json | 2 +- frontend/kubecloud/public/env.js | 6 ------ 2 files changed, 1 insertion(+), 7 deletions(-) delete mode 100644 frontend/kubecloud/public/env.js diff --git a/backend/notification-config-example.json b/backend/notification-config-example.json index 8040bb3c..fcb69d9a 100644 --- a/backend/notification-config-example.json +++ b/backend/notification-config-example.json @@ -1,5 +1,5 @@ { - "email_templates_dir_path": "./internal/templates/notifications", + "email_templates_dir_path": "../internal/templates/notifications", "template_types": { "deployment": { "default": { diff --git a/frontend/kubecloud/public/env.js b/frontend/kubecloud/public/env.js deleted file mode 100644 index 4ed8dac0..00000000 --- a/frontend/kubecloud/public/env.js +++ /dev/null @@ -1,6 +0,0 @@ -window.__ENV__ = { - VITE_API_BASE_URL: 'http://localhost:8080/api', - VITE_NETWORK: 'dev', - VITE_STRIPE_PUBLISHABLE_KEY: - 'pk_test_51Rdp72C4WqB88qsawZgMYCdQo9t6AMBJktvBAwvca8qYHwDobDihxmesYs3oisucfGmk7n0FmLBPdizSTCjlwPcu005CEA2vQ8', -}