github.com/juju/juju@v0.0.0-20240430160146-1752b71fcf00/caas/kubernetes/provider/resources/clusterrolebinding.go (about)

     1  // Copyright 2021 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package resources
     5  
     6  import (
     7  	"context"
     8  	"time"
     9  
    10  	"github.com/juju/errors"
    11  	corev1 "k8s.io/api/core/v1"
    12  	rbacv1 "k8s.io/api/rbac/v1"
    13  	k8serrors "k8s.io/apimachinery/pkg/api/errors"
    14  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    15  	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
    16  	"k8s.io/apimachinery/pkg/runtime"
    17  	"k8s.io/apimachinery/pkg/types"
    18  	"k8s.io/client-go/kubernetes"
    19  	"k8s.io/utils/pointer"
    20  
    21  	k8sconstants "github.com/juju/juju/caas/kubernetes/provider/constants"
    22  	"github.com/juju/juju/caas/kubernetes/provider/utils"
    23  	"github.com/juju/juju/core/status"
    24  )
    25  
    26  // ClusterRoleBinding extends the k8s cluster role binding.
    27  type ClusterRoleBinding struct {
    28  	rbacv1.ClusterRoleBinding
    29  }
    30  
    31  // NewClusterRoleBinding creates a new role resource.
    32  func NewClusterRoleBinding(name string, in *rbacv1.ClusterRoleBinding) *ClusterRoleBinding {
    33  	if in == nil {
    34  		in = &rbacv1.ClusterRoleBinding{}
    35  	}
    36  	in.SetName(name)
    37  	return &ClusterRoleBinding{*in}
    38  }
    39  
    40  // Clone returns a copy of the resource.
    41  func (rb *ClusterRoleBinding) Clone() Resource {
    42  	clone := *rb
    43  	return &clone
    44  }
    45  
    46  // ID returns a comparable ID for the Resource
    47  func (rb *ClusterRoleBinding) ID() ID {
    48  	return ID{"ClusterRoleBinding", rb.Name, rb.Namespace}
    49  }
    50  
    51  // Apply patches the resource change.
    52  func (rb *ClusterRoleBinding) Apply(ctx context.Context, client kubernetes.Interface) error {
    53  	api := client.RbacV1().ClusterRoleBindings()
    54  	data, err := runtime.Encode(unstructured.UnstructuredJSONScheme, &rb.ClusterRoleBinding)
    55  	if err != nil {
    56  		return errors.Trace(err)
    57  	}
    58  	res, err := api.Patch(ctx, rb.Name, types.StrategicMergePatchType, data, metav1.PatchOptions{
    59  		FieldManager: JujuFieldManager,
    60  	})
    61  	if k8serrors.IsNotFound(err) {
    62  		res, err = api.Create(ctx, &rb.ClusterRoleBinding, metav1.CreateOptions{
    63  			FieldManager: JujuFieldManager,
    64  		})
    65  	}
    66  	if k8serrors.IsConflict(err) {
    67  		return errors.Annotatef(errConflict, "cluster role binding %q", rb.Name)
    68  	}
    69  	if err != nil {
    70  		return errors.Trace(err)
    71  	}
    72  	rb.ClusterRoleBinding = *res
    73  	return nil
    74  }
    75  
    76  // Get refreshes the resource.
    77  func (rb *ClusterRoleBinding) Get(ctx context.Context, client kubernetes.Interface) error {
    78  	api := client.RbacV1().ClusterRoleBindings()
    79  	res, err := api.Get(ctx, rb.Name, metav1.GetOptions{})
    80  	if k8serrors.IsNotFound(err) {
    81  		return errors.NewNotFound(err, "k8s")
    82  	} else if err != nil {
    83  		return errors.Trace(err)
    84  	}
    85  	rb.ClusterRoleBinding = *res
    86  	return nil
    87  }
    88  
    89  // Delete removes the resource.
    90  func (rb *ClusterRoleBinding) Delete(ctx context.Context, client kubernetes.Interface) error {
    91  	api := client.RbacV1().ClusterRoleBindings()
    92  	err := api.Delete(ctx, rb.Name, metav1.DeleteOptions{
    93  		PropagationPolicy:  k8sconstants.DeletePropagationBackground(),
    94  		GracePeriodSeconds: pointer.Int64Ptr(0),
    95  		Preconditions:      utils.NewUIDPreconditions(rb.UID),
    96  	})
    97  	if k8serrors.IsNotFound(err) {
    98  		return nil
    99  	} else if err != nil {
   100  		return errors.Trace(err)
   101  	}
   102  	return nil
   103  }
   104  
   105  // shouldDelete checks if there are any changes in the immutable field to decide
   106  // if the existing cluster role binding should be deleted or not.
   107  func shouldDelete(existing, new rbacv1.ClusterRoleBinding) bool {
   108  	return existing.RoleRef.APIGroup != new.RoleRef.APIGroup ||
   109  		existing.RoleRef.Kind != new.RoleRef.Kind ||
   110  		existing.RoleRef.Name != new.RoleRef.Name
   111  }
   112  
   113  // Ensure ensures this cluster role exists in it's desired form inside the
   114  // cluster. If the object does not exist it's updated and if the object exists
   115  // it's updated. The method also takes an optional set of claims to test the
   116  // existing Kubernetes object with to assert ownership before overwriting it.
   117  func (rb *ClusterRoleBinding) Ensure(
   118  	ctx context.Context,
   119  	client kubernetes.Interface,
   120  	claims ...Claim,
   121  ) ([]func(), error) {
   122  	// TODO(caas): roll this into Apply()
   123  	cleanups := []func(){}
   124  
   125  	existing := ClusterRoleBinding{rb.ClusterRoleBinding}
   126  	err := existing.Get(ctx, client)
   127  	if err != nil && !errors.IsNotFound(err) {
   128  		return cleanups, errors.Annotatef(err, "getting existing cluster role binding %q", rb.Name)
   129  	}
   130  	doUpdate := err == nil
   131  	if err == nil {
   132  		hasClaim, err := RunClaims(claims...).Assert(&existing.ClusterRoleBinding)
   133  		if err != nil {
   134  			return cleanups, errors.Annotatef(err, "checking for existing cluster role binding %q", rb.Name)
   135  		}
   136  		if !hasClaim {
   137  			return cleanups, errors.AlreadyExistsf(
   138  				"cluster role binding %q not controlled by juju", rb.Name)
   139  		}
   140  		if shouldDelete(existing.ClusterRoleBinding, rb.ClusterRoleBinding) {
   141  			// RoleRef is immutable, delete the cluster role binding then re-create it.
   142  			if err := existing.Delete(ctx, client); err != nil {
   143  				return cleanups, errors.Annotatef(
   144  					err,
   145  					"delete cluster role binding %q because roleref has changed",
   146  					existing.Name)
   147  			}
   148  			doUpdate = false
   149  		}
   150  	}
   151  
   152  	cleanups = append(cleanups, func() { _ = rb.Delete(ctx, client) })
   153  	if !doUpdate {
   154  		return cleanups, rb.Apply(ctx, client)
   155  	} else if err := rb.Update(ctx, client); err != nil {
   156  		return cleanups, err
   157  	}
   158  	return cleanups, nil
   159  }
   160  
   161  // Events emitted by the resource.
   162  func (rb *ClusterRoleBinding) Events(ctx context.Context, client kubernetes.Interface) ([]corev1.Event, error) {
   163  	return ListEventsForObject(ctx, client, rb.Namespace, rb.Name, "ClusterRoleBinding")
   164  }
   165  
   166  // ComputeStatus returns a juju status for the resource.
   167  func (rb *ClusterRoleBinding) ComputeStatus(_ context.Context, _ kubernetes.Interface, now time.Time) (string, status.Status, time.Time, error) {
   168  	if rb.DeletionTimestamp != nil {
   169  		return "", status.Terminated, rb.DeletionTimestamp.Time, nil
   170  	}
   171  	return "", status.Active, now, nil
   172  }
   173  
   174  // Update updates the object in the Kubernetes cluster to the new representation
   175  func (rb *ClusterRoleBinding) Update(ctx context.Context, client kubernetes.Interface) error {
   176  	out, err := client.RbacV1().ClusterRoleBindings().Update(
   177  		ctx,
   178  		&rb.ClusterRoleBinding,
   179  		metav1.UpdateOptions{
   180  			FieldManager: JujuFieldManager,
   181  		},
   182  	)
   183  	if k8serrors.IsNotFound(err) {
   184  		return errors.Annotatef(err, "updating cluster role binding %q", rb.Name)
   185  	} else if err != nil {
   186  		return errors.Trace(err)
   187  	}
   188  	rb.ClusterRoleBinding = *out
   189  	return nil
   190  }