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 ```