Skip to content

Commit

Permalink
(feat): add graph deprecation logic (#574)
Browse files Browse the repository at this point in the history
to signal when an installed bundle has deprecations
associated with it and prefer bundles with deprecations
less than non-deprecated bundles

Signed-off-by: everettraven <everettraven@gmail.com>
  • Loading branch information
everettraven authored Jan 12, 2024
1 parent fe692ed commit 1e6ad96
Show file tree
Hide file tree
Showing 15 changed files with 892 additions and 10 deletions.
12 changes: 12 additions & 0 deletions api/v1alpha1/clusterextension_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,12 @@ const (
// TODO(user): add more Types, here and into init()
TypeInstalled = "Installed"
TypeResolved = "Resolved"
// TypeDeprecated is a rollup condition that is present when
// any of the deprecated conditions are present.
TypeDeprecated = "Deprecated"
TypePackageDeprecated = "PackageDeprecated"
TypeChannelDeprecated = "ChannelDeprecated"
TypeBundleDeprecated = "BundleDeprecated"

ReasonBundleLookupFailed = "BundleLookupFailed"
ReasonInstallationFailed = "InstallationFailed"
Expand All @@ -80,13 +86,18 @@ const (
ReasonResolutionFailed = "ResolutionFailed"
ReasonResolutionUnknown = "ResolutionUnknown"
ReasonSuccess = "Success"
ReasonDeprecated = "Deprecated"
)

func init() {
// TODO(user): add Types from above
conditionsets.ConditionTypes = append(conditionsets.ConditionTypes,
TypeInstalled,
TypeResolved,
TypeDeprecated,
TypePackageDeprecated,
TypeChannelDeprecated,
TypeBundleDeprecated,
)
// TODO(user): add Reasons from above
conditionsets.ConditionReasons = append(conditionsets.ConditionReasons,
Expand All @@ -98,6 +109,7 @@ func init() {
ReasonInstallationStatusUnknown,
ReasonInvalidSpec,
ReasonSuccess,
ReasonDeprecated,
)
}

Expand Down
13 changes: 10 additions & 3 deletions cmd/resolutioncli/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,9 @@ func (c *indexRefClient) Bundles(ctx context.Context) ([]*catalogmetadata.Bundle
}

var (
channels []*catalogmetadata.Channel
bundles []*catalogmetadata.Bundle
channels []*catalogmetadata.Channel
bundles []*catalogmetadata.Bundle
deprecations []*catalogmetadata.Deprecation
)

for i := range cfg.Channels {
Expand All @@ -63,10 +64,16 @@ func (c *indexRefClient) Bundles(ctx context.Context) ([]*catalogmetadata.Bundle
})
}

for i := range cfg.Deprecations {
deprecations = append(deprecations, &catalogmetadata.Deprecation{
Deprecation: cfg.Deprecations[i],
})
}

// TODO: update fake catalog name string to be catalog name once we support multiple catalogs in CLI
catalogName := "offline-catalog"

bundles, err = client.PopulateExtraFields(catalogName, channels, bundles)
bundles, err = client.PopulateExtraFields(catalogName, channels, bundles, deprecations)
if err != nil {
return nil, err
}
Expand Down
41 changes: 39 additions & 2 deletions internal/catalogmetadata/client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ func (c *Client) Bundles(ctx context.Context) ([]*catalogmetadata.Bundle, error)
}
channels := []*catalogmetadata.Channel{}
bundles := []*catalogmetadata.Bundle{}
deprecations := []*catalogmetadata.Deprecation{}

rc, err := c.fetcher.FetchCatalogContents(ctx, catalog.DeepCopy())
if err != nil {
Expand All @@ -81,6 +82,12 @@ func (c *Client) Bundles(ctx context.Context) ([]*catalogmetadata.Bundle, error)
return fmt.Errorf("error unmarshalling bundle from catalog metadata: %s", err)
}
bundles = append(bundles, &content)
case declcfg.SchemaDeprecation:
var content catalogmetadata.Deprecation
if err := json.Unmarshal(meta.Blob, &content); err != nil {
return fmt.Errorf("error unmarshalling deprecation from catalog metadata: %s", err)
}
deprecations = append(deprecations, &content)
}

return nil
Expand All @@ -89,7 +96,7 @@ func (c *Client) Bundles(ctx context.Context) ([]*catalogmetadata.Bundle, error)
return nil, fmt.Errorf("error processing response: %s", err)
}

bundles, err = PopulateExtraFields(catalog.Name, channels, bundles)
bundles, err = PopulateExtraFields(catalog.Name, channels, bundles, deprecations)
if err != nil {
return nil, err
}
Expand All @@ -100,7 +107,7 @@ func (c *Client) Bundles(ctx context.Context) ([]*catalogmetadata.Bundle, error)
return allBundles, nil
}

func PopulateExtraFields(catalogName string, channels []*catalogmetadata.Channel, bundles []*catalogmetadata.Bundle) ([]*catalogmetadata.Bundle, error) {
func PopulateExtraFields(catalogName string, channels []*catalogmetadata.Channel, bundles []*catalogmetadata.Bundle, deprecations []*catalogmetadata.Deprecation) ([]*catalogmetadata.Bundle, error) {
bundlesMap := map[string]*catalogmetadata.Bundle{}
for i := range bundles {
bundleKey := fmt.Sprintf("%s-%s", bundles[i].Package, bundles[i].Name)
Expand All @@ -121,5 +128,35 @@ func PopulateExtraFields(catalogName string, channels []*catalogmetadata.Channel
}
}

// According to https://docs.google.com/document/d/1EzefSzoGZL2ipBt-eCQwqqNwlpOIt7wuwjG6_8ZCi5s/edit?usp=sharing
// the olm.deprecations FBC object is only valid when either 0 or 1 instances exist
// for any given package
deprecationMap := make(map[string]*catalogmetadata.Deprecation, len(deprecations))
for _, deprecation := range deprecations {
deprecationMap[deprecation.Package] = deprecation
}

for i := range bundles {
if dep, ok := deprecationMap[bundles[i].Package]; ok {
for _, entry := range dep.Entries {
switch entry.Reference.Schema {
case declcfg.SchemaPackage:
bundles[i].Deprecations = append(bundles[i].Deprecations, entry)
case declcfg.SchemaChannel:
for _, ch := range bundles[i].InChannels {
if ch.Name == entry.Reference.Name {
bundles[i].Deprecations = append(bundles[i].Deprecations, entry)
break
}
}
case declcfg.SchemaBundle:
if bundles[i].Name == entry.Reference.Name {
bundles[i].Deprecations = append(bundles[i].Deprecations, entry)
}
}
}
}
}

return bundles, nil
}
39 changes: 39 additions & 0 deletions internal/catalogmetadata/client/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,45 @@ func TestClient(t *testing.T) {
},
fetcher: &MockFetcher{},
},
{
name: "deprecated at the package, channel, and bundle level",
fakeCatalog: func() ([]client.Object, []*catalogmetadata.Bundle, map[string][]byte) {
objs, bundles, catalogContentMap := defaultFakeCatalog()

catalogContentMap["catalog-1"] = append(catalogContentMap["catalog-1"],
[]byte(`{"schema": "olm.deprecations", "package":"fake1", "entries":[{"message": "fake1 is deprecated", "reference": {"schema": "olm.package"}}, {"message":"channel stable is deprecated", "reference": {"schema": "olm.channel", "name": "stable"}}, {"message": "bundle fake1.v1.0.0 is deprecated", "reference":{"schema":"olm.bundle", "name":"fake1.v1.0.0"}}]}`)...)

for i := range bundles {
if bundles[i].Package == "fake1" && bundles[i].CatalogName == "catalog-1" && bundles[i].Name == "fake1.v1.0.0" {
bundles[i].Deprecations = append(bundles[i].Deprecations, declcfg.DeprecationEntry{
Reference: declcfg.PackageScopedReference{
Schema: "olm.package",
},
Message: "fake1 is deprecated",
})

bundles[i].Deprecations = append(bundles[i].Deprecations, declcfg.DeprecationEntry{
Reference: declcfg.PackageScopedReference{
Schema: "olm.channel",
Name: "stable",
},
Message: "channel stable is deprecated",
})

bundles[i].Deprecations = append(bundles[i].Deprecations, declcfg.DeprecationEntry{
Reference: declcfg.PackageScopedReference{
Schema: "olm.bundle",
Name: "fake1.v1.0.0",
},
Message: "bundle fake1.v1.0.0 is deprecated",
})
}
}

return objs, bundles, catalogContentMap
},
fetcher: &MockFetcher{},
},
} {
t.Run(tt.name, func(t *testing.T) {
ctx := context.Background()
Expand Down
6 changes: 6 additions & 0 deletions internal/catalogmetadata/filter/bundle_predicates.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,3 +70,9 @@ func Replaces(bundleName string) Predicate[catalogmetadata.Bundle] {
return false
}
}

func WithDeprecation(deprecated bool) Predicate[catalogmetadata.Bundle] {
return func(bundle *catalogmetadata.Bundle) bool {
return bundle.HasDeprecation() == deprecated
}
}
16 changes: 16 additions & 0 deletions internal/catalogmetadata/filter/bundle_predicates_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -158,3 +158,19 @@ func TestReplaces(t *testing.T) {
assert.False(t, f(b2))
assert.False(t, f(b3))
}

func TestWithDeprecation(t *testing.T) {
b1 := &catalogmetadata.Bundle{
Deprecations: []declcfg.DeprecationEntry{
{
Reference: declcfg.PackageScopedReference{},
},
},
}

b2 := &catalogmetadata.Bundle{}

f := filter.WithDeprecation(true)
assert.True(t, f(b1))
assert.False(t, f(b2))
}
19 changes: 19 additions & 0 deletions internal/catalogmetadata/sort/sort.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,25 @@ func ByVersion(b1, b2 *catalogmetadata.Bundle) bool {
return ver1.GT(*ver2)
}

// ByDeprecation is a sort "less" function that orders bundles
// that are deprecated lower than ones without deprecations
func ByDeprecated(b1, b2 *catalogmetadata.Bundle) bool {
b1Val := 1
b2Val := 1

if b1.IsDeprecated() {
b1Val = b1Val - 1
}

if b2.IsDeprecated() {
b2Val = b2Val - 1
}

// Check for "greater than" because we
// non deprecated on top
return b1Val > b2Val
}

// compareErrors returns 0 if both errors are either nil or not nil
// -1 if err1 is nil and err2 is not nil
// +1 if err1 is not nil and err2 is nil
Expand Down
51 changes: 51 additions & 0 deletions internal/catalogmetadata/sort/sort_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

"github.com/operator-framework/operator-registry/alpha/declcfg"
"github.com/operator-framework/operator-registry/alpha/property"
Expand Down Expand Up @@ -81,3 +82,53 @@ func TestByVersion(t *testing.T) {
assert.Equal(t, b5empty, toSort[4])
})
}

func TestByDeprecated(t *testing.T) {
b1 := &catalogmetadata.Bundle{
CatalogName: "foo",
Bundle: declcfg.Bundle{
Name: "bar",
},
}

b2 := &catalogmetadata.Bundle{
CatalogName: "foo",
Bundle: declcfg.Bundle{
Name: "baz",
},
Deprecations: []declcfg.DeprecationEntry{
{
Reference: declcfg.PackageScopedReference{
Schema: "olm.bundle",
Name: "baz",
},
},
},
}

toSort := []*catalogmetadata.Bundle{b2, b1}
sort.SliceStable(toSort, func(i, j int) bool {
return catalogsort.ByDeprecated(toSort[i], toSort[j])
})

require.Len(t, toSort, 2)
assert.Equal(t, b1, toSort[0])
assert.Equal(t, b2, toSort[1])

// Channel deprecation association != bundle deprecated
b2.Deprecations[0] = declcfg.DeprecationEntry{
Reference: declcfg.PackageScopedReference{
Schema: "olm.channel",
Name: "badchannel",
},
}

toSort = []*catalogmetadata.Bundle{b2, b1}
sort.SliceStable(toSort, func(i, j int) bool {
return catalogsort.ByDeprecated(toSort[i], toSort[j])
})
// No bundles are deprecated so ordering should remain the same
require.Len(t, toSort, 2)
assert.Equal(t, b2, toSort[0])
assert.Equal(t, b1, toSort[1])
}
44 changes: 41 additions & 3 deletions internal/catalogmetadata/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ const (
)

type Schemas interface {
Package | Bundle | Channel
Package | Bundle | Channel | Deprecation
}

type Package struct {
Expand All @@ -29,15 +29,20 @@ type Channel struct {
declcfg.Channel
}

type Deprecation struct {
declcfg.Deprecation
}

type PackageRequired struct {
property.PackageRequired
SemverRange bsemver.Range `json:"-"`
}

type Bundle struct {
declcfg.Bundle
CatalogName string
InChannels []*Channel
CatalogName string
InChannels []*Channel
Deprecations []declcfg.DeprecationEntry

mu sync.RWMutex
// these properties are lazy loaded as they are requested
Expand Down Expand Up @@ -140,6 +145,39 @@ func (b *Bundle) propertiesByType(propType string) []*property.Property {
return b.propertiesMap[propType]
}

// HasDeprecation returns true if the bundle
// has any deprecations associated with it.
// This may return true even in cases where the bundle
// may be associated with an olm.channel deprecation
// but the bundle is not considered "deprecated" because
// the bundle is selected via a non-deprecated channel.
func (b *Bundle) HasDeprecation() bool {
return len(b.Deprecations) > 0
}

// IsDeprecated returns true if the bundle
// has been explicitly deprecated. This can occur
// in one of two ways:
// - the olm.package the bundle belongs to has been deprecated
// - the bundle itself has been deprecated
// this function does not take into consideration
// olm.channel deprecations associated with the bundle
// as a bundle can be present in multiple channels with
// some channels being deprecated and some not.
func (b *Bundle) IsDeprecated() bool {
for _, dep := range b.Deprecations {
if dep.Reference.Schema == declcfg.SchemaPackage && dep.Reference.Name == b.Package {
return true
}

if dep.Reference.Schema == declcfg.SchemaBundle && dep.Reference.Name == b.Name {
return true
}
}

return false
}

func loadOneFromProps[T any](bundle *Bundle, propType string, required bool) (T, error) {
r, err := loadFromProps[T](bundle, propType, required)
if err != nil {
Expand Down
Loading

0 comments on commit 1e6ad96

Please sign in to comment.