Skip to content

Commit fd02095

Browse files
authored
Add initial SemVer upgrade support (#444)
* Add initial SemVer upgrade support OLM will now use SemVer to determine next upgrade. In this iteration we only support upgrade within the same major versions: e.g 1.0.1 can be upgraded to 1.0.2 or 1.3.2, but not 2.0.0. Signed-off-by: Mikalai Radchuk <[email protected]> * Update E2E * Add tests for semver upgrades * Label semver and legacy tests so it is possible to filter one or the other out * Update Makefile to no run legacy tests by default to match default deployment of operator-controller Signed-off-by: Mikalai Radchuk <[email protected]> --------- Signed-off-by: Mikalai Radchuk <[email protected]>
1 parent c9d387e commit fd02095

File tree

5 files changed

+162
-44
lines changed

5 files changed

+162
-44
lines changed

Makefile

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,8 @@ export XDG_DATA_HOME ?= /tmp/.local/share
2020
include .bingo/Variables.mk
2121

2222
# ARTIFACT_PATH is the absolute path to the directory where the operator-controller e2e tests will store the artifacts
23-
# for example: ARTIFACT_PATH=/tmp/artifacts make test
24-
export ARTIFACT_PATH ?=
23+
# for example: ARTIFACT_PATH=/tmp/artifacts make test
24+
export ARTIFACT_PATH ?=
2525

2626
OPERATOR_CONTROLLER_NAMESPACE ?= operator-controller-system
2727
KIND_CLUSTER_NAME ?= operator-controller
@@ -156,11 +156,19 @@ kind-load-test-artifacts: $(KIND) #EXHELP Load the e2e testdata container images
156156
$(CONTAINER_RUNTIME) build $(TESTDATA_DIR)/bundles/registry-v1/prometheus-operator.v0.37.0 -t localhost/testdata/bundles/registry-v1/prometheus-operator:v0.37.0
157157
$(CONTAINER_RUNTIME) build $(TESTDATA_DIR)/bundles/registry-v1/prometheus-operator.v0.47.0 -t localhost/testdata/bundles/registry-v1/prometheus-operator:v0.47.0
158158
$(CONTAINER_RUNTIME) build $(TESTDATA_DIR)/bundles/registry-v1/prometheus-operator.v0.65.1 -t localhost/testdata/bundles/registry-v1/prometheus-operator:v0.65.1
159+
$(CONTAINER_RUNTIME) tag localhost/testdata/bundles/registry-v1/prometheus-operator:v0.65.1 localhost/testdata/bundles/registry-v1/prometheus-operator:v1.0.0
160+
$(CONTAINER_RUNTIME) tag localhost/testdata/bundles/registry-v1/prometheus-operator:v0.65.1 localhost/testdata/bundles/registry-v1/prometheus-operator:v1.0.1
161+
$(CONTAINER_RUNTIME) tag localhost/testdata/bundles/registry-v1/prometheus-operator:v0.65.1 localhost/testdata/bundles/registry-v1/prometheus-operator:v1.2.0
162+
$(CONTAINER_RUNTIME) tag localhost/testdata/bundles/registry-v1/prometheus-operator:v0.65.1 localhost/testdata/bundles/registry-v1/prometheus-operator:v2.0.0
159163
$(CONTAINER_RUNTIME) build $(TESTDATA_DIR)/bundles/plain-v0/plain.v0.1.0 -t localhost/testdata/bundles/plain-v0/plain:v0.1.0
160164
$(CONTAINER_RUNTIME) build $(TESTDATA_DIR)/catalogs -f $(TESTDATA_DIR)/catalogs/test-catalog.Dockerfile -t localhost/testdata/catalogs/test-catalog:e2e
161165
$(KIND) load docker-image localhost/testdata/bundles/registry-v1/prometheus-operator:v0.37.0 --name $(KIND_CLUSTER_NAME)
162166
$(KIND) load docker-image localhost/testdata/bundles/registry-v1/prometheus-operator:v0.47.0 --name $(KIND_CLUSTER_NAME)
163167
$(KIND) load docker-image localhost/testdata/bundles/registry-v1/prometheus-operator:v0.65.1 --name $(KIND_CLUSTER_NAME)
168+
$(KIND) load docker-image localhost/testdata/bundles/registry-v1/prometheus-operator:v1.0.0 --name $(KIND_CLUSTER_NAME)
169+
$(KIND) load docker-image localhost/testdata/bundles/registry-v1/prometheus-operator:v1.0.1 --name $(KIND_CLUSTER_NAME)
170+
$(KIND) load docker-image localhost/testdata/bundles/registry-v1/prometheus-operator:v1.2.0 --name $(KIND_CLUSTER_NAME)
171+
$(KIND) load docker-image localhost/testdata/bundles/registry-v1/prometheus-operator:v2.0.0 --name $(KIND_CLUSTER_NAME)
164172
$(KIND) load docker-image localhost/testdata/bundles/plain-v0/plain:v0.1.0 --name $(KIND_CLUSTER_NAME)
165173
$(KIND) load docker-image localhost/testdata/catalogs/test-catalog:e2e --name $(KIND_CLUSTER_NAME)
166174

internal/resolution/variablesources/installed_package.go

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,15 @@ import (
55
"fmt"
66
"sort"
77

8+
mmsemver "github.com/Masterminds/semver/v3"
89
"github.com/operator-framework/deppy/pkg/deppy"
910
"github.com/operator-framework/deppy/pkg/deppy/input"
1011

1112
"github.com/operator-framework/operator-controller/internal/catalogmetadata"
1213
catalogfilter "github.com/operator-framework/operator-controller/internal/catalogmetadata/filter"
1314
catalogsort "github.com/operator-framework/operator-controller/internal/catalogmetadata/sort"
1415
"github.com/operator-framework/operator-controller/internal/resolution/variables"
16+
"github.com/operator-framework/operator-controller/pkg/features"
1517
)
1618

1719
var _ input.VariableSource = &InstalledPackageVariableSource{}
@@ -59,10 +61,15 @@ func (r *InstalledPackageVariableSource) notFoundError() error {
5961
}
6062

6163
func NewInstalledPackageVariableSource(catalogClient BundleProvider, bundleImage string) (*InstalledPackageVariableSource, error) {
64+
successors := legacySemanticsSuccessors
65+
if features.OperatorControllerFeatureGate.Enabled(features.ForceSemverUpgradeConstraints) {
66+
successors = semverSuccessors
67+
}
68+
6269
return &InstalledPackageVariableSource{
6370
catalogClient: catalogClient,
6471
bundleImage: bundleImage,
65-
successors: legacySemanticsSuccessors,
72+
successors: successors,
6673
}, nil
6774
}
6875

@@ -83,3 +90,28 @@ func legacySemanticsSuccessors(allBundles []*catalogmetadata.Bundle, installedBu
8390

8491
return upgradeEdges, nil
8592
}
93+
94+
// semverSuccessors returns successors based on Semver.
95+
// Successors will not include versions outside the major version of the
96+
// installed bundle as major version is intended to indicate breaking changes.
97+
func semverSuccessors(allBundles []*catalogmetadata.Bundle, installedBundle *catalogmetadata.Bundle) ([]*catalogmetadata.Bundle, error) {
98+
currentVersion, err := installedBundle.Version()
99+
if err != nil {
100+
return nil, err
101+
}
102+
103+
// Based on current version create a caret range comparison constraint
104+
// to allow only minor and patch version as successors and exclude current version.
105+
constraintStr := fmt.Sprintf("^%s, != %s", currentVersion.String(), currentVersion.String())
106+
wantedVersionRangeConstraint, err := mmsemver.NewConstraint(constraintStr)
107+
if err != nil {
108+
return nil, err
109+
}
110+
111+
upgradeEdges := catalogfilter.Filter(allBundles, catalogfilter.InMastermindsSemverRange(wantedVersionRangeConstraint))
112+
sort.SliceStable(upgradeEdges, func(i, j int) bool {
113+
return catalogsort.ByVersion(upgradeEdges[i], upgradeEdges[j])
114+
})
115+
116+
return upgradeEdges, nil
117+
}

internal/resolution/variablesources/installed_package_test.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,27 @@ func TestInstalledPackageVariableSource(t *testing.T) {
121121
const bundleImage = "registry.io/repo/[email protected]"
122122
fakeCatalogClient := testutil.NewFakeCatalogClient(bundleList)
123123

124+
t.Run("with ForceSemverUpgradeConstraints feature gate enabled", func(t *testing.T) {
125+
defer featuregatetesting.SetFeatureGateDuringTest(t, features.OperatorControllerFeatureGate, features.ForceSemverUpgradeConstraints, true)()
126+
127+
ipvs, err := variablesources.NewInstalledPackageVariableSource(&fakeCatalogClient, bundleImage)
128+
require.NoError(t, err)
129+
130+
variables, err := ipvs.GetVariables(context.TODO())
131+
require.NoError(t, err)
132+
require.Len(t, variables, 1)
133+
packageVariable, ok := variables[0].(*olmvariables.InstalledPackageVariable)
134+
assert.True(t, ok)
135+
assert.Equal(t, deppy.IdentifierFromString("installed package test-package"), packageVariable.Identifier())
136+
137+
// ensure bundles are in version order (high to low)
138+
bundles := packageVariable.Bundles()
139+
require.Len(t, bundles, 3)
140+
assert.Equal(t, "test-package.v2.2.0", packageVariable.Bundles()[0].Name)
141+
assert.Equal(t, "test-package.v2.1.0", packageVariable.Bundles()[1].Name)
142+
assert.Equal(t, "test-package.v2.0.0", packageVariable.Bundles()[2].Name)
143+
})
144+
124145
t.Run("with ForceSemverUpgradeConstraints feature gate disabled", func(t *testing.T) {
125146
defer featuregatetesting.SetFeatureGateDuringTest(t, features.OperatorControllerFeatureGate, features.ForceSemverUpgradeConstraints, false)()
126147

test/e2e/install_test.go

Lines changed: 50 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ var _ = Describe("Operator Install", func() {
6767
g.Expect(cond.Status).To(Equal(metav1.ConditionTrue))
6868
g.Expect(cond.Reason).To(Equal(operatorv1alpha1.ReasonSuccess))
6969
g.Expect(cond.Message).To(ContainSubstring("resolved to"))
70-
g.Expect(operator.Status.ResolvedBundleResource).To(Equal("localhost/testdata/bundles/registry-v1/prometheus-operator:v0.65.1"))
70+
g.Expect(operator.Status.ResolvedBundleResource).To(Equal("localhost/testdata/bundles/registry-v1/prometheus-operator:v2.0.0"))
7171
}).Should(Succeed())
7272

7373
By("eventually installing the package successfully")
@@ -180,48 +180,57 @@ var _ = Describe("Operator Install", func() {
180180
}).Should(Succeed())
181181
})
182182

183-
It("handles upgrade edges correctly", func() {
184-
By("creating a valid Operator resource")
185-
operator.Spec = operatorv1alpha1.OperatorSpec{
186-
PackageName: "prometheus",
187-
Version: "0.37.0",
188-
}
189-
Expect(c.Create(ctx, operator)).To(Succeed())
190-
By("eventually reporting a successful resolution")
191-
Eventually(func(g Gomega) {
192-
g.Expect(c.Get(ctx, types.NamespacedName{Name: operator.Name}, operator)).To(Succeed())
193-
cond := apimeta.FindStatusCondition(operator.Status.Conditions, operatorv1alpha1.TypeResolved)
194-
g.Expect(cond).ToNot(BeNil())
195-
g.Expect(cond.Reason).To(Equal(operatorv1alpha1.ReasonSuccess))
196-
g.Expect(cond.Message).To(ContainSubstring("resolved to"))
197-
g.Expect(operator.Status.ResolvedBundleResource).ToNot(BeEmpty())
198-
}).Should(Succeed())
183+
When("resolving upgrade edges", func() {
184+
BeforeEach(func() {
185+
By("creating an Operator at a specified version")
186+
operator.Spec = operatorv1alpha1.OperatorSpec{
187+
PackageName: "prometheus",
188+
Version: "1.0.0",
189+
}
190+
Expect(c.Create(ctx, operator)).To(Succeed())
191+
By("eventually reporting a successful resolution")
192+
Eventually(func(g Gomega) {
193+
g.Expect(c.Get(ctx, types.NamespacedName{Name: operator.Name}, operator)).To(Succeed())
194+
cond := apimeta.FindStatusCondition(operator.Status.Conditions, operatorv1alpha1.TypeResolved)
195+
g.Expect(cond).ToNot(BeNil())
196+
g.Expect(cond.Reason).To(Equal(operatorv1alpha1.ReasonSuccess))
197+
g.Expect(cond.Message).To(ContainSubstring("resolved to"))
198+
g.Expect(operator.Status.ResolvedBundleResource).To(Equal("localhost/testdata/bundles/registry-v1/prometheus-operator:v1.0.0"))
199+
}).Should(Succeed())
200+
})
199201

200-
By("updating the Operator resource to a non-successor version")
201-
operator.Spec.Version = "0.65.1" // current (0.37.0) and successor (0.47.0) are the only values that would be SAT.
202-
Expect(c.Update(ctx, operator)).To(Succeed())
203-
By("eventually reporting an unsatisfiable resolution")
204-
Eventually(func(g Gomega) {
205-
g.Expect(c.Get(ctx, types.NamespacedName{Name: operator.Name}, operator)).To(Succeed())
206-
cond := apimeta.FindStatusCondition(operator.Status.Conditions, operatorv1alpha1.TypeResolved)
207-
g.Expect(cond).ToNot(BeNil())
208-
g.Expect(cond.Reason).To(Equal(operatorv1alpha1.ReasonResolutionFailed))
209-
g.Expect(cond.Message).To(MatchRegexp(`^constraints not satisfiable:.*; installed package prometheus requires at least one of.*0.47.0[^,]*,[^,]*0.37.0[^;]*;.*`))
210-
g.Expect(operator.Status.ResolvedBundleResource).To(BeEmpty())
211-
}).Should(Succeed())
202+
It("does not allow to upgrade the Operator to a non-successor version", func() {
203+
By("updating the Operator resource to a non-successor version")
204+
// Semver only allows upgrades within major version at the moment.
205+
operator.Spec.Version = "2.0.0"
206+
Expect(c.Update(ctx, operator)).To(Succeed())
207+
By("eventually reporting an unsatisfiable resolution")
208+
Eventually(func(g Gomega) {
209+
g.Expect(c.Get(ctx, types.NamespacedName{Name: operator.Name}, operator)).To(Succeed())
210+
cond := apimeta.FindStatusCondition(operator.Status.Conditions, operatorv1alpha1.TypeResolved)
211+
g.Expect(cond).ToNot(BeNil())
212+
g.Expect(cond.Reason).To(Equal(operatorv1alpha1.ReasonResolutionFailed))
213+
g.Expect(cond.Message).To(ContainSubstring("constraints not satisfiable"))
214+
g.Expect(cond.Message).To(ContainSubstring("installed package prometheus requires at least one of test-catalog-prometheus-prometheus-operator.1.2.0, test-catalog-prometheus-prometheus-operator.1.0.1, test-catalog-prometheus-prometheus-operator.1.0.0;"))
215+
g.Expect(operator.Status.ResolvedBundleResource).To(BeEmpty())
216+
}).Should(Succeed())
217+
})
212218

213-
By("updating the Operator resource to a valid upgrade edge")
214-
operator.Spec.Version = "0.47.0"
215-
Expect(c.Update(ctx, operator)).To(Succeed())
216-
By("eventually reporting a successful resolution and bundle path")
217-
Eventually(func(g Gomega) {
218-
g.Expect(c.Get(ctx, types.NamespacedName{Name: operator.Name}, operator)).To(Succeed())
219-
cond := apimeta.FindStatusCondition(operator.Status.Conditions, operatorv1alpha1.TypeResolved)
220-
g.Expect(cond).ToNot(BeNil())
221-
g.Expect(cond.Reason).To(Equal(operatorv1alpha1.ReasonSuccess))
222-
g.Expect(cond.Message).To(ContainSubstring("resolved to"))
223-
g.Expect(operator.Status.ResolvedBundleResource).ToNot(BeEmpty())
224-
}).Should(Succeed())
219+
It("does allow to upgrade the Operator to any of the successor versions within non-zero major version", func() {
220+
By("updating the Operator resource by skipping versions")
221+
// Test catalog has versions between the initial version and new version
222+
operator.Spec.Version = "1.2.0"
223+
Expect(c.Update(ctx, operator)).To(Succeed())
224+
By("eventually reporting a successful resolution and bundle path")
225+
Eventually(func(g Gomega) {
226+
g.Expect(c.Get(ctx, types.NamespacedName{Name: operator.Name}, operator)).To(Succeed())
227+
cond := apimeta.FindStatusCondition(operator.Status.Conditions, operatorv1alpha1.TypeResolved)
228+
g.Expect(cond).ToNot(BeNil())
229+
g.Expect(cond.Reason).To(Equal(operatorv1alpha1.ReasonSuccess))
230+
g.Expect(cond.Message).To(ContainSubstring("resolved to"))
231+
g.Expect(operator.Status.ResolvedBundleResource).To(Equal("localhost/testdata/bundles/registry-v1/prometheus-operator:v1.2.0"))
232+
}).Should(Succeed())
233+
})
225234
})
226235

227236
AfterEach(func() {

testdata/catalogs/test-catalog/catalog.yaml

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,14 @@ entries:
1818
replaces: prometheus-operator.0.37.0
1919
- name: prometheus-operator.0.65.1
2020
replaces: prometheus-operator.0.47.0
21+
- name: prometheus-operator.1.0.0
22+
replaces: prometheus-operator.0.65.1
23+
- name: prometheus-operator.1.0.1
24+
replaces: prometheus-operator.1.0.0
25+
- name: prometheus-operator.1.2.0
26+
replaces: prometheus-operator.1.0.1
27+
- name: prometheus-operator.2.0.0
28+
replaces: prometheus-operator.1.2.0
2129
---
2230
schema: olm.bundle
2331
name: prometheus-operator.0.37.0
@@ -49,6 +57,46 @@ properties:
4957
packageName: prometheus
5058
version: 0.65.1
5159
---
60+
schema: olm.bundle
61+
name: prometheus-operator.1.0.0
62+
package: prometheus
63+
image: localhost/testdata/bundles/registry-v1/prometheus-operator:v1.0.0
64+
properties:
65+
- type: olm.package
66+
value:
67+
packageName: prometheus
68+
version: 1.0.0
69+
---
70+
schema: olm.bundle
71+
name: prometheus-operator.1.0.1
72+
package: prometheus
73+
image: localhost/testdata/bundles/registry-v1/prometheus-operator:v1.0.1
74+
properties:
75+
- type: olm.package
76+
value:
77+
packageName: prometheus
78+
version: 1.0.1
79+
---
80+
schema: olm.bundle
81+
name: prometheus-operator.1.2.0
82+
package: prometheus
83+
image: localhost/testdata/bundles/registry-v1/prometheus-operator:v1.2.0
84+
properties:
85+
- type: olm.package
86+
value:
87+
packageName: prometheus
88+
version: 1.2.0
89+
---
90+
schema: olm.bundle
91+
name: prometheus-operator.2.0.0
92+
package: prometheus
93+
image: localhost/testdata/bundles/registry-v1/prometheus-operator:v2.0.0
94+
properties:
95+
- type: olm.package
96+
value:
97+
packageName: prometheus
98+
version: 2.0.0
99+
---
52100
schema: olm.package
53101
name: plain
54102
defaultChannel: beta

0 commit comments

Comments
 (0)