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 }