Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
118 changes: 118 additions & 0 deletions frontend/kubecloud/src/components/dashboard/ManageClusterView.vue
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,20 @@
</td>
<td>{{ node.contract_id || '-' }}</td>
<td>
<v-btn
icon
size="small"
color="primary"
variant="text"
@click="nodeToRepair = node.original_name"
class="repair-node-btn"
>
<v-icon icon="mdi-hammer-screwdriver" size="small" />
<v-tooltip activator="parent" location="top">
Repair Node
</v-tooltip>
</v-btn>

<v-btn
icon
size="small"
Expand All @@ -130,6 +144,40 @@
</v-container>
</div>

<v-dialog :model-value="nodeToRepair !== ''" @update:model-value="nodeToRepair = ''" max-width="600" :persistent="repairing">
<v-card>
<v-card-title class="text-h6">
Repair {{ nodeToRepair }} node
</v-card-title>

<v-card-text>
<p>Pick a node in order to replace the old one</p>
<NodeSelect
:loading="nodesLoading || validatingNode"
v-model="addFormNodeId"
@update:modelValue="val => 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"
/>
</v-card-text>

<v-card-actions>
<v-spacer></v-spacer>
<v-btn color="grey" variant="text" @click="nodeToRepair = ''" :disabled="repairing">
Cancel
</v-btn>
<v-btn color="primary" variant="text" :disabled="!addFormNodeId || !!nodeValidationError || nodesLoading || validatingNode" @click="repairNode(nodeToRepair, addFormNodeId!)" :loading="repairing">
Repair
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>


<!-- Delete Confirmation Dialog -->
<v-dialog v-model="deleteConfirmDialog" max-width="400">
Expand Down Expand Up @@ -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<number|null>(null);
const { nodes, loading: nodesLoading, fetchNodes } = useNodes()
onMounted(fetchNodes)

const haveEnoughBalance = computed(() => {
return userStore.netBalance >= 5
})
Expand All @@ -210,6 +266,64 @@ const loading = ref(true)
const notFound = ref(false)
const deleteConfirmDialog = ref(false);
const nodeToDelete = ref<string>('');
const nodeToRepair = ref<string>('');

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(() =>
Expand All @@ -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
Expand Down
5 changes: 5 additions & 0 deletions frontend/kubecloud/src/components/deploy/Step1DefineVMs.vue
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,11 @@
</h4>
<v-btn color="primary" prepend-icon="mdi-plus" size="small" variant="outlined" @click="addMaster">Add Master</v-btn>
</div>

<v-alert variant="tonal" type="info" class="mb-4">
To achieve high availability in your cluster, consider having at least three master nodes.
</v-alert>

<DeployVMCard v-for="(master, masterIdx) in masters" :key="masterIdx" :vm="master" type="master" :availableSshKeys="availableSshKeys" @edit="() => openEditNodeModal('master', masterIdx)" @delete="() => removeMaster(masterIdx)" />
<div v-if="!masters.length" class="empty-state">
<p>No master nodes configured</p>
Expand Down
14 changes: 14 additions & 0 deletions frontend/kubecloud/src/utils/userService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -311,6 +311,20 @@ export class UserService {
return response.data.data
}

async waitTaskTocomplete(id: string): Promise<boolean> {
const res = await api.get<ApiResponse<"completed" | "failed">>(`/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
Expand Down
Loading