@@ -18,6 +18,7 @@ import (
1818 "encoding/json"
1919 "errors"
2020 "fmt"
21+ "sort"
2122 "strings"
2223
2324 operatorsv1alpha1 "github.com/operator-framework/api/pkg/operators/v1alpha1"
@@ -28,11 +29,19 @@ import (
2829 appsv1 "k8s.io/api/apps/v1"
2930 corev1 "k8s.io/api/core/v1"
3031 rbacv1 "k8s.io/api/rbac/v1"
32+ apiextv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
3133
3234 "github.com/operator-framework/operator-sdk/internal/generate/collector"
3335 "github.com/operator-framework/operator-sdk/internal/util/k8sutil"
3436)
3537
38+ // serviceportPath is refers to the group of webhook service and
39+ // path names and port.
40+ type serviceportPath struct {
41+ Port * int32
42+ Path string
43+ }
44+
3645// ApplyTo applies relevant manifests in c to csv, sorts the applied updates,
3746// and validates the result.
3847func ApplyTo (c * collector.Manifests , csv * operatorsv1alpha1.ClusterServiceVersion ) error {
@@ -295,9 +304,150 @@ func applyWebhooks(c *collector.Manifests, csv *operatorsv1alpha1.ClusterService
295304 }
296305 webhookDescriptions = append (webhookDescriptions , mutatingToWebhookDescription (webhook , depName , svc ))
297306 }
307+
308+ for _ , svc := range c .Services {
309+ crdToConfigMap := getConvWebhookCRDNamesAndConfig (c , svc .GetName ())
310+
311+ if len (crdToConfigMap ) != 0 {
312+ depName := findMatchingDepNameFromService (c , & svc )
313+ des , err := conversionToWebhookDescription (crdToConfigMap , depName , & svc )
314+ // not sure if we should exit here, is it reasonable to just log it and continue with
315+ // the next step?
316+ if err != nil {
317+ log .Fatal (err )
318+ }
319+ webhookDescriptions = append (webhookDescriptions , des ... )
320+ }
321+ }
298322 csv .Spec .WebhookDefinitions = webhookDescriptions
299323}
300324
325+ // conversionToWebhookDescription takes in a map of {crdNames, apiextv.WebhookConversion} and groups
326+ // all the crds with same port and path. It then creates a webhook description for each unique combination of
327+ // port and path.
328+ // For example: if we have the following map: {crd1:[portX+pathX], crd2: [portX+pathX], crd3: [portY:partY]},
329+ // we will create 2 webhook descriptions: one with [portX+pathX]:[crd1, crd2] and the other with [portY:pathY]:[crd3]
330+ func conversionToWebhookDescription (crdToConfig map [string ]apiextv1.WebhookConversion , depName string , ws * corev1.Service ) ([]operatorsv1alpha1.WebhookDescription , error ) {
331+ des := make ([]operatorsv1alpha1.WebhookDescription , 0 )
332+
333+ // this is a map of serviceportAndPath configs, and the respective CRDs.
334+ webhookDescriptions := crdGroups (crdToConfig )
335+
336+ for serviceConfig , crds := range webhookDescriptions {
337+ // we need this to get the conversionReviewVersions.
338+ // here, we assume all crds having same servicePortAndPath config will have
339+ // same conversion review versions.
340+ // this should technically never error.
341+ config , ok := crdToConfig [crds [0 ]]
342+ if ! ok {
343+ return nil , fmt .Errorf ("webhook config for crd %q not found" , crds [0 ])
344+ }
345+
346+ description := operatorsv1alpha1.WebhookDescription {
347+ Type : operatorsv1alpha1 .ConversionWebhook ,
348+ ConversionCRDs : crds ,
349+ AdmissionReviewVersions : config .ConversionReviewVersions ,
350+ WebhookPath : & serviceConfig .Path ,
351+ DeploymentName : depName ,
352+ GenerateName : getGenerateName (crds ),
353+ SideEffects : func () * admissionregv1.SideEffectClass {
354+ seNone := admissionregv1 .SideEffectClassNone
355+ return & seNone
356+ }(),
357+ }
358+
359+ if len (description .AdmissionReviewVersions ) == 0 {
360+ log .Infof ("conversionReviewVersion not found for the deployment %q" , depName )
361+ }
362+
363+ var webhookServiceRefPort int32 = 443
364+
365+ if serviceConfig .Port != nil {
366+ webhookServiceRefPort = * serviceConfig .Port
367+ }
368+
369+ if ws != nil {
370+ for _ , port := range ws .Spec .Ports {
371+ if webhookServiceRefPort == port .Port {
372+ description .ContainerPort = port .Port
373+ description .TargetPort = & port .TargetPort
374+ break
375+ }
376+ }
377+ }
378+
379+ if description .DeploymentName == "" {
380+ if config .ClientConfig .Service != nil {
381+ description .DeploymentName = strings .TrimSuffix (config .ClientConfig .Service .Name , "-service" )
382+ }
383+ }
384+
385+ description .WebhookPath = & serviceConfig .Path
386+ des = append (des , description )
387+ }
388+
389+ return des , nil
390+ }
391+
392+ // crdGroups groups the crds with similar service port and name. It returns a map of serviceportPath
393+ // and the corresponding crd names.
394+ func crdGroups (crdToConfig map [string ]apiextv1.WebhookConversion ) map [serviceportPath ][]string {
395+
396+ uniqueConfig := make (map [serviceportPath ][]string )
397+
398+ for crdName , config := range crdToConfig {
399+ serviceportPath := serviceportPath {
400+ Port : config .ClientConfig .Service .Port ,
401+ Path : * config .ClientConfig .Service .Path ,
402+ }
403+
404+ if _ , ok := uniqueConfig [serviceportPath ]; ! ok {
405+ uniqueConfig [serviceportPath ] = make ([]string , 0 )
406+ }
407+
408+ uniqueConfig [serviceportPath ] = append (uniqueConfig [serviceportPath ], crdName )
409+ }
410+
411+ return uniqueConfig
412+ }
413+
414+ func getConvWebhookCRDNamesAndConfig (c * collector.Manifests , serviceName string ) map [string ]apiextv1.WebhookConversion {
415+ if serviceName == "" {
416+ return nil
417+ }
418+
419+ crdToConfig := make (map [string ]apiextv1.WebhookConversion )
420+
421+ for _ , crd := range c .V1CustomResourceDefinitions {
422+ if crd .Spec .Conversion != nil {
423+ whConv := crd .Spec .Conversion .Webhook
424+ if whConv != nil && whConv .ClientConfig != nil && whConv .ClientConfig .Service != nil {
425+ if whConv .ClientConfig .Service .Name == serviceName {
426+ crdToConfig [crd .GetName ()] = * whConv
427+ }
428+ }
429+ }
430+ }
431+
432+ for _ , crd := range c .V1beta1CustomResourceDefinitions {
433+ whConv := crd .Spec .Conversion
434+ if whConv != nil && whConv .WebhookClientConfig != nil && whConv .WebhookClientConfig .Service != nil {
435+ if whConv .WebhookClientConfig .Service .Name == serviceName {
436+ v1whConv := apiextv1.WebhookConversion {
437+ ClientConfig : & apiextv1.WebhookClientConfig {Service : & apiextv1.ServiceReference {}},
438+ ConversionReviewVersions : crd .Spec .Conversion .ConversionReviewVersions ,
439+ }
440+ if path := whConv .WebhookClientConfig .Service .Path ; path != nil {
441+ v1whConv .ClientConfig .Service .Path = new (string )
442+ * v1whConv .ClientConfig .Service .Path = * path
443+ }
444+ crdToConfig [crd .GetName ()] = v1whConv
445+ }
446+ }
447+ }
448+ return crdToConfig
449+ }
450+
301451// The default AdmissionReviewVersions set in a CSV if not set in the source webhook.
302452var defaultAdmissionReviewVersions = []string {"v1beta1" }
303453
@@ -391,8 +541,7 @@ func mutatingToWebhookDescription(webhook admissionregv1.MutatingWebhook, depNam
391541}
392542
393543// findMatchingDeploymentAndServiceForWebhook matches a Service to a webhook's client config (if it uses a service)
394- // then matches that Service to a Deployment by comparing label selectors (if the Service uses label selectors).
395- // The names of both Service and Deployment are returned if found.
544+ // and uses that service to find the deployment name.
396545func findMatchingDeploymentAndServiceForWebhook (c * collector.Manifests , wcc admissionregv1.WebhookClientConfig ) (depName string , ws * corev1.Service ) {
397546 // Return if a service reference is not specified, since a URL will be in that case.
398547 if wcc .Service == nil {
@@ -422,15 +571,23 @@ func findMatchingDeploymentAndServiceForWebhook(c *collector.Manifests, wcc admi
422571 return
423572 }
424573
425- // Match service against pod labels, in which the webhook server will be running.
574+ depName = findMatchingDepNameFromService (c , ws )
575+
576+ return depName , ws
577+ }
578+
579+ // findMatchingDepNameFromService matches the provided service to a deployment by comparing label selectors (if
580+ // Service uses label selectors).
581+ func findMatchingDepNameFromService (c * collector.Manifests , ws * corev1.Service ) (depName string ) {
582+ // Match service against pod labels, in which the webhook server will be running
426583 for _ , dep := range c .Deployments {
427584 podTemplateLabels := dep .Spec .Template .GetLabels ()
428585 if len (podTemplateLabels ) == 0 {
429586 continue
430587 }
431588
432589 depName = dep .GetName ()
433- // Check that all labels match.
590+ // Check that all labels match
434591 for key , serviceValue := range ws .Spec .Selector {
435592 if podTemplateValue , hasKey := podTemplateLabels [key ]; ! hasKey || podTemplateValue != serviceValue {
436593 depName = ""
@@ -441,8 +598,7 @@ func findMatchingDeploymentAndServiceForWebhook(c *collector.Manifests, wcc admi
441598 break
442599 }
443600 }
444-
445- return depName , ws
601+ return depName
446602}
447603
448604// applyCustomResources updates csv's "alm-examples" annotation with the
@@ -495,3 +651,16 @@ func validate(csv *operatorsv1alpha1.ClusterServiceVersion) error {
495651
496652 return nil
497653}
654+
655+ // generateName takes in a list of crds, and returns a conversion webhook generator name.
656+ func getGenerateName (crds []string ) string {
657+ sort .Strings (crds )
658+ joinedResourceNames := strings.Builder {}
659+
660+ for _ , name := range crds {
661+ if name != "" {
662+ joinedResourceNames .WriteString (strings .Split (name , "." )[0 ])
663+ }
664+ }
665+ return fmt .Sprintf ("c%s.kb.io" , joinedResourceNames .String ())
666+ }
0 commit comments