Skip to content

Commit 80b05dd

Browse files
Feat(dokku): destroy dokku cmd (#558)
1 parent 3d9459d commit 80b05dd

6 files changed

Lines changed: 114 additions & 20 deletions

File tree

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
### Added
11+
12+
- stack destroy command for supabase, dokku and starter-kit
13+
1014
## [3.24.1] - 2025-10-14
1115

1216
### Changed

internal/commands/stack/dokku/deploy.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ func (s *deployDokkuCommand) InitCommand() {
6363
fs.StringVar(&s.githubUser, "github-user", s.githubUser, "Used to allow Dokku to push your app images to your GitHub Container Registry")
6464
fs.StringVar(&s.certManagerEmail, "cert-manager-email", "ops@example.com", "Email for TLS cert registration (default: ops@example.com)")
6565
fs.StringVar(&s.globalDomain, "global-domain", s.globalDomain, "Example: example.com. If you do not have a domain name leave this empty and it will get the value of the ingress nginx load balancer automatically. Example: lb-0a39e6584…")
66-
fs.IntVar(&s.numNodes, "num-nodes", 3, "Number of nodes in the Dokku cluster (default: 3)")
66+
fs.IntVar(&s.numNodes, "num-nodes", 1, "Number of nodes in the Dokku cluster (default: 1)")
6767
fs.StringVar(&s.sshPath, "ssh-path", defaultSSH, "Path to your private SSH key (default: ~/.ssh/id_rsa). Needed to be able to ‘git push dokku@<host>:<app>’ when deploying apps with git push")
6868
fs.StringVar(&s.sshPubPath, "ssh-path-pub", defaultSSHPub, "Path to your public SSH key (default: ~/.ssh/id_rsa.pub)")
6969
fs.StringVar(&s.githubPackageURL, "github-package-url", "ghcr.io", "Container registry hostname (default: ghcr.io)")

internal/commands/stack/dokku/destroy.go

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,24 +20,25 @@ func DestroyDokkuCommand() commands.Command {
2020

2121
type destroyDokkuCommand struct {
2222
*commands.BaseCommand
23-
zone string
24-
name string
23+
zone string
24+
name string
25+
deleteStorage bool
2526
}
2627

2728
func (s *destroyDokkuCommand) InitCommand() {
2829
fs := &pflag.FlagSet{}
2930
fs.StringVar(&s.zone, "zone", s.zone, "Zone for the stack deployment")
3031
fs.StringVar(&s.name, "name", s.name, "Supabase stack name")
32+
fs.BoolVar(&s.deleteStorage, "delete-storage", false, "Delete associated UpCloud storage resources")
3133
s.AddFlags(fs)
3234

3335
commands.Must(s.Cobra().MarkFlagRequired("zone"))
3436
commands.Must(s.Cobra().MarkFlagRequired("name"))
3537
}
3638

3739
// ExecuteWithoutArguments implements commands.NoArgumentCommand
38-
// TODO: This is not deleting the LB that dokku creates. Need to find a way to identify it.
3940
func (c *destroyDokkuCommand) ExecuteWithoutArguments(exec commands.Executor) (output.Output, error) {
40-
err := stack.DestroyStack(exec, c.name, c.zone, false, false, stack.StackTypeDokku)
41+
err := stack.DestroyStack(exec, c.name, c.zone, c.deleteStorage, false, stack.StackTypeDokku)
4142
if err != nil {
4243
return nil, err
4344
}

internal/commands/stack/helm.go

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -210,10 +210,16 @@ func DeployHelmReleaseFromRepo(
210210
valsFiles []string,
211211
upgrade bool,
212212
) error {
213+
// Wait for the Kubernetes API server to be ready
214+
err := WaitForAPIServer(kubeClient)
215+
if err != nil {
216+
return fmt.Errorf("waiting for API server: %w", err)
217+
}
218+
213219
os.Setenv("HELM_NAMESPACE", releaseName)
214220

215221
// Ensure the target namespace exists
216-
err := CreateNamespace(kubeClient, releaseName)
222+
err = CreateNamespace(kubeClient, releaseName)
217223
if err != nil {
218224
return fmt.Errorf("ensuring namespace %q exists: %w", releaseName, err)
219225
}

internal/commands/stack/stackops.go

Lines changed: 97 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -248,10 +248,6 @@ func DestroyStack(exec commands.Executor, name, zone string, deleteStorage, dele
248248
}
249249

250250
var uuids []string
251-
// Collect PVC volume UUIDs for deletion if deleteStorage is true
252-
// This is done before uninstalling the helm release to ensure we get all PVCs
253-
// associated with the Supabase stack
254-
// If deleteStorage is false, we skip this step and leave the PVCs intact
255251
if deleteStorage {
256252
msg = fmt.Sprintln("Collecting PVC volume UUIDs for deletion")
257253
exec.PushProgressStarted(msg)
@@ -372,9 +368,6 @@ func DestroyStack(exec commands.Executor, name, zone string, deleteStorage, dele
372368
return err
373369
}
374370

375-
msg = fmt.Sprintf("Deleting network %s in zone %s", clusterName, zone)
376-
exec.PushProgressStarted(msg)
377-
378371
networkName := fmt.Sprintf("%s-%s-%s", SupabaseResourceRootNameNetwork, name, zone)
379372
networks, err := exec.All().GetNetworks(exec.Context())
380373
if err != nil {
@@ -393,19 +386,110 @@ func DestroyStack(exec commands.Executor, name, zone string, deleteStorage, dele
393386
break
394387
}
395388
}
396-
exec.PushProgressSuccess(msg)
397389
case StackTypeDokku:
390+
logDir := os.TempDir()
398391
clusterName := fmt.Sprintf("%s-%s-%s", DokkuResourceRootNameCluster, name, zone)
399-
networkName := fmt.Sprintf("%s-%s-%s", DokkuResourceRootNameNetwork, name, zone)
392+
msg := fmt.Sprintf("Searching cluster %s in zone %s", clusterName, zone)
393+
exec.PushProgressStarted(msg)
394+
clusters, err := exec.All().GetKubernetesClusters(exec.Context(), &request.GetKubernetesClustersRequest{})
395+
if err != nil {
396+
return fmt.Errorf("failed to get kubernetes clusters: %w", err)
397+
}
398+
399+
var cluster *upcloud.KubernetesCluster
400+
for _, cl := range clusters {
401+
if cl.Name == clusterName {
402+
cluster = &cl
403+
break
404+
}
405+
}
406+
407+
if cluster == nil {
408+
return fmt.Errorf("a cluster with the name '%s' was not found", clusterName)
409+
}
410+
411+
exec.PushProgressSuccess(msg)
400412

401-
resources, err := all.ListResources(exec, []string{clusterName, networkName}, []string{})
413+
msg = fmt.Sprintln("Preparing to start deleting resources")
414+
exec.PushProgressStarted(msg)
415+
416+
kubeconfigPath, err := WriteKubeconfigToFile(exec, cluster.UUID, logDir)
402417
if err != nil {
403-
return err
418+
return fmt.Errorf("failed to write kubeconfig to file: %w", err)
404419
}
405420

406-
err = all.DeleteResources(exec, resources, 16)
421+
if err := os.Setenv("KUBECONFIG", kubeconfigPath); err != nil {
422+
return fmt.Errorf("set KUBECONFIG: %w", err)
423+
}
424+
425+
exec.PushProgressSuccess(msg)
426+
427+
kubeClient, err := GetKubernetesClient(kubeconfigPath)
407428
if err != nil {
408-
return err
429+
return fmt.Errorf("failed to create Kubernetes client: %w", err)
430+
}
431+
432+
var uuids []string
433+
if deleteStorage {
434+
msg = fmt.Sprintln("Collecting PVC volume UUIDs for deletion")
435+
exec.PushProgressStarted(msg)
436+
437+
uuids, err = CollectPVCVolumeUUIDs(exec.Context(), exec, kubeClient, "dokku")
438+
if err != nil {
439+
return fmt.Errorf("failed to retrieve PVC volume UUIDs: %w", err)
440+
}
441+
442+
exec.PushProgressSuccess(msg)
443+
}
444+
445+
msg = fmt.Sprintf("Uninstalling ingress nginx helm release: %s", clusterName)
446+
exec.PushProgressStarted(msg)
447+
448+
err = UninstallHelmRelease("ingress-nginx", logDir)
449+
if err != nil {
450+
exec.PushProgressUpdateMessage(msg, "failed to uninstall ingress-nginx. You might have to manually delete the load balancer")
451+
}
452+
453+
exec.PushProgressSuccess(msg)
454+
455+
msg = fmt.Sprintf("Deleting Kubernetes cluster %s in zone %s", clusterName, zone)
456+
exec.PushProgressStarted(msg)
457+
458+
_, err = kubernetes.Delete(exec, cluster.UUID, true)
459+
if err != nil {
460+
return fmt.Errorf("failed to delete kubernetes cluster: %w", err)
461+
}
462+
463+
exec.PushProgressSuccess(msg)
464+
465+
if deleteStorage && len(uuids) > 0 {
466+
msg = fmt.Sprintln("Deleting PVC volumes")
467+
exec.PushProgressStarted(msg)
468+
469+
err = DeletePVCVolumesByUUIDs(exec, uuids)
470+
if err != nil {
471+
return fmt.Errorf("failed to delete PVC volumes: %w", err)
472+
}
473+
exec.PushProgressSuccess(msg)
474+
}
475+
476+
networkName := fmt.Sprintf("%s-%s-%s", DokkuResourceRootNameNetwork, name, zone)
477+
networks, err := exec.All().GetNetworks(exec.Context())
478+
if err != nil {
479+
return fmt.Errorf("failed to get networks: %w", err)
480+
}
481+
482+
for _, net := range networks.Networks {
483+
if net.Name == networkName {
484+
msg := fmt.Sprintf("Deleting network %s in zone %s", networkName, zone)
485+
exec.PushProgressStarted(msg)
486+
_, err = network.Delete(exec, net.UUID)
487+
if err != nil {
488+
return fmt.Errorf("failed to delete network: %w", err)
489+
}
490+
exec.PushProgressSuccess(msg)
491+
break
492+
}
409493
}
410494
case StackTypeStarterKit:
411495
clusterName := fmt.Sprintf("%s-%s-%s", StarterKitResourceRootNameCluster, name, zone)

internal/commands/stack/supabase/destroy.go

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,6 @@ func (s *destroySupabaseCommand) InitCommand() {
3939
}
4040

4141
// ExecuteWithoutArguments implements commands.NoArgumentCommand
42-
// TODO: This is not deleting the LB that supabase creates. Need to find a way to identify it.
4342
func (c *destroySupabaseCommand) ExecuteWithoutArguments(exec commands.Executor) (output.Output, error) {
4443
err := stack.DestroyStack(exec, c.name, c.zone, c.deleteStorage, c.deleteObjectStorage, stack.StackTypeSupabase)
4544
if err != nil {

0 commit comments

Comments
 (0)