11package convert
22
33import (
4+ "cmp"
45 "errors"
56 "fmt"
7+ "maps"
68 "slices"
79
810 "k8s.io/apimachinery/pkg/util/sets"
11+
12+ "github.com/operator-framework/api/pkg/operators/v1alpha1"
913)
1014
1115type BundleValidator []func (v1 * RegistryV1 ) []error
@@ -25,6 +29,9 @@ var RegistryV1BundleValidator = BundleValidator{
2529 CheckDeploymentSpecUniqueness ,
2630 CheckCRDResourceUniqueness ,
2731 CheckOwnedCRDExistence ,
32+ CheckWebhookDeploymentReferentialIntegrity ,
33+ CheckWebhookNameUniqueness ,
34+ CheckConversionWebhookCRDReferences ,
2835}
2936
3037// CheckDeploymentSpecUniqueness checks that each strategy deployment spec in the csv has a unique name.
@@ -87,3 +94,96 @@ func CheckCRDResourceUniqueness(rv1 *RegistryV1) []error {
8794 }
8895 return errs
8996}
97+
98+ // CheckWebhookDeploymentReferentialIntegrity checks that each webhook definition in the csv
99+ // references an existing strategy deployment spec. Errors are sorted by strategy deployment spec name,
100+ // webhook type, and webhook name.
101+ func CheckWebhookDeploymentReferentialIntegrity (rv1 * RegistryV1 ) []error {
102+ webhooksByDeployment := map [string ][]v1alpha1.WebhookDescription {}
103+ for _ , wh := range rv1 .CSV .Spec .WebhookDefinitions {
104+ webhooksByDeployment [wh .DeploymentName ] = append (webhooksByDeployment [wh .DeploymentName ], wh )
105+ }
106+
107+ for _ , depl := range rv1 .CSV .Spec .InstallStrategy .StrategySpec .DeploymentSpecs {
108+ delete (webhooksByDeployment , depl .Name )
109+ }
110+
111+ var errs []error
112+ // Loop through sorted keys to keep error messages ordered by deployment name
113+ for _ , deploymentName := range slices .Sorted (maps .Keys (webhooksByDeployment )) {
114+ webhookDefns := webhooksByDeployment [deploymentName ]
115+ slices .SortFunc (webhookDefns , func (a , b v1alpha1.WebhookDescription ) int {
116+ return cmp .Or (cmp .Compare (a .Type , b .Type ), cmp .Compare (a .GenerateName , b .GenerateName ))
117+ })
118+ for _ , webhookDef := range webhookDefns {
119+ errs = append (errs , fmt .Errorf ("webhook '%s' of type '%s' references non-existent deployment '%s'" , webhookDef .GenerateName , webhookDef .Type , webhookDef .DeploymentName ))
120+ }
121+ }
122+ return errs
123+ }
124+
125+ // CheckWebhookNameUniqueness checks that each webhook definition of each type (validating, mutating, or conversion)
126+ // has a unique name. Webhooks of different types can have the same name. Errors are sorted by webhook type
127+ // and name.
128+ func CheckWebhookNameUniqueness (rv1 * RegistryV1 ) []error {
129+ webhookNameSetByType := map [v1alpha1.WebhookAdmissionType ]sets.Set [string ]{}
130+ duplicateWebhooksByType := map [v1alpha1.WebhookAdmissionType ]sets.Set [string ]{}
131+ for _ , wh := range rv1 .CSV .Spec .WebhookDefinitions {
132+ if _ , ok := webhookNameSetByType [wh .Type ]; ! ok {
133+ webhookNameSetByType [wh .Type ] = sets.Set [string ]{}
134+ }
135+ if webhookNameSetByType [wh .Type ].Has (wh .GenerateName ) {
136+ if _ , ok := duplicateWebhooksByType [wh .Type ]; ! ok {
137+ duplicateWebhooksByType [wh .Type ] = sets.Set [string ]{}
138+ }
139+ duplicateWebhooksByType [wh .Type ].Insert (wh .GenerateName )
140+ }
141+ webhookNameSetByType [wh .Type ].Insert (wh .GenerateName )
142+ }
143+
144+ var errs []error
145+ for _ , whType := range slices .Sorted (maps .Keys (duplicateWebhooksByType )) {
146+ for _ , webhookName := range slices .Sorted (slices .Values (duplicateWebhooksByType [whType ].UnsortedList ())) {
147+ errs = append (errs , fmt .Errorf ("duplicate webhook '%s' of type '%s'" , webhookName , whType ))
148+ }
149+ }
150+ return errs
151+ }
152+
153+ // CheckConversionWebhookCRDReferences checks defined conversion webhooks reference bundle owned CRDs.
154+ // Errors are sorted by webhook name and CRD name.
155+ func CheckConversionWebhookCRDReferences (rv1 * RegistryV1 ) []error {
156+ //nolint:prealloc
157+ var conversionWebhooks []v1alpha1.WebhookDescription
158+ for _ , wh := range rv1 .CSV .Spec .WebhookDefinitions {
159+ if wh .Type != v1alpha1 .ConversionWebhook {
160+ continue
161+ }
162+ conversionWebhooks = append (conversionWebhooks , wh )
163+ }
164+
165+ if len (conversionWebhooks ) == 0 {
166+ return nil
167+ }
168+
169+ ownedCRDNames := sets.Set [string ]{}
170+ for _ , crd := range rv1 .CSV .Spec .CustomResourceDefinitions .Owned {
171+ ownedCRDNames .Insert (crd .Name )
172+ }
173+
174+ slices .SortFunc (conversionWebhooks , func (a , b v1alpha1.WebhookDescription ) int {
175+ return cmp .Compare (a .GenerateName , b .GenerateName )
176+ })
177+
178+ var errs []error
179+ for _ , webhook := range conversionWebhooks {
180+ webhookCRDs := webhook .ConversionCRDs
181+ slices .Sort (webhookCRDs )
182+ for _ , crd := range webhookCRDs {
183+ if ! ownedCRDNames .Has (crd ) {
184+ errs = append (errs , fmt .Errorf ("conversion webhook '%s' references custom resource definition '%s' not owned bundle" , webhook .GenerateName , crd ))
185+ }
186+ }
187+ }
188+ return errs
189+ }
0 commit comments