diff --git a/cmd/dlt/cluster/cmd.go b/cmd/dlt/cluster/cmd.go index 38ccd1e7f2..5e8413a9c3 100644 --- a/cmd/dlt/cluster/cmd.go +++ b/cmd/dlt/cluster/cmd.go @@ -41,6 +41,12 @@ var args struct { mode string } +var confirmDelete = confirm.Confirm + +var runUninstallLogs = func(clusterKey string) { + uninstallLogs.Cmd.Run(uninstallLogs.Cmd, []string{clusterKey}) +} + var Cmd = &cobra.Command{ Use: "cluster", Short: "Delete cluster", @@ -76,10 +82,18 @@ func init() { ) } -func run(_ *cobra.Command, _ []string) { +func run(cmd *cobra.Command, argv []string) { r := rosa.NewRuntime().WithAWS().WithOCM() defer r.Cleanup() + err := runWithRuntime(r, cmd, argv) + if err != nil { + r.Reporter.Errorf("%s", err) + os.Exit(1) + } +} + +func runWithRuntime(r *rosa.Runtime, _ *cobra.Command, _ []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 !confirmDelete("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 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}) + runUninstallLogs(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..f57edba200 100644 --- a/cmd/dlt/cluster/cmd_test.go +++ b/cmd/dlt/cluster/cmd_test.go @@ -9,6 +9,8 @@ import ( 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" ) @@ -21,6 +23,258 @@ 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() { + var argv []string + + BeforeEach(func() { + argv = []string{} + args.bestEffort = false + args.watch = false + confirmDelete = func(string, ...interface{}) bool { return true } + runUninstallLogs = func(string) {} + 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 := test.RunWithOutputCaptureAndArgv(runWithRuntime, t.RosaRuntime, Cmd, &argv) + 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) + confirmDelete = func(string, ...interface{}) bool { return false } + + stdout, stderr, err := test.RunWithOutputCaptureAndArgv(runWithRuntime, t.RosaRuntime, Cmd, &argv) + 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 := test.RunWithOutputCaptureAndArgv(runWithRuntime, t.RosaRuntime, Cmd, &argv) + 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 := test.RunWithOutputCaptureAndArgv(runWithRuntime, t.RosaRuntime, Cmd, &argv) + 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 := test.RunWithOutputCaptureAndArgv(runWithRuntime, t.RosaRuntime, Cmd, &argv) + 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 + runUninstallLogs = func(clusterKey string) { + watchedClusterKey = clusterKey + sawDisabledWarning = arguments.DisableRegionDeprecationWarning + } + + stdout, stderr, err := test.RunWithOutputCaptureAndArgv(runWithRuntime, t.RosaRuntime, Cmd, &argv) + 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 := test.RunWithOutputCaptureAndArgv(runWithRuntime, t.RosaRuntime, Cmd, &argv) + 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 := test.RunWithOutputCaptureAndArgv(runWithRuntime, t.RosaRuntime, Cmd, &argv) + Expect(err).To(HaveOccurred()) + Expect(stderr).To(BeEmpty()) + Expect(stdout).To(BeEmpty()) + 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 := test.RunWithOutputCaptureAndArgv(runWithRuntime, t.RosaRuntime, Cmd, &argv) + Expect(err).To(HaveOccurred()) + Expect(stderr).To(BeEmpty()) + Expect(stdout).To(BeEmpty()) + Expect(err.Error()).To(ContainSubstring("forbidden")) + }) }) Context("handleClusterDelete", func() {