Repository: operator-framework/operator-lifecycle-manager (go)

Issue:
Add 'deprecated' condition to Subscription status
As an OLM user, I would like to know when a Subscription has a deprecated Operator installed.

Acceptance Criteria:

- When the catalog entry used by a Subscription is marked as "deprecated", the "Deprecated" condition will be added to the Subscription status.
- Tests are written




Pull request description:
Update Subscription Status with Deprecation Warnings
**Description of the change:**

Adds the `OperatorDeprecated` Condition to the status of Subscriptions when the bundle has been deprecated.

~~Doing this in the step resolver is convenient because:~~
- ~~We have ready access to the cache entries.~~
- ~~Subscription status is already updated in this area.~~
- ~~Deprecated status should be added promptly if a catalog update occurs which adds the property to a bundle.~~

Modifying the subscription status within the resolver was both problematic and risky. We've changed strategies and are now updating status during catalog state update. This carries much lower risk.

**Remaining Tasks:**
- [x] Populate cache entries with deprecation info.
- [x] Add additional deprecation condition types (package, channel, bundle).
- [x] ~~Update logic in catalog operator's `syncResolvingNamespace` to account for subs updated with only `OperatorDeprecated` and do not necessarily require InstallPlan updates in the namespace (currently it assumes all updates are new bundles).~~
- [x] Update test and/or add more tests with additional condition types (as above).
- [x] Update go.mod to use merged/released API updates to `api` and `registry`.
- [x] Squash commits where appropriate.

**Motivation for the change:**

Closes #3099 
[Brief](https://docs.google.com/document/d/1ipnf5uMiGZh5GXt2ey3oxziFx9lZv0qiFmB8g54EFwQ/edit?usp=sharing)
[RFC](https://docs.google.com/document/d/1SLgERsnNwNN7PuABdzysmxYxzD-hiyh7yuJc1m2Hy98/edit?usp=sharing)

**Architectural changes:**




Patch:
diff --git a/go.mod b/go.mod
index 68cd7f056..4a7271ab6 100644
--- a/go.mod
+++ b/go.mod
@@ -24,7 +24,7 @@ require (
 	github.com/onsi/gomega v1.27.10
 	github.com/openshift/api v3.9.0+incompatible
 	github.com/openshift/client-go v0.0.0-20220525160904-9e1acff93e4a
-	github.com/operator-framework/api v0.19.0
+	github.com/operator-framework/api v0.20.0
 	github.com/operator-framework/operator-registry v1.33.0
 	github.com/otiai10/copy v1.12.0
 	github.com/pkg/errors v0.9.1
diff --git a/go.sum b/go.sum
index 09f2a74a1..d5233c025 100644
--- a/go.sum
+++ b/go.sum
@@ -621,8 +621,8 @@ github.com/openshift/api v0.0.0-20221021112143-4226c2167e40 h1:PxjGCA72RtsdHWToZ
 github.com/openshift/api v0.0.0-20221021112143-4226c2167e40/go.mod h1:aQ6LDasvHMvHZXqLHnX2GRmnfTWCF/iIwz8EMTTIE9A=
 github.com/openshift/client-go v0.0.0-20221019143426-16aed247da5c h1:CV76yFOTXmq9VciBR3Bve5ZWzSxdft7gaMVB3kS0rwg=
 github.com/openshift/client-go v0.0.0-20221019143426-16aed247da5c/go.mod h1:lFMO8mLHXWFzSdYvGNo8ivF9SfF6zInA8ZGw4phRnUE=
-github.com/operator-framework/api v0.19.0 h1:QU1CTJU+CufoeneA5rsNlP/uP96s8vDHWUYDFZTauzA=
-github.com/operator-framework/api v0.19.0/go.mod h1:SCCslqke6AVOJ5JM+NqNE1CHuAgJLScsL66pnPaSMXs=
+github.com/operator-framework/api v0.20.0 h1:A2YCRhr+6s0k3pRJacnwjh1Ue8BqjIGuQ2jvPg9XCB4=
+github.com/operator-framework/api v0.20.0/go.mod h1:rXPOhrQ6mMeXqCmpDgt1ALoar9ZlHL+Iy5qut9R99a4=
 github.com/operator-framework/operator-registry v1.33.0 h1:rFvYf6vLdXSUhoePhg8w1S5FHgyFraiNLXDwl47epck=
 github.com/operator-framework/operator-registry v1.33.0/go.mod h1:1V/m2m7iH/o5ROEuMxWa/yhj4ExaPGT6V/ncGS+m6Js=
 github.com/otiai10/copy v1.12.0 h1:cLMgSQnXBs1eehF0Wy/FAGsgDTDmAqFR7rQylBb1nDY=
diff --git a/pkg/controller/operators/catalog/operator.go b/pkg/controller/operators/catalog/operator.go
index 31d78b682..7394fb124 100644
--- a/pkg/controller/operators/catalog/operator.go
+++ b/pkg/controller/operators/catalog/operator.go
@@ -346,6 +346,7 @@ func NewOperator(ctx context.Context, kubeconfigPath string, clock utilclock.Clo
 		subscription.WithAppendedReconcilers(subscription.ReconcilerFromLegacySyncHandler(op.syncSubscriptions, nil)),
 		subscription.WithRegistryReconcilerFactory(op.reconciler),
 		subscription.WithGlobalCatalogNamespace(op.namespace),
+		subscription.WithSourceProvider(resolverSourceProvider),
 	)
 	if err != nil {
 		return nil, err
diff --git a/pkg/controller/operators/catalog/subscription/config.go b/pkg/controller/operators/catalog/subscription/config.go
index 045de3804..9f739b0e5 100644
--- a/pkg/controller/operators/catalog/subscription/config.go
+++ b/pkg/controller/operators/catalog/subscription/config.go
@@ -10,6 +10,7 @@ import (
 
 	"github.com/operator-framework/operator-lifecycle-manager/pkg/api/client/clientset/versioned"
 	"github.com/operator-framework/operator-lifecycle-manager/pkg/controller/registry/reconciler"
+	resolverCache "github.com/operator-framework/operator-lifecycle-manager/pkg/controller/registry/resolver/cache"
 	"github.com/operator-framework/operator-lifecycle-manager/pkg/lib/kubestate"
 	"github.com/operator-framework/operator-lifecycle-manager/pkg/lib/operatorlister"
 )
@@ -26,6 +27,7 @@ type syncerConfig struct {
 	reconcilers               kubestate.ReconcilerChain
 	registryReconcilerFactory reconciler.RegistryReconcilerFactory
 	globalCatalogNamespace    string
+	sourceProvider            resolverCache.SourceProvider
 }
 
 // SyncerOption is a configuration option for a subscription syncer.
@@ -128,6 +130,12 @@ func WithGlobalCatalogNamespace(namespace string) SyncerOption {
 	}
 }
 
+func WithSourceProvider(provider resolverCache.SourceProvider) SyncerOption {
+	return func(config *syncerConfig) {
+		config.sourceProvider = provider
+	}
+}
+
 func newInvalidConfigError(msg string) error {
 	return errors.Errorf("invalid subscription syncer config: %s", msg)
 }
diff --git a/pkg/controller/operators/catalog/subscription/reconciler.go b/pkg/controller/operators/catalog/subscription/reconciler.go
index 35f8a8298..982e6164d 100644
--- a/pkg/controller/operators/catalog/subscription/reconciler.go
+++ b/pkg/controller/operators/catalog/subscription/reconciler.go
@@ -5,8 +5,10 @@ import (
 	"errors"
 	"fmt"
 	"sort"
+	"strings"
 
 	"github.com/sirupsen/logrus"
+	corev1 "k8s.io/api/core/v1"
 	apierrors "k8s.io/apimachinery/pkg/api/errors"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/labels"
@@ -18,8 +20,10 @@ import (
 	"github.com/operator-framework/operator-lifecycle-manager/pkg/api/client/clientset/versioned"
 	listers "github.com/operator-framework/operator-lifecycle-manager/pkg/api/client/listers/operators/v1alpha1"
 	"github.com/operator-framework/operator-lifecycle-manager/pkg/controller/registry/reconciler"
+	"github.com/operator-framework/operator-lifecycle-manager/pkg/controller/registry/resolver/cache"
 	"github.com/operator-framework/operator-lifecycle-manager/pkg/lib/kubestate"
 	"github.com/operator-framework/operator-lifecycle-manager/pkg/lib/queueinformer"
+	"github.com/operator-framework/operator-registry/pkg/api"
 )
 
 // ReconcilerFromLegacySyncHandler returns a reconciler that invokes the given legacy sync handler and on delete funcs.
@@ -57,6 +61,7 @@ type catalogHealthReconciler struct {
 	catalogLister             listers.CatalogSourceLister
 	registryReconcilerFactory reconciler.RegistryReconcilerFactory
 	globalCatalogNamespace    string
+	sourceProvider            cache.SourceProvider
 }
 
 // Reconcile reconciles subscription catalog health conditions.
@@ -84,7 +89,15 @@ func (c *catalogHealthReconciler) Reconcile(ctx context.Context, in kubestate.St
 					break
 				}
 
-				next, err = s.UpdateHealth(c.now(), c.client.OperatorsV1alpha1().Subscriptions(ns), catalogHealth...)
+				var healthUpdated, deprecationUpdated bool
+				next, healthUpdated = s.UpdateHealth(c.now(), catalogHealth...)
+				deprecationUpdated, err = c.updateDeprecatedStatus(ctx, s.Subscription())
+				if err != nil {
+					return next, err
+				}
+				if healthUpdated || deprecationUpdated {
+					_, err = c.client.OperatorsV1alpha1().Subscriptions(ns).UpdateStatus(ctx, s.Subscription(), metav1.UpdateOptions{})
+				}
 			case SubscriptionExistsState:
 				if s == nil {
 					err = errors.New("nil state")
@@ -109,6 +122,119 @@ func (c *catalogHealthReconciler) Reconcile(ctx context.Context, in kubestate.St
 	return
 }
 
+// updateDeprecatedStatus adds deprecation status conditions to the subscription when present in the cache entry then
+// returns a bool value of true if any changes to the existing subscription have occurred.
+func (c *catalogHealthReconciler) updateDeprecatedStatus(ctx context.Context, sub *v1alpha1.Subscription) (bool, error) {
+	if c.sourceProvider == nil {
+		return false, nil
+	}
+
+	source, ok := c.sourceProvider.Sources(sub.Namespace)[cache.SourceKey{
+		Name:      sub.Spec.CatalogSource,
+		Namespace: sub.Namespace,
+	}]
+	if !ok {
+		return false, nil
+	}
+	snapshot, err := source.Snapshot(ctx)
+	if err != nil {
+		return false, err
+	}
+	if len(snapshot.Entries) == 0 {
+		return false, nil
+	}
+
+	changed := false
+	rollupMessages := []string{}
+	var deprecations *cache.Deprecations
+
+	found := false
+	for _, entry := range snapshot.Entries {
+		// Find the cache entry that matches this subscription
+		if entry.SourceInfo == nil || entry.Package() != sub.Spec.Package {
+			continue
+		}
+		if sub.Spec.Channel != "" && entry.Channel() != sub.Spec.Channel {
+			continue
+		}
+		if sub.Status.InstalledCSV != entry.Name {
+			continue
+		}
+		deprecations = entry.SourceInfo.Deprecations
+		found = true
+		break
+	}
+	if !found {
+		// No matching entry found
+		return false, nil
+	}
+	conditionTypes := []v1alpha1.SubscriptionConditionType{
+		v1alpha1.SubscriptionPackageDeprecated,
+		v1alpha1.SubscriptionChannelDeprecated,
+		v1alpha1.SubscriptionBundleDeprecated,
+	}
+	for _, conditionType := range conditionTypes {
+		oldCondition := sub.Status.GetCondition(conditionType)
+		var deprecation *api.Deprecation
+		if deprecations != nil {
+			switch conditionType {
+			case v1alpha1.SubscriptionPackageDeprecated:
+				deprecation = deprecations.Package
+			case v1alpha1.SubscriptionChannelDeprecated:
+				deprecation = deprecations.Channel
+			case v1alpha1.SubscriptionBundleDeprecated:
+				deprecation = deprecations.Bundle
+			}
+		}
+		if deprecation != nil {
+			if conditionType == v1alpha1.SubscriptionChannelDeprecated && sub.Spec.Channel == "" {
+				// Special case: If optional field sub.Spec.Channel is unset do not apply a channel
+				// deprecation message and remove them if any exist.
+				sub.Status.RemoveConditions(conditionType)
+				if oldCondition.Status == corev1.ConditionTrue {
+					changed = true
+				}
+				continue
+			}
+			newCondition := v1alpha1.SubscriptionCondition{
+				Type:               conditionType,
+				Message:            deprecation.Message,
+				Status:             corev1.ConditionTrue,
+				LastTransitionTime: c.now(),
+			}
+			rollupMessages = append(rollupMessages, deprecation.Message)
+			if oldCondition.Message != newCondition.Message {
+				// oldCondition's message was empty or has changed; add or update the condition
+				sub.Status.SetCondition(newCondition)
+				changed = true
+			}
+		} else if oldCondition.Status == corev1.ConditionTrue {
+			// No longer deprecated at this level; remove the condition
+			sub.Status.RemoveConditions(conditionType)
+			changed = true
+		}
+	}
+
+	if !changed {
+		// No need to update rollup condition if no other conditions have changed
+		return false, nil
+	}
+	if len(rollupMessages) > 0 {
+		rollupCondition := v1alpha1.SubscriptionCondition{
+			Type:               v1alpha1.SubscriptionDeprecated,
+			Message:            strings.Join(rollupMessages, "; "),
+			Status:             corev1.ConditionTrue,
+			LastTransitionTime: c.now(),
+		}
+		sub.Status.SetCondition(rollupCondition)
+	} else {
+		// No rollup message means no deprecation conditions were set; remove the rollup if it exists
+		sub.Status.RemoveConditions(v1alpha1.SubscriptionDeprecated)
+	}
+
+	return true, nil
+}
+
 // catalogHealth gets the health of catalogs that can affect Susbcriptions in the given namespace.
 // This means all catalogs in the given namespace, as well as any catalogs in the operator's global catalog namespace.
 func (c *catalogHealthReconciler) catalogHealth(namespace string) ([]v1alpha1.SubscriptionCatalogHealth, error) {
diff --git a/pkg/controller/operators/catalog/subscription/state.go b/pkg/controller/operators/catalog/subscription/state.go
index 92cba65c5..50e26b67b 100644
--- a/pkg/controller/operators/catalog/subscription/state.go
+++ b/pkg/controller/operators/catalog/subscription/state.go
@@ -63,8 +63,9 @@ type CatalogHealthState interface {
 	isCatalogHealthState()
 
 	// UpdateHealth transitions the CatalogHealthState to another CatalogHealthState based on the given subscription catalog health.
-	// The state's underlying subscription may be updated on the cluster. If the subscription is updated, the resulting state will contain the updated version.
-	UpdateHealth(now *metav1.Time, client clientv1alpha1.SubscriptionInterface, health ...v1alpha1.SubscriptionCatalogHealth) (CatalogHealthState, error)
+	// The state's underlying subscription may be updated. If the subscription is updated, a boolean value of 'true' will be
+	// returned and the resulting state will contain the updated version.
+	UpdateHealth(now *metav1.Time, health ...v1alpha1.SubscriptionCatalogHealth) (CatalogHealthState, bool)
 }
 
 // CatalogHealthKnownState describes subscription states in which all relevant catalog health is known.
@@ -218,7 +219,7 @@ type catalogHealthState struct {
 
 func (c *catalogHealthState) isCatalogHealthState() {}
 
-func (c *catalogHealthState) UpdateHealth(now *metav1.Time, client clientv1alpha1.SubscriptionInterface, catalogHealth ...v1alpha1.SubscriptionCatalogHealth) (CatalogHealthState, error) {
+func (c *catalogHealthState) UpdateHealth(now *metav1.Time, catalogHealth ...v1alpha1.SubscriptionCatalogHealth) (CatalogHealthState, bool) {
 	in := c.Subscription()
 	out := in.DeepCopy()
 
@@ -291,7 +292,7 @@ func (c *catalogHealthState) UpdateHealth(now *metav1.Time, client clientv1alpha
 
 	if !update && cond.Equals(in.Status.GetCondition(v1alpha1.SubscriptionCatalogSourcesUnhealthy)) {
 		// Nothing to do, transition to self
-		return known, nil
+		return known, false
 	}
 
 	cond.LastTransitionTime = now
@@ -299,16 +300,10 @@ func (c *catalogHealthState) UpdateHealth(now *metav1.Time, client clientv1alpha
 	out.Status.SetCondition(cond)
 	out.Status.CatalogHealth = catalogHealth
 
-	updated, err := client.UpdateStatus(context.TODO(), out, metav1.UpdateOptions{})
-	if err != nil {
-		// Error occurred, transition to self
-		return c, err
-	}
-
 	// Inject updated subscription into the state
-	known.setSubscription(updated)
+	known.setSubscription(out)
 
-	return known, nil
+	return known, true
 }
 
 func NewCatalogHealthState(s SubscriptionExistsState) CatalogHealthState {
diff --git a/pkg/controller/operators/catalog/subscription/syncer.go b/pkg/controller/operators/catalog/subscription/syncer.go
index d58ad8878..04e8edeb5 100644
--- a/pkg/controller/operators/catalog/subscription/syncer.go
+++ b/pkg/controller/operators/catalog/subscription/syncer.go
@@ -13,6 +13,7 @@ import (
 	"github.com/operator-framework/api/pkg/operators/install"
 	"github.com/operator-framework/api/pkg/operators/v1alpha1"
 	listers "github.com/operator-framework/operator-lifecycle-manager/pkg/api/client/listers/operators/v1alpha1"
+	resolverCache "github.com/operator-framework/operator-lifecycle-manager/pkg/controller/registry/resolver/cache"
 	"github.com/operator-framework/operator-lifecycle-manager/pkg/lib/kubestate"
 	"github.com/operator-framework/operator-lifecycle-manager/pkg/lib/ownerutil"
 	"github.com/operator-framework/operator-lifecycle-manager/pkg/metrics"
@@ -34,6 +35,7 @@ type subscriptionSyncer struct {
 	installPlanLister      listers.InstallPlanLister
 	globalCatalogNamespace string
 	notify                 kubestate.NotifyFunc
+	sourceProvider         resolverCache.SourceProvider
 }
 
 // now returns the Syncer's current time.
@@ -214,6 +216,7 @@ func newSyncerWithConfig(ctx context.Context, config *syncerConfig) (kubestate.S
 		reconcilers:       config.reconcilers,
 		subscriptionCache: config.subscriptionInformer.GetIndexer(),
 		installPlanLister: config.lister.OperatorsV1alpha1().InstallPlanLister(),
+		sourceProvider:    config.sourceProvider,
 		notify: func(event kubestate.ResourceEvent) {
 			// Notify Subscriptions by enqueuing to the Subscription queue.
 			config.subscriptionQueue.Add(event)
@@ -234,6 +237,7 @@ func newSyncerWithConfig(ctx context.Context, config *syncerConfig) (kubestate.S
 			catalogLister:             config.lister.OperatorsV1alpha1().CatalogSourceLister(),
 			registryReconcilerFactory: config.registryReconcilerFactory,
 			globalCatalogNamespace:    config.globalCatalogNamespace,
+			sourceProvider:            config.sourceProvider,
 		},
 	}
 	s.reconcilers = append(defaultReconcilers, s.reconcilers...)
diff --git a/pkg/controller/registry/resolver/cache/operators.go b/pkg/controller/registry/resolver/cache/operators.go
index 85d9f3d91..f636cc8c9 100644
--- a/pkg/controller/registry/resolver/cache/operators.go
+++ b/pkg/controller/registry/resolver/cache/operators.go
@@ -140,6 +140,7 @@ type OperatorSourceInfo struct {
 	StartingCSV    string
 	Catalog        SourceKey
 	DefaultChannel bool
+	Deprecations   *Deprecations
 	Subscription   *v1alpha1.Subscription
 }
 
@@ -147,6 +148,12 @@ func (i *OperatorSourceInfo) String() string {
 	return fmt.Sprintf("%s/%s in %s/%s", i.Package, i.Channel, i.Catalog.Name, i.Catalog.Namespace)
 }
 
+type Deprecations struct {
+	Package *api.Deprecation
+	Channel *api.Deprecation
+	Bundle  *api.Deprecation
+}
+
 type Entry struct {
 	Name         string
 	Replaces     string
diff --git a/pkg/controller/registry/resolver/source_registry.go b/pkg/controller/registry/resolver/source_registry.go
index ab3550a93..bbfef6c54 100644
--- a/pkg/controller/registry/resolver/source_registry.go
+++ b/pkg/controller/registry/resolver/source_registry.go
@@ -109,7 +109,7 @@ func (s *registrySource) Snapshot(ctx context.Context) (*cache.Snapshot, error)
 	// Fetching default channels this way makes many round trips
 	// -- may need to either add a new API to fetch all at once,
 	// or embed the information into Bundle.
-	defaultChannels := make(map[string]string)
+	packages := make(map[string]*api.Package)
 
 	it, err := s.client.ListBundles(ctx)
 	if err != nil {
@@ -118,21 +118,39 @@ func (s *registrySource) Snapshot(ctx context.Context) (*cache.Snapshot, error)
 
 	var operators []*cache.Entry
 	for b := it.Next(); b != nil; b = it.Next() {
-		defaultChannel, ok := defaultChannels[b.PackageName]
+		p, ok := packages[b.PackageName]
 		if !ok {
-			if p, err := s.client.GetPackage(ctx, b.PackageName); err != nil {
+			if p, err = s.client.GetPackage(ctx, b.PackageName); err != nil {
 				s.logger.Printf("failed to retrieve default channel for bundle, continuing: %v", err)
 				continue
 			} else {
-				defaultChannels[b.PackageName] = p.DefaultChannelName
-				defaultChannel = p.DefaultChannelName
+				packages[b.PackageName] = p
 			}
 		}
-		o, err := newOperatorFromBundle(b, "", s.key, defaultChannel)
+		o, err := newOperatorFromBundle(b, "", s.key, p.DefaultChannelName)
 		if err != nil {
 			s.logger.Printf("failed to construct operator from bundle, continuing: %v", err)
 			continue
 		}
+		var deprecations *cache.Deprecations
+		if p.Deprecation != nil {
+			deprecations = &cache.Deprecations{Package: &api.Deprecation{Message: fmt.Sprintf("olm.package/%s: %s", p.Name, p.Deprecation.GetMessage())}}
+		}
+		for _, c := range p.Channels {
+			if c.Name == b.ChannelName && c.Deprecation != nil {
+				if deprecations == nil {
+					deprecations = &cache.Deprecations{}
+				}
+				deprecations.Channel = &api.Deprecation{Message: fmt.Sprintf("olm.channel/%s: %s", c.Name, c.Deprecation.GetMessage())}
+			}
+		}
+		if b.Deprecation != nil {
+			if deprecations == nil {
+				deprecations = &cache.Deprecations{}
+			}
+			deprecations.Bundle = &api.Deprecation{Message: fmt.Sprintf("olm.bundle/%s: %s", b.CsvName, b.Deprecation.GetMessage())}
+		}
+		o.SourceInfo.Deprecations = deprecations
 		o.ProvidedAPIs = o.ProvidedAPIs.StripPlural()
 		o.RequiredAPIs = o.RequiredAPIs.StripPlural()
 		o.Replaces = b.Replaces
diff --git a/vendor/github.com/operator-framework/api/pkg/operators/v1alpha1/subscription_types.go b/vendor/github.com/operator-framework/api/pkg/operators/v1alpha1/subscription_types.go
index 2452f9a1c..672489264 100644
--- a/vendor/github.com/operator-framework/api/pkg/operators/v1alpha1/subscription_types.go
+++ b/vendor/github.com/operator-framework/api/pkg/operators/v1alpha1/subscription_types.go
@@ -117,6 +117,19 @@ const (
 
 	// SubscriptionBundleUnpackFailed indicates that the unpack job failed
 	SubscriptionBundleUnpackFailed SubscriptionConditionType = "BundleUnpackFailed"
+
+	// SubscriptionDeprecated is a roll-up condition which indicates that the Operator currently installed with this Subscription
+	//has been deprecated. It will be present when any of the three deprecation types (Package, Channel, Bundle) are present.
+	SubscriptionDeprecated SubscriptionConditionType = "Deprecated"
+
+	// SubscriptionOperatorDeprecated indicates that the Package currently installed with this Subscription has been deprecated.
+	SubscriptionPackageDeprecated SubscriptionConditionType = "PackageDeprecated"
+
+	// SubscriptionOperatorDeprecated indicates that the Channel used with this Subscription has been deprecated.
+	SubscriptionChannelDeprecated SubscriptionConditionType = "ChannelDeprecated"
+
+	// SubscriptionOperatorDeprecated indicates that the Bundle currently installed with this Subscription has been deprecated.
+	SubscriptionBundleDeprecated SubscriptionConditionType = "BundleDeprecated"
 )
 
 const (
diff --git a/vendor/modules.txt b/vendor/modules.txt
index 0ad9fa5d9..a149bfce4 100644
--- a/vendor/modules.txt
+++ b/vendor/modules.txt
@@ -719,7 +719,7 @@ github.com/openshift/client-go/config/informers/externalversions/config/v1alpha1
 github.com/openshift/client-go/config/informers/externalversions/internalinterfaces
 github.com/openshift/client-go/config/listers/config/v1
 github.com/openshift/client-go/config/listers/config/v1alpha1
-# github.com/operator-framework/api v0.19.0
+# github.com/operator-framework/api v0.20.0
 ## explicit; go 1.19
 github.com/operator-framework/api/crds
 github.com/operator-framework/api/pkg/constraints


Test patch:
diff --git a/pkg/controller/operators/catalog/subscription/state_test.go b/pkg/controller/operators/catalog/subscription/state_test.go
index 9612c9aff..7242b968c 100644
--- a/pkg/controller/operators/catalog/subscription/state_test.go
+++ b/pkg/controller/operators/catalog/subscription/state_test.go
@@ -31,7 +31,7 @@ func TestUpdateHealth(t *testing.T) {
 	type want struct {
 		transitioned CatalogHealthState
 		terminal     bool
-		err          error
+		updated      bool
 	}
 
 	tests := []struct {
@@ -73,6 +73,7 @@ func TestUpdateHealth(t *testing.T) {
 				now: &now,
 			},
 			want: want{
+				updated: true,
 				transitioned: newCatalogUnhealthyState(&v1alpha1.Subscription{
 					ObjectMeta: metav1.ObjectMeta{
 						Name:      "sub",
@@ -132,6 +133,7 @@ func TestUpdateHealth(t *testing.T) {
 				},
 			},
 			want: want{
+				updated: true,
 				transitioned: newCatalogUnhealthyState(&v1alpha1.Subscription{
 					ObjectMeta: metav1.ObjectMeta{
 						Name:      "sub",
@@ -210,6 +212,7 @@ func TestUpdateHealth(t *testing.T) {
 				},
 			},
 			want: want{
+				updated: false,
 				transitioned: newCatalogUnhealthyState(&v1alpha1.Subscription{
 					ObjectMeta: metav1.ObjectMeta{
 						Name:      "sub",
@@ -289,6 +292,7 @@ func TestUpdateHealth(t *testing.T) {
 				},
 			},
 			want: want{
+				updated: true,
 				transitioned: newCatalogHealthyState(&v1alpha1.Subscription{
 					ObjectMeta: metav1.ObjectMeta{
 						Name:      "sub",
@@ -368,6 +372,7 @@ func TestUpdateHealth(t *testing.T) {
 				},
 			},
 			want: want{
+				updated: true,
 				transitioned: newCatalogUnhealthyState(&v1alpha1.Subscription{
 					ObjectMeta: metav1.ObjectMeta{
 						Name:      "sub",
@@ -447,6 +452,7 @@ func TestUpdateHealth(t *testing.T) {
 				},
 			},
 			want: want{
+				updated: false,
 				transitioned: newCatalogHealthyState(&v1alpha1.Subscription{
 					ObjectMeta: metav1.ObjectMeta{
 						Name:      "sub",
@@ -473,19 +479,11 @@ func TestUpdateHealth(t *testing.T) {
 
 	for _, tt := range tests {
 		t.Run(tt.description, func(t *testing.T) {
-			fakeClient := tt.fields.existingObjs.fakeClientset(t).OperatorsV1alpha1().Subscriptions(tt.fields.namespace)
-			transitioned, err := tt.fields.state.UpdateHealth(tt.args.now, fakeClient, tt.args.catalogHealth...)
-			require.Equal(t, tt.want.err, err)
+			transitioned, updated := tt.fields.state.UpdateHealth(tt.args.now, tt.args.catalogHealth...)
 			require.Equal(t, tt.want.transitioned, transitioned)
-
+			require.Equal(t, tt.want.updated, updated)
 			if tt.want.transitioned != nil {
 				require.Equal(t, tt.want.terminal, transitioned.Terminal())
-
-				// Ensure the client's view of the subscription matches the typestate's
-				sub := transitioned.(SubscriptionState).Subscription()
-				clusterSub, err := fakeClient.Get(context.TODO(), sub.GetName(), metav1.GetOptions{})
-				require.NoError(t, err)
-				require.Equal(t, sub, clusterSub)
 			}
 		})
 	}
diff --git a/test/e2e/subscription_e2e_test.go b/test/e2e/subscription_e2e_test.go
index e22c8ea0b..925934522 100644
--- a/test/e2e/subscription_e2e_test.go
+++ b/test/e2e/subscription_e2e_test.go
@@ -2534,6 +2534,151 @@ var _ = Describe("Subscription", func() {
 				_, err = fetchSubscription(crc, generatedNamespace.GetName(), subName, subscriptionHasCurrentCSV("example-operator.v0.2.0"))
 				Expect(err).Should(BeNil())
 			})
+
+			It("should report deprecation conditions when package, channel, and bundle are referenced in an olm.deprecations object", func() {
+				By("patching the OperatorGroup to reduce the bundle unpacking timeout")
+				ogNN := types.NamespacedName{Name: operatorGroup.GetName(), Namespace: generatedNamespace.GetName()}
+				addBundleUnpackTimeoutOGAnnotation(context.Background(), ctx.Ctx().Client(), ogNN, "5m")
+
+				By("updating the catalog with a fixed v0.2.0 bundle image marked deprecated")
+				provider, err := NewFileBasedFiledBasedCatalogProvider(filepath.Join(testdataDir, subscriptionTestDataBaseDir, "example-operator.v0.2.0-deprecations.yaml"))
+				Expect(err).To(BeNil())
+				err = magicCatalog.UpdateCatalog(context.Background(), provider)
+				Expect(err).To(BeNil())
+
+				By("waiting for the subscription to have v0.2.0 installed with a Bundle Deprecated condition")
+				sub, err := fetchSubscription(crc, generatedNamespace.GetName(), subName, subscriptionHasCondition(
+					v1alpha1.SubscriptionBundleDeprecated,
+					corev1.ConditionTrue,
+					"",
+					"olm.bundle/example-operator.v0.2.0: bundle \"example-operator.v0.2.0\" has been deprecated. Please switch to a different one."))
+				Expect(err).Should(BeNil())
+
+				By("checking for the deprecated conditions")
+				// Operator is deprecated at all three levels in the catalog
+				packageCondition := sub.Status.GetCondition(operatorsv1alpha1.SubscriptionPackageDeprecated)
+				Expect(packageCondition.Status).To(Equal(corev1.ConditionTrue))
+				channelCondition := sub.Status.GetCondition(operatorsv1alpha1.SubscriptionChannelDeprecated)
+				Expect(channelCondition.Status).To(Equal(corev1.ConditionTrue))
+				bundleCondition := sub.Status.GetCondition(operatorsv1alpha1.SubscriptionBundleDeprecated)
+				Expect(bundleCondition.Status).To(Equal(corev1.ConditionTrue))
+
+				By("verifying that a roll-up condition is present containing all deprecation conditions")
+				// Roll-up condition should be present and contain deprecation messages from all three levels
+				rollUpCondition := sub.Status.GetCondition(operatorsv1alpha1.SubscriptionDeprecated)
+				Expect(rollUpCondition.Status).To(Equal(corev1.ConditionTrue))
+				Expect(rollUpCondition.Message).To(ContainSubstring(packageCondition.Message))
+				Expect(rollUpCondition.Message).To(ContainSubstring(channelCondition.Message))
+				Expect(rollUpCondition.Message).To(ContainSubstring(bundleCondition.Message))
+
+				By("updating the catalog with a fixed v0.3.0 bundle image no longer marked deprecated")
+				provider, err = NewFileBasedFiledBasedCatalogProvider(filepath.Join(testdataDir, subscriptionTestDataBaseDir, "example-operator.v0.3.0.yaml"))
+				Expect(err).To(BeNil())
+				err = magicCatalog.UpdateCatalog(context.Background(), provider)
+				Expect(err).To(BeNil())
+
+				By("waiting for the subscription to have v0.3.0 installed with no deprecation message present")
+				_, err = fetchSubscription(crc, generatedNamespace.GetName(), subName, subscriptionDoesNotHaveCondition(v1alpha1.SubscriptionDeprecated))
+				Expect(err).Should(BeNil())
+			})
+
+			It("should report only package and channel deprecation conditions when bundle is no longer deprecated", func() {
+				By("patching the OperatorGroup to reduce the bundle unpacking timeout")
+				ogNN := types.NamespacedName{Name: operatorGroup.GetName(), Namespace: generatedNamespace.GetName()}
+				addBundleUnpackTimeoutOGAnnotation(context.Background(), ctx.Ctx().Client(), ogNN, "5m")
+
+				By("updating the catalog with a fixed v0.2.0 bundle image marked deprecated at all levels")
+				provider, err := NewFileBasedFiledBasedCatalogProvider(filepath.Join(testdataDir, subscriptionTestDataBaseDir, "example-operator.v0.2.0-deprecations.yaml"))
+				Expect(err).To(BeNil())
+				err = magicCatalog.UpdateCatalog(context.Background(), provider)
+				Expect(err).To(BeNil())
+
+				By("waiting for the subscription to have v0.2.0 installed with a Bundle Deprecated condition")
+				sub, err := fetchSubscription(crc, generatedNamespace.GetName(), subName, subscriptionHasCondition(
+					v1alpha1.SubscriptionBundleDeprecated,
+					corev1.ConditionTrue,
+					"",
+					"olm.bundle/example-operator.v0.2.0: bundle \"example-operator.v0.2.0\" has been deprecated. Please switch to a different one."))
+				Expect(err).Should(BeNil())
+
+				By("checking for the bundle deprecated condition")
+				bundleCondition := sub.Status.GetCondition(operatorsv1alpha1.SubscriptionBundleDeprecated)
+				Expect(bundleCondition.Status).To(Equal(corev1.ConditionTrue))
+				bundleDeprecatedMessage := bundleCondition.Message
+
+				By("updating the catalog with a fixed v0.3.0 bundle marked partially deprecated")
+				provider, err = NewFileBasedFiledBasedCatalogProvider(filepath.Join(testdataDir, subscriptionTestDataBaseDir, "example-operator.v0.3.0-deprecations.yaml"))
+				Expect(err).To(BeNil())
+				err = magicCatalog.UpdateCatalog(context.Background(), provider)
+				Expect(err).To(BeNil())
+
+				By("waiting for the subscription to have v0.3.0 installed with a Package deprecated condition")
+				_, err = fetchSubscription(crc, generatedNamespace.GetName(), subName, subscriptionDoesNotHaveCondition(v1alpha1.SubscriptionBundleDeprecated))
+				Expect(err).Should(BeNil())
+				sub, err = fetchSubscription(crc, generatedNamespace.GetName(), subName, subscriptionHasCondition(
+					v1alpha1.SubscriptionPackageDeprecated,
+					corev1.ConditionTrue,
+					"",
+					"olm.package/packageA: packageA has been deprecated. Please switch to packageB."))
+				Expect(err).Should(BeNil())
+
+				By("checking for the deprecated conditions")
+				// Operator is deprecated at only Package and Channel levels
+				packageCondition := sub.Status.GetCondition(operatorsv1alpha1.SubscriptionPackageDeprecated)
+				Expect(packageCondition.Status).To(Equal(corev1.ConditionTrue))
+				channelCondition := sub.Status.GetCondition(operatorsv1alpha1.SubscriptionChannelDeprecated)
+				Expect(channelCondition.Status).To(Equal(corev1.ConditionTrue))
+				bundleCondition = sub.Status.GetCondition(operatorsv1alpha1.SubscriptionBundleDeprecated)
+				Expect(bundleCondition.Status).To(Equal(corev1.ConditionUnknown))
+
+				By("verifying that a roll-up condition is present not containing bundle deprecation condition")
+				// Roll-up condition should be present and contain deprecation messages from Package and Channel levels
+				rollUpCondition := sub.Status.GetCondition(operatorsv1alpha1.SubscriptionDeprecated)
+				Expect(rollUpCondition.Status).To(Equal(corev1.ConditionTrue))
+				Expect(rollUpCondition.Message).To(ContainSubstring(packageCondition.Message))
+				Expect(rollUpCondition.Message).To(ContainSubstring(channelCondition.Message))
+				Expect(rollUpCondition.Message).ToNot(ContainSubstring(bundleDeprecatedMessage))
+			})
+
+			It("should report deprecated status when catalog is updated to deprecate an installed bundle", func() {
+				By("patching the OperatorGroup to reduce the bundle unpacking timeout")
+				ogNN := types.NamespacedName{Name: operatorGroup.GetName(), Namespace: generatedNamespace.GetName()}
+				addBundleUnpackTimeoutOGAnnotation(context.Background(), ctx.Ctx().Client(), ogNN, "5m")
+
+				By("updating the catalog with a fixed v0.2.0 bundle not marked deprecated")
+				provider, err := NewFileBasedFiledBasedCatalogProvider(filepath.Join(testdataDir, subscriptionTestDataBaseDir, "example-operator.v0.2.0.yaml"))
+				Expect(err).To(BeNil())
+				err = magicCatalog.UpdateCatalog(context.Background(), provider)
+				Expect(err).To(BeNil())
+
+				By("waiting for the subscription to have v0.2.0 installed")
+				sub, err := fetchSubscription(crc, generatedNamespace.GetName(), subName, subscriptionHasCurrentCSV("example-operator.v0.2.0"))
+				Expect(err).Should(BeNil())
+
+				By("the subscription should not be marked deprecated")
+				rollupCondition := sub.Status.GetCondition(operatorsv1alpha1.SubscriptionDeprecated)
+				Expect(rollupCondition.Status).To(Equal(corev1.ConditionUnknown))
+
+				By("updating the catalog with a fixed v0.2.0 bundle image marked deprecated at all levels")
+				provider, err = NewFileBasedFiledBasedCatalogProvider(filepath.Join(testdataDir, subscriptionTestDataBaseDir, "example-operator.v0.2.0-deprecations.yaml"))
+				Expect(err).To(BeNil())
+				err = magicCatalog.UpdateCatalog(context.Background(), provider)
+				Expect(err).To(BeNil())
+
+				By("checking for the bundle deprecated condition")
+				Eventually(func() (bool, error) {
+					sub, err := fetchSubscription(crc, generatedNamespace.GetName(), subName, subscriptionHasCurrentCSV("example-operator.v0.2.0"))
+					if err != nil {
+						return false, err
+					}
+					cond := sub.Status.GetCondition(v1alpha1.SubscriptionDeprecated)
+					if cond.Status != corev1.ConditionTrue {
+						return false, fmt.Errorf("%s condition not found", v1alpha1.SubscriptionDeprecated)
+					}
+
+					return true, nil
+				}, 2*time.Minute, time.Second*10).Should(BeTrue())
+			})
 		})
 	})
 	When("bundle unpack retries are enabled", func() {
@@ -3079,6 +3224,19 @@ func subscriptionHasCondition(condType operatorsv1alpha1.SubscriptionConditionTy
 	}
 }
 
+func subscriptionDoesNotHaveCondition(condType operatorsv1alpha1.SubscriptionConditionType) subscriptionStateChecker {
+	return func(subscription *operatorsv1alpha1.Subscription) bool {
+		cond := subscription.Status.GetCondition(condType)
+		if cond.Status == corev1.ConditionUnknown {
+			fmt.Printf("subscription condition not found\n")
+			return true
+		}
+
+		fmt.Printf("subscription condition found: %v\n", cond)
+		return false
+	}
+}
+
 func fetchSubscription(crc versioned.Interface, namespace, name string, checker subscriptionStateChecker) (*operatorsv1alpha1.Subscription, error) {
 	var fetchedSubscription *operatorsv1alpha1.Subscription
 
diff --git a/test/e2e/testdata/subscription/example-operator.v0.2.0-deprecations.yaml b/test/e2e/testdata/subscription/example-operator.v0.2.0-deprecations.yaml
new file mode 100644
index 000000000..be5158dff
--- /dev/null
+++ b/test/e2e/testdata/subscription/example-operator.v0.2.0-deprecations.yaml
@@ -0,0 +1,41 @@
+---
+schema: olm.package
+name: packageA
+defaultChannel: stable
+---
+schema: olm.channel
+package: packageA
+name: stable
+entries:
+  - name: example-operator.v0.2.0
+    replaces: example-operator.v0.1.0
+---
+schema: olm.bundle
+name: example-operator.v0.2.0
+package: packageA
+image: quay.io/olmtest/example-operator-bundle:0.2.0
+properties:
+  - type: olm.gvk
+    value:
+      group: example.com
+      kind: TestA
+      version: v1alpha1
+  - type: olm.package
+    value:
+      packageName: packageA
+      version: 1.0.1
+---
+schema: olm.deprecations
+package: packageA
+entries:
+  - reference:
+      schema: olm.package
+    message: packageA has been deprecated. Please switch to packageB.
+  - reference:
+      schema: olm.channel
+      name: stable
+    message: channel "stable" has been deprecated. Please switch to a different one.
+  - reference:
+      schema: olm.bundle
+      name: example-operator.v0.2.0
+    message: bundle "example-operator.v0.2.0" has been deprecated. Please switch to a different one.
diff --git a/test/e2e/testdata/subscription/example-operator.v0.3.0-deprecations.yaml b/test/e2e/testdata/subscription/example-operator.v0.3.0-deprecations.yaml
new file mode 100644
index 000000000..261308fbc
--- /dev/null
+++ b/test/e2e/testdata/subscription/example-operator.v0.3.0-deprecations.yaml
@@ -0,0 +1,37 @@
+---
+schema: olm.package
+name: packageA
+defaultChannel: stable
+---
+schema: olm.channel
+package: packageA
+name: stable
+entries:
+  - name: example-operator.v0.3.0
+    replaces: example-operator.v0.2.0
+---
+schema: olm.bundle
+name: example-operator.v0.3.0
+package: packageA
+image: quay.io/olmtest/example-operator-bundle:0.3.0
+properties:
+  - type: olm.gvk
+    value:
+      group: example.com
+      kind: TestA
+      version: v1alpha1
+  - type: olm.package
+    value:
+      packageName: packageA
+      version: 0.3.0
+---
+schema: olm.deprecations
+package: packageA
+entries:
+  - reference:
+      schema: olm.package
+    message: packageA has been deprecated. Please switch to packageB.
+  - reference:
+      schema: olm.channel
+      name: stable
+    message: channel "stable" has been deprecated. Please switch to a different one.