-
Notifications
You must be signed in to change notification settings - Fork 71
⚠️ WIP: ClusterExtensionRevision API Updates #2491
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -42,14 +42,18 @@ const ( | |||||
| type ClusterExtensionRevisionSpec struct { | ||||||
| // lifecycleState specifies the lifecycle state of the ClusterExtensionRevision. | ||||||
| // | ||||||
| // When set to "Active" (the default), the revision is actively managed and reconciled. | ||||||
| // When set to "Active", the revision is actively managed and reconciled. | ||||||
| // When set to "Archived", the revision is inactive and any resources not managed by a subsequent revision are deleted. | ||||||
| // The revision is removed from the owner list of all objects previously under management. | ||||||
| // All objects that did not transition to a succeeding revision are deleted. | ||||||
| // | ||||||
| // Once a revision is set to "Archived", it cannot be un-archived. | ||||||
| // | ||||||
| // +kubebuilder:default="Active" | ||||||
| // It is possible for more than one revision to be "Active" simultaneously. This will occur when | ||||||
| // moving from one revision to another. The old revision will not be set to "Archived" until the | ||||||
| // new revision has been completely rolled out. | ||||||
| // | ||||||
| // +required | ||||||
| // +kubebuilder:validation:Enum=Active;Archived | ||||||
| // +kubebuilder:validation:XValidation:rule="oldSelf == 'Active' || oldSelf == 'Archived' && oldSelf == self", message="cannot un-archive" | ||||||
| LifecycleState ClusterExtensionRevisionLifecycleState `json:"lifecycleState,omitempty"` | ||||||
|
|
@@ -82,7 +86,10 @@ type ClusterExtensionRevisionSpec struct { | |||||
| // | ||||||
| // Once set, even if empty, the phases field is immutable. | ||||||
| // | ||||||
| // Each phase in the list must have a unique name. The maximum number of phases is 20. | ||||||
| // | ||||||
| // +kubebuilder:validation:XValidation:rule="self == oldSelf || oldSelf.size() == 0", message="phases is immutable" | ||||||
| // +kubebuilder:validation:MaxItems=20 | ||||||
| // +listType=map | ||||||
| // +listMapKey=name | ||||||
| // +optional | ||||||
|
|
@@ -125,13 +132,17 @@ type ClusterExtensionRevisionPhase struct { | |||||
| // | ||||||
| // [RFC 1123]: https://tools.ietf.org/html/rfc1123 | ||||||
| // | ||||||
| // +required | ||||||
| // +kubebuilder:validation:MinLength=1 | ||||||
| // +kubebuilder:validation:MaxLength=63 | ||||||
| // +kubebuilder:validation:Pattern=`^[a-z]([-a-z0-9]*[a-z0-9])?$` | ||||||
| // +kubebuilder:validation:XValidation:rule=`!format.dns1123Label().validate(self).hasValue()`,message="the value must consist of only lowercase alphanumeric characters and hyphens, and must start with an alphabetic character and end with an alphanumeric character." | ||||||
| Name string `json:"name"` | ||||||
|
Comment on lines
+135
to
139
|
||||||
|
|
||||||
| // objects is a required list of all Kubernetes objects that belong to this phase. | ||||||
| // | ||||||
| // All objects in this list are applied to the cluster in no particular order. | ||||||
| // All objects in this list are applied to the cluster in no particular order. The maximum number of objects per phase is 50. | ||||||
| // +required | ||||||
| // +kubebuilder:validation:MaxItems=50 | ||||||
pedjak marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||
| Objects []ClusterExtensionRevisionObject `json:"objects"` | ||||||
| } | ||||||
|
|
||||||
|
|
@@ -149,7 +160,9 @@ type ClusterExtensionRevisionObject struct { | |||||
| // collisionProtection controls whether the operator can adopt and modify objects | ||||||
| // that already exist on the cluster. | ||||||
| // | ||||||
| // When set to "Prevent" (the default), the operator only manages objects it created itself. | ||||||
| // Allowed values are: "Prevent", "IfNoController", and "None". | ||||||
| // | ||||||
| // When set to "Prevent", the operator only manages objects it created itself. | ||||||
| // This prevents ownership collisions. | ||||||
| // | ||||||
| // When set to "IfNoController", the operator can adopt and modify pre-existing objects | ||||||
|
|
@@ -161,9 +174,8 @@ type ClusterExtensionRevisionObject struct { | |||||
| // Use this setting with extreme caution as it may cause multiple controllers to fight over | ||||||
| // the same resource, resulting in increased load on the API server and etcd. | ||||||
| // | ||||||
| // +kubebuilder:default="Prevent" | ||||||
| // +required | ||||||
| // +kubebuilder:validation:Enum=Prevent;IfNoController;None | ||||||
| // +optional | ||||||
| CollisionProtection CollisionProtection `json:"collisionProtection,omitempty"` | ||||||
pedjak marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||
| CollisionProtection CollisionProtection `json:"collisionProtection,omitempty"` | |
| CollisionProtection CollisionProtection `json:"collisionProtection"` |
Copilot
AI
Feb 12, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
CollisionProtection is marked +required, but the JSON tag still includes omitempty. As with LifecycleState, this can lead to marshaled revisions that omit a required field when the zero value is present. Consider removing omitempty to match repo conventions for required fields.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -21,16 +21,18 @@ func TestClusterExtensionRevisionImmutability(t *testing.T) { | |
| }{ | ||
| "revision is immutable": { | ||
| spec: ClusterExtensionRevisionSpec{ | ||
| Revision: 1, | ||
| LifecycleState: ClusterExtensionRevisionLifecycleStateActive, | ||
| Revision: 1, | ||
| }, | ||
| updateFunc: func(cer *ClusterExtensionRevision) { | ||
| cer.Spec.Revision = 2 | ||
| }, | ||
| }, | ||
| "phases may be initially empty": { | ||
| spec: ClusterExtensionRevisionSpec{ | ||
| Revision: 1, | ||
| Phases: []ClusterExtensionRevisionPhase{}, | ||
| LifecycleState: ClusterExtensionRevisionLifecycleStateActive, | ||
| Revision: 1, | ||
| Phases: []ClusterExtensionRevisionPhase{}, | ||
| }, | ||
| updateFunc: func(cer *ClusterExtensionRevision) { | ||
| cer.Spec.Phases = []ClusterExtensionRevisionPhase{ | ||
|
|
@@ -44,7 +46,8 @@ func TestClusterExtensionRevisionImmutability(t *testing.T) { | |
| }, | ||
| "phases may be initially unset": { | ||
| spec: ClusterExtensionRevisionSpec{ | ||
| Revision: 1, | ||
| LifecycleState: ClusterExtensionRevisionLifecycleStateActive, | ||
| Revision: 1, | ||
| }, | ||
| updateFunc: func(cer *ClusterExtensionRevision) { | ||
| cer.Spec.Phases = []ClusterExtensionRevisionPhase{ | ||
|
|
@@ -58,7 +61,8 @@ func TestClusterExtensionRevisionImmutability(t *testing.T) { | |
| }, | ||
| "phases are immutable if not empty": { | ||
| spec: ClusterExtensionRevisionSpec{ | ||
| Revision: 1, | ||
| LifecycleState: ClusterExtensionRevisionLifecycleStateActive, | ||
| Revision: 1, | ||
| Phases: []ClusterExtensionRevisionPhase{ | ||
| { | ||
| Name: "foo", | ||
|
|
@@ -107,20 +111,87 @@ func TestClusterExtensionRevisionValidity(t *testing.T) { | |
| }{ | ||
| "revision cannot be negative": { | ||
| spec: ClusterExtensionRevisionSpec{ | ||
| Revision: -1, | ||
| LifecycleState: ClusterExtensionRevisionLifecycleStateActive, | ||
| Revision: -1, | ||
| }, | ||
| valid: false, | ||
| }, | ||
| "revision cannot be zero": { | ||
| spec: ClusterExtensionRevisionSpec{}, | ||
| spec: ClusterExtensionRevisionSpec{ | ||
| LifecycleState: ClusterExtensionRevisionLifecycleStateActive, | ||
| }, | ||
| valid: false, | ||
| }, | ||
| "revision must be positive": { | ||
| spec: ClusterExtensionRevisionSpec{ | ||
| Revision: 1, | ||
| LifecycleState: ClusterExtensionRevisionLifecycleStateActive, | ||
| Revision: 1, | ||
| }, | ||
| valid: true, | ||
| }, | ||
| "lifecycleState must be set": { | ||
| spec: ClusterExtensionRevisionSpec{ | ||
| Revision: 1, | ||
| }, | ||
| valid: false, | ||
| }, | ||
| "inlinePhases must have no more than 20 phases": { | ||
| spec: ClusterExtensionRevisionSpec{ | ||
| LifecycleState: ClusterExtensionRevisionLifecycleStateActive, | ||
| Revision: 1, | ||
| Phases: make([]ClusterExtensionRevisionPhase, 21), | ||
| }, | ||
| valid: false, | ||
| }, | ||
| "inlinePhases entries must have no more than 50 objects": { | ||
| spec: ClusterExtensionRevisionSpec{ | ||
|
Comment on lines
+138
to
+147
|
||
| LifecycleState: ClusterExtensionRevisionLifecycleStateActive, | ||
| Revision: 1, | ||
| Phases: []ClusterExtensionRevisionPhase{ | ||
| { | ||
| Name: "too-many-objects", | ||
| Objects: make([]ClusterExtensionRevisionObject, 51), | ||
| }, | ||
| }, | ||
| }, | ||
| valid: false, | ||
| }, | ||
| "inlinePhases entry names cannot be empty": { | ||
| spec: ClusterExtensionRevisionSpec{ | ||
| LifecycleState: ClusterExtensionRevisionLifecycleStateActive, | ||
| Revision: 1, | ||
| Phases: []ClusterExtensionRevisionPhase{ | ||
| { | ||
| Name: "", | ||
| }, | ||
| }, | ||
| }, | ||
| valid: false, | ||
| }, | ||
| "inlinePhases entry names cannot start with symbols": { | ||
| spec: ClusterExtensionRevisionSpec{ | ||
| LifecycleState: ClusterExtensionRevisionLifecycleStateActive, | ||
| Revision: 1, | ||
| Phases: []ClusterExtensionRevisionPhase{ | ||
| { | ||
| Name: "-invalid", | ||
| }, | ||
| }, | ||
| }, | ||
| valid: false, | ||
| }, | ||
| "inlinePhases entry names cannot start with numeric characters": { | ||
| spec: ClusterExtensionRevisionSpec{ | ||
| LifecycleState: ClusterExtensionRevisionLifecycleStateActive, | ||
| Revision: 1, | ||
| Phases: []ClusterExtensionRevisionPhase{ | ||
| { | ||
| Name: "1-invalid", | ||
| }, | ||
| }, | ||
| }, | ||
| valid: false, | ||
| }, | ||
| } { | ||
| t.Run(name, func(t *testing.T) { | ||
| cer := &ClusterExtensionRevision{ | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -57,16 +57,19 @@ spec: | |||||||||
| description: spec defines the desired state of the ClusterExtensionRevision. | ||||||||||
| properties: | ||||||||||
| lifecycleState: | ||||||||||
| default: Active | ||||||||||
| description: |- | ||||||||||
| lifecycleState specifies the lifecycle state of the ClusterExtensionRevision. | ||||||||||
|
|
||||||||||
| When set to "Active" (the default), the revision is actively managed and reconciled. | ||||||||||
| When set to "Active", the revision is actively managed and reconciled. | ||||||||||
| When set to "Archived", the revision is inactive and any resources not managed by a subsequent revision are deleted. | ||||||||||
| The revision is removed from the owner list of all objects previously under management. | ||||||||||
| All objects that did not transition to a succeeding revision are deleted. | ||||||||||
|
|
||||||||||
| Once a revision is set to "Archived", it cannot be un-archived. | ||||||||||
|
|
||||||||||
| It is possible for more than one revision to be "Active" simultaneously. This will occur when | ||||||||||
| moving from one revision to another. The old revision will not be set to "Archived" until the | ||||||||||
| new revision has been completely rolled out. | ||||||||||
| enum: | ||||||||||
| - Active | ||||||||||
| - Archived | ||||||||||
|
|
@@ -92,6 +95,8 @@ spec: | |||||||||
| The revision progresses to the next phase only after all objects in the current phase pass their readiness probes. | ||||||||||
|
|
||||||||||
| Once set, even if empty, the phases field is immutable. | ||||||||||
|
|
||||||||||
| Each phase in the list must have a unique name. The maximum number of phases is 20. | ||||||||||
| items: | ||||||||||
| description: |- | ||||||||||
| ClusterExtensionRevisionPhase represents a group of objects that are applied together. The phase is considered | ||||||||||
|
|
@@ -109,25 +114,31 @@ spec: | |||||||||
|
|
||||||||||
| [RFC 1123]: https://tools.ietf.org/html/rfc1123 | ||||||||||
| maxLength: 63 | ||||||||||
| pattern: ^[a-z]([-a-z0-9]*[a-z0-9])?$ | ||||||||||
| minLength: 1 | ||||||||||
| type: string | ||||||||||
| x-kubernetes-validations: | ||||||||||
| - message: the value must consist of only lowercase alphanumeric | ||||||||||
| characters and hyphens, and must start with an alphabetic | ||||||||||
| character and end with an alphanumeric character. | ||||||||||
|
Comment on lines
+121
to
+122
|
||||||||||
| characters and hyphens, and must start with an alphabetic | |
| character and end with an alphanumeric character. | |
| characters and hyphens, must start and end with an alphanumeric | |
| character, and be no longer than 63 characters. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
lifecycleStateis marked+requiredbut the JSON tag still usesomitempty, which is inconsistent with other required API fields in this repo (e.g.CatalogFilter.PackageName). Consider removingomitemptyso clients using typed structs can’t accidentally omit a required field during marshaling.