Skip to content

Commit 1f9c5f4

Browse files
Add conversion webhook definition to csv
This PR adds the conversion webhook definition to csv. The csv generator includes the logic to verfiy if the {v1, v1beta1} crd has conversion clinet config defined. If so, it scaffolds the conversion wh configuration defails in the csv Signed-off-by: varshaprasad96 <[email protected]>
1 parent 40221aa commit 1f9c5f4

File tree

6 files changed

+263
-6
lines changed

6 files changed

+263
-6
lines changed

internal/generate/clusterserviceversion/clusterserviceversion_test.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -252,6 +252,10 @@ var _ = Describe("Generating a ClusterServiceVersion", func() {
252252
csv, err := g.generate()
253253
Expect(err).ToNot(HaveOccurred())
254254
Expect(csv).To(Equal(updatedCSV))
255+
256+
// verify if conversion webhooks are added
257+
Expect(len(csv.Spec.WebhookDefinitions)).NotTo(Equal(0))
258+
Expect(containsConversionWebhookDefinition(csv.Spec.WebhookDefinitions)).To(BeTrue())
255259
})
256260
})
257261

@@ -395,3 +399,12 @@ func upgradeCSV(csv *v1alpha1.ClusterServiceVersion, name, version string) *v1al
395399

396400
return upgraded
397401
}
402+
403+
func containsConversionWebhookDefinition(whdef []v1alpha1.WebhookDescription) bool {
404+
for _, def := range whdef {
405+
if def.Type == v1alpha1.ConversionWebhook {
406+
return true
407+
}
408+
}
409+
return false
410+
}

internal/generate/clusterserviceversion/clusterserviceversion_updaters.go

Lines changed: 175 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -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.
3847
func 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.
302452
var 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.
396545
func 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+
}

internal/generate/clusterserviceversion/clusterserviceversion_updaters_test.go

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -332,6 +332,49 @@ var _ = Describe("findMatchingDeploymentAndServiceForWebhook", func() {
332332
Expect(service.GetName()).To(Equal(serviceName1))
333333
})
334334
})
335+
336+
Context("match webhookconfigs with same service and path", func() {
337+
path1 := "/whPath"
338+
port1 := new(int32)
339+
*port1 = 2311
340+
crdToConfigPath := map[string]apiextv1.WebhookConversion{
341+
"crd-test-1": apiextv1.WebhookConversion{
342+
ClientConfig: &apiextv1.WebhookClientConfig{
343+
Service: &apiextv1.ServiceReference{
344+
Path: &path1,
345+
Port: port1,
346+
},
347+
},
348+
},
349+
350+
"crd-test-2": apiextv1.WebhookConversion{
351+
ClientConfig: &apiextv1.WebhookClientConfig{
352+
Service: &apiextv1.ServiceReference{
353+
Path: &path1,
354+
Port: port1,
355+
},
356+
},
357+
},
358+
}
359+
360+
val := crdGroups(crdToConfigPath)
361+
362+
Expect(len(val)).To(BeEquivalentTo(1))
363+
364+
test := serviceportPath{
365+
Port: port1,
366+
Path: path1,
367+
}
368+
369+
g := val[test]
370+
371+
Expect(g).NotTo(BeNil())
372+
Expect(len(g)).To(BeEquivalentTo(2))
373+
Expect(g).To(ContainElement("crd-test-2"))
374+
Expect(g).To(ContainElement("crd-test-1"))
375+
376+
})
377+
335378
})
336379

337380
func newDeployment(name string, labels map[string]string) (dep appsv1.Deployment) {

internal/generate/testdata/clusterserviceversions/output/memcached-operator.clusterserviceversion.yaml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -197,3 +197,14 @@ spec:
197197
targetPort: 9443
198198
type: MutatingAdmissionWebhook
199199
webhookPath: /mutate-cache-my-domain-v1alpha1-memcached
200+
- admissionReviewVersions:
201+
- v1beta1
202+
containerPort: 443
203+
conversionCRDs:
204+
- memcacheds.cache.example.com
205+
deploymentName: memcached-operator-controller-manager
206+
generateName: cmemcacheds.kb.io
207+
sideEffects: None
208+
targetPort: 9443
209+
type: ConversionWebhook
210+
webhookPath: /convert

internal/generate/testdata/clusterserviceversions/output/with-ui-metadata.clusterserviceversion.yaml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -253,3 +253,14 @@ spec:
253253
targetPort: 9443
254254
type: MutatingAdmissionWebhook
255255
webhookPath: /mutate-cache-my-domain-v1alpha1-memcached
256+
- admissionReviewVersions:
257+
- v1beta1
258+
containerPort: 443
259+
conversionCRDs:
260+
- memcacheds.cache.example.com
261+
deploymentName: memcached-operator-controller-manager
262+
generateName: cmemcacheds.kb.io
263+
sideEffects: None
264+
targetPort: 9443
265+
type: ConversionWebhook
266+
webhookPath: /convert

internal/generate/testdata/go/static/basic.operator.yaml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,16 @@ metadata:
1313
creationTimestamp: null
1414
name: memcacheds.cache.example.com
1515
spec:
16+
conversion:
17+
strategy: Webhook
18+
webhookClientConfig:
19+
caBundle: Cg==
20+
service:
21+
name: memcached-operator-webhook-service
22+
namespace: memcached-operator-system
23+
path: /convert
24+
conversionReviewVersions:
25+
- v1beta1
1626
group: cache.example.com
1727
names:
1828
kind: Memcached

0 commit comments

Comments
 (0)