diff --git a/CHANGELOG.md b/CHANGELOG.md index 23b0621ec1..5f86399f0b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,8 @@ IMPROVEMENTS: * Control Plane * TLS: Support PKCS1 and PKCS8 private keys for Consul certificate authority. [[GH-843](https://github.com/hashicorp/consul-k8s/pull/843)] +* CLI + * Delete jobs, cluster roles, and cluster role bindings on `uninstall`. [[GH-820](https://github.com/hashicorp/consul-k8s/pull/820)] BUG FIXES: * Control Plane diff --git a/cli/cmd/uninstall/uninstall.go b/cli/cmd/uninstall/uninstall.go index 0e218c5d0f..0e85ea4eef 100644 --- a/cli/cmd/uninstall/uninstall.go +++ b/cli/cmd/uninstall/uninstall.go @@ -257,10 +257,11 @@ func (c *Command) Run(args []string) int { c.UI.Output("Name: %s", foundReleaseName, terminal.WithInfoStyle()) c.UI.Output("Namespace %s", foundReleaseNamespace, terminal.WithInfoStyle()) } - // Prompt with a warning for approval before deleting PVCs, Secrets, Service Accounts, Roles, and Role Bindings. + // Prompt with a warning for approval before deleting PVCs, Secrets, Service Accounts, Roles, Role Bindings, + // Jobs, Cluster Roles, and Cluster Role Bindings. if !c.flagAutoApprove { confirmation, err := c.UI.Input(&terminal.Input{ - Prompt: fmt.Sprintf("WARNING: Proceed with deleting PVCs, Secrets, Service Accounts, Roles, and Role Bindings for the following installation? \n\n Name: %s \n Namespace: %s \n\n Only approve if all data from this installation can be deleted. (y/N)", foundReleaseName, foundReleaseNamespace), + Prompt: fmt.Sprintf("WARNING: Proceed with deleting PVCs, Secrets, Service Accounts, Roles, Role Bindings, Jobs, Cluster Roles, and Cluster Role Bindings for the following installation? \n\n Name: %s \n Namespace: %s \n\n Only approve if all data from this installation can be deleted. (y/N)", foundReleaseName, foundReleaseNamespace), Style: terminal.WarningStyle, Secret: false, }) @@ -299,6 +300,21 @@ func (c *Command) Run(args []string) int { return 1 } + if err := c.deleteJobs(foundReleaseName, foundReleaseNamespace); err != nil { + c.UI.Output(err.Error(), terminal.WithErrorStyle()) + return 1 + } + + if err := c.deleteClusterRoles(foundReleaseName); err != nil { + c.UI.Output(err.Error(), terminal.WithErrorStyle()) + return 1 + } + + if err := c.deleteClusterRoleBindings(foundReleaseName); err != nil { + c.UI.Output(err.Error(), terminal.WithErrorStyle()) + return 1 + } + return 0 } @@ -476,3 +492,87 @@ func (c *Command) deleteRoleBindings(foundReleaseName, foundReleaseNamespace str } return nil } + +// deleteJobs deletes jobs that have the label release={{foundReleaseName}}. +func (c *Command) deleteJobs(foundReleaseName, foundReleaseNamespace string) error { + var jobNames []string + jobSelector := metav1.ListOptions{LabelSelector: fmt.Sprintf("release=%s", foundReleaseName)} + jobs, err := c.kubernetes.BatchV1().Jobs(foundReleaseNamespace).List(c.Ctx, jobSelector) + if err != nil { + return fmt.Errorf("deleteJobs: %s", err) + } + if len(jobs.Items) == 0 { + c.UI.Output("No Consul jobs found.", terminal.WithSuccessStyle()) + return nil + } + for _, job := range jobs.Items { + err := c.kubernetes.BatchV1().Jobs(foundReleaseNamespace).Delete(c.Ctx, job.Name, metav1.DeleteOptions{}) + if err != nil { + return fmt.Errorf("deleteJobs: error deleting Job %q: %s", job.Name, err) + } + jobNames = append(jobNames, job.Name) + } + if len(jobNames) > 0 { + for _, job := range jobNames { + c.UI.Output("Deleted Jobs => %s", job, terminal.WithSuccessStyle()) + } + c.UI.Output("Consul jobs deleted.", terminal.WithSuccessStyle()) + } + return nil +} + +// deleteClusterRoles deletes clusterRoles that have the label release={{foundReleaseName}}. +func (c *Command) deleteClusterRoles(foundReleaseName string) error { + var clusterRolesNames []string + clusterRolesSelector := metav1.ListOptions{LabelSelector: fmt.Sprintf("release=%s", foundReleaseName)} + clusterRoles, err := c.kubernetes.RbacV1().ClusterRoles().List(c.Ctx, clusterRolesSelector) + if err != nil { + return fmt.Errorf("deleteClusterRoles: %s", err) + } + if len(clusterRoles.Items) == 0 { + c.UI.Output("No Consul cluster roles found.", terminal.WithSuccessStyle()) + return nil + } + for _, clusterRole := range clusterRoles.Items { + err := c.kubernetes.RbacV1().ClusterRoles().Delete(c.Ctx, clusterRole.Name, metav1.DeleteOptions{}) + if err != nil { + return fmt.Errorf("deleteClusterRoles: error deleting cluster role %q: %s", clusterRole.Name, err) + } + clusterRolesNames = append(clusterRolesNames, clusterRole.Name) + } + if len(clusterRolesNames) > 0 { + for _, clusterRole := range clusterRolesNames { + c.UI.Output("Deleted cluster role => %s", clusterRole, terminal.WithSuccessStyle()) + } + c.UI.Output("Consul cluster roles deleted.", terminal.WithSuccessStyle()) + } + return nil +} + +// deleteClusterRoleBindings deletes clusterrolebindings that have the label release={{foundReleaseName}}. +func (c *Command) deleteClusterRoleBindings(foundReleaseName string) error { + var clusterRoleBindingsNames []string + clusterRoleBindingsSelector := metav1.ListOptions{LabelSelector: fmt.Sprintf("release=%s", foundReleaseName)} + clusterRoleBindings, err := c.kubernetes.RbacV1().ClusterRoleBindings().List(c.Ctx, clusterRoleBindingsSelector) + if err != nil { + return fmt.Errorf("deleteClusterRoleBindings: %s", err) + } + if len(clusterRoleBindings.Items) == 0 { + c.UI.Output("No Consul cluster role bindings found.", terminal.WithSuccessStyle()) + return nil + } + for _, clusterRoleBinding := range clusterRoleBindings.Items { + err := c.kubernetes.RbacV1().ClusterRoleBindings().Delete(c.Ctx, clusterRoleBinding.Name, metav1.DeleteOptions{}) + if err != nil { + return fmt.Errorf("deleteClusterRoleBindings: error deleting cluster role binding %q: %s", clusterRoleBinding.Name, err) + } + clusterRoleBindingsNames = append(clusterRoleBindingsNames, clusterRoleBinding.Name) + } + if len(clusterRoleBindingsNames) > 0 { + for _, clusterRoleBinding := range clusterRoleBindingsNames { + c.UI.Output("Deleted cluster role binding => %s", clusterRoleBinding, terminal.WithSuccessStyle()) + } + c.UI.Output("Consul cluster role bindings deleted.", terminal.WithSuccessStyle()) + } + return nil +} diff --git a/cli/cmd/uninstall/uninstall_test.go b/cli/cmd/uninstall/uninstall_test.go index 95696b481c..6e46f51506 100644 --- a/cli/cmd/uninstall/uninstall_test.go +++ b/cli/cmd/uninstall/uninstall_test.go @@ -8,6 +8,7 @@ import ( "github.com/hashicorp/consul-k8s/cli/cmd/common" "github.com/hashicorp/go-hclog" "github.com/stretchr/testify/require" + batchv1 "k8s.io/api/batch/v1" v1 "k8s.io/api/core/v1" rbacv1 "k8s.io/api/rbac/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -52,6 +53,7 @@ func TestDeletePVCs(t *testing.T) { pvcs, err := c.kubernetes.CoreV1().PersistentVolumeClaims("default").List(context.Background(), metav1.ListOptions{}) require.NoError(t, err) require.Len(t, pvcs.Items, 1) + require.Equal(t, pvcs.Items[0].Name, pvc3.Name) } func TestDeleteSecrets(t *testing.T) { @@ -73,15 +75,26 @@ func TestDeleteSecrets(t *testing.T) { }, }, } + secret3 := &v1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "unrelated-test-secret3", + Labels: map[string]string{ + "release": "unrelated", + }, + }, + } _, err := c.kubernetes.CoreV1().Secrets("default").Create(context.Background(), secret, metav1.CreateOptions{}) require.NoError(t, err) _, err = c.kubernetes.CoreV1().Secrets("default").Create(context.Background(), secret2, metav1.CreateOptions{}) require.NoError(t, err) + _, err = c.kubernetes.CoreV1().Secrets("default").Create(context.Background(), secret3, metav1.CreateOptions{}) + require.NoError(t, err) err = c.deleteSecrets("consul", "default") require.NoError(t, err) secrets, err := c.kubernetes.CoreV1().Secrets("default").List(context.Background(), metav1.ListOptions{}) require.NoError(t, err) - require.Len(t, secrets.Items, 0) + require.Len(t, secrets.Items, 1) + require.Equal(t, secrets.Items[0].Name, secret3.Name) } func TestDeleteServiceAccounts(t *testing.T) { @@ -103,15 +116,26 @@ func TestDeleteServiceAccounts(t *testing.T) { }, }, } + sa3 := &v1.ServiceAccount{ + ObjectMeta: metav1.ObjectMeta{ + Name: "consul-test-sa3", + Labels: map[string]string{ + "release": "unrelated", + }, + }, + } _, err := c.kubernetes.CoreV1().ServiceAccounts("default").Create(context.Background(), sa, metav1.CreateOptions{}) require.NoError(t, err) _, err = c.kubernetes.CoreV1().ServiceAccounts("default").Create(context.Background(), sa2, metav1.CreateOptions{}) require.NoError(t, err) + _, err = c.kubernetes.CoreV1().ServiceAccounts("default").Create(context.Background(), sa3, metav1.CreateOptions{}) + require.NoError(t, err) err = c.deleteServiceAccounts("consul", "default") require.NoError(t, err) sas, err := c.kubernetes.CoreV1().ServiceAccounts("default").List(context.Background(), metav1.ListOptions{}) require.NoError(t, err) - require.Len(t, sas.Items, 0) + require.Len(t, sas.Items, 1) + require.Equal(t, sas.Items[0].Name, sa3.Name) } func TestDeleteRoles(t *testing.T) { @@ -133,15 +157,26 @@ func TestDeleteRoles(t *testing.T) { }, }, } + role3 := &rbacv1.Role{ + ObjectMeta: metav1.ObjectMeta{ + Name: "consul-test-role3", + Labels: map[string]string{ + "release": "unrelated", + }, + }, + } _, err := c.kubernetes.RbacV1().Roles("default").Create(context.Background(), role, metav1.CreateOptions{}) require.NoError(t, err) _, err = c.kubernetes.RbacV1().Roles("default").Create(context.Background(), role2, metav1.CreateOptions{}) require.NoError(t, err) + _, err = c.kubernetes.RbacV1().Roles("default").Create(context.Background(), role3, metav1.CreateOptions{}) + require.NoError(t, err) err = c.deleteRoles("consul", "default") require.NoError(t, err) roles, err := c.kubernetes.RbacV1().Roles("default").List(context.Background(), metav1.ListOptions{}) require.NoError(t, err) - require.Len(t, roles.Items, 0) + require.Len(t, roles.Items, 1) + require.Equal(t, roles.Items[0].Name, role3.Name) } func TestDeleteRoleBindings(t *testing.T) { @@ -163,15 +198,149 @@ func TestDeleteRoleBindings(t *testing.T) { }, }, } + rolebinding3 := &rbacv1.RoleBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: "consul-test-role3", + Labels: map[string]string{ + "release": "unrelated", + }, + }, + } _, err := c.kubernetes.RbacV1().RoleBindings("default").Create(context.Background(), rolebinding, metav1.CreateOptions{}) require.NoError(t, err) _, err = c.kubernetes.RbacV1().RoleBindings("default").Create(context.Background(), rolebinding2, metav1.CreateOptions{}) require.NoError(t, err) + _, err = c.kubernetes.RbacV1().RoleBindings("default").Create(context.Background(), rolebinding3, metav1.CreateOptions{}) + require.NoError(t, err) err = c.deleteRoleBindings("consul", "default") require.NoError(t, err) rolebindings, err := c.kubernetes.RbacV1().RoleBindings("default").List(context.Background(), metav1.ListOptions{}) require.NoError(t, err) - require.Len(t, rolebindings.Items, 0) + require.Len(t, rolebindings.Items, 1) + require.Equal(t, rolebindings.Items[0].Name, rolebinding3.Name) +} + +func TestDeleteJobs(t *testing.T) { + c := getInitializedCommand(t) + c.kubernetes = fake.NewSimpleClientset() + job := &batchv1.Job{ + ObjectMeta: metav1.ObjectMeta{ + Name: "consul-test-job1", + Labels: map[string]string{ + "release": "consul", + }, + }, + } + job2 := &batchv1.Job{ + ObjectMeta: metav1.ObjectMeta{ + Name: "consul-test-job2", + Labels: map[string]string{ + "release": "consul", + }, + }, + } + job3 := &batchv1.Job{ + ObjectMeta: metav1.ObjectMeta{ + Name: "consul-test-job3", + Labels: map[string]string{ + "release": "unrelated", + }, + }, + } + _, err := c.kubernetes.BatchV1().Jobs("default").Create(context.Background(), job, metav1.CreateOptions{}) + require.NoError(t, err) + _, err = c.kubernetes.BatchV1().Jobs("default").Create(context.Background(), job2, metav1.CreateOptions{}) + require.NoError(t, err) + _, err = c.kubernetes.BatchV1().Jobs("default").Create(context.Background(), job3, metav1.CreateOptions{}) + require.NoError(t, err) + err = c.deleteJobs("consul", "default") + require.NoError(t, err) + jobs, err := c.kubernetes.BatchV1().Jobs("default").List(context.Background(), metav1.ListOptions{}) + require.NoError(t, err) + require.Len(t, jobs.Items, 1) + require.Equal(t, jobs.Items[0].Name, job3.Name) +} + +func TestDeleteClusterRoles(t *testing.T) { + c := getInitializedCommand(t) + c.kubernetes = fake.NewSimpleClientset() + clusterrole := &rbacv1.ClusterRole{ + ObjectMeta: metav1.ObjectMeta{ + Name: "consul-test-clusterrole1", + Labels: map[string]string{ + "release": "consul", + }, + }, + } + clusterrole2 := &rbacv1.ClusterRole{ + ObjectMeta: metav1.ObjectMeta{ + Name: "consul-test-clusterrole2", + Labels: map[string]string{ + "release": "consul", + }, + }, + } + clusterrole3 := &rbacv1.ClusterRole{ + ObjectMeta: metav1.ObjectMeta{ + Name: "consul-test-clusterrole3", + Labels: map[string]string{ + "release": "unrelated", + }, + }, + } + _, err := c.kubernetes.RbacV1().ClusterRoles().Create(context.Background(), clusterrole, metav1.CreateOptions{}) + require.NoError(t, err) + _, err = c.kubernetes.RbacV1().ClusterRoles().Create(context.Background(), clusterrole2, metav1.CreateOptions{}) + require.NoError(t, err) + _, err = c.kubernetes.RbacV1().ClusterRoles().Create(context.Background(), clusterrole3, metav1.CreateOptions{}) + require.NoError(t, err) + err = c.deleteClusterRoles("consul") + require.NoError(t, err) + clusterroles, err := c.kubernetes.RbacV1().ClusterRoles().List(context.Background(), metav1.ListOptions{}) + require.NoError(t, err) + require.Len(t, clusterroles.Items, 1) + require.Equal(t, clusterroles.Items[0].Name, clusterrole3.Name) +} + +func TestDeleteClusterRoleBindings(t *testing.T) { + c := getInitializedCommand(t) + c.kubernetes = fake.NewSimpleClientset() + clusterrolebinding := &rbacv1.ClusterRoleBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: "consul-test-clusterrolebinding1", + Labels: map[string]string{ + "release": "consul", + }, + }, + } + clusterrolebinding2 := &rbacv1.ClusterRoleBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: "consul-test-clusterrolebinding2", + Labels: map[string]string{ + "release": "consul", + }, + }, + } + clusterrolebinding3 := &rbacv1.ClusterRoleBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: "consul-test-clusterrolebinding3", + Labels: map[string]string{ + "release": "unrelated", + }, + }, + } + _, err := c.kubernetes.RbacV1().ClusterRoleBindings().Create(context.Background(), clusterrolebinding, metav1.CreateOptions{}) + require.NoError(t, err) + _, err = c.kubernetes.RbacV1().ClusterRoleBindings().Create(context.Background(), clusterrolebinding2, metav1.CreateOptions{}) + require.NoError(t, err) + _, err = c.kubernetes.RbacV1().ClusterRoleBindings().Create(context.Background(), clusterrolebinding3, metav1.CreateOptions{}) + require.NoError(t, err) + err = c.deleteClusterRoleBindings("consul") + require.NoError(t, err) + clusterrolebindings, err := c.kubernetes.RbacV1().ClusterRoleBindings().List(context.Background(), metav1.ListOptions{}) + require.NoError(t, err) + require.Len(t, clusterrolebindings.Items, 1) + require.Equal(t, clusterrolebindings.Items[0].Name, clusterrolebinding3.Name) } // getInitializedCommand sets up a command struct for tests.