diff --git a/src/books/bare-admin-cluster-creation/digital-ocean.ts b/src/books/bare-admin-cluster-creation/digital-ocean.ts index 4a1c4e3..d3e5792 100644 --- a/src/books/bare-admin-cluster-creation/digital-ocean.ts +++ b/src/books/bare-admin-cluster-creation/digital-ocean.ts @@ -48,7 +48,7 @@ class DigitalOceanBareAdminClusterCreator implements BareAdminClusterCloudCreato async () => { await $`pls setup`.cwd(tofuDir); await $`pls ${{ raw: L0 }}:init`.cwd(tofuDir); - await $`pls ${{ raw: L0 }}:apply`.cwd(tofuDir); + await $`pls ${{ raw: L0 }}:apply -- -auto-approve`.cwd(tofuDir); }, ]); @@ -72,7 +72,7 @@ class DigitalOceanBareAdminClusterCreator implements BareAdminClusterCloudCreato 'Build L1 Generic Infrastructure', async () => { await $`pls ${{ raw: L1G }}:init`.cwd(tofuDir); - await $`pls ${{ raw: L1G }}:apply`.cwd(tofuDir); + await $`pls ${{ raw: L1G }}:apply -- -auto-approve`.cwd(tofuDir); }, ]); @@ -81,7 +81,7 @@ class DigitalOceanBareAdminClusterCreator implements BareAdminClusterCloudCreato 'Build L1 Infrastructure', async () => { await $`pls ${{ raw: L1 }}:init`.cwd(tofuDir); - await $`pls ${{ raw: L1 }}:apply`.cwd(tofuDir); + await $`pls ${{ raw: L1 }}:apply -- -auto-approve`.cwd(tofuDir); }, ]); diff --git a/src/books/graceful-admin-cluster-destruction/generic.ts b/src/books/graceful-admin-cluster-destruction/generic.ts index 40bea99..f571159 100644 --- a/src/books/graceful-admin-cluster-destruction/generic.ts +++ b/src/books/graceful-admin-cluster-destruction/generic.ts @@ -66,7 +66,7 @@ class GenericGracefulAdminClusterDestructor { 'Destroy Generic Infrastructure', async () => { await $`pls ${{ raw: L1G }}:init`.cwd(tofuDir); - await $`pls ${{ raw: L1G }}:destroy`.cwd(tofuDir); + await $`pls ${{ raw: L1G }}:destroy -- -auto-approve`.cwd(tofuDir); }, ]); @@ -77,7 +77,7 @@ class GenericGracefulAdminClusterDestructor { async () => { await $`pls ${{ raw: L1 }}:init`.cwd(tofuDir); await $`pls ${{ raw: L1 }}:state:rm -- 'kubernetes_namespace.sulfoxide'`.cwd(tofuDir).nothrow(); - await $`pls ${{ raw: L1 }}:destroy`.cwd(tofuDir); + await $`pls ${{ raw: L1 }}:destroy -- -auto-approve`.cwd(tofuDir); }, ]); @@ -86,7 +86,7 @@ class GenericGracefulAdminClusterDestructor { await this.task.Run([ 'Destroy L0 Infrastructure', async () => { - await $`pls ${{ raw: L0 }}:destroy`.cwd(tofuDir); + await $`pls ${{ raw: L0 }}:destroy -- -auto-approve`.cwd(tofuDir); }, ]); diff --git a/src/books/graceful-physical-cluster-destruction/aws.ts b/src/books/graceful-physical-cluster-destruction/aws.ts new file mode 100644 index 0000000..7c6ac99 --- /dev/null +++ b/src/books/graceful-physical-cluster-destruction/aws.ts @@ -0,0 +1,289 @@ +import type { + LandscapeCluster, + ServiceTreeLandscapePrincipal, + ServiceTreeService, +} from '../../lib/service-tree-def.ts'; +import path from 'node:path'; +import { $ } from 'bun'; +import { KubectlUtil, type ResourceSearch } from '../../lib/utility/kubectl-util.ts'; +import type { TaskRunner } from '../../tasks/tasks.ts'; +import type { YamlManipulator } from '../../lib/utility/yaml-manipulator.ts'; +import type { UtilPrompter } from '../../lib/prompts/util-prompter.ts'; +import type { GracefulClusterCloudDestructor } from './cloud.ts'; + +class AwsGracefulPhysicalClusterDestructor implements GracefulClusterCloudDestructor { + slug: string; + + constructor( + private task: TaskRunner, + private y: YamlManipulator, + private k: KubectlUtil, + private up: UtilPrompter, + private sulfoxideTofu: ServiceTreeService, + private sulfoxideHelium: ServiceTreeService, + private sulfoxideGold: ServiceTreeService, + private virtualLandscapes: ServiceTreeLandscapePrincipal[], + slug: string, + ) { + this.slug = slug; + } + + async Run( + [phyLandscape, phyCluster]: LandscapeCluster, + [adminLandscape, adminCluster]: LandscapeCluster, + ): Promise { + const phy = { landscape: phyLandscape, cluster: phyCluster }; + const admin = { landscape: adminLandscape, cluster: adminCluster }; + + // common variables + const tofu = this.sulfoxideTofu; + const He = this.sulfoxideHelium; + const Au = this.sulfoxideGold; + + const pCtx = `${phy.landscape.slug}-${phy.cluster.principal.slug}`; + const aCtx = `${admin.landscape.slug}-${admin.cluster.principal.slug}`; + const aNS = `${He.platform.slug}-${He.principal.slug}`; + + const tofuDir = `./platforms/${tofu.platform.slug}/${tofu.principal.slug}`; + const He_Dir = `./platforms/${He.platform.slug}/${He.principal.slug}`; + + const yamlPath = path.join(He_Dir, 'chart', `values.${admin.landscape.slug}.${admin.cluster.set.slug}.yaml`); + + // Update ArgoCD configurations + await this.task.Run([ + 'Update Helium Configuration', + async () => { + console.log(`🗑️ Removing ArgoCD configurations. Path: ${yamlPath}`); + await this.y.Mutate(yamlPath, [ + [['connector', 'clusters', phy.landscape.slug, phy.cluster.principal.slug, 'enable'], false], + [['connector', 'clusters', phy.landscape.slug, phy.cluster.principal.slug, 'deployAppSet'], false], + [['connector', 'clusters', phy.landscape.slug, phy.cluster.principal.slug, 'aoa', 'enable'], false], + [['connector', 'clusters', phy.landscape.slug, phy.cluster.principal.slug, 'destination'], ''], + ]); + }, + ]); + + // Apply ArgoCD configurations + const adminPls = `${admin.landscape.slug}:${admin.cluster.set.slug}`; + await this.task.Run([ + 'Apply Helium Configuration', + async () => { + await $`pls ${{ raw: adminPls }}:install -- --kube-context ${aCtx} --namespace ${aNS}`.cwd(He_Dir); + }, + ]); + + // Must clean up load balancer properly + await this.task.Run([ + 'Delete Internal Ingress App', + async () => { + // delete root app + await this.k.Delete({ + kind: 'app', + context: aCtx, + namespace: aNS, + name: `${phy.landscape.slug}-${phy.cluster.principal.slug}-carbon`, + }); + await this.k.DeleteRange({ + kind: 'app', + context: aCtx, + namespace: aNS, + selector: [ + ['atomi.cloud/cluster', phy.cluster.principal.slug], + ['atomi.cloud/landscape', phy.landscape.slug], + ['atomi.cloud/element', Au.principal.slug], + ], + }); + }, + ]); + + await this.task.Run([ + 'Delete Internal Ingress Service', + async () => { + const name = `${phy.landscape.slug}-${Au.principal.slug}-ingress-nginx-controller`; + await this.k.Delete({ + kind: 'service', + context: pCtx, + namespace: Au.platform.slug, + name, + }); + + await this.k.Wait(0, 5, { + kind: 'service', + context: pCtx, + namespace: Au.platform.slug, + fieldSelector: [['metadata.name', name]], + }); + }, + ]); + + // delete applications from ArgoCD + const appsToRemove: ResourceSearch = { + kind: 'app', + context: aCtx, + namespace: aNS, + selector: [['atomi.cloud/cluster', phy.cluster.principal.slug]], + }; + + const deleteApps = async () => { + console.log(`🗑️ Delete Root Application: ${phy.landscape.slug}-${phy.cluster.principal.slug}-carbon`); + await this.k.Delete({ + kind: 'app', + context: aCtx, + namespace: aNS, + name: `${phy.landscape.slug}-${phy.cluster.principal.slug}-carbon`, + }); + console.log('✅ Root Application deleted'); + + console.log('🗑️ Deleting applications...'); + await this.k.DeleteRange(appsToRemove); + console.log('✅ Applications deleted'); + }; + + await this.task.Run(['Delete Applications', deleteApps]); + + await this.task.Run([ + 'Wait for Applications to be deleted', + async () => { + return await this.k.Wait(0, 3, appsToRemove, { + count: 3, + action: async () => { + const deleteApp = await this.up.YesNo('Do you want to manually delete the applications?'); + if (deleteApp) await deleteApps(); + return false; + }, + }); + }, + ]); + + // Delete all validating webhooks + await this.task.Run([ + 'Delete Validating Webhooks', + async () => { + await $`kubectl --context ${pCtx} delete validatingwebhookconfigurations --all`.nothrow(); + }, + ]); + + // Delete all namespaces + await this.task.Run([ + 'Delete Namespaces', + async () => { + for (const namespace of ['pichu', 'pikachu', 'raichu', 'sulfoxide']) { + console.log(` 🗑️ Removing namespace: ${namespace}`); + await this.k.DeleteNamespace({ + context: pCtx, + namespace, + }); + console.log(` ✅ Namespace removed: ${namespace}`); + } + }, + ]); + + // Wait for Nodes to be decommissioned + await this.task.Exec([ + 'Wait for Nodes to be decommissioned', + async () => { + await this.k.Wait(0, 5, { + kind: 'node', + context: pCtx, + selector: [['karpenter.sh/nodepool', 'bottlerocket']], + }); + }, + ]); + + // setup tofu repository correctly + await this.task.Run([ + 'Setup Tofu', + async () => { + await $`pls setup`.cwd(tofuDir); + }, + ]); + + // destroy generic infrastructure + const L1G = `${phy.landscape.slug}:l1:${phy.cluster.set.slug}`; + await this.task.Run([ + 'Destroy Generic Infrastructure', + async () => { + await $`pls ${{ raw: L1G }}:destroy -- -auto-approve`.cwd(tofuDir); + }, + ]); + + // destroy L1 infrastructure + const L1 = `${phy.landscape.slug}:l1:${phy.cluster.principal.slug}`; + await this.task.Run([ + 'Destroy L1 Infrastructure', + async () => { + await $`pls ${{ raw: L1 }}:state:rm -- 'kubernetes_namespace.sulfoxide'`.cwd(tofuDir).nothrow(); + await $`pls ${{ raw: L1 }}:destroy -- -auto-approve`.cwd(tofuDir); + }, + ]); + + // destroy L0 infrastructure + const L0 = `${phy.landscape.slug}:l0:${phy.cluster.principal.slug}`; + await this.task.Run([ + 'Destroy L0 Infrastructure', + async () => { + await $`pls ${{ raw: L0 }}:state:rm -- 'module.cluster.module.proxy_secret.kubernetes_namespace.kubernetes-access'` + .cwd(tofuDir) + .nothrow(); + await $`pls ${{ raw: L0 }}:destroy -- -auto-approve`.cwd(tofuDir); + }, + ]); + + // update kubectl configurations + await this.task.Run([ + 'Retrieve Kubectl Configurations', + async () => { + await $`pls kubectl`; + }, + ]); + + await this.task.Run([ + 'Delete ExternalSecret in admin', + async () => { + for (const ns of this.virtualLandscapes.map(x => x.slug)) { + await this.k.Delete({ + kind: 'externalsecret', + context: aCtx, + namespace: aNS, + name: `phase-6-${ns}-${phy.cluster.principal.slug}-cluster-secret`, + }); + } + await this.k.Delete({ + kind: 'externalsecret', + context: aCtx, + namespace: aNS, + name: `${phy.landscape.slug}-${phy.cluster.principal.slug}-external-secret`, + }); + await this.k.Delete({ + kind: 'externalsecret', + context: aCtx, + namespace: aNS, + name: `${phy.landscape.slug}-${phy.cluster.principal.slug}-external-secret-bearer-token`, + }); + await this.k.Delete({ + kind: 'externalsecret', + context: aCtx, + namespace: aNS, + name: `${phy.landscape.slug}-${phy.cluster.principal.slug}-external-secret-ca-crt`, + }); + }, + ]); + + // delete pointers to old cluster in admin + await this.task.Run([ + 'Delete SecretStore in admin', + async () => { + for (const ns of this.virtualLandscapes.map(x => x.slug)) { + await this.k.Delete({ + kind: 'secretstore', + context: aCtx, + namespace: aNS, + name: `phase-6-${ns}-${phy.cluster.principal.slug}`, + }); + } + }, + ]); + } +} + +export { AwsGracefulPhysicalClusterDestructor }; diff --git a/src/books/graceful-physical-cluster-destruction/cloud.ts b/src/books/graceful-physical-cluster-destruction/cloud.ts new file mode 100644 index 0000000..b64772c --- /dev/null +++ b/src/books/graceful-physical-cluster-destruction/cloud.ts @@ -0,0 +1,9 @@ +import type { LandscapeCluster } from '../../lib/service-tree-def.ts'; + +interface GracefulClusterCloudDestructor { + slug: string; + + Run(phy: LandscapeCluster, admin: LandscapeCluster): Promise; +} + +export type { GracefulClusterCloudDestructor }; diff --git a/src/books/graceful-physical-cluster-destruction/generic.ts b/src/books/graceful-physical-cluster-destruction/digital-ocean.ts similarity index 84% rename from src/books/graceful-physical-cluster-destruction/generic.ts rename to src/books/graceful-physical-cluster-destruction/digital-ocean.ts index e9bac88..eda8422 100644 --- a/src/books/graceful-physical-cluster-destruction/generic.ts +++ b/src/books/graceful-physical-cluster-destruction/digital-ocean.ts @@ -9,8 +9,11 @@ import { KubectlUtil, type ResourceSearch } from '../../lib/utility/kubectl-util import type { TaskRunner } from '../../tasks/tasks.ts'; import type { YamlManipulator } from '../../lib/utility/yaml-manipulator.ts'; import type { UtilPrompter } from '../../lib/prompts/util-prompter.ts'; +import type { GracefulClusterCloudDestructor } from './cloud.ts'; + +class DigitalOceanGracefulPhysicalClusterDestructor implements GracefulClusterCloudDestructor { + slug: string; -class GenericGracefulPhysicalClusterDestructor { constructor( private task: TaskRunner, private y: YamlManipulator, @@ -19,7 +22,10 @@ class GenericGracefulPhysicalClusterDestructor { private sulfoxideTofu: ServiceTreeService, private sulfoxideHelium: ServiceTreeService, private virtualLandscapes: ServiceTreeLandscapePrincipal[], - ) {} + slug: string, + ) { + this.slug = slug; + } async Run( [phyLandscape, phyCluster]: LandscapeCluster, @@ -70,27 +76,32 @@ class GenericGracefulPhysicalClusterDestructor { namespace: adminNamespaceSlug, selector: [['atomi.cloud/cluster', phy.cluster.principal.slug]], }; + + const deleteApps = async () => { + console.log(`🗑️ Delete Root Application: ${phy.landscape.slug}-${phy.cluster.principal.slug}-carbon`); + await this.k.Delete({ + kind: 'app', + context: adminContextSlug, + namespace: adminNamespaceSlug, + name: `${phy.landscape.slug}-${phy.cluster.principal.slug}-carbon`, + }); + console.log('✅ Root Application deleted'); + + console.log('🗑️ Deleting applications...'); + await this.k.DeleteRange(appsToRemove); + console.log('✅ Applications deleted'); + }; + + await this.task.Run(['Delete Applications', deleteApps]); + await this.task.Run([ - 'Delete Applications', + 'Wait for Applications to be deleted', async () => { return await this.k.Wait(0, 3, appsToRemove, { count: 3, action: async () => { const deleteApp = await this.up.YesNo('Do you want to manually delete the applications?'); - if (deleteApp) { - console.log(`🗑️ Delete Root Application: ${phy.landscape.slug}-${phy.cluster.principal.slug}-carbon`); - await this.k.Delete({ - kind: 'app', - context: adminContextSlug, - namespace: adminNamespaceSlug, - name: `${phy.landscape.slug}-${phy.cluster.principal.slug}-carbon`, - }); - console.log('✅ Root Application deleted'); - - console.log('🗑️ Deleting applications...'); - await this.k.DeleteRange(appsToRemove); - console.log('✅ Applications deleted'); - } + if (deleteApp) await deleteApps(); return false; }, }); @@ -133,7 +144,7 @@ class GenericGracefulPhysicalClusterDestructor { await this.task.Run([ 'Destroy Generic Infrastructure', async () => { - await $`pls ${{ raw: L1G }}:destroy`.cwd(tofuDir); + await $`pls ${{ raw: L1G }}:destroy -- -auto-approve`.cwd(tofuDir); }, ]); @@ -143,7 +154,7 @@ class GenericGracefulPhysicalClusterDestructor { 'Destroy L1 Infrastructure', async () => { await $`pls ${{ raw: L1 }}:state:rm -- 'kubernetes_namespace.sulfoxide'`.cwd(tofuDir).nothrow(); - await $`pls ${{ raw: L1 }}:destroy`.cwd(tofuDir); + await $`pls ${{ raw: L1 }}:destroy -- -auto-approve`.cwd(tofuDir); }, ]); @@ -155,7 +166,7 @@ class GenericGracefulPhysicalClusterDestructor { await $`pls ${{ raw: L0 }}:state:rm -- 'module.cluster.module.proxy_secret.kubernetes_namespace.kubernetes-access'` .cwd(tofuDir) .nothrow(); - await $`pls ${{ raw: L0 }}:destroy`.cwd(tofuDir); + await $`pls ${{ raw: L0 }}:destroy -- -auto-approve`.cwd(tofuDir); }, ]); @@ -216,4 +227,4 @@ class GenericGracefulPhysicalClusterDestructor { } } -export { GenericGracefulPhysicalClusterDestructor }; +export { DigitalOceanGracefulPhysicalClusterDestructor }; diff --git a/src/books/graceful-physical-cluster-destruction/index.ts b/src/books/graceful-physical-cluster-destruction/index.ts index 14df6d9..98a2752 100644 --- a/src/books/graceful-physical-cluster-destruction/index.ts +++ b/src/books/graceful-physical-cluster-destruction/index.ts @@ -1,7 +1,7 @@ import type { RunBook } from '../run-book.ts'; import type { ServiceTreePrompter } from '../../lib/prompts/landscape.ts'; import type { ServiceTreePrinter } from '../../lib/utility/service-tree-printer.ts'; -import type { GenericGracefulPhysicalClusterDestructor } from './generic.ts'; +import type { GracefulClusterCloudDestructor } from './cloud.ts'; class GracefulPhysicalClusterDestructor implements RunBook { name: string = 'Graceful Physical Cluster Destruction'; @@ -10,7 +10,7 @@ class GracefulPhysicalClusterDestructor implements RunBook { constructor( private stp: ServiceTreePrompter, private p: ServiceTreePrinter, - private destructor: GenericGracefulPhysicalClusterDestructor, + private clouds: GracefulClusterCloudDestructor[], ) {} async Run(): Promise { @@ -22,7 +22,10 @@ class GracefulPhysicalClusterDestructor implements RunBook { this.p.Print('Physical', [phyLandscape, phyCluster]); this.p.Print('Admin', [adminLandscape, adminCluster]); - await this.destructor.Run([phyLandscape, phyCluster], [adminLandscape, adminCluster]); + const destructor = this.clouds.find(x => x.slug === phyCluster.cloud.slug); + if (!destructor) return console.log('⚠️ Cloud not supported'); + + await destructor.Run([phyLandscape, phyCluster], [adminLandscape, adminCluster]); } } diff --git a/src/books/physical-cluster-creation/aws.ts b/src/books/physical-cluster-creation/aws.ts index 08c8f5a..bdaf42b 100644 --- a/src/books/physical-cluster-creation/aws.ts +++ b/src/books/physical-cluster-creation/aws.ts @@ -66,7 +66,7 @@ class AwsPhysicalClusterCreator implements PhysicalClusterCloudCreator { async () => { await $`pls setup`.cwd(tofuDir); await $`pls ${{ raw: L0 }}:init`.cwd(tofuDir); - await $`pls ${{ raw: L0 }}:apply`.cwd(tofuDir); + await $`pls ${{ raw: L0 }}:apply -- -auto-approve`.cwd(tofuDir); }, ]); @@ -89,7 +89,7 @@ class AwsPhysicalClusterCreator implements PhysicalClusterCloudCreator { 'Build L1 Generic Infrastructure', async () => { await $`pls ${{ raw: L1G }}:init`.cwd(tofuDir); - await $`pls ${{ raw: L1G }}:apply`.cwd(tofuDir); + await $`pls ${{ raw: L1G }}:apply -- -auto-approve`.cwd(tofuDir); }, ]); @@ -147,7 +147,7 @@ class AwsPhysicalClusterCreator implements PhysicalClusterCloudCreator { 'Build L1 Infrastructure', async () => { await $`pls ${{ raw: L1 }}:init`.cwd(tofuDir); - await $`pls ${{ raw: L1 }}:apply`.cwd(tofuDir); + await $`pls ${{ raw: L1 }}:apply -- -auto-approve`.cwd(tofuDir); }, ]); diff --git a/src/books/physical-cluster-creation/digital-ocean.ts b/src/books/physical-cluster-creation/digital-ocean.ts index d12fcf4..d50d2ec 100644 --- a/src/books/physical-cluster-creation/digital-ocean.ts +++ b/src/books/physical-cluster-creation/digital-ocean.ts @@ -50,7 +50,7 @@ class DigitalOceanPhysicalClusterCreator implements PhysicalClusterCloudCreator async () => { await $`pls setup`.cwd(tofuDir); await $`pls ${phyLandscape.slug}:l0:${phyCluster.principal.slug}:init`.cwd(tofuDir); - await $`pls ${phyLandscape.slug}:l0:${phyCluster.principal.slug}:apply`.cwd(tofuDir); + await $`pls ${phyLandscape.slug}:l0:${phyCluster.principal.slug}:apply -- -auto-approve`.cwd(tofuDir); }, ]); @@ -74,7 +74,7 @@ class DigitalOceanPhysicalClusterCreator implements PhysicalClusterCloudCreator 'Build L1 Generic Infrastructure', async () => { await $`pls ${phyLandscape.slug}:l1:${phyCluster.set.slug}:init`.cwd(tofuDir); - await $`pls ${phyLandscape.slug}:l1:${phyCluster.set.slug}:apply`.cwd(tofuDir); + await $`pls ${phyLandscape.slug}:l1:${phyCluster.set.slug}:apply -- -auto-approve`.cwd(tofuDir); }, ]); @@ -83,7 +83,7 @@ class DigitalOceanPhysicalClusterCreator implements PhysicalClusterCloudCreator 'Build L1 Infrastructure', async () => { await $`pls ${phyLandscape.slug}:l1:${phyCluster.principal.slug}:init`.cwd(tofuDir); - await $`pls ${phyLandscape.slug}:l1:${phyCluster.principal.slug}:apply`.cwd(tofuDir); + await $`pls ${phyLandscape.slug}:l1:${phyCluster.principal.slug}:apply -- -auto-approve`.cwd(tofuDir); }, ]); diff --git a/src/init/runbooks.ts b/src/init/runbooks.ts index a330897..152c834 100644 --- a/src/init/runbooks.ts +++ b/src/init/runbooks.ts @@ -14,10 +14,11 @@ import { DigitalOceanFullAdminClusterCreator } from '../books/full-admin-cluster import { FullAdminClusterCreator } from '../books/full-admin-cluster-creation'; import { GracefulAdminClusterDestructor } from '../books/graceful-admin-cluster-destruction'; import { GenericGracefulAdminClusterDestructor } from '../books/graceful-admin-cluster-destruction/generic.ts'; -import { GenericGracefulPhysicalClusterDestructor } from '../books/graceful-physical-cluster-destruction/generic.ts'; +import { DigitalOceanGracefulPhysicalClusterDestructor } from '../books/graceful-physical-cluster-destruction/digital-ocean.ts'; import { AdminClusterMigrator } from '../books/admin-cluster-migration'; import { AdminClusterTransitioner } from '../books/admin-cluster-migration/transition.ts'; import { AwsPhysicalClusterCreator } from '../books/physical-cluster-creation/aws.ts'; +import { AwsGracefulPhysicalClusterDestructor } from '../books/graceful-physical-cluster-destruction/aws.ts'; function initRunBooks(d: Dependencies, t: TaskGenerator): RunBook[] { const sulfoxide = SERVICE_TREE.sulfoxide; @@ -56,20 +57,34 @@ function initRunBooks(d: Dependencies, t: TaskGenerator): RunBook[] { ); // graceful physical cluster destruction - const genericPhyGracefulDestructor = new GenericGracefulPhysicalClusterDestructor( - d.taskRunner, - d.yamlManipulator, - d.kubectl, - d.utilPrompter, - sulfoxide.services.tofu, - sulfoxide.services.argocd, - LANDSCAPE_TREE.v, - ); + const phyGracefulDestructors = [ + new DigitalOceanGracefulPhysicalClusterDestructor( + d.taskRunner, + d.yamlManipulator, + d.kubectl, + d.utilPrompter, + sulfoxide.services.tofu, + sulfoxide.services.argocd, + LANDSCAPE_TREE.v, + CLOUDS.DigitalOcean.slug, + ), + new AwsGracefulPhysicalClusterDestructor( + d.taskRunner, + d.yamlManipulator, + d.kubectl, + d.utilPrompter, + sulfoxide.services.tofu, + sulfoxide.services.argocd, + sulfoxide.services.external_ingress, + LANDSCAPE_TREE.v, + CLOUDS.AWS.slug, + ), + ]; const phyGracefulDestructor = new GracefulPhysicalClusterDestructor( d.stp, d.serviceTreePrinter, - genericPhyGracefulDestructor, + phyGracefulDestructors, ); // bare admin cluster creation diff --git a/src/lib/utility/kubectl-util.ts b/src/lib/utility/kubectl-util.ts index ff3cac4..cc5ed4d 100644 --- a/src/lib/utility/kubectl-util.ts +++ b/src/lib/utility/kubectl-util.ts @@ -4,7 +4,7 @@ import type { UtilPrompter } from '../prompts/util-prompter.ts'; interface ResourceSearch { kind: string; context: string; - namespace: string; + namespace?: string; selector?: [string, string][]; fieldSelector?: [string, string][]; } @@ -12,7 +12,7 @@ interface ResourceSearch { interface Resource { kind: string; context: string; - namespace: string; + namespace?: string; name: string; } @@ -38,29 +38,31 @@ class KubectlUtil { } async Scale(r: Resource, replicas: number): Promise { - const cmds = $.escape( - `kubectl scale --context ${r.context} -n ${r.namespace} ${r.kind} ${r.name} --replicas=${replicas}`, - ); + const ns = r.namespace == null ? '' : `-n ${r.namespace}`; + const cmds = $.escape(`kubectl scale --context ${r.context} ${ns} ${r.kind} ${r.name} --replicas=${replicas}`); console.log(`🖥️ Scale Execute Command: ${cmds}`); - await $`kubectl scale --context ${r.context} -n ${r.namespace} ${r.kind} ${r.name} --replicas=${{ raw: replicas.toString(10) }}`; + await $`kubectl scale --context ${r.context} ${{ raw: ns }} ${r.kind} ${r.name} --replicas=${{ raw: replicas.toString(10) }}`; } async GetReplica(r: Resource): Promise { + const ns = r.namespace == null ? '' : `-n ${r.namespace}`; const { stdout } = - await $`kubectl get --context ${r.context} --namespace ${r.namespace} ${r.kind} ${r.name} -o jsonpath="{.status.replicas}"`.quiet(); + await $`kubectl get --context ${r.context} ${{ raw: ns }} ${r.kind} ${r.name} -o jsonpath="{.status.replicas}"`.quiet(); return parseInt(stdout.toString().trim(), 10); } async WaitForReplica(r: Resource): Promise { + const ns = r.namespace == null ? '' : `-n ${r.namespace}`; const replicas = await this.GetReplica(r); const cmds = $.escape( - `kubectl --context ${r.context} -n ${r.namespace} wait --for=jsonpath=.status.readyReplicas=${replicas} --timeout=600s ${r.kind} ${r.name}`, + `kubectl --context ${r.context} ${ns} wait --for=jsonpath=.status.readyReplicas=${replicas} --timeout=600s ${r.kind} ${r.name}`, ); console.log(`🖥️ WaitForReplicas Execute Command: ${cmds}`); - await $`kubectl --context ${r.context} -n ${r.namespace} wait --for=jsonpath=.status.readyReplicas=${{ raw: replicas.toString(10) }} --timeout=600s ${r.kind} ${r.name}`; + await $`kubectl --context ${r.context} ${{ raw: ns }} wait --for=jsonpath=.status.readyReplicas=${{ raw: replicas.toString(10) }} --timeout=600s ${r.kind} ${r.name}`; } async WaitForApplication(r: Resource): Promise { + const ns = r.namespace == null ? '' : `-n ${r.namespace}`; console.log(`🚧 Waiting for ${r.kind} ${r.name} to be healthy...`); await this.Wait(1, 5, { kind: r.kind, @@ -69,16 +71,16 @@ class KubectlUtil { fieldSelector: [['metadata.name', r.name]], }); const cmds = $.escape( - `kubectl --context ${r.context} -n ${r.namespace} wait --for=jsonpath=.status.health.status=Healthy --timeout=6000s ${r.kind} ${r.name}`, + `kubectl --context ${r.context} ${ns} wait --for=jsonpath=.status.health.status=Healthy --timeout=6000s ${r.kind} ${r.name}`, ); console.log(`🖥️ WaitForApplication Execute Command: ${cmds}`); - await $`kubectl --context ${r.context} -n ${r.namespace} wait --for=jsonpath=.status.health.status=Healthy --timeout=6000s ${r.kind} ${r.name}`; + await $`kubectl --context ${r.context} ${{ raw: ns }} wait --for=jsonpath=.status.health.status=Healthy --timeout=6000s ${r.kind} ${r.name}`; const cmd = $.escape( - `kubectl --context ${r.context} -n ${r.namespace} wait --for=jsonpath=.status.sync.status=Synced --timeout=6000s ${r.kind} ${r.name}`, + `kubectl --context ${r.context} ${ns} wait --for=jsonpath=.status.sync.status=Synced --timeout=6000s ${r.kind} ${r.name}`, ); console.log(`🖥️ WaitForApplication Execute Command: ${cmd}`); - await $`kubectl --context ${r.context} -n ${r.namespace} wait --for=jsonpath=.status.sync.status=Synced --timeout=6000s ${r.kind} ${r.name}`; + await $`kubectl --context ${r.context} ${{ raw: ns }} wait --for=jsonpath=.status.sync.status=Synced --timeout=6000s ${r.kind} ${r.name}`; } async WaitForApplications(target: number, search: ResourceSearch): Promise { @@ -96,13 +98,13 @@ class KubectlUtil { async GetRange(search: ResourceSearch): Promise { const flags = await this.generateFlags(search); - const cmds = $.escape( - `kubectl get ${search.kind} --context ${search.context} --namespace ${search.namespace} ${flags} -o json`, - ); + const ns = search.namespace == null ? '' : `-n ${search.namespace}`; + + const cmds = $.escape(`kubectl get ${search.kind} --context ${search.context} ${ns} ${flags} -o json`); console.log(`🖥️ GetRange Execute Command: ${cmds}`); const obj = - await $`kubectl get ${search.kind} --context ${search.context} --namespace ${search.namespace} ${{ raw: flags }} -o json`.json(); + await $`kubectl get ${search.kind} --context ${search.context} ${{ raw: ns }} ${{ raw: flags }} -o json`.json(); return obj.items.map((item: any) => ({ kind: item.kind, context: search.context, @@ -148,11 +150,11 @@ class KubectlUtil { async DeleteRange(search: ResourceSearch): Promise { const flags = await this.generateFlags(search); - const cmds = $.escape( - `kubectl delete ${search.kind} --context ${search.context} --namespace ${search.namespace} ${flags}`, - ); + const ns = search.namespace == null ? '' : `-n ${search.namespace}`; + + const cmds = $.escape(`kubectl delete ${search.kind} --context ${search.context} ${ns} ${flags}`); console.log(`🖥️ DeleteRange Execute Command: ${cmds}`); - await $`kubectl delete ${search.kind} --context ${search.context} --namespace ${search.namespace} ${{ raw: flags }} `; + await $`kubectl delete ${search.kind} --context ${search.context} ${{ raw: ns }} ${{ raw: flags }} `; } async Delete(r: Resource): Promise { diff --git a/src/tasks/tasks.ts b/src/tasks/tasks.ts index d385f8b..64aef75 100644 --- a/src/tasks/tasks.ts +++ b/src/tasks/tasks.ts @@ -49,9 +49,13 @@ class TaskRunner { console.log(pc.red(`❌ Task ${quoted} failed`)); const loop = await this.up.YesNo(`Do you want to retry?`); if (!loop) { - // user wants to abort - console.log(pc.red(`❌ Task ${quoted} aborted`)); - process.exit(1); + const exit = await this.up.YesNo('Do you want to exit? (no will skip to next step'); + if (exit) { + // user wants to abort + console.log(pc.red(`❌ Task ${quoted} aborted`)); + process.exit(1); + } + return; } } }