From 65aaa1d69a68a0e41013ca2a58cb1ac094ad3cce Mon Sep 17 00:00:00 2001 From: Stefan Prodan Date: Mon, 31 Jan 2022 13:18:58 +0200 Subject: [PATCH] Ensure object are finalized under impersonation If the service account used for impersonation has been deleted, skip pruning, log the error and continue with finalization to allow tenants removals from clusters. Signed-off-by: Stefan Prodan --- controllers/kustomization_controller.go | 18 +++++++----- controllers/kustomization_impersonation.go | 29 +++++++++++++++++++ .../kustomization_impersonation_test.go | 20 +++++++++++++ 3 files changed, 60 insertions(+), 7 deletions(-) diff --git a/controllers/kustomization_controller.go b/controllers/kustomization_controller.go index dc6f5198..60d047b4 100644 --- a/controllers/kustomization_controller.go +++ b/controllers/kustomization_controller.go @@ -922,13 +922,12 @@ func (r *KustomizationReconciler) finalize(ctx context.Context, kustomization ku objects, _ := ListObjectsInInventory(kustomization.Status.Inventory) impersonation := NewKustomizeImpersonation(kustomization, r.Client, r.StatusPoller, r.DefaultServiceAccount) - kubeClient, _, err := impersonation.GetClient(ctx) - if err != nil { - // when impersonation fails, log the stale objects and continue with the finalization - msg := fmt.Sprintf("unable to prune objects: \n%s", ssa.FmtUnstructuredList(objects)) - log.Error(fmt.Errorf("failed to build kube client: %w", err), msg) - r.event(ctx, kustomization, kustomization.Status.LastAppliedRevision, events.EventSeverityError, msg, nil) - } else { + if impersonation.CanFinalize(ctx) { + kubeClient, _, err := impersonation.GetClient(ctx) + if err != nil { + return ctrl.Result{}, err + } + resourceManager := ssa.NewResourceManager(kubeClient, nil, ssa.Owner{ Field: r.ControllerName, Group: kustomizev1.GroupVersion.Group, @@ -953,6 +952,11 @@ func (r *KustomizationReconciler) finalize(ctx context.Context, kustomization ku if changeSet != nil && len(changeSet.Entries) > 0 { r.event(ctx, kustomization, kustomization.Status.LastAppliedRevision, events.EventSeverityInfo, changeSet.String(), nil) } + } else { + // when the account to impersonate is gone, log the stale objects and continue with the finalization + msg := fmt.Sprintf("unable to prune objects: \n%s", ssa.FmtUnstructuredList(objects)) + log.Error(fmt.Errorf("skiping pruning, failed to find account to impersonate"), msg) + r.event(ctx, kustomization, kustomization.Status.LastAppliedRevision, events.EventSeverityError, msg, nil) } } diff --git a/controllers/kustomization_impersonation.go b/controllers/kustomization_impersonation.go index 8b646967..a5e75c12 100644 --- a/controllers/kustomization_impersonation.go +++ b/controllers/kustomization_impersonation.go @@ -19,7 +19,9 @@ package controllers import ( "context" "fmt" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" "k8s.io/client-go/rest" "k8s.io/client-go/tools/clientcmd" @@ -69,6 +71,33 @@ func (ki *KustomizeImpersonation) GetClient(ctx context.Context) (client.Client, } } +// CanFinalize asserts if the given Kustomization can be finalized using impersonation. +func (ki *KustomizeImpersonation) CanFinalize(ctx context.Context) bool { + name := ki.defaultServiceAccount + if sa := ki.kustomization.Spec.ServiceAccountName; sa != "" { + name = sa + } + if name == "" { + return true + } + + sa := &corev1.ServiceAccount{ + TypeMeta: metav1.TypeMeta{ + Kind: "ServiceAccount", + APIVersion: "v1", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: ki.kustomization.Namespace, + }, + } + if err := ki.Client.Get(ctx, client.ObjectKeyFromObject(sa), sa); err != nil { + return false + } + + return true +} + func (ki *KustomizeImpersonation) setImpersonationConfig(restConfig *rest.Config) { name := ki.defaultServiceAccount if sa := ki.kustomization.Spec.ServiceAccountName; sa != "" { diff --git a/controllers/kustomization_impersonation_test.go b/controllers/kustomization_impersonation_test.go index 02abf4a7..50341a91 100644 --- a/controllers/kustomization_impersonation_test.go +++ b/controllers/kustomization_impersonation_test.go @@ -29,6 +29,7 @@ import ( . "github.com/onsi/gomega" corev1 "k8s.io/api/core/v1" rbacv1 "k8s.io/api/rbac/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" apimeta "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" @@ -101,6 +102,7 @@ data: Kind: sourcev1.GitRepositoryKind, }, TargetNamespace: id, + Prune: true, }, } @@ -187,4 +189,22 @@ data: g.Expect(readyCondition.Reason).To(Equal(meta.ReconciliationSucceededReason)) }) + + t.Run("can finalize impersonating service account", func(t *testing.T) { + saK := &kustomizev1.Kustomization{} + err = k8sClient.Get(context.Background(), client.ObjectKeyFromObject(kustomization), saK) + g.Expect(err).NotTo(HaveOccurred()) + + err = k8sClient.Delete(context.Background(), saK) + g.Expect(err).NotTo(HaveOccurred()) + + g.Eventually(func() bool { + err := k8sClient.Get(context.Background(), client.ObjectKeyFromObject(kustomization), resultK) + return apierrors.IsNotFound(err) + }, timeout, time.Second).Should(BeTrue()) + + resultConfig := &corev1.ConfigMap{} + err := k8sClient.Get(context.Background(), types.NamespacedName{Name: id, Namespace: id}, resultConfig) + g.Expect(apierrors.IsNotFound(err)).To(BeTrue()) + }) }