Skip to content

Commit

Permalink
[v1.0.x] generate: correctly set CSV webhookDefinition deployment n…
Browse files Browse the repository at this point in the history
…ames (#3904)

Co-authored-by: Eric Stroczynski <[email protected]>
  • Loading branch information
openshift-cherrypick-robot and estroz authored Sep 17, 2020
1 parent c48d05c commit 11ad4ee
Show file tree
Hide file tree
Showing 6 changed files with 374 additions and 8 deletions.
6 changes: 6 additions & 0 deletions changelog/fragments/3761.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
entries:
- description: >
`generate <bundle|packagemanifests>` will populate a CSV's `webhookDefinition[].deploymentName`
by selecting an input Deployment via its PodTemplate labels using a webhook Service's label selectors,
defaulting to "<service.metadata.name>-service" if none is selected.
kind: bugfix
Original file line number Diff line number Diff line change
Expand Up @@ -262,21 +262,32 @@ func applyCustomResourceDefinitions(c *collector.Manifests, csv *operatorsv1alph
csv.Spec.CustomResourceDefinitions.Owned = ownedDescs
}

// applyWebhooks updates csv's webhookDefinitions with any
// mutating and validating webhooks in the collector.
// applyWebhooks updates csv's webhookDefinitions with any mutating and validating webhooks in the collector.
func applyWebhooks(c *collector.Manifests, csv *operatorsv1alpha1.ClusterServiceVersion) {
webhookDescriptions := []operatorsv1alpha1.WebhookDescription{}
for _, webhook := range c.ValidatingWebhooks {
webhookDescriptions = append(webhookDescriptions, validatingToWebhookDescription(webhook))
depName, serviceName := findMatchingDeploymentAndServiceForWebhook(c, webhook.ClientConfig)
if serviceName == "" && depName == "" {
log.Infof("No service found for validating webhook %q", webhook.Name)
} else if depName == "" {
log.Infof("No deployment is selected by service %q for validating webhook %q", serviceName, webhook.Name)
}
webhookDescriptions = append(webhookDescriptions, validatingToWebhookDescription(webhook, depName))
}
for _, webhook := range c.MutatingWebhooks {
webhookDescriptions = append(webhookDescriptions, mutatingToWebhookDescription(webhook))
depName, serviceName := findMatchingDeploymentAndServiceForWebhook(c, webhook.ClientConfig)
if serviceName == "" && depName == "" {
log.Infof("No service found for mutating webhook %q", webhook.Name)
} else if depName == "" {
log.Infof("No deployment is selected by service %q for mutating webhook %q", serviceName, webhook.Name)
}
webhookDescriptions = append(webhookDescriptions, mutatingToWebhookDescription(webhook, depName))
}
csv.Spec.WebhookDefinitions = webhookDescriptions
}

// validatingToWebhookDescription transforms webhook into a WebhookDescription.
func validatingToWebhookDescription(webhook admissionregv1.ValidatingWebhook) operatorsv1alpha1.WebhookDescription {
func validatingToWebhookDescription(webhook admissionregv1.ValidatingWebhook, depName string) operatorsv1alpha1.WebhookDescription {
description := operatorsv1alpha1.WebhookDescription{
Type: operatorsv1alpha1.ValidatingAdmissionWebhook,
GenerateName: webhook.Name,
Expand All @@ -288,18 +299,22 @@ func validatingToWebhookDescription(webhook admissionregv1.ValidatingWebhook) op
TimeoutSeconds: webhook.TimeoutSeconds,
AdmissionReviewVersions: webhook.AdmissionReviewVersions,
}

if serviceRef := webhook.ClientConfig.Service; serviceRef != nil {
if serviceRef.Port != nil {
description.ContainerPort = *serviceRef.Port
}
description.DeploymentName = strings.TrimSuffix(serviceRef.Name, "-service")
description.DeploymentName = depName
if description.DeploymentName == "" {
description.DeploymentName = strings.TrimSuffix(serviceRef.Name, "-service")
}
description.WebhookPath = serviceRef.Path
}
return description
}

// mutatingToWebhookDescription transforms webhook into a WebhookDescription.
func mutatingToWebhookDescription(webhook admissionregv1.MutatingWebhook) operatorsv1alpha1.WebhookDescription {
func mutatingToWebhookDescription(webhook admissionregv1.MutatingWebhook, depName string) operatorsv1alpha1.WebhookDescription {
description := operatorsv1alpha1.WebhookDescription{
Type: operatorsv1alpha1.MutatingAdmissionWebhook,
GenerateName: webhook.Name,
Expand All @@ -312,16 +327,77 @@ func mutatingToWebhookDescription(webhook admissionregv1.MutatingWebhook) operat
AdmissionReviewVersions: webhook.AdmissionReviewVersions,
ReinvocationPolicy: webhook.ReinvocationPolicy,
}

if serviceRef := webhook.ClientConfig.Service; serviceRef != nil {
if serviceRef.Port != nil {
description.ContainerPort = *serviceRef.Port
}
description.DeploymentName = strings.TrimSuffix(serviceRef.Name, "-service")
description.DeploymentName = depName
if description.DeploymentName == "" {
description.DeploymentName = strings.TrimSuffix(serviceRef.Name, "-service")
}
description.WebhookPath = serviceRef.Path
}
return description
}

// findMatchingDeploymentAndServiceForWebhook matches a Service to a webhook's client config (if it uses a service)
// then matches that Service to a Deployment by comparing label selectors (if the Service uses label selectors).
// The names of both Service and Deployment are returned if found.
func findMatchingDeploymentAndServiceForWebhook(c *collector.Manifests, wcc admissionregv1.WebhookClientConfig) (depName, serviceName string) {
// Return if a service reference is not specified, since a URL will be in that case.
if wcc.Service == nil {
return
}

// Find the matching service, if any. The webhook server may be externally managed
// if no service is created by the operator.
var ws *corev1.Service
for i, service := range c.Services {
if service.GetName() == wcc.Service.Name {
ws = &c.Services[i]
break
}
}
if ws == nil {
return
}
serviceName = ws.GetName()

// Only ExternalName-type services cannot have selectors.
if ws.Spec.Type == corev1.ServiceTypeExternalName {
return
}

// If a selector does not exist, there is either an Endpoint or EndpointSlice object accompanying
// the service so it should not be added to the CSV.
if len(ws.Spec.Selector) == 0 {
return
}

// Match service against pod labels, in which the webhook server will be running.
for _, dep := range c.Deployments {
podTemplateLabels := dep.Spec.Template.GetLabels()
if len(podTemplateLabels) == 0 {
continue
}

depName = dep.GetName()
// Check that all labels match.
for key, serviceValue := range ws.Spec.Selector {
if podTemplateValue, hasKey := podTemplateLabels[key]; !hasKey || podTemplateValue != serviceValue {
depName = ""
break
}
}
if depName != "" {
break
}
}

return depName, serviceName
}

// applyCustomResources updates csv's "alm-examples" annotation with the
// Custom Resources in the collector.
func applyCustomResources(c *collector.Manifests, csv *operatorsv1alpha1.ClusterServiceVersion) error {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
// Copyright 2020 The Operator-SDK Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package clusterserviceversion

import (
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
admissionregv1 "k8s.io/api/admissionregistration/v1"
appsv1 "k8s.io/api/apps/v1"
corev1 "k8s.io/api/core/v1"

"github.com/operator-framework/operator-sdk/internal/generate/collector"
)

var _ = Describe("findMatchingDeploymentAndServiceForWebhook", func() {

var (
c *collector.Manifests
wcc admissionregv1.WebhookClientConfig

depName1 = "dep-name-1"
depName2 = "dep-name-2"
serviceName1 = "service-name-1"
serviceName2 = "service-name-2"
)

BeforeEach(func() {
c = &collector.Manifests{}
wcc = admissionregv1.WebhookClientConfig{}
wcc.Service = &admissionregv1.ServiceReference{}
})

Context("webhook config has a matching service name", func() {
By("parsing one deployment and one service with one label")
It("returns the first service and deployment", func() {
labels := map[string]string{"operator-name": "test-operator"}
c.Deployments = []appsv1.Deployment{newDeployment(depName1, labels)}
c.Services = []corev1.Service{newService(serviceName1, labels)}
wcc.Service.Name = serviceName1
depName, serviceName := findMatchingDeploymentAndServiceForWebhook(c, wcc)
Expect(depName).To(Equal(depName1))
Expect(serviceName).To(Equal(serviceName1))
})

By("parsing two deployments and two services with non-intersecting labels")
It("returns the first service and deployment", func() {
labels1 := map[string]string{"operator-name": "test-operator"}
labels2 := map[string]string{"foo": "bar"}
c.Deployments = []appsv1.Deployment{
newDeployment(depName1, labels1),
newDeployment(depName2, labels2),
}
c.Services = []corev1.Service{
newService(serviceName1, labels1),
newService(serviceName2, labels2),
}
wcc.Service.Name = serviceName1
depName, serviceName := findMatchingDeploymentAndServiceForWebhook(c, wcc)
Expect(depName).To(Equal(depName1))
Expect(serviceName).To(Equal(serviceName1))
})

By("parsing two deployments and two services with a label subset")
It("returns the first service and second deployment", func() {
labels1 := map[string]string{"operator-name": "test-operator"}
labels2 := map[string]string{"operator-name": "test-operator", "foo": "bar"}
c.Deployments = []appsv1.Deployment{
newDeployment(depName2, labels2),
newDeployment(depName1, labels1),
}
c.Services = []corev1.Service{newService(serviceName1, labels1)}
wcc.Service.Name = serviceName1
depName, serviceName := findMatchingDeploymentAndServiceForWebhook(c, wcc)
Expect(depName).To(Equal(depName2))
Expect(serviceName).To(Equal(serviceName1))
})
})

Context("webhook config does not have a matching service", func() {
By("parsing one deployment and one service with one label")
It("returns neither service nor deployment name", func() {
labels := map[string]string{"operator-name": "test-operator"}
c.Deployments = []appsv1.Deployment{newDeployment(depName1, labels)}
c.Services = []corev1.Service{newService(serviceName1, labels)}
wcc.Service.Name = serviceName2
depName, serviceName := findMatchingDeploymentAndServiceForWebhook(c, wcc)
Expect(depName).To(BeEmpty())
Expect(serviceName).To(BeEmpty())
})
})

Context("webhook config has a matching service but labels do not match", func() {
By("parsing one deployment and one service with one label")
It("returns the first service and no deployment", func() {
labels1 := map[string]string{"operator-name": "test-operator"}
labels2 := map[string]string{"foo": "bar"}
c.Deployments = []appsv1.Deployment{newDeployment(depName1, labels1)}
c.Services = []corev1.Service{newService(serviceName1, labels2)}
wcc.Service.Name = serviceName1
depName, serviceName := findMatchingDeploymentAndServiceForWebhook(c, wcc)
Expect(depName).To(BeEmpty())
Expect(serviceName).To(Equal(serviceName1))
})

By("parsing one deployment and one service with two intersecting labels")
It("returns the first service and no deployment", func() {
labels1 := map[string]string{"operator-name": "test-operator", "foo": "bar"}
labels2 := map[string]string{"foo": "bar", "baz": "bat"}
c.Deployments = []appsv1.Deployment{newDeployment(depName1, labels1)}
c.Services = []corev1.Service{newService(serviceName1, labels2)}
wcc.Service.Name = serviceName1
depName, serviceName := findMatchingDeploymentAndServiceForWebhook(c, wcc)
Expect(depName).To(BeEmpty())
Expect(serviceName).To(Equal(serviceName1))
})
})
})

func newDeployment(name string, labels map[string]string) appsv1.Deployment {
dep := appsv1.Deployment{}
dep.SetName(name)
dep.Spec.Template.SetLabels(labels)
return dep
}

func newService(name string, labels map[string]string) corev1.Service {
s := corev1.Service{}
s.SetName(name)
s.Spec.Selector = labels
return s
}
18 changes: 18 additions & 0 deletions internal/generate/collector/manifests.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ type Manifests struct {
ClusterRoleBindings []rbacv1.ClusterRoleBinding
Deployments []appsv1.Deployment
ServiceAccounts []corev1.ServiceAccount
Services []corev1.Service
V1CustomResourceDefinitions []apiextv1.CustomResourceDefinition
V1beta1CustomResourceDefinitions []apiextv1beta1.CustomResourceDefinition
ValidatingWebhooks []admissionregv1.ValidatingWebhook
Expand All @@ -61,6 +62,7 @@ var (
roleBindingGK = rbacv1.SchemeGroupVersion.WithKind("RoleBinding").GroupKind()
clusterRoleBindingGK = rbacv1.SchemeGroupVersion.WithKind("ClusterRoleBinding").GroupKind()
serviceAccountGK = corev1.SchemeGroupVersion.WithKind("ServiceAccount").GroupKind()
serviceGK = corev1.SchemeGroupVersion.WithKind("Service").GroupKind()
deploymentGK = appsv1.SchemeGroupVersion.WithKind("Deployment").GroupKind()
crdGK = apiextv1.SchemeGroupVersion.WithKind("CustomResourceDefinition").GroupKind()
validatingWebhookCfgGK = admissionregv1.SchemeGroupVersion.WithKind("ValidatingWebhookConfiguration").GroupKind()
Expand Down Expand Up @@ -104,6 +106,8 @@ func (c *Manifests) UpdateFromDirs(deployDir, crdsDir string) error {
err = c.addClusterRoleBindings(manifest)
case serviceAccountGK:
err = c.addServiceAccounts(manifest)
case serviceGK:
err = c.addServices(manifest)
case deploymentGK:
err = c.addDeployments(manifest)
case crdGK:
Expand Down Expand Up @@ -172,6 +176,8 @@ func (c *Manifests) UpdateFromReader(r io.Reader) error {
err = c.addClusterRoleBindings(manifest)
case serviceAccountGK:
err = c.addServiceAccounts(manifest)
case serviceGK:
err = c.addServices(manifest)
case deploymentGK:
err = c.addDeployments(manifest)
case crdGK:
Expand Down Expand Up @@ -266,6 +272,18 @@ func (c *Manifests) addServiceAccounts(rawManifests ...[]byte) error {
return nil
}

// addServices assumes all manifest data in rawManifests are Services and adds them to the collector.
func (c *Manifests) addServices(rawManifests ...[]byte) error {
for _, rawManifest := range rawManifests {
s := corev1.Service{}
if err := yaml.Unmarshal(rawManifest, &s); err != nil {
return err
}
c.Services = append(c.Services, s)
}
return nil
}

// addDeployments assumes all manifest data in rawManifests are Deployments
// and adds them to the collector.
func (c *Manifests) addDeployments(rawManifests ...[]byte) error {
Expand Down
Loading

0 comments on commit 11ad4ee

Please sign in to comment.