Skip to content

Commit 1282f72

Browse files
author
Per Goncalves da Silva
committed
Add registry+v1 bundlvalidators
Signed-off-by: Per Goncalves da Silva <[email protected]>
1 parent f537b92 commit 1282f72

File tree

2 files changed

+314
-0
lines changed

2 files changed

+314
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
package convert
2+
3+
import (
4+
"fmt"
5+
"slices"
6+
7+
"k8s.io/apimachinery/pkg/util/sets"
8+
)
9+
10+
type BundleValidator []func(v1 *RegistryV1) []error
11+
12+
func (v BundleValidator) Validate(rv1 *RegistryV1) []error {
13+
var errs []error
14+
for _, validator := range v {
15+
errs = append(errs, validator(rv1)...)
16+
}
17+
return errs
18+
}
19+
20+
func NewBundleValidator() BundleValidator {
21+
// NOTE: if you update this list, Test_BundleValidatorHasAllValidationFns will fail until
22+
// you bring the same changes over to that test. This helps ensure all validation rules are executed
23+
// while giving us the flexibility to test each validation function individually
24+
return BundleValidator{
25+
CheckDeploymentSpecUniqueness,
26+
CheckCRDResourceUniqueness,
27+
CheckOwnedCRDExistence,
28+
}
29+
}
30+
31+
// CheckDeploymentSpecUniqueness checks that each strategy deployment spec in the csv has a unique name.
32+
// Errors are sorted by deployment name.
33+
func CheckDeploymentSpecUniqueness(rv1 *RegistryV1) []error {
34+
deploymentNameSet := sets.Set[string]{}
35+
duplicateDeploymentNames := sets.Set[string]{}
36+
for _, dep := range rv1.CSV.Spec.InstallStrategy.StrategySpec.DeploymentSpecs {
37+
if deploymentNameSet.Has(dep.Name) {
38+
duplicateDeploymentNames.Insert(dep.Name)
39+
}
40+
deploymentNameSet.Insert(dep.Name)
41+
}
42+
43+
//nolint:prealloc
44+
var errs []error
45+
for _, d := range slices.Sorted(slices.Values(duplicateDeploymentNames.UnsortedList())) {
46+
errs = append(errs, fmt.Errorf("cluster service version contains duplicate strategy deployment spec '%s'", d))
47+
}
48+
return errs
49+
}
50+
51+
// CheckOwnedCRDExistence checks bundle owned custom resource definitions declared in the csv exist in the bundle
52+
func CheckOwnedCRDExistence(rv1 *RegistryV1) []error {
53+
crdsNames := sets.Set[string]{}
54+
for _, crd := range rv1.CRDs {
55+
crdsNames.Insert(crd.Name)
56+
}
57+
58+
//nolint:prealloc
59+
var errs []error
60+
missingCRDNames := sets.Set[string]{}
61+
for _, crd := range rv1.CSV.Spec.CustomResourceDefinitions.Owned {
62+
if !crdsNames.Has(crd.Name) {
63+
missingCRDNames.Insert(crd.Name)
64+
}
65+
}
66+
67+
for _, crdName := range slices.Sorted(slices.Values(missingCRDNames.UnsortedList())) {
68+
errs = append(errs, fmt.Errorf("cluster service definition references owned custom resource definition '%s' not found in bundle", crdName))
69+
}
70+
return errs
71+
}
72+
73+
// CheckCRDResourceUniqueness checks that the bundle CRD names are unique
74+
func CheckCRDResourceUniqueness(rv1 *RegistryV1) []error {
75+
crdsNames := sets.Set[string]{}
76+
duplicateCRDNames := sets.Set[string]{}
77+
for _, crd := range rv1.CRDs {
78+
if crdsNames.Has(crd.Name) {
79+
duplicateCRDNames.Insert(crd.Name)
80+
}
81+
crdsNames.Insert(crd.Name)
82+
}
83+
84+
//nolint:prealloc
85+
var errs []error
86+
for _, crdName := range slices.Sorted(slices.Values(duplicateCRDNames.UnsortedList())) {
87+
errs = append(errs, fmt.Errorf("bundle contains duplicate custom resource definition '%s'", crdName))
88+
}
89+
return errs
90+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,224 @@
1+
package convert_test
2+
3+
import (
4+
"errors"
5+
"reflect"
6+
"testing"
7+
8+
"github.com/stretchr/testify/require"
9+
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
10+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
11+
12+
"github.com/operator-framework/api/pkg/operators/v1alpha1"
13+
14+
"github.com/operator-framework/operator-controller/internal/operator-controller/rukpak/convert"
15+
)
16+
17+
func Test_BundleValidatorHasAllValidationFns(t *testing.T) {
18+
expectedValidationFns := []func(v1 *convert.RegistryV1) []error{
19+
convert.CheckDeploymentSpecUniqueness,
20+
convert.CheckCRDResourceUniqueness,
21+
convert.CheckOwnedCRDExistence,
22+
}
23+
actualValidationFns := convert.NewBundleValidator()
24+
25+
require.Equal(t, len(expectedValidationFns), len(actualValidationFns))
26+
for i := range expectedValidationFns {
27+
require.Equal(t, reflect.ValueOf(expectedValidationFns[i]).Pointer(), reflect.ValueOf(actualValidationFns[i]).Pointer(), "bundle validator has unexpected validation function")
28+
}
29+
}
30+
31+
func Test_BundleValidatorCallsAllValidationFnsInOrder(t *testing.T) {
32+
actual := ""
33+
validator := convert.BundleValidator{
34+
func(v1 *convert.RegistryV1) []error {
35+
actual += "h"
36+
return nil
37+
},
38+
func(v1 *convert.RegistryV1) []error {
39+
actual += "i"
40+
return nil
41+
},
42+
}
43+
require.Empty(t, validator.Validate(nil))
44+
require.Equal(t, "hi", actual)
45+
}
46+
47+
func Test_CheckDeploymentSpecUniqueness(t *testing.T) {
48+
for _, tc := range []struct {
49+
name string
50+
bundle *convert.RegistryV1
51+
expectedErrs []error
52+
}{
53+
{
54+
name: "accepts bundles with unique deployment strategy spec names",
55+
bundle: &convert.RegistryV1{
56+
CSV: MakeCSV(
57+
WithStrategyDeploymentSpecs(
58+
v1alpha1.StrategyDeploymentSpec{Name: "test-deployment-one"},
59+
v1alpha1.StrategyDeploymentSpec{Name: "test-deployment-two"},
60+
),
61+
),
62+
},
63+
}, {
64+
name: "rejects bundles with duplicate deployment strategy spec names",
65+
bundle: &convert.RegistryV1{
66+
CSV: MakeCSV(
67+
WithStrategyDeploymentSpecs(
68+
v1alpha1.StrategyDeploymentSpec{Name: "test-deployment-one"},
69+
v1alpha1.StrategyDeploymentSpec{Name: "test-deployment-two"},
70+
v1alpha1.StrategyDeploymentSpec{Name: "test-deployment-one"},
71+
),
72+
),
73+
},
74+
expectedErrs: []error{
75+
errors.New("cluster service version contains duplicate strategy deployment spec 'test-deployment-one'"),
76+
},
77+
}, {
78+
name: "errors are ordered by deployment strategy spec name",
79+
bundle: &convert.RegistryV1{
80+
CSV: MakeCSV(
81+
WithStrategyDeploymentSpecs(
82+
v1alpha1.StrategyDeploymentSpec{Name: "test-deployment-a"},
83+
v1alpha1.StrategyDeploymentSpec{Name: "test-deployment-b"},
84+
v1alpha1.StrategyDeploymentSpec{Name: "test-deployment-c"},
85+
v1alpha1.StrategyDeploymentSpec{Name: "test-deployment-b"},
86+
v1alpha1.StrategyDeploymentSpec{Name: "test-deployment-a"},
87+
),
88+
),
89+
},
90+
expectedErrs: []error{
91+
errors.New("cluster service version contains duplicate strategy deployment spec 'test-deployment-a'"),
92+
errors.New("cluster service version contains duplicate strategy deployment spec 'test-deployment-b'"),
93+
},
94+
},
95+
} {
96+
t.Run(tc.name, func(t *testing.T) {
97+
errs := convert.CheckDeploymentSpecUniqueness(tc.bundle)
98+
require.Equal(t, tc.expectedErrs, errs)
99+
})
100+
}
101+
}
102+
103+
func Test_CRDResourceUniqueness(t *testing.T) {
104+
for _, tc := range []struct {
105+
name string
106+
bundle *convert.RegistryV1
107+
expectedErrs []error
108+
}{
109+
{
110+
name: "accepts bundles with unique custom resource definition resources",
111+
bundle: &convert.RegistryV1{
112+
CRDs: []apiextensionsv1.CustomResourceDefinition{
113+
{ObjectMeta: metav1.ObjectMeta{Name: "a.crd.something"}},
114+
{ObjectMeta: metav1.ObjectMeta{Name: "b.crd.something"}},
115+
},
116+
},
117+
}, {
118+
name: "rejects bundles with duplicate custom resource definition resources",
119+
bundle: &convert.RegistryV1{CRDs: []apiextensionsv1.CustomResourceDefinition{
120+
{ObjectMeta: metav1.ObjectMeta{Name: "a.crd.something"}},
121+
{ObjectMeta: metav1.ObjectMeta{Name: "a.crd.something"}},
122+
}},
123+
expectedErrs: []error{
124+
errors.New("bundle contains duplicate custom resource definition 'a.crd.something'"),
125+
},
126+
}, {
127+
name: "errors are ordered by custom resource definition name",
128+
bundle: &convert.RegistryV1{CRDs: []apiextensionsv1.CustomResourceDefinition{
129+
{ObjectMeta: metav1.ObjectMeta{Name: "c.crd.something"}},
130+
{ObjectMeta: metav1.ObjectMeta{Name: "c.crd.something"}},
131+
{ObjectMeta: metav1.ObjectMeta{Name: "a.crd.something"}},
132+
{ObjectMeta: metav1.ObjectMeta{Name: "a.crd.something"}},
133+
}},
134+
expectedErrs: []error{
135+
errors.New("bundle contains duplicate custom resource definition 'a.crd.something'"),
136+
errors.New("bundle contains duplicate custom resource definition 'c.crd.something'"),
137+
},
138+
},
139+
} {
140+
t.Run(tc.name, func(t *testing.T) {
141+
err := convert.CheckCRDResourceUniqueness(tc.bundle)
142+
require.Equal(t, tc.expectedErrs, err)
143+
})
144+
}
145+
}
146+
147+
func Test_CheckOwnedCRDExistence(t *testing.T) {
148+
for _, tc := range []struct {
149+
name string
150+
bundle *convert.RegistryV1
151+
expectedErrs []error
152+
}{
153+
{
154+
name: "accepts bundles with existing owned custom resource definition resources",
155+
bundle: &convert.RegistryV1{
156+
CRDs: []apiextensionsv1.CustomResourceDefinition{
157+
{ObjectMeta: metav1.ObjectMeta{Name: "a.crd.something"}},
158+
{ObjectMeta: metav1.ObjectMeta{Name: "b.crd.something"}},
159+
},
160+
CSV: MakeCSV(
161+
WithOwnedCRDs(
162+
v1alpha1.CRDDescription{Name: "a.crd.something"},
163+
v1alpha1.CRDDescription{Name: "b.crd.something"},
164+
),
165+
),
166+
},
167+
}, {
168+
name: "rejects bundles with missing owned custom resource definition resources",
169+
bundle: &convert.RegistryV1{
170+
CRDs: []apiextensionsv1.CustomResourceDefinition{},
171+
CSV: MakeCSV(
172+
WithOwnedCRDs(v1alpha1.CRDDescription{Name: "a.crd.something"}),
173+
),
174+
},
175+
expectedErrs: []error{
176+
errors.New("cluster service definition references owned custom resource definition 'a.crd.something' not found in bundle"),
177+
},
178+
}, {
179+
name: "errors are ordered by owned custom resource definition name",
180+
bundle: &convert.RegistryV1{
181+
CRDs: []apiextensionsv1.CustomResourceDefinition{},
182+
CSV: MakeCSV(
183+
WithOwnedCRDs(
184+
v1alpha1.CRDDescription{Name: "a.crd.something"},
185+
v1alpha1.CRDDescription{Name: "c.crd.something"},
186+
v1alpha1.CRDDescription{Name: "b.crd.something"},
187+
),
188+
),
189+
},
190+
expectedErrs: []error{
191+
errors.New("cluster service definition references owned custom resource definition 'a.crd.something' not found in bundle"),
192+
errors.New("cluster service definition references owned custom resource definition 'b.crd.something' not found in bundle"),
193+
errors.New("cluster service definition references owned custom resource definition 'c.crd.something' not found in bundle"),
194+
},
195+
},
196+
} {
197+
t.Run(tc.name, func(t *testing.T) {
198+
errs := convert.CheckOwnedCRDExistence(tc.bundle)
199+
require.Equal(t, tc.expectedErrs, errs)
200+
})
201+
}
202+
}
203+
204+
type CSVOption func(version *v1alpha1.ClusterServiceVersion)
205+
206+
func WithStrategyDeploymentSpecs(strategyDeploymentSpecs ...v1alpha1.StrategyDeploymentSpec) CSVOption {
207+
return func(csv *v1alpha1.ClusterServiceVersion) {
208+
csv.Spec.InstallStrategy.StrategySpec.DeploymentSpecs = strategyDeploymentSpecs
209+
}
210+
}
211+
212+
func WithOwnedCRDs(crdDesc ...v1alpha1.CRDDescription) CSVOption {
213+
return func(csv *v1alpha1.ClusterServiceVersion) {
214+
csv.Spec.CustomResourceDefinitions.Owned = crdDesc
215+
}
216+
}
217+
218+
func MakeCSV(opts ...CSVOption) v1alpha1.ClusterServiceVersion {
219+
csv := v1alpha1.ClusterServiceVersion{}
220+
for _, opt := range opts {
221+
opt(&csv)
222+
}
223+
return csv
224+
}

0 commit comments

Comments
 (0)