github.com/operator-framework/operator-lifecycle-manager@v0.30.0/doc/contributors/design-proposals/subscription-status.md (about)

     1  # Improved Subscription Status
     2  
     3  Status: Pending
     4  
     5  Version: Alpha
     6  
     7  Implementation Owner: TBD
     8  
     9  ## Motivation
    10  
    11  The `Subscription` `CustomResource` needs to expose useful information when a failure scenario is encountered. Failures can be encountered throughout a `Subscription`'s existence and can include issues with `InstallPlan` resolution, `CatalogSource` connectivity, `ClusterServiceVersion` (CSV) status, and more. To surface this information, explicit status for `Subscriptions` will be introduced via [status conditions](#status-conditions) which will be set by new, specialized status sync handlers for resources of interest (`Subscriptions`, `InstallPlan`s, `CatalogSource`s and CSVs).
    12  
    13  ### Following Conventions
    14  
    15  In order to design a status that makes sense in the context of kubernetes resources, it's important to conform to current conventions. This will also help us avoid pitfalls that may have already been solved.
    16  
    17  #### Status Conditions
    18  
    19  The [kube api-conventions docs](https://github.com/kubernetes/community/blob/master/contributors/devel/sig-architecture/api-conventions.md#typical-status-properties) state that:
    20  > Conditions should be added to explicitly convey properties that users and components care about rather than requiring those properties to be inferred from other observations.
    21  
    22  A few internal Kubernetes resources that implement status conditions:
    23  
    24  - [NodeStatus](https://github.com/kubernetes/kubernetes/blob/6c31101257bfcd47fa53702cea07fe2eedf2ad92/pkg/apis/core/types.go#L3556)
    25  - [DeploymentStatus](https://github.com/kubernetes/kubernetes/blob/f5574bf62a051c4a41a3fff717cc0bad735827eb/pkg/apis/apps/types.go#L415)
    26  - [DaemonSetStatus](https://github.com/kubernetes/kubernetes/blob/f5574bf62a051c4a41a3fff717cc0bad735827eb/pkg/apis/apps/types.go#L582)
    27  - [ReplicaSetStatus](https://github.com/kubernetes/kubernetes/blob/f5574bf62a051c4a41a3fff717cc0bad735827eb/pkg/apis/apps/types.go#L751)
    28  
    29  Introducing status conditions will let us have an explicit, level-based view of the current abnormal state of a `Subscription`. They are essentially orthogonal states (regions) of the compound state (`SubscriptionStatus`)¹. A conditionᵢ has a set of sub states [Unknown, True, False] each with sub states of their own [Reasonsᵢ],where Reasonsᵢ contains the set of transition reasons for conditionᵢ. This compound state can be used to inform a decision about performing an operation on the cluster.
    30  
    31  > 1. [What is a statechart?](https://statecharts.github.io/what-is-a-statechart.html); see 'A state can have many "regions"'
    32  
    33  #### References to Related Objects
    34  
    35  The [kube api-convention docs](https://github.com/kubernetes/community/blob/master/contributors/devel/sig-architecture/api-conventions.md#references-to-related-objects) state that:
    36  > References to specific objects, especially specific resource versions and/or specific fields of those objects, are specified using the ObjectReference type (or other types representing strict subsets of it).
    37  
    38  Rather than building our own abstractions to reference managed resources (like `InstallPlan`s), we can take advantage of the pre-existing `ObjectReference` type.
    39  
    40  ## Proposal
    41  
    42  ### Changes to SubscriptionStatus
    43  
    44  - Introduce a `SubscriptionCondition` type
    45    - Describes a single state of a `Subscription` explicity
    46  - Introduce a `SubscriptionConditionType` field
    47    - Describes the type of a condition
    48  - Introduce a `Conditions` field of type `[]SubscriptionCondition` to `SubscriptionStatus`
    49    - Describes multiple potentially orthogonal states of a `Subscription` explicitly
    50  - Introduce an `InstallPlanRef` field of type [*corev1.ObjectReference](https://github.com/kubernetes/kubernetes/blob/f5574bf62a051c4a41a3fff717cc0bad735827eb/pkg/apis/core/types.go#L3993)
    51    - To replace custom type with existing apimachinery type
    52  - Deprecate the `Install` field
    53    - Value will be kept up to date to support older clients until a major version change
    54  - Introduce a `SubscriptionCatalogStatus` type
    55    - Describes a Subscription's view of a CatalogSource's status
    56  - Introduce a `CatalogStatus` field of type `[]SubscriptionCatalogStatus`
    57    - CatalogStatus contains the Subscription's view of its relevant CatalogSources' status
    58  
    59  ### Changes to Subscription Reconciliation
    60  
    61  Changes to `Subscription` reconciliation can be broken into three parts:
    62  
    63  1. Phase in use of `SubscriptionStatus.Install` with `SubscriptionStatus.InstallPlanRef`:
    64     - Write to `Install` and `InstallPlanRef` but still read from `Install`
    65     - Read from `InstallPlanRef`
    66     - Stop writing to `Install`
    67  2. Create independent sync handlers and workqueues for resources of interest (status-handler) that only update specific `SubscriptionStatus` fields and `StatusConditions`:
    68     - Build actionable state reactively through objects of interest
    69     - Treat omitted `SubscriptionConditionTypes` in `SubscriptionStatus.Conditions` as having `ConditionStatus` "Unknown"
    70     - Add new status-handlers with new workqueues for:
    71       - `Subscription`s
    72       - `CatalogSource`s
    73       - `InstallPlan`s
    74       - CSVs
    75     - These sync handlers can be phased-in incrementally:
    76       - Add a conditions block and the `UpToDate` field, and ensure the `UpToDate` field is set properly when updating status
    77       - Pick one condition to start detecting, and write its status
    78       - Repeat with other conditions. This is a good opportunity to parallelize work immediate value to end-users (they start seeing the new conditions ASAP)
    79       - Once all conditions are being synchronized, start using them to set the state of other fields (e.g. `UpToDate`)
    80  3. Add status-handler logic to toggle the `SubscriptionStatus.UpToDate` field:
    81     - Whenever `SubscriptionStatus.InstalledCSV == SubscriptionStatus.CurrentCSV` and `SubscriptionStatus.Conditions` has a `SubscriptionConditionType` of type `SubscriptionInstalledCSVReplacementAvailable` with `Status == "True"`, set `SubscriptionStatus.UpToDate = true`
    82     - Whenever `SubscriptionStatus.InstalledCSV != SubscriptionStatus.CurrentCSV`, set `SubscriptionStatus.UpToDate = false`
    83  
    84  ## Implementation
    85  
    86  ### SubscriptionStatus
    87  
    88  Updated SusbcriptionStatus resource:
    89  
    90  ```go
    91  import (
    92      // ...
    93      corev1 "k8s.io/kubernetes/pkg/apis/core/v1"
    94      metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    95      // ...
    96  )
    97  
    98  type SubscriptionStatus struct {
    99      // ObservedGeneration is the generation observed by the Subscription controller.
   100      // +optional
   101      ObservedGeneration int64 `json:"observedGeneration,omitempty"`
   102  
   103      // CurrentCSV is the CSV the Subscription is progressing to.
   104      // +optional
   105      CurrentCSV   string                `json:"currentCSV,omitempty"`
   106  
   107      // InstalledCSV is the CSV currently installed by the Subscription.
   108      // +optional
   109      InstalledCSV string                `json:"installedCSV,omitempty"`
   110  
   111      // Install is a reference to the latest InstallPlan generated for the Subscription.
   112      // DEPRECATED: InstallPlanRef
   113      // +optional
   114      Install      *InstallPlanReference `json:"installplan,omitempty"`
   115  
   116      // State represents the current state of the Subscription
   117      // +optional
   118      State       SubscriptionState `json:"state,omitempty"`
   119  
   120      // Reason is the reason the Subscription was transitioned to its current state.
   121      // +optional
   122      Reason      ConditionReason   `json:"reason,omitempty"`
   123  
   124      // InstallPlanRef is a reference to the latest InstallPlan that contains the Subscription's current CSV.
   125      // +optional
   126      InstallPlanRef *corev1.ObjectReference `json:"installPlanRef,omitempty"`
   127  
   128      // CatalogStatus contains the Subscription's view of its relevant CatalogSources' status.
   129      // It is used to determine SubscriptionStatusConditions related to CatalogSources.
   130      // +optional
   131      CatalogStatus []SubscriptionCatalogStatus `json:"catalogStatus,omitempty"`
   132  
   133      // UpToDate is true when the latest CSV for the Subscription's package and channel is installed and running; false otherwise.
   134      //
   135      // This field is not a status SubscriptionCondition because it "represents a well-known state that applies to all instances of a kind"
   136      // (see https://github.com/kubernetes/community/blob/master/contributors/devel/sig-architecture/api-conventions.md#typical-status-properties).
   137      // In this case, all Subscriptions are either up to date or not up to date.
   138      UpToDate bool `json:"UpToDate"`
   139  
   140      // LastUpdated represents the last time that the Subscription status was updated.
   141      LastUpdated metav1.Time       `json:"lastUpdated"`
   142  
   143      // Conditions is a list of the latest available observations about a Subscription's current state.
   144      // +optional
   145      Conditions []SubscriptionCondition  `json:"conditions,omitempty"`
   146  }
   147  
   148  // SubscriptionCatalogHealth describes a Subscription's view of a CatalogSource's status.
   149  type SubscriptionCatalogStatus struct {
   150      // CatalogSourceRef is a reference to a CatalogSource.
   151      CatalogSourceRef *corev1.ObjectReference `json:"catalogSourceRef"`
   152  
   153      // LastUpdated represents the last time that the CatalogSourceHealth changed
   154      LastUpdated `json:"lastUpdated"`
   155  
   156      // Healthy is true if the CatalogSource is healthy; false otherwise.
   157      Healthy bool `json:"healthy"`
   158  }
   159  
   160  // SubscriptionConditionType indicates an explicit state condition about a Subscription in "abnormal-true"
   161  // polarity form (see https://github.com/kubernetes/community/blob/master/contributors/devel/sig-architecture/api-conventions.md#typical-status-properties).
   162  type SusbcriptionConditionType string
   163  
   164  const (
   165      // SubscriptionResolutionFails indicates the Subscription has failed to resolve a set
   166      SubscriptionResolutionFailed SubscriptionConditionType = "ResolutionFailed"
   167  
   168      // SubscriptionCatalogSourcesUnhealthy indicates that some or all of the CatalogSources to be used in resolution are unhealthy.
   169      SubscriptionCatalogSourcesUnhealthy SubscriptionConditionType = "CatalogSourcesUnhealthy"
   170  
   171      // SubscriptionCatalogSourceInvalid indicates the CatalogSource specified in the SubscriptionSpec is not valid.
   172      SubscriptionCatalogSourceInvalid SubscriptionConditionType = "CatalogSourceInvalid"
   173  
   174      // SubscriptionPackageChannelInvalid indicates the package and channel specified in the SubscriptionSpec is not valid.
   175      SubscriptionPackageChannelInvalid SubscriptionConditionType = "PackageChannelInvalid"
   176  
   177      // SubscriptionInstallPlanFailed indicates the InstallPlan responsible for installing the current CSV has failed.
   178      SubscriptionInstallPlanFailed SubscriptionConditionType = "InstallPlanFailed"
   179  
   180      // SubscriptionInstallPlanMissing indicates the InstallPlan responsible for installing the current CSV is missing.
   181      SubscriptionInstallPlanMissing SubscriptionConditionType = "InstallPlanMissing"
   182  
   183      // SubscriptionInstallPlanAwaitingManualApproval indicates the InstallPlan responsible for installing the current CSV is waiting 
   184      // for manual approval.
   185      SubscriptionInstallPlanAwaitingManualApproval SubscriptionConditionType = "InstallPlanAwaitingManualApproval"
   186  
   187      // SubscriptionInstalledCSVReplacementAvailable indicates there exists a replacement for the installed CSV.
   188      SubscriptionInstalledCSVReplacementAvailable SubscriptionConditionType = "InstalledCSVReplacementAvailable"
   189  
   190      // SubscriptionInstalledCSVMissing indicates the installed CSV is missing.
   191      SubscriptionInstalledCSVMissing SubscriptionConditionType = "InstalledCSVMissing"
   192  
   193      // SubscriptionInstalledCSVFailed indicates the installed CSV has failed.
   194      SubscriptionInstalledCSVFailed SubscriptionConditionType = "InstalledCSVFailed"
   195  )
   196  
   197  type SubscriptionCondition struct {
   198      // Type is the type of Subscription condition.
   199      Type               SubscriptionConditionType   `json:"type" description:"type of Subscription condition"`
   200  
   201      // Status is the status of the condition, one of True, False, Unknown.
   202      Status             corev1.ConditionStatus    `json:"status" description:"status of the condition, one of True, False, Unknown"`
   203  
   204      // Reason is a one-word CamelCase reason for the condition's last transition.
   205      // +optional
   206      Reason             string            `json:"reason,omitempty" description:"one-word CamelCase reason for the condition's last transition"`
   207  
   208      // Message is a human-readable message indicating details about last transition.
   209      // +optional
   210      Message            string            `json:"message,omitempty" description:"human-readable message indicating details about last transition"`
   211  
   212      // LastHeartbeatTime is the last time we got an update on a given condition
   213      // +optional
   214      LastHeartbeatTime  *metav1.Time  `json:"lastHeartbeatTime,omitempty" description:"last time we got an update on a given condition"`
   215  
   216      // LastTransitionTime is the last time the condition transit from one status to another
   217      // +optional
   218      LastTransitionTime *metav1.Time  `json:"lastTransitionTime,omitempty" description:"last time the condition transit from one status to another"`
   219  }
   220  ```
   221  
   222  ### Subscription Reconciliation
   223  
   224  Phasing in `SusbcriptionStatus.InstallPlanRef`:
   225  
   226  - Create a helper function to convert `ObjectReference`s into `InstallPlanReference`s in _pkg/api/apis/operators/v1alpha1/subscription_types.go_
   227  
   228  ```go
   229  package v1alpha1
   230  
   231  import (
   232      // ...
   233      corev1 "k8s.io/api/core/v1"
   234      // ...
   235  )
   236  // ...
   237  func NewInstallPlanReference(ref *corev1.ObjectReference) *InstallPlanReference {
   238      return &InstallPlanReference{
   239          APIVersion: ref.APIVersion,
   240          Kind:       ref.Kind,
   241          Name:       ref.Name,
   242          UID:        ref.UID,
   243      }
   244  }
   245  ```
   246  
   247  - Define an interface and method for generating `ObjectReferences` for `InstallPlan`s in _pkg/api/apis/operators/referencer.go_
   248  
   249  ```go
   250  package operators
   251  
   252  import (
   253      "fmt"
   254      // ...
   255      corev1 "k8s.io/api/core/v1"
   256      "k8s.io/apimachinery/pkg/api/meta"
   257      // ...
   258      "github.com/operator-framework/api/pkg/operators/v1alpha1"
   259      "github.com/operator-framework/api/pkg/operators/v1alpha2"
   260  )
   261  
   262  // CannotReferenceError indicates that an ObjectReference could not be generated for a resource.
   263  type CannotReferenceError struct{
   264      obj interface{}
   265      msg string
   266  }
   267  
   268  // Error returns the error's error string.
   269  func (err *CannotReferenceError) Error() string {
   270      return fmt.Sprintf("cannot reference %v: %s", obj, msg)
   271  }
   272  
   273  // NewCannotReferenceError returns a pointer to a CannotReferenceError instantiated with the given object and message.
   274  func NewCannotReferenceError(obj interface{}, msg string) *CannotReferenceError {
   275      return &CannotReferenceError{obj: obj, msg: msg}
   276  }
   277  
   278  // ObjectReferencer knows how to return an ObjectReference for a resource.
   279  type ObjectReferencer interface {
   280      // ObjectReferenceFor returns an ObjectReference for the given resource.
   281      ObjectReferenceFor(obj interface{}) (*corev1.ObjectReference, error)  
   282  }
   283  
   284  // ObjectReferencerFunc is a function type that implements ObjectReferencer.
   285  type ObjectReferencerFunc func(obj interface{}) (*corev1.ObjectReference, error)
   286  
   287  // ObjectReferenceFor returns an ObjectReference for the current resource by invoking itself.
   288  func (f ObjectReferencerFunc) ObjectReferenceFor(obj interface{}) (*corev1.ObjectReference, error) {
   289      return f(obj)
   290  }
   291  
   292  // OperatorsObjectReferenceFor generates an ObjectReference for the given resource if it's provided by the operators.coreos.com API group.
   293  func OperatorsObjectReferenceFor(obj interface{}) (*corev1.ObjectReference, error) {
   294      // Attempt to access ObjectMeta
   295      objMeta, err := meta.Accessor(obj)
   296      if err != nil {
   297          return nil, NewCannotReferenceError(obj, err.Error())
   298      }
   299  
   300      ref := &corev1.ObjectReference{
   301          Namespace: objMeta.GetNamespace(),
   302          Name: objMeta.GetName(),
   303          UID: objMeta.GetUI(),
   304      }
   305      switch objMeta.(type) {
   306      case *v1alpha1.ClusterServiceVersion:
   307          ref.Kind = v1alpha1.ClusterServiceVersionKind
   308          ref.APIVersion = v1alpha1.SchemeGroupVersion.String()
   309      case *v1alpha1.InstallPlan:
   310          ref.Kind = v1alpha1.InstallPlanKind
   311          ref.APIVersion = v1alpha1.SchemeGroupVersion.String()
   312      case *v1alpha1.Subscription:
   313          ref.Kind = v1alpha1.SubscriptionKind
   314          ref.APIVersion = v1alpha1.SchemeGroupVersion.String()
   315      case *v1alpha1.CatalogSource:
   316          ref.Kind = v1alpha1.CatalogSourceKind
   317          ref.APIVersion = v1alpha1.SchemeGroupVersion.String()
   318      case *v1.OperatorGroup:
   319          ref.Kind = v1alpha2.OperatorGroupKind
   320          ref.APIVersion = v1alpha2.SchemeGroupVersion.String()
   321      case v1alpha1.ClusterServiceVersion:
   322          ref.Kind = v1alpha1.ClusterServiceVersionKind
   323          ref.APIVersion = v1alpha1.SchemeGroupVersion.String()
   324      case v1alpha1.InstallPlan:
   325          ref.Kind = v1alpha1.InstallPlanKind
   326          ref.APIVersion = v1alpha1.SchemeGroupVersion.String()
   327      case v1alpha1.Subscription:
   328          ref.Kind = v1alpha1.SubscriptionKind
   329          ref.APIVersion = v1alpha1.SchemeGroupVersion.String()
   330      case v1alpha1.CatalogSource:
   331          ref.Kind = v1alpha1.CatalogSourceKind
   332          ref.APIVersion = v1alpha1.SchemeGroupVersion.String()
   333      case v1.OperatorGroup:
   334          ref.Kind = v1alpha2.OperatorGroupKind
   335          ref.APIVersion = v1alpha2.SchemeGroupVersion.String()
   336      default:
   337          return nil, NewCannotReferenceError(objMeta, "resource not a valid olm kind")
   338      }
   339  
   340      return ref, nil
   341  }
   342  
   343  type ReferenceSet map[*corev1.ObjectReference]struct{}
   344  
   345  type ReferenceSetBuilder interface {
   346      Build(obj interface{}) (ReferenceSet, error)
   347  }
   348  
   349  type ReferenceSetBuilderFunc func(obj interface{}) (ReferenceSet, error)
   350  
   351  func (f ReferenceSetBuilderFunc) Build(obj interface{}) (ReferenceSet, error) {
   352      return f(obj)
   353  }
   354  
   355  func BuildOperatorsReferenceSet(obj interface{}) (ReferenceSet, error) {
   356      referencer := ObjectReferencer(OperatorsObjectReferenceFor)
   357      obj := []interface{}
   358      set := make(ReferenceSet)
   359      switch v := obj.(type) {
   360      case []*v1alpha1.ClusterServiceVersion:
   361          for _, o := range v {
   362              ref, err := referencer.ObjectReferenceFor(o)
   363              if err != nil {
   364                  return nil, err
   365              }
   366              set[ref] = struct{}{}
   367          }
   368      case []*v1alpha1.InstallPlan:
   369          for _, o := range v {
   370              ref, err := referencer.ObjectReferenceFor(o)
   371              if err != nil {
   372                  return nil, err
   373              }
   374              set[ref] = struct{}{}
   375          }
   376      case []*v1alpha1.Subscription:
   377          for _, o := range v {
   378              ref, err := referencer.ObjectReferenceFor(o)
   379              if err != nil {
   380                  return nil, err
   381              }
   382              set[ref] = struct{}{}
   383          }
   384      case []*v1alpha1.CatalogSource:
   385          for _, o := range v {
   386              ref, err := referencer.ObjectReferenceFor(o)
   387              if err != nil {
   388                  return nil, err
   389              }
   390              set[ref] = struct{}{}
   391          }
   392      case []*v1.OperatorGroup:
   393          for _, o := range v {
   394              ref, err := referencer.ObjectReferenceFor(o)
   395              if err != nil {
   396                  return nil, err
   397              }
   398              set[ref] = struct{}{}
   399          }
   400      case []v1alpha1.ClusterServiceVersion:
   401          for _, o := range v {
   402              ref, err := referencer.ObjectReferenceFor(o)
   403              if err != nil {
   404                  return nil, err
   405              }
   406              set[ref] = struct{}{}
   407          }
   408      case []v1alpha1.InstallPlan:
   409          for _, o := range v {
   410              ref, err := referencer.ObjectReferenceFor(o)
   411              if err != nil {
   412                  return nil, err
   413              }
   414              set[ref] = struct{}{}
   415          }
   416      case []v1alpha1.Subscription:
   417          for _, o := range v {
   418              ref, err := referencer.ObjectReferenceFor(o)
   419              if err != nil {
   420                  return nil, err
   421              }
   422              set[ref] = struct{}{}
   423          }
   424      case []v1alpha1.CatalogSource:
   425          for _, o := range v {
   426              ref, err := referencer.ObjectReferenceFor(o)
   427              if err != nil {
   428                  return nil, err
   429              }
   430              set[ref] = struct{}{}
   431          }
   432      case []v1.OperatorGroup:
   433          for _, o := range v {
   434              ref, err := referencer.ObjectReferenceFor(o)
   435              if err != nil {
   436                  return nil, err
   437              }
   438              set[ref] = struct{}{}
   439          }
   440      default:
   441          // Could be a single resource
   442          ref, err := referencer.ObjectReferenceFor(o)
   443          if err != nil {
   444              return nil, err
   445          }
   446          set[ref] = struct{}{}
   447      }
   448  
   449      return set, nil
   450  }
   451  
   452  
   453  ```
   454  
   455  - Add an `ObjectReferencer` field to the [catalog-operator](https://github.com/operator-framework/operator-lifecycle-manager/blob/22691a771a330fc05608a7ec1516d31a17a13ded/pkg/controller/operators/catalog/operator.go#L58)
   456  
   457  ```go
   458  package catalog
   459  
   460  import (
   461      // ...
   462      "github.com/operator-framework/api/pkg/operators"
   463      // ...
   464  )
   465  // ...
   466  type Operator struct {
   467      // ...
   468      referencer operators.ObjectReferencer
   469  }
   470  // ...
   471  func NewOperator(kubeconfigPath string, logger *logrus.Logger, wakeupInterval time.Duration, configmapRegistryImage, operatorNamespace string, watchedNamespaces ...string) (*Operator, error) {
   472      // ...
   473      op := &Operator{
   474          // ...
   475          referencer: operators.ObjectReferencerFunc(operators.OperatorsObjectReferenceFor),
   476      }
   477      // ...
   478  }
   479  //  ...
   480  ```
   481  
   482  - Generate `ObjectReference`s in [ensureInstallPlan(...)](https://github.com/operator-framework/operator-lifecycle-manager/blob/22691a771a330fc05608a7ec1516d31a17a13ded/pkg/controller/operators/catalog/operator.go#L804)
   483  
   484  ```go
   485  func (o *Operator) ensureInstallPlan(logger *logrus.Entry, namespace string, subs []*v1alpha1.Subscription, installPlanApproval v1alpha1.Approval, steps []*v1alpha1.Step) (*corev1.ObjectReference, error) {
   486      // ...
   487      for _, installPlan := range installPlans {
   488          if installPlan.Status.CSVManifestsMatch(steps) {
   489              logger.Infof("found InstallPlan with matching manifests: %s", installPlan.GetName())
   490              return a.referencer.ObjectReferenceFor(installPlan), nil
   491          }
   492      }
   493      // ...
   494  }
   495  ```
   496  
   497  Write to `SusbcriptionStatus.InstallPlan` and `SubscriptionStatus.InstallPlanRef`:
   498  
   499  - Generate `ObjectReference`s in [createInstallPlan(...)](https://github.com/operator-framework/operator-lifecycle-manager/blob/22691a771a330fc05608a7ec1516d31a17a13ded/pkg/controller/operators/catalog/operator.go#L863)
   500  
   501  ```go
   502  func (o *Operator) createInstallPlan(namespace string, subs []*v1alpha1.Subscription, installPlanApproval v1alpha1.Approval, steps []*v1alpha1.Step) (*corev1.ObjectReference, error) {
   503      // ...
   504      return a.referencer.ObjectReferenceFor(res), nil
   505  }
   506  ```
   507  
   508  - Use `ObjectReference` to populate both `SusbcriptionStatus.InstallPlan` and `SubscriptionStatus.InstallPlanRef` in [updateSubscriptionStatus](https://github.com/operator-framework/operator-lifecycle-manager/blob/22691a771a330fc05608a7ec1516d31a17a13ded/pkg/controller/operators/catalog/operator.go#L774)
   509  
   510  ```go
   511  func (o *Operator) updateSubscriptionStatus(namespace string, subs []*v1alpha1.Subscription, installPlanRef *corev1.ObjectReference) error {
   512      // ...
   513      for _, sub := range subs {
   514          // ...
   515          if installPlanRef != nil {
   516              sub.Status.InstallPlanRef = installPlanRef
   517              sub.Status.Install = v1alpha1.NewInstallPlanReference(installPlanRef)
   518              sub.Status.State = v1alpha1.SubscriptionStateUpgradePending
   519          }
   520          // ...
   521      }
   522      // ...
   523  }
   524  ```
   525  
   526  Phase in orthogonal `SubscriptionStatus` condition updates (pick a condition type to start with):
   527  
   528  - Pick `SubscriptionCatalogSourcesUnhealthy`
   529  - Add `SusbcriptionCondition` getter and setter helper methods to `SubscriptionStatus`
   530  
   531  ```go
   532  // GetCondition returns the SubscriptionCondition of the given type if it exists in the SubscriptionStatus' Conditions; returns a condition of the given type with a ConditionStatus of "Unknown" if not found.
   533  func (status SubscriptionStatus) GetCondition(conditionType SubscriptionConditionType) SubscriptionCondition {
   534      for _, cond := range status.Conditions {
   535          if cond.Type == conditionType {
   536              return cond
   537          }
   538      }
   539  
   540      return SubscriptionCondition{
   541          Type: conditionType,
   542          Status: corev1.ConditionUnknown,
   543          // ...
   544      }
   545  }
   546  
   547  // SetCondition sets the given SubscriptionCondition in the SubscriptionStatus' Conditions.
   548  func (status SubscriptionStatus) SetCondition(condition SubscriptionCondition) {
   549      for i, cond := range status.Conditions {
   550          if cond.Type == condition.Type {
   551              cond[i] = condition
   552              return
   553          }
   554      }
   555  
   556      status.Conditions = append(status.Conditions, condition)
   557  }
   558  ```
   559  
   560  - Add a `ReferenceSetBuilder` field to the [catalog-operator](https://github.com/operator-framework/operator-lifecycle-manager/blob/22691a771a330fc05608a7ec1516d31a17a13ded/pkg/controller/operators/catalog/operator.go#L58)
   561  
   562  ```go
   563  package catalog
   564  
   565  import (
   566      // ...
   567      "github.com/operator-framework/api/pkg/operators"
   568      // ...
   569  )
   570  // ...
   571  type Operator struct {
   572      // ...
   573      referenceSetBuilder operators.ReferenceSetBuilder
   574  }
   575  // ...
   576  func NewOperator(kubeconfigPath string, logger *logrus.Logger, wakeupInterval time.Duration, configmapRegistryImage, operatorNamespace string, watchedNamespaces ...string) (*Operator, error) {
   577      // ...
   578      op := &Operator{
   579          // ...
   580          referenceSetBuilder: operators.ReferenceSetBuilderFunc(operators.BuildOperatorsReferenceSet),
   581      }
   582      // ...
   583  }
   584  //  ...
   585  ```
   586  
   587  - Define a new `CatalogSource` sync function that checks the health of a given `CatalogSource` and the health of every `CatalogSource` in its namespace and the global namespace and updates all `Subscription`s that have visibility on it with the condition state
   588  
   589  ```go
   590  // syncSusbcriptionCatalogStatus generates a SubscriptionCatalogStatus for a CatalogSource and updates the
   591  // status of all Subscriptions in its namespace; for CatalogSources in the global catalog namespace, Subscriptions
   592  // in all namespaces are updated.
   593  func (o *Operator) syncSubscriptionCatalogStatus(obj interface{}) (syncError error) {
   594      catsrc, ok := obj.(*v1alpha1.CatalogSource)
   595      if !ok {
   596          o.Log.Debugf("wrong type: %#v", obj)
   597          return fmt.Errorf("casting CatalogSource failed")
   598      }
   599  
   600      logger := o.Log.WithFields(logrus.Fields{
   601          "catsrc": catsrc.GetName(),
   602          "namespace": catsrc.GetNamespace(),
   603          "id":     queueinformer.NewLoopID(),
   604      })
   605      logger.Debug("syncing subscription catalogsource status")
   606  
   607      // Get SubscriptionCatalogStatus
   608      sourceKey := resolver.CatalogKey{Name: owner.Name, Namespace: metaObj.GetNamespace()}
   609      status := o.getSubscriptionCatalogStatus(logger, sourceKey, a.referencer.ObjectReferenceFor(catsrc))
   610  
   611      // Update the status of all Subscriptions that can view this CatalogSource
   612      syncError = updateSubscriptionCatalogStatus(logger, status)
   613  }
   614  
   615  // getSubscriptionCatalogStatus gets the SubscriptionCatalogStatus for a given SourceKey and ObjectReference.
   616  func (o *Operator) getSubscriptionCatalogStatus(logger logrus.Entry, sourceKey resolver.SourceKey, *corev1.ObjectReference) *v1alpha1.SubscriptionCatalogStatus {
   617      // TODO: Implement this
   618  }
   619  
   620  // updateSubscriptionCatalogStatus updates all Subscriptions in the CatalogSource namespace with the given SubscriptionCatalogStatus;
   621  // for CatalogSources in the global catalog namespace, Subscriptions in all namespaces are updated.
   622  func (o *Operator) updateSubscriptionCatalogStatus(logger logrus.Entry, status SubscriptionCatalogStatus) error {
   623      // TODO: Implement this. It should handle removing CatalogStatus entries to non-existent CatalogSources.
   624  }
   625  ```
   626  
   627  - Define a new `Subscription` sync function that checks the `CatalogStatus` field and sets `SubscriptionCondition`s relating to `CatalogSource` status
   628  
   629  ```go
   630  func (o *Operator) syncSubscriptionCatalogConditions(obj interface{}) (syncError error) {
   631      sub, ok := obj.(*v1alpha1.Subscription)
   632      if !ok {
   633          o.Log.Debugf("wrong type: %#v", obj)
   634          return fmt.Errorf("casting Subscription failed")
   635      }
   636  
   637      logger := o.Log.WithFields(logrus.Fields{
   638          "sub": sub.GetName(),
   639          "namespace": sub.GetNamespace(),
   640          "id":     queueinformer.NewLoopID(),
   641      })
   642      logger.Debug("syncing subscription catalogsource conditions")
   643  
   644      // Get the list of CatalogSources visible to the Subscription
   645      catsrcs, err := o.listResolvableCatalogSources(sub.GetNamespace())
   646      if err != nil {
   647          logger.WithError(err).Warn("could not list resolvable catalogsources")
   648          syncError = err
   649          return
   650      }
   651  
   652      // Build reference set from resolvable catalogsources
   653      refSet, err := o.referenceSetBuilder.Build(catsrcs)
   654      if err != nil {
   655          logger.WithError(err).Warn("could not build object reference set of resolvable catalogsources")
   656          syncError = err
   657          return
   658      }
   659  
   660      // Defer an update to the Subscription
   661      out := sub.DeepCopy()
   662      defer func() {
   663          // TODO: Implement update SubscriptionStatus using out if syncError == nil and Subscription has changed
   664      }()
   665  
   666      // Update CatalogSource related CatalogSourceConditions
   667      currentSources = len(refSet)
   668      knownSources = len(sub.Status.CatalogStatus)
   669  
   670      // unhealthyUpdated is set to true when a change has been made to the condition of type SubscriptionCatalogSourcesUnhealthy
   671      unhealthyUpdated := false
   672      // TODO: Add flags for other condition types
   673  
   674      if currentSources > knownSources {
   675          // Flip SubscriptionCatalogSourcesUnhealthy to "Unknown"
   676          condition := out.Status.GetCondition(v1alpha1.SubscriptionCatalogSourcesUnhealthy)
   677          condition.Status = corev1.ConditionUnknown
   678          condition.Reason = "MissingCatalogInfo"
   679          condition.Message = fmt.Sprintf("info on health of %d/%d catalogsources not yet known", currentSources - knownSources, currentSources)
   680          condition.LastSync = timeNow()
   681          out.Status.SetCondition(condition)
   682          unhealthyUpdated = true
   683      }
   684  
   685      // TODO: Add flags for other condition types to loop predicate
   686      for i := 0; !unhealthyUpdated && i < knownSources; i++ {
   687          status := sub.Status.CatalogSources
   688  
   689          if !unhealthyUpdated {
   690              if status.CatalogSourceRef == nil {
   691                  // Flip SubscriptionCatalogSourcesUnhealthy to "Unknown"
   692                  condition := out.Status.GetCondition(v1alpha1.SubscriptionCatalogSourcesUnhealthy)
   693                  condition.Status = corev1.ConditionUnknown
   694                  condition.Reason = "CatalogInfoInvalid"
   695                  condition.Message = "info missing reference to catalogsource"
   696                  condition.LastSync = timeNow()
   697                  out.Status.SetCondition(condition)
   698                  unhealthyUpdated = true
   699                  break
   700              }
   701  
   702              if _, ok := refSet[status.CatalogSourceRef]; !ok {
   703                  // Flip SubscriptionCatalogSourcesUnhealthy to "Unknown"
   704                  condition := out.Status.GetCondition(v1alpha1.SubscriptionCatalogSourcesUnhealthy)
   705                  condition.Status = corev1.ConditionUnknown
   706                  condition.Reason = "CatalogInfoInconsistent"
   707                  condition.Message = fmt.Sprintf("info found for non-existent catalogsource %s/%s", ref.Name, ref.Namespace)
   708                  condition.LastSync = timeNow()
   709                  out.Status.SetCondition(condition)
   710                  unhealthyUpdated = true
   711                  break
   712              }
   713  
   714              if !status.CatalogSourceRef.Healthy {
   715                  // Flip SubscriptionCatalogSourcesUnhealthy to "True"
   716                  condition := out.Status.GetCondition(v1alpha1.SubscriptionCatalogSourcesUnhealthy)
   717                  condition.Status = corev1.ConditionTrue
   718                  condition.Reason = "CatalogSourcesUnhealthy"
   719                  condition.Message = "one or more visible catalogsources are unhealthy"
   720                  condition.LastSync = timeNow()
   721                  out.Status.SetCondition(condition)
   722                  unhealthyUpdated = true
   723                  break
   724              }
   725          }
   726  
   727          // TODO: Set any other conditions relating to the CatalogSource status
   728      }
   729  
   730      if !unhealthyUpdated {
   731          // Flip SubscriptionCatalogSourcesUnhealthy to "False"
   732          condition := out.Status.GetCondition(v1alpha1.SubscriptionCatalogSourcesUnhealthy)
   733          condition.Status = corev1.ConditionFalse
   734          condition.Reason = "CatalogSourcesHealthy"
   735          condition.Message = "all catalogsources are healthy"
   736          condition.LastSync = timeNow()
   737          out.Status.SetCondition(condition)
   738          unhealthyUpdated = true
   739      }
   740  }
   741  
   742  // listResolvableCatalogSources returns a list of the CatalogSources that can be used in resolution for a Subscription in the given namespace.
   743  func (o *Operator) listResolvableCatalogSources(namespace string) ([]v1alpha1.CatalogSource, error) {
   744      // TODO: Implement this. Should be the union of CatalogSources in the given namespace and the global catalog namespace.
   745  }
   746  ```
   747  
   748  - Register new [QueueIndexer](https://github.com/operator-framework/operator-lifecycle-manager/blob/a88f5349eb80da2367b00a5191c0a7b50074f331/pkg/lib/queueinformer/queueindexer.go#L14)s with separate workqueues for handling `syncSubscriptionCatalogStatus` and `syncSubscriptionCatalogConditions` to the [catalog-operator](https://github.com/operator-framework/operator-lifecycle-manager/blob/22691a771a330fc05608a7ec1516d31a17a13ded/pkg/controller/operators/catalog/operator.go#L58). Use the same cache feeding other respective workqueues.
   749  
   750  ```go
   751  package catalog
   752  // ...
   753  type Operator struct {
   754      // ...
   755      subscriptionCatalogStatusIndexer *queueinformer.QueueIndexer
   756      subscriptionStatusIndexer *queueinformer.QueueIndexer
   757  }
   758  // ...
   759  func NewOperator(kubeconfigPath string, logger *logrus.Logger, wakeupInterval time.Duration, configmapRegistryImage, operatorNamespace string, watchedNamespaces ...string) (*Operator, error) {
   760      // ...
   761      // Register separate queue for syncing SubscriptionStatus from CatalogSource updates
   762      subCatStatusQueue := workqueue.NewNamedRateLimitingQueue(workqueue.DefaultControllerRateLimiter(), "subCatStatus")
   763      subCatQueueIndexer := queueinformer.NewQueueIndexer(subCatStatusQueue, op.catsrcIndexers, op.syncSubscriptionCatalogStatus, "subCatStatus", logger, metrics.NewMetricsNil())
   764      op.RegisterQueueIndexer(subCatQueueIndexer)
   765      op.subscriptionCatalogStatusIndexer = subCatQueueIndexer
   766      // ...
   767      // Register separate queue for syncing SubscriptionStatus
   768      subStatusQueue := workqueue.NewNamedRateLimitingQueue(workqueue.DefaultControllerRateLimiter(), "subStatus")
   769      subQueueIndexer := queueinformer.NewQueueIndexer(csvStatusQueue, op.subIndexers, op.syncSubscriptionCatalogConditions, "subStatus", logger, metrics.NewMetricsNil())
   770      op.RegisterQueueIndexer(subQueueIndexer)
   771      op.subscriptionStatusIndexer = subQueueIndexer
   772      // ...
   773  }
   774  //  ...
   775  ```