Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 20 additions & 5 deletions cmd/dlt/cluster/cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -80,23 +80,36 @@ 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 {
r.Reporter.Warnf("Deleting cluster '%s' with 'best effort' means that certain resources may be left behind"+
" 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)
Comment thread
amandahla marked this conversation as resolved.
}

if cluster.AWS().STS().RoleARN() != "" {
Expand All @@ -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 {
Expand Down
297 changes: 297 additions & 0 deletions cmd/dlt/cluster/cmd_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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() {
Comment thread
olucasfreitas marked this conversation as resolved.
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() {
Expand Down