diff --git a/packages/grid_client/src/high_level/base.ts b/packages/grid_client/src/high_level/base.ts index 5cf0c24a25..7483842821 100644 --- a/packages/grid_client/src/high_level/base.ts +++ b/packages/grid_client/src/high_level/base.ts @@ -136,7 +136,9 @@ class HighLevelBase { } if (network instanceof Network) { const hasAccessPoint = network.hasAccessPoint(node_id); - if (hasAccessPoint && network.nodes.length !== 1) { + // Only skip network deletion if we have access point, multiple nodes, AND remaining workloads + // If remainingWorkloads is 0, we're deleting the entire deployment, so delete all network contracts + if (hasAccessPoint && network.nodes.length !== 1 && remainingWorkloads.length !== 0) { console.log( `network ${network.name} still has access point:${hasAccessPoint} and number of nodes ${network.nodes.length}`, ); @@ -191,6 +193,27 @@ class HighLevelBase { } } } + + // Final safety: if this was a full deployment delete, make sure no + // network-node contracts are left behind (e.g., due to access-point guards). + if (network instanceof Network && remainingWorkloads.length === 0) { + const nodesSnapshot = [...network.nodes]; + for (const n of nodesSnapshot) { + if (network.getNodeReservedIps(n.node_id).length !== 0) continue; + const contract_id = await network.deleteNode(n.node_id); + for (let d of network.deployments) { + d = await deploymentFactory.fromObj(d); + if (d.contract_id !== contract_id) continue; + if (d.workloads.length === 1) { + twinDeployments.push(new TwinDeployment(d, Operations.delete, 0, 0, "", network)); + } else { + d.workloads = d.workloads.filter(item => item.name !== network?.name); + twinDeployments.push(new TwinDeployment(d, Operations.update, 0, 0, "", network)); + } + } + } + } + return [twinDeployments, remainingWorkloads, deletedNodes, deletedIps, network]; } diff --git a/packages/playground/src/utils/delete_deployment.ts b/packages/playground/src/utils/delete_deployment.ts index 7694f8be4d..5e5cc35eb2 100644 --- a/packages/playground/src/utils/delete_deployment.ts +++ b/packages/playground/src/utils/delete_deployment.ts @@ -3,7 +3,7 @@ import type { GridClient } from "@threefold/grid_client"; import { ProjectName } from "@/types"; import { loadVM } from "./deploy_vm"; -import { getSubdomain, loadDeploymentGateways } from "./gateway"; +import { getDeploymentIps, getSubdomain, loadDeploymentGateways } from "./gateway"; import { updateGrid } from "./grid"; export interface DeleteDeploymentOptions { @@ -40,15 +40,13 @@ export async function deleteDeployment(grid: GridClient, options: DeleteDeployme /* Delete deployment */ if (options.k8s) { - const { gateways } = await loadDeploymentGateways(grid, { filter: () => true }); - for (const gateway of gateways) { - try { - await grid.gateway.delete_name({ name: gateway.name }); - } catch (error) { - console.error("Error while deleting k8s gateway.", error); - } + const result = await grid.k8s.delete({ name: options.name }); + try { + await deleteK8sGateways(grid, options.name, options.ip); + } catch (error) { + console.error("Error during gateway cleanup after K8s deletion:", error); } - return grid.k8s.delete({ name: options.name }); + return result; } // if Caprover deployment should handled by machines.delete if (options.deploymentName && !options.isCaprover) { @@ -123,19 +121,57 @@ function isVm(projectName: string) { return false; } -async function deleteVmGateways(grid: GridClient, ips?: string[]) { +async function deleteGatewaysByIps(grid: GridClient, ips: string[]) { + if (!ips.length) return; + const { gateways } = await loadDeploymentGateways(grid, { - filter: ips ? gw => gw.backends.some(bk => ips.some(ip => bk.includes(ip))) : undefined, + filter: gw => gw.backends.some(bk => ips.some(ip => bk.includes(ip))), }); - for (const gateway of gateways) { - try { - if (gateway.type.includes("name")) { - await grid.gateway.delete_name(gateway); - } else { - await grid.gateway.delete_fqdn(gateway); - } - } catch (error) { - console.log("Error while deleting vm gateway", error); + + if (gateways.length === 0) return; + + const deletionPromises = gateways.map(async gateway => { + if (gateway.type.includes("name")) { + return await grid.gateway.delete_name(gateway); + } else { + return await grid.gateway.delete_fqdn(gateway); + } + }); + + // Wait for all gateway deletions to complete + await Promise.allSettled(deletionPromises); +} + +async function deleteVmGateways(grid: GridClient, ips?: string[]) { + if (!ips || ips.length === 0) return; + await deleteGatewaysByIps(grid, ips); +} + +async function deleteK8sGateways(grid: GridClient, deploymentName: string, ips?: string[]) { + // Get deployment IPs - try from deployment first, fallback to provided IPs + let deploymentIps: string[] = []; + + try { + const k8sDeployment = await grid.k8s.getObj(deploymentName); + const fetchedIps = [ + ...(k8sDeployment.masters?.flatMap(getDeploymentIps) ?? []), + ...(k8sDeployment.workers?.flatMap(getDeploymentIps) ?? []), + ]; + + if (fetchedIps.length > 0) { + deploymentIps = fetchedIps; + } else if (ips && ips.length > 0) { + deploymentIps = ips; + } + } catch (error) { + console.error("Error while fetching K8s deployment for gateway deletion:", error); + if (ips && ips.length > 0) { + deploymentIps = ips; } } + + if (deploymentIps.length === 0) return; + + // Delete all gateways pointing to this K8s cluster + await deleteGatewaysByIps(grid, deploymentIps); } diff --git a/packages/playground/src/weblets/tf_deployment_list.vue b/packages/playground/src/weblets/tf_deployment_list.vue index a66c6e01bd..69c84e79c2 100644 --- a/packages/playground/src/weblets/tf_deployment_list.vue +++ b/packages/playground/src/weblets/tf_deployment_list.vue @@ -546,11 +546,19 @@ async function onDelete(k8s = false) { item[0].workloads[0].name as string, ); } else { + // For K8s, collect IPs from all masters and workers + let deploymentIps: string[] = []; + if (k8s && item.masters && item.workers) { + deploymentIps = [...item.masters.flatMap(getDeploymentIps), ...item.workers.flatMap(getDeploymentIps)]; + } else { + deploymentIps = getDeploymentIps(item); + } + await deleteDeployment(updateGrid(grid!, { projectName: item.projectName }), { deploymentName: item.deploymentName, name: k8s ? item.deploymentName : item.name, projectName: item.projectName, - ip: getDeploymentIps(item), + ip: deploymentIps, k8s, isCaprover: item.projectName?.toLowerCase().includes(ProjectName.Caprover.toLowerCase()), });