From 997ff47c5b41f880a31d4b1acdf32c3db75117ca Mon Sep 17 00:00:00 2001 From: Per Goncalves da Silva Date: Fri, 10 Oct 2025 15:28:13 +0200 Subject: [PATCH 1/4] Add AVAILABLE print column to ClusterExtensionRevision Signed-off-by: Per Goncalves da Silva --- api/v1/clusterextensionrevision_types.go | 213 ++++++++++++----------- 1 file changed, 108 insertions(+), 105 deletions(-) diff --git a/api/v1/clusterextensionrevision_types.go b/api/v1/clusterextensionrevision_types.go index b94abd107..f861e15e2 100644 --- a/api/v1/clusterextensionrevision_types.go +++ b/api/v1/clusterextensionrevision_types.go @@ -17,130 +17,131 @@ limitations under the License. package v1 import ( - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - "k8s.io/apimachinery/pkg/types" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/types" ) const ( - ClusterExtensionRevisionKind = "ClusterExtensionRevision" - - // Condition Types - ClusterExtensionRevisionTypeAvailable = "Available" - ClusterExtensionRevisionTypeSucceeded = "Succeeded" - - // Condition Reasons - ClusterExtensionRevisionReasonAvailable = "Available" - ClusterExtensionRevisionReasonReconcileFailure = "ReconcileFailure" - ClusterExtensionRevisionReasonRevisionValidationFailure = "RevisionValidationFailure" - ClusterExtensionRevisionReasonPhaseValidationError = "PhaseValidationError" - ClusterExtensionRevisionReasonObjectCollisions = "ObjectCollisions" - ClusterExtensionRevisionReasonRolloutSuccess = "RolloutSuccess" - ClusterExtensionRevisionReasonProbeFailure = "ProbeFailure" - ClusterExtensionRevisionReasonIncomplete = "Incomplete" - ClusterExtensionRevisionReasonProgressing = "Progressing" + ClusterExtensionRevisionKind = "ClusterExtensionRevision" + + // Condition Types + ClusterExtensionRevisionTypeAvailable = "Available" + ClusterExtensionRevisionTypeSucceeded = "Succeeded" + + // Condition Reasons + ClusterExtensionRevisionReasonAvailable = "Available" + ClusterExtensionRevisionReasonReconcileFailure = "ReconcileFailure" + ClusterExtensionRevisionReasonRevisionValidationFailure = "RevisionValidationFailure" + ClusterExtensionRevisionReasonPhaseValidationError = "PhaseValidationError" + ClusterExtensionRevisionReasonObjectCollisions = "ObjectCollisions" + ClusterExtensionRevisionReasonRolloutSuccess = "RolloutSuccess" + ClusterExtensionRevisionReasonProbeFailure = "ProbeFailure" + ClusterExtensionRevisionReasonIncomplete = "Incomplete" + ClusterExtensionRevisionReasonProgressing = "Progressing" + ClusterExtensionRevisionReasonArchived = "Archived" ) // ClusterExtensionRevisionSpec defines the desired state of ClusterExtensionRevision. type ClusterExtensionRevisionSpec struct { - // Specifies the lifecycle state of the ClusterExtensionRevision. - // - // +kubebuilder:default="Active" - // +kubebuilder:validation:Enum=Active;Paused;Archived - // +kubebuilder:validation:XValidation:rule="oldSelf == 'Active' || oldSelf == 'Paused' || oldSelf == 'Archived' && oldSelf == self", message="can not un-archive" - LifecycleState ClusterExtensionRevisionLifecycleState `json:"lifecycleState,omitempty"` - // Revision is a sequence number representing a specific revision of the ClusterExtension instance. - // Must be positive. Each ClusterExtensionRevision of the same parent ClusterExtension needs to have - // a unique value assigned. It is immutable after creation. The new revision number must always be previous revision +1. - // - // +kubebuilder:validation:Required - // +kubebuilder:validation:Minimum:=1 - // +kubebuilder:validation:XValidation:rule="self == oldSelf", message="revision is immutable" - Revision int64 `json:"revision"` - // Phases are groups of objects that will be applied at the same time. - // All objects in the phase will have to pass their probes in order to progress to the next phase. - // - // +kubebuilder:validation:XValidation:rule="self == oldSelf || oldSelf.size() == 0", message="phases is immutable" - // +listType=map - // +listMapKey=name - // +optional - Phases []ClusterExtensionRevisionPhase `json:"phases,omitempty"` - // Previous references previous revisions that objects can be adopted from. - // - // +kubebuilder:validation:XValidation:rule="self == oldSelf", message="previous is immutable" - Previous []ClusterExtensionRevisionPrevious `json:"previous,omitempty"` + // Specifies the lifecycle state of the ClusterExtensionRevision. + // + // +kubebuilder:default="Active" + // +kubebuilder:validation:Enum=Active;Paused;Archived + // +kubebuilder:validation:XValidation:rule="oldSelf == 'Active' || oldSelf == 'Paused' || oldSelf == 'Archived' && oldSelf == self", message="can not un-archive" + LifecycleState ClusterExtensionRevisionLifecycleState `json:"lifecycleState,omitempty"` + // Revision is a sequence number representing a specific revision of the ClusterExtension instance. + // Must be positive. Each ClusterExtensionRevision of the same parent ClusterExtension needs to have + // a unique value assigned. It is immutable after creation. The new revision number must always be previous revision +1. + // + // +kubebuilder:validation:Required + // +kubebuilder:validation:Minimum:=1 + // +kubebuilder:validation:XValidation:rule="self == oldSelf", message="revision is immutable" + Revision int64 `json:"revision"` + // Phases are groups of objects that will be applied at the same time. + // All objects in the phase will have to pass their probes in order to progress to the next phase. + // + // +kubebuilder:validation:XValidation:rule="self == oldSelf || oldSelf.size() == 0", message="phases is immutable" + // +listType=map + // +listMapKey=name + // +optional + Phases []ClusterExtensionRevisionPhase `json:"phases,omitempty"` + // Previous references previous revisions that objects can be adopted from. + // + // +kubebuilder:validation:XValidation:rule="self == oldSelf", message="previous is immutable" + Previous []ClusterExtensionRevisionPrevious `json:"previous,omitempty"` } // ClusterExtensionRevisionLifecycleState specifies the lifecycle state of the ClusterExtensionRevision. type ClusterExtensionRevisionLifecycleState string const ( - // ClusterExtensionRevisionLifecycleStateActive / "Active" is the default lifecycle state. - ClusterExtensionRevisionLifecycleStateActive ClusterExtensionRevisionLifecycleState = "Active" - // ClusterExtensionRevisionLifecycleStatePaused / "Paused" disables reconciliation of the ClusterExtensionRevision. - // Only Status updates will still propagated, but object changes will not be reconciled. - ClusterExtensionRevisionLifecycleStatePaused ClusterExtensionRevisionLifecycleState = "Paused" - // ClusterExtensionRevisionLifecycleStateArchived / "Archived" disables reconciliation while also "scaling to zero", - // which deletes all objects that are not excluded via the pausedFor property and - // removes itself from the owner list of all other objects previously under management. - ClusterExtensionRevisionLifecycleStateArchived ClusterExtensionRevisionLifecycleState = "Archived" + // ClusterExtensionRevisionLifecycleStateActive / "Active" is the default lifecycle state. + ClusterExtensionRevisionLifecycleStateActive ClusterExtensionRevisionLifecycleState = "Active" + // ClusterExtensionRevisionLifecycleStatePaused / "Paused" disables reconciliation of the ClusterExtensionRevision. + // Only Status updates will still propagated, but object changes will not be reconciled. + ClusterExtensionRevisionLifecycleStatePaused ClusterExtensionRevisionLifecycleState = "Paused" + // ClusterExtensionRevisionLifecycleStateArchived / "Archived" disables reconciliation while also "scaling to zero", + // which deletes all objects that are not excluded via the pausedFor property and + // removes itself from the owner list of all other objects previously under management. + ClusterExtensionRevisionLifecycleStateArchived ClusterExtensionRevisionLifecycleState = "Archived" ) // ClusterExtensionRevisionPhase are groups of objects that will be applied at the same time. // All objects in the a phase will have to pass their probes in order to progress to the next phase. type ClusterExtensionRevisionPhase struct { - // Name identifies this phase. - // - // +kubebuilder:validation:MaxLength=63 - // +kubebuilder:validation:Pattern=`^[a-z]([-a-z0-9]*[a-z0-9])?$` - Name string `json:"name"` - // Objects are a list of all the objects within this phase. - Objects []ClusterExtensionRevisionObject `json:"objects"` + // Name identifies this phase. + // + // +kubebuilder:validation:MaxLength=63 + // +kubebuilder:validation:Pattern=`^[a-z]([-a-z0-9]*[a-z0-9])?$` + Name string `json:"name"` + // Objects are a list of all the objects within this phase. + Objects []ClusterExtensionRevisionObject `json:"objects"` } // ClusterExtensionRevisionObject contains an object and settings for it. type ClusterExtensionRevisionObject struct { - // +kubebuilder:validation:EmbeddedResource - // +kubebuilder:pruning:PreserveUnknownFields - Object unstructured.Unstructured `json:"object"` - // CollisionProtection controls whether OLM can adopt and modify objects - // already existing on the cluster or even owned by another controller. - // - // +kubebuilder:default="Prevent" - // +optional - CollisionProtection CollisionProtection `json:"collisionProtection,omitempty"` + // +kubebuilder:validation:EmbeddedResource + // +kubebuilder:pruning:PreserveUnknownFields + Object unstructured.Unstructured `json:"object"` + // CollisionProtection controls whether OLM can adopt and modify objects + // already existing on the cluster or even owned by another controller. + // + // +kubebuilder:default="Prevent" + // +optional + CollisionProtection CollisionProtection `json:"collisionProtection,omitempty"` } // CollisionProtection specifies if and how ownership collisions are prevented. type CollisionProtection string const ( - // CollisionProtectionPrevent prevents owner collisions entirely - // by only allowing to work with objects itself has created. - CollisionProtectionPrevent CollisionProtection = "Prevent" - // CollisionProtectionIfNoController allows to patch and override - // objects already present if they are not owned by another controller. - CollisionProtectionIfNoController CollisionProtection = "IfNoController" - // CollisionProtectionNone allows to patch and override objects - // already present and owned by other controllers. - // Be careful! This setting may cause multiple controllers to fight over a resource, - // causing load on the API server and etcd. - CollisionProtectionNone CollisionProtection = "None" + // CollisionProtectionPrevent prevents owner collisions entirely + // by only allowing to work with objects itself has created. + CollisionProtectionPrevent CollisionProtection = "Prevent" + // CollisionProtectionIfNoController allows to patch and override + // objects already present if they are not owned by another controller. + CollisionProtectionIfNoController CollisionProtection = "IfNoController" + // CollisionProtectionNone allows to patch and override objects + // already present and owned by other controllers. + // Be careful! This setting may cause multiple controllers to fight over a resource, + // causing load on the API server and etcd. + CollisionProtectionNone CollisionProtection = "None" ) type ClusterExtensionRevisionPrevious struct { - // +kubebuilder:validation:Required - Name string `json:"name"` - // +kubebuilder:validation:Required - UID types.UID `json:"uid"` + // +kubebuilder:validation:Required + Name string `json:"name"` + // +kubebuilder:validation:Required + UID types.UID `json:"uid"` } // ClusterExtensionRevisionStatus defines the observed state of a ClusterExtensionRevision. type ClusterExtensionRevisionStatus struct { - // +listType=map - // +listMapKey=type - // +optional - Conditions []metav1.Condition `json:"conditions,omitempty"` + // +listType=map + // +listMapKey=type + // +optional + Conditions []metav1.Condition `json:"conditions,omitempty"` } // +kubebuilder:object:root=true @@ -148,34 +149,36 @@ type ClusterExtensionRevisionStatus struct { // +kubebuilder:subresource:status // ClusterExtensionRevision is the Schema for the clusterextensionrevisions API +// +kubebuilder:printcolumn:name="Available",type=string,JSONPath=`.status.conditions[?(@.type=='Available')].status` +// +kubebuilder:printcolumn:name=Age,type=date,JSONPath=`.metadata.creationTimestamp` type ClusterExtensionRevision struct { - metav1.TypeMeta `json:",inline"` - metav1.ObjectMeta `json:"metadata,omitempty"` + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` - // spec is an optional field that defines the desired state of the ClusterExtension. - // +optional - Spec ClusterExtensionRevisionSpec `json:"spec,omitempty"` + // spec is an optional field that defines the desired state of the ClusterExtension. + // +optional + Spec ClusterExtensionRevisionSpec `json:"spec,omitempty"` - // status is an optional field that defines the observed state of the ClusterExtension. - // +optional - Status ClusterExtensionRevisionStatus `json:"status,omitempty"` + // status is an optional field that defines the observed state of the ClusterExtension. + // +optional + Status ClusterExtensionRevisionStatus `json:"status,omitempty"` } // +kubebuilder:object:root=true // ClusterExtensionRevisionList contains a list of ClusterExtensionRevision type ClusterExtensionRevisionList struct { - metav1.TypeMeta `json:",inline"` + metav1.TypeMeta `json:",inline"` - // +optional - metav1.ListMeta `json:"metadata,omitempty"` + // +optional + metav1.ListMeta `json:"metadata,omitempty"` - // items is a required list of ClusterExtensionRevision objects. - // - // +kubebuilder:validation:Required - Items []ClusterExtensionRevision `json:"items"` + // items is a required list of ClusterExtensionRevision objects. + // + // +kubebuilder:validation:Required + Items []ClusterExtensionRevision `json:"items"` } func init() { - SchemeBuilder.Register(&ClusterExtensionRevision{}, &ClusterExtensionRevisionList{}) + SchemeBuilder.Register(&ClusterExtensionRevision{}, &ClusterExtensionRevisionList{}) } From e781e2dc3482b51ddb6f947522fa192cb2e3b0a6 Mon Sep 17 00:00:00 2001 From: Per Goncalves da Silva Date: Fri, 10 Oct 2025 15:28:33 +0200 Subject: [PATCH 2/4] Update resources for print column Signed-off-by: Per Goncalves da Silva --- api/v1/clusterextensionrevision_types.go | 212 +++++++++--------- ...ramework.io_clusterextensionrevisions.yaml | 9 +- manifests/experimental-e2e.yaml | 9 +- manifests/experimental.yaml | 9 +- 4 files changed, 130 insertions(+), 109 deletions(-) diff --git a/api/v1/clusterextensionrevision_types.go b/api/v1/clusterextensionrevision_types.go index f861e15e2..13ac4ce2a 100644 --- a/api/v1/clusterextensionrevision_types.go +++ b/api/v1/clusterextensionrevision_types.go @@ -17,131 +17,131 @@ limitations under the License. package v1 import ( - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - "k8s.io/apimachinery/pkg/types" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/types" ) const ( - ClusterExtensionRevisionKind = "ClusterExtensionRevision" - - // Condition Types - ClusterExtensionRevisionTypeAvailable = "Available" - ClusterExtensionRevisionTypeSucceeded = "Succeeded" - - // Condition Reasons - ClusterExtensionRevisionReasonAvailable = "Available" - ClusterExtensionRevisionReasonReconcileFailure = "ReconcileFailure" - ClusterExtensionRevisionReasonRevisionValidationFailure = "RevisionValidationFailure" - ClusterExtensionRevisionReasonPhaseValidationError = "PhaseValidationError" - ClusterExtensionRevisionReasonObjectCollisions = "ObjectCollisions" - ClusterExtensionRevisionReasonRolloutSuccess = "RolloutSuccess" - ClusterExtensionRevisionReasonProbeFailure = "ProbeFailure" - ClusterExtensionRevisionReasonIncomplete = "Incomplete" - ClusterExtensionRevisionReasonProgressing = "Progressing" - ClusterExtensionRevisionReasonArchived = "Archived" + ClusterExtensionRevisionKind = "ClusterExtensionRevision" + + // Condition Types + ClusterExtensionRevisionTypeAvailable = "Available" + ClusterExtensionRevisionTypeSucceeded = "Succeeded" + + // Condition Reasons + ClusterExtensionRevisionReasonAvailable = "Available" + ClusterExtensionRevisionReasonReconcileFailure = "ReconcileFailure" + ClusterExtensionRevisionReasonRevisionValidationFailure = "RevisionValidationFailure" + ClusterExtensionRevisionReasonPhaseValidationError = "PhaseValidationError" + ClusterExtensionRevisionReasonObjectCollisions = "ObjectCollisions" + ClusterExtensionRevisionReasonRolloutSuccess = "RolloutSuccess" + ClusterExtensionRevisionReasonProbeFailure = "ProbeFailure" + ClusterExtensionRevisionReasonIncomplete = "Incomplete" + ClusterExtensionRevisionReasonProgressing = "Progressing" + ClusterExtensionRevisionReasonArchived = "Archived" ) // ClusterExtensionRevisionSpec defines the desired state of ClusterExtensionRevision. type ClusterExtensionRevisionSpec struct { - // Specifies the lifecycle state of the ClusterExtensionRevision. - // - // +kubebuilder:default="Active" - // +kubebuilder:validation:Enum=Active;Paused;Archived - // +kubebuilder:validation:XValidation:rule="oldSelf == 'Active' || oldSelf == 'Paused' || oldSelf == 'Archived' && oldSelf == self", message="can not un-archive" - LifecycleState ClusterExtensionRevisionLifecycleState `json:"lifecycleState,omitempty"` - // Revision is a sequence number representing a specific revision of the ClusterExtension instance. - // Must be positive. Each ClusterExtensionRevision of the same parent ClusterExtension needs to have - // a unique value assigned. It is immutable after creation. The new revision number must always be previous revision +1. - // - // +kubebuilder:validation:Required - // +kubebuilder:validation:Minimum:=1 - // +kubebuilder:validation:XValidation:rule="self == oldSelf", message="revision is immutable" - Revision int64 `json:"revision"` - // Phases are groups of objects that will be applied at the same time. - // All objects in the phase will have to pass their probes in order to progress to the next phase. - // - // +kubebuilder:validation:XValidation:rule="self == oldSelf || oldSelf.size() == 0", message="phases is immutable" - // +listType=map - // +listMapKey=name - // +optional - Phases []ClusterExtensionRevisionPhase `json:"phases,omitempty"` - // Previous references previous revisions that objects can be adopted from. - // - // +kubebuilder:validation:XValidation:rule="self == oldSelf", message="previous is immutable" - Previous []ClusterExtensionRevisionPrevious `json:"previous,omitempty"` + // Specifies the lifecycle state of the ClusterExtensionRevision. + // + // +kubebuilder:default="Active" + // +kubebuilder:validation:Enum=Active;Paused;Archived + // +kubebuilder:validation:XValidation:rule="oldSelf == 'Active' || oldSelf == 'Paused' || oldSelf == 'Archived' && oldSelf == self", message="can not un-archive" + LifecycleState ClusterExtensionRevisionLifecycleState `json:"lifecycleState,omitempty"` + // Revision is a sequence number representing a specific revision of the ClusterExtension instance. + // Must be positive. Each ClusterExtensionRevision of the same parent ClusterExtension needs to have + // a unique value assigned. It is immutable after creation. The new revision number must always be previous revision +1. + // + // +kubebuilder:validation:Required + // +kubebuilder:validation:Minimum:=1 + // +kubebuilder:validation:XValidation:rule="self == oldSelf", message="revision is immutable" + Revision int64 `json:"revision"` + // Phases are groups of objects that will be applied at the same time. + // All objects in the phase will have to pass their probes in order to progress to the next phase. + // + // +kubebuilder:validation:XValidation:rule="self == oldSelf || oldSelf.size() == 0", message="phases is immutable" + // +listType=map + // +listMapKey=name + // +optional + Phases []ClusterExtensionRevisionPhase `json:"phases,omitempty"` + // Previous references previous revisions that objects can be adopted from. + // + // +kubebuilder:validation:XValidation:rule="self == oldSelf", message="previous is immutable" + Previous []ClusterExtensionRevisionPrevious `json:"previous,omitempty"` } // ClusterExtensionRevisionLifecycleState specifies the lifecycle state of the ClusterExtensionRevision. type ClusterExtensionRevisionLifecycleState string const ( - // ClusterExtensionRevisionLifecycleStateActive / "Active" is the default lifecycle state. - ClusterExtensionRevisionLifecycleStateActive ClusterExtensionRevisionLifecycleState = "Active" - // ClusterExtensionRevisionLifecycleStatePaused / "Paused" disables reconciliation of the ClusterExtensionRevision. - // Only Status updates will still propagated, but object changes will not be reconciled. - ClusterExtensionRevisionLifecycleStatePaused ClusterExtensionRevisionLifecycleState = "Paused" - // ClusterExtensionRevisionLifecycleStateArchived / "Archived" disables reconciliation while also "scaling to zero", - // which deletes all objects that are not excluded via the pausedFor property and - // removes itself from the owner list of all other objects previously under management. - ClusterExtensionRevisionLifecycleStateArchived ClusterExtensionRevisionLifecycleState = "Archived" + // ClusterExtensionRevisionLifecycleStateActive / "Active" is the default lifecycle state. + ClusterExtensionRevisionLifecycleStateActive ClusterExtensionRevisionLifecycleState = "Active" + // ClusterExtensionRevisionLifecycleStatePaused / "Paused" disables reconciliation of the ClusterExtensionRevision. + // Only Status updates will still propagated, but object changes will not be reconciled. + ClusterExtensionRevisionLifecycleStatePaused ClusterExtensionRevisionLifecycleState = "Paused" + // ClusterExtensionRevisionLifecycleStateArchived / "Archived" disables reconciliation while also "scaling to zero", + // which deletes all objects that are not excluded via the pausedFor property and + // removes itself from the owner list of all other objects previously under management. + ClusterExtensionRevisionLifecycleStateArchived ClusterExtensionRevisionLifecycleState = "Archived" ) // ClusterExtensionRevisionPhase are groups of objects that will be applied at the same time. // All objects in the a phase will have to pass their probes in order to progress to the next phase. type ClusterExtensionRevisionPhase struct { - // Name identifies this phase. - // - // +kubebuilder:validation:MaxLength=63 - // +kubebuilder:validation:Pattern=`^[a-z]([-a-z0-9]*[a-z0-9])?$` - Name string `json:"name"` - // Objects are a list of all the objects within this phase. - Objects []ClusterExtensionRevisionObject `json:"objects"` + // Name identifies this phase. + // + // +kubebuilder:validation:MaxLength=63 + // +kubebuilder:validation:Pattern=`^[a-z]([-a-z0-9]*[a-z0-9])?$` + Name string `json:"name"` + // Objects are a list of all the objects within this phase. + Objects []ClusterExtensionRevisionObject `json:"objects"` } // ClusterExtensionRevisionObject contains an object and settings for it. type ClusterExtensionRevisionObject struct { - // +kubebuilder:validation:EmbeddedResource - // +kubebuilder:pruning:PreserveUnknownFields - Object unstructured.Unstructured `json:"object"` - // CollisionProtection controls whether OLM can adopt and modify objects - // already existing on the cluster or even owned by another controller. - // - // +kubebuilder:default="Prevent" - // +optional - CollisionProtection CollisionProtection `json:"collisionProtection,omitempty"` + // +kubebuilder:validation:EmbeddedResource + // +kubebuilder:pruning:PreserveUnknownFields + Object unstructured.Unstructured `json:"object"` + // CollisionProtection controls whether OLM can adopt and modify objects + // already existing on the cluster or even owned by another controller. + // + // +kubebuilder:default="Prevent" + // +optional + CollisionProtection CollisionProtection `json:"collisionProtection,omitempty"` } // CollisionProtection specifies if and how ownership collisions are prevented. type CollisionProtection string const ( - // CollisionProtectionPrevent prevents owner collisions entirely - // by only allowing to work with objects itself has created. - CollisionProtectionPrevent CollisionProtection = "Prevent" - // CollisionProtectionIfNoController allows to patch and override - // objects already present if they are not owned by another controller. - CollisionProtectionIfNoController CollisionProtection = "IfNoController" - // CollisionProtectionNone allows to patch and override objects - // already present and owned by other controllers. - // Be careful! This setting may cause multiple controllers to fight over a resource, - // causing load on the API server and etcd. - CollisionProtectionNone CollisionProtection = "None" + // CollisionProtectionPrevent prevents owner collisions entirely + // by only allowing to work with objects itself has created. + CollisionProtectionPrevent CollisionProtection = "Prevent" + // CollisionProtectionIfNoController allows to patch and override + // objects already present if they are not owned by another controller. + CollisionProtectionIfNoController CollisionProtection = "IfNoController" + // CollisionProtectionNone allows to patch and override objects + // already present and owned by other controllers. + // Be careful! This setting may cause multiple controllers to fight over a resource, + // causing load on the API server and etcd. + CollisionProtectionNone CollisionProtection = "None" ) type ClusterExtensionRevisionPrevious struct { - // +kubebuilder:validation:Required - Name string `json:"name"` - // +kubebuilder:validation:Required - UID types.UID `json:"uid"` + // +kubebuilder:validation:Required + Name string `json:"name"` + // +kubebuilder:validation:Required + UID types.UID `json:"uid"` } // ClusterExtensionRevisionStatus defines the observed state of a ClusterExtensionRevision. type ClusterExtensionRevisionStatus struct { - // +listType=map - // +listMapKey=type - // +optional - Conditions []metav1.Condition `json:"conditions,omitempty"` + // +listType=map + // +listMapKey=type + // +optional + Conditions []metav1.Condition `json:"conditions,omitempty"` } // +kubebuilder:object:root=true @@ -152,33 +152,33 @@ type ClusterExtensionRevisionStatus struct { // +kubebuilder:printcolumn:name="Available",type=string,JSONPath=`.status.conditions[?(@.type=='Available')].status` // +kubebuilder:printcolumn:name=Age,type=date,JSONPath=`.metadata.creationTimestamp` type ClusterExtensionRevision struct { - metav1.TypeMeta `json:",inline"` - metav1.ObjectMeta `json:"metadata,omitempty"` + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` - // spec is an optional field that defines the desired state of the ClusterExtension. - // +optional - Spec ClusterExtensionRevisionSpec `json:"spec,omitempty"` + // spec is an optional field that defines the desired state of the ClusterExtension. + // +optional + Spec ClusterExtensionRevisionSpec `json:"spec,omitempty"` - // status is an optional field that defines the observed state of the ClusterExtension. - // +optional - Status ClusterExtensionRevisionStatus `json:"status,omitempty"` + // status is an optional field that defines the observed state of the ClusterExtension. + // +optional + Status ClusterExtensionRevisionStatus `json:"status,omitempty"` } // +kubebuilder:object:root=true // ClusterExtensionRevisionList contains a list of ClusterExtensionRevision type ClusterExtensionRevisionList struct { - metav1.TypeMeta `json:",inline"` + metav1.TypeMeta `json:",inline"` - // +optional - metav1.ListMeta `json:"metadata,omitempty"` + // +optional + metav1.ListMeta `json:"metadata,omitempty"` - // items is a required list of ClusterExtensionRevision objects. - // - // +kubebuilder:validation:Required - Items []ClusterExtensionRevision `json:"items"` + // items is a required list of ClusterExtensionRevision objects. + // + // +kubebuilder:validation:Required + Items []ClusterExtensionRevision `json:"items"` } func init() { - SchemeBuilder.Register(&ClusterExtensionRevision{}, &ClusterExtensionRevisionList{}) + SchemeBuilder.Register(&ClusterExtensionRevision{}, &ClusterExtensionRevisionList{}) } diff --git a/helm/olmv1/base/operator-controller/crd/experimental/olm.operatorframework.io_clusterextensionrevisions.yaml b/helm/olmv1/base/operator-controller/crd/experimental/olm.operatorframework.io_clusterextensionrevisions.yaml index 89a6f646b..5004c8c6f 100644 --- a/helm/olmv1/base/operator-controller/crd/experimental/olm.operatorframework.io_clusterextensionrevisions.yaml +++ b/helm/olmv1/base/operator-controller/crd/experimental/olm.operatorframework.io_clusterextensionrevisions.yaml @@ -15,7 +15,14 @@ spec: singular: clusterextensionrevision scope: Cluster versions: - - name: v1 + - additionalPrinterColumns: + - jsonPath: .status.conditions[?(@.type=='Available')].status + name: Available + type: string + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1 schema: openAPIV3Schema: description: ClusterExtensionRevision is the Schema for the clusterextensionrevisions diff --git a/manifests/experimental-e2e.yaml b/manifests/experimental-e2e.yaml index 1bc93321e..d08ffc8a7 100644 --- a/manifests/experimental-e2e.yaml +++ b/manifests/experimental-e2e.yaml @@ -606,7 +606,14 @@ spec: singular: clusterextensionrevision scope: Cluster versions: - - name: v1 + - additionalPrinterColumns: + - jsonPath: .status.conditions[?(@.type=='Available')].status + name: Available + type: string + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1 schema: openAPIV3Schema: description: ClusterExtensionRevision is the Schema for the clusterextensionrevisions diff --git a/manifests/experimental.yaml b/manifests/experimental.yaml index 69128a8b7..fc1dd3dcf 100644 --- a/manifests/experimental.yaml +++ b/manifests/experimental.yaml @@ -571,7 +571,14 @@ spec: singular: clusterextensionrevision scope: Cluster versions: - - name: v1 + - additionalPrinterColumns: + - jsonPath: .status.conditions[?(@.type=='Available')].status + name: Available + type: string + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1 schema: openAPIV3Schema: description: ClusterExtensionRevision is the Schema for the clusterextensionrevisions From d6acdc14905961362145f56887a07005ea034f3d Mon Sep 17 00:00:00 2001 From: Per Goncalves da Silva Date: Fri, 10 Oct 2025 15:30:03 +0200 Subject: [PATCH 3/4] Set Available condition to Unknown on archived revisions Signed-off-by: Per Goncalves da Silva --- .../clusterextensionrevision_controller.go | 108 ++++++++++-------- ...lusterextensionrevision_controller_test.go | 43 ++++++- 2 files changed, 104 insertions(+), 47 deletions(-) diff --git a/internal/operator-controller/controllers/clusterextensionrevision_controller.go b/internal/operator-controller/controllers/clusterextensionrevision_controller.go index fc6316e09..05c5ed6fb 100644 --- a/internal/operator-controller/controllers/clusterextensionrevision_controller.go +++ b/internal/operator-controller/controllers/clusterextensionrevision_controller.go @@ -118,52 +118,8 @@ func (c *ClusterExtensionRevisionReconciler) reconcile(ctx context.Context, rev revision, opts, previous := toBoxcutterRevision(rev) - if !rev.DeletionTimestamp.IsZero() || - rev.Spec.LifecycleState == ocv1.ClusterExtensionRevisionLifecycleStateArchived { - // - // Teardown - // - tres, err := c.RevisionEngine.Teardown(ctx, *revision) - if err != nil { - meta.SetStatusCondition(&rev.Status.Conditions, metav1.Condition{ - Type: ocv1.ClusterExtensionRevisionTypeAvailable, - Status: metav1.ConditionFalse, - Reason: ocv1.ClusterExtensionRevisionReasonReconcileFailure, - Message: err.Error(), - ObservedGeneration: rev.Generation, - }) - return ctrl.Result{}, fmt.Errorf("revision teardown: %v", err) - } - - l.Info("teardown report", "report", tres.String()) - if !tres.IsComplete() { - // TODO: If it is not complete, it seems like it would be good to update - // the status in some way to tell the user that the teardown is still - // in progress. - return ctrl.Result{}, nil - } - - if err := c.TrackingCache.Free(ctx, rev); err != nil { - meta.SetStatusCondition(&rev.Status.Conditions, metav1.Condition{ - Type: ocv1.ClusterExtensionRevisionTypeAvailable, - Status: metav1.ConditionFalse, - Reason: ocv1.ClusterExtensionRevisionReasonReconcileFailure, - Message: err.Error(), - ObservedGeneration: rev.Generation, - }) - return ctrl.Result{}, fmt.Errorf("error stopping informers: %v", err) - } - if err := c.removeFinalizer(ctx, rev, clusterExtensionRevisionTeardownFinalizer); err != nil { - meta.SetStatusCondition(&rev.Status.Conditions, metav1.Condition{ - Type: "Available", - Status: metav1.ConditionFalse, - Reason: "ReconcileFailure", - Message: err.Error(), - ObservedGeneration: rev.Generation, - }) - return ctrl.Result{}, fmt.Errorf("error removing teardown finalizer: %v", err) - } - return ctrl.Result{}, nil + if !rev.DeletionTimestamp.IsZero() || rev.Spec.LifecycleState == ocv1.ClusterExtensionRevisionLifecycleStateArchived { + return c.teardown(ctx, rev, revision) } // @@ -339,6 +295,66 @@ func (c *ClusterExtensionRevisionReconciler) reconcile(ctx context.Context, rev return ctrl.Result{}, nil } +func (c *ClusterExtensionRevisionReconciler) teardown(ctx context.Context, rev *ocv1.ClusterExtensionRevision, revision *boxcutter.Revision) (ctrl.Result, error) { + l := log.FromContext(ctx) + + tres, err := c.RevisionEngine.Teardown(ctx, *revision) + if err != nil { + meta.SetStatusCondition(&rev.Status.Conditions, metav1.Condition{ + Type: ocv1.ClusterExtensionRevisionTypeAvailable, + Status: metav1.ConditionFalse, + Reason: ocv1.ClusterExtensionRevisionReasonReconcileFailure, + Message: err.Error(), + ObservedGeneration: rev.Generation, + }) + return ctrl.Result{}, fmt.Errorf("revision teardown: %v", err) + } + + l.Info("teardown report", "report", tres.String()) + if !tres.IsComplete() { + // TODO: If it is not complete, it seems like it would be good to update + // the status in some way to tell the user that the teardown is still + // in progress. + return ctrl.Result{}, nil + } + + if err := c.TrackingCache.Free(ctx, rev); err != nil { + meta.SetStatusCondition(&rev.Status.Conditions, metav1.Condition{ + Type: ocv1.ClusterExtensionRevisionTypeAvailable, + Status: metav1.ConditionFalse, + Reason: ocv1.ClusterExtensionRevisionReasonReconcileFailure, + Message: err.Error(), + ObservedGeneration: rev.Generation, + }) + return ctrl.Result{}, fmt.Errorf("error stopping informers: %v", err) + } + + // Ensure Available condition is set to Unknown before removing the finalizer when archiving + if rev.Spec.LifecycleState == ocv1.ClusterExtensionRevisionLifecycleStateArchived && + !meta.IsStatusConditionPresentAndEqual(rev.Status.Conditions, ocv1.ClusterExtensionRevisionTypeAvailable, metav1.ConditionUnknown) { + meta.SetStatusCondition(&rev.Status.Conditions, metav1.Condition{ + Type: ocv1.ClusterExtensionRevisionTypeAvailable, + Status: metav1.ConditionUnknown, + Reason: ocv1.ClusterExtensionRevisionReasonArchived, + Message: "revision is archived", + ObservedGeneration: rev.Generation, + }) + return ctrl.Result{}, nil + } + + if err := c.removeFinalizer(ctx, rev, clusterExtensionRevisionTeardownFinalizer); err != nil { + meta.SetStatusCondition(&rev.Status.Conditions, metav1.Condition{ + Type: ocv1.ClusterExtensionRevisionTypeAvailable, + Status: metav1.ConditionFalse, + Reason: ocv1.ClusterExtensionRevisionReasonReconcileFailure, + Message: err.Error(), + ObservedGeneration: rev.Generation, + }) + return ctrl.Result{}, fmt.Errorf("error removing teardown finalizer: %v", err) + } + return ctrl.Result{}, nil +} + type Sourcerer interface { Source(handler handler.EventHandler, predicates ...predicate.Predicate) source.Source } diff --git a/internal/operator-controller/controllers/clusterextensionrevision_controller_test.go b/internal/operator-controller/controllers/clusterextensionrevision_controller_test.go index 694bd4d4a..873a6cc74 100644 --- a/internal/operator-controller/controllers/clusterextensionrevision_controller_test.go +++ b/internal/operator-controller/controllers/clusterextensionrevision_controller_test.go @@ -544,6 +544,40 @@ func Test_ClusterExtensionRevisionReconciler_Reconcile_Deletion(t *testing.T) { require.NotContains(t, "olm.operatorframework.io/teardown", rev.Finalizers) }, }, + { + name: "set Available condition to Unknown with reason Archived when archiving revision", + revisionResult: mockRevisionResult{}, + existingObjs: func() []client.Object { + ext := newTestClusterExtension() + rev1 := newTestClusterExtensionRevision(clusterExtensionRevisionName) + rev1.Finalizers = []string{ + "olm.operatorframework.io/teardown", + } + rev1.Spec.LifecycleState = ocv1.ClusterExtensionRevisionLifecycleStateArchived + require.NoError(t, controllerutil.SetControllerReference(ext, rev1, testScheme)) + return []client.Object{rev1, ext} + }, + revisionEngineTeardownFn: func(t *testing.T) func(ctx context.Context, rev machinerytypes.Revision, opts ...machinerytypes.RevisionTeardownOption) (machinery.RevisionTeardownResult, error) { + return func(ctx context.Context, rev machinerytypes.Revision, opts ...machinerytypes.RevisionTeardownOption) (machinery.RevisionTeardownResult, error) { + return &mockRevisionTeardownResult{ + isComplete: true, + }, nil + } + }, + validate: func(t *testing.T, c client.Client) { + rev := &ocv1.ClusterExtensionRevision{} + err := c.Get(t.Context(), client.ObjectKey{ + Name: clusterExtensionRevisionName, + }, rev) + require.NoError(t, err) + cond := meta.FindStatusCondition(rev.Status.Conditions, ocv1.ClusterExtensionRevisionTypeAvailable) + require.NotNil(t, cond) + require.Equal(t, metav1.ConditionUnknown, cond.Status) + require.Equal(t, ocv1.ClusterExtensionRevisionReasonArchived, cond.Reason) + require.Equal(t, "revision is archived", cond.Message) + require.Equal(t, int64(1), cond.ObservedGeneration) + }, + }, { name: "revision is torn down when in archived state and finalizer is removed", revisionResult: mockRevisionResult{}, @@ -554,6 +588,13 @@ func Test_ClusterExtensionRevisionReconciler_Reconcile_Deletion(t *testing.T) { "olm.operatorframework.io/teardown", } rev1.Spec.LifecycleState = ocv1.ClusterExtensionRevisionLifecycleStateArchived + meta.SetStatusCondition(&rev1.Status.Conditions, metav1.Condition{ + Type: ocv1.ClusterExtensionRevisionTypeAvailable, + Status: metav1.ConditionUnknown, + Reason: ocv1.ClusterExtensionRevisionReasonArchived, + Message: "revision is archived", + ObservedGeneration: rev1.Generation, + }) require.NoError(t, controllerutil.SetControllerReference(ext, rev1, testScheme)) return []client.Object{rev1, ext} }, @@ -570,7 +611,7 @@ func Test_ClusterExtensionRevisionReconciler_Reconcile_Deletion(t *testing.T) { Name: clusterExtensionRevisionName, }, rev) require.NoError(t, err) - require.NotContains(t, "olm.operatorframework.io/teardown", rev.Finalizers) + require.NotContains(t, rev.Finalizers, "olm.operatorframework.io/teardown") }, }, { From 0b26ee536c5500481e384ef8163700a1d5cddc52 Mon Sep 17 00:00:00 2001 From: Per Goncalves da Silva Date: Fri, 10 Oct 2025 16:39:50 +0200 Subject: [PATCH 4/4] Remove condition setting on finalizer removal Signed-off-by: Per Goncalves da Silva --- .../controllers/clusterextensionrevision_controller.go | 7 ------- 1 file changed, 7 deletions(-) diff --git a/internal/operator-controller/controllers/clusterextensionrevision_controller.go b/internal/operator-controller/controllers/clusterextensionrevision_controller.go index 05c5ed6fb..888249161 100644 --- a/internal/operator-controller/controllers/clusterextensionrevision_controller.go +++ b/internal/operator-controller/controllers/clusterextensionrevision_controller.go @@ -343,13 +343,6 @@ func (c *ClusterExtensionRevisionReconciler) teardown(ctx context.Context, rev * } if err := c.removeFinalizer(ctx, rev, clusterExtensionRevisionTeardownFinalizer); err != nil { - meta.SetStatusCondition(&rev.Status.Conditions, metav1.Condition{ - Type: ocv1.ClusterExtensionRevisionTypeAvailable, - Status: metav1.ConditionFalse, - Reason: ocv1.ClusterExtensionRevisionReasonReconcileFailure, - Message: err.Error(), - ObservedGeneration: rev.Generation, - }) return ctrl.Result{}, fmt.Errorf("error removing teardown finalizer: %v", err) } return ctrl.Result{}, nil