From 6e432540463a93ec672af69e3bef828a27ca58c1 Mon Sep 17 00:00:00 2001 From: Moritz Clasmeier Date: Tue, 27 Oct 2026 14:23:50 +0100 Subject: [PATCH 1/4] Discover namespaces from cluster for teardown instead of relying on manifest Teardown no longer loads the on-cluster roxie manifest or assembles a deploy config. Instead, it discovers Central and SecuredCluster CRs directly from the cluster using `kubectl get -A`, and add-on teardown discovers Helm releases by prefix. Parallelize waiting for PVCs and other resources on teardown --- cmd/teardown.go | 13 +- internal/deployer/addons.go | 31 ++--- internal/deployer/deployer.go | 217 +++++++++++++++++++++------------- internal/k8s/resource.go | 41 +++++++ 4 files changed, 197 insertions(+), 105 deletions(-) diff --git a/cmd/teardown.go b/cmd/teardown.go index 37f01b9..b5f69cf 100644 --- a/cmd/teardown.go +++ b/cmd/teardown.go @@ -2,7 +2,6 @@ package main import ( "context" - "errors" "fmt" "time" @@ -10,7 +9,6 @@ import ( "github.com/stackrox/roxie/internal/component" "github.com/stackrox/roxie/internal/deployer" "github.com/stackrox/roxie/internal/env" - "github.com/stackrox/roxie/internal/k8s" log "github.com/stackrox/roxie/internal/logger" "github.com/stackrox/roxie/internal/manifest" ) @@ -60,16 +58,7 @@ func runTeardown(cmd *cobra.Command, args []string) error { ctx, cancel := context.WithTimeout(context.Background(), 30*time.Minute) defer cancel() - var clusterConfig *deployer.Config - clusterManifest, err := manifest.LoadManifestSecret(ctx) - if errors.Is(err, k8s.ErrResourceNotFound) { - log.Infof("No roxie manifest found in cluster, proceeding without it") - } else if err != nil { - log.Warningf("Failed to load manifest from cluster: %v", err) - } else { - clusterConfig = &clusterManifest.Config - } - + clusterConfig := new(deployer.DefaultConfig()) deploySettings, err := assembleConfigForCommand(clusterConfig, deploySettingsFromArgs, skipUserConfig) if err != nil { return err diff --git a/internal/deployer/addons.go b/internal/deployer/addons.go index 8123d8f..5f2adce 100644 --- a/internal/deployer/addons.go +++ b/internal/deployer/addons.go @@ -8,6 +8,7 @@ import ( "regexp" "slices" + "github.com/stackrox/roxie/internal/helm" log "github.com/stackrox/roxie/internal/logger" ) @@ -52,24 +53,26 @@ func (d *Deployer) deployAddOns(ctx context.Context, addOns []AddOn) error { return nil } -func (d *Deployer) teardownAddOns(ctx context.Context, addOns []AddOn) { - if len(addOns) == 0 { - return +// teardownDiscoveredAddOns uninstalls add-on Helm releases in the given +// namespace. Add-ons are lifecycle-aligned with Central, so the caller +// passes the Central namespace. +func (d *Deployer) teardownDiscoveredAddOns(ctx context.Context, namespace string) error { + releases, err := helm.ListByPrefix(ctx, addOnReleasePrefix, namespace) + if err != nil { + return fmt.Errorf("listing add-on Helm releases in namespace %s: %w", namespace, err) + } + if len(releases) == 0 { + return nil } - log.Infof("Tearing down %d add-on(s)...", len(addOns)) - - for _, addon := range addOns { - if err := addon.Teardown(ctx); err != nil { - if !addon.IsOptional() { - log.Errorf("Failed to tear down non-optional add-on %q: %v", addon.Name(), err) - } else { - log.Warningf("Failed to tear down optional add-on %q: %v", addon.Name(), err) - } - } else { - log.Successf("Add-on %q torn down", addon.Name()) + log.Infof("Tearing down %d add-on(s)...", len(releases)) + for _, release := range releases { + if err := helm.Uninstall(ctx, release, namespace); err != nil { + return fmt.Errorf("tearing down add-on release %q: %w", release, err) } + log.Successf("Add-on release %q torn down", release) } + return nil } // ResolveEnabledAddOns returns the enabled add-ons sorted by descending priority (name for ties at zero). diff --git a/internal/deployer/deployer.go b/internal/deployer/deployer.go index a54b240..bf68e23 100644 --- a/internal/deployer/deployer.go +++ b/internal/deployer/deployer.go @@ -3,10 +3,10 @@ package deployer import ( "context" "crypto/rand" + "errors" "fmt" "os" "os/exec" - "slices" "strconv" "strings" "sync" @@ -14,6 +14,7 @@ import ( "time" "github.com/fatih/color" + "golang.org/x/sync/errgroup" "github.com/stackrox/roxie/internal/component" "github.com/stackrox/roxie/internal/containerrt" @@ -87,11 +88,11 @@ func (d *Deployer) deleteResources(ctx context.Context, namespace string, resour } // Expects that reconciliation for the RHACS operator is paused. -func (d *Deployer) deleteCentralResources(ctx context.Context) error { +func (d *Deployer) deleteCentralResources(ctx context.Context, namespace string) error { log.Info("Deleting Central resources") crExists := true - if _, err := k8s.RetrieveResourceFromCluster(ctx, d.config.Central.Namespace, "central", "stackrox-central-services"); err != nil { + if _, err := k8s.RetrieveResourceFromCluster(ctx, namespace, "central", "stackrox-central-services"); err != nil { if !k8s.IsResourceNotFound(err) { return fmt.Errorf("retrieving Central CR: %w", err) } @@ -100,12 +101,12 @@ func (d *Deployer) deleteCentralResources(ctx context.Context) error { if crExists { log.Info("Removing any pause-reconcile annotation from Central") - if err := d.removePauseReconcileAnnotation(ctx, "central", "stackrox-central-services", d.config.Central.Namespace); err != nil { + if err := d.removePauseReconcileAnnotation(ctx, "central", "stackrox-central-services", namespace); err != nil { return err } log.Debug("Removed any pause-reconcile annotation from Central") - err := d.deleteResource(ctx, d.config.Central.Namespace, "central", "stackrox-central-services", "--wait") + err := d.deleteResource(ctx, namespace, "central", "stackrox-central-services", "--wait") if err != nil { return err } @@ -114,6 +115,7 @@ func (d *Deployer) deleteCentralResources(ctx context.Context) error { log.Info("Deletion of Central resources requested, but Central CR is not present anymore") } + var group errgroup.Group for _, resource := range []ResourceToDelete{ {Name: "central-db", Kind: "pvc"}, {Name: "central-db-backup", Kind: "pvc"}, @@ -125,36 +127,42 @@ func (d *Deployer) deleteCentralResources(ctx context.Context) error { {Name: "scanner-db-password", Kind: "secret", OwnerName: centralCrName}, {Name: "stackrox-central-helm", Kind: "configmap"}, } { - log.Dimf("Attempting to delete %s/%s", resource.Kind, resource.Name) - if resource.OwnerName != "" { - // Avoid deletion if the resource does not have the expected owner. - // (e.g. in case central and secured cluster are deployed into the same namespace). - obj, err := k8s.RetrieveResourceFromCluster(ctx, d.config.Central.Namespace, resource.Kind, resource.Name) - if err != nil { - if !k8s.IsResourceNotFound(err) { - log.Warningf("Failed to retrieve %s/%s for owner checking: %v. Skipping deletion. Deployment might be affected.", resource.Kind, resource.Name, err) + group.Go(func() error { + log.Dimf("%s: Attempting to delete %s/%s", namespace, resource.Kind, resource.Name) + if resource.OwnerName != "" { + // Avoid deletion if the resource does not have the expected owner. + // (e.g. in case central and secured cluster are deployed into the same namespace). + obj, err := k8s.RetrieveResourceFromCluster(ctx, namespace, resource.Kind, resource.Name) + if err != nil { + if !k8s.IsResourceNotFound(err) { + log.Warningf("%s: Failed to retrieve %s/%s for owner checking: %v. Skipping deletion. Deployment might be affected.", namespace, resource.Kind, resource.Name, err) + return fmt.Errorf("retrieving resource %s/%s from namespace %s: %w", resource.Kind, resource.Name, namespace, err) + } + return nil // resource not found + } + if k8s.ResourceNotOwnedByName(obj, resource.OwnerName) { + log.Dimf("%s: Skipping deletion of %s/%s: not owned by %s", namespace, resource.Kind, resource.Name, resource.OwnerName) + return nil } - continue } - if k8s.ResourceNotOwnedByName(obj, resource.OwnerName) { - log.Dimf("Skipping deletion of %s/%s: not owned by %s", resource.Kind, resource.Name, resource.OwnerName) - continue + + if err := d.deleteResource(ctx, namespace, resource.Kind, resource.Name); err != nil { + return fmt.Errorf("failed to delete %s/%s in namepace %s: %w", resource.Kind, resource.Name, namespace, err) } - } - if err := d.deleteResource(ctx, d.config.Central.Namespace, resource.Kind, resource.Name); err != nil { - return fmt.Errorf("failed to delete %s/%s: %w", resource.Kind, resource.Name, err) - } + log.Dimf("%s: Deleted %s/%s", namespace, resource.Kind, resource.Name) + return nil + }) } - return nil + return group.Wait() } -func (d *Deployer) deleteSecuredClusterResources(ctx context.Context) error { +func (d *Deployer) deleteSecuredClusterResources(ctx context.Context, namespace string) error { log.Info("Deleting SecuredCluster resources") crExists := true - if _, err := k8s.RetrieveResourceFromCluster(ctx, d.config.SecuredCluster.Namespace, "securedcluster", "stackrox-secured-cluster-services"); err != nil { + if _, err := k8s.RetrieveResourceFromCluster(ctx, namespace, "securedcluster", securedClusterCrName); err != nil { if !k8s.IsResourceNotFound(err) { return fmt.Errorf("retrieving SecuredCluster CR: %w", err) } @@ -163,12 +171,12 @@ func (d *Deployer) deleteSecuredClusterResources(ctx context.Context) error { if crExists { log.Info("Removing any pause-reconcile annotation from SecuredCluster") - if err := d.removePauseReconcileAnnotation(ctx, "securedcluster", "stackrox-secured-cluster-services", d.config.SecuredCluster.Namespace); err != nil { + if err := d.removePauseReconcileAnnotation(ctx, "securedcluster", securedClusterCrName, namespace); err != nil { return err } log.Debug("Removed any pause-reconcile annotation from SecuredCluster") - err := d.deleteResource(ctx, d.config.SecuredCluster.Namespace, "securedcluster", "stackrox-secured-cluster-services", "--wait") + err := d.deleteResource(ctx, namespace, "securedcluster", securedClusterCrName, "--wait") if err != nil { return err } @@ -178,6 +186,7 @@ func (d *Deployer) deleteSecuredClusterResources(ctx context.Context) error { } // Delete resources, which are treated special. + group, ctx := errgroup.WithContext(ctx) for _, resource := range []ResourceToDelete{ {Name: "cluster-registration-secret", Kind: "secret"}, // scanner-v4-db is not managed by the operator's ReconcilePVCExtension, so its ownerReference @@ -188,28 +197,35 @@ func (d *Deployer) deleteSecuredClusterResources(ctx context.Context) error { // when both are deployed into the same namespace. {Name: "scanner-db-password", Kind: "secret", OwnerName: securedClusterCrName}, } { - log.Dimf("Attempting to delete %s/%s", resource.Kind, resource.Name) - if resource.OwnerName != "" { - // Avoid deletion if the resource does not have the expected owner. - // (e.g. in case central and secured cluster are deployed into the same namespace). - obj, err := k8s.RetrieveResourceFromCluster(ctx, d.config.SecuredCluster.Namespace, resource.Kind, resource.Name) - if err != nil { - if !k8s.IsResourceNotFound(err) { - log.Warningf("Failed to retrieve %s/%s for owner checking: %v. Skipping deletion. Deployment might be affected.", resource.Kind, resource.Name, err) + group.Go(func() error { + log.Dimf("%s: Attempting to delete %s/%s", namespace, resource.Kind, resource.Name) + if resource.OwnerName != "" { + // Avoid deletion if the resource does not have the expected owner. + // (e.g. in case central and secured cluster are deployed into the same namespace). + obj, err := k8s.RetrieveResourceFromCluster(ctx, namespace, resource.Kind, resource.Name) + if err != nil { + if !k8s.IsResourceNotFound(err) { + log.Warningf("%s: Failed to retrieve %s/%s for owner checking: %v. Skipping deletion. Deployment might be affected.", namespace, resource.Kind, resource.Name, err) + return fmt.Errorf("retrieving resource %s/%s from namespace %s: %w", resource.Kind, resource.Name, namespace, err) + } + return nil // resource not found + } + if k8s.ResourceNotOwnedByName(obj, resource.OwnerName) { + log.Dimf("%s: Skipping deletion of %s/%s: not owned by %s", namespace, resource.Kind, resource.Name, resource.OwnerName) + return nil } - continue } - if k8s.ResourceNotOwnedByName(obj, resource.OwnerName) { - log.Dimf("Skipping deletion of %s/%s: not owned by %s", resource.Kind, resource.Name, resource.OwnerName) - continue + + if err := d.deleteResource(ctx, namespace, resource.Kind, resource.Name); err != nil { + return fmt.Errorf("failed to delete %s/%s in namespace %s: %w", resource.Kind, resource.Name, namespace, err) } - } - if err := d.deleteResource(ctx, d.config.SecuredCluster.Namespace, resource.Kind, resource.Name); err != nil { - return fmt.Errorf("failed to delete %s/%s: %w", resource.Kind, resource.Name, err) - } + + log.Dimf("%s: Deleted %s/%s", namespace, resource.Kind, resource.Name) + return nil + }) } - return nil + return group.Wait() } func (d *Deployer) SetConfig(config Config) { @@ -417,7 +433,7 @@ func (d *Deployer) deployCentral(ctx context.Context) error { log.Infof("Deploying Central to namespace %s", d.config.Central.Namespace) if d.namespaceExists(d.config.Central.Namespace) { log.Info("Existing Central deployment found, tearing down...") - if err := d.teardownCentral(ctx); err != nil { + if err := d.teardownCentralWithAddOns(ctx); err != nil { log.Warningf("Error during teardown: %v", err) } } @@ -451,27 +467,17 @@ func (d *Deployer) deploySecuredCluster(ctx context.Context) error { func (d *Deployer) Teardown(ctx context.Context, components component.Component) error { log.Infof("Starting teardown of %s", components) - if components.IncludesAddOns() { - enabledAddOns, err := d.ResolveEnabledAddOns() - if err != nil { - return fmt.Errorf("resolving enabled add-ons: %w", err) - } - slices.Reverse(enabledAddOns) // Tearing down in opposite order as deploying. - d.teardownAddOns(ctx, enabledAddOns) - } - switch components { case component.Central: - return d.teardownCentral(ctx) + return d.teardownCentralWithAddOns(ctx) + case component.AddOns: + return d.teardownAddOns(ctx) case component.SecuredCluster: return d.teardownSecuredCluster(ctx) case component.Operator: return d.teardownOperator(ctx) case component.Both, component.All: - // Tear down components in parallel for better performance var wg sync.WaitGroup - - // Always tear down central and sensor wg.Add(2) go func() { @@ -483,7 +489,7 @@ func (d *Deployer) Teardown(ctx context.Context, components component.Component) go func() { defer wg.Done() - if err := d.teardownCentral(ctx); err != nil { + if err := d.teardownCentralWithAddOns(ctx); err != nil { log.Warningf("Error tearing down central: %v", err) } }() @@ -498,62 +504,115 @@ func (d *Deployer) Teardown(ctx context.Context, components component.Component) } } return nil - case component.AddOns: - return nil default: return fmt.Errorf("unknown component: %s", components) } } -func (d *Deployer) teardownCentral(ctx context.Context) error { - log.Infof("🗑️ Tearing down central in namespace %s", d.config.Central.Namespace) +// teardownCentralWithAddOns discovers the Central CR namespace from the +// cluster and tears down add-ons and Central resources in that namespace. +func (d *Deployer) teardownCentralWithAddOns(ctx context.Context) error { + namespace, err := k8s.DiscoverCRNamespace(ctx, "central", centralCrName) + if err != nil { + return fmt.Errorf("discovering Central namespace: %w", err) + } + if namespace == "" { + log.Infof("No Central CR found on cluster, skipping Central teardown") + return d.teardownAddOns(ctx) + } + + addOnsErr := d.teardownDiscoveredAddOns(ctx, namespace) + if addOnsErr != nil { + addOnsErr = fmt.Errorf("tearing down add-ons: %w", addOnsErr) + } + + centralErr := d.teardownCentral(ctx, namespace) + + return errors.Join(addOnsErr, centralErr) +} - if !d.namespaceExists(d.config.Central.Namespace) { - log.Infof("Namespace %s doesn't exist, skipping", d.config.Central.Namespace) +func (d *Deployer) teardownAddOns(ctx context.Context) error { + namespace, err := k8s.DiscoverCRNamespace(ctx, "central", centralCrName) + if err != nil { + return fmt.Errorf("discovering Central namespace: %w", err) + } + // Add-ons are Helm releases owned by roxie, not by the operator. Fall back to + // the configured namespace so they can get cleaned up even if the Central CR is gone. + if namespace == "" && d.config.Central.Namespace != "" { + namespace = d.config.Central.Namespace + if !d.namespaceExists(namespace) { + log.Infof("No Central CR found and namespace %s does not exist, skipping add-on teardown", namespace) + return nil + } + log.Dimf("No Central CR found, falling back to configured namespace %s for add-on teardown", namespace) + } + if namespace == "" { + log.Infof("No Central CR found and no configured namespace, skipping add-on teardown") return nil } + return d.teardownDiscoveredAddOns(ctx, namespace) +} + +func (d *Deployer) teardownCentral(ctx context.Context, namespace string) error { + log.Infof("🗑️ Tearing down central in namespace %s", namespace) d.portForward.Stop() d.stopDetachedPortForward() // Add pause-reconcile annotation to not have the operator interfere during resource deletion. - if d.doesResourceExist(ctx, "central", "stackrox-central-services", d.config.Central.Namespace) { - if err := d.addPauseReconcileAnnotation(ctx, "central", "stackrox-central-services", d.config.Central.Namespace); err != nil { + if d.doesResourceExist(ctx, "central", centralCrName, namespace) { + if err := d.addPauseReconcileAnnotation(ctx, "central", centralCrName, namespace); err != nil { log.Warningf("Error adding pause-reconcile annotation: %v", err) } } log.Info("⏳ Waiting for Central resources to be fully deleted...") - if err := d.deleteCentralResources(ctx); err != nil { + if err := d.deleteCentralResources(ctx, namespace); err != nil { return fmt.Errorf("failed to delete Central resources: %w", err) } - log.Successf("✓ Central resources in namespace %s have been deleted", d.config.Central.Namespace) + log.Successf("✓ Central resources in namespace %s have been deleted", namespace) return nil } func (d *Deployer) teardownSecuredCluster(ctx context.Context) error { - log.Infof("🗑️ Tearing down secured cluster in namespace %s", d.config.SecuredCluster.Namespace) - - if !d.namespaceExists(d.config.SecuredCluster.Namespace) { - log.Infof("Namespace %s doesn't exist, skipping", d.config.SecuredCluster.Namespace) + namespaces, err := k8s.DiscoverCRNamespaces(ctx, "securedcluster", securedClusterCrName) + if err != nil { + return fmt.Errorf("discovering SecuredCluster namespaces: %w", err) + } + if len(namespaces) == 0 { + log.Infof("No SecuredCluster CRs found on cluster, skipping SecuredCluster teardown") return nil } - if d.doesResourceExist(ctx, "securedcluster", "stackrox-secured-cluster-services", d.config.SecuredCluster.Namespace) { - // Add pause-reconcile annotation to not have the operator interfere during resource deletion. - if err := d.addPauseReconcileAnnotation(ctx, "securedcluster", "stackrox-secured-cluster-services", d.config.SecuredCluster.Namespace); err != nil { + var group errgroup.Group + for _, ns := range namespaces { + group.Go(func() error { + if err := d.teardownSecuredClusterInNamespace(ctx, ns); err != nil { + log.Warningf("Error tearing down SecuredCluster in namespace %s: %v", ns, err) + return fmt.Errorf("tearing down SecuredCluster in namespace %s: %w", ns, err) + } + return nil + }) + } + return group.Wait() +} + +func (d *Deployer) teardownSecuredClusterInNamespace(ctx context.Context, namespace string) error { + log.Infof("🗑️ Tearing down secured cluster in namespace %s", namespace) + + if d.doesResourceExist(ctx, "securedcluster", securedClusterCrName, namespace) { + if err := d.addPauseReconcileAnnotation(ctx, "securedcluster", securedClusterCrName, namespace); err != nil { log.Warningf("Error adding pause-reconcile annotation: %v", err) } } log.Info("⏳ Waiting for SecuredCluster resources to be fully deleted...") - err := d.deleteSecuredClusterResources(ctx) - if err != nil { - return fmt.Errorf("failed to delete SecuredCluster resources: %w", err) + if err := d.deleteSecuredClusterResources(ctx, namespace); err != nil { + return fmt.Errorf("failed to delete SecuredCluster resources in namespace %s: %w", namespace, err) } - log.Successf("✓ SecuredCluster resources in namespace %s have been deleted", d.config.SecuredCluster.Namespace) + log.Successf("✓ SecuredCluster resources in namespace %s have been deleted", namespace) return nil } diff --git a/internal/k8s/resource.go b/internal/k8s/resource.go index 67a04ae..90a7286 100644 --- a/internal/k8s/resource.go +++ b/internal/k8s/resource.go @@ -62,6 +62,47 @@ func IsResourceNotFound(err error) bool { return errors.Is(err, ErrResourceNotFound) } +// DiscoverCRNamespaces returns the namespaces that contain a custom resource of +// the given type and name. It returns (nil, nil) when the CR type is not +// registered on the cluster or no matching instances are found. +func DiscoverCRNamespaces(ctx context.Context, crType, crName string) ([]string, error) { + jsonpathExpr := fmt.Sprintf(`{.items[?(@.metadata.name=="%s")].metadata.namespace}`, crName) + result, err := RunKubectl(ctx, KubectlOptions{ + Args: []string{"get", crType, "-A", "-o", "jsonpath=" + jsonpathExpr}, + }) + if err != nil { + stderr := strings.ToLower(result.Stderr) + if strings.Contains(stderr, "the server doesn't have a resource type") { + return nil, nil + } + return nil, fmt.Errorf("discovering %s/%s: %w", crType, crName, err) + } + output := strings.TrimSpace(result.Stdout) + if output == "" { + return nil, nil + } + namespaces := strings.Fields(output) + log.Infof("Discovered %s/%s in namespace(s): %s", crType, crName, strings.Join(namespaces, ", ")) + return namespaces, nil +} + +// DiscoverCRNamespace discovers the single namespace containing the named custom +// resource. It returns ("", nil) when no instance is found, and an error when +// the CR exists in more than one namespace. +func DiscoverCRNamespace(ctx context.Context, crType, crName string) (string, error) { + namespaces, err := DiscoverCRNamespaces(ctx, crType, crName) + if err != nil { + return "", err + } + if len(namespaces) > 1 { + return "", fmt.Errorf("found %s/%s in multiple namespaces: %s", crType, crName, strings.Join(namespaces, ", ")) + } + if len(namespaces) == 0 { + return "", nil + } + return namespaces[0], nil +} + // ResourceNotOwnedByName checks if the given unstructured object does not have an owner reference with the specified owner name. // The negative version of this function is better suited for our needs, because there can be multiple owner references and // we are only interested in checking if a given resource is NOT owned by a specific owner. From 8029e2886533b6b1c61b0cb675b81a664d636087 Mon Sep 17 00:00:00 2001 From: Moritz Clasmeier Date: Fri, 30 Oct 2026 07:36:34 +0100 Subject: [PATCH 2/4] go mod tidy --- go.mod | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/go.mod b/go.mod index af447ad..f007115 100644 --- a/go.mod +++ b/go.mod @@ -132,7 +132,7 @@ require ( go.opentelemetry.io/otel/trace v1.45.0 // indirect go.yaml.in/yaml/v2 v2.4.4 // indirect golang.org/x/net v0.60.0 // indirect - golang.org/x/sync v0.23.0 // indirect + golang.org/x/sync v0.23.0 golang.org/x/sys v0.49.0 // indirect golang.org/x/text v0.42.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect From 5c8fc20e0c7dd812e2f6b46967f4813f1878933b Mon Sep 17 00:00:00 2001 From: Moritz Clasmeier <111092021+mclasmeier@users.noreply.github.com> Date: Fri, 30 Oct 2026 16:11:40 +0100 Subject: [PATCH 3/4] Update internal/deployer/deployer.go Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- internal/deployer/deployer.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/deployer/deployer.go b/internal/deployer/deployer.go index bf68e23..37884b2 100644 --- a/internal/deployer/deployer.go +++ b/internal/deployer/deployer.go @@ -147,7 +147,7 @@ func (d *Deployer) deleteCentralResources(ctx context.Context, namespace string) } if err := d.deleteResource(ctx, namespace, resource.Kind, resource.Name); err != nil { - return fmt.Errorf("failed to delete %s/%s in namepace %s: %w", resource.Kind, resource.Name, namespace, err) + return fmt.Errorf("failed to delete %s/%s in namespace %s: %w", resource.Kind, resource.Name, namespace, err) } log.Dimf("%s: Deleted %s/%s", namespace, resource.Kind, resource.Name) From 29a234fb801dccba4b936e667ad1325d982bc214 Mon Sep 17 00:00:00 2001 From: Moritz Clasmeier Date: Fri, 30 Oct 2026 16:17:15 +0100 Subject: [PATCH 4/4] Teardown addons independently, even if some fail --- internal/deployer/addons.go | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/internal/deployer/addons.go b/internal/deployer/addons.go index 5f2adce..975a04b 100644 --- a/internal/deployer/addons.go +++ b/internal/deployer/addons.go @@ -66,13 +66,19 @@ func (d *Deployer) teardownDiscoveredAddOns(ctx context.Context, namespace strin } log.Infof("Tearing down %d add-on(s)...", len(releases)) + var errs []error for _, release := range releases { + if err := ctx.Err(); err != nil { + errs = append(errs, err) + break + } if err := helm.Uninstall(ctx, release, namespace); err != nil { - return fmt.Errorf("tearing down add-on release %q: %w", release, err) + errs = append(errs, fmt.Errorf("tearing down add-on release %q: %w", release, err)) + continue } log.Successf("Add-on release %q torn down", release) } - return nil + return errors.Join(errs...) } // ResolveEnabledAddOns returns the enabled add-ons sorted by descending priority (name for ties at zero).