Skip to content

Commit 94e5940

Browse files
author
Per Goncalves da Silva
committed
add webhook bundle validators
Signed-off-by: Per Goncalves da Silva <[email protected]>
1 parent 08cf766 commit 94e5940

File tree

3 files changed

+682
-12
lines changed

3 files changed

+682
-12
lines changed

Diff for: internal/operator-controller/rukpak/convert/registryv1_test.go

+29-12
Original file line numberDiff line numberDiff line change
@@ -570,23 +570,40 @@ func TestRegistryV1SuiteGenerateWebhooks_WebhookSupportFGEnabled(t *testing.T) {
570570
t.Log("It should enforce limitations")
571571
t.Log("It should allow bundles with webhooks")
572572
t.Log("By creating a registry v1 bundle")
573-
csv := v1alpha1.ClusterServiceVersion{
574-
ObjectMeta: metav1.ObjectMeta{
575-
Name: "testCSV",
576-
},
577-
Spec: v1alpha1.ClusterServiceVersionSpec{
578-
InstallModes: []v1alpha1.InstallMode{{Type: v1alpha1.InstallModeTypeAllNamespaces, Supported: true}},
579-
WebhookDefinitions: []v1alpha1.WebhookDescription{{ConversionCRDs: []string{"fake-webhook.package-with-webhooks.io"}}},
580-
},
581-
}
582-
watchNamespaces := []string{metav1.NamespaceAll}
583573
registryv1Bundle := render.RegistryV1{
584574
PackageName: "testPkg",
585-
CSV: csv,
575+
CRDs: []apiextensionsv1.CustomResourceDefinition{
576+
{
577+
ObjectMeta: metav1.ObjectMeta{
578+
Name: "fake-webhook.package-with-webhooks.io",
579+
},
580+
},
581+
},
582+
CSV: MakeCSV(
583+
WithName("testCSV"),
584+
WithInstallModeSupportFor(v1alpha1.InstallModeTypeAllNamespaces),
585+
WithOwnedCRDs(
586+
v1alpha1.CRDDescription{
587+
Name: "fake-webhook.package-with-webhooks.io",
588+
},
589+
),
590+
WithStrategyDeploymentSpecs(
591+
v1alpha1.StrategyDeploymentSpec{
592+
Name: "some-deployment",
593+
},
594+
),
595+
WithWebhookDefinitions(
596+
v1alpha1.WebhookDescription{
597+
Type: v1alpha1.ConversionWebhook,
598+
ConversionCRDs: []string{"fake-webhook.package-with-webhooks.io"},
599+
DeploymentName: "some-deployment",
600+
},
601+
),
602+
),
586603
}
587604

588605
t.Log("By converting to plain")
589-
plainBundle, err := convert.PlainConverter.Convert(registryv1Bundle, installNamespace, watchNamespaces)
606+
plainBundle, err := convert.PlainConverter.Convert(registryv1Bundle, installNamespace, []string{metav1.NamespaceAll})
590607
require.NoError(t, err)
591608
require.NotNil(t, plainBundle)
592609
}

Diff for: internal/operator-controller/rukpak/render/validators/validator.go

+127
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
package validators
22

33
import (
4+
"cmp"
45
"errors"
56
"fmt"
7+
"maps"
68
"slices"
9+
"strings"
710

811
"k8s.io/apimachinery/pkg/util/sets"
912

@@ -21,6 +24,10 @@ var RegistryV1BundleValidator = render.BundleValidator{
2124
CheckCRDResourceUniqueness,
2225
CheckOwnedCRDExistence,
2326
CheckPackageNameNotEmpty,
27+
CheckWebhookDeploymentReferentialIntegrity,
28+
CheckWebhookNameUniqueness,
29+
CheckConversionWebhookCRDReferenceUniqueness,
30+
CheckConversionWebhooksReferenceOwnedCRDs,
2431
}
2532

2633
// CheckDeploymentSpecUniqueness checks that each strategy deployment spec in the csv has a unique name.
@@ -109,3 +116,123 @@ func CheckWebhookSupport(rv1 *render.RegistryV1) []error {
109116

110117
return nil
111118
}
119+
120+
// CheckWebhookDeploymentReferentialIntegrity checks that each webhook definition in the csv
121+
// references an existing strategy deployment spec. Errors are sorted by strategy deployment spec name,
122+
// webhook type, and webhook name.
123+
func CheckWebhookDeploymentReferentialIntegrity(rv1 *render.RegistryV1) []error {
124+
webhooksByDeployment := map[string][]v1alpha1.WebhookDescription{}
125+
for _, wh := range rv1.CSV.Spec.WebhookDefinitions {
126+
webhooksByDeployment[wh.DeploymentName] = append(webhooksByDeployment[wh.DeploymentName], wh)
127+
}
128+
129+
for _, depl := range rv1.CSV.Spec.InstallStrategy.StrategySpec.DeploymentSpecs {
130+
delete(webhooksByDeployment, depl.Name)
131+
}
132+
133+
var errs []error
134+
// Loop through sorted keys to keep error messages ordered by deployment name
135+
for _, deploymentName := range slices.Sorted(maps.Keys(webhooksByDeployment)) {
136+
webhookDefns := webhooksByDeployment[deploymentName]
137+
slices.SortFunc(webhookDefns, func(a, b v1alpha1.WebhookDescription) int {
138+
return cmp.Or(cmp.Compare(a.Type, b.Type), cmp.Compare(a.GenerateName, b.GenerateName))
139+
})
140+
for _, webhookDef := range webhookDefns {
141+
errs = append(errs, fmt.Errorf("webhook '%s' of type '%s' references non-existent deployment '%s'", webhookDef.GenerateName, webhookDef.Type, webhookDef.DeploymentName))
142+
}
143+
}
144+
return errs
145+
}
146+
147+
// CheckWebhookNameUniqueness checks that each webhook definition of each type (validating, mutating, or conversion)
148+
// has a unique name. Webhooks of different types can have the same name. Errors are sorted by webhook type
149+
// and name.
150+
func CheckWebhookNameUniqueness(rv1 *render.RegistryV1) []error {
151+
webhookNameSetByType := map[v1alpha1.WebhookAdmissionType]sets.Set[string]{}
152+
duplicateWebhooksByType := map[v1alpha1.WebhookAdmissionType]sets.Set[string]{}
153+
for _, wh := range rv1.CSV.Spec.WebhookDefinitions {
154+
if _, ok := webhookNameSetByType[wh.Type]; !ok {
155+
webhookNameSetByType[wh.Type] = sets.Set[string]{}
156+
}
157+
if webhookNameSetByType[wh.Type].Has(wh.GenerateName) {
158+
if _, ok := duplicateWebhooksByType[wh.Type]; !ok {
159+
duplicateWebhooksByType[wh.Type] = sets.Set[string]{}
160+
}
161+
duplicateWebhooksByType[wh.Type].Insert(wh.GenerateName)
162+
}
163+
webhookNameSetByType[wh.Type].Insert(wh.GenerateName)
164+
}
165+
166+
var errs []error
167+
for _, whType := range slices.Sorted(maps.Keys(duplicateWebhooksByType)) {
168+
for _, webhookName := range slices.Sorted(slices.Values(duplicateWebhooksByType[whType].UnsortedList())) {
169+
errs = append(errs, fmt.Errorf("duplicate webhook '%s' of type '%s'", webhookName, whType))
170+
}
171+
}
172+
return errs
173+
}
174+
175+
// CheckConversionWebhooksReferenceOwnedCRDs checks defined conversion webhooks reference bundle owned CRDs.
176+
// Errors are sorted by webhook name and CRD name.
177+
func CheckConversionWebhooksReferenceOwnedCRDs(rv1 *render.RegistryV1) []error {
178+
//nolint:prealloc
179+
var conversionWebhooks []v1alpha1.WebhookDescription
180+
for _, wh := range rv1.CSV.Spec.WebhookDefinitions {
181+
if wh.Type != v1alpha1.ConversionWebhook {
182+
continue
183+
}
184+
conversionWebhooks = append(conversionWebhooks, wh)
185+
}
186+
187+
if len(conversionWebhooks) == 0 {
188+
return nil
189+
}
190+
191+
ownedCRDNames := sets.Set[string]{}
192+
for _, crd := range rv1.CSV.Spec.CustomResourceDefinitions.Owned {
193+
ownedCRDNames.Insert(crd.Name)
194+
}
195+
196+
slices.SortFunc(conversionWebhooks, func(a, b v1alpha1.WebhookDescription) int {
197+
return cmp.Compare(a.GenerateName, b.GenerateName)
198+
})
199+
200+
var errs []error
201+
for _, webhook := range conversionWebhooks {
202+
webhookCRDs := webhook.ConversionCRDs
203+
slices.Sort(webhookCRDs)
204+
for _, crd := range webhookCRDs {
205+
if !ownedCRDNames.Has(crd) {
206+
errs = append(errs, fmt.Errorf("conversion webhook '%s' references custom resource definition '%s' not owned bundle", webhook.GenerateName, crd))
207+
}
208+
}
209+
}
210+
return errs
211+
}
212+
213+
// CheckConversionWebhookCRDReferenceUniqueness checks no two (or more) conversion webhooks reference the same CRD.
214+
func CheckConversionWebhookCRDReferenceUniqueness(rv1 *render.RegistryV1) []error {
215+
// collect webhooks by crd
216+
crdToWh := map[string][]string{}
217+
for _, wh := range rv1.CSV.Spec.WebhookDefinitions {
218+
if wh.Type != v1alpha1.ConversionWebhook {
219+
continue
220+
}
221+
for _, crd := range wh.ConversionCRDs {
222+
crdToWh[crd] = append(crdToWh[crd], wh.GenerateName)
223+
}
224+
}
225+
226+
// remove crds with single webhook
227+
maps.DeleteFunc(crdToWh, func(crd string, whs []string) bool {
228+
return len(whs) == 1
229+
})
230+
231+
errs := make([]error, 0, len(crdToWh))
232+
orderedCRDs := slices.Sorted(maps.Keys(crdToWh))
233+
for _, crd := range orderedCRDs {
234+
orderedWhs := strings.Join(slices.Sorted(slices.Values(crdToWh[crd])), ",")
235+
errs = append(errs, fmt.Errorf("conversion webhooks [%s] reference same custom resource definition '%s'", orderedWhs, crd))
236+
}
237+
return errs
238+
}

0 commit comments

Comments
 (0)