Skip to content

Commit

Permalink
Implement support for upgrade Edges
Browse files Browse the repository at this point in the history
Signed-off-by: Joaquim Moreno Prusi <joaquim@redhat.com>
Signed-off-by: Per Goncalves da Silva <pegoncal@redhat.com>

Co-authored-by: Per Goncalves da Silva <pegoncal@redhat.com>
  • Loading branch information
jmprusi and Per Goncalves da Silva committed Jul 12, 2023
1 parent 61b563f commit 2da8d70
Show file tree
Hide file tree
Showing 41 changed files with 56,329 additions and 351 deletions.
4 changes: 4 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -116,10 +116,14 @@ kind-cluster-cleanup: $(KIND) ## Delete the kind cluster
$(KIND) delete cluster --name ${KIND_CLUSTER_NAME}

kind-load-test-artifacts: $(KIND) ## Load the e2e testdata container images into a kind cluster
$(CONTAINER_RUNTIME) build $(TESTDATA_DIR)/bundles/registry-v1/prometheus-operator.v0.37.0 -t localhost/testdata/bundles/registry-v1/prometheus-operator:v0.37.0
$(CONTAINER_RUNTIME) build $(TESTDATA_DIR)/bundles/registry-v1/prometheus-operator.v0.47.0 -t localhost/testdata/bundles/registry-v1/prometheus-operator:v0.47.0
$(CONTAINER_RUNTIME) build $(TESTDATA_DIR)/bundles/registry-v1/prometheus-operator.v0.65.1 -t localhost/testdata/bundles/registry-v1/prometheus-operator:v0.65.1
$(CONTAINER_RUNTIME) build $(TESTDATA_DIR)/bundles/plain-v0/plain.v0.1.0 -t localhost/testdata/bundles/plain-v0/plain:v0.1.0
$(CONTAINER_RUNTIME) build $(TESTDATA_DIR)/catalogs -f $(TESTDATA_DIR)/catalogs/test-catalog.Dockerfile -t localhost/testdata/catalogs/test-catalog:e2e
$(KIND) load docker-image localhost/testdata/bundles/registry-v1/prometheus-operator:v0.37.0 --name $(KIND_CLUSTER_NAME)
$(KIND) load docker-image localhost/testdata/bundles/registry-v1/prometheus-operator:v0.47.0 --name $(KIND_CLUSTER_NAME)
$(KIND) load docker-image localhost/testdata/bundles/registry-v1/prometheus-operator:v0.65.1 --name $(KIND_CLUSTER_NAME)
$(KIND) load docker-image localhost/testdata/bundles/plain-v0/plain:v0.1.0 --name $(KIND_CLUSTER_NAME)
$(KIND) load docker-image localhost/testdata/catalogs/test-catalog:e2e --name $(KIND_CLUSTER_NAME)

Expand Down
1 change: 1 addition & 0 deletions config/samples/operators_v1alpha1_operator.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,4 @@ metadata:
name: operator-sample
spec:
packageName: argocd-operator
version: 0.6.0
3 changes: 3 additions & 0 deletions internal/controllers/variable_source.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@ func NewVariableSource(cl client.Client) variablesources.NestedVariableSource {
func(inputVariableSource input.VariableSource) (input.VariableSource, error) {
return variablesources.NewOperatorVariableSource(cl, inputVariableSource), nil
},
func(inputVariableSource input.VariableSource) (input.VariableSource, error) {
return variablesources.NewBundleDeploymentVariableSource(cl, inputVariableSource), nil
},
func(inputVariableSource input.VariableSource) (input.VariableSource, error) {
return variablesources.NewBundlesAndDepsVariableSource(inputVariableSource), nil
},
Expand Down
65 changes: 43 additions & 22 deletions internal/resolution/entities/bundle_entity.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,6 @@ const (

// ----

type ChannelProperties struct {
property.Channel
Replaces string `json:"replaces,omitempty"`
Skips []string `json:"skips,omitempty"`
SkipRange string `json:"skipRange,omitempty"`
}

type propertyRequirement bool

const (
Expand All @@ -46,6 +39,10 @@ type PackageRequired struct {

type GVK property.GVK

type Replaces struct {
Replaces string `json:"replaces"`
}

func (g GVK) String() string {
return fmt.Sprintf(`group:"%s" version:"%s" kind:"%s"`, g.Group, g.Version, g.Kind)
}
Expand All @@ -64,15 +61,16 @@ type BundleEntity struct {
*input.Entity

// these properties are lazy loaded as they are requested
bundlePackage *property.Package
providedGVKs []GVK
requiredGVKs []GVKRequired
requiredPackages []PackageRequired
channelProperties *ChannelProperties
semVersion *semver.Version
bundlePath string
mediaType string
mu sync.RWMutex
bundlePackage *property.Package
providedGVKs []GVK
requiredGVKs []GVKRequired
requiredPackages []PackageRequired
channel *property.Channel
replaces *Replaces
semVersion *semver.Version
bundlePath string
mediaType string
mu sync.RWMutex
}

func NewBundleEntity(entity *input.Entity) *BundleEntity {
Expand Down Expand Up @@ -121,14 +119,35 @@ func (b *BundleEntity) ChannelName() (string, error) {
if err := b.loadChannelProperties(); err != nil {
return "", err
}
return b.channelProperties.ChannelName, nil
return b.channel.ChannelName, nil
}

func (b *BundleEntity) ChannelProperties() (*ChannelProperties, error) {
func (b *BundleEntity) Channel() (*property.Channel, error) {
if err := b.loadChannelProperties(); err != nil {
return nil, err
}
return b.channelProperties, nil
return b.channel, nil
}

func (b *BundleEntity) Replaces() (string, error) {
if err := b.loadReplaces(); err != nil {
return "", err

Check warning on line 134 in internal/resolution/entities/bundle_entity.go

View check run for this annotation

Codecov / codecov/patch

internal/resolution/entities/bundle_entity.go#L134

Added line #L134 was not covered by tests
}
return b.replaces.Replaces, nil
}

func (b *BundleEntity) loadReplaces() error {
b.mu.Lock()
defer b.mu.Unlock()
if b.replaces == nil {
// TODO: move property name to constant
replaces, err := loadFromEntity[Replaces](b.Entity, "olm.replaces", optional)
if err != nil {
return fmt.Errorf("error determining replaces for entity '%s': %w", b.ID, err)

Check warning on line 146 in internal/resolution/entities/bundle_entity.go

View check run for this annotation

Codecov / codecov/patch

internal/resolution/entities/bundle_entity.go#L146

Added line #L146 was not covered by tests
}
b.replaces = &replaces
}
return nil
}

func (b *BundleEntity) BundlePath() (string, error) {
Expand Down Expand Up @@ -228,12 +247,12 @@ func (b *BundleEntity) loadRequiredPackages() error {
func (b *BundleEntity) loadChannelProperties() error {
b.mu.Lock()
defer b.mu.Unlock()
if b.channelProperties == nil {
channel, err := loadFromEntity[ChannelProperties](b.Entity, property.TypeChannel, required)
if b.channel == nil {
channel, err := loadFromEntity[property.Channel](b.Entity, property.TypeChannel, required)
if err != nil {
return fmt.Errorf("error determining bundle channel properties for entity '%s': %w", b.ID, err)
}
b.channelProperties = &channel
b.channel = &channel
}
return nil
}
Expand All @@ -255,6 +274,8 @@ func loadFromEntity[T interface{}](entity *input.Entity, propertyName string, re
deserializedProperty := *new(T)
propertyValue, ok := entity.Properties[propertyName]
if ok {
// TODO: In order to avoid invalid properties we should use a decoder that only allows the properties we expect.
// ie. decoder.DisallowUnknownFields()
if err := json.Unmarshal([]byte(propertyValue), &deserializedProperty); err != nil {
return deserializedProperty, fmt.Errorf("property '%s' ('%s') could not be parsed: %w", propertyName, propertyValue, err)
}
Expand Down
43 changes: 30 additions & 13 deletions internal/resolution/entities/bundle_entity_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -205,28 +205,24 @@ var _ = Describe("BundleEntity", func() {
})
})

Describe("ChannelProperties", func() {
Describe("Channel", func() {
It("should return the bundle channel properties if present", func() {
entity := input.NewEntity("operatorhub/prometheus/0.14.0", map[string]string{
"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"}`,
})
bundleEntity := olmentity.NewBundleEntity(entity)
channelProperties, err := bundleEntity.ChannelProperties()
channelProperties, err := bundleEntity.Channel()
Expect(err).ToNot(HaveOccurred())
Expect(*channelProperties).To(Equal(olmentity.ChannelProperties{
Channel: property.Channel{
ChannelName: "beta",
Priority: 0,
},
Replaces: "bundle.v1.0.0",
Skips: []string{"bundle.v0.9.0", "bundle.v0.9.6"},
SkipRange: ">=0.9.0 <=0.9.6",
}))
Expect(*channelProperties).To(Equal(property.Channel{
ChannelName: "beta",
Priority: 0,
},
))
})
It("should return an error if the property is not found", func() {
entity := input.NewEntity("operatorhub/prometheus/0.14.0", map[string]string{})
bundleEntity := olmentity.NewBundleEntity(entity)
channelProperties, err := bundleEntity.ChannelProperties()
channelProperties, err := bundleEntity.Channel()
Expect(channelProperties).To(BeNil())
Expect(err.Error()).To(Equal("error determining bundle channel properties for entity 'operatorhub/prometheus/0.14.0': required property 'olm.channel' not found"))
})
Expand All @@ -235,7 +231,7 @@ var _ = Describe("BundleEntity", func() {
"olm.channel": "badChannelPropertiesStructure",
})
bundleEntity := olmentity.NewBundleEntity(entity)
channelProperties, err := bundleEntity.ChannelProperties()
channelProperties, err := bundleEntity.Channel()
Expect(channelProperties).To(BeNil())
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"))
})
Expand Down Expand Up @@ -269,6 +265,27 @@ var _ = Describe("BundleEntity", func() {
})
})

Describe("Replaces", func() {
It("should return the replaces property if present", func() {
entity := input.NewEntity("test", map[string]string{
"olm.replaces": `{"replaces": "test.v0.2.0"}`,
})
bundleEntity := olmentity.NewBundleEntity(entity)
replaces, err := bundleEntity.Replaces()
Expect(err).ToNot(HaveOccurred())
Expect(replaces).To(Equal("test.v0.2.0"))
})
It("should not return an error if the property is not found", func() {
entity := input.NewEntity("test", map[string]string{
"olm.thingy": `{"whatever":"this"}`,
})
bundleEntity := olmentity.NewBundleEntity(entity)
replaces, err := bundleEntity.Replaces()
Expect(replaces).To(BeEmpty())
Expect(err).NotTo(HaveOccurred())
})
})

Describe("MediaType", func() {
It("should return the bundle mediatype property if present", func() {
entity := input.NewEntity("operatorhub/prometheus/0.14.0", map[string]string{
Expand Down
7 changes: 7 additions & 0 deletions internal/resolution/entitysources/catalogdsource.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,10 @@ func (es *CatalogdEntitySource) Iterate(ctx context.Context, fn input.IteratorFu
return nil
}

type replacesProperty struct {
Replaces string `json:"replaces"`
}

func getEntities(ctx context.Context, client client.Client) (input.EntityList, error) {
entityList := input.EntityList{}
bundleMetadatas, packageMetdatas, err := fetchMetadata(ctx, client)
Expand Down Expand Up @@ -106,6 +110,9 @@ func getEntities(ctx context.Context, client client.Client) (input.EntityList, e
if catalogScopedEntryName == bundle.Name {
channelValue, _ := json.Marshal(property.Channel{ChannelName: ch.Name, Priority: 0})
props[property.TypeChannel] = string(channelValue)
// TODO(jmprusi): Add the proper PropertyType for this
replacesValue, _ := json.Marshal(replacesProperty{Replaces: b.Replaces})
props["olm.replaces"] = string(replacesValue)
entity := input.Entity{
ID: deppy.IdentifierFromString(fmt.Sprintf("%s%s%s", bundle.Name, bundle.Spec.Package, ch.Name)),
Properties: props,
Expand Down
22 changes: 22 additions & 0 deletions internal/resolution/util/predicates/predicates.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,3 +56,25 @@ func ProvidesGVK(gvk *olmentity.GVK) input.Predicate {
return false
}
}

func WithBundleImage(bundleImage string) input.Predicate {
return func(entity *input.Entity) bool {
bundleEntity := olmentity.NewBundleEntity(entity)
bundlePath, err := bundleEntity.BundlePath()
if err != nil {
return false

Check warning on line 65 in internal/resolution/util/predicates/predicates.go

View check run for this annotation

Codecov / codecov/patch

internal/resolution/util/predicates/predicates.go#L65

Added line #L65 was not covered by tests
}
return bundlePath == bundleImage
}
}

func Replaces(bundleID string) input.Predicate {
return func(entity *input.Entity) bool {
bundleEntity := olmentity.NewBundleEntity(entity)
replaces, err := bundleEntity.Replaces()
if err != nil {
return false

Check warning on line 76 in internal/resolution/util/predicates/predicates.go

View check run for this annotation

Codecov / codecov/patch

internal/resolution/util/predicates/predicates.go#L76

Added line #L76 was not covered by tests
}
return replaces == bundleID
}
}
20 changes: 20 additions & 0 deletions internal/resolution/util/predicates/predicates_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,4 +68,24 @@ var _ = Describe("Predicates", func() {
})(entity)).To(BeFalse())
})
})

Describe("WithBundleImage", func() {
It("should return true when the entity provides the same bundle image", func() {
entity := input.NewEntity("test", map[string]string{
olmentity.PropertyBundlePath: `"registry.io/repo/image@sha256:1234567890"`,
})
Expect(predicates.WithBundleImage("registry.io/repo/image@sha256:1234567890")(entity)).To(BeTrue())
Expect(predicates.WithBundleImage("registry.io/repo/image@sha256:0987654321")(entity)).To(BeFalse())
})
})

Describe("Replaces", func() {
It("should return true when the entity provides the replaces property", func() {
entity := input.NewEntity("test", map[string]string{
"olm.replaces": `{"replaces": "test.v0.2.0"}`,
})
Expect(predicates.Replaces("test.v0.2.0")(entity)).To(BeTrue())
Expect(predicates.Replaces("test.v0.1.0")(entity)).To(BeFalse())
})
})
})
4 changes: 2 additions & 2 deletions internal/resolution/util/sort/sort.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,8 @@ func packageOrder(e1, e2 *entities.BundleEntity) int {
}

func channelOrder(e1, e2 *entities.BundleEntity) int {
channelProperties1, err1 := e1.ChannelProperties()
channelProperties2, err2 := e2.ChannelProperties()
channelProperties1, err1 := e1.Channel()
channelProperties2, err2 := e2.Channel()
errComp := compareErrors(err1, err2)
if errComp != 0 {
return errComp
Expand Down
32 changes: 32 additions & 0 deletions internal/resolution/variables/installed_package.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package variables

import (
"fmt"

"github.com/operator-framework/deppy/pkg/deppy"
"github.com/operator-framework/deppy/pkg/deppy/constraint"
"github.com/operator-framework/deppy/pkg/deppy/input"

olmentity "github.com/operator-framework/operator-controller/internal/resolution/entities"
)

type InstalledPackageVariable struct {
*input.SimpleVariable
bundleEntities []*olmentity.BundleEntity
}

func (r *InstalledPackageVariable) BundleEntities() []*olmentity.BundleEntity {
return r.bundleEntities

Check warning on line 19 in internal/resolution/variables/installed_package.go

View check run for this annotation

Codecov / codecov/patch

internal/resolution/variables/installed_package.go#L19

Added line #L19 was not covered by tests
}

func NewInstalledPackageVariable(bundleImage string, bundleEntities []*olmentity.BundleEntity) *InstalledPackageVariable {
id := deppy.IdentifierFromString(fmt.Sprintf("installed package %s", bundleImage))
entityIDs := make([]deppy.Identifier, 0, len(bundleEntities))
for _, bundle := range bundleEntities {
entityIDs = append(entityIDs, bundle.ID)

Check warning on line 26 in internal/resolution/variables/installed_package.go

View check run for this annotation

Codecov / codecov/patch

internal/resolution/variables/installed_package.go#L23-L26

Added lines #L23 - L26 were not covered by tests
}
return &InstalledPackageVariable{
SimpleVariable: input.NewSimpleVariable(id, constraint.Mandatory(), constraint.Dependency(entityIDs...)),
bundleEntities: bundleEntities,

Check warning on line 30 in internal/resolution/variables/installed_package.go

View check run for this annotation

Codecov / codecov/patch

internal/resolution/variables/installed_package.go#L28-L30

Added lines #L28 - L30 were not covered by tests
}
}
58 changes: 58 additions & 0 deletions internal/resolution/variablesources/bundle_deployment.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package variablesources

import (
"context"
"fmt"

"github.com/operator-framework/deppy/pkg/deppy"
"github.com/operator-framework/deppy/pkg/deppy/input"
rukpakv1alpha1 "github.com/operator-framework/rukpak/api/v1alpha1"
"sigs.k8s.io/controller-runtime/pkg/client"
)

var _ input.VariableSource = &BundleDeploymentVariableSource{}

type BundleDeploymentVariableSource struct {
client client.Client
inputVariableSource input.VariableSource
}

func NewBundleDeploymentVariableSource(cl client.Client, inputVariableSource input.VariableSource) *BundleDeploymentVariableSource {
return &BundleDeploymentVariableSource{
client: cl,
inputVariableSource: inputVariableSource,

Check warning on line 23 in internal/resolution/variablesources/bundle_deployment.go

View check run for this annotation

Codecov / codecov/patch

internal/resolution/variablesources/bundle_deployment.go#L21-L23

Added lines #L21 - L23 were not covered by tests
}
}

func (o *BundleDeploymentVariableSource) GetVariables(ctx context.Context, entitySource input.EntitySource) ([]deppy.Variable, error) {
variableSources := SliceVariableSource{}
if o.inputVariableSource != nil {
variableSources = append(variableSources, o.inputVariableSource)

Check warning on line 30 in internal/resolution/variablesources/bundle_deployment.go

View check run for this annotation

Codecov / codecov/patch

internal/resolution/variablesources/bundle_deployment.go#L28-L30

Added lines #L28 - L30 were not covered by tests
}

bundleDeployments := rukpakv1alpha1.BundleDeploymentList{}
if err := o.client.List(ctx, &bundleDeployments); err != nil {
return nil, err

Check warning on line 35 in internal/resolution/variablesources/bundle_deployment.go

View check run for this annotation

Codecov / codecov/patch

internal/resolution/variablesources/bundle_deployment.go#L33-L35

Added lines #L33 - L35 were not covered by tests
}

processed := map[string]struct{}{}
if len(bundleDeployments.Items) > 1 {
fmt.Println("hello")

Check warning on line 40 in internal/resolution/variablesources/bundle_deployment.go

View check run for this annotation

Codecov / codecov/patch

internal/resolution/variablesources/bundle_deployment.go#L38-L40

Added lines #L38 - L40 were not covered by tests
}
for _, bundleDeployment := range bundleDeployments.Items {
sourceImage := bundleDeployment.Spec.Template.Spec.Source.Image
if sourceImage != nil && sourceImage.Ref != "" {
if _, ok := processed[sourceImage.Ref]; ok {
continue

Check warning on line 46 in internal/resolution/variablesources/bundle_deployment.go

View check run for this annotation

Codecov / codecov/patch

internal/resolution/variablesources/bundle_deployment.go#L42-L46

Added lines #L42 - L46 were not covered by tests
}
processed[sourceImage.Ref] = struct{}{}
ips, err := NewInstalledPackageVariableSource(bundleDeployment.Spec.Template.Spec.Source.Image.Ref)
if err != nil {
return nil, err

Check warning on line 51 in internal/resolution/variablesources/bundle_deployment.go

View check run for this annotation

Codecov / codecov/patch

internal/resolution/variablesources/bundle_deployment.go#L48-L51

Added lines #L48 - L51 were not covered by tests
}
variableSources = append(variableSources, ips)

Check warning on line 53 in internal/resolution/variablesources/bundle_deployment.go

View check run for this annotation

Codecov / codecov/patch

internal/resolution/variablesources/bundle_deployment.go#L53

Added line #L53 was not covered by tests
}
}

return variableSources.GetVariables(ctx, entitySource)

Check warning on line 57 in internal/resolution/variablesources/bundle_deployment.go

View check run for this annotation

Codecov / codecov/patch

internal/resolution/variablesources/bundle_deployment.go#L57

Added line #L57 was not covered by tests
}
Loading

0 comments on commit 2da8d70

Please sign in to comment.