diff --git a/api/v1alpha1/clusterextension_types.go b/api/v1alpha1/clusterextension_types.go index 783e0c182..c617324dd 100644 --- a/api/v1alpha1/clusterextension_types.go +++ b/api/v1alpha1/clusterextension_types.go @@ -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" @@ -80,6 +86,7 @@ const ( ReasonResolutionFailed = "ResolutionFailed" ReasonResolutionUnknown = "ResolutionUnknown" ReasonSuccess = "Success" + ReasonDeprecated = "Deprecated" ) func init() { @@ -87,6 +94,10 @@ func init() { conditionsets.ConditionTypes = append(conditionsets.ConditionTypes, TypeInstalled, TypeResolved, + TypeDeprecated, + TypePackageDeprecated, + TypeChannelDeprecated, + TypeBundleDeprecated, ) // TODO(user): add Reasons from above conditionsets.ConditionReasons = append(conditionsets.ConditionReasons, @@ -98,6 +109,7 @@ func init() { ReasonInstallationStatusUnknown, ReasonInvalidSpec, ReasonSuccess, + ReasonDeprecated, ) } diff --git a/cmd/resolutioncli/client.go b/cmd/resolutioncli/client.go index b9b931cf7..0d36a8909 100644 --- a/cmd/resolutioncli/client.go +++ b/cmd/resolutioncli/client.go @@ -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 { @@ -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 } diff --git a/internal/catalogmetadata/client/client.go b/internal/catalogmetadata/client/client.go index 80d879824..172f686c6 100644 --- a/internal/catalogmetadata/client/client.go +++ b/internal/catalogmetadata/client/client.go @@ -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 { @@ -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 @@ -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 } @@ -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) @@ -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 } diff --git a/internal/catalogmetadata/client/client_test.go b/internal/catalogmetadata/client/client_test.go index 89ddcfac9..9bcc54850 100644 --- a/internal/catalogmetadata/client/client_test.go +++ b/internal/catalogmetadata/client/client_test.go @@ -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() diff --git a/internal/catalogmetadata/filter/bundle_predicates.go b/internal/catalogmetadata/filter/bundle_predicates.go index e62df9187..1d5761a95 100644 --- a/internal/catalogmetadata/filter/bundle_predicates.go +++ b/internal/catalogmetadata/filter/bundle_predicates.go @@ -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 + } +} diff --git a/internal/catalogmetadata/filter/bundle_predicates_test.go b/internal/catalogmetadata/filter/bundle_predicates_test.go index 95e9482f0..3617f47ce 100644 --- a/internal/catalogmetadata/filter/bundle_predicates_test.go +++ b/internal/catalogmetadata/filter/bundle_predicates_test.go @@ -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)) +} diff --git a/internal/catalogmetadata/sort/sort.go b/internal/catalogmetadata/sort/sort.go index 7b220b7db..adfd0dd5c 100644 --- a/internal/catalogmetadata/sort/sort.go +++ b/internal/catalogmetadata/sort/sort.go @@ -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 diff --git a/internal/catalogmetadata/sort/sort_test.go b/internal/catalogmetadata/sort/sort_test.go index b18ce616f..9d1e2e275 100644 --- a/internal/catalogmetadata/sort/sort_test.go +++ b/internal/catalogmetadata/sort/sort_test.go @@ -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" @@ -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]) +} diff --git a/internal/catalogmetadata/types.go b/internal/catalogmetadata/types.go index 4eb72bcf4..bcbdd3390 100644 --- a/internal/catalogmetadata/types.go +++ b/internal/catalogmetadata/types.go @@ -18,7 +18,7 @@ const ( ) type Schemas interface { - Package | Bundle | Channel + Package | Bundle | Channel | Deprecation } type Package struct { @@ -29,6 +29,10 @@ type Channel struct { declcfg.Channel } +type Deprecation struct { + declcfg.Deprecation +} + type PackageRequired struct { property.PackageRequired SemverRange bsemver.Range `json:"-"` @@ -36,8 +40,9 @@ type PackageRequired struct { 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 @@ -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 { diff --git a/internal/catalogmetadata/types_test.go b/internal/catalogmetadata/types_test.go index 8ecb92508..a148ed738 100644 --- a/internal/catalogmetadata/types_test.go +++ b/internal/catalogmetadata/types_test.go @@ -201,3 +201,31 @@ func TestBundleMediaType(t *testing.T) { }) } } + +func TestBundleHasDeprecation(t *testing.T) { + for _, tt := range []struct { + name string + bundle *catalogmetadata.Bundle + deprecated bool + }{ + { + name: "has deprecation entries", + bundle: &catalogmetadata.Bundle{ + Deprecations: []declcfg.DeprecationEntry{ + { + Reference: declcfg.PackageScopedReference{}, + }, + }, + }, + deprecated: true, + }, + { + name: "has no deprecation entries", + bundle: &catalogmetadata.Bundle{}, + }, + } { + t.Run(tt.name, func(t *testing.T) { + assert.Equal(t, tt.deprecated, tt.bundle.HasDeprecation()) + }) + } +} diff --git a/internal/controllers/clusterextension_controller.go b/internal/controllers/clusterextension_controller.go index 56c256e3d..0a0ac5233 100644 --- a/internal/controllers/clusterextension_controller.go +++ b/internal/controllers/clusterextension_controller.go @@ -19,11 +19,13 @@ package controllers import ( "context" "fmt" + "strings" "github.com/go-logr/logr" catalogd "github.com/operator-framework/catalogd/api/core/v1alpha1" "github.com/operator-framework/deppy/pkg/deppy" "github.com/operator-framework/deppy/pkg/deppy/solver" + "github.com/operator-framework/operator-registry/alpha/declcfg" rukpakv1alpha1 "github.com/operator-framework/rukpak/api/v1alpha1" "k8s.io/apimachinery/pkg/api/equality" apimeta "k8s.io/apimachinery/pkg/api/meta" @@ -130,6 +132,8 @@ func (r *ClusterExtensionReconciler) reconcile(ctx context.Context, ext *ocv1alp // hasn't been attempted yet, due to the spec being invalid. ext.Status.ResolvedBundleResource = "" setResolvedStatusConditionUnknown(&ext.Status.Conditions, "validation has not been attempted as spec is invalid", ext.GetGeneration()) + + setDeprecationStatusesUnknown(&ext.Status.Conditions, "deprecation checks have not been attempted as spec is invalid", ext.GetGeneration()) return ctrl.Result{}, nil } @@ -140,6 +144,8 @@ func (r *ClusterExtensionReconciler) reconcile(ctx context.Context, ext *ocv1alp setInstalledStatusConditionUnknown(&ext.Status.Conditions, "installation has not been attempted due to failure to gather data for resolution", ext.GetGeneration()) ext.Status.ResolvedBundleResource = "" setResolvedStatusConditionFailed(&ext.Status.Conditions, err.Error(), ext.GetGeneration()) + + setDeprecationStatusesUnknown(&ext.Status.Conditions, "deprecation checks have not been attempted due to failure to gather data for resolution", ext.GetGeneration()) return ctrl.Result{}, err } @@ -150,6 +156,8 @@ func (r *ClusterExtensionReconciler) reconcile(ctx context.Context, ext *ocv1alp setInstalledStatusConditionUnknown(&ext.Status.Conditions, "installation has not been attempted as resolution failed", ext.GetGeneration()) ext.Status.ResolvedBundleResource = "" setResolvedStatusConditionFailed(&ext.Status.Conditions, err.Error(), ext.GetGeneration()) + + setDeprecationStatusesUnknown(&ext.Status.Conditions, "deprecation checks have not been attempted as resolution failed", ext.GetGeneration()) return ctrl.Result{}, err } @@ -161,6 +169,8 @@ func (r *ClusterExtensionReconciler) reconcile(ctx context.Context, ext *ocv1alp setInstalledStatusConditionUnknown(&ext.Status.Conditions, "installation has not been attempted as resolution failed", ext.GetGeneration()) ext.Status.ResolvedBundleResource = "" setResolvedStatusConditionFailed(&ext.Status.Conditions, err.Error(), ext.GetGeneration()) + + setDeprecationStatusesUnknown(&ext.Status.Conditions, "deprecation checks have not been attempted as resolution failed", ext.GetGeneration()) return ctrl.Result{}, err } @@ -168,14 +178,18 @@ func (r *ClusterExtensionReconciler) reconcile(ctx context.Context, ext *ocv1alp ext.Status.ResolvedBundleResource = bundle.Image setResolvedStatusConditionSuccess(&ext.Status.Conditions, fmt.Sprintf("resolved to %q", bundle.Image), ext.GetGeneration()) + // TODO: Question - Should we set the deprecation statuses after we have successfully resolved instead of after a successful installation? + mediaType, err := bundle.MediaType() if err != nil { setInstalledStatusConditionFailed(&ext.Status.Conditions, err.Error(), ext.GetGeneration()) + setDeprecationStatusesUnknown(&ext.Status.Conditions, "deprecation checks have not been attempted as installation has failed", ext.GetGeneration()) return ctrl.Result{}, err } bundleProvisioner, err := mapBundleMediaTypeToBundleProvisioner(mediaType) if err != nil { setInstalledStatusConditionFailed(&ext.Status.Conditions, err.Error(), ext.GetGeneration()) + setDeprecationStatusesUnknown(&ext.Status.Conditions, "deprecation checks have not been attempted as installation has failed", ext.GetGeneration()) return ctrl.Result{}, err } // Ensure a BundleDeployment exists with its bundle source from the bundle @@ -185,6 +199,7 @@ func (r *ClusterExtensionReconciler) reconcile(ctx context.Context, ext *ocv1alp // originally Reason: ocv1alpha1.ReasonInstallationFailed ext.Status.InstalledBundleResource = "" setInstalledStatusConditionFailed(&ext.Status.Conditions, err.Error(), ext.GetGeneration()) + setDeprecationStatusesUnknown(&ext.Status.Conditions, "deprecation checks have not been attempted as installation has failed", ext.GetGeneration()) return ctrl.Result{}, err } @@ -194,6 +209,7 @@ func (r *ClusterExtensionReconciler) reconcile(ctx context.Context, ext *ocv1alp // originally Reason: ocv1alpha1.ReasonInstallationStatusUnknown ext.Status.InstalledBundleResource = "" setInstalledStatusConditionUnknown(&ext.Status.Conditions, err.Error(), ext.GetGeneration()) + setDeprecationStatusesUnknown(&ext.Status.Conditions, "deprecation checks have not been attempted as installation has failed", ext.GetGeneration()) return ctrl.Result{}, err } @@ -201,6 +217,8 @@ func (r *ClusterExtensionReconciler) reconcile(ctx context.Context, ext *ocv1alp // existing BundleDeployment object status. mapBDStatusToInstalledCondition(existingTypedBundleDeployment, ext) + SetDeprecationStatus(ext, bundle) + // set the status of the cluster extension based on the respective bundle deployment status conditions. return ctrl.Result{}, nil } @@ -267,6 +285,88 @@ func mapBDStatusToInstalledCondition(existingTypedBundleDeployment *rukpakv1alph } } +// setDeprecationStatus will set the appropriate deprecation statuses for a ClusterExtension +// based on the provided bundle +func SetDeprecationStatus(ext *ocv1alpha1.ClusterExtension, bundle *catalogmetadata.Bundle) { + // reset conditions to false + conditionTypes := []string{ + ocv1alpha1.TypeDeprecated, + ocv1alpha1.TypePackageDeprecated, + ocv1alpha1.TypeChannelDeprecated, + ocv1alpha1.TypeBundleDeprecated, + } + + for _, conditionType := range conditionTypes { + apimeta.SetStatusCondition(&ext.Status.Conditions, metav1.Condition{ + Type: conditionType, + Reason: ocv1alpha1.ReasonDeprecated, + Status: metav1.ConditionFalse, + Message: "", + ObservedGeneration: ext.Generation, + }) + } + + // There are two early return scenarios here: + // 1) The bundle is not deprecated (i.e no package or bundle deprecations) + // AND there are no other deprecations associated with the bundle + // 2) The bundle is not deprecated, there are deprecations associated + // with the bundle (i.e at least one channel the bundle is present in is deprecated), + // and the ClusterExtension does not specify a channel. This is because the channel deprecations + // are a loose deprecation coupling on the bundle. A ClusterExtension installation is only + // considered deprecated by a channel deprecation when a deprecated channel is specified via + // the spec.channel field. + if (!bundle.IsDeprecated() && !bundle.HasDeprecation()) || (!bundle.IsDeprecated() && ext.Spec.Channel == "") { + return + } + + deprecationMessages := []string{} + + for _, deprecation := range bundle.Deprecations { + switch deprecation.Reference.Schema { + case declcfg.SchemaPackage: + apimeta.SetStatusCondition(&ext.Status.Conditions, metav1.Condition{ + Type: ocv1alpha1.TypePackageDeprecated, + Reason: ocv1alpha1.ReasonDeprecated, + Status: metav1.ConditionTrue, + Message: deprecation.Message, + ObservedGeneration: ext.Generation, + }) + case declcfg.SchemaChannel: + if ext.Spec.Channel != deprecation.Reference.Name { + continue + } + + apimeta.SetStatusCondition(&ext.Status.Conditions, metav1.Condition{ + Type: ocv1alpha1.TypeChannelDeprecated, + Reason: ocv1alpha1.ReasonDeprecated, + Status: metav1.ConditionTrue, + Message: deprecation.Message, + ObservedGeneration: ext.Generation, + }) + case declcfg.SchemaBundle: + apimeta.SetStatusCondition(&ext.Status.Conditions, metav1.Condition{ + Type: ocv1alpha1.TypeBundleDeprecated, + Reason: ocv1alpha1.ReasonDeprecated, + Status: metav1.ConditionTrue, + Message: deprecation.Message, + ObservedGeneration: ext.Generation, + }) + } + + deprecationMessages = append(deprecationMessages, deprecation.Message) + } + + if len(deprecationMessages) > 0 { + apimeta.SetStatusCondition(&ext.Status.Conditions, metav1.Condition{ + Type: ocv1alpha1.TypeDeprecated, + Reason: ocv1alpha1.ReasonDeprecated, + Status: metav1.ConditionTrue, + Message: strings.Join(deprecationMessages, ";"), + ObservedGeneration: ext.Generation, + }) + } +} + func (r *ClusterExtensionReconciler) bundleFromSolution(selection []deppy.Variable, packageName string) (*catalogmetadata.Bundle, error) { for _, variable := range selection { switch v := variable.(type) { @@ -457,6 +557,25 @@ func setInstalledStatusConditionUnknown(conditions *[]metav1.Condition, message }) } +func setDeprecationStatusesUnknown(conditions *[]metav1.Condition, message string, generation int64) { + conditionTypes := []string{ + ocv1alpha1.TypeDeprecated, + ocv1alpha1.TypePackageDeprecated, + ocv1alpha1.TypeChannelDeprecated, + ocv1alpha1.TypeBundleDeprecated, + } + + for _, conditionType := range conditionTypes { + apimeta.SetStatusCondition(conditions, metav1.Condition{ + Type: conditionType, + Reason: ocv1alpha1.ReasonDeprecated, + Status: metav1.ConditionUnknown, + Message: message, + ObservedGeneration: generation, + }) + } +} + // Generate reconcile requests for all cluster extensions affected by a catalog change func clusterExtensionRequestsForCatalog(c client.Reader, logger logr.Logger) handler.MapFunc { return func(ctx context.Context, _ client.Object) []reconcile.Request { diff --git a/internal/controllers/clusterextension_controller_test.go b/internal/controllers/clusterextension_controller_test.go index 3e8b749c6..2947a7dff 100644 --- a/internal/controllers/clusterextension_controller_test.go +++ b/internal/controllers/clusterextension_controller_test.go @@ -6,6 +6,8 @@ import ( "fmt" "testing" + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" "github.com/operator-framework/operator-registry/alpha/declcfg" "github.com/operator-framework/operator-registry/alpha/property" rukpakv1alpha1 "github.com/operator-framework/rukpak/api/v1alpha1" @@ -24,6 +26,7 @@ import ( ocv1alpha1 "github.com/operator-framework/operator-controller/api/v1alpha1" "github.com/operator-framework/operator-controller/internal/catalogmetadata" "github.com/operator-framework/operator-controller/internal/conditionsets" + "github.com/operator-framework/operator-controller/internal/controllers" "github.com/operator-framework/operator-controller/pkg/features" ) @@ -1564,6 +1567,450 @@ func TestClusterExtensionDowngrade(t *testing.T) { }) } +func TestSetDeprecationStatus(t *testing.T) { + for _, tc := range []struct { + name string + clusterExtension *ocv1alpha1.ClusterExtension + expectedClusterExtension *ocv1alpha1.ClusterExtension + bundle *catalogmetadata.Bundle + }{ + { + name: "non-deprecated bundle, no deprecations associated with bundle, all deprecation statuses set to False", + clusterExtension: &ocv1alpha1.ClusterExtension{ + ObjectMeta: metav1.ObjectMeta{ + Generation: 1, + }, + Status: ocv1alpha1.ClusterExtensionStatus{ + Conditions: []metav1.Condition{}, + }, + }, + expectedClusterExtension: &ocv1alpha1.ClusterExtension{ + ObjectMeta: metav1.ObjectMeta{ + Generation: 1, + }, + Status: ocv1alpha1.ClusterExtensionStatus{ + Conditions: []metav1.Condition{ + { + Type: ocv1alpha1.TypeDeprecated, + Reason: ocv1alpha1.ReasonDeprecated, + Status: metav1.ConditionFalse, + ObservedGeneration: 1, + }, + { + Type: ocv1alpha1.TypePackageDeprecated, + Reason: ocv1alpha1.ReasonDeprecated, + Status: metav1.ConditionFalse, + ObservedGeneration: 1, + }, + { + Type: ocv1alpha1.TypeChannelDeprecated, + Reason: ocv1alpha1.ReasonDeprecated, + Status: metav1.ConditionFalse, + ObservedGeneration: 1, + }, + { + Type: ocv1alpha1.TypeBundleDeprecated, + Reason: ocv1alpha1.ReasonDeprecated, + Status: metav1.ConditionFalse, + ObservedGeneration: 1, + }, + }, + }, + }, + bundle: &catalogmetadata.Bundle{}, + }, + { + name: "non-deprecated bundle, olm.channel deprecations associated with bundle, no channel specified, all deprecation statuses set to False", + clusterExtension: &ocv1alpha1.ClusterExtension{ + ObjectMeta: metav1.ObjectMeta{ + Generation: 1, + }, + Status: ocv1alpha1.ClusterExtensionStatus{ + Conditions: []metav1.Condition{}, + }, + }, + expectedClusterExtension: &ocv1alpha1.ClusterExtension{ + ObjectMeta: metav1.ObjectMeta{ + Generation: 1, + }, + Status: ocv1alpha1.ClusterExtensionStatus{ + Conditions: []metav1.Condition{ + { + Type: ocv1alpha1.TypeDeprecated, + Reason: ocv1alpha1.ReasonDeprecated, + Status: metav1.ConditionFalse, + ObservedGeneration: 1, + }, + { + Type: ocv1alpha1.TypePackageDeprecated, + Reason: ocv1alpha1.ReasonDeprecated, + Status: metav1.ConditionFalse, + ObservedGeneration: 1, + }, + { + Type: ocv1alpha1.TypeChannelDeprecated, + Reason: ocv1alpha1.ReasonDeprecated, + Status: metav1.ConditionFalse, + ObservedGeneration: 1, + }, + { + Type: ocv1alpha1.TypeBundleDeprecated, + Reason: ocv1alpha1.ReasonDeprecated, + Status: metav1.ConditionFalse, + ObservedGeneration: 1, + }, + }, + }, + }, + bundle: &catalogmetadata.Bundle{ + Deprecations: []declcfg.DeprecationEntry{ + { + Reference: declcfg.PackageScopedReference{ + Schema: declcfg.SchemaChannel, + Name: "badchannel", + }, + }, + }, + }, + }, + { + name: "non-deprecated bundle, olm.channel deprecations associated with bundle, non-deprecated channel specified, all deprecation statuses set to False", + clusterExtension: &ocv1alpha1.ClusterExtension{ + ObjectMeta: metav1.ObjectMeta{ + Generation: 1, + }, + Spec: ocv1alpha1.ClusterExtensionSpec{ + Channel: "nondeprecated", + }, + Status: ocv1alpha1.ClusterExtensionStatus{ + Conditions: []metav1.Condition{}, + }, + }, + expectedClusterExtension: &ocv1alpha1.ClusterExtension{ + ObjectMeta: metav1.ObjectMeta{ + Generation: 1, + }, + Spec: ocv1alpha1.ClusterExtensionSpec{ + Channel: "nondeprecated", + }, + Status: ocv1alpha1.ClusterExtensionStatus{ + Conditions: []metav1.Condition{ + { + Type: ocv1alpha1.TypeDeprecated, + Reason: ocv1alpha1.ReasonDeprecated, + Status: metav1.ConditionFalse, + ObservedGeneration: 1, + }, + { + Type: ocv1alpha1.TypePackageDeprecated, + Reason: ocv1alpha1.ReasonDeprecated, + Status: metav1.ConditionFalse, + ObservedGeneration: 1, + }, + { + Type: ocv1alpha1.TypeChannelDeprecated, + Reason: ocv1alpha1.ReasonDeprecated, + Status: metav1.ConditionFalse, + ObservedGeneration: 1, + }, + { + Type: ocv1alpha1.TypeBundleDeprecated, + Reason: ocv1alpha1.ReasonDeprecated, + Status: metav1.ConditionFalse, + ObservedGeneration: 1, + }, + }, + }, + }, + bundle: &catalogmetadata.Bundle{ + Deprecations: []declcfg.DeprecationEntry{ + { + Reference: declcfg.PackageScopedReference{ + Schema: declcfg.SchemaChannel, + Name: "badchannel", + }, + }, + }, + }, + }, + { + name: "non-deprecated bundle, olm.channel deprecations associated with bundle, deprecated channel specified, ChannelDeprecated and Deprecated status set to true", + clusterExtension: &ocv1alpha1.ClusterExtension{ + ObjectMeta: metav1.ObjectMeta{ + Generation: 1, + }, + Spec: ocv1alpha1.ClusterExtensionSpec{ + Channel: "badchannel", + }, + Status: ocv1alpha1.ClusterExtensionStatus{ + Conditions: []metav1.Condition{}, + }, + }, + expectedClusterExtension: &ocv1alpha1.ClusterExtension{ + ObjectMeta: metav1.ObjectMeta{ + Generation: 1, + }, + Spec: ocv1alpha1.ClusterExtensionSpec{ + Channel: "badchannel", + }, + Status: ocv1alpha1.ClusterExtensionStatus{ + Conditions: []metav1.Condition{ + { + Type: ocv1alpha1.TypeDeprecated, + Reason: ocv1alpha1.ReasonDeprecated, + Status: metav1.ConditionTrue, + ObservedGeneration: 1, + }, + { + Type: ocv1alpha1.TypePackageDeprecated, + Reason: ocv1alpha1.ReasonDeprecated, + Status: metav1.ConditionFalse, + ObservedGeneration: 1, + }, + { + Type: ocv1alpha1.TypeChannelDeprecated, + Reason: ocv1alpha1.ReasonDeprecated, + Status: metav1.ConditionTrue, + ObservedGeneration: 1, + }, + { + Type: ocv1alpha1.TypeBundleDeprecated, + Reason: ocv1alpha1.ReasonDeprecated, + Status: metav1.ConditionFalse, + ObservedGeneration: 1, + }, + }, + }, + }, + bundle: &catalogmetadata.Bundle{ + Deprecations: []declcfg.DeprecationEntry{ + { + Reference: declcfg.PackageScopedReference{ + Schema: declcfg.SchemaChannel, + Name: "badchannel", + }, + Message: "bad channel!", + }, + }, + }, + }, + { + name: "deprecated package + bundle, olm.channel deprecations associated with bundle, deprecated channel specified, all deprecation statuses set to true", + clusterExtension: &ocv1alpha1.ClusterExtension{ + ObjectMeta: metav1.ObjectMeta{ + Generation: 1, + }, + Spec: ocv1alpha1.ClusterExtensionSpec{ + Channel: "badchannel", + }, + Status: ocv1alpha1.ClusterExtensionStatus{ + Conditions: []metav1.Condition{}, + }, + }, + expectedClusterExtension: &ocv1alpha1.ClusterExtension{ + ObjectMeta: metav1.ObjectMeta{ + Generation: 1, + }, + Spec: ocv1alpha1.ClusterExtensionSpec{ + Channel: "badchannel", + }, + Status: ocv1alpha1.ClusterExtensionStatus{ + Conditions: []metav1.Condition{ + { + Type: ocv1alpha1.TypeDeprecated, + Reason: ocv1alpha1.ReasonDeprecated, + Status: metav1.ConditionTrue, + ObservedGeneration: 1, + }, + { + Type: ocv1alpha1.TypePackageDeprecated, + Reason: ocv1alpha1.ReasonDeprecated, + Status: metav1.ConditionTrue, + ObservedGeneration: 1, + }, + { + Type: ocv1alpha1.TypeChannelDeprecated, + Reason: ocv1alpha1.ReasonDeprecated, + Status: metav1.ConditionTrue, + ObservedGeneration: 1, + }, + { + Type: ocv1alpha1.TypeBundleDeprecated, + Reason: ocv1alpha1.ReasonDeprecated, + Status: metav1.ConditionTrue, + ObservedGeneration: 1, + }, + }, + }, + }, + bundle: &catalogmetadata.Bundle{ + Deprecations: []declcfg.DeprecationEntry{ + { + Reference: declcfg.PackageScopedReference{ + Schema: declcfg.SchemaChannel, + Name: "badchannel", + }, + Message: "bad channel!", + }, + { + Reference: declcfg.PackageScopedReference{ + Schema: declcfg.SchemaPackage, + }, + Message: "bad package!", + }, + { + Reference: declcfg.PackageScopedReference{ + Schema: declcfg.SchemaBundle, + Name: "badbundle", + }, + Message: "bad bundle!", + }, + }, + }, + }, + { + name: "deprecated bundle, olm.channel deprecations associated with bundle, deprecated channel specified, all deprecation statuses set to true except PackageDeprecated", + clusterExtension: &ocv1alpha1.ClusterExtension{ + ObjectMeta: metav1.ObjectMeta{ + Generation: 1, + }, + Spec: ocv1alpha1.ClusterExtensionSpec{ + Channel: "badchannel", + }, + Status: ocv1alpha1.ClusterExtensionStatus{ + Conditions: []metav1.Condition{}, + }, + }, + expectedClusterExtension: &ocv1alpha1.ClusterExtension{ + ObjectMeta: metav1.ObjectMeta{ + Generation: 1, + }, + Spec: ocv1alpha1.ClusterExtensionSpec{ + Channel: "badchannel", + }, + Status: ocv1alpha1.ClusterExtensionStatus{ + Conditions: []metav1.Condition{ + { + Type: ocv1alpha1.TypeDeprecated, + Reason: ocv1alpha1.ReasonDeprecated, + Status: metav1.ConditionTrue, + ObservedGeneration: 1, + }, + { + Type: ocv1alpha1.TypePackageDeprecated, + Reason: ocv1alpha1.ReasonDeprecated, + Status: metav1.ConditionFalse, + ObservedGeneration: 1, + }, + { + Type: ocv1alpha1.TypeChannelDeprecated, + Reason: ocv1alpha1.ReasonDeprecated, + Status: metav1.ConditionTrue, + ObservedGeneration: 1, + }, + { + Type: ocv1alpha1.TypeBundleDeprecated, + Reason: ocv1alpha1.ReasonDeprecated, + Status: metav1.ConditionTrue, + ObservedGeneration: 1, + }, + }, + }, + }, + bundle: &catalogmetadata.Bundle{ + Deprecations: []declcfg.DeprecationEntry{ + { + Reference: declcfg.PackageScopedReference{ + Schema: declcfg.SchemaChannel, + Name: "badchannel", + }, + Message: "bad channel!", + }, + { + Reference: declcfg.PackageScopedReference{ + Schema: declcfg.SchemaBundle, + Name: "badbundle", + }, + Message: "bad bundle!", + }, + }, + }, + }, + { + name: "deprecated package, olm.channel deprecations associated with bundle, deprecated channel specified, all deprecation statuses set to true except BundleDeprecated", + clusterExtension: &ocv1alpha1.ClusterExtension{ + ObjectMeta: metav1.ObjectMeta{ + Generation: 1, + }, + Spec: ocv1alpha1.ClusterExtensionSpec{ + Channel: "badchannel", + }, + Status: ocv1alpha1.ClusterExtensionStatus{ + Conditions: []metav1.Condition{}, + }, + }, + expectedClusterExtension: &ocv1alpha1.ClusterExtension{ + ObjectMeta: metav1.ObjectMeta{ + Generation: 1, + }, + Spec: ocv1alpha1.ClusterExtensionSpec{ + Channel: "badchannel", + }, + Status: ocv1alpha1.ClusterExtensionStatus{ + Conditions: []metav1.Condition{ + { + Type: ocv1alpha1.TypeDeprecated, + Reason: ocv1alpha1.ReasonDeprecated, + Status: metav1.ConditionTrue, + ObservedGeneration: 1, + }, + { + Type: ocv1alpha1.TypePackageDeprecated, + Reason: ocv1alpha1.ReasonDeprecated, + Status: metav1.ConditionTrue, + ObservedGeneration: 1, + }, + { + Type: ocv1alpha1.TypeChannelDeprecated, + Reason: ocv1alpha1.ReasonDeprecated, + Status: metav1.ConditionTrue, + ObservedGeneration: 1, + }, + { + Type: ocv1alpha1.TypeBundleDeprecated, + Reason: ocv1alpha1.ReasonDeprecated, + Status: metav1.ConditionFalse, + ObservedGeneration: 1, + }, + }, + }, + }, + bundle: &catalogmetadata.Bundle{ + Deprecations: []declcfg.DeprecationEntry{ + { + Reference: declcfg.PackageScopedReference{ + Schema: declcfg.SchemaChannel, + Name: "badchannel", + }, + Message: "bad channel!", + }, + { + Reference: declcfg.PackageScopedReference{ + Schema: declcfg.SchemaPackage, + }, + Message: "bad package!", + }, + }, + }, + }, + } { + t.Run(tc.name, func(t *testing.T) { + controllers.SetDeprecationStatus(tc.clusterExtension, tc.bundle) + assert.Equal(t, "", cmp.Diff(tc.expectedClusterExtension, tc.clusterExtension, cmpopts.IgnoreFields(metav1.Condition{}, "Message", "LastTransitionTime"))) + }) + } +} + var ( prometheusAlphaChannel = catalogmetadata.Channel{ Channel: declcfg.Channel{ diff --git a/internal/resolution/variablesources/required_package.go b/internal/resolution/variablesources/required_package.go index a06f50062..2f23a6062 100644 --- a/internal/resolution/variablesources/required_package.go +++ b/internal/resolution/variablesources/required_package.go @@ -56,6 +56,9 @@ func MakeRequiredPackageVariables(allBundles []*catalogmetadata.Bundle, clusterE sort.SliceStable(resultSet, func(i, j int) bool { return catalogsort.ByVersion(resultSet[i], resultSet[j]) }) + sort.SliceStable(resultSet, func(i, j int) bool { + return catalogsort.ByDeprecated(resultSet[i], resultSet[j]) + }) result = append(result, olmvariables.NewRequiredPackageVariable(packageName, resultSet)) } diff --git a/internal/resolution/variablesources/required_package_test.go b/internal/resolution/variablesources/required_package_test.go index b510b5be3..164f9a411 100644 --- a/internal/resolution/variablesources/required_package_test.go +++ b/internal/resolution/variablesources/required_package_test.go @@ -63,6 +63,63 @@ func TestMakeRequiredPackageVariables(t *testing.T) { }, InChannels: []*catalogmetadata.Channel{&stableChannel}, }, + "test-package.v4.0.0": { + Bundle: declcfg.Bundle{ + Name: "test-package.v4.0.0", + Package: "test-package", + Properties: []property.Property{ + {Type: property.TypePackage, Value: json.RawMessage(`{"packageName": "test-package", "version": "4.0.0"}`)}, + }, + }, + InChannels: []*catalogmetadata.Channel{&stableChannel}, + Deprecations: []declcfg.DeprecationEntry{ + { + Reference: declcfg.PackageScopedReference{ + Schema: declcfg.SchemaBundle, + Name: "test-package.v4.0.0", + }, + Message: "test-package.v4.0.0 has been deprecated", + }, + }, + }, + "test-package.v4.1.0": { + Bundle: declcfg.Bundle{ + Name: "test-package.v4.1.0", + Package: "test-package", + Properties: []property.Property{ + {Type: property.TypePackage, Value: json.RawMessage(`{"packageName": "test-package", "version": "4.1.0"}`)}, + }, + }, + InChannels: []*catalogmetadata.Channel{&stableChannel}, + Deprecations: []declcfg.DeprecationEntry{ + { + Reference: declcfg.PackageScopedReference{ + Schema: declcfg.SchemaBundle, + Name: "test-package.v4.1.0", + }, + Message: "test-package.v4.1.0 has been deprecated", + }, + }, + }, + "test-package.v5.0.0": { + Bundle: declcfg.Bundle{ + Name: "test-package.v5.0.0", + Package: "test-package", + Properties: []property.Property{ + {Type: property.TypePackage, Value: json.RawMessage(`{"packageName": "test-package", "version": "5.0.0"}`)}, + }, + }, + InChannels: []*catalogmetadata.Channel{&stableChannel}, + Deprecations: []declcfg.DeprecationEntry{ + { + Reference: declcfg.PackageScopedReference{ + Schema: declcfg.SchemaBundle, + Name: "test-package.v5.0.0", + }, + Message: "test-package.v5.0.0 has been deprecated", + }, + }, + }, // We need at least one bundle from different package // to make sure that we are filtering it out. @@ -111,6 +168,9 @@ func TestMakeRequiredPackageVariables(t *testing.T) { bundleSet["test-package.v3.0.0"], bundleSet["test-package.v2.0.0"], bundleSet["test-package.v1.0.0"], + bundleSet["test-package.v5.0.0"], + bundleSet["test-package.v4.1.0"], + bundleSet["test-package.v4.0.0"], }), }, }, diff --git a/test/e2e/install_test.go b/test/e2e/install_test.go index fae5385bb..326ef926f 100644 --- a/test/e2e/install_test.go +++ b/test/e2e/install_test.go @@ -81,7 +81,7 @@ func TestClusterExtensionInstallRegistry(t *testing.T) { t.Log("By eventually reporting a successful resolution and bundle path") require.EventuallyWithT(t, func(ct *assert.CollectT) { assert.NoError(ct, c.Get(context.Background(), types.NamespacedName{Name: clusterExtension.Name}, clusterExtension)) - assert.Len(ct, clusterExtension.Status.Conditions, 2) + assert.Len(ct, clusterExtension.Status.Conditions, 6) cond := apimeta.FindStatusCondition(clusterExtension.Status.Conditions, ocv1alpha1.TypeResolved) if !assert.NotNil(ct, cond) { return @@ -137,7 +137,7 @@ func TestClusterExtensionInstallPlain(t *testing.T) { t.Log("By eventually reporting a successful resolution and bundle path") require.EventuallyWithT(t, func(ct *assert.CollectT) { assert.NoError(ct, c.Get(context.Background(), types.NamespacedName{Name: clusterExtension.Name}, clusterExtension)) - assert.Len(ct, clusterExtension.Status.Conditions, 2) + assert.Len(ct, clusterExtension.Status.Conditions, 6) cond := apimeta.FindStatusCondition(clusterExtension.Status.Conditions, ocv1alpha1.TypeResolved) if !assert.NotNil(ct, cond) { return