From 3c4c6317e5b03ed75bb2ff6a61cff918b77c452f Mon Sep 17 00:00:00 2001 From: lufreita Date: Fri, 17 Jul 2026 17:07:20 -0300 Subject: [PATCH] ROSAENG-61173 | fix: complete delete cluster command coverage Signed-off-by: lufreita --- cmd/dlt/cluster/cmd.go | 25 ++- cmd/dlt/cluster/cmd_test.go | 297 ++++++++++++++++++++++++++++++++++++ 2 files changed, 317 insertions(+), 5 deletions(-) diff --git a/cmd/dlt/cluster/cmd.go b/cmd/dlt/cluster/cmd.go index 38ccd1e7f2..5a0e444053 100644 --- a/cmd/dlt/cluster/cmd.go +++ b/cmd/dlt/cluster/cmd.go @@ -80,6 +80,20 @@ func run(_ *cobra.Command, _ []string) { r := rosa.NewRuntime().WithAWS().WithOCM() defer r.Cleanup() + err := runWithRuntime(r, confirm.Confirm, func(clusterKey string) { + uninstallLogs.Cmd.Run(uninstallLogs.Cmd, []string{clusterKey}) + }) + if err != nil { + r.Reporter.Errorf("%s", err) + os.Exit(1) + } +} + +func runWithRuntime( + r *rosa.Runtime, + confirmFn func(string, ...interface{}) bool, + uninstallLogsFn func(string), +) error { clusterKey := r.GetClusterKey() if args.bestEffort { @@ -87,16 +101,15 @@ func run(_ *cobra.Command, _ []string) { " in AWS account '%s'. These resources will need to be deleted manually.", clusterKey, r.Creator.AccountID) } - if !confirm.Confirm("delete cluster %s", clusterKey) { - os.Exit(0) + if !confirmFn("delete cluster %s", clusterKey) { + return nil } cluster := r.FetchCluster() err := handleClusterDelete(r, cluster, clusterKey, args.bestEffort) if err != nil { - r.Reporter.Errorf("%s", err) - os.Exit(1) + return fmt.Errorf("failed to delete cluster '%s': %w", clusterKey, err) } if cluster.AWS().STS().RoleARN() != "" { @@ -122,13 +135,15 @@ func run(_ *cobra.Command, _ []string) { } if args.watch { arguments.DisableRegionDeprecationWarning = true // disable region deprecation warning - uninstallLogs.Cmd.Run(uninstallLogs.Cmd, []string{clusterKey}) + uninstallLogsFn(clusterKey) arguments.DisableRegionDeprecationWarning = false // enable region deprecation again } else { r.Reporter.Infof("To watch your cluster uninstallation logs, run 'rosa logs uninstall -c %s --watch'", clusterKey, ) } + + return nil } func handleClusterDelete(r *rosa.Runtime, cluster *cmv1.Cluster, clusterKey string, bestEffort bool) error { diff --git a/cmd/dlt/cluster/cmd_test.go b/cmd/dlt/cluster/cmd_test.go index d3bea33739..297f0995fd 100644 --- a/cmd/dlt/cluster/cmd_test.go +++ b/cmd/dlt/cluster/cmd_test.go @@ -2,16 +2,43 @@ package cluster import ( "fmt" + "io" "net/http" + "os" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" cmv1 "github.com/openshift-online/ocm-sdk-go/clustersmgmt/v1" . "github.com/openshift-online/ocm-sdk-go/testing" + "github.com/openshift/rosa/pkg/arguments" + "github.com/openshift/rosa/pkg/interactive" "github.com/openshift/rosa/pkg/test" ) +func captureRun(fn func() error) (string, string, error) { + rout, wout, _ := os.Pipe() + rerr, werr, _ := os.Pipe() + origOut := os.Stdout + origErr := os.Stderr + defer func() { + os.Stdout = origOut + os.Stderr = origErr + }() + os.Stdout = wout + os.Stderr = werr + + var err error + go func() { + err = fn() + wout.Close() + werr.Close() + }() + stdout, _ := io.ReadAll(rout) + stderr, _ := io.ReadAll(rerr) + return string(stdout), string(stderr), err +} + var _ = Describe("Delete cluster", func() { var ( t *test.TestingRuntime @@ -21,6 +48,276 @@ var _ = Describe("Delete cluster", func() { BeforeEach(func() { t = test.NewTestRuntime() clusterId = test.MockClusterID + args.bestEffort = false + args.watch = false + interactive.SetEnabled(false) + arguments.DisableRegionDeprecationWarning = false + }) + + Context("runWithRuntime", func() { + stubConfirm := func(string, ...interface{}) bool { return true } + stubNoConfirm := func(string, ...interface{}) bool { return false } + stubLogs := func(string) {} + + BeforeEach(func() { + args.bestEffort = false + args.watch = false + interactive.SetEnabled(false) + arguments.DisableRegionDeprecationWarning = false + }) + + It("runs the non-STS happy path and prints the uninstall log hint", func() { + clusterReady := test.MockCluster(func(c *cmv1.ClusterBuilder) { + c.State(cmv1.ClusterStateReady) + c.AWS(cmv1.NewAWS().STS(cmv1.NewSTS())) + }) + t.SetCluster(clusterId, clusterReady) + + statusBody := fmt.Sprintf(`{ + "kind": "ClusterStatus", + "id": "%s", + "state": "ready" + }`, clusterId) + t.ApiServer.AppendHandlers(RespondWithJSON(http.StatusOK, statusBody)) + t.ApiServer.AppendHandlers(RespondWithJSON( + http.StatusOK, test.FormatClusterList([]*cmv1.Cluster{clusterReady}))) + t.ApiServer.AppendHandlers(RespondWithJSON(http.StatusOK, "")) + + stdout, stderr, err := captureRun(func() error { + return runWithRuntime(t.RosaRuntime, stubConfirm, stubLogs) + }) + Expect(err).NotTo(HaveOccurred()) + Expect(stderr).To(BeEmpty()) + Expect(stdout).To(ContainSubstring("will start uninstalling")) + Expect(stdout).To(ContainSubstring("rosa logs uninstall -c")) + Expect(stdout).To(ContainSubstring("--watch")) + }) + + It("returns cleanly when deletion is not confirmed", func() { + clusterReady := test.MockCluster(func(c *cmv1.ClusterBuilder) { + c.State(cmv1.ClusterStateReady) + c.AWS(cmv1.NewAWS().STS(cmv1.NewSTS())) + }) + t.SetCluster(clusterId, clusterReady) + + stdout, stderr, err := captureRun(func() error { + return runWithRuntime(t.RosaRuntime, stubNoConfirm, stubLogs) + }) + Expect(err).NotTo(HaveOccurred()) + Expect(stderr).To(BeEmpty()) + Expect(stdout).To(BeEmpty()) + }) + + It("prints the best-effort warning and passes the flag through", func() { + clusterReady := test.MockCluster(func(c *cmv1.ClusterBuilder) { + c.State(cmv1.ClusterStateReady) + c.AWS(cmv1.NewAWS().STS(cmv1.NewSTS())) + }) + t.SetCluster(clusterId, clusterReady) + args.bestEffort = true + + statusBody := fmt.Sprintf(`{ + "kind": "ClusterStatus", + "id": "%s", + "state": "ready" + }`, clusterId) + t.ApiServer.AppendHandlers(RespondWithJSON(http.StatusOK, statusBody)) + t.ApiServer.AppendHandlers(RespondWithJSON( + http.StatusOK, test.FormatClusterList([]*cmv1.Cluster{clusterReady}))) + t.ApiServer.AppendHandlers(RespondWithJSON(http.StatusOK, "")) + + stdout, stderr, err := captureRun(func() error { + return runWithRuntime(t.RosaRuntime, stubConfirm, stubLogs) + }) + Expect(err).NotTo(HaveOccurred()) + Expect(stderr).To(ContainSubstring("best effort")) + Expect(stderr).To(ContainSubstring("certain resources may be left behind")) + Expect(stdout).To(ContainSubstring("will start uninstalling")) + }) + + It("prints STS cleanup guidance for clusters with operator roles", func() { + clusterReady := test.MockCluster(func(c *cmv1.ClusterBuilder) { + c.State(cmv1.ClusterStateReady) + c.AWS(cmv1.NewAWS().STS( + cmv1.NewSTS(). + RoleARN("arn:aws:iam::123456789012:role/Installer"). + OIDCEndpointURL("https://oidc.example.com"). + OperatorRolePrefix("my-prefix"). + OperatorIAMRoles( + cmv1.NewOperatorIAMRole(). + Name("ebs-cloud-credentials"). + Namespace("openshift-cluster-csi-drivers"). + RoleARN("arn:aws:iam::123456789012:role/op-role"), + ), + )) + }) + t.SetCluster(clusterId, clusterReady) + + statusBody := fmt.Sprintf(`{ + "kind": "ClusterStatus", + "id": "%s", + "state": "ready" + }`, clusterId) + t.ApiServer.AppendHandlers(RespondWithJSON(http.StatusOK, statusBody)) + t.ApiServer.AppendHandlers(RespondWithJSON( + http.StatusOK, test.FormatClusterList([]*cmv1.Cluster{clusterReady}))) + t.ApiServer.AppendHandlers(RespondWithJSON(http.StatusOK, "")) + + stdout, stderr, err := captureRun(func() error { + return runWithRuntime(t.RosaRuntime, stubConfirm, stubLogs) + }) + Expect(err).NotTo(HaveOccurred()) + Expect(stderr).To(BeEmpty()) + Expect(stdout).To(ContainSubstring("Operator IAM Roles:")) + Expect(stdout).To(ContainSubstring("arn:aws:iam::123456789012:role/op-role")) + Expect(stdout).To(ContainSubstring("OIDC Provider : https://oidc.example.com")) + Expect(stdout).To(ContainSubstring("rosa delete operator-roles -c")) + Expect(stdout).To(ContainSubstring("rosa delete oidc-provider -c")) + }) + + It("prints STS cleanup guidance without operator role output when none remain", func() { + clusterReady := test.MockCluster(func(c *cmv1.ClusterBuilder) { + c.State(cmv1.ClusterStateReady) + c.AWS(cmv1.NewAWS().STS( + cmv1.NewSTS(). + RoleARN("arn:aws:iam::123456789012:role/Installer"). + OIDCEndpointURL("https://oidc-no-roles.example.com"), + )) + }) + t.SetCluster(clusterId, clusterReady) + + statusBody := fmt.Sprintf(`{ + "kind": "ClusterStatus", + "id": "%s", + "state": "ready" + }`, clusterId) + t.ApiServer.AppendHandlers(RespondWithJSON(http.StatusOK, statusBody)) + t.ApiServer.AppendHandlers(RespondWithJSON( + http.StatusOK, test.FormatClusterList([]*cmv1.Cluster{clusterReady}))) + t.ApiServer.AppendHandlers(RespondWithJSON(http.StatusOK, "")) + + stdout, stderr, err := captureRun(func() error { + return runWithRuntime(t.RosaRuntime, stubConfirm, stubLogs) + }) + Expect(err).NotTo(HaveOccurred()) + Expect(stderr).To(BeEmpty()) + Expect(stdout).NotTo(ContainSubstring("Operator IAM Roles:")) + Expect(stdout).To(ContainSubstring("OIDC Provider : https://oidc-no-roles.example.com")) + Expect(stdout).To(ContainSubstring("rosa delete operator-roles -c")) + Expect(stdout).To(ContainSubstring("rosa delete oidc-provider -c")) + }) + + It("runs uninstall logs when watch is enabled and restores the deprecation warning flag", func() { + clusterReady := test.MockCluster(func(c *cmv1.ClusterBuilder) { + c.State(cmv1.ClusterStateReady) + c.AWS(cmv1.NewAWS().STS(cmv1.NewSTS())) + }) + t.SetCluster(clusterId, clusterReady) + args.watch = true + + statusBody := fmt.Sprintf(`{ + "kind": "ClusterStatus", + "id": "%s", + "state": "ready" + }`, clusterId) + t.ApiServer.AppendHandlers(RespondWithJSON(http.StatusOK, statusBody)) + t.ApiServer.AppendHandlers(RespondWithJSON( + http.StatusOK, test.FormatClusterList([]*cmv1.Cluster{clusterReady}))) + t.ApiServer.AppendHandlers(RespondWithJSON(http.StatusOK, "")) + + var watchedClusterKey string + var sawDisabledWarning bool + watchStub := func(clusterKey string) { + watchedClusterKey = clusterKey + sawDisabledWarning = arguments.DisableRegionDeprecationWarning + } + + stdout, stderr, err := captureRun(func() error { + return runWithRuntime(t.RosaRuntime, stubConfirm, watchStub) + }) + Expect(err).NotTo(HaveOccurred()) + Expect(stderr).To(BeEmpty()) + Expect(stdout).To(ContainSubstring("will start uninstalling")) + Expect(watchedClusterKey).To(Equal(clusterId)) + Expect(sawDisabledWarning).To(BeTrue()) + Expect(arguments.DisableRegionDeprecationWarning).To(BeFalse()) + Expect(stdout).NotTo(ContainSubstring("rosa logs uninstall -c")) + }) + + It("returns the already-uninstalling path through the command wrapper", func() { + clusterReady := test.MockCluster(func(c *cmv1.ClusterBuilder) { + c.State(cmv1.ClusterStateReady) + c.AWS(cmv1.NewAWS().STS(cmv1.NewSTS())) + }) + t.SetCluster(clusterId, clusterReady) + + statusBody := fmt.Sprintf(`{ + "kind": "ClusterStatus", + "id": "%s", + "state": "uninstalling" + }`, clusterId) + t.ApiServer.AppendHandlers(RespondWithJSON(http.StatusOK, statusBody)) + + stdout, stderr, err := captureRun(func() error { + return runWithRuntime(t.RosaRuntime, stubConfirm, stubLogs) + }) + Expect(err).NotTo(HaveOccurred()) + Expect(stderr).To(BeEmpty()) + Expect(stdout).To(ContainSubstring("already uninstalling")) + Expect(stdout).To(ContainSubstring("rosa logs uninstall -c")) + }) + + It("returns an error from GetClusterState through the command wrapper", func() { + clusterReady := test.MockCluster(func(c *cmv1.ClusterBuilder) { + c.State(cmv1.ClusterStateReady) + c.AWS(cmv1.NewAWS().STS(cmv1.NewSTS())) + }) + t.SetCluster(clusterId, clusterReady) + + t.ApiServer.AppendHandlers(RespondWithJSON(http.StatusInternalServerError, "")) + + stdout, stderr, err := captureRun(func() error { + return runWithRuntime(t.RosaRuntime, stubConfirm, stubLogs) + }) + Expect(err).To(HaveOccurred()) + Expect(stderr).To(BeEmpty()) + Expect(stdout).To(BeEmpty()) + Expect(err.Error()).To(ContainSubstring("failed to delete cluster")) + Expect(err.Error()).To(ContainSubstring("expected response content type")) + }) + + It("returns a delete error through the command wrapper", func() { + clusterReady := test.MockCluster(func(c *cmv1.ClusterBuilder) { + c.State(cmv1.ClusterStateReady) + c.AWS(cmv1.NewAWS().STS(cmv1.NewSTS())) + }) + t.SetCluster(clusterId, clusterReady) + + statusBody := fmt.Sprintf(`{ + "kind": "ClusterStatus", + "id": "%s", + "state": "ready" + }`, clusterId) + t.ApiServer.AppendHandlers(RespondWithJSON(http.StatusOK, statusBody)) + t.ApiServer.AppendHandlers(RespondWithJSON( + http.StatusOK, test.FormatClusterList([]*cmv1.Cluster{clusterReady}))) + t.ApiServer.AppendHandlers(RespondWithJSON(http.StatusForbidden, `{ + "kind": "Error", + "id": "403", + "href": "/api/clusters_mgmt/v1/errors/403", + "code": "CLUSTERS-MGMT-403", + "reason": "forbidden" + }`)) + + stdout, stderr, err := captureRun(func() error { + return runWithRuntime(t.RosaRuntime, stubConfirm, stubLogs) + }) + Expect(err).To(HaveOccurred()) + Expect(stderr).To(BeEmpty()) + Expect(stdout).To(BeEmpty()) + Expect(err.Error()).To(ContainSubstring("failed to delete cluster")) + Expect(err.Error()).To(ContainSubstring("forbidden")) + }) }) Context("handleClusterDelete", func() {