Skip to content

Commit c145c46

Browse files
jmprusiPer Goncalves da Silva
authored andcommitted
Implement support for upgrade Edges
Signed-off-by: Joaquim Moreno Prusi <[email protected]> Signed-off-by: Per Goncalves da Silva <[email protected]> Co-authored-by: Per Goncalves da Silva <[email protected]>
1 parent bacf52b commit c145c46

File tree

44 files changed

+56631
-351
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

44 files changed

+56631
-351
lines changed

Makefile

+4
Original file line numberDiff line numberDiff line change
@@ -130,10 +130,14 @@ kind-cluster-cleanup: $(KIND) ## Delete the kind cluster
130130

131131
.PHONY: kind-load-test-artifacts
132132
kind-load-test-artifacts: $(KIND) ## Load the e2e testdata container images into a kind cluster
133+
$(CONTAINER_RUNTIME) build $(TESTDATA_DIR)/bundles/registry-v1/prometheus-operator.v0.37.0 -t localhost/testdata/bundles/registry-v1/prometheus-operator:v0.37.0
133134
$(CONTAINER_RUNTIME) build $(TESTDATA_DIR)/bundles/registry-v1/prometheus-operator.v0.47.0 -t localhost/testdata/bundles/registry-v1/prometheus-operator:v0.47.0
135+
$(CONTAINER_RUNTIME) build $(TESTDATA_DIR)/bundles/registry-v1/prometheus-operator.v0.65.1 -t localhost/testdata/bundles/registry-v1/prometheus-operator:v0.65.1
134136
$(CONTAINER_RUNTIME) build $(TESTDATA_DIR)/bundles/plain-v0/plain.v0.1.0 -t localhost/testdata/bundles/plain-v0/plain:v0.1.0
135137
$(CONTAINER_RUNTIME) build $(TESTDATA_DIR)/catalogs -f $(TESTDATA_DIR)/catalogs/test-catalog.Dockerfile -t localhost/testdata/catalogs/test-catalog:e2e
138+
$(KIND) load docker-image localhost/testdata/bundles/registry-v1/prometheus-operator:v0.37.0 --name $(KIND_CLUSTER_NAME)
136139
$(KIND) load docker-image localhost/testdata/bundles/registry-v1/prometheus-operator:v0.47.0 --name $(KIND_CLUSTER_NAME)
140+
$(KIND) load docker-image localhost/testdata/bundles/registry-v1/prometheus-operator:v0.65.1 --name $(KIND_CLUSTER_NAME)
137141
$(KIND) load docker-image localhost/testdata/bundles/plain-v0/plain:v0.1.0 --name $(KIND_CLUSTER_NAME)
138142
$(KIND) load docker-image localhost/testdata/catalogs/test-catalog:e2e --name $(KIND_CLUSTER_NAME)
139143

config/samples/operators_v1alpha1_operator.yaml

+1
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,4 @@ metadata:
1010
name: operator-sample
1111
spec:
1212
packageName: argocd-operator
13+
version: 0.6.0

internal/controllers/variable_source.go

+3
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,9 @@ func NewVariableSource(cl client.Client) variablesources.NestedVariableSource {
2929
func(inputVariableSource input.VariableSource) (input.VariableSource, error) {
3030
return variablesources.NewOperatorVariableSource(cl, inputVariableSource), nil
3131
},
32+
func(inputVariableSource input.VariableSource) (input.VariableSource, error) {
33+
return variablesources.NewBundleDeploymentVariableSource(cl, inputVariableSource), nil
34+
},
3235
func(inputVariableSource input.VariableSource) (input.VariableSource, error) {
3336
return variablesources.NewBundlesAndDepsVariableSource(inputVariableSource), nil
3437
},

internal/resolution/entities/bundle_entity.go

+42-22
Original file line numberDiff line numberDiff line change
@@ -25,13 +25,6 @@ const (
2525

2626
// ----
2727

28-
type ChannelProperties struct {
29-
property.Channel
30-
Replaces string `json:"replaces,omitempty"`
31-
Skips []string `json:"skips,omitempty"`
32-
SkipRange string `json:"skipRange,omitempty"`
33-
}
34-
3528
type propertyRequirement bool
3629

3730
const (
@@ -46,6 +39,10 @@ type PackageRequired struct {
4639

4740
type GVK property.GVK
4841

42+
type Replaces struct {
43+
Replaces string `json:"replaces"`
44+
}
45+
4946
func (g GVK) String() string {
5047
return fmt.Sprintf(`group:"%s" version:"%s" kind:"%s"`, g.Group, g.Version, g.Kind)
5148
}
@@ -64,15 +61,16 @@ type BundleEntity struct {
6461
*input.Entity
6562

6663
// these properties are lazy loaded as they are requested
67-
bundlePackage *property.Package
68-
providedGVKs []GVK
69-
requiredGVKs []GVKRequired
70-
requiredPackages []PackageRequired
71-
channelProperties *ChannelProperties
72-
semVersion *semver.Version
73-
bundlePath string
74-
mediaType string
75-
mu sync.RWMutex
64+
bundlePackage *property.Package
65+
providedGVKs []GVK
66+
requiredGVKs []GVKRequired
67+
requiredPackages []PackageRequired
68+
channel *property.Channel
69+
replaces *Replaces
70+
semVersion *semver.Version
71+
bundlePath string
72+
mediaType string
73+
mu sync.RWMutex
7674
}
7775

7876
func NewBundleEntity(entity *input.Entity) *BundleEntity {
@@ -121,14 +119,34 @@ func (b *BundleEntity) ChannelName() (string, error) {
121119
if err := b.loadChannelProperties(); err != nil {
122120
return "", err
123121
}
124-
return b.channelProperties.ChannelName, nil
122+
return b.channel.ChannelName, nil
125123
}
126124

127-
func (b *BundleEntity) ChannelProperties() (*ChannelProperties, error) {
125+
func (b *BundleEntity) Channel() (*property.Channel, error) {
128126
if err := b.loadChannelProperties(); err != nil {
129127
return nil, err
130128
}
131-
return b.channelProperties, nil
129+
return b.channel, nil
130+
}
131+
132+
func (b *BundleEntity) Replaces() (string, error) {
133+
if err := b.loadReplaces(); err != nil {
134+
return "", err
135+
}
136+
return b.replaces.Replaces, nil
137+
}
138+
139+
func (b *BundleEntity) loadReplaces() error {
140+
b.mu.Lock()
141+
defer b.mu.Unlock()
142+
if b.replaces == nil {
143+
replaces, err := loadFromEntity[Replaces](b.Entity, "olm.replaces", optional)
144+
if err != nil {
145+
return fmt.Errorf("error determining replaces for entity '%s': %w", b.ID, err)
146+
}
147+
b.replaces = &replaces
148+
}
149+
return nil
132150
}
133151

134152
func (b *BundleEntity) BundlePath() (string, error) {
@@ -228,12 +246,12 @@ func (b *BundleEntity) loadRequiredPackages() error {
228246
func (b *BundleEntity) loadChannelProperties() error {
229247
b.mu.Lock()
230248
defer b.mu.Unlock()
231-
if b.channelProperties == nil {
232-
channel, err := loadFromEntity[ChannelProperties](b.Entity, property.TypeChannel, required)
249+
if b.channel == nil {
250+
channel, err := loadFromEntity[property.Channel](b.Entity, property.TypeChannel, required)
233251
if err != nil {
234252
return fmt.Errorf("error determining bundle channel properties for entity '%s': %w", b.ID, err)
235253
}
236-
b.channelProperties = &channel
254+
b.channel = &channel
237255
}
238256
return nil
239257
}
@@ -255,6 +273,8 @@ func loadFromEntity[T interface{}](entity *input.Entity, propertyName string, re
255273
deserializedProperty := *new(T)
256274
propertyValue, ok := entity.Properties[propertyName]
257275
if ok {
276+
// TODO: In order to avoid invalid properties we should use a decoder that only allows the properties we expect.
277+
// ie. decoder.DisallowUnknownFields()
258278
if err := json.Unmarshal([]byte(propertyValue), &deserializedProperty); err != nil {
259279
return deserializedProperty, fmt.Errorf("property '%s' ('%s') could not be parsed: %w", propertyName, propertyValue, err)
260280
}

internal/resolution/entities/bundle_entity_test.go

+72-13
Original file line numberDiff line numberDiff line change
@@ -205,28 +205,24 @@ var _ = Describe("BundleEntity", func() {
205205
})
206206
})
207207

208-
Describe("ChannelProperties", func() {
208+
Describe("Channel", func() {
209209
It("should return the bundle channel properties if present", func() {
210210
entity := input.NewEntity("operatorhub/prometheus/0.14.0", map[string]string{
211211
"olm.channel": `{"channelName":"beta","priority":0, "replaces": "bundle.v1.0.0", "skips": ["bundle.v0.9.0", "bundle.v0.9.6"], "skipRange": ">=0.9.0 <=0.9.6"}`,
212212
})
213213
bundleEntity := olmentity.NewBundleEntity(entity)
214-
channelProperties, err := bundleEntity.ChannelProperties()
214+
channelProperties, err := bundleEntity.Channel()
215215
Expect(err).ToNot(HaveOccurred())
216-
Expect(*channelProperties).To(Equal(olmentity.ChannelProperties{
217-
Channel: property.Channel{
218-
ChannelName: "beta",
219-
Priority: 0,
220-
},
221-
Replaces: "bundle.v1.0.0",
222-
Skips: []string{"bundle.v0.9.0", "bundle.v0.9.6"},
223-
SkipRange: ">=0.9.0 <=0.9.6",
224-
}))
216+
Expect(*channelProperties).To(Equal(property.Channel{
217+
ChannelName: "beta",
218+
Priority: 0,
219+
},
220+
))
225221
})
226222
It("should return an error if the property is not found", func() {
227223
entity := input.NewEntity("operatorhub/prometheus/0.14.0", map[string]string{})
228224
bundleEntity := olmentity.NewBundleEntity(entity)
229-
channelProperties, err := bundleEntity.ChannelProperties()
225+
channelProperties, err := bundleEntity.Channel()
230226
Expect(channelProperties).To(BeNil())
231227
Expect(err.Error()).To(Equal("error determining bundle channel properties for entity 'operatorhub/prometheus/0.14.0': required property 'olm.channel' not found"))
232228
})
@@ -235,7 +231,7 @@ var _ = Describe("BundleEntity", func() {
235231
"olm.channel": "badChannelPropertiesStructure",
236232
})
237233
bundleEntity := olmentity.NewBundleEntity(entity)
238-
channelProperties, err := bundleEntity.ChannelProperties()
234+
channelProperties, err := bundleEntity.Channel()
239235
Expect(channelProperties).To(BeNil())
240236
Expect(err.Error()).To(Equal("error determining bundle channel properties for entity 'operatorhub/prometheus/0.14.0': property 'olm.channel' ('badChannelPropertiesStructure') could not be parsed: invalid character 'b' looking for beginning of value"))
241237
})
@@ -269,6 +265,27 @@ var _ = Describe("BundleEntity", func() {
269265
})
270266
})
271267

268+
Describe("Replaces", func() {
269+
It("should return the replaces property if present", func() {
270+
entity := input.NewEntity("test", map[string]string{
271+
"olm.replaces": `{"replaces": "test.v0.2.0"}`,
272+
})
273+
bundleEntity := olmentity.NewBundleEntity(entity)
274+
replaces, err := bundleEntity.Replaces()
275+
Expect(err).ToNot(HaveOccurred())
276+
Expect(replaces).To(Equal("test.v0.2.0"))
277+
})
278+
It("should not return an error if the property is not found", func() {
279+
entity := input.NewEntity("test", map[string]string{
280+
"olm.thingy": `{"whatever":"this"}`,
281+
})
282+
bundleEntity := olmentity.NewBundleEntity(entity)
283+
replaces, err := bundleEntity.Replaces()
284+
Expect(replaces).To(BeEmpty())
285+
Expect(err).NotTo(HaveOccurred())
286+
})
287+
})
288+
272289
Describe("MediaType", func() {
273290
It("should return the bundle mediatype property if present", func() {
274291
entity := input.NewEntity("operatorhub/prometheus/0.14.0", map[string]string{
@@ -296,4 +313,46 @@ var _ = Describe("BundleEntity", func() {
296313
Expect(err.Error()).To(Equal("error determining bundle mediatype for entity 'operatorhub/prometheus/0.14.0': property 'olm.bundle.mediatype' ('badtype') could not be parsed: invalid character 'b' looking for beginning of value"))
297314
})
298315
})
316+
317+
// Increase test coverage
318+
Describe("GVKRequired properties", func() {
319+
It("should return the GVKRequired properties", func() {
320+
gvk := olmentity.GVKRequired{
321+
Group: "foo.io",
322+
Kind: "Foo",
323+
Version: "v1",
324+
}
325+
Expect(gvk.AsGVK().Version).To(Equal("v1"))
326+
Expect(gvk.AsGVK().Group).To(Equal("foo.io"))
327+
Expect(gvk.AsGVK().Kind).To(Equal("Foo"))
328+
})
329+
It("should return the GVKRequired properties as a string", func() {
330+
gvk := olmentity.GVKRequired{
331+
Group: "foo.io",
332+
Kind: "Foo",
333+
Version: "v1",
334+
}
335+
Expect(gvk.String()).To(Equal(`group:"foo.io" version:"v1" kind:"Foo"`))
336+
})
337+
})
338+
Describe("GVK properties", func() {
339+
It("should return the gvk properties", func() {
340+
gvk := olmentity.GVK{
341+
Group: "foo.io",
342+
Kind: "Foo",
343+
Version: "v1",
344+
}
345+
Expect(gvk.Version).To(Equal("v1"))
346+
Expect(gvk.Group).To(Equal("foo.io"))
347+
Expect(gvk.Kind).To(Equal("Foo"))
348+
})
349+
It("should return the gvk properties as a string", func() {
350+
gvk := olmentity.GVK{
351+
Group: "foo.io",
352+
Kind: "Foo",
353+
Version: "v1",
354+
}
355+
Expect(gvk.String()).To(Equal(`group:"foo.io" version:"v1" kind:"Foo"`))
356+
})
357+
})
299358
})

internal/resolution/entitysources/catalogdsource.go

+7
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,10 @@ func (es *CatalogdEntitySource) Iterate(ctx context.Context, fn input.IteratorFu
7171
return nil
7272
}
7373

74+
type replacesProperty struct {
75+
Replaces string `json:"replaces"`
76+
}
77+
7478
func getEntities(ctx context.Context, client client.Client) (input.EntityList, error) {
7579
entityList := input.EntityList{}
7680
bundleMetadatas, packageMetdatas, err := fetchMetadata(ctx, client)
@@ -106,6 +110,9 @@ func getEntities(ctx context.Context, client client.Client) (input.EntityList, e
106110
if catalogScopedEntryName == bundle.Name {
107111
channelValue, _ := json.Marshal(property.Channel{ChannelName: ch.Name, Priority: 0})
108112
props[property.TypeChannel] = string(channelValue)
113+
// TODO(jmprusi): Add the proper PropertyType for this
114+
replacesValue, _ := json.Marshal(replacesProperty{Replaces: b.Replaces})
115+
props["olm.replaces"] = string(replacesValue)
109116
entity := input.Entity{
110117
ID: deppy.IdentifierFromString(fmt.Sprintf("%s%s%s", bundle.Name, bundle.Spec.Package, ch.Name)),
111118
Properties: props,

internal/resolution/util/predicates/predicates.go

+22
Original file line numberDiff line numberDiff line change
@@ -56,3 +56,25 @@ func ProvidesGVK(gvk *olmentity.GVK) input.Predicate {
5656
return false
5757
}
5858
}
59+
60+
func WithBundleImage(bundleImage string) input.Predicate {
61+
return func(entity *input.Entity) bool {
62+
bundleEntity := olmentity.NewBundleEntity(entity)
63+
bundlePath, err := bundleEntity.BundlePath()
64+
if err != nil {
65+
return false
66+
}
67+
return bundlePath == bundleImage
68+
}
69+
}
70+
71+
func Replaces(bundleID string) input.Predicate {
72+
return func(entity *input.Entity) bool {
73+
bundleEntity := olmentity.NewBundleEntity(entity)
74+
replaces, err := bundleEntity.Replaces()
75+
if err != nil {
76+
return false
77+
}
78+
return replaces == bundleID
79+
}
80+
}

internal/resolution/util/predicates/predicates_test.go

+55
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,10 @@ var _ = Describe("Predicates", func() {
2727
Expect(predicates.WithPackageName("mypackage")(entity)).To(BeTrue())
2828
Expect(predicates.WithPackageName("notmypackage")(entity)).To(BeFalse())
2929
})
30+
It("should return false when the entity does not have a package name", func() {
31+
entity := input.NewEntity("test", map[string]string{})
32+
Expect(predicates.WithPackageName("mypackage")(entity)).To(BeFalse())
33+
})
3034
})
3135

3236
Describe("InSemverRange", func() {
@@ -39,6 +43,11 @@ var _ = Describe("Predicates", func() {
3943
Expect(predicates.InSemverRange(inRange)(entity)).To(BeTrue())
4044
Expect(predicates.InSemverRange(notInRange)(entity)).To(BeFalse())
4145
})
46+
It("should return false when the entity does not have a version", func() {
47+
entity := input.NewEntity("test", map[string]string{})
48+
inRange := semver.MustParseRange(">=1.0.0")
49+
Expect(predicates.InSemverRange(inRange)(entity)).To(BeFalse())
50+
})
4251
})
4352

4453
Describe("InChannel", func() {
@@ -49,6 +58,10 @@ var _ = Describe("Predicates", func() {
4958
Expect(predicates.InChannel("stable")(entity)).To(BeTrue())
5059
Expect(predicates.InChannel("unstable")(entity)).To(BeFalse())
5160
})
61+
It("should return false when the entity does not have a channel", func() {
62+
entity := input.NewEntity("test", map[string]string{})
63+
Expect(predicates.InChannel("stable")(entity)).To(BeFalse())
64+
})
5265
})
5366

5467
Describe("ProvidesGVK", func() {
@@ -67,5 +80,47 @@ var _ = Describe("Predicates", func() {
6780
Kind: "Baz",
6881
})(entity)).To(BeFalse())
6982
})
83+
It("should return false when the entity does not provide a gvk", func() {
84+
entity := input.NewEntity("test", map[string]string{})
85+
Expect(predicates.ProvidesGVK(&olmentity.GVK{
86+
Group: "foo.io",
87+
Version: "v1",
88+
Kind: "Foo",
89+
})(entity)).To(BeFalse())
90+
})
91+
})
92+
93+
Describe("WithBundleImage", func() {
94+
It("should return true when the entity provides the same bundle image", func() {
95+
entity := input.NewEntity("test", map[string]string{
96+
olmentity.PropertyBundlePath: `"registry.io/repo/image@sha256:1234567890"`,
97+
})
98+
Expect(predicates.WithBundleImage("registry.io/repo/image@sha256:1234567890")(entity)).To(BeTrue())
99+
Expect(predicates.WithBundleImage("registry.io/repo/image@sha256:0987654321")(entity)).To(BeFalse())
100+
})
101+
It("should return false when the entity does not provide a bundle image", func() {
102+
entity := input.NewEntity("test", map[string]string{})
103+
Expect(predicates.WithBundleImage("registry.io/repo/image@sha256:1234567890")(entity)).To(BeFalse())
104+
})
105+
})
106+
107+
Describe("Replaces", func() {
108+
It("should return true when the entity provides the replaces property", func() {
109+
entity := input.NewEntity("test", map[string]string{
110+
"olm.replaces": `{"replaces": "test.v0.2.0"}`,
111+
})
112+
Expect(predicates.Replaces("test.v0.2.0")(entity)).To(BeTrue())
113+
Expect(predicates.Replaces("test.v0.1.0")(entity)).To(BeFalse())
114+
})
115+
It("should return false when the entity does not provide a replaces property", func() {
116+
entity := input.NewEntity("test", map[string]string{})
117+
Expect(predicates.Replaces("test.v0.2.0")(entity)).To(BeFalse())
118+
})
119+
It("should return false when the replace value is not a valid json", func() {
120+
entity := input.NewEntity("test", map[string]string{
121+
"olm.replaces": `{"replaces"}`,
122+
})
123+
Expect(predicates.Replaces("test.v0.2.0")(entity)).To(BeFalse())
124+
})
70125
})
71126
})

0 commit comments

Comments
 (0)