diff --git a/frontend/kubecloud/src/components/dashboard/ManageClusterView.vue b/frontend/kubecloud/src/components/dashboard/ManageClusterView.vue
index f356e986..5c58869f 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
+ validateNode(val)"
+ label="Select Node"
+ :items="nodes.filter(n => n.nodeId !== filteredNodesMap[nodeToRepair])"
+ :get-node-resources="node => ({ cpu: getTotalCPU(node), ram: getAvailableRAM(node), storage: getAvailableStorage(node) })"
+ :cpu-label="'CPU'"
+ :gpu-icon="'mdi-nvidia'"
+ :error="!!nodeValidationError"
+ :error-messages="nodeValidationError"
+ />
+
+
+
+
+
+ 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,64 @@ 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 new Promise(res => setTimeout(res, 5000))
+ 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 +336,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/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.
+
+
openEditNodeModal('master', masterIdx)" @delete="() => removeMaster(masterIdx)" />
No master nodes configured
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
|