Skip to content

Commit 8f50e41

Browse files
committed
Detect WebhookDescription changes in CSVs
Problem: Once a CSV has reached the Succeeded phase, updating WebhookDescriptions in the CSV on cluster does not cause OLM to update existing ValidatingWebhookConfigurations or MutatingWebhookConfigurations on cluster. Solution: Update OLM to calculate if a change to the WebhookDescription has occurred when a CSV is updated and if so update the on cluster resources.
1 parent fa4270b commit 8f50e41

File tree

4 files changed

+87
-1
lines changed

4 files changed

+87
-1
lines changed

pkg/controller/install/webhook.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,17 @@ package install
33
import (
44
"context"
55
"fmt"
6+
"hash/fnv"
67

78
"github.com/operator-framework/api/pkg/operators/v1alpha1"
9+
hashutil "github.com/operator-framework/operator-lifecycle-manager/pkg/lib/kubernetes/pkg/util/hash"
810
"github.com/operator-framework/operator-lifecycle-manager/pkg/lib/ownerutil"
911

1012
log "github.com/sirupsen/logrus"
1113
admissionregistrationv1 "k8s.io/api/admissionregistration/v1"
1214
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
1315
"k8s.io/apimachinery/pkg/labels"
16+
"k8s.io/apimachinery/pkg/util/rand"
1417
)
1518

1619
func ValidWebhookRules(rules []admissionregistrationv1.RuleWithOperations) error {
@@ -105,6 +108,7 @@ func (i *StrategyDeploymentInstaller) createOrUpdateMutatingWebhook(ogNamespacel
105108
webhook.Webhooks = []admissionregistrationv1.MutatingWebhook{
106109
desc.GetMutatingWebhook(i.owner.GetNamespace(), ogNamespacelabelSelector, caPEM),
107110
}
111+
addWebhookLabels(&webhook, desc)
108112

109113
// Attempt an update
110114
if _, err := i.strategyClient.GetOpClient().KubernetesInterface().AdmissionregistrationV1().MutatingWebhookConfigurations().Update(context.TODO(), &webhook, metav1.UpdateOptions{}); err != nil {
@@ -151,6 +155,7 @@ func (i *StrategyDeploymentInstaller) createOrUpdateValidatingWebhook(ogNamespac
151155
webhook.Webhooks = []admissionregistrationv1.ValidatingWebhook{
152156
desc.GetValidatingWebhook(i.owner.GetNamespace(), ogNamespacelabelSelector, caPEM),
153157
}
158+
addWebhookLabels(&webhook, desc)
154159

155160
// Attempt an update
156161
if _, err := i.strategyClient.GetOpClient().KubernetesInterface().AdmissionregistrationV1().ValidatingWebhookConfigurations().Update(context.TODO(), &webhook, metav1.UpdateOptions{}); err != nil {
@@ -163,6 +168,7 @@ func (i *StrategyDeploymentInstaller) createOrUpdateValidatingWebhook(ogNamespac
163168
}
164169

165170
const WebhookDescKey = "webhookDescriptionGenerateName"
171+
const WebhookHashKey = "webhookDescriptionSHA"
166172

167173
// addWebhookLabels adds webhook labels to an object
168174
func addWebhookLabels(object metav1.Object, webhookDesc v1alpha1.WebhookDescription) error {
@@ -171,7 +177,15 @@ func addWebhookLabels(object metav1.Object, webhookDesc v1alpha1.WebhookDescript
171177
labels = map[string]string{}
172178
}
173179
labels[WebhookDescKey] = webhookDesc.GenerateName
180+
labels[WebhookHashKey] = HashWebhookDesc(webhookDesc)
174181
object.SetLabels(labels)
175182

176183
return nil
177184
}
185+
186+
// HashWebhookDesc calculates a hash given a webhookDescription
187+
func HashWebhookDesc(webhookDesc v1alpha1.WebhookDescription) string {
188+
hasher := fnv.New32a()
189+
hashutil.DeepHashObject(hasher, &webhookDesc)
190+
return rand.SafeEncodeString(fmt.Sprint(hasher.Sum32()))
191+
}

pkg/controller/operators/olm/apiservices.go

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -486,3 +486,34 @@ func (a *Operator) updateDeploymentSpecsWithApiServiceData(csv *v1alpha1.Cluster
486486
}
487487
return strategyDetailsDeployment, nil
488488
}
489+
490+
func (a *Operator) areWebhooksAvailable(csv *v1alpha1.ClusterServiceVersion) (bool, error) {
491+
for _, desc := range csv.Spec.WebhookDefinitions {
492+
// Create Webhook Label Selector
493+
webhookLabels := ownerutil.OwnerLabel(csv, v1alpha1.ClusterServiceVersionKind)
494+
webhookLabels[install.WebhookDescKey] = desc.GenerateName
495+
webhookLabels[install.WebhookHashKey] = install.HashWebhookDesc(desc)
496+
webhookSelector := labels.SelectorFromSet(webhookLabels).String()
497+
498+
webhookCount := 0
499+
switch desc.Type {
500+
case v1alpha1.ValidatingAdmissionWebhook:
501+
webhookList, err := a.opClient.KubernetesInterface().AdmissionregistrationV1().ValidatingWebhookConfigurations().List(context.TODO(), metav1.ListOptions{LabelSelector: webhookSelector})
502+
if err != nil {
503+
return false, err
504+
}
505+
webhookCount = len(webhookList.Items)
506+
case v1alpha1.MutatingAdmissionWebhook:
507+
webhookList, err := a.opClient.KubernetesInterface().AdmissionregistrationV1().MutatingWebhookConfigurations().List(context.TODO(), metav1.ListOptions{LabelSelector: webhookSelector})
508+
if err != nil {
509+
return false, err
510+
}
511+
webhookCount = len(webhookList.Items)
512+
}
513+
if webhookCount == 0 {
514+
a.logger.Info("Expected Webhook does not exist")
515+
return false, nil
516+
}
517+
}
518+
return true, nil
519+
}

pkg/controller/operators/olm/operator.go

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1721,13 +1721,14 @@ func (a *Operator) checkReplacementsAndUpdateStatus(csv *v1alpha1.ClusterService
17211721
func (a *Operator) updateInstallStatus(csv *v1alpha1.ClusterServiceVersion, installer install.StrategyInstaller, strategy install.Strategy, requeuePhase v1alpha1.ClusterServiceVersionPhase, requeueConditionReason v1alpha1.ConditionReason) error {
17221722
apiServicesInstalled, apiServiceErr := a.areAPIServicesAvailable(csv)
17231723
strategyInstalled, strategyErr := installer.CheckInstalled(strategy)
1724+
webhooksInstalled, webhookErr := a.areWebhooksAvailable(csv)
17241725
now := a.now()
17251726

17261727
if strategyErr != nil {
17271728
a.logger.WithError(strategyErr).Debug("operator not installed")
17281729
}
17291730

1730-
if strategyInstalled && apiServicesInstalled {
1731+
if strategyInstalled && apiServicesInstalled && webhooksInstalled {
17311732
// if there's no error, we're successfully running
17321733
csv.SetPhaseWithEventIfChanged(v1alpha1.CSVPhaseSucceeded, v1alpha1.CSVReasonInstallSuccessful, "install strategy completed with no errors", now, a.recorder)
17331734
return nil
@@ -1753,6 +1754,16 @@ func (a *Operator) updateInstallStatus(csv *v1alpha1.ClusterServiceVersion, inst
17531754
return fmt.Errorf("APIServices not installed")
17541755
}
17551756

1757+
if !webhooksInstalled || webhookErr != nil {
1758+
msg := "Webhooks not installed"
1759+
csv.SetPhaseWithEventIfChanged(requeuePhase, requeueConditionReason, fmt.Sprintf(msg), now, a.recorder)
1760+
if err := a.csvQueueSet.Requeue(csv.GetNamespace(), csv.GetName()); err != nil {
1761+
a.logger.Warn(err.Error())
1762+
}
1763+
1764+
return fmt.Errorf(msg)
1765+
}
1766+
17561767
if strategyErr != nil {
17571768
csv.SetPhaseWithEventIfChanged(requeuePhase, requeueConditionReason, fmt.Sprintf("installing: %s", strategyErr), now, a.recorder)
17581769
if err := a.csvQueueSet.Requeue(csv.GetNamespace(), csv.GetName()); err != nil {

test/e2e/webhook_e2e_test.go

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,13 +119,15 @@ var _ = Describe("CSVs with a Webhook", func() {
119119
})
120120
It("Creates Webhooks scoped to a single namespace", func() {
121121
sideEffect := admissionregistrationv1.SideEffectClassNone
122+
timeout := int32(30)
122123
webhook := v1alpha1.WebhookDescription{
123124
GenerateName: webhookName,
124125
Type: v1alpha1.ValidatingAdmissionWebhook,
125126
DeploymentName: genName("webhook-dep-"),
126127
ContainerPort: 443,
127128
AdmissionReviewVersions: []string{"v1beta1", "v1"},
128129
SideEffects: &sideEffect,
130+
TimeoutSeconds: &timeout,
129131
}
130132

131133
csv := createCSVWithWebhook(namespace.GetName(), webhook)
@@ -147,6 +149,34 @@ var _ = Describe("CSVs with a Webhook", func() {
147149
MatchExpressions: []metav1.LabelSelectorRequirement(nil),
148150
}
149151
Expect(actualWebhook.Webhooks[0].NamespaceSelector).Should(Equal(expected))
152+
153+
// Ensure that changes to the WebhookDescription within the CSV trigger an update to on cluster resources
154+
newTimeout := int32(5)
155+
Eventually(func() (bool, error) {
156+
existingCSV, err := crc.OperatorsV1alpha1().ClusterServiceVersions(namespace.Name).Get(context.TODO(), csv.GetName(), metav1.GetOptions{})
157+
if err != nil {
158+
return false, err
159+
}
160+
existingCSV.Spec.WebhookDefinitions[0].TimeoutSeconds = &newTimeout
161+
162+
existingCSV, err = crc.OperatorsV1alpha1().ClusterServiceVersions(namespace.Name).Update(context.TODO(), existingCSV, metav1.UpdateOptions{})
163+
if err != nil {
164+
return false, nil
165+
}
166+
return true, nil
167+
}, time.Minute, 5*time.Second).Should(BeTrue())
168+
Eventually(func() bool {
169+
actualWebhook, err = getWebhookWithGenName(c, webhook)
170+
if err != nil {
171+
return false
172+
}
173+
174+
if *actualWebhook.Webhooks[0].TimeoutSeconds != newTimeout {
175+
return false
176+
}
177+
178+
return true
179+
}, time.Minute, 5*time.Second).Should(BeTrue())
150180
})
151181
It("Fails to install a CSV if multiple Webhooks share the same name", func() {
152182
sideEffect := admissionregistrationv1.SideEffectClassNone

0 commit comments

Comments
 (0)