1
0
Fork 0
mirror of https://github.com/kyverno/kyverno.git synced 2025-03-05 15:37:19 +00:00
kyverno/pkg/controllers/webhook/cleanup.go
Vishal Choudhary c0d6eaddb3
feat: delete webhook configurations after kyverno is uninstalled (#10782)
* feat: delete webhook configurations after kyverno is uninstalled

Signed-off-by: Vishal Choudhary <vishal.choudhary@nirmata.com>

* feat: optionally add permissions

Signed-off-by: Vishal Choudhary <vishal.choudhary@nirmata.com>

* fix: linter

Signed-off-by: Vishal Choudhary <vishal.choudhary@nirmata.com>

* fix: disable finalizers in latest manifest

Signed-off-by: Vishal Choudhary <vishal.choudhary@nirmata.com>

* fix: move webhook cleanup to webhooks controller

Signed-off-by: Vishal Choudhary <vishal.choudhary@nirmata.com>

* feat: add finalizers on deployment

Signed-off-by: Vishal Choudhary <vishal.choudhary@nirmata.com>

* fix: refactor

Signed-off-by: Vishal Choudhary <vishal.choudhary@nirmata.com>

* feat: add roles to cleanupcontroller

Signed-off-by: Vishal Choudhary <vishal.choudhary@nirmata.com>

* feat: add cleanup to generic controllers

Signed-off-by: Vishal Choudhary <vishal.choudhary@nirmata.com>

* feat: add webhook cleanup in generic controllers

Signed-off-by: Vishal Choudhary <vishal.choudhary@nirmata.com>

* feat: remove unnecessary clusterrole and clusterrole bindings

Signed-off-by: Vishal Choudhary <vishal.choudhary@nirmata.com>

* feat: make this behaviour opt-in

Signed-off-by: Vishal Choudhary <vishal.choudhary@nirmata.com>

* feat: reconcile webhook setup on deployment change

Signed-off-by: Vishal Choudhary <vishal.choudhary@nirmata.com>

* fix: update codegen and remove unused vars

Signed-off-by: Vishal Choudhary <vishal.choudhary@nirmata.com>

* feat: add finalizers to chart

Signed-off-by: Vishal Choudhary <vishal.choudhary@nirmata.com>

---------

Signed-off-by: Vishal Choudhary <vishal.choudhary@nirmata.com>
Co-authored-by: Jim Bugwadia <jim@nirmata.com>
Co-authored-by: shuting <shuting@nirmata.com>
2024-09-04 10:59:59 +00:00

282 lines
10 KiB
Go

package webhook
import (
"context"
"github.com/go-logr/logr"
"github.com/kyverno/kyverno/pkg/config"
controllerutils "github.com/kyverno/kyverno/pkg/utils/controller"
rbacv1 "k8s.io/api/rbac/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/util/retry"
)
// WebhookCleanupSetup creates temporary rbac owned by kyverno resources, these roles and cluster roles get automatically deleted when kyverno is uninstalled
// It creates the following resources:
// 1. Creates a temporary cluster role binding to give permission to delete kyverno's cluster role and set its owner ref to aggregated cluster role itself.
// 2. Creates a temporary role and role binding with permissions to delete a service account, roles and role bindings with owner ref set to the service account.
func WebhookCleanupSetup(
kubeClient kubernetes.Interface,
finalizer string,
) func(context.Context, logr.Logger) error {
return func(ctx context.Context, logger logr.Logger) error {
name := config.KyvernoRoleName()
coreName := name + ":core"
tempRbacName := name + ":temporary"
// create temporary rbac
cr, err := kubeClient.RbacV1().ClusterRoles().Get(ctx, coreName, metav1.GetOptions{})
if err != nil {
logger.Error(err, "failed to get cluster role binding")
return err
}
coreClusterRoleBinding := &rbacv1.ClusterRoleBinding{
ObjectMeta: metav1.ObjectMeta{
Name: coreName,
OwnerReferences: []metav1.OwnerReference{
{
APIVersion: "rbac.authorization.k8s.io/v1",
Kind: "ClusterRole",
Name: cr.Name,
UID: cr.UID,
},
},
},
Subjects: []rbacv1.Subject{
{
Kind: "ServiceAccount",
Name: config.KyvernoServiceAccountName(),
Namespace: config.KyvernoNamespace(),
APIGroup: "",
},
},
RoleRef: rbacv1.RoleRef{
APIGroup: "rbac.authorization.k8s.io",
Kind: "ClusterRole",
Name: coreName,
},
}
if crb, err := kubeClient.RbacV1().ClusterRoleBindings().Create(ctx, coreClusterRoleBinding, metav1.CreateOptions{}); err != nil && !apierrors.IsAlreadyExists(err) {
logger.Error(err, "failed to create temporary clusterrolebinding", "name", crb.Name)
return err
} else if !apierrors.IsAlreadyExists(err) {
logger.V(4).Info("temporary clusterrolebinding created", "clusterrolebinding", crb.Name)
}
// create temporary rbac
sa, err := kubeClient.CoreV1().ServiceAccounts(config.KyvernoNamespace()).Get(ctx, config.KyvernoServiceAccountName(), metav1.GetOptions{})
if err != nil {
logger.Error(err, "failed to get service account")
return err
}
role := &rbacv1.Role{
ObjectMeta: metav1.ObjectMeta{
Name: tempRbacName,
Namespace: config.KyvernoNamespace(),
OwnerReferences: []metav1.OwnerReference{
{
APIVersion: "v1",
Kind: "ServiceAccount",
Name: sa.Name,
UID: sa.UID,
},
},
},
Rules: []rbacv1.PolicyRule{
{
APIGroups: []string{""},
Resources: []string{"serviceaccounts"},
ResourceNames: []string{config.KyvernoServiceAccountName()},
Verbs: []string{"get", "update", "delete"},
},
{
APIGroups: []string{"rbac.authorization.k8s.io"},
Resources: []string{"rolebindings", "roles"},
ResourceNames: []string{name},
Verbs: []string{"get", "update"},
},
{
APIGroups: []string{"apps"},
Resources: []string{"deployments"},
ResourceNames: []string{config.KyvernoDeploymentName()},
Verbs: []string{"get", "update"},
},
},
}
if r, err := kubeClient.RbacV1().Roles(config.KyvernoNamespace()).Create(ctx, role, metav1.CreateOptions{}); err != nil && !apierrors.IsAlreadyExists(err) {
logger.Error(err, "failed to create temporary role", "name", r.Name)
return err
} else if !apierrors.IsAlreadyExists(err) {
logger.V(4).Info("temporary role created in kyverno namespace", "role", r.Name, "namespace", r.Namespace)
}
roleBinding := &rbacv1.RoleBinding{
ObjectMeta: metav1.ObjectMeta{
Name: tempRbacName,
Namespace: config.KyvernoNamespace(),
OwnerReferences: []metav1.OwnerReference{
{
APIVersion: "v1",
Kind: "ServiceAccount",
Name: sa.Name,
UID: sa.UID,
},
},
},
Subjects: []rbacv1.Subject{
{
Kind: "ServiceAccount",
Name: config.KyvernoServiceAccountName(),
Namespace: config.KyvernoNamespace(),
APIGroup: "",
},
},
RoleRef: rbacv1.RoleRef{
APIGroup: "rbac.authorization.k8s.io",
Kind: "Role",
Name: tempRbacName,
},
}
if rb, err := kubeClient.RbacV1().RoleBindings(config.KyvernoNamespace()).Create(ctx, roleBinding, metav1.CreateOptions{}); err != nil && !apierrors.IsAlreadyExists(err) {
logger.Error(err, "failed to create temporary rolebinding", "name", rb.Name)
return err
} else if !apierrors.IsAlreadyExists(err) {
logger.V(4).Info("temporary rolebinding created in kyverno namespace", "rolebinding", rb.Name, "namespace", rb.Namespace)
}
// Add finalizers
if err := AddFinalizers(ctx, kubeClient.RbacV1().ClusterRoles(), coreName, finalizer); err != nil && !apierrors.IsNotFound(err) {
logger.Error(err, "failed to add finalizer to clusterrole", "name", coreName)
return err
}
if err := AddFinalizers(ctx, kubeClient.RbacV1().RoleBindings(config.KyvernoNamespace()), name, finalizer); err != nil {
logger.Error(err, "failed to add finalizer to rolebindings", "name", name, "namespace", config.KyvernoNamespace())
return err
}
if err := AddFinalizers(ctx, kubeClient.RbacV1().Roles(config.KyvernoNamespace()), name, finalizer); err != nil {
logger.Error(err, "failed to add finalizer to role", "name", name, "namespace", config.KyvernoNamespace())
return err
}
if err := AddFinalizers(ctx, kubeClient.AppsV1().Deployments(config.KyvernoNamespace()), config.KyvernoDeploymentName(), finalizer); err != nil {
logger.Error(err, "failed to add finalizer to deployment", "name", config.KyvernoDeploymentName(), "namespace", config.KyvernoNamespace())
return err
}
if err := AddFinalizers(ctx, kubeClient.CoreV1().ServiceAccounts(config.KyvernoNamespace()), config.KyvernoServiceAccountName(), finalizer); err != nil {
logger.Error(err, "failed to add finalizer to serviceaccount", "name", config.KyvernoServiceAccountName(), "namespace", config.KyvernoNamespace())
return err
}
return nil
}
}
// WebhookCleanupHandler is run after webhook configuration cleanup is performed to delete roles and service account.
// Admission controller cluster and namespaced roles and role bindings have finalizers to block their deletion until admission controller terminates.
// This handler removes the finalizers on roles and service account after they are used to cleanup webhook cfg.
// It does the following:
//
// Deletes the cluster scoped rbac in order:
// a. Removes finalizers from controller cluster role binding
// b. Removes finalizers from controller core cluster role
// c. Removes finalizers from controller aggregated cluster role
// d. Temporary cluster role and cluster role binding created by WebhookCleanupSetup gets garbage collected after (c) automatically
//
// Deletes the namespace scoped rbac in order:
// a. Removes finalizers from controller role binding.
// b. Removes finalizers from controller role.
// c. Removes finalizers from controller service account
// d. Temporary role and role binding created by WebhookCleanupSetup gets garbage collected after (c) automatically
func WebhookCleanupHandler(
kubeClient kubernetes.Interface,
finalizer string,
) func(context.Context, logr.Logger) error {
return func(ctx context.Context, logger logr.Logger) error {
name := config.KyvernoRoleName()
coreName := name + ":core"
// cleanup cluster scoped rbac
if err := DeleteFinalizers(ctx, kubeClient.RbacV1().ClusterRoles(), coreName, finalizer); err != nil {
logger.Error(err, "failed to delete finalizer from clusterrole", "name", coreName)
return err
}
// cleanup namespace scoped rbac
if err := DeleteFinalizers(ctx, kubeClient.RbacV1().RoleBindings(config.KyvernoNamespace()), name, finalizer); err != nil {
logger.Error(err, "failed to delete finalizer from rolebindings", "name", name, "namespace", config.KyvernoNamespace())
return err
}
if err := DeleteFinalizers(ctx, kubeClient.RbacV1().Roles(config.KyvernoNamespace()), name, finalizer); err != nil {
logger.Error(err, "failed to delete finalizer from role", "name", name, "namespace", config.KyvernoNamespace())
return err
}
if err := DeleteFinalizers(ctx, kubeClient.AppsV1().Deployments(config.KyvernoNamespace()), config.KyvernoDeploymentName(), finalizer); err != nil {
logger.Error(err, "failed to delete finalizer from deployment", "name", config.KyvernoDeploymentName(), "namespace", config.KyvernoNamespace())
return err
}
if err := DeleteFinalizers(ctx, kubeClient.CoreV1().ServiceAccounts(config.KyvernoNamespace()), config.KyvernoServiceAccountName(), finalizer); err != nil {
logger.Error(err, "failed to delete finalizer from serviceaccount", "name", config.KyvernoServiceAccountName(), "namespace", config.KyvernoNamespace())
return err
}
return nil
}
}
func DeleteFinalizers[T metav1.Object](ctx context.Context, client controllerutils.ObjectClient[T], name, finalizer string) error {
return retry.RetryOnConflict(retry.DefaultRetry, func() error {
obj, err := client.Get(ctx, name, metav1.GetOptions{})
if err != nil {
return err
}
finalizers := make([]string, 0)
for _, f := range obj.GetFinalizers() {
if f != finalizer {
finalizers = append(finalizers, f)
}
}
obj.SetFinalizers(finalizers)
_, err = client.Update(ctx, obj, metav1.UpdateOptions{})
if err != nil {
return err
}
return nil
})
}
func AddFinalizers[T metav1.Object](ctx context.Context, client controllerutils.ObjectClient[T], name, finalizer string) error {
return retry.RetryOnConflict(retry.DefaultRetry, func() error {
obj, err := client.Get(ctx, name, metav1.GetOptions{})
if err != nil {
return err
}
finalizers := obj.GetFinalizers()
for _, f := range finalizers {
if f == finalizer {
return nil
}
}
finalizers = append(finalizers, finalizer)
obj.SetFinalizers(finalizers)
_, err = client.Update(ctx, obj, metav1.UpdateOptions{})
if err != nil {
return err
}
return nil
})
}