From 12e578b1a94c44af63cdf20d62893d81eda2b295 Mon Sep 17 00:00:00 2001 From: Predrag Knezevic Date: Fri, 5 Dec 2025 19:06:55 +0100 Subject: [PATCH] Propagate `ClusterExtensionRevision` conditions to `ClusterExtension` status * `ClusterExtension`'s `Available` and `Progressing` conditions are equal to the counterparts on the latest installed active revision * When a new revision is rolling out, mirror its `Progressing` condition to its counterpart on `ClusterExtension` to improve visibility and to signal that a transition is in progress * Adds `.status.activeRevisions` field to the `ClusterExtension` to track the active revisions and mirrors `Available` and `Progressing` conditions for those not marked yet as installed This changes opens up a possibility for an observer to get insights on `ClusterExtension` update process: * When Reason on `Progressing` condition flips to `RollingOut`, an update is in progress * More then one revision could be listed under `.status.activeRevisions` during upgrade (installed + those in progress) * `Available` and `Progressing` conditions of an active, but not yet available revision, are listed under `.status.activeRevisions.conditions` so that can be easier to understand update issues The new behavior is marked as experimental behind Boxcutter feature gate. --- api/v1/clusterextension_types.go | 19 +++ api/v1/zz_generated.deepcopy.go | 29 ++++ cmd/operator-controller/main.go | 2 +- docs/api-reference/olmv1-api-reference.md | 18 +++ ...peratorframework.io_clusterextensions.yaml | 81 ++++++++++ ...peratorframework.io_clusterextensions.yaml | 3 + .../operator-controller/applier/boxcutter.go | 2 - .../controllers/boxcutter_reconcile_steps.go | 61 ++++++- .../clusterextension_controller.go | 2 + manifests/experimental-e2e.yaml | 81 ++++++++++ manifests/experimental.yaml | 81 ++++++++++ manifests/standard-e2e.yaml | 3 + manifests/standard.yaml | 3 + test/e2e/cluster_extension_revision_test.go | 55 ++++++- .../v1.0.2/manifests/bundle.configmap.yaml | 11 ++ .../olm.operatorframework.com_olme2etest.yaml | 27 ++++ .../testoperator.clusterserviceversion.yaml | 151 ++++++++++++++++++ .../manifests/testoperator.networkpolicy.yaml | 8 + .../v1.0.2/metadata/annotations.yaml | 10 ++ .../test-catalog/v1/configs/catalog.yaml | 11 ++ 20 files changed, 650 insertions(+), 8 deletions(-) create mode 100644 testdata/images/bundles/test-operator/v1.0.2/manifests/bundle.configmap.yaml create mode 100644 testdata/images/bundles/test-operator/v1.0.2/manifests/olm.operatorframework.com_olme2etest.yaml create mode 100644 testdata/images/bundles/test-operator/v1.0.2/manifests/testoperator.clusterserviceversion.yaml create mode 100644 testdata/images/bundles/test-operator/v1.0.2/manifests/testoperator.networkpolicy.yaml create mode 100644 testdata/images/bundles/test-operator/v1.0.2/metadata/annotations.yaml diff --git a/api/v1/clusterextension_types.go b/api/v1/clusterextension_types.go index 2a19fc98f3..17cccfe5a7 100644 --- a/api/v1/clusterextension_types.go +++ b/api/v1/clusterextension_types.go @@ -472,6 +472,17 @@ type BundleMetadata struct { Version string `json:"version"` } +// RevisionStatus defines the observed state of a ClusterExtensionRevision. +type RevisionStatus struct { + // name is the revision name that can be used to retrieve it. + Name string `json:"name"` + // Conditions is a set of conditions associated with the revision. + // +listType=map + // +listMapKey=type + // +optional + Conditions []metav1.Condition `json:"conditions,omitempty"` +} + // ClusterExtensionStatus defines the observed state of a ClusterExtension. type ClusterExtensionStatus struct { // The set of condition types which apply to all spec.source variations are Installed and Progressing. @@ -504,6 +515,13 @@ type ClusterExtensionStatus struct { // // +optional Install *ClusterExtensionInstallStatus `json:"install,omitempty"` + + // activeRevisions is a list of currently active ClusterExtensionRevisions. + // +listType=map + // +listMapKey=name + // +optional + // + ActiveRevisions []RevisionStatus `json:"activeRevisions,omitempty"` } // ClusterExtensionInstallStatus is a representation of the status of the identified bundle. @@ -523,6 +541,7 @@ type ClusterExtensionInstallStatus struct { // +kubebuilder:printcolumn:name="Installed Bundle",type=string,JSONPath=`.status.install.bundle.name` // +kubebuilder:printcolumn:name=Version,type=string,JSONPath=`.status.install.bundle.version` // +kubebuilder:printcolumn:name="Installed",type=string,JSONPath=`.status.conditions[?(@.type=='Installed')].status` +// +kubebuilder:printcolumn:name="Available",type=string,JSONPath=`.status.conditions[?(@.type=='Available')].status` // +kubebuilder:printcolumn:name="Progressing",type=string,JSONPath=`.status.conditions[?(@.type=='Progressing')].status` // +kubebuilder:printcolumn:name=Age,type=date,JSONPath=`.metadata.creationTimestamp` diff --git a/api/v1/zz_generated.deepcopy.go b/api/v1/zz_generated.deepcopy.go index cc27ec68fe..0c5c67c164 100644 --- a/api/v1/zz_generated.deepcopy.go +++ b/api/v1/zz_generated.deepcopy.go @@ -522,6 +522,13 @@ func (in *ClusterExtensionStatus) DeepCopyInto(out *ClusterExtensionStatus) { *out = new(ClusterExtensionInstallStatus) **out = **in } + if in.ActiveRevisions != nil { + in, out := &in.ActiveRevisions, &out.ActiveRevisions + *out = make([]RevisionStatus, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClusterExtensionStatus. @@ -609,6 +616,28 @@ func (in *ResolvedImageSource) DeepCopy() *ResolvedImageSource { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *RevisionStatus) DeepCopyInto(out *RevisionStatus) { + *out = *in + if in.Conditions != nil { + in, out := &in.Conditions, &out.Conditions + *out = make([]metav1.Condition, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RevisionStatus. +func (in *RevisionStatus) DeepCopy() *RevisionStatus { + if in == nil { + return nil + } + out := new(RevisionStatus) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ServiceAccountReference) DeepCopyInto(out *ServiceAccountReference) { *out = *in diff --git a/cmd/operator-controller/main.go b/cmd/operator-controller/main.go index c72ba60f23..a1d03feee4 100644 --- a/cmd/operator-controller/main.go +++ b/cmd/operator-controller/main.go @@ -628,7 +628,7 @@ func (c *boxcutterReconcilerConfigurator) Configure(ceReconciler *controllers.Cl controllers.RetrieveRevisionStates(revisionStatesGetter), controllers.ResolveBundle(c.resolver), controllers.UnpackBundle(c.imagePuller, c.imageCache), - controllers.ApplyBundle(appl), + controllers.ApplyBundleWithBoxcutter(appl), } baseDiscoveryClient, err := discovery.NewDiscoveryClientForConfig(c.mgr.GetConfig()) diff --git a/docs/api-reference/olmv1-api-reference.md b/docs/api-reference/olmv1-api-reference.md index 54f95eca29..fcff1a33ec 100644 --- a/docs/api-reference/olmv1-api-reference.md +++ b/docs/api-reference/olmv1-api-reference.md @@ -361,6 +361,7 @@ _Appears in:_ | --- | --- | --- | --- | | `conditions` _[Condition](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.31/#condition-v1-meta) array_ | The set of condition types which apply to all spec.source variations are Installed and Progressing.
The Installed condition represents whether or not the bundle has been installed for this ClusterExtension.
When Installed is True and the Reason is Succeeded, the bundle has been successfully installed.
When Installed is False and the Reason is Failed, the bundle has failed to install.
The Progressing condition represents whether or not the ClusterExtension is advancing towards a new state.
When Progressing is True and the Reason is Succeeded, the ClusterExtension is making progress towards a new state.
When Progressing is True and the Reason is Retrying, the ClusterExtension has encountered an error that could be resolved on subsequent reconciliation attempts.
When Progressing is False and the Reason is Blocked, the ClusterExtension has encountered an error that requires manual intervention for recovery.

When Progressing is True and Reason is RollingOut, the ClusterExtension has one or more ClusterExtensionRevisions in active roll out.

When the ClusterExtension is sourced from a catalog, if may also communicate a deprecation condition.
These are indications from a package owner to guide users away from a particular package, channel, or bundle.
BundleDeprecated is set if the requested bundle version is marked deprecated in the catalog.
ChannelDeprecated is set if the requested channel is marked deprecated in the catalog.
PackageDeprecated is set if the requested package is marked deprecated in the catalog.
Deprecated is a rollup condition that is present when any of the deprecated conditions are present. | | | | `install` _[ClusterExtensionInstallStatus](#clusterextensioninstallstatus)_ | install is a representation of the current installation status for this ClusterExtension. | | | +| `activeRevisions` _[RevisionStatus](#revisionstatus) array_ | activeRevisions is a list of currently active ClusterExtensionRevisions.
| | | @@ -435,6 +436,23 @@ _Appears in:_ | `ref` _string_ | ref contains the resolved image digest-based reference.
The digest format is used so users can use other tooling to fetch the exact
OCI manifests that were used to extract the catalog contents. | | MaxLength: 1000
Required: \{\}
| +#### RevisionStatus + + + +RevisionStatus defines the observed state of a ClusterExtensionRevision. + + + +_Appears in:_ +- [ClusterExtensionStatus](#clusterextensionstatus) + +| Field | Description | Default | Validation | +| --- | --- | --- | --- | +| `name` _string_ | name is the revision name that can be used to retrieve it. | | | +| `conditions` _[Condition](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.31/#condition-v1-meta) array_ | Conditions is a set of conditions associated with the revision. | | | + + #### ServiceAccountReference diff --git a/helm/olmv1/base/operator-controller/crd/experimental/olm.operatorframework.io_clusterextensions.yaml b/helm/olmv1/base/operator-controller/crd/experimental/olm.operatorframework.io_clusterextensions.yaml index a6738cbe38..e65d31f15a 100644 --- a/helm/olmv1/base/operator-controller/crd/experimental/olm.operatorframework.io_clusterextensions.yaml +++ b/helm/olmv1/base/operator-controller/crd/experimental/olm.operatorframework.io_clusterextensions.yaml @@ -25,6 +25,9 @@ spec: - jsonPath: .status.conditions[?(@.type=='Installed')].status name: Installed type: string + - jsonPath: .status.conditions[?(@.type=='Available')].status + name: Available + type: string - jsonPath: .status.conditions[?(@.type=='Progressing')].status name: Progressing type: string @@ -506,6 +509,84 @@ spec: description: status is an optional field that defines the observed state of the ClusterExtension. properties: + activeRevisions: + description: activeRevisions is a list of currently active ClusterExtensionRevisions. + items: + description: RevisionStatus defines the observed state of a ClusterExtensionRevision. + properties: + conditions: + description: Conditions is a set of conditions associated with + the revision. + items: + description: Condition contains details for one aspect of + the current state of this API Resource. + properties: + lastTransitionTime: + description: |- + lastTransitionTime is the last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: |- + message is a human readable message indicating details about the transition. + This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: |- + observedGeneration represents the .metadata.generation that the condition was set based upon. + For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + with respect to the current state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: |- + reason contains a programmatic identifier indicating the reason for the condition's last transition. + Producers of specific condition types may define expected values and meanings for this field, + and whether the values are considered a guaranteed API. + The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, + Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map + name: + description: name is the revision name that can be used to retrieve + it. + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map conditions: description: |- The set of condition types which apply to all spec.source variations are Installed and Progressing. diff --git a/helm/olmv1/base/operator-controller/crd/standard/olm.operatorframework.io_clusterextensions.yaml b/helm/olmv1/base/operator-controller/crd/standard/olm.operatorframework.io_clusterextensions.yaml index a0983e41f9..f2930fd0ff 100644 --- a/helm/olmv1/base/operator-controller/crd/standard/olm.operatorframework.io_clusterextensions.yaml +++ b/helm/olmv1/base/operator-controller/crd/standard/olm.operatorframework.io_clusterextensions.yaml @@ -25,6 +25,9 @@ spec: - jsonPath: .status.conditions[?(@.type=='Installed')].status name: Installed type: string + - jsonPath: .status.conditions[?(@.type=='Available')].status + name: Available + type: string - jsonPath: .status.conditions[?(@.type=='Progressing')].status name: Progressing type: string diff --git a/internal/operator-controller/applier/boxcutter.go b/internal/operator-controller/applier/boxcutter.go index 5a11bfe656..c69ab5a1a0 100644 --- a/internal/operator-controller/applier/boxcutter.go +++ b/internal/operator-controller/applier/boxcutter.go @@ -399,8 +399,6 @@ func (bc *Boxcutter) apply(ctx context.Context, contentFS fs.FS, ext *ocv1.Clust default: return false, progressingCondition.Message, nil } - } else if availableCondition != nil && availableCondition.Status != metav1.ConditionTrue { - return false, "", errors.New(availableCondition.Message) } else if succeededCondition != nil && succeededCondition.Status != metav1.ConditionTrue { return false, succeededCondition.Message, nil } diff --git a/internal/operator-controller/controllers/boxcutter_reconcile_steps.go b/internal/operator-controller/controllers/boxcutter_reconcile_steps.go index 6c6a27451d..dbed7942c3 100644 --- a/internal/operator-controller/controllers/boxcutter_reconcile_steps.go +++ b/internal/operator-controller/controllers/boxcutter_reconcile_steps.go @@ -25,6 +25,7 @@ import ( apimeta "k8s.io/apimachinery/pkg/api/meta" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/log" ocv1 "github.com/operator-framework/operator-controller/api/v1" "github.com/operator-framework/operator-controller/internal/operator-controller/labels" @@ -54,12 +55,14 @@ func (d *BoxcutterRevisionStatesGetter) GetRevisionStates(ctx context.Context, e continue } - // TODO: the setting of these annotations (happens in boxcutter applier when we pass in "revisionAnnotations") + // TODO: the setting of these annotations (happens in boxcutter applier when we pass in "storageLabels") // is fairly decoupled from this code where we get the annotations back out. We may want to co-locate // the set/get logic a bit better to make it more maintainable and less likely to get out of sync. rm := &RevisionMetadata{ - Package: rev.Annotations[labels.PackageNameKey], - Image: rev.Annotations[labels.BundleReferenceKey], + RevName: rev.Name, + Package: rev.Labels[labels.PackageNameKey], + Image: rev.Annotations[labels.BundleReferenceKey], + Conditions: rev.Status.Conditions, BundleMetadata: ocv1.BundleMetadata{ Name: rev.Annotations[labels.BundleNameKey], Version: rev.Annotations[labels.BundleVersionKey], @@ -89,3 +92,55 @@ func MigrateStorage(m StorageMigrator) ReconcileStepFunc { return nil, nil } } + +func ApplyBundleWithBoxcutter(a Applier) ReconcileStepFunc { + return func(ctx context.Context, state *reconcileState, ext *ocv1.ClusterExtension) (*ctrl.Result, error) { + l := log.FromContext(ctx) + storeLbls := map[string]string{ + labels.BundleNameKey: state.resolvedRevisionMetadata.Name, + labels.PackageNameKey: state.resolvedRevisionMetadata.Package, + labels.BundleVersionKey: state.resolvedRevisionMetadata.Version, + labels.BundleReferenceKey: state.resolvedRevisionMetadata.Image, + } + objLbls := map[string]string{ + labels.OwnerKindKey: ocv1.ClusterExtensionKind, + labels.OwnerNameKey: ext.GetName(), + } + + l.Info("applying bundle contents") + if _, _, err := a.Apply(ctx, state.imageFS, ext, objLbls, storeLbls); err != nil { + // If there was an error applying the resolved bundle, + // report the error via the Progressing condition. + setStatusProgressing(ext, wrapErrorWithResolutionInfo(state.resolvedRevisionMetadata.BundleMetadata, err)) + return nil, err + } + + // Mirror Available/Progressing conditions from the installed revision + if i := state.revisionStates.Installed; i != nil { + for _, cndType := range []string{ocv1.ClusterExtensionRevisionTypeAvailable, ocv1.ClusterExtensionRevisionTypeProgressing} { + if cnd := apimeta.FindStatusCondition(i.Conditions, cndType); cnd != nil { + cnd.ObservedGeneration = ext.GetGeneration() + apimeta.SetStatusCondition(&ext.Status.Conditions, *cnd) + } + } + ext.Status.Install = &ocv1.ClusterExtensionInstallStatus{ + Bundle: i.BundleMetadata, + } + ext.Status.ActiveRevisions = []ocv1.RevisionStatus{{Name: i.RevName}} + } + for idx, r := range state.revisionStates.RollingOut { + rs := ocv1.RevisionStatus{Name: r.RevName, Conditions: r.Conditions} + // Mirror Progressing condition from the latest active revision + if idx == len(state.revisionStates.RollingOut)-1 { + if pcnd := apimeta.FindStatusCondition(r.Conditions, ocv1.ClusterExtensionRevisionTypeProgressing); pcnd != nil { + pcnd.ObservedGeneration = ext.GetGeneration() + apimeta.SetStatusCondition(&ext.Status.Conditions, *pcnd) + } + } + ext.Status.ActiveRevisions = append(ext.Status.ActiveRevisions, rs) + } + + setInstalledStatusFromRevisionStates(ext, state.revisionStates) + return nil, nil + } +} diff --git a/internal/operator-controller/controllers/clusterextension_controller.go b/internal/operator-controller/controllers/clusterextension_controller.go index 2b2f0d532b..84e685e2da 100644 --- a/internal/operator-controller/controllers/clusterextension_controller.go +++ b/internal/operator-controller/controllers/clusterextension_controller.go @@ -332,9 +332,11 @@ func clusterExtensionRequestsForCatalog(c client.Reader, logger logr.Logger) crh } type RevisionMetadata struct { + RevName string Package string Image string ocv1.BundleMetadata + Conditions []metav1.Condition } type RevisionStates struct { diff --git a/manifests/experimental-e2e.yaml b/manifests/experimental-e2e.yaml index 47a36152c5..6cbf2ecbd9 100644 --- a/manifests/experimental-e2e.yaml +++ b/manifests/experimental-e2e.yaml @@ -925,6 +925,9 @@ spec: - jsonPath: .status.conditions[?(@.type=='Installed')].status name: Installed type: string + - jsonPath: .status.conditions[?(@.type=='Available')].status + name: Available + type: string - jsonPath: .status.conditions[?(@.type=='Progressing')].status name: Progressing type: string @@ -1406,6 +1409,84 @@ spec: description: status is an optional field that defines the observed state of the ClusterExtension. properties: + activeRevisions: + description: activeRevisions is a list of currently active ClusterExtensionRevisions. + items: + description: RevisionStatus defines the observed state of a ClusterExtensionRevision. + properties: + conditions: + description: Conditions is a set of conditions associated with + the revision. + items: + description: Condition contains details for one aspect of + the current state of this API Resource. + properties: + lastTransitionTime: + description: |- + lastTransitionTime is the last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: |- + message is a human readable message indicating details about the transition. + This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: |- + observedGeneration represents the .metadata.generation that the condition was set based upon. + For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + with respect to the current state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: |- + reason contains a programmatic identifier indicating the reason for the condition's last transition. + Producers of specific condition types may define expected values and meanings for this field, + and whether the values are considered a guaranteed API. + The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, + Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map + name: + description: name is the revision name that can be used to retrieve + it. + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map conditions: description: |- The set of condition types which apply to all spec.source variations are Installed and Progressing. diff --git a/manifests/experimental.yaml b/manifests/experimental.yaml index a6b68fc0a4..87aaf61922 100644 --- a/manifests/experimental.yaml +++ b/manifests/experimental.yaml @@ -890,6 +890,9 @@ spec: - jsonPath: .status.conditions[?(@.type=='Installed')].status name: Installed type: string + - jsonPath: .status.conditions[?(@.type=='Available')].status + name: Available + type: string - jsonPath: .status.conditions[?(@.type=='Progressing')].status name: Progressing type: string @@ -1371,6 +1374,84 @@ spec: description: status is an optional field that defines the observed state of the ClusterExtension. properties: + activeRevisions: + description: activeRevisions is a list of currently active ClusterExtensionRevisions. + items: + description: RevisionStatus defines the observed state of a ClusterExtensionRevision. + properties: + conditions: + description: Conditions is a set of conditions associated with + the revision. + items: + description: Condition contains details for one aspect of + the current state of this API Resource. + properties: + lastTransitionTime: + description: |- + lastTransitionTime is the last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: |- + message is a human readable message indicating details about the transition. + This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: |- + observedGeneration represents the .metadata.generation that the condition was set based upon. + For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + with respect to the current state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: |- + reason contains a programmatic identifier indicating the reason for the condition's last transition. + Producers of specific condition types may define expected values and meanings for this field, + and whether the values are considered a guaranteed API. + The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, + Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map + name: + description: name is the revision name that can be used to retrieve + it. + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map conditions: description: |- The set of condition types which apply to all spec.source variations are Installed and Progressing. diff --git a/manifests/standard-e2e.yaml b/manifests/standard-e2e.yaml index 1aed38ba96..c1d04b766b 100644 --- a/manifests/standard-e2e.yaml +++ b/manifests/standard-e2e.yaml @@ -650,6 +650,9 @@ spec: - jsonPath: .status.conditions[?(@.type=='Installed')].status name: Installed type: string + - jsonPath: .status.conditions[?(@.type=='Available')].status + name: Available + type: string - jsonPath: .status.conditions[?(@.type=='Progressing')].status name: Progressing type: string diff --git a/manifests/standard.yaml b/manifests/standard.yaml index 34cc579181..fd1a279b6c 100644 --- a/manifests/standard.yaml +++ b/manifests/standard.yaml @@ -615,6 +615,9 @@ spec: - jsonPath: .status.conditions[?(@.type=='Installed')].status name: Installed type: string + - jsonPath: .status.conditions[?(@.type=='Available')].status + name: Available + type: string - jsonPath: .status.conditions[?(@.type=='Progressing')].status name: Progressing type: string diff --git a/test/e2e/cluster_extension_revision_test.go b/test/e2e/cluster_extension_revision_test.go index 322b6fd211..b24e763534 100644 --- a/test/e2e/cluster_extension_revision_test.go +++ b/test/e2e/cluster_extension_revision_test.go @@ -37,8 +37,9 @@ func TestClusterExtensionRevision(t *testing.T) { Source: ocv1.SourceConfig{ SourceType: "Catalog", Catalog: &ocv1.CatalogFilter{ - PackageName: "test", - Version: "1.0.1", + PackageName: "test", + Version: "1.0.1", + UpgradeConstraintPolicy: ocv1.UpgradeConstraintPolicySelfCertified, Selector: &metav1.LabelSelector{ MatchLabels: map[string]string{"olm.operatorframework.io/metadata.name": extensionCatalog.Name}, }, @@ -91,6 +92,9 @@ func TestClusterExtensionRevision(t *testing.T) { require.Equal(ct, ocv1.ReasonSucceeded, cond.Reason) require.Contains(ct, cond.Message, "Installed bundle") require.NotEmpty(ct, clusterExtension.Status.Install.Bundle) + require.Len(ct, clusterExtension.Status.ActiveRevisions, 1) + require.Equal(ct, clusterExtension.Status.ActiveRevisions[0].Name, clusterExtensionRevision.Name) + require.Empty(ct, clusterExtension.Status.ActiveRevisions[0].Conditions) }, pollDuration, pollInterval) t.Log("Check Deployment Availability Probe") @@ -112,6 +116,14 @@ func TestClusterExtensionRevision(t *testing.T) { require.Equal(ct, ocv1.ClusterExtensionRevisionReasonProbeFailure, cond.Reason) }, pollDuration, pollInterval) + t.Log("By propagating Available:False to ClusterExtension") + require.EventuallyWithT(t, func(ct *assert.CollectT) { + require.NoError(ct, c.Get(context.Background(), types.NamespacedName{Name: clusterExtension.Name}, clusterExtension)) + cond := apimeta.FindStatusCondition(clusterExtensionRevision.Status.Conditions, ocv1.ClusterExtensionRevisionTypeAvailable) + require.NotNil(ct, cond) + require.Equal(ct, metav1.ConditionFalse, cond.Status) + }, pollDuration, pollInterval) + t.Log("By making the operator pod ready") podName = getPodName(t, clusterExtension.Spec.Namespace, client.MatchingLabels{"app": "olme2etest"}) podExec(t, clusterExtension.Spec.Namespace, podName, []string{"touch", "/var/www/ready"}) @@ -130,6 +142,14 @@ func TestClusterExtensionRevision(t *testing.T) { require.Equal(ct, ocv1.ClusterExtensionRevisionReasonProbesSucceeded, cond.Reason) }, pollDuration, pollInterval) + t.Log("By propagating Available:True to ClusterExtension") + require.EventuallyWithT(t, func(ct *assert.CollectT) { + require.NoError(ct, c.Get(context.Background(), types.NamespacedName{Name: clusterExtension.Name}, clusterExtension)) + cond := apimeta.FindStatusCondition(clusterExtensionRevision.Status.Conditions, ocv1.ClusterExtensionRevisionTypeAvailable) + require.NotNil(ct, cond) + require.Equal(ct, metav1.ConditionTrue, cond.Status) + }, pollDuration, pollInterval) + t.Log("Check archiving") t.Log("By upgrading the cluster extension to v1.2.0") require.EventuallyWithT(t, func(ct *assert.CollectT) { @@ -181,6 +201,37 @@ func TestClusterExtensionRevision(t *testing.T) { require.Equal(ct, metav1.ConditionUnknown, cond.Status) require.Equal(ct, ocv1.ClusterExtensionRevisionReasonArchived, cond.Reason) }, pollDuration, pollInterval) + + t.Log("By upgrading the cluster extension to v1.0.2 containing bad image reference") + require.EventuallyWithT(t, func(ct *assert.CollectT) { + require.NoError(ct, c.Get(context.Background(), types.NamespacedName{Name: clusterExtension.Name}, clusterExtension)) + clusterExtension.Spec.Source.Catalog.Version = "1.0.2" + require.NoError(t, c.Update(context.Background(), clusterExtension)) + }, pollDuration, pollInterval) + + t.Log("By revision-3 eventually reporting Progressing:True:Succeeded and Available:False:ProbeFailure conditions") + require.EventuallyWithT(t, func(ct *assert.CollectT) { + require.NoError(ct, c.Get(context.Background(), types.NamespacedName{Name: fmt.Sprintf("%s-3", clusterExtension.Name)}, &clusterExtensionRevision)) + cond := apimeta.FindStatusCondition(clusterExtensionRevision.Status.Conditions, ocv1.ClusterExtensionRevisionTypeProgressing) + require.NotNil(ct, cond) + require.Equal(ct, metav1.ConditionTrue, cond.Status) + require.Equal(ct, ocv1.ReasonSucceeded, cond.Reason) + + cond = apimeta.FindStatusCondition(clusterExtensionRevision.Status.Conditions, ocv1.ClusterExtensionRevisionTypeAvailable) + require.NotNil(ct, cond) + require.Equal(ct, metav1.ConditionFalse, cond.Status) + require.Equal(ct, ocv1.ClusterExtensionRevisionReasonProbeFailure, cond.Reason) + }, pollDuration, pollInterval) + + t.Log("By eventually reporting more than one active revision") + require.EventuallyWithT(t, func(ct *assert.CollectT) { + require.NoError(ct, c.Get(context.Background(), types.NamespacedName{Name: clusterExtension.Name}, clusterExtension)) + require.Len(ct, clusterExtension.Status.ActiveRevisions, 2) + require.Equal(ct, clusterExtension.Status.ActiveRevisions[0].Name, fmt.Sprintf("%s-2", clusterExtension.Name)) + require.Equal(ct, clusterExtension.Status.ActiveRevisions[1].Name, fmt.Sprintf("%s-3", clusterExtension.Name)) + require.Empty(ct, clusterExtension.Status.ActiveRevisions[0].Conditions) + require.NotEmpty(ct, clusterExtension.Status.ActiveRevisions[1].Conditions) + }, pollDuration, pollInterval) } func getPodName(t *testing.T, podNamespace string, matchingLabels client.MatchingLabels) string { diff --git a/testdata/images/bundles/test-operator/v1.0.2/manifests/bundle.configmap.yaml b/testdata/images/bundles/test-operator/v1.0.2/manifests/bundle.configmap.yaml new file mode 100644 index 0000000000..0279603bfc --- /dev/null +++ b/testdata/images/bundles/test-operator/v1.0.2/manifests/bundle.configmap.yaml @@ -0,0 +1,11 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: test-configmap + annotations: + shouldNotTemplate: > + The namespace is {{ $labels.namespace }}. The templated $labels.namespace is NOT expected to be processed by OLM's rendering engine for registry+v1 bundles. + +data: + version: "v1.0.2" + name: "test-configmap" diff --git a/testdata/images/bundles/test-operator/v1.0.2/manifests/olm.operatorframework.com_olme2etest.yaml b/testdata/images/bundles/test-operator/v1.0.2/manifests/olm.operatorframework.com_olme2etest.yaml new file mode 100644 index 0000000000..44e64cef79 --- /dev/null +++ b/testdata/images/bundles/test-operator/v1.0.2/manifests/olm.operatorframework.com_olme2etest.yaml @@ -0,0 +1,27 @@ +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.16.1 + name: olme2etests.olm.operatorframework.io +spec: + group: olm.operatorframework.io + names: + kind: OLME2ETest + listKind: OLME2ETestList + plural: olme2etests + singular: olme2etest + scope: Cluster + versions: + - name: v1 + served: true + storage: true + schema: + openAPIV3Schema: + type: object + properties: + spec: + type: object + properties: + testField: + type: string diff --git a/testdata/images/bundles/test-operator/v1.0.2/manifests/testoperator.clusterserviceversion.yaml b/testdata/images/bundles/test-operator/v1.0.2/manifests/testoperator.clusterserviceversion.yaml new file mode 100644 index 0000000000..f39fd69f5f --- /dev/null +++ b/testdata/images/bundles/test-operator/v1.0.2/manifests/testoperator.clusterserviceversion.yaml @@ -0,0 +1,151 @@ +apiVersion: operators.coreos.com/v1alpha1 +kind: ClusterServiceVersion +metadata: + annotations: + alm-examples: |- + [ + { + "apiVersion": "olme2etests.olm.operatorframework.io/v1", + "kind": "OLME2ETests", + "metadata": { + "labels": { + "app.kubernetes.io/managed-by": "kustomize", + "app.kubernetes.io/name": "test" + }, + "name": "test-sample" + }, + "spec": null + } + ] + capabilities: Basic Install + createdAt: "2024-10-24T19:21:40Z" + operators.operatorframework.io/builder: operator-sdk-v1.34.1 + operators.operatorframework.io/project_layout: go.kubebuilder.io/v4 + name: testoperator.v1.0.2 + namespace: placeholder +spec: + apiservicedefinitions: {} + customresourcedefinitions: + owned: + - description: Configures subsections of Alertmanager configuration specific to each namespace + displayName: OLME2ETest + kind: OLME2ETest + name: olme2etests.olm.operatorframework.io + version: v1 + description: OLM E2E Testing Operator with a wrong image ref + displayName: test-operator + icon: + - base64data: "" + mediatype: "" + install: + spec: + deployments: + - label: + app.kubernetes.io/component: controller + app.kubernetes.io/name: test-operator + app.kubernetes.io/version: 1.0.2 + name: test-operator + spec: + replicas: 1 + selector: + matchLabels: + app: olme2etest + template: + metadata: + labels: + app: olme2etest + spec: + terminationGracePeriodSeconds: 0 + volumes: + - name: scripts + configMap: + name: httpd-script + defaultMode: 0755 + containers: + - name: busybox-httpd-container + # This image ref is wrong and should trigger ImagePullBackOff condition + image: wrong/image + serviceAccountName: simple-bundle-manager + clusterPermissions: + - rules: + - apiGroups: + - authentication.k8s.io + resources: + - tokenreviews + verbs: + - create + - apiGroups: + - authorization.k8s.io + resources: + - subjectaccessreviews + verbs: + - create + serviceAccountName: simple-bundle-manager + permissions: + - rules: + - apiGroups: + - "" + resources: + - configmaps + - serviceaccounts + verbs: + - get + - list + - watch + - create + - update + - patch + - delete + - apiGroups: + - networking.k8s.io + resources: + - networkpolicies + verbs: + - get + - list + - create + - update + - delete + - apiGroups: + - coordination.k8s.io + resources: + - leases + verbs: + - get + - list + - watch + - create + - update + - patch + - delete + - apiGroups: + - "" + resources: + - events + verbs: + - create + - patch + serviceAccountName: simple-bundle-manager + strategy: deployment + installModes: + - supported: false + type: OwnNamespace + - supported: true + type: SingleNamespace + - supported: false + type: MultiNamespace + - supported: true + type: AllNamespaces + keywords: + - registry + links: + - name: simple-bundle + url: https://simple-bundle.domain + maintainers: + - email: main#simple-bundle.domain + name: Simple Bundle + maturity: beta + provider: + name: Simple Bundle + url: https://simple-bundle.domain + version: 1.0.2 diff --git a/testdata/images/bundles/test-operator/v1.0.2/manifests/testoperator.networkpolicy.yaml b/testdata/images/bundles/test-operator/v1.0.2/manifests/testoperator.networkpolicy.yaml new file mode 100644 index 0000000000..20a5ea834f --- /dev/null +++ b/testdata/images/bundles/test-operator/v1.0.2/manifests/testoperator.networkpolicy.yaml @@ -0,0 +1,8 @@ +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: test-operator-network-policy +spec: + podSelector: {} + policyTypes: + - Ingress diff --git a/testdata/images/bundles/test-operator/v1.0.2/metadata/annotations.yaml b/testdata/images/bundles/test-operator/v1.0.2/metadata/annotations.yaml new file mode 100644 index 0000000000..404f0f4a34 --- /dev/null +++ b/testdata/images/bundles/test-operator/v1.0.2/metadata/annotations.yaml @@ -0,0 +1,10 @@ +annotations: + # Core bundle annotations. + operators.operatorframework.io.bundle.mediatype.v1: registry+v1 + operators.operatorframework.io.bundle.manifests.v1: manifests/ + operators.operatorframework.io.bundle.metadata.v1: metadata/ + operators.operatorframework.io.bundle.package.v1: test + operators.operatorframework.io.bundle.channels.v1: beta + operators.operatorframework.io.metrics.builder: operator-sdk-v1.28.0 + operators.operatorframework.io.metrics.mediatype.v1: metrics+v1 + operators.operatorframework.io.metrics.project_layout: unknown diff --git a/testdata/images/catalogs/test-catalog/v1/configs/catalog.yaml b/testdata/images/catalogs/test-catalog/v1/configs/catalog.yaml index 437175a4e3..49340a2f96 100644 --- a/testdata/images/catalogs/test-catalog/v1/configs/catalog.yaml +++ b/testdata/images/catalogs/test-catalog/v1/configs/catalog.yaml @@ -7,6 +7,7 @@ name: alpha package: test entries: - name: test-operator.1.0.0 + - name: test-operator.1.0.2 --- schema: olm.channel name: beta @@ -39,6 +40,16 @@ properties: version: 1.0.1 --- schema: olm.bundle +name: test-operator.1.0.2 +package: test +image: docker-registry.operator-controller-e2e.svc.cluster.local:5000/bundles/registry-v1/test-operator:v1.0.2 +properties: + - type: olm.package + value: + packageName: test + version: 1.0.2 +--- +schema: olm.bundle name: test-operator.1.2.0 package: test image: docker-registry.operator-controller-e2e.svc.cluster.local:5000/bundles/registry-v1/test-operator:v1.2.0