Skip to content

Commit 5da9b1b

Browse files
committed
Add e2e test case for existing CR validation
Signed-off-by: Vu Dinh <[email protected]>
1 parent 92b4a2f commit 5da9b1b

File tree

6 files changed

+244
-165
lines changed

6 files changed

+244
-165
lines changed

Documentation/design/dependency-resolution.md

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,23 @@
22

33
OLM manages the dependency resolution and upgrade lifecycle of running operators. In many ways, thes problems OLM faces are similar to other OS package managers like `apt`/`dkpg` and `yum`/`rpm`.
44

5-
However, there is one constraint that similar systems don't generally have that OLM does: because operators are always running, OLM attempts to ensure that at no point in time are you left with a set of operators that do not work with each other.
5+
However, there is one constraint that similar systems don't generally have that OLM does: because operators are always running, OLM attempts to ensure that at no point in time are you left with a set of operators that do not work with each other.
66

77
This means that OLM needs to never:
8-
8+
99
- install a set of operators that require APIs that can't be provided
1010
- update an operator in a way that breaks another that depends upon it
11-
11+
1212
The following examples motivate why OLM's dependency resolution and upgrade strategy works as it does, followed by a description of the current algorithm.
1313

14+
## CustomResourceDefinition (CRD) Upgrade
15+
16+
OLM will upgrade CRD right away if it is owned by singular CSV. If CRD is owned by multiple CSVs, then CRD is upgraded when it is
17+
satisfied all of the following backward compatible conditions:
18+
19+
- All existing versions in current CRD are present in new CRD
20+
- All existing instances (Custom Resource) of current CRD are valid when validated against new CRD's validation schema
21+
1422
# Example: Deprecate dependant API
1523

1624
A and B are APIs (e.g. CRDs)
@@ -41,7 +49,7 @@ If we attempt to update A without simultaneously updating B, or vice-versa, we w
4149
This is another case we prevent with OLM's upgrade strategy.
4250

4351

44-
# Dependency resolution
52+
# Dependency resolution
4553

4654
A Provider is an operator which "Owns" a CRD or APIService.
4755

@@ -55,12 +63,12 @@ Consider the set of operators defined by running operators in a namespace:
5563
provisionally add the operator to the generation
5664
else
5765
check for a replacement in the source/package/channel
58-
66+
5967
// Generation resolution
6068
For each required API with no provider in gen:
6169
search through prioritized sources to pick a provider
6270
provisionally add any new operators found to the generation, this could also add to the required APIs without providers
63-
71+
6472
// Downgrade
6573
if there are still required APIs that can't be satisfied by sources:
6674
downgrade the operator(s) that require the APIs that can't be satisfied

pkg/controller/operators/catalog/operator.go

Lines changed: 17 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@ import (
2020
extinf "k8s.io/apiextensions-apiserver/pkg/client/informers/externalversions"
2121
k8serrors "k8s.io/apimachinery/pkg/api/errors"
2222
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
23-
conversion "k8s.io/apimachinery/pkg/conversion"
2423
"k8s.io/apimachinery/pkg/labels"
2524
"k8s.io/apimachinery/pkg/runtime/schema"
2625
utilclock "k8s.io/apimachinery/pkg/util/clock"
@@ -1045,26 +1044,12 @@ func (o *Operator) ResolvePlan(plan *v1alpha1.InstallPlan) error {
10451044
return nil
10461045
}
10471046

1048-
// TODO: in the future this will be extended to verify more than just whether or not the schema changed
1049-
func checkCRDSchemas(oldCRD *v1beta1ext.CustomResourceDefinition, newCRD *v1beta1ext.CustomResourceDefinition) bool {
1050-
if reflect.DeepEqual(oldCRD.Spec.Validation, newCRD.Spec.Validation) {
1051-
return true
1052-
}
1053-
1054-
return false
1055-
}
1056-
1057-
func safeToUpdate(oldCRD *v1beta1ext.CustomResourceDefinition, newCRD *v1beta1ext.CustomResourceDefinition) error {
1058-
shouldCheckSchema := len(oldCRD.Spec.Versions) == len(newCRD.Spec.Versions) // no version bump
1059-
1060-
// 1) if there's no version change, verify that schemas match 2) ensure all old versions are present
1047+
// Ensure all existing versions are present in new CRD
1048+
func ensureCRDVersions(oldCRD *v1beta1ext.CustomResourceDefinition, newCRD *v1beta1ext.CustomResourceDefinition) error {
10611049
for _, oldVersion := range oldCRD.Spec.Versions {
10621050
var versionPresent bool
10631051
for _, newVersion := range newCRD.Spec.Versions {
10641052
if oldVersion.Name == newVersion.Name {
1065-
if shouldCheckSchema && !checkCRDSchemas(oldCRD, newCRD) {
1066-
return fmt.Errorf("not allowing CRD (%v) update with multiple owners with schema change on version %v", newCRD.GetName(), oldVersion)
1067-
}
10681053
versionPresent = true
10691054
}
10701055
}
@@ -1073,32 +1058,29 @@ func safeToUpdate(oldCRD *v1beta1ext.CustomResourceDefinition, newCRD *v1beta1ex
10731058
}
10741059
}
10751060

1076-
// ensure a CRD with multiple versions isn't checked
1077-
if newCRD.Spec.Version != "" {
1078-
if !checkCRDSchemas(oldCRD, newCRD) {
1079-
return fmt.Errorf("not allowing CRD (%v) update with multiple owners with schema change on (single) version %v", newCRD.GetName(), newCRD.Spec.Version)
1080-
}
1081-
}
10821061
return nil
10831062
}
10841063

10851064
func (o *Operator) validateCustomResourceDefinition(oldCRD *v1beta1ext.CustomResourceDefinition, newCRD *v1beta1ext.CustomResourceDefinition) error {
10861065
o.logger.Debugf("Comparing %#v to %#v", oldCRD.Spec.Validation, newCRD.Spec.Validation)
1087-
var convertedCRD *apiextensions.CustomResourceDefinition
1088-
var scope conversion.Scope
1089-
if err := v1beta1ext.Convert_v1beta1_CustomResourceDefinition_To_apiextensions_CustomResourceDefinition(newCRD, convertedCRD, scope); err != nil {
1066+
// If validation schema is unchanged, return right away
1067+
if reflect.DeepEqual(oldCRD.Spec.Validation, newCRD.Spec.Validation) {
1068+
return nil
1069+
}
1070+
convertedCRD := &apiextensions.CustomResourceDefinition{}
1071+
if err := v1beta1ext.Convert_v1beta1_CustomResourceDefinition_To_apiextensions_CustomResourceDefinition(newCRD, convertedCRD, nil); err != nil {
10901072
return err
10911073
}
10921074
for _, oldVersion := range oldCRD.Spec.Versions {
1093-
gvr := schema.GroupVersionResource{Group: oldCRD.Spec.Group, Version: oldVersion.Name, Resource: newCRD.Spec.Names.Plural}
1075+
gvr := schema.GroupVersionResource{Group: oldCRD.Spec.Group, Version: oldVersion.Name, Resource: oldCRD.Spec.Names.Plural}
10941076
err := o.validateExistingCRs(gvr, convertedCRD)
10951077
if err != nil {
10961078
return err
10971079
}
10981080
}
10991081

11001082
if oldCRD.Spec.Version != "" {
1101-
gvr := schema.GroupVersionResource{Group: oldCRD.Spec.Group, Version: oldCRD.Spec.Version, Resource: newCRD.Spec.Names.Plural}
1083+
gvr := schema.GroupVersionResource{Group: oldCRD.Spec.Group, Version: oldCRD.Spec.Version, Resource: oldCRD.Spec.Names.Plural}
11021084
err := o.validateExistingCRs(gvr, convertedCRD)
11031085
if err != nil {
11041086
return err
@@ -1118,7 +1100,7 @@ func (o *Operator) validateExistingCRs(gvr schema.GroupVersionResource, newCRD *
11181100
if err != nil {
11191101
return fmt.Errorf("error creating validator for schema %#v: %s", newCRD.Spec.Validation, err)
11201102
}
1121-
err = validation.ValidateCustomResource(cr, validator)
1103+
err = validation.ValidateCustomResource(cr.UnstructuredContent(), validator)
11221104
if err != nil {
11231105
return fmt.Errorf("error validating custom resource against new schema %#v: %s", newCRD.Spec.Validation, err)
11241106
}
@@ -1188,21 +1170,22 @@ func (o *Operator) ExecutePlan(plan *v1alpha1.InstallPlan) error {
11881170
crd.SetResourceVersion(currentCRD.GetResourceVersion())
11891171
if len(matchedCSV) == 1 {
11901172
o.logger.Debugf("Found one owner for CRD %v", crd)
1191-
if err = o.validateCustomResourceDefinition(currentCRD, &crd); err != nil {
1192-
return errorwrap.Wrapf(err, "error validating existing CRs agains new CRD's schema: %s", step.Resource.Name)
1193-
}
1173+
11941174
_, err = o.opClient.ApiextensionsV1beta1Interface().ApiextensionsV1beta1().CustomResourceDefinitions().Update(&crd)
11951175
if err != nil {
11961176
return errorwrap.Wrapf(err, "error updating CRD: %s", step.Resource.Name)
11971177
}
11981178
} else if len(matchedCSV) > 1 {
11991179
o.logger.Debugf("Found multiple owners for CRD %v", crd)
1200-
if err := safeToUpdate(currentCRD, &crd); err != nil {
1201-
return err
1180+
1181+
if err := ensureCRDVersions(currentCRD, &crd); err != nil {
1182+
return errorwrap.Wrapf(err, "error missing existing CRD version(s) in new CRD: %s", step.Resource.Name)
12021183
}
1184+
12031185
if err = o.validateCustomResourceDefinition(currentCRD, &crd); err != nil {
12041186
return errorwrap.Wrapf(err, "error validating existing CRs agains new CRD's schema: %s", step.Resource.Name)
12051187
}
1188+
12061189
_, err = o.opClient.ApiextensionsV1beta1Interface().ApiextensionsV1beta1().CustomResourceDefinitions().Update(&crd)
12071190
if err != nil {
12081191
return errorwrap.Wrapf(err, "error update CRD: %s", step.Resource.Name)

pkg/controller/operators/catalog/operator_test.go

Lines changed: 34 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -125,34 +125,18 @@ func TestTransitionInstallPlan(t *testing.T) {
125125
}
126126
}
127127

128-
func TestSafeToUpdate(t *testing.T) {
128+
func TestEnsureCRDVersions(t *testing.T) {
129129
mainCRDPlural := "ins-main-abcde"
130-
differentSchema := &v1beta1.CustomResourceValidation{
131-
OpenAPIV3Schema: &v1beta1.JSONSchemaProps{
132-
Properties: map[string]v1beta1.JSONSchemaProps{
133-
"spec": {
134-
Type: "object",
135-
Description: "Spec of a test object.",
136-
Properties: map[string]v1beta1.JSONSchemaProps{
137-
"scalar": {
138-
Type: "number",
139-
Description: "Scalar value that should have a min and max.",
140-
Minimum: func() *float64 {
141-
var min float64 = 2
142-
return &min
143-
}(),
144-
Maximum: func() *float64 {
145-
var max float64 = 256
146-
return &max
147-
}(),
148-
},
149-
},
150-
},
151-
},
130+
131+
currentVersions := []v1beta1.CustomResourceDefinitionVersion{
132+
{
133+
Name: "v1alpha1",
134+
Served: true,
135+
Storage: true,
152136
},
153137
}
154138

155-
differentVersions := []v1beta1.CustomResourceDefinitionVersion{
139+
addedVersions := []v1beta1.CustomResourceDefinitionVersion{
156140
{
157141
Name: "v1alpha1",
158142
Served: true,
@@ -165,53 +149,56 @@ func TestSafeToUpdate(t *testing.T) {
165149
},
166150
}
167151

152+
missingVersions := []v1beta1.CustomResourceDefinitionVersion{
153+
{
154+
Name: "v1alpha2",
155+
Served: true,
156+
Storage: true,
157+
},
158+
}
159+
168160
tests := []struct {
169161
name string
170162
oldCRD v1beta1.CustomResourceDefinition
171163
newCRD v1beta1.CustomResourceDefinition
172164
expectedFailure bool
173165
}{
174166
{
175-
// in reality this would never happen within ExecutePlan, but fully exercises function
176-
name: "same version, same schema",
177-
oldCRD: crd(mainCRDPlural),
178-
newCRD: crd(mainCRDPlural),
179-
},
180-
{
181-
name: "same version, different schema",
182-
oldCRD: crd(mainCRDPlural),
183-
newCRD: func() v1beta1.CustomResourceDefinition {
184-
newCRD := crd(mainCRDPlural)
185-
newCRD.Spec.Validation = differentSchema
186-
return newCRD
167+
name: "existing versions are present",
168+
oldCRD: func() v1beta1.CustomResourceDefinition {
169+
oldCRD := crd(mainCRDPlural)
170+
oldCRD.Spec.Version = ""
171+
oldCRD.Spec.Versions = currentVersions
172+
return oldCRD
187173
}(),
188-
expectedFailure: true,
189-
},
190-
{
191-
name: "different version, different schema",
192-
oldCRD: crd(mainCRDPlural),
193174
newCRD: func() v1beta1.CustomResourceDefinition {
194175
newCRD := crd(mainCRDPlural)
195176
newCRD.Spec.Version = ""
196-
newCRD.Spec.Versions = differentVersions
197-
newCRD.Spec.Validation = differentSchema
177+
newCRD.Spec.Versions = addedVersions
198178
return newCRD
199179
}(),
180+
expectedFailure: false,
200181
},
201182
{
202-
name: "different version, same schema",
203-
oldCRD: crd(mainCRDPlural),
183+
name: "missing versions in new CRD",
184+
oldCRD: func() v1beta1.CustomResourceDefinition {
185+
oldCRD := crd(mainCRDPlural)
186+
oldCRD.Spec.Version = ""
187+
oldCRD.Spec.Versions = currentVersions
188+
return oldCRD
189+
}(),
204190
newCRD: func() v1beta1.CustomResourceDefinition {
205191
newCRD := crd(mainCRDPlural)
206192
newCRD.Spec.Version = ""
207-
newCRD.Spec.Versions = differentVersions
193+
newCRD.Spec.Versions = missingVersions
208194
return newCRD
209195
}(),
196+
expectedFailure: true,
210197
},
211198
}
212199

213200
for _, tt := range tests {
214-
err := safeToUpdate(&tt.oldCRD, &tt.newCRD)
201+
err := ensureCRDVersions(&tt.oldCRD, &tt.newCRD)
215202
if tt.expectedFailure {
216203
require.Error(t, err)
217204
}

0 commit comments

Comments
 (0)