github.com/operator-framework/operator-lifecycle-manager@v0.30.0/pkg/controller/install/webhook.go (about)

     1  package install
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  
     7  	log "github.com/sirupsen/logrus"
     8  	admissionregistrationv1 "k8s.io/api/admissionregistration/v1"
     9  	apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
    10  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    11  	"k8s.io/apimachinery/pkg/labels"
    12  	"k8s.io/client-go/util/retry"
    13  
    14  	"github.com/operator-framework/api/pkg/operators/v1alpha1"
    15  	hashutil "github.com/operator-framework/operator-lifecycle-manager/pkg/lib/kubernetes/pkg/util/hash"
    16  	"github.com/operator-framework/operator-lifecycle-manager/pkg/lib/ownerutil"
    17  )
    18  
    19  func ValidWebhookRules(rules []admissionregistrationv1.RuleWithOperations) error {
    20  	for _, rule := range rules {
    21  		apiGroupMap := listToMap(rule.APIGroups)
    22  
    23  		// protect OLM resources
    24  		if contains(apiGroupMap, "*") {
    25  			return fmt.Errorf("webhook rules cannot include all groups")
    26  		}
    27  
    28  		if contains(apiGroupMap, "operators.coreos.com") {
    29  			return fmt.Errorf("webhook rules cannot include the OLM group")
    30  		}
    31  
    32  		// protect Admission Webhook resources
    33  		if contains(apiGroupMap, "admissionregistration.k8s.io") {
    34  			resourceGroupMap := listToMap(rule.Resources)
    35  			if contains(resourceGroupMap, "*") || contains(resourceGroupMap, "MutatingWebhookConfiguration") || contains(resourceGroupMap, "ValidatingWebhookConfiguration") {
    36  				return fmt.Errorf("webhook rules cannot include MutatingWebhookConfiguration or ValidatingWebhookConfiguration resources")
    37  			}
    38  		}
    39  	}
    40  	return nil
    41  }
    42  
    43  func listToMap(list []string) map[string]struct{} {
    44  	result := make(map[string]struct{})
    45  	for _, ele := range list {
    46  		result[ele] = struct{}{}
    47  	}
    48  	return result
    49  }
    50  
    51  func contains(m map[string]struct{}, tar string) bool {
    52  	_, present := m[tar]
    53  	return present
    54  }
    55  
    56  func (i *StrategyDeploymentInstaller) createOrUpdateWebhook(caPEM []byte, desc v1alpha1.WebhookDescription) error {
    57  	operatorGroups, err := i.strategyClient.GetOpLister().OperatorsV1().OperatorGroupLister().OperatorGroups(i.owner.GetNamespace()).List(labels.Everything())
    58  	if err != nil || len(operatorGroups) != 1 {
    59  		return fmt.Errorf("error retrieving OperatorGroup info")
    60  	}
    61  	ogNamespacelabelSelector, err := operatorGroups[0].NamespaceLabelSelector()
    62  	if err != nil {
    63  		return err
    64  	}
    65  
    66  	switch desc.Type {
    67  	case v1alpha1.ValidatingAdmissionWebhook:
    68  		return i.createOrUpdateValidatingWebhook(ogNamespacelabelSelector, caPEM, desc)
    69  	case v1alpha1.MutatingAdmissionWebhook:
    70  		return i.createOrUpdateMutatingWebhook(ogNamespacelabelSelector, caPEM, desc)
    71  	case v1alpha1.ConversionWebhook:
    72  		return i.createOrUpdateConversionWebhook(caPEM, desc)
    73  	}
    74  	return nil
    75  }
    76  
    77  func (i *StrategyDeploymentInstaller) createOrUpdateMutatingWebhook(ogNamespacelabelSelector *metav1.LabelSelector, caPEM []byte, desc v1alpha1.WebhookDescription) error {
    78  	webhookLabels := ownerutil.OwnerLabel(i.owner, i.owner.GetObjectKind().GroupVersionKind().Kind)
    79  	webhookLabels[WebhookDescKey] = desc.GenerateName
    80  	webhookSelector := labels.SelectorFromSet(webhookLabels).String()
    81  
    82  	existingWebhooks, err := i.strategyClient.GetOpClient().KubernetesInterface().AdmissionregistrationV1().MutatingWebhookConfigurations().List(context.TODO(), metav1.ListOptions{LabelSelector: webhookSelector})
    83  	if err != nil {
    84  		return err
    85  	}
    86  
    87  	if len(existingWebhooks.Items) == 0 {
    88  		// Create a MutatingWebhookConfiguration
    89  		webhook := admissionregistrationv1.MutatingWebhookConfiguration{
    90  			ObjectMeta: metav1.ObjectMeta{
    91  				GenerateName: desc.GenerateName + "-",
    92  				Namespace:    i.owner.GetNamespace(),
    93  				Labels:       ownerutil.OwnerLabel(i.owner, i.owner.GetObjectKind().GroupVersionKind().Kind),
    94  			},
    95  			Webhooks: []admissionregistrationv1.MutatingWebhook{
    96  				desc.GetMutatingWebhook(i.owner.GetNamespace(), ogNamespacelabelSelector, caPEM),
    97  			},
    98  		}
    99  		addWebhookLabels(&webhook, desc)
   100  
   101  		if _, err := i.strategyClient.GetOpClient().KubernetesInterface().AdmissionregistrationV1().MutatingWebhookConfigurations().Create(context.TODO(), &webhook, metav1.CreateOptions{}); err != nil {
   102  			log.Errorf("Webhooks: Error creating MutatingWebhookConfiguration: %v", err)
   103  			return err
   104  		}
   105  	}
   106  	for _, webhook := range existingWebhooks.Items {
   107  		webhook = *webhook.DeepCopy()
   108  		// Update the list of webhooks
   109  		webhook.Webhooks = []admissionregistrationv1.MutatingWebhook{
   110  			desc.GetMutatingWebhook(i.owner.GetNamespace(), ogNamespacelabelSelector, caPEM),
   111  		}
   112  		addWebhookLabels(&webhook, desc)
   113  
   114  		// Attempt an update
   115  		if _, err := i.strategyClient.GetOpClient().KubernetesInterface().AdmissionregistrationV1().MutatingWebhookConfigurations().Update(context.TODO(), &webhook, metav1.UpdateOptions{}); err != nil {
   116  			log.Warnf("could not update MutatingWebhookConfiguration %s", webhook.GetName())
   117  			return err
   118  		}
   119  	}
   120  
   121  	return nil
   122  }
   123  
   124  func (i *StrategyDeploymentInstaller) createOrUpdateValidatingWebhook(ogNamespacelabelSelector *metav1.LabelSelector, caPEM []byte, desc v1alpha1.WebhookDescription) error {
   125  	webhookLabels := ownerutil.OwnerLabel(i.owner, i.owner.GetObjectKind().GroupVersionKind().Kind)
   126  	webhookLabels[WebhookDescKey] = desc.GenerateName
   127  	webhookSelector := labels.SelectorFromSet(webhookLabels).String()
   128  
   129  	existingWebhooks, err := i.strategyClient.GetOpClient().KubernetesInterface().AdmissionregistrationV1().ValidatingWebhookConfigurations().List(context.TODO(), metav1.ListOptions{LabelSelector: webhookSelector})
   130  	if err != nil {
   131  		return err
   132  	}
   133  
   134  	if len(existingWebhooks.Items) == 0 {
   135  		// Create a ValidatingWebhookConfiguration
   136  		webhook := admissionregistrationv1.ValidatingWebhookConfiguration{
   137  			ObjectMeta: metav1.ObjectMeta{
   138  				GenerateName: desc.GenerateName + "-",
   139  				Namespace:    i.owner.GetNamespace(),
   140  				Labels:       ownerutil.OwnerLabel(i.owner, i.owner.GetObjectKind().GroupVersionKind().Kind),
   141  			},
   142  			Webhooks: []admissionregistrationv1.ValidatingWebhook{
   143  				desc.GetValidatingWebhook(i.owner.GetNamespace(), ogNamespacelabelSelector, caPEM),
   144  			},
   145  		}
   146  		addWebhookLabels(&webhook, desc)
   147  
   148  		if _, err := i.strategyClient.GetOpClient().KubernetesInterface().AdmissionregistrationV1().ValidatingWebhookConfigurations().Create(context.TODO(), &webhook, metav1.CreateOptions{}); err != nil {
   149  			log.Errorf("Webhooks: Error creating ValidatingWebhookConfiguration: %v", err)
   150  			return err
   151  		}
   152  
   153  		return nil
   154  	}
   155  	for _, webhook := range existingWebhooks.Items {
   156  		webhook = *webhook.DeepCopy()
   157  		// Update the list of webhooks
   158  		webhook.Webhooks = []admissionregistrationv1.ValidatingWebhook{
   159  			desc.GetValidatingWebhook(i.owner.GetNamespace(), ogNamespacelabelSelector, caPEM),
   160  		}
   161  		addWebhookLabels(&webhook, desc)
   162  
   163  		// Attempt an update
   164  		if _, err := i.strategyClient.GetOpClient().KubernetesInterface().AdmissionregistrationV1().ValidatingWebhookConfigurations().Update(context.TODO(), &webhook, metav1.UpdateOptions{}); err != nil {
   165  			log.Warnf("could not update ValidatingWebhookConfiguration %s", webhook.GetName())
   166  			return err
   167  		}
   168  	}
   169  
   170  	return nil
   171  }
   172  
   173  // check if csv supports only AllNamespaces install mode
   174  func isSingletonOperator(csv v1alpha1.ClusterServiceVersion) bool {
   175  	// check if AllNamespaces is supported and other install modes are not supported
   176  	for _, installMode := range csv.Spec.InstallModes {
   177  		if installMode.Type == v1alpha1.InstallModeTypeAllNamespaces && !installMode.Supported {
   178  			return false
   179  		}
   180  		if installMode.Type != v1alpha1.InstallModeTypeAllNamespaces && installMode.Supported {
   181  			return false
   182  		}
   183  	}
   184  	return true
   185  }
   186  
   187  func (i *StrategyDeploymentInstaller) createOrUpdateConversionWebhook(caPEM []byte, desc v1alpha1.WebhookDescription) error {
   188  	// get a list of owned CRDs
   189  	csv, ok := i.owner.(*v1alpha1.ClusterServiceVersion)
   190  	if !ok {
   191  		return fmt.Errorf("unable to manage conversion webhook: conversion webhook owner must be a ClusterServiceVersion")
   192  	}
   193  	if !isSingletonOperator(*csv) {
   194  		return fmt.Errorf("unable to manage conversion webhook: CSVs with conversion webhooks must support only AllNamespaces")
   195  	}
   196  
   197  	if len(desc.ConversionCRDs) == 0 {
   198  		return fmt.Errorf("unable to manager conversion webhook: conversion webhook must have at least one CRD specified")
   199  	}
   200  
   201  	// iterate over all the ConversionCRDs
   202  	for _, conversionCRD := range desc.ConversionCRDs {
   203  		if err := retry.RetryOnConflict(retry.DefaultRetry, func() error {
   204  			// Get existing CRD on cluster
   205  			crd, err := i.strategyClient.GetOpClient().ApiextensionsInterface().ApiextensionsV1().CustomResourceDefinitions().Get(context.TODO(), conversionCRD, metav1.GetOptions{})
   206  			if err != nil {
   207  				return fmt.Errorf("unable to get CRD %s specified in Conversion Webhook: %v", conversionCRD, err)
   208  			}
   209  
   210  			// check if this CRD is an owned CRD
   211  			foundCRD := false
   212  			for _, ownedCRD := range csv.Spec.CustomResourceDefinitions.Owned {
   213  				if ownedCRD.Name == conversionCRD {
   214  					foundCRD = true
   215  					break
   216  				}
   217  			}
   218  			if !foundCRD {
   219  				return fmt.Errorf("csv %s does not own CRD %s", csv.GetName(), conversionCRD)
   220  			}
   221  
   222  			// crd.Spec.Conversion.Strategy specifies how custom resources are converted between versions.
   223  			// Allowed values are:
   224  			// 	- None: The converter only change the apiVersion and would not touch any other field in the custom resource.
   225  			// 	- Webhook: API Server will call to an external webhook to do the conversion. This requires crd.Spec.preserveUnknownFields to be false.
   226  			// References:
   227  			//  - https://docs.openshift.com/container-platform/4.5/rest_api/extension_apis/customresourcedefinition-apiextensions-k8s-io-v1.html
   228  			// 	- https://kubernetes.io/blog/2019/06/20/crd-structural-schema/#pruning-don-t-preserve-unknown-fields
   229  			// By default the strategy is none
   230  			// Reference:
   231  			// 	- https://v1-15.docs.kubernetes.io/docs/tasks/access-kubernetes-api/custom-resources/custom-resource-definition-versioning/#specify-multiple-versions
   232  			if crd.Spec.PreserveUnknownFields {
   233  				return fmt.Errorf("crd.Spec.PreserveUnknownFields must be false to let API Server call webhook to do the conversion")
   234  			}
   235  
   236  			// Conversion WebhookClientConfig should not be set when Strategy is None
   237  			// https://v1-15.docs.kubernetes.io/docs/tasks/access-kubernetes-api/custom-resources/custom-resource-definition-versioning/#specify-multiple-versions
   238  			// Conversion WebhookClientConfig needs to be set when Strategy is None
   239  			// https://v1-15.docs.kubernetes.io/docs/tasks/access-kubernetes-api/custom-resources/custom-resource-definition-versioning/#configure-customresourcedefinition-to-use-conversion-webhooks
   240  
   241  			// use user defined path for CRD conversion webhook, else set default value
   242  			conversionWebhookPath := "/"
   243  			if desc.WebhookPath != nil {
   244  				conversionWebhookPath = *desc.WebhookPath
   245  			}
   246  
   247  			// Override Name, Namespace, and CABundle
   248  			crd.Spec.Conversion = &apiextensionsv1.CustomResourceConversion{
   249  				Strategy: "Webhook",
   250  				Webhook: &apiextensionsv1.WebhookConversion{
   251  					ClientConfig: &apiextensionsv1.WebhookClientConfig{
   252  						Service: &apiextensionsv1.ServiceReference{
   253  							Namespace: i.owner.GetNamespace(),
   254  							Name:      desc.DomainName() + "-service",
   255  							Path:      &conversionWebhookPath,
   256  							Port:      &desc.ContainerPort,
   257  						},
   258  						CABundle: caPEM,
   259  					},
   260  					ConversionReviewVersions: desc.AdmissionReviewVersions,
   261  				},
   262  			}
   263  
   264  			// update CRD conversion Specs
   265  			if _, err = i.strategyClient.GetOpClient().ApiextensionsInterface().ApiextensionsV1().CustomResourceDefinitions().Update(context.TODO(), crd, metav1.UpdateOptions{}); err != nil {
   266  				return fmt.Errorf("Error updating CRD with Conversion info: %w", err)
   267  			}
   268  			return nil
   269  		}); err != nil {
   270  			return err
   271  		}
   272  	}
   273  
   274  	return nil
   275  }
   276  
   277  const WebhookDescKey = "olm.webhook-description-generate-name"
   278  const WebhookHashKey = "olm.webhook-description-hash"
   279  
   280  // addWebhookLabels adds webhook labels to an object
   281  func addWebhookLabels(object metav1.Object, webhookDesc v1alpha1.WebhookDescription) error {
   282  	labels := object.GetLabels()
   283  	if labels == nil {
   284  		labels = map[string]string{}
   285  	}
   286  	hash, err := hashutil.DeepHashObject(&webhookDesc)
   287  	if err != nil {
   288  		return err
   289  	}
   290  	labels[WebhookDescKey] = webhookDesc.GenerateName
   291  	labels[WebhookHashKey] = hash
   292  	labels[OLMManagedLabelKey] = OLMManagedLabelValue
   293  	object.SetLabels(labels)
   294  
   295  	return nil
   296  }