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

     1  // Copyright 2020 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package provider
     5  
     6  import (
     7  	"context"
     8  	"fmt"
     9  
    10  	"github.com/juju/errors"
    11  	admissionregistrationv1 "k8s.io/api/admissionregistration/v1"
    12  	admissionregistrationv1beta1 "k8s.io/api/admissionregistration/v1beta1"
    13  	k8serrors "k8s.io/apimachinery/pkg/api/errors"
    14  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    15  	k8slabels "k8s.io/apimachinery/pkg/labels"
    16  
    17  	"github.com/juju/juju/caas/kubernetes/provider/constants"
    18  	k8sspecs "github.com/juju/juju/caas/kubernetes/provider/specs"
    19  	"github.com/juju/juju/caas/kubernetes/provider/utils"
    20  	k8sannotations "github.com/juju/juju/core/annotations"
    21  )
    22  
    23  func (k *kubernetesClient) getAdmissionControllerLabels(appName string) map[string]string {
    24  	return utils.LabelsMerge(
    25  		utils.LabelsForApp(appName, k.IsLegacyLabels()),
    26  		utils.LabelsForModel(k.CurrentModel(), k.IsLegacyLabels()),
    27  	)
    28  }
    29  
    30  const annotationDisableNamePrefixValue = "true"
    31  
    32  func decideNameForGlobalResource(meta k8sspecs.Meta, namespace string, isLegacy bool) string {
    33  	name := meta.Name
    34  	key := utils.AnnotationDisableNameKey(isLegacy)
    35  	if k8sannotations.New(meta.Annotations).Has(key, annotationDisableNamePrefixValue) {
    36  		return name
    37  	}
    38  	return fmt.Sprintf("%s-%s", namespace, name)
    39  }
    40  
    41  func (k *kubernetesClient) ensureMutatingWebhookConfigurations(
    42  	appName string, annotations k8sannotations.Annotation, cfgs []k8sspecs.K8sMutatingWebhook,
    43  ) (cleanUps []func(), err error) {
    44  	if k.namespace == "" {
    45  		return nil, errNoNamespace
    46  	}
    47  	k8sVersion, err := k.Version()
    48  	if err != nil {
    49  		return nil, errors.Annotate(err, "getting k8s api version")
    50  	}
    51  	for _, v := range cfgs {
    52  		obj := metav1.ObjectMeta{
    53  			Name:        decideNameForGlobalResource(v.Meta, k.namespace, k.IsLegacyLabels()),
    54  			Namespace:   k.namespace,
    55  			Labels:      utils.LabelsMerge(v.Labels, k.getAdmissionControllerLabels(appName)),
    56  			Annotations: k8sannotations.New(v.Annotations).Merge(annotations),
    57  		}
    58  
    59  		logger.Infof("ensuring mutating webhook %q with version %q", obj.GetName(), v.APIVersion())
    60  		var cfgCleanup func()
    61  		switch v.APIVersion() {
    62  		case k8sspecs.K8sWebhookV1:
    63  			if k8sVersion.Major == 1 && k8sVersion.Minor < 16 {
    64  				return cleanUps, errors.NotSupportedf("mutating webhook version %q", v.APIVersion())
    65  			} else {
    66  				cfgCleanup, err = k.ensureMutatingWebhookConfigurationV1(&admissionregistrationv1.MutatingWebhookConfiguration{
    67  					ObjectMeta: obj,
    68  					Webhooks:   toMutatingWebhookV1(v.Webhooks),
    69  				})
    70  			}
    71  		case k8sspecs.K8sWebhookV1Beta1:
    72  			if k8sVersion.Major == 1 && k8sVersion.Minor < 16 {
    73  				cfgCleanup, err = k.ensureMutatingWebhookConfigurationV1beta1(&admissionregistrationv1beta1.MutatingWebhookConfiguration{
    74  					ObjectMeta: obj,
    75  					Webhooks:   toMutatingWebhookV1beta1(v.Webhooks),
    76  				})
    77  			} else {
    78  				var webHooks []admissionregistrationv1.MutatingWebhook
    79  				webHooks, err = convertToMutatingWebhookV1(v.Webhooks)
    80  				if err != nil {
    81  					err = errors.Annotatef(err, "cannot convert v1beta1 MutatingWebhookConfiguration to v1")
    82  					break
    83  				}
    84  				cfgCleanup, err = k.ensureMutatingWebhookConfigurationV1(&admissionregistrationv1.MutatingWebhookConfiguration{
    85  					ObjectMeta: obj,
    86  					Webhooks:   webHooks,
    87  				})
    88  			}
    89  		default:
    90  			// This should never happen.
    91  			return cleanUps, errors.NotSupportedf("mutating webhook version %q", v.APIVersion())
    92  		}
    93  
    94  		cleanUps = append(cleanUps, cfgCleanup)
    95  		if err != nil {
    96  			return cleanUps, errors.Annotatef(err, "ensuring mutating webhook %q with version %q", obj.GetName(), v.APIVersion())
    97  		}
    98  	}
    99  	return cleanUps, nil
   100  }
   101  
   102  func toMutatingWebhookV1beta1(i []k8sspecs.K8sMutatingWebhookSpec) (o []admissionregistrationv1beta1.MutatingWebhook) {
   103  	for _, v := range i {
   104  		o = append(o, v.SpecV1Beta1)
   105  	}
   106  	return o
   107  }
   108  
   109  func toMutatingWebhookV1(i []k8sspecs.K8sMutatingWebhookSpec) (o []admissionregistrationv1.MutatingWebhook) {
   110  	for _, v := range i {
   111  		o = append(o, v.SpecV1)
   112  	}
   113  	return o
   114  }
   115  
   116  func convertToMutatingWebhookV1(i []k8sspecs.K8sMutatingWebhookSpec) (o []admissionregistrationv1.MutatingWebhook, _ error) {
   117  	for _, v := range i {
   118  		o = append(o, k8sspecs.UpgradeK8sMutatingWebhookSpecV1Beta1(v.SpecV1Beta1))
   119  	}
   120  	return o, nil
   121  }
   122  
   123  func (k *kubernetesClient) ensureMutatingWebhookConfigurationV1(cfg *admissionregistrationv1.MutatingWebhookConfiguration) (func(), error) {
   124  	cleanUp := func() {}
   125  	api := k.client().AdmissionregistrationV1().MutatingWebhookConfigurations()
   126  	out, err := api.Create(context.TODO(), cfg, metav1.CreateOptions{})
   127  	if err == nil {
   128  		logger.Debugf("MutatingWebhookConfiguration %q created", out.GetName())
   129  		cleanUp = func() {
   130  			_ = api.Delete(context.TODO(), out.GetName(), utils.NewPreconditionDeleteOptions(out.GetUID()))
   131  		}
   132  		return cleanUp, nil
   133  	}
   134  	if !k8serrors.IsAlreadyExists(err) {
   135  		return cleanUp, errors.Trace(err)
   136  	}
   137  
   138  	existingItems, err := api.List(context.TODO(), metav1.ListOptions{
   139  		LabelSelector: utils.LabelsToSelector(cfg.GetLabels()).String(),
   140  	})
   141  	if k8serrors.IsNotFound(err) || existingItems == nil || len(existingItems.Items) == 0 {
   142  		// cfg.Name is already used for an existing MutatingWebhookConfiguration.
   143  		return cleanUp, errors.AlreadyExistsf("MutatingWebhookConfiguration %q", cfg.GetName())
   144  	}
   145  	if err != nil {
   146  		return cleanUp, errors.Trace(err)
   147  	}
   148  	existingCfg, err := api.Get(context.TODO(), cfg.GetName(), metav1.GetOptions{})
   149  	if err != nil {
   150  		return cleanUp, errors.Trace(err)
   151  	}
   152  	cfg.SetResourceVersion(existingCfg.GetResourceVersion())
   153  	_, err = api.Update(context.TODO(), cfg, metav1.UpdateOptions{})
   154  	logger.Debugf("updating MutatingWebhookConfiguration %q", cfg.GetName())
   155  	return cleanUp, errors.Trace(err)
   156  }
   157  
   158  func (k *kubernetesClient) EnsureMutatingWebhookConfiguration(cfg *admissionregistrationv1.MutatingWebhookConfiguration) (func(), error) {
   159  	return k.ensureMutatingWebhookConfigurationV1(cfg)
   160  }
   161  
   162  // EnsureMutatingWebhookConfiguration ensures the provided mutating webhook
   163  // exists in the given Kubernetes cluster.
   164  // Returned func is used for cleaning up the mutating webhook when error is non
   165  // nil.
   166  func (k *kubernetesClient) ensureMutatingWebhookConfigurationV1beta1(cfg *admissionregistrationv1beta1.MutatingWebhookConfiguration) (func(), error) {
   167  	cleanUp := func() {}
   168  	api := k.client().AdmissionregistrationV1beta1().MutatingWebhookConfigurations()
   169  	out, err := api.Create(context.TODO(), cfg, metav1.CreateOptions{})
   170  	if err == nil {
   171  		logger.Debugf("MutatingWebhookConfiguration %q created", out.GetName())
   172  		cleanUp = func() {
   173  			_ = api.Delete(context.TODO(), out.GetName(), utils.NewPreconditionDeleteOptions(out.GetUID()))
   174  		}
   175  		return cleanUp, nil
   176  	}
   177  	if !k8serrors.IsAlreadyExists(err) {
   178  		return cleanUp, errors.Trace(err)
   179  	}
   180  	existingItems, err := api.List(context.TODO(), metav1.ListOptions{
   181  		LabelSelector: utils.LabelsToSelector(cfg.GetLabels()).String(),
   182  	})
   183  	if k8serrors.IsNotFound(err) || existingItems == nil || len(existingItems.Items) == 0 {
   184  		// cfg.Name is already used for an existing MutatingWebhookConfiguration.
   185  		return cleanUp, errors.AlreadyExistsf("MutatingWebhookConfiguration %q", cfg.GetName())
   186  	}
   187  	if err != nil {
   188  		return cleanUp, errors.Trace(err)
   189  	}
   190  	existingCfg, err := api.Get(context.TODO(), cfg.GetName(), metav1.GetOptions{})
   191  	if err != nil {
   192  		return cleanUp, errors.Trace(err)
   193  	}
   194  	cfg.SetResourceVersion(existingCfg.GetResourceVersion())
   195  	_, err = api.Update(context.TODO(), cfg, metav1.UpdateOptions{})
   196  	logger.Debugf("updating MutatingWebhookConfiguration %q", cfg.GetName())
   197  	return cleanUp, errors.Trace(err)
   198  }
   199  
   200  func (k *kubernetesClient) listMutatingWebhookConfigurations(selector k8slabels.Selector) ([]admissionregistrationv1.MutatingWebhookConfiguration, error) {
   201  	listOps := metav1.ListOptions{
   202  		LabelSelector: selector.String(),
   203  	}
   204  	cfgList, err := k.client().AdmissionregistrationV1().MutatingWebhookConfigurations().List(context.TODO(), listOps)
   205  	if err != nil {
   206  		return nil, errors.Trace(err)
   207  	}
   208  	if len(cfgList.Items) == 0 {
   209  		return nil, errors.NotFoundf("MutatingWebhookConfiguration with selector %q", selector)
   210  	}
   211  	return cfgList.Items, nil
   212  }
   213  
   214  func (k *kubernetesClient) deleteMutatingWebhookConfigurations(selector k8slabels.Selector) error {
   215  	err := k.client().AdmissionregistrationV1().MutatingWebhookConfigurations().DeleteCollection(context.TODO(), metav1.DeleteOptions{
   216  		PropagationPolicy: constants.DefaultPropagationPolicy(),
   217  	}, metav1.ListOptions{
   218  		LabelSelector: selector.String(),
   219  	})
   220  	if k8serrors.IsNotFound(err) {
   221  		return nil
   222  	}
   223  	return errors.Trace(err)
   224  }
   225  
   226  func (k *kubernetesClient) deleteMutatingWebhookConfigurationsForApp(appName string) error {
   227  	selector := utils.LabelsToSelector(k.getAdmissionControllerLabels(appName))
   228  	return errors.Trace(k.deleteMutatingWebhookConfigurations(selector))
   229  }
   230  
   231  func (k *kubernetesClient) ensureValidatingWebhookConfigurations(
   232  	appName string, annotations k8sannotations.Annotation, cfgs []k8sspecs.K8sValidatingWebhook,
   233  ) (cleanUps []func(), err error) {
   234  	if k.namespace == "" {
   235  		return nil, errNoNamespace
   236  	}
   237  	k8sVersion, err := k.Version()
   238  	if err != nil {
   239  		return nil, errors.Annotate(err, "getting k8s api version")
   240  	}
   241  	for _, v := range cfgs {
   242  		obj := metav1.ObjectMeta{
   243  			Name:        decideNameForGlobalResource(v.Meta, k.namespace, k.IsLegacyLabels()),
   244  			Namespace:   k.namespace,
   245  			Labels:      utils.LabelsMerge(v.Labels, k.getAdmissionControllerLabels(appName)),
   246  			Annotations: k8sannotations.New(v.Annotations).Merge(annotations),
   247  		}
   248  
   249  		logger.Infof("ensuring validating webhook %q with version %q", obj.GetName(), v.APIVersion())
   250  		var cfgCleanup func()
   251  		switch v.APIVersion() {
   252  		case k8sspecs.K8sWebhookV1:
   253  			if k8sVersion.Major == 1 && k8sVersion.Minor < 16 {
   254  				return cleanUps, errors.NotSupportedf("validating webhook version %q", v.APIVersion())
   255  			} else {
   256  				cfgCleanup, err = k.ensureValidatingWebhookConfigurationV1(&admissionregistrationv1.ValidatingWebhookConfiguration{
   257  					ObjectMeta: obj,
   258  					Webhooks:   toValidatingWebhookV1(v.Webhooks),
   259  				})
   260  			}
   261  		case k8sspecs.K8sWebhookV1Beta1:
   262  			if k8sVersion.Major == 1 && k8sVersion.Minor < 16 {
   263  				cfgCleanup, err = k.ensureValidatingWebhookConfigurationV1beta1(&admissionregistrationv1beta1.ValidatingWebhookConfiguration{
   264  					ObjectMeta: obj,
   265  					Webhooks:   toValidatingWebhookV1beta1(v.Webhooks),
   266  				})
   267  			} else {
   268  				var webHooks []admissionregistrationv1.ValidatingWebhook
   269  				webHooks, err = convertToValidatingWebhookV1(v.Webhooks)
   270  				if err != nil {
   271  					err = errors.Annotatef(err, "cannot convert v1beta1 ValidatingWebhookConfiguration to v1")
   272  					break
   273  				}
   274  				cfgCleanup, err = k.ensureValidatingWebhookConfigurationV1(&admissionregistrationv1.ValidatingWebhookConfiguration{
   275  					ObjectMeta: obj,
   276  					Webhooks:   webHooks,
   277  				})
   278  			}
   279  		default:
   280  			// This should never happen.
   281  			return cleanUps, errors.NotSupportedf("validating webhook version %q", v.APIVersion())
   282  		}
   283  		cleanUps = append(cleanUps, cfgCleanup)
   284  		if err != nil {
   285  			return cleanUps, errors.Annotatef(err, "ensuring validating webhook %q with version %q", obj.GetName(), v.APIVersion())
   286  		}
   287  	}
   288  	return cleanUps, nil
   289  }
   290  
   291  func toValidatingWebhookV1beta1(i []k8sspecs.K8sValidatingWebhookSpec) (o []admissionregistrationv1beta1.ValidatingWebhook) {
   292  	for _, v := range i {
   293  		o = append(o, v.SpecV1Beta1)
   294  	}
   295  	return o
   296  }
   297  
   298  func toValidatingWebhookV1(i []k8sspecs.K8sValidatingWebhookSpec) (o []admissionregistrationv1.ValidatingWebhook) {
   299  	for _, v := range i {
   300  		o = append(o, v.SpecV1)
   301  	}
   302  	return o
   303  }
   304  
   305  func convertToValidatingWebhookV1(i []k8sspecs.K8sValidatingWebhookSpec) (o []admissionregistrationv1.ValidatingWebhook, _ error) {
   306  	for _, v := range i {
   307  		o = append(o, k8sspecs.UpgradeK8sValidatingWebhookSpecV1Beta1(v.SpecV1Beta1))
   308  	}
   309  	return o, nil
   310  }
   311  
   312  func (k *kubernetesClient) ensureValidatingWebhookConfigurationV1(cfg *admissionregistrationv1.ValidatingWebhookConfiguration) (func(), error) {
   313  	cleanUp := func() {}
   314  	api := k.client().AdmissionregistrationV1().ValidatingWebhookConfigurations()
   315  	out, err := api.Create(context.TODO(), cfg, metav1.CreateOptions{})
   316  	if err == nil {
   317  		logger.Debugf("ValidatingWebhookConfiguration %q created", out.GetName())
   318  		cleanUp = func() {
   319  			_ = api.Delete(context.TODO(), out.GetName(), utils.NewPreconditionDeleteOptions(out.GetUID()))
   320  		}
   321  		return cleanUp, nil
   322  	}
   323  	if !k8serrors.IsAlreadyExists(err) {
   324  		return cleanUp, errors.Trace(err)
   325  	}
   326  
   327  	existingItems, err := api.List(context.TODO(), metav1.ListOptions{
   328  		LabelSelector: utils.LabelsToSelector(cfg.GetLabels()).String(),
   329  	})
   330  	if k8serrors.IsNotFound(err) || len(existingItems.Items) == 0 {
   331  		// cfg.Name is already used for an existing ValidatingWebhookConfiguration.
   332  		return cleanUp, errors.AlreadyExistsf("ValidatingWebhookConfiguration %q", cfg.GetName())
   333  	}
   334  	if err != nil {
   335  		return cleanUp, errors.Trace(err)
   336  	}
   337  	existingCfg, err := api.Get(context.TODO(), cfg.GetName(), metav1.GetOptions{})
   338  	if err != nil {
   339  		return cleanUp, errors.Trace(err)
   340  	}
   341  	cfg.SetResourceVersion(existingCfg.GetResourceVersion())
   342  	_, err = api.Update(context.TODO(), cfg, metav1.UpdateOptions{})
   343  	logger.Debugf("updating ValidatingWebhookConfiguration %q", cfg.GetName())
   344  	return cleanUp, errors.Trace(err)
   345  }
   346  
   347  func (k *kubernetesClient) ensureValidatingWebhookConfigurationV1beta1(cfg *admissionregistrationv1beta1.ValidatingWebhookConfiguration) (func(), error) {
   348  	cleanUp := func() {}
   349  	api := k.client().AdmissionregistrationV1beta1().ValidatingWebhookConfigurations()
   350  	out, err := api.Create(context.TODO(), cfg, metav1.CreateOptions{})
   351  	if err == nil {
   352  		logger.Debugf("ValidatingWebhookConfiguration %q created", out.GetName())
   353  		cleanUp = func() {
   354  			_ = api.Delete(context.TODO(), out.GetName(), utils.NewPreconditionDeleteOptions(out.GetUID()))
   355  		}
   356  		return cleanUp, nil
   357  	}
   358  	if !k8serrors.IsAlreadyExists(err) {
   359  		return cleanUp, errors.Trace(err)
   360  	}
   361  
   362  	existingItems, err := api.List(context.TODO(), metav1.ListOptions{
   363  		LabelSelector: utils.LabelsToSelector(cfg.GetLabels()).String(),
   364  	})
   365  	if k8serrors.IsNotFound(err) || len(existingItems.Items) == 0 {
   366  		// cfg.Name is already used for an existing ValidatingWebhookConfiguration.
   367  		return cleanUp, errors.AlreadyExistsf("ValidatingWebhookConfiguration %q", cfg.GetName())
   368  	}
   369  	if err != nil {
   370  		return cleanUp, errors.Trace(err)
   371  	}
   372  	existingCfg, err := api.Get(context.TODO(), cfg.GetName(), metav1.GetOptions{})
   373  	if err != nil {
   374  		return cleanUp, errors.Trace(err)
   375  	}
   376  	cfg.SetResourceVersion(existingCfg.GetResourceVersion())
   377  	_, err = api.Update(context.TODO(), cfg, metav1.UpdateOptions{})
   378  	logger.Debugf("updating ValidatingWebhookConfiguration %q", cfg.GetName())
   379  	return cleanUp, errors.Trace(err)
   380  }
   381  
   382  func (k *kubernetesClient) listValidatingWebhookConfigurations(selector k8slabels.Selector) ([]admissionregistrationv1.ValidatingWebhookConfiguration, error) {
   383  	listOps := metav1.ListOptions{
   384  		LabelSelector: selector.String(),
   385  	}
   386  	cfgList, err := k.client().AdmissionregistrationV1().ValidatingWebhookConfigurations().List(context.TODO(), listOps)
   387  	if err != nil {
   388  		return nil, errors.Trace(err)
   389  	}
   390  	if len(cfgList.Items) == 0 {
   391  		return nil, errors.NotFoundf("ValidatingWebhookConfiguration with selector %q", selector)
   392  	}
   393  	return cfgList.Items, nil
   394  }
   395  
   396  func (k *kubernetesClient) deleteValidatingWebhookConfigurations(selector k8slabels.Selector) error {
   397  	err := k.client().AdmissionregistrationV1().ValidatingWebhookConfigurations().DeleteCollection(context.TODO(), metav1.DeleteOptions{
   398  		PropagationPolicy: constants.DefaultPropagationPolicy(),
   399  	}, metav1.ListOptions{
   400  		LabelSelector: selector.String(),
   401  	})
   402  	if k8serrors.IsNotFound(err) {
   403  		return nil
   404  	}
   405  	return errors.Trace(err)
   406  }
   407  
   408  func (k *kubernetesClient) deleteValidatingWebhookConfigurationsForApp(appName string) error {
   409  	selector := utils.LabelsToSelector(k.getAdmissionControllerLabels(appName))
   410  	return errors.Trace(k.deleteValidatingWebhookConfigurations(selector))
   411  }