From c4700597540e6f98aa45f12c2b97002d5e6ad028 Mon Sep 17 00:00:00 2001 From: "Per G. da Silva" Date: Wed, 11 Nov 2026 09:34:30 +0100 Subject: [PATCH 1/2] =?UTF-8?q?=E2=9C=A8:=20dedupe=20ClusterObjectDeployme?= =?UTF-8?q?nt=20template=20and=20ClusterObjectSet=20spec=20into=20shared?= =?UTF-8?q?=20content=20struct?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Introduce ClusterObjectSetContent holding the phases, progressionProbes, and collisionProtection fields shared between ClusterObjectSet.spec and ClusterObjectDeployment template specs. Both spec structs embed it anonymously (json:",inline"), preserving the existing JSON paths. Immutability of the content fields is enforced via struct-level CEL rules on the COS-only ClusterObjectSetSpec wrapper, so the COD template remains mutable. Signed-off-by: Per G. da Silva --- api/v1/clusterobjectdeployment_types.go | 29 +- api/v1/clusterobjectdeployment_types_test.go | 6 +- api/v1/clusterobjectset_types.go | 26 +- api/v1/clusterobjectset_types_test.go | 546 ++++++++++-------- api/v1/validation_test.go | 60 +- api/v1/zz_generated.deepcopy.go | 59 +- .../v1/clusterobjectdeploymenttemplatespec.go | 23 +- .../api/v1/clusterobjectsetcontent.go | 107 ++++ .../api/v1/clusterobjectsetspec.go | 54 +- applyconfigurations/utils.go | 2 + cmd/object-controller/main_test.go | 6 +- docs/api-reference/olmv1-api-reference.md | 28 +- ...framework.io_clusterobjectdeployments.yaml | 41 +- ...peratorframework.io_clusterobjectsets.yaml | 28 +- .../clusterobjectdeployment_controller.go | 10 +- ...jectdeployment_controller_internal_test.go | 22 +- ...clusterobjectdeployment_controller_test.go | 44 +- ...usterobjectset_controller_internal_test.go | 84 +-- .../clusterobjectset_controller_test.go | 26 +- .../controllers/resolve_ref_test.go | 24 +- .../object-controller/status/status_test.go | 8 +- .../applier/boxcutter_test.go | 6 +- .../boxcutter_reconcile_steps_test.go | 20 +- manifests/experimental-e2e.yaml | 69 ++- manifests/experimental.yaml | 69 ++- 25 files changed, 814 insertions(+), 583 deletions(-) create mode 100644 applyconfigurations/api/v1/clusterobjectsetcontent.go diff --git a/api/v1/clusterobjectdeployment_types.go b/api/v1/clusterobjectdeployment_types.go index f34bb7f3a3..746e9ee0b0 100644 --- a/api/v1/clusterobjectdeployment_types.go +++ b/api/v1/clusterobjectdeployment_types.go @@ -124,31 +124,10 @@ type ClusterObjectDeploymentTemplateMetadata struct { // ClusterObjectDeploymentTemplateSpec contains mutable ClusterObjectSet content. type ClusterObjectDeploymentTemplateSpec struct { - // phases is an ordered list of 1 to 20 uniquely named phases. Each phase contains - // at most 50 objects, each expressed either as an inlineManifest (an embedded - // Kubernetes object) or a secretRef (a reference to an immutable Secret holding - // a serialized manifest). Phase, object, and probe order participate in template identity. - // +required - // +kubebuilder:validation:MinItems=1 - // +kubebuilder:validation:MaxItems=20 - // +listType=map - // +listMapKey=name - Phases []ClusterObjectSetPhase `json:"phases,omitempty"` // These are ordered groups of objects, not an enum describing lifecycle state. - - // progressionProbes supplies custom readiness probes for objects in every phase. - // The same selector and assertion semantics as ClusterObjectSet.spec.progressionProbes apply. - // When set, must contain 1 to 20 probes. - // +optional - // +kubebuilder:validation:MinItems=1 - // +kubebuilder:validation:MaxItems=20 - // +listType=atomic - ProgressionProbes []ProgressionProbe `json:"progressionProbes,omitempty"` - - // collisionProtection is the required default collision protection strategy for all objects - // in the revision. Individual phases or objects may override this value. - // The resolution order is: object > phase > spec. - // +required - CollisionProtection CollisionProtection `json:"collisionProtection,omitempty"` + // ClusterObjectSetContent holds the mutable revision content that is + // propagated to each ClusterObjectSet created from this template. Changes + // to these fields create a new revision; existing revisions remain immutable. + ClusterObjectSetContent `json:",inline"` } // ClusterObjectDeploymentStatus reports rollout progress. diff --git a/api/v1/clusterobjectdeployment_types_test.go b/api/v1/clusterobjectdeployment_types_test.go index e095e8b64d..e93e7face6 100644 --- a/api/v1/clusterobjectdeployment_types_test.go +++ b/api/v1/clusterobjectdeployment_types_test.go @@ -345,8 +345,10 @@ func newTestClusterObjectDeployment(name string) *ClusterObjectDeployment { Spec: ClusterObjectDeploymentSpec{ Template: ClusterObjectDeploymentTemplate{ Spec: ClusterObjectDeploymentTemplateSpec{ - CollisionProtection: CollisionProtectionPrevent, - Phases: []ClusterObjectSetPhase{{Name: "deploy", Objects: []ClusterObjectSetObject{{Type: ObjectTypeInlineManifest, InlineManifest: configMap()}}}}, + ClusterObjectSetContent: ClusterObjectSetContent{ + CollisionProtection: CollisionProtectionPrevent, + Phases: []ClusterObjectSetPhase{{Name: "deploy", Objects: []ClusterObjectSetObject{{Type: ObjectTypeInlineManifest, InlineManifest: configMap()}}}}, + }, }, }, }, diff --git a/api/v1/clusterobjectset_types.go b/api/v1/clusterobjectset_types.go index 7302196814..473c4a0f42 100644 --- a/api/v1/clusterobjectset_types.go +++ b/api/v1/clusterobjectset_types.go @@ -46,8 +46,12 @@ const ( ) // ClusterObjectSetSpec defines the desired state of ClusterObjectSet. +// The content fields (phases, progressionProbes, collisionProtection) are +// immutable: once set they may only be changed by creating a new revision. // +kubebuilder:validation:XValidation:rule="!has(oldSelf.phases) || has(self.phases)",message="phases cannot be removed once set" -// +kubebuilder:validation:XValidation:rule="!has(oldSelf.progressionProbes) || has(self.progressionProbes)",message="progressionProbes cannot be removed once set" +// +kubebuilder:validation:XValidation:rule="!has(oldSelf.phases) || self.phases == oldSelf.phases || oldSelf.phases.size() == 0",message="phases is immutable" +// +kubebuilder:validation:XValidation:rule="!has(oldSelf.progressionProbes) || !has(self.progressionProbes) && oldSelf.progressionProbes.size() == 0 || has(self.progressionProbes) && (self.progressionProbes == oldSelf.progressionProbes || oldSelf.progressionProbes.size() == 0)",message="progressionProbes is immutable" +// +kubebuilder:validation:XValidation:rule="self.collisionProtection == oldSelf.collisionProtection",message="collisionProtection is immutable" type ClusterObjectSetSpec struct { // group is a required, immutable identifier that links related revisions together. // All revisions sharing the same group form an ordered sequence, regardless of owner references. @@ -90,7 +94,17 @@ type ClusterObjectSetSpec struct { // +kubebuilder:validation:XValidation:rule="self == oldSelf", message="revision is immutable" Revision int64 `json:"revision,omitempty"` - // phases is a required, immutable list of phases that group objects to be applied together. + // ClusterObjectSetContent holds the immutable revision content shared with + // the ClusterObjectDeployment template. Immutability of these fields is + // enforced at the ClusterObjectSetSpec level, not here, so that the same + // structure can be embedded mutably in a ClusterObjectDeployment template. + ClusterObjectSetContent `json:",inline"` +} + +// ClusterObjectSetContent holds the revision content shared between +// ClusterObjectSet.spec and a ClusterObjectDeployment's template spec. +type ClusterObjectSetContent struct { + // phases is a required list of phases that group objects to be applied together. // // Objects are organized into phases based on their Group-Kind. Common phases include: // - namespaces: Namespace objects @@ -104,11 +118,8 @@ type ClusterObjectSetSpec struct { // All objects in a phase are applied in no particular order. // The revision progresses to the next phase only after all objects in the current phase pass their readiness probes. // - // Once set, 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:MinItems=1 // +kubebuilder:validation:MaxItems=20 // +listType=map @@ -128,10 +139,6 @@ type ClusterObjectSetSpec struct { // // The maximum number of probes is 20. // - // Once set, the progressionProbes field is immutable, as it is - // part of the immutable content of a revision. - // - // +kubebuilder:validation:XValidation:rule="self == oldSelf || oldSelf.size() == 0", message="progressionProbes is immutable" // +kubebuilder:validation:MinItems=1 // +kubebuilder:validation:MaxItems=20 // +listType=atomic @@ -146,7 +153,6 @@ type ClusterObjectSetSpec struct { // its own collisionProtection. The resolution order is: object > phase > spec. // // +required - // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="collisionProtection is immutable" CollisionProtection CollisionProtection `json:"collisionProtection,omitempty"` } diff --git a/api/v1/clusterobjectset_types_test.go b/api/v1/clusterobjectset_types_test.go index 0e447a8786..49ff298e63 100644 --- a/api/v1/clusterobjectset_types_test.go +++ b/api/v1/clusterobjectset_types_test.go @@ -27,11 +27,13 @@ func TestClusterObjectSetImmutability(t *testing.T) { }{ "group is immutable": { spec: ClusterObjectSetSpec{ - Group: "immutability-group-change", - LifecycleState: ClusterObjectSetLifecycleStateActive, - Revision: 1, - CollisionProtection: CollisionProtectionPrevent, - Phases: []ClusterObjectSetPhase{{Name: "deploy", Objects: []ClusterObjectSetObject{}}}, + Group: "immutability-group-change", + LifecycleState: ClusterObjectSetLifecycleStateActive, + Revision: 1, + ClusterObjectSetContent: ClusterObjectSetContent{ + CollisionProtection: CollisionProtectionPrevent, + Phases: []ClusterObjectSetPhase{{Name: "deploy", Objects: []ClusterObjectSetObject{}}}, + }, }, updateFunc: func(cos *ClusterObjectSet) { cos.Spec.Group = "another-group" @@ -40,11 +42,13 @@ func TestClusterObjectSetImmutability(t *testing.T) { }, "group cannot be cleared": { spec: ClusterObjectSetSpec{ - Group: "immutability-group-cleared", - LifecycleState: ClusterObjectSetLifecycleStateActive, - Revision: 1, - CollisionProtection: CollisionProtectionPrevent, - Phases: []ClusterObjectSetPhase{{Name: "deploy", Objects: []ClusterObjectSetObject{}}}, + Group: "immutability-group-cleared", + LifecycleState: ClusterObjectSetLifecycleStateActive, + Revision: 1, + ClusterObjectSetContent: ClusterObjectSetContent{ + CollisionProtection: CollisionProtectionPrevent, + Phases: []ClusterObjectSetPhase{{Name: "deploy", Objects: []ClusterObjectSetObject{}}}, + }, }, updateFunc: func(cos *ClusterObjectSet) { cos.Spec.Group = "" @@ -53,11 +57,13 @@ func TestClusterObjectSetImmutability(t *testing.T) { }, "unchanged group permits lifecycle update": { spec: ClusterObjectSetSpec{ - Group: "immutability-lifecycle-update", - LifecycleState: ClusterObjectSetLifecycleStateActive, - Revision: 1, - CollisionProtection: CollisionProtectionPrevent, - Phases: []ClusterObjectSetPhase{{Name: "deploy", Objects: []ClusterObjectSetObject{}}}, + Group: "immutability-lifecycle-update", + LifecycleState: ClusterObjectSetLifecycleStateActive, + Revision: 1, + ClusterObjectSetContent: ClusterObjectSetContent{ + CollisionProtection: CollisionProtectionPrevent, + Phases: []ClusterObjectSetPhase{{Name: "deploy", Objects: []ClusterObjectSetObject{}}}, + }, }, updateFunc: func(cos *ClusterObjectSet) { cos.Spec.LifecycleState = ClusterObjectSetLifecycleStateArchived @@ -66,11 +72,13 @@ func TestClusterObjectSetImmutability(t *testing.T) { }, "revision is immutable": { spec: ClusterObjectSetSpec{ - Group: "immutability-revision-change", - LifecycleState: ClusterObjectSetLifecycleStateActive, - Revision: 1, - CollisionProtection: CollisionProtectionPrevent, - Phases: []ClusterObjectSetPhase{{Name: "deploy", Objects: []ClusterObjectSetObject{}}}, + Group: "immutability-revision-change", + LifecycleState: ClusterObjectSetLifecycleStateActive, + Revision: 1, + ClusterObjectSetContent: ClusterObjectSetContent{ + CollisionProtection: CollisionProtectionPrevent, + Phases: []ClusterObjectSetPhase{{Name: "deploy", Objects: []ClusterObjectSetObject{}}}, + }, }, updateFunc: func(cos *ClusterObjectSet) { cos.Spec.Revision = 2 @@ -79,14 +87,16 @@ func TestClusterObjectSetImmutability(t *testing.T) { }, "phases are immutable once set": { spec: ClusterObjectSetSpec{ - Group: "immutability-populated-phases", - LifecycleState: ClusterObjectSetLifecycleStateActive, - Revision: 1, - CollisionProtection: CollisionProtectionPrevent, - Phases: []ClusterObjectSetPhase{ - { - Name: "foo", - Objects: []ClusterObjectSetObject{}, + Group: "immutability-populated-phases", + LifecycleState: ClusterObjectSetLifecycleStateActive, + Revision: 1, + ClusterObjectSetContent: ClusterObjectSetContent{ + CollisionProtection: CollisionProtectionPrevent, + Phases: []ClusterObjectSetPhase{ + { + Name: "foo", + Objects: []ClusterObjectSetObject{}, + }, }, }, }, @@ -102,11 +112,13 @@ func TestClusterObjectSetImmutability(t *testing.T) { }, "spec collisionProtection is immutable": { spec: ClusterObjectSetSpec{ - Group: "immutability-collision-protection", - LifecycleState: ClusterObjectSetLifecycleStateActive, - Revision: 1, - CollisionProtection: CollisionProtectionPrevent, - Phases: []ClusterObjectSetPhase{{Name: "deploy", Objects: []ClusterObjectSetObject{}}}, + Group: "immutability-collision-protection", + LifecycleState: ClusterObjectSetLifecycleStateActive, + Revision: 1, + ClusterObjectSetContent: ClusterObjectSetContent{ + CollisionProtection: CollisionProtectionPrevent, + Phases: []ClusterObjectSetPhase{{Name: "deploy", Objects: []ClusterObjectSetObject{}}}, + }, }, updateFunc: func(cos *ClusterObjectSet) { cos.Spec.CollisionProtection = CollisionProtectionNone @@ -115,11 +127,13 @@ func TestClusterObjectSetImmutability(t *testing.T) { }, "progressionProbes may be initially unset": { spec: ClusterObjectSetSpec{ - Group: "immutability-unset-probes", - LifecycleState: ClusterObjectSetLifecycleStateActive, - Revision: 1, - CollisionProtection: CollisionProtectionPrevent, - Phases: []ClusterObjectSetPhase{{Name: "deploy", Objects: []ClusterObjectSetObject{}}}, + Group: "immutability-unset-probes", + LifecycleState: ClusterObjectSetLifecycleStateActive, + Revision: 1, + ClusterObjectSetContent: ClusterObjectSetContent{ + CollisionProtection: CollisionProtectionPrevent, + Phases: []ClusterObjectSetPhase{{Name: "deploy", Objects: []ClusterObjectSetObject{}}}, + }, }, updateFunc: func(cos *ClusterObjectSet) { cos.Spec.ProgressionProbes = []ProgressionProbe{validProbe()} @@ -128,12 +142,14 @@ func TestClusterObjectSetImmutability(t *testing.T) { }, "progressionProbes are immutable once set": { spec: ClusterObjectSetSpec{ - Group: "immutability-populated-probes", - LifecycleState: ClusterObjectSetLifecycleStateActive, - Revision: 1, - CollisionProtection: CollisionProtectionPrevent, - Phases: []ClusterObjectSetPhase{{Name: "deploy", Objects: []ClusterObjectSetObject{}}}, - ProgressionProbes: []ProgressionProbe{validProbe()}, + Group: "immutability-populated-probes", + LifecycleState: ClusterObjectSetLifecycleStateActive, + Revision: 1, + ClusterObjectSetContent: ClusterObjectSetContent{ + CollisionProtection: CollisionProtectionPrevent, + Phases: []ClusterObjectSetPhase{{Name: "deploy", Objects: []ClusterObjectSetObject{}}}, + ProgressionProbes: []ProgressionProbe{validProbe()}, + }, }, updateFunc: func(cos *ClusterObjectSet) { cos.Spec.ProgressionProbes = []ProgressionProbe{validProbe(), validProbe()} @@ -142,11 +158,13 @@ func TestClusterObjectSetImmutability(t *testing.T) { }, "lifecycleState cannot be un-archived": { spec: ClusterObjectSetSpec{ - Group: "immutability-unarchive", - LifecycleState: ClusterObjectSetLifecycleStateArchived, - Revision: 1, - CollisionProtection: CollisionProtectionPrevent, - Phases: []ClusterObjectSetPhase{{Name: "deploy", Objects: []ClusterObjectSetObject{}}}, + Group: "immutability-unarchive", + LifecycleState: ClusterObjectSetLifecycleStateArchived, + Revision: 1, + ClusterObjectSetContent: ClusterObjectSetContent{ + CollisionProtection: CollisionProtectionPrevent, + Phases: []ClusterObjectSetPhase{{Name: "deploy", Objects: []ClusterObjectSetObject{}}}, + }, }, updateFunc: func(cos *ClusterObjectSet) { cos.Spec.LifecycleState = ClusterObjectSetLifecycleStateActive @@ -155,11 +173,13 @@ func TestClusterObjectSetImmutability(t *testing.T) { }, "lifecycleState can transition from active to archived": { spec: ClusterObjectSetSpec{ - Group: "immutability-archive", - LifecycleState: ClusterObjectSetLifecycleStateActive, - Revision: 1, - CollisionProtection: CollisionProtectionPrevent, - Phases: []ClusterObjectSetPhase{{Name: "deploy", Objects: []ClusterObjectSetObject{}}}, + Group: "immutability-archive", + LifecycleState: ClusterObjectSetLifecycleStateActive, + Revision: 1, + ClusterObjectSetContent: ClusterObjectSetContent{ + CollisionProtection: CollisionProtectionPrevent, + Phases: []ClusterObjectSetPhase{{Name: "deploy", Objects: []ClusterObjectSetObject{}}}, + }, }, updateFunc: func(cos *ClusterObjectSet) { cos.Spec.LifecycleState = ClusterObjectSetLifecycleStateArchived @@ -168,11 +188,13 @@ func TestClusterObjectSetImmutability(t *testing.T) { }, "metadata can be updated": { spec: ClusterObjectSetSpec{ - Group: "immutability-metadata", - Revision: 1, - LifecycleState: ClusterObjectSetLifecycleStateActive, - CollisionProtection: CollisionProtectionPrevent, - Phases: []ClusterObjectSetPhase{{Name: "deploy", Objects: []ClusterObjectSetObject{}}}, + Group: "immutability-metadata", + Revision: 1, + LifecycleState: ClusterObjectSetLifecycleStateActive, + ClusterObjectSetContent: ClusterObjectSetContent{ + CollisionProtection: CollisionProtectionPrevent, + Phases: []ClusterObjectSetPhase{{Name: "deploy", Objects: []ClusterObjectSetObject{}}}, + }, }, updateFunc: func(cos *ClusterObjectSet) { cos.Labels = map[string]string{"test": "updated"} @@ -212,46 +234,52 @@ func TestClusterObjectSetValidity(t *testing.T) { }{ "lifecycleState must be set": { spec: ClusterObjectSetSpec{ - CollisionProtection: CollisionProtectionPrevent, - Group: "validity-missing-lifecycle", - Revision: 1, + ClusterObjectSetContent: ClusterObjectSetContent{ + CollisionProtection: CollisionProtectionPrevent, + }, + Group: "validity-missing-lifecycle", + Revision: 1, }, valid: false, expectedError: "spec.lifecycleState: Required value", }, "phases must have no more than 20 phases": { spec: ClusterObjectSetSpec{ - CollisionProtection: CollisionProtectionPrevent, - Group: "validity-too-many-phases", - LifecycleState: ClusterObjectSetLifecycleStateActive, - Revision: 1, - Phases: func() []ClusterObjectSetPhase { - phases := make([]ClusterObjectSetPhase, 21) - for i := range phases { - phases[i] = ClusterObjectSetPhase{Name: fmt.Sprintf("phase-%d", i), Objects: []ClusterObjectSetObject{}} - } - return phases - }(), + Group: "validity-too-many-phases", + LifecycleState: ClusterObjectSetLifecycleStateActive, + Revision: 1, + ClusterObjectSetContent: ClusterObjectSetContent{ + CollisionProtection: CollisionProtectionPrevent, + Phases: func() []ClusterObjectSetPhase { + phases := make([]ClusterObjectSetPhase, 21) + for i := range phases { + phases[i] = ClusterObjectSetPhase{Name: fmt.Sprintf("phase-%d", i), Objects: []ClusterObjectSetObject{}} + } + return phases + }(), + }, }, valid: false, expectedError: "spec.phases: Too many", }, "phases entries must have no more than 50 objects": { spec: ClusterObjectSetSpec{ - CollisionProtection: CollisionProtectionPrevent, - Group: "validity-too-many-objects", - LifecycleState: ClusterObjectSetLifecycleStateActive, - Revision: 1, - Phases: []ClusterObjectSetPhase{ - { - Name: "too-many-objects", - Objects: func() []ClusterObjectSetObject { - objects := make([]ClusterObjectSetObject, 51) - for i := range objects { - objects[i] = ClusterObjectSetObject{InlineManifest: configMap()} - } - return objects - }(), + Group: "validity-too-many-objects", + LifecycleState: ClusterObjectSetLifecycleStateActive, + Revision: 1, + ClusterObjectSetContent: ClusterObjectSetContent{ + CollisionProtection: CollisionProtectionPrevent, + Phases: []ClusterObjectSetPhase{ + { + Name: "too-many-objects", + Objects: func() []ClusterObjectSetObject { + objects := make([]ClusterObjectSetObject, 51) + for i := range objects { + objects[i] = ClusterObjectSetObject{InlineManifest: configMap()} + } + return objects + }(), + }, }, }, }, @@ -260,14 +288,16 @@ func TestClusterObjectSetValidity(t *testing.T) { }, "phases entry names cannot be empty": { spec: ClusterObjectSetSpec{ - CollisionProtection: CollisionProtectionPrevent, - Group: "validity-empty-phase-name", - LifecycleState: ClusterObjectSetLifecycleStateActive, - Revision: 1, - Phases: []ClusterObjectSetPhase{ - { - Name: "", - Objects: []ClusterObjectSetObject{}, + Group: "validity-empty-phase-name", + LifecycleState: ClusterObjectSetLifecycleStateActive, + Revision: 1, + ClusterObjectSetContent: ClusterObjectSetContent{ + CollisionProtection: CollisionProtectionPrevent, + Phases: []ClusterObjectSetPhase{ + { + Name: "", + Objects: []ClusterObjectSetObject{}, + }, }, }, }, @@ -276,14 +306,16 @@ func TestClusterObjectSetValidity(t *testing.T) { }, "phases entry names cannot start with symbols": { spec: ClusterObjectSetSpec{ - CollisionProtection: CollisionProtectionPrevent, - Group: "validity-symbol-phase-name", - LifecycleState: ClusterObjectSetLifecycleStateActive, - Revision: 1, - Phases: []ClusterObjectSetPhase{ - { - Name: "-invalid", - Objects: []ClusterObjectSetObject{}, + Group: "validity-symbol-phase-name", + LifecycleState: ClusterObjectSetLifecycleStateActive, + Revision: 1, + ClusterObjectSetContent: ClusterObjectSetContent{ + CollisionProtection: CollisionProtectionPrevent, + Phases: []ClusterObjectSetPhase{ + { + Name: "-invalid", + Objects: []ClusterObjectSetObject{}, + }, }, }, }, @@ -292,14 +324,16 @@ func TestClusterObjectSetValidity(t *testing.T) { }, "phases entry names can start with numeric characters": { spec: ClusterObjectSetSpec{ - CollisionProtection: CollisionProtectionPrevent, - Group: "validity-numeric-phase-name", - LifecycleState: ClusterObjectSetLifecycleStateActive, - Revision: 1, - Phases: []ClusterObjectSetPhase{ - { - Name: "1-valid", - Objects: []ClusterObjectSetObject{}, + Group: "validity-numeric-phase-name", + LifecycleState: ClusterObjectSetLifecycleStateActive, + Revision: 1, + ClusterObjectSetContent: ClusterObjectSetContent{ + CollisionProtection: CollisionProtectionPrevent, + Phases: []ClusterObjectSetPhase{ + { + Name: "1-valid", + Objects: []ClusterObjectSetObject{}, + }, }, }, }, @@ -307,14 +341,16 @@ func TestClusterObjectSetValidity(t *testing.T) { }, "phases entry names cannot exceed 63 characters": { spec: ClusterObjectSetSpec{ - Group: "validity-case-10", - LifecycleState: ClusterObjectSetLifecycleStateActive, - Revision: 1, - CollisionProtection: CollisionProtectionPrevent, - Phases: []ClusterObjectSetPhase{ - { - Name: strings.Repeat("a", 64), - Objects: []ClusterObjectSetObject{}, + Group: "validity-case-10", + LifecycleState: ClusterObjectSetLifecycleStateActive, + Revision: 1, + ClusterObjectSetContent: ClusterObjectSetContent{ + CollisionProtection: CollisionProtectionPrevent, + Phases: []ClusterObjectSetPhase{ + { + Name: strings.Repeat("a", 64), + Objects: []ClusterObjectSetObject{}, + }, }, }, }, @@ -323,18 +359,20 @@ func TestClusterObjectSetValidity(t *testing.T) { }, "phases entry names must be unique": { spec: ClusterObjectSetSpec{ - Group: "validity-case-11", - LifecycleState: ClusterObjectSetLifecycleStateActive, - Revision: 1, - CollisionProtection: CollisionProtectionPrevent, - Phases: []ClusterObjectSetPhase{ - { - Name: "deploy", - Objects: []ClusterObjectSetObject{}, - }, - { - Name: "deploy", - Objects: []ClusterObjectSetObject{}, + Group: "validity-case-11", + LifecycleState: ClusterObjectSetLifecycleStateActive, + Revision: 1, + ClusterObjectSetContent: ClusterObjectSetContent{ + CollisionProtection: CollisionProtectionPrevent, + Phases: []ClusterObjectSetPhase{ + { + Name: "deploy", + Objects: []ClusterObjectSetObject{}, + }, + { + Name: "deploy", + Objects: []ClusterObjectSetObject{}, + }, }, }, }, @@ -343,31 +381,37 @@ func TestClusterObjectSetValidity(t *testing.T) { }, "spec collisionProtection accepts Prevent": { spec: ClusterObjectSetSpec{ - Group: "validity-prevent-collisions", - LifecycleState: ClusterObjectSetLifecycleStateActive, - Revision: 1, - CollisionProtection: CollisionProtectionPrevent, - Phases: []ClusterObjectSetPhase{{Name: "deploy", Objects: []ClusterObjectSetObject{}}}, + Group: "validity-prevent-collisions", + LifecycleState: ClusterObjectSetLifecycleStateActive, + Revision: 1, + ClusterObjectSetContent: ClusterObjectSetContent{ + CollisionProtection: CollisionProtectionPrevent, + Phases: []ClusterObjectSetPhase{{Name: "deploy", Objects: []ClusterObjectSetObject{}}}, + }, }, valid: true, }, "spec collisionProtection accepts IfNoController": { spec: ClusterObjectSetSpec{ - Group: "validity-if-no-controller", - LifecycleState: ClusterObjectSetLifecycleStateActive, - Revision: 1, - CollisionProtection: CollisionProtectionIfNoController, - Phases: []ClusterObjectSetPhase{{Name: "deploy", Objects: []ClusterObjectSetObject{}}}, + Group: "validity-if-no-controller", + LifecycleState: ClusterObjectSetLifecycleStateActive, + Revision: 1, + ClusterObjectSetContent: ClusterObjectSetContent{ + CollisionProtection: CollisionProtectionIfNoController, + Phases: []ClusterObjectSetPhase{{Name: "deploy", Objects: []ClusterObjectSetObject{}}}, + }, }, valid: true, }, "spec collisionProtection accepts None": { spec: ClusterObjectSetSpec{ - Group: "validity-allow-collisions", - LifecycleState: ClusterObjectSetLifecycleStateActive, - Revision: 1, - CollisionProtection: CollisionProtectionNone, - Phases: []ClusterObjectSetPhase{{Name: "deploy", Objects: []ClusterObjectSetObject{}}}, + Group: "validity-allow-collisions", + LifecycleState: ClusterObjectSetLifecycleStateActive, + Revision: 1, + ClusterObjectSetContent: ClusterObjectSetContent{ + CollisionProtection: CollisionProtectionNone, + Phases: []ClusterObjectSetPhase{{Name: "deploy", Objects: []ClusterObjectSetObject{}}}, + }, }, valid: true, }, @@ -382,27 +426,31 @@ func TestClusterObjectSetValidity(t *testing.T) { }, "spec collisionProtection rejects invalid values": { spec: ClusterObjectSetSpec{ - Group: "validity-invalid-collision-protection", - LifecycleState: ClusterObjectSetLifecycleStateActive, - Revision: 1, - CollisionProtection: CollisionProtection("Invalid"), + Group: "validity-invalid-collision-protection", + LifecycleState: ClusterObjectSetLifecycleStateActive, + Revision: 1, + ClusterObjectSetContent: ClusterObjectSetContent{ + CollisionProtection: CollisionProtection("Invalid"), + }, }, valid: false, expectedError: "spec.collisionProtection: Unsupported value", }, "object collisionProtection is optional": { spec: ClusterObjectSetSpec{ - Group: "validity-optional-object-collision-protection", - LifecycleState: ClusterObjectSetLifecycleStateActive, - Revision: 1, - CollisionProtection: CollisionProtectionPrevent, - Phases: []ClusterObjectSetPhase{ - { - Name: "deploy", - Objects: []ClusterObjectSetObject{ - { - Type: ObjectTypeInlineManifest, - InlineManifest: configMap(), + Group: "validity-optional-object-collision-protection", + LifecycleState: ClusterObjectSetLifecycleStateActive, + Revision: 1, + ClusterObjectSetContent: ClusterObjectSetContent{ + CollisionProtection: CollisionProtectionPrevent, + Phases: []ClusterObjectSetPhase{ + { + Name: "deploy", + Objects: []ClusterObjectSetObject{ + { + Type: ObjectTypeInlineManifest, + InlineManifest: configMap(), + }, }, }, }, @@ -412,17 +460,19 @@ func TestClusterObjectSetValidity(t *testing.T) { }, "object with inlineObject is valid": { spec: ClusterObjectSetSpec{ - Group: "validity-inline-object", - LifecycleState: ClusterObjectSetLifecycleStateActive, - Revision: 1, - CollisionProtection: CollisionProtectionPrevent, - Phases: []ClusterObjectSetPhase{ - { - Name: "deploy", - Objects: []ClusterObjectSetObject{ - { - Type: ObjectTypeInlineManifest, - InlineManifest: configMap(), + Group: "validity-inline-object", + LifecycleState: ClusterObjectSetLifecycleStateActive, + Revision: 1, + ClusterObjectSetContent: ClusterObjectSetContent{ + CollisionProtection: CollisionProtectionPrevent, + Phases: []ClusterObjectSetPhase{ + { + Name: "deploy", + Objects: []ClusterObjectSetObject{ + { + Type: ObjectTypeInlineManifest, + InlineManifest: configMap(), + }, }, }, }, @@ -432,17 +482,19 @@ func TestClusterObjectSetValidity(t *testing.T) { }, "object with secretRef is valid": { spec: ClusterObjectSetSpec{ - Group: "validity-ref-object", - LifecycleState: ClusterObjectSetLifecycleStateActive, - Revision: 1, - CollisionProtection: CollisionProtectionPrevent, - Phases: []ClusterObjectSetPhase{ - { - Name: "deploy", - Objects: []ClusterObjectSetObject{ - { - Type: ObjectTypeSecretReference, - SecretRef: SecretSourceRef{Name: "my-secret", Key: "my-key"}, + Group: "validity-ref-object", + LifecycleState: ClusterObjectSetLifecycleStateActive, + Revision: 1, + ClusterObjectSetContent: ClusterObjectSetContent{ + CollisionProtection: CollisionProtectionPrevent, + Phases: []ClusterObjectSetPhase{ + { + Name: "deploy", + Objects: []ClusterObjectSetObject{ + { + Type: ObjectTypeSecretReference, + SecretRef: SecretSourceRef{Name: "my-secret", Key: "my-key"}, + }, }, }, }, @@ -472,18 +524,20 @@ func TestClusterObjectSetValidity(t *testing.T) { }, "object with type InlineManifest but secretRef also set is invalid": { spec: ClusterObjectSetSpec{ - Group: "validity-object-and-ref", - LifecycleState: ClusterObjectSetLifecycleStateActive, - Revision: 1, - CollisionProtection: CollisionProtectionPrevent, - Phases: []ClusterObjectSetPhase{ - { - Name: "deploy", - Objects: []ClusterObjectSetObject{ - { - Type: ObjectTypeInlineManifest, - InlineManifest: configMap(), - SecretRef: SecretSourceRef{Name: "my-secret", Key: "my-key"}, + Group: "validity-object-and-ref", + LifecycleState: ClusterObjectSetLifecycleStateActive, + Revision: 1, + ClusterObjectSetContent: ClusterObjectSetContent{ + CollisionProtection: CollisionProtectionPrevent, + Phases: []ClusterObjectSetPhase{ + { + Name: "deploy", + Objects: []ClusterObjectSetObject{ + { + Type: ObjectTypeInlineManifest, + InlineManifest: configMap(), + SecretRef: SecretSourceRef{Name: "my-secret", Key: "my-key"}, + }, }, }, }, @@ -494,15 +548,17 @@ func TestClusterObjectSetValidity(t *testing.T) { }, "object without a type is invalid": { spec: ClusterObjectSetSpec{ - Group: "validity-object-and-ref-missing", - LifecycleState: ClusterObjectSetLifecycleStateActive, - Revision: 1, - CollisionProtection: CollisionProtectionPrevent, - Phases: []ClusterObjectSetPhase{ - { - Name: "deploy", - Objects: []ClusterObjectSetObject{ - {}, + Group: "validity-object-and-ref-missing", + LifecycleState: ClusterObjectSetLifecycleStateActive, + Revision: 1, + ClusterObjectSetContent: ClusterObjectSetContent{ + CollisionProtection: CollisionProtectionPrevent, + Phases: []ClusterObjectSetPhase{ + { + Name: "deploy", + Objects: []ClusterObjectSetObject{ + {}, + }, }, }, }, @@ -512,15 +568,17 @@ func TestClusterObjectSetValidity(t *testing.T) { }, "object with type InlineManifest but no object is invalid": { spec: ClusterObjectSetSpec{ - Group: "validity-object-type-no-object", - LifecycleState: ClusterObjectSetLifecycleStateActive, - Revision: 1, - CollisionProtection: CollisionProtectionPrevent, - Phases: []ClusterObjectSetPhase{ - { - Name: "deploy", - Objects: []ClusterObjectSetObject{ - {Type: ObjectTypeInlineManifest}, + Group: "validity-object-type-no-object", + LifecycleState: ClusterObjectSetLifecycleStateActive, + Revision: 1, + ClusterObjectSetContent: ClusterObjectSetContent{ + CollisionProtection: CollisionProtectionPrevent, + Phases: []ClusterObjectSetPhase{ + { + Name: "deploy", + Objects: []ClusterObjectSetObject{ + {Type: ObjectTypeInlineManifest}, + }, }, }, }, @@ -794,11 +852,13 @@ func TestClusterObjectSetCanonicalName(t *testing.T) { cos := &ClusterObjectSet{ ObjectMeta: metav1.ObjectMeta{Name: tc.name, GenerateName: tc.generateName}, Spec: ClusterObjectSetSpec{ - Group: tc.group, - Revision: tc.revision, - LifecycleState: ClusterObjectSetLifecycleStateActive, - CollisionProtection: CollisionProtectionPrevent, - Phases: []ClusterObjectSetPhase{{Name: "deploy", Objects: []ClusterObjectSetObject{}}}, + Group: tc.group, + Revision: tc.revision, + LifecycleState: ClusterObjectSetLifecycleStateActive, + ClusterObjectSetContent: ClusterObjectSetContent{ + CollisionProtection: CollisionProtectionPrevent, + Phases: []ClusterObjectSetPhase{{Name: "deploy", Objects: []ClusterObjectSetObject{}}}, + }, }, } err := c.Create(t.Context(), cos) @@ -879,11 +939,13 @@ func TestClusterObjectSetGroupFieldSelector(t *testing.T) { cos := &ClusterObjectSet{ ObjectMeta: metav1.ObjectMeta{Name: fixture.name}, Spec: ClusterObjectSetSpec{ - Group: fixture.group, - Revision: fixture.revision, - LifecycleState: ClusterObjectSetLifecycleStateActive, - CollisionProtection: CollisionProtectionPrevent, - Phases: []ClusterObjectSetPhase{{Name: "deploy", Objects: []ClusterObjectSetObject{}}}, + Group: fixture.group, + Revision: fixture.revision, + LifecycleState: ClusterObjectSetLifecycleStateActive, + ClusterObjectSetContent: ClusterObjectSetContent{ + CollisionProtection: CollisionProtectionPrevent, + Phases: []ClusterObjectSetPhase{{Name: "deploy", Objects: []ClusterObjectSetObject{}}}, + }, }, } require.NoError(t, c.Create(t.Context(), cos)) @@ -913,17 +975,19 @@ func TestClusterObjectSetGroupFieldSelector(t *testing.T) { // so that each case only varies the probe under test. func probeSpec(group string, probes ...ProgressionProbe) ClusterObjectSetSpec { return ClusterObjectSetSpec{ - Group: group, - LifecycleState: ClusterObjectSetLifecycleStateActive, - Revision: 1, - CollisionProtection: CollisionProtectionPrevent, - Phases: []ClusterObjectSetPhase{ - { - Name: "deploy", - Objects: []ClusterObjectSetObject{}, + Group: group, + LifecycleState: ClusterObjectSetLifecycleStateActive, + Revision: 1, + ClusterObjectSetContent: ClusterObjectSetContent{ + CollisionProtection: CollisionProtectionPrevent, + Phases: []ClusterObjectSetPhase{ + { + Name: "deploy", + Objects: []ClusterObjectSetObject{}, + }, }, + ProgressionProbes: probes, }, - ProgressionProbes: probes, } } @@ -940,14 +1004,16 @@ func validProbe() ProgressionProbe { // from the given ref, so that each SecretSourceRef case only varies the ref under test. func refSpec(group string, ref SecretSourceRef) ClusterObjectSetSpec { return ClusterObjectSetSpec{ - Group: group, - LifecycleState: ClusterObjectSetLifecycleStateActive, - Revision: 1, - CollisionProtection: CollisionProtectionPrevent, - Phases: []ClusterObjectSetPhase{ - { - Name: "deploy", - Objects: []ClusterObjectSetObject{{Type: ObjectTypeSecretReference, SecretRef: ref}}, + Group: group, + LifecycleState: ClusterObjectSetLifecycleStateActive, + Revision: 1, + ClusterObjectSetContent: ClusterObjectSetContent{ + CollisionProtection: CollisionProtectionPrevent, + Phases: []ClusterObjectSetPhase{ + { + Name: "deploy", + Objects: []ClusterObjectSetObject{{Type: ObjectTypeSecretReference, SecretRef: ref}}, + }, }, }, } diff --git a/api/v1/validation_test.go b/api/v1/validation_test.go index 3dbee5c68f..9304206b08 100644 --- a/api/v1/validation_test.go +++ b/api/v1/validation_test.go @@ -55,13 +55,15 @@ func TestValidate(t *testing.T) { } objectSetWithInlineManifest := func(group string, m unstructured.Unstructured) ClusterObjectSetSpec { return ClusterObjectSetSpec{ - Group: group, - Revision: 1, - CollisionProtection: CollisionProtectionPrevent, - LifecycleState: ClusterObjectSetLifecycleStateActive, - Phases: []ClusterObjectSetPhase{{Name: "deploy", Objects: []ClusterObjectSetObject{ - {Type: ObjectTypeInlineManifest, InlineManifest: m}, - }}}, + Group: group, + Revision: 1, + LifecycleState: ClusterObjectSetLifecycleStateActive, + ClusterObjectSetContent: ClusterObjectSetContent{ + CollisionProtection: CollisionProtectionPrevent, + Phases: []ClusterObjectSetPhase{{Name: "deploy", Objects: []ClusterObjectSetObject{ + {Type: ObjectTypeInlineManifest, InlineManifest: m}, + }}}, + }, } } c := newClient(t) @@ -265,9 +267,11 @@ func newRevisionHistoryLimitCOD(name string, limit *int32) *ClusterObjectDeploym RevisionHistoryLimit: limit, Template: ClusterObjectDeploymentTemplate{ Spec: ClusterObjectDeploymentTemplateSpec{ - CollisionProtection: CollisionProtectionPrevent, - Phases: []ClusterObjectSetPhase{ - {Name: "deploy", Objects: []ClusterObjectSetObject{{Type: ObjectTypeInlineManifest, InlineManifest: configMap()}}}, + ClusterObjectSetContent: ClusterObjectSetContent{ + CollisionProtection: CollisionProtectionPrevent, + Phases: []ClusterObjectSetPhase{ + {Name: "deploy", Objects: []ClusterObjectSetObject{{Type: ObjectTypeInlineManifest, InlineManifest: configMap()}}}, + }, }, }, }, @@ -315,11 +319,13 @@ func TestClusterObjectSetCompletedAtImmutable(t *testing.T) { cos := &ClusterObjectSet{ ObjectMeta: metav1.ObjectMeta{Name: "cos-completedat-immutable-1"}, Spec: ClusterObjectSetSpec{ - Group: "cos-completedat-immutable", - Revision: 1, - CollisionProtection: CollisionProtectionPrevent, - LifecycleState: ClusterObjectSetLifecycleStateActive, - Phases: []ClusterObjectSetPhase{{Name: "deploy", Objects: []ClusterObjectSetObject{}}}, + Group: "cos-completedat-immutable", + Revision: 1, + LifecycleState: ClusterObjectSetLifecycleStateActive, + ClusterObjectSetContent: ClusterObjectSetContent{ + CollisionProtection: CollisionProtectionPrevent, + Phases: []ClusterObjectSetPhase{{Name: "deploy", Objects: []ClusterObjectSetObject{}}}, + }, }, } require.NoError(t, c.Create(t.Context(), cos)) @@ -347,11 +353,13 @@ func TestClusterObjectSetCompletedAtCannotBeRemoved(t *testing.T) { cos := &ClusterObjectSet{ ObjectMeta: metav1.ObjectMeta{Name: "cos-completedat-noremove-1"}, Spec: ClusterObjectSetSpec{ - Group: "cos-completedat-noremove", - Revision: 1, - CollisionProtection: CollisionProtectionPrevent, - LifecycleState: ClusterObjectSetLifecycleStateActive, - Phases: []ClusterObjectSetPhase{{Name: "deploy", Objects: []ClusterObjectSetObject{}}}, + Group: "cos-completedat-noremove", + Revision: 1, + LifecycleState: ClusterObjectSetLifecycleStateActive, + ClusterObjectSetContent: ClusterObjectSetContent{ + CollisionProtection: CollisionProtectionPrevent, + Phases: []ClusterObjectSetPhase{{Name: "deploy", Objects: []ClusterObjectSetObject{}}}, + }, }, } require.NoError(t, c.Create(t.Context(), cos)) @@ -374,11 +382,13 @@ func TestClusterObjectSetDigestImmutable(t *testing.T) { cos := &ClusterObjectSet{ ObjectMeta: metav1.ObjectMeta{Name: "cos-digest-immutable-1"}, Spec: ClusterObjectSetSpec{ - Group: "cos-digest-immutable", - Revision: 1, - CollisionProtection: CollisionProtectionPrevent, - LifecycleState: ClusterObjectSetLifecycleStateActive, - Phases: []ClusterObjectSetPhase{{Name: "deploy", Objects: []ClusterObjectSetObject{}}}, + Group: "cos-digest-immutable", + Revision: 1, + LifecycleState: ClusterObjectSetLifecycleStateActive, + ClusterObjectSetContent: ClusterObjectSetContent{ + CollisionProtection: CollisionProtectionPrevent, + Phases: []ClusterObjectSetPhase{{Name: "deploy", Objects: []ClusterObjectSetObject{}}}, + }, }, } require.NoError(t, c.Create(t.Context(), cos)) diff --git a/api/v1/zz_generated.deepcopy.go b/api/v1/zz_generated.deepcopy.go index e5a8a5f61a..e5627866a4 100644 --- a/api/v1/zz_generated.deepcopy.go +++ b/api/v1/zz_generated.deepcopy.go @@ -567,20 +567,7 @@ func (in *ClusterObjectDeploymentTemplateMetadata) DeepCopy() *ClusterObjectDepl // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ClusterObjectDeploymentTemplateSpec) DeepCopyInto(out *ClusterObjectDeploymentTemplateSpec) { *out = *in - if in.Phases != nil { - in, out := &in.Phases, &out.Phases - *out = make([]ClusterObjectSetPhase, len(*in)) - for i := range *in { - (*in)[i].DeepCopyInto(&(*out)[i]) - } - } - if in.ProgressionProbes != nil { - in, out := &in.ProgressionProbes, &out.ProgressionProbes - *out = make([]ProgressionProbe, len(*in)) - for i := range *in { - (*in)[i].DeepCopyInto(&(*out)[i]) - } - } + in.ClusterObjectSetContent.DeepCopyInto(&out.ClusterObjectSetContent) } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClusterObjectDeploymentTemplateSpec. @@ -620,6 +607,35 @@ func (in *ClusterObjectSet) DeepCopyObject() runtime.Object { return nil } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ClusterObjectSetContent) DeepCopyInto(out *ClusterObjectSetContent) { + *out = *in + if in.Phases != nil { + in, out := &in.Phases, &out.Phases + *out = make([]ClusterObjectSetPhase, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.ProgressionProbes != nil { + in, out := &in.ProgressionProbes, &out.ProgressionProbes + *out = make([]ProgressionProbe, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClusterObjectSetContent. +func (in *ClusterObjectSetContent) DeepCopy() *ClusterObjectSetContent { + if in == nil { + return nil + } + out := new(ClusterObjectSetContent) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ClusterObjectSetList) DeepCopyInto(out *ClusterObjectSetList) { *out = *in @@ -694,20 +710,7 @@ func (in *ClusterObjectSetPhase) DeepCopy() *ClusterObjectSetPhase { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ClusterObjectSetSpec) DeepCopyInto(out *ClusterObjectSetSpec) { *out = *in - if in.Phases != nil { - in, out := &in.Phases, &out.Phases - *out = make([]ClusterObjectSetPhase, len(*in)) - for i := range *in { - (*in)[i].DeepCopyInto(&(*out)[i]) - } - } - if in.ProgressionProbes != nil { - in, out := &in.ProgressionProbes, &out.ProgressionProbes - *out = make([]ProgressionProbe, len(*in)) - for i := range *in { - (*in)[i].DeepCopyInto(&(*out)[i]) - } - } + in.ClusterObjectSetContent.DeepCopyInto(&out.ClusterObjectSetContent) } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClusterObjectSetSpec. diff --git a/applyconfigurations/api/v1/clusterobjectdeploymenttemplatespec.go b/applyconfigurations/api/v1/clusterobjectdeploymenttemplatespec.go index 8f3010dc52..9997f4479e 100644 --- a/applyconfigurations/api/v1/clusterobjectdeploymenttemplatespec.go +++ b/applyconfigurations/api/v1/clusterobjectdeploymenttemplatespec.go @@ -27,19 +27,10 @@ import ( // // ClusterObjectDeploymentTemplateSpec contains mutable ClusterObjectSet content. type ClusterObjectDeploymentTemplateSpecApplyConfiguration struct { - // phases is an ordered list of 1 to 20 uniquely named phases. Each phase contains - // at most 50 objects, each expressed either as an inlineManifest (an embedded - // Kubernetes object) or a secretRef (a reference to an immutable Secret holding - // a serialized manifest). Phase, object, and probe order participate in template identity. - Phases []ClusterObjectSetPhaseApplyConfiguration `json:"phases,omitempty"` - // progressionProbes supplies custom readiness probes for objects in every phase. - // The same selector and assertion semantics as ClusterObjectSet.spec.progressionProbes apply. - // When set, must contain 1 to 20 probes. - ProgressionProbes []ProgressionProbeApplyConfiguration `json:"progressionProbes,omitempty"` - // collisionProtection is the required default collision protection strategy for all objects - // in the revision. Individual phases or objects may override this value. - // The resolution order is: object > phase > spec. - CollisionProtection *apiv1.CollisionProtection `json:"collisionProtection,omitempty"` + // ClusterObjectSetContent holds the mutable revision content that is + // propagated to each ClusterObjectSet created from this template. Changes + // to these fields create a new revision; existing revisions remain immutable. + ClusterObjectSetContentApplyConfiguration `json:""` } // ClusterObjectDeploymentTemplateSpecApplyConfiguration constructs a declarative configuration of the ClusterObjectDeploymentTemplateSpec type for use with @@ -56,7 +47,7 @@ func (b *ClusterObjectDeploymentTemplateSpecApplyConfiguration) WithPhases(value if values[i] == nil { panic("nil value passed to WithPhases") } - b.Phases = append(b.Phases, *values[i]) + b.ClusterObjectSetContentApplyConfiguration.Phases = append(b.ClusterObjectSetContentApplyConfiguration.Phases, *values[i]) } return b } @@ -69,7 +60,7 @@ func (b *ClusterObjectDeploymentTemplateSpecApplyConfiguration) WithProgressionP if values[i] == nil { panic("nil value passed to WithProgressionProbes") } - b.ProgressionProbes = append(b.ProgressionProbes, *values[i]) + b.ClusterObjectSetContentApplyConfiguration.ProgressionProbes = append(b.ClusterObjectSetContentApplyConfiguration.ProgressionProbes, *values[i]) } return b } @@ -78,6 +69,6 @@ func (b *ClusterObjectDeploymentTemplateSpecApplyConfiguration) WithProgressionP // and returns the receiver, so that objects can be built by chaining "With" function invocations. // If called multiple times, the CollisionProtection field is set to the value of the last call. func (b *ClusterObjectDeploymentTemplateSpecApplyConfiguration) WithCollisionProtection(value apiv1.CollisionProtection) *ClusterObjectDeploymentTemplateSpecApplyConfiguration { - b.CollisionProtection = &value + b.ClusterObjectSetContentApplyConfiguration.CollisionProtection = &value return b } diff --git a/applyconfigurations/api/v1/clusterobjectsetcontent.go b/applyconfigurations/api/v1/clusterobjectsetcontent.go new file mode 100644 index 0000000000..70fcaea93a --- /dev/null +++ b/applyconfigurations/api/v1/clusterobjectsetcontent.go @@ -0,0 +1,107 @@ +/* +Copyright 2026. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by controller-gen. DO NOT EDIT. + +package v1 + +import ( + apiv1 "github.com/operator-framework/operator-controller/api/v1" +) + +// ClusterObjectSetContentApplyConfiguration represents a declarative configuration of the ClusterObjectSetContent type for use +// with apply. +// +// ClusterObjectSetContent holds the revision content shared between +// ClusterObjectSet.spec and a ClusterObjectDeployment's template spec. +type ClusterObjectSetContentApplyConfiguration struct { + // phases is a required list of phases that group objects to be applied together. + // + // Objects are organized into phases based on their Group-Kind. Common phases include: + // - namespaces: Namespace objects + // - policies: ResourceQuota, LimitRange, NetworkPolicy objects + // - rbac: ServiceAccount, Role, RoleBinding, ClusterRole, ClusterRoleBinding objects + // - crds: CustomResourceDefinition objects + // - storage: PersistentVolume, PersistentVolumeClaim, StorageClass objects + // - deploy: Deployment, StatefulSet, DaemonSet, Service, ConfigMap, Secret objects + // - publish: Ingress, APIService, Route, Webhook objects + // + // All objects in a phase are applied in no particular order. + // The revision progresses to the next phase only after all objects in the current phase pass their readiness probes. + // + // Each phase in the list must have a unique name. The maximum number of phases is 20. + Phases []ClusterObjectSetPhaseApplyConfiguration `json:"phases,omitempty"` + // progressionProbes is an optional field which provides the ability to define custom readiness probes + // for objects defined within spec.phases. As documented in that field, most kubernetes-native objects + // within the phases already have some kind of readiness check built-in, but this field allows for checks + // which are tailored to the objects being rolled out - particularly custom resources. + // + // Probes defined within the progressionProbes list will apply to every phase in the revision. However, the probes will only + // execute against phase objects which are a match for the provided selector type. For instance, a probe using a GroupKind selector + // for ConfigMaps will automatically be considered to have passed for any non-ConfigMap object, but will halt any phase containing + // a ConfigMap if that particular object does not pass the probe check. + // + // The maximum number of probes is 20. + // + // + ProgressionProbes []ProgressionProbeApplyConfiguration `json:"progressionProbes,omitempty"` + // collisionProtection is a required field that specifies the default collision protection + // strategy for all objects in this revision. Individual phases or objects can override this value. + // + // This value is used as the default for any phase or object that does not explicitly specify + // its own collisionProtection. The resolution order is: object > phase > spec. + CollisionProtection *apiv1.CollisionProtection `json:"collisionProtection,omitempty"` +} + +// ClusterObjectSetContentApplyConfiguration constructs a declarative configuration of the ClusterObjectSetContent type for use with +// apply. +func ClusterObjectSetContent() *ClusterObjectSetContentApplyConfiguration { + return &ClusterObjectSetContentApplyConfiguration{} +} + +// WithPhases adds the given value to the Phases field in the declarative configuration +// and returns the receiver, so that objects can be build by chaining "With" function invocations. +// If called multiple times, values provided by each call will be appended to the Phases field. +func (b *ClusterObjectSetContentApplyConfiguration) WithPhases(values ...*ClusterObjectSetPhaseApplyConfiguration) *ClusterObjectSetContentApplyConfiguration { + for i := range values { + if values[i] == nil { + panic("nil value passed to WithPhases") + } + b.Phases = append(b.Phases, *values[i]) + } + return b +} + +// WithProgressionProbes adds the given value to the ProgressionProbes field in the declarative configuration +// and returns the receiver, so that objects can be build by chaining "With" function invocations. +// If called multiple times, values provided by each call will be appended to the ProgressionProbes field. +func (b *ClusterObjectSetContentApplyConfiguration) WithProgressionProbes(values ...*ProgressionProbeApplyConfiguration) *ClusterObjectSetContentApplyConfiguration { + for i := range values { + if values[i] == nil { + panic("nil value passed to WithProgressionProbes") + } + b.ProgressionProbes = append(b.ProgressionProbes, *values[i]) + } + return b +} + +// WithCollisionProtection sets the CollisionProtection field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the CollisionProtection field is set to the value of the last call. +func (b *ClusterObjectSetContentApplyConfiguration) WithCollisionProtection(value apiv1.CollisionProtection) *ClusterObjectSetContentApplyConfiguration { + b.CollisionProtection = &value + return b +} diff --git a/applyconfigurations/api/v1/clusterobjectsetspec.go b/applyconfigurations/api/v1/clusterobjectsetspec.go index 328f8284cc..28ce0e5fbb 100644 --- a/applyconfigurations/api/v1/clusterobjectsetspec.go +++ b/applyconfigurations/api/v1/clusterobjectsetspec.go @@ -26,6 +26,8 @@ import ( // with apply. // // ClusterObjectSetSpec defines the desired state of ClusterObjectSet. +// The content fields (phases, progressionProbes, collisionProtection) are +// immutable: once set they may only be changed by creating a new revision. type ClusterObjectSetSpecApplyConfiguration struct { // group is a required, immutable identifier that links related revisions together. // All revisions sharing the same group form an ordered sequence, regardless of owner references. @@ -51,47 +53,11 @@ type ClusterObjectSetSpecApplyConfiguration struct { // The revision field must be an integer from 1 to 9999999999, inclusive. // Each ClusterObjectSet in the same group must have a unique revision number. Revision *int64 `json:"revision,omitempty"` - // phases is a required, immutable list of phases that group objects to be applied together. - // - // Objects are organized into phases based on their Group-Kind. Common phases include: - // - namespaces: Namespace objects - // - policies: ResourceQuota, LimitRange, NetworkPolicy objects - // - rbac: ServiceAccount, Role, RoleBinding, ClusterRole, ClusterRoleBinding objects - // - crds: CustomResourceDefinition objects - // - storage: PersistentVolume, PersistentVolumeClaim, StorageClass objects - // - deploy: Deployment, StatefulSet, DaemonSet, Service, ConfigMap, Secret objects - // - publish: Ingress, APIService, Route, Webhook objects - // - // All objects in a phase are applied in no particular order. - // The revision progresses to the next phase only after all objects in the current phase pass their readiness probes. - // - // Once set, the phases field is immutable. - // - // Each phase in the list must have a unique name. The maximum number of phases is 20. - Phases []ClusterObjectSetPhaseApplyConfiguration `json:"phases,omitempty"` - // progressionProbes is an optional field which provides the ability to define custom readiness probes - // for objects defined within spec.phases. As documented in that field, most kubernetes-native objects - // within the phases already have some kind of readiness check built-in, but this field allows for checks - // which are tailored to the objects being rolled out - particularly custom resources. - // - // Probes defined within the progressionProbes list will apply to every phase in the revision. However, the probes will only - // execute against phase objects which are a match for the provided selector type. For instance, a probe using a GroupKind selector - // for ConfigMaps will automatically be considered to have passed for any non-ConfigMap object, but will halt any phase containing - // a ConfigMap if that particular object does not pass the probe check. - // - // The maximum number of probes is 20. - // - // Once set, the progressionProbes field is immutable, as it is - // part of the immutable content of a revision. - // - // - ProgressionProbes []ProgressionProbeApplyConfiguration `json:"progressionProbes,omitempty"` - // collisionProtection is a required field that specifies the default collision protection - // strategy for all objects in this revision. Individual phases or objects can override this value. - // - // This value is used as the default for any phase or object that does not explicitly specify - // its own collisionProtection. The resolution order is: object > phase > spec. - CollisionProtection *apiv1.CollisionProtection `json:"collisionProtection,omitempty"` + // ClusterObjectSetContent holds the immutable revision content shared with + // the ClusterObjectDeployment template. Immutability of these fields is + // enforced at the ClusterObjectSetSpec level, not here, so that the same + // structure can be embedded mutably in a ClusterObjectDeployment template. + ClusterObjectSetContentApplyConfiguration `json:""` } // ClusterObjectSetSpecApplyConfiguration constructs a declarative configuration of the ClusterObjectSetSpec type for use with @@ -132,7 +98,7 @@ func (b *ClusterObjectSetSpecApplyConfiguration) WithPhases(values ...*ClusterOb if values[i] == nil { panic("nil value passed to WithPhases") } - b.Phases = append(b.Phases, *values[i]) + b.ClusterObjectSetContentApplyConfiguration.Phases = append(b.ClusterObjectSetContentApplyConfiguration.Phases, *values[i]) } return b } @@ -145,7 +111,7 @@ func (b *ClusterObjectSetSpecApplyConfiguration) WithProgressionProbes(values .. if values[i] == nil { panic("nil value passed to WithProgressionProbes") } - b.ProgressionProbes = append(b.ProgressionProbes, *values[i]) + b.ClusterObjectSetContentApplyConfiguration.ProgressionProbes = append(b.ClusterObjectSetContentApplyConfiguration.ProgressionProbes, *values[i]) } return b } @@ -154,6 +120,6 @@ func (b *ClusterObjectSetSpecApplyConfiguration) WithProgressionProbes(values .. // and returns the receiver, so that objects can be built by chaining "With" function invocations. // If called multiple times, the CollisionProtection field is set to the value of the last call. func (b *ClusterObjectSetSpecApplyConfiguration) WithCollisionProtection(value apiv1.CollisionProtection) *ClusterObjectSetSpecApplyConfiguration { - b.CollisionProtection = &value + b.ClusterObjectSetContentApplyConfiguration.CollisionProtection = &value return b } diff --git a/applyconfigurations/utils.go b/applyconfigurations/utils.go index 793ad57468..f0bc2bb5c8 100644 --- a/applyconfigurations/utils.go +++ b/applyconfigurations/utils.go @@ -74,6 +74,8 @@ func ForKind(kind schema.GroupVersionKind) interface{} { return &apiv1.ClusterObjectDeploymentTemplateSpecApplyConfiguration{} case v1.SchemeGroupVersion.WithKind("ClusterObjectSet"): return &apiv1.ClusterObjectSetApplyConfiguration{} + case v1.SchemeGroupVersion.WithKind("ClusterObjectSetContent"): + return &apiv1.ClusterObjectSetContentApplyConfiguration{} case v1.SchemeGroupVersion.WithKind("ClusterObjectSetObject"): return &apiv1.ClusterObjectSetObjectApplyConfiguration{} case v1.SchemeGroupVersion.WithKind("ClusterObjectSetPhase"): diff --git a/cmd/object-controller/main_test.go b/cmd/object-controller/main_test.go index a646da7ef8..7299e5da85 100644 --- a/cmd/object-controller/main_test.go +++ b/cmd/object-controller/main_test.go @@ -118,8 +118,10 @@ func TestStandaloneController(t *testing.T) { Spec: ocv1.ClusterObjectSetSpec{ Group: name, LifecycleState: ocv1.ClusterObjectSetLifecycleStateActive, Revision: 1, - CollisionProtection: ocv1.CollisionProtectionPrevent, - Phases: []ocv1.ClusterObjectSetPhase{{Name: "deploy", Objects: []ocv1.ClusterObjectSetObject{obj}}}, + ClusterObjectSetContent: ocv1.ClusterObjectSetContent{ + CollisionProtection: ocv1.CollisionProtectionPrevent, + Phases: []ocv1.ClusterObjectSetPhase{{Name: "deploy", Objects: []ocv1.ClusterObjectSetObject{obj}}}, + }, }, } require.NoError(t, cl.Create(ctx, cos)) diff --git a/docs/api-reference/olmv1-api-reference.md b/docs/api-reference/olmv1-api-reference.md index 73bd8af5be..5e331a8205 100644 --- a/docs/api-reference/olmv1-api-reference.md +++ b/docs/api-reference/olmv1-api-reference.md @@ -512,9 +512,28 @@ _Appears in:_ | Field | Description | Default | Validation | | --- | --- | --- | --- | -| `phases` _[ClusterObjectSetPhase](#clusterobjectsetphase) array_ | phases is an ordered list of 1 to 20 uniquely named phases. Each phase contains
at most 50 objects, each expressed either as an inlineManifest (an embedded
Kubernetes object) or a secretRef (a reference to an immutable Secret holding
a serialized manifest). Phase, object, and probe order participate in template identity. | | MaxItems: 20
MinItems: 1
Required: \{\}
| -| `progressionProbes` _[ProgressionProbe](#progressionprobe) array_ | progressionProbes supplies custom readiness probes for objects in every phase.
The same selector and assertion semantics as ClusterObjectSet.spec.progressionProbes apply.
When set, must contain 1 to 20 probes. | | MaxItems: 20
MinItems: 1
Optional: \{\}
| -| `collisionProtection` _[CollisionProtection](#collisionprotection)_ | collisionProtection is the required default collision protection strategy for all objects
in the revision. Individual phases or objects may override this value.
The resolution order is: object > phase > spec. | | Enum: [Prevent IfNoController None]
Required: \{\}
| +| `phases` _[ClusterObjectSetPhase](#clusterobjectsetphase) array_ | phases is a required list of phases that group objects to be applied together.
Objects are organized into phases based on their Group-Kind. Common phases include:
- namespaces: Namespace objects
- policies: ResourceQuota, LimitRange, NetworkPolicy objects
- rbac: ServiceAccount, Role, RoleBinding, ClusterRole, ClusterRoleBinding objects
- crds: CustomResourceDefinition objects
- storage: PersistentVolume, PersistentVolumeClaim, StorageClass objects
- deploy: Deployment, StatefulSet, DaemonSet, Service, ConfigMap, Secret objects
- publish: Ingress, APIService, Route, Webhook objects
All objects in a phase are applied in no particular order.
The revision progresses to the next phase only after all objects in the current phase pass their readiness probes.
Each phase in the list must have a unique name. The maximum number of phases is 20. | | MaxItems: 20
MinItems: 1
Required: \{\}
| +| `progressionProbes` _[ProgressionProbe](#progressionprobe) array_ | progressionProbes is an optional field which provides the ability to define custom readiness probes
for objects defined within spec.phases. As documented in that field, most kubernetes-native objects
within the phases already have some kind of readiness check built-in, but this field allows for checks
which are tailored to the objects being rolled out - particularly custom resources.
Probes defined within the progressionProbes list will apply to every phase in the revision. However, the probes will only
execute against phase objects which are a match for the provided selector type. For instance, a probe using a GroupKind selector
for ConfigMaps will automatically be considered to have passed for any non-ConfigMap object, but will halt any phase containing
a ConfigMap if that particular object does not pass the probe check.
The maximum number of probes is 20.
**Experimental channel:** | | MaxItems: 20
MinItems: 1
Optional: \{\}
| +| `collisionProtection` _[CollisionProtection](#collisionprotection)_ | collisionProtection is a required field that specifies the default collision protection
strategy for all objects in this revision. Individual phases or objects can override this value.
This value is used as the default for any phase or object that does not explicitly specify
its own collisionProtection. The resolution order is: object > phase > spec. | | Enum: [Prevent IfNoController None]
Required: \{\}
| + + +#### ClusterObjectSetContent + + + +ClusterObjectSetContent holds the revision content shared between +ClusterObjectSet.spec and a ClusterObjectDeployment's template spec. + + + +_Appears in:_ +- [ClusterObjectDeploymentTemplateSpec](#clusterobjectdeploymenttemplatespec) + +| Field | Description | Default | Validation | +| --- | --- | --- | --- | +| `phases` _[ClusterObjectSetPhase](#clusterobjectsetphase) array_ | phases is a required list of phases that group objects to be applied together.
Objects are organized into phases based on their Group-Kind. Common phases include:
- namespaces: Namespace objects
- policies: ResourceQuota, LimitRange, NetworkPolicy objects
- rbac: ServiceAccount, Role, RoleBinding, ClusterRole, ClusterRoleBinding objects
- crds: CustomResourceDefinition objects
- storage: PersistentVolume, PersistentVolumeClaim, StorageClass objects
- deploy: Deployment, StatefulSet, DaemonSet, Service, ConfigMap, Secret objects
- publish: Ingress, APIService, Route, Webhook objects
All objects in a phase are applied in no particular order.
The revision progresses to the next phase only after all objects in the current phase pass their readiness probes.
Each phase in the list must have a unique name. The maximum number of phases is 20. | | MaxItems: 20
MinItems: 1
Required: \{\}
| +| `progressionProbes` _[ProgressionProbe](#progressionprobe) array_ | progressionProbes is an optional field which provides the ability to define custom readiness probes
for objects defined within spec.phases. As documented in that field, most kubernetes-native objects
within the phases already have some kind of readiness check built-in, but this field allows for checks
which are tailored to the objects being rolled out - particularly custom resources.
Probes defined within the progressionProbes list will apply to every phase in the revision. However, the probes will only
execute against phase objects which are a match for the provided selector type. For instance, a probe using a GroupKind selector
for ConfigMaps will automatically be considered to have passed for any non-ConfigMap object, but will halt any phase containing
a ConfigMap if that particular object does not pass the probe check.
The maximum number of probes is 20.
**Experimental channel:** | | MaxItems: 20
MinItems: 1
Optional: \{\}
| +| `collisionProtection` _[CollisionProtection](#collisionprotection)_ | collisionProtection is a required field that specifies the default collision protection
strategy for all objects in this revision. Individual phases or objects can override this value.
This value is used as the default for any phase or object that does not explicitly specify
its own collisionProtection. The resolution order is: object > phase > spec. | | Enum: [Prevent IfNoController None]
Required: \{\}
| @@ -552,6 +571,7 @@ complete only after all objects pass their status probes. _Appears in:_ - [ClusterObjectDeploymentTemplateSpec](#clusterobjectdeploymenttemplatespec) +- [ClusterObjectSetContent](#clusterobjectsetcontent) | Field | Description | Default | Validation | | --- | --- | --- | --- | @@ -571,6 +591,7 @@ _Validation:_ _Appears in:_ - [ClusterObjectDeploymentTemplateSpec](#clusterobjectdeploymenttemplatespec) +- [ClusterObjectSetContent](#clusterobjectsetcontent) - [ClusterObjectSetObject](#clusterobjectsetobject) - [ClusterObjectSetPhase](#clusterobjectsetphase) @@ -738,6 +759,7 @@ ProgressionProbe provides a custom probe definition, consisting of an object sel _Appears in:_ - [ClusterObjectDeploymentTemplateSpec](#clusterobjectdeploymenttemplatespec) +- [ClusterObjectSetContent](#clusterobjectsetcontent) | Field | Description | Default | Validation | | --- | --- | --- | --- | diff --git a/helm/olmv1/base/object-controller/crd/experimental/olm.operatorframework.io_clusterobjectdeployments.yaml b/helm/olmv1/base/object-controller/crd/experimental/olm.operatorframework.io_clusterobjectdeployments.yaml index 138c5675f4..d79582dcee 100644 --- a/helm/olmv1/base/object-controller/crd/experimental/olm.operatorframework.io_clusterobjectdeployments.yaml +++ b/helm/olmv1/base/object-controller/crd/experimental/olm.operatorframework.io_clusterobjectdeployments.yaml @@ -127,9 +127,11 @@ spec: properties: collisionProtection: description: |- - collisionProtection is the required default collision protection strategy for all objects - in the revision. Individual phases or objects may override this value. - The resolution order is: object > phase > spec. + collisionProtection is a required field that specifies the default collision protection + strategy for all objects in this revision. Individual phases or objects can override this value. + + This value is used as the default for any phase or object that does not explicitly specify + its own collisionProtection. The resolution order is: object > phase > spec. enum: - Prevent - IfNoController @@ -137,10 +139,21 @@ spec: type: string phases: description: |- - phases is an ordered list of 1 to 20 uniquely named phases. Each phase contains - at most 50 objects, each expressed either as an inlineManifest (an embedded - Kubernetes object) or a secretRef (a reference to an immutable Secret holding - a serialized manifest). Phase, object, and probe order participate in template identity. + phases is a required list of phases that group objects to be applied together. + + Objects are organized into phases based on their Group-Kind. Common phases include: + - namespaces: Namespace objects + - policies: ResourceQuota, LimitRange, NetworkPolicy objects + - rbac: ServiceAccount, Role, RoleBinding, ClusterRole, ClusterRoleBinding objects + - crds: CustomResourceDefinition objects + - storage: PersistentVolume, PersistentVolumeClaim, StorageClass objects + - deploy: Deployment, StatefulSet, DaemonSet, Service, ConfigMap, Secret objects + - publish: Ingress, APIService, Route, Webhook objects + + All objects in a phase are applied in no particular order. + The revision progresses to the next phase only after all objects in the current phase pass their readiness probes. + + Each phase in the list must have a unique name. The maximum number of phases is 20. items: description: |- ClusterObjectSetPhase represents a group of objects that are applied together. The phase is considered @@ -326,9 +339,17 @@ spec: x-kubernetes-list-type: map progressionProbes: description: |- - progressionProbes supplies custom readiness probes for objects in every phase. - The same selector and assertion semantics as ClusterObjectSet.spec.progressionProbes apply. - When set, must contain 1 to 20 probes. + progressionProbes is an optional field which provides the ability to define custom readiness probes + for objects defined within spec.phases. As documented in that field, most kubernetes-native objects + within the phases already have some kind of readiness check built-in, but this field allows for checks + which are tailored to the objects being rolled out - particularly custom resources. + + Probes defined within the progressionProbes list will apply to every phase in the revision. However, the probes will only + execute against phase objects which are a match for the provided selector type. For instance, a probe using a GroupKind selector + for ConfigMaps will automatically be considered to have passed for any non-ConfigMap object, but will halt any phase containing + a ConfigMap if that particular object does not pass the probe check. + + The maximum number of probes is 20. items: description: ProgressionProbe provides a custom probe definition, consisting of an object selection method and assertions. diff --git a/helm/olmv1/base/object-controller/crd/experimental/olm.operatorframework.io_clusterobjectsets.yaml b/helm/olmv1/base/object-controller/crd/experimental/olm.operatorframework.io_clusterobjectsets.yaml index deed373a72..6fdbae4d7b 100644 --- a/helm/olmv1/base/object-controller/crd/experimental/olm.operatorframework.io_clusterobjectsets.yaml +++ b/helm/olmv1/base/object-controller/crd/experimental/olm.operatorframework.io_clusterobjectsets.yaml @@ -89,9 +89,6 @@ spec: - IfNoController - None type: string - x-kubernetes-validations: - - message: collisionProtection is immutable - rule: self == oldSelf group: description: |- group is a required, immutable identifier that links related revisions together. @@ -132,7 +129,7 @@ spec: self phases: description: |- - phases is a required, immutable list of phases that group objects to be applied together. + phases is a required list of phases that group objects to be applied together. Objects are organized into phases based on their Group-Kind. Common phases include: - namespaces: Namespace objects @@ -146,8 +143,6 @@ spec: All objects in a phase are applied in no particular order. The revision progresses to the next phase only after all objects in the current phase pass their readiness probes. - Once set, the phases field is immutable. - Each phase in the list must have a unique name. The maximum number of phases is 20. items: description: |- @@ -327,9 +322,6 @@ spec: x-kubernetes-list-map-keys: - name x-kubernetes-list-type: map - x-kubernetes-validations: - - message: phases is immutable - rule: self == oldSelf || oldSelf.size() == 0 progressionProbes: description: |- progressionProbes is an optional field which provides the ability to define custom readiness probes @@ -343,9 +335,6 @@ spec: a ConfigMap if that particular object does not pass the probe check. The maximum number of probes is 20. - - Once set, the progressionProbes field is immutable, as it is - part of the immutable content of a revision. items: description: ProgressionProbe provides a custom probe definition, consisting of an object selection method and assertions. @@ -596,9 +585,6 @@ spec: minItems: 1 type: array x-kubernetes-list-type: atomic - x-kubernetes-validations: - - message: progressionProbes is immutable - rule: self == oldSelf || oldSelf.size() == 0 revision: description: |- revision is a required, immutable sequence number identifying a specific @@ -623,8 +609,16 @@ spec: x-kubernetes-validations: - message: phases cannot be removed once set rule: '!has(oldSelf.phases) || has(self.phases)' - - message: progressionProbes cannot be removed once set - rule: '!has(oldSelf.progressionProbes) || has(self.progressionProbes)' + - message: phases is immutable + rule: '!has(oldSelf.phases) || self.phases == oldSelf.phases || oldSelf.phases.size() + == 0' + - message: progressionProbes is immutable + rule: '!has(oldSelf.progressionProbes) || !has(self.progressionProbes) + && oldSelf.progressionProbes.size() == 0 || has(self.progressionProbes) + && (self.progressionProbes == oldSelf.progressionProbes || oldSelf.progressionProbes.size() + == 0)' + - message: collisionProtection is immutable + rule: self.collisionProtection == oldSelf.collisionProtection status: description: |- status is optional and defines the observed state of the ClusterObjectSet. diff --git a/internal/object-controller/controllers/clusterobjectdeployment_controller.go b/internal/object-controller/controllers/clusterobjectdeployment_controller.go index 2c1a1a1da9..3237582a36 100644 --- a/internal/object-controller/controllers/clusterobjectdeployment_controller.go +++ b/internal/object-controller/controllers/clusterobjectdeployment_controller.go @@ -663,12 +663,10 @@ func buildClusterObjectSet(cod *ocv1.ClusterObjectDeployment, revision int64, ha Annotations: maps.Clone(cod.Spec.Template.Metadata.Annotations), }, Spec: ocv1.ClusterObjectSetSpec{ - Group: cod.Name, - Revision: revision, - LifecycleState: ocv1.ClusterObjectSetLifecycleStateActive, - Phases: cod.Spec.Template.Spec.Phases, - ProgressionProbes: cod.Spec.Template.Spec.ProgressionProbes, - CollisionProtection: cod.Spec.Template.Spec.CollisionProtection, + Group: cod.Name, + Revision: revision, + LifecycleState: ocv1.ClusterObjectSetLifecycleStateActive, + ClusterObjectSetContent: cod.Spec.Template.Spec.ClusterObjectSetContent, }, } } diff --git a/internal/object-controller/controllers/clusterobjectdeployment_controller_internal_test.go b/internal/object-controller/controllers/clusterobjectdeployment_controller_internal_test.go index 75d432cb9d..841eea5867 100644 --- a/internal/object-controller/controllers/clusterobjectdeployment_controller_internal_test.go +++ b/internal/object-controller/controllers/clusterobjectdeployment_controller_internal_test.go @@ -22,16 +22,18 @@ import ( func testTemplate() ocv1.ClusterObjectDeploymentTemplate { return ocv1.ClusterObjectDeploymentTemplate{ Spec: ocv1.ClusterObjectDeploymentTemplateSpec{ - CollisionProtection: ocv1.CollisionProtectionNone, - Phases: []ocv1.ClusterObjectSetPhase{ - { - Name: "resources", - Objects: []ocv1.ClusterObjectSetObject{ - {Type: ocv1.ObjectTypeInlineManifest, InlineManifest: unstructured.Unstructured{Object: map[string]interface{}{ - "apiVersion": "v1", - "kind": "ConfigMap", - "metadata": map[string]interface{}{"name": "cm", "namespace": "ns"}, - }}}, + ClusterObjectSetContent: ocv1.ClusterObjectSetContent{ + CollisionProtection: ocv1.CollisionProtectionNone, + Phases: []ocv1.ClusterObjectSetPhase{ + { + Name: "resources", + Objects: []ocv1.ClusterObjectSetObject{ + {Type: ocv1.ObjectTypeInlineManifest, InlineManifest: unstructured.Unstructured{Object: map[string]interface{}{ + "apiVersion": "v1", + "kind": "ConfigMap", + "metadata": map[string]interface{}{"name": "cm", "namespace": "ns"}, + }}}, + }, }, }, }, diff --git a/internal/object-controller/controllers/clusterobjectdeployment_controller_test.go b/internal/object-controller/controllers/clusterobjectdeployment_controller_test.go index 94f0298243..0e358b0b63 100644 --- a/internal/object-controller/controllers/clusterobjectdeployment_controller_test.go +++ b/internal/object-controller/controllers/clusterobjectdeployment_controller_test.go @@ -40,18 +40,20 @@ func codFixture() *ocv1.ClusterObjectDeployment { ProgressDeadlineMinutes: 15, Template: ocv1.ClusterObjectDeploymentTemplate{ Spec: ocv1.ClusterObjectDeploymentTemplateSpec{ - CollisionProtection: ocv1.CollisionProtectionPrevent, - Phases: []ocv1.ClusterObjectSetPhase{{ - Name: "resources", - Objects: []ocv1.ClusterObjectSetObject{{ - Type: ocv1.ObjectTypeInlineManifest, - InlineManifest: unstructured.Unstructured{Object: map[string]interface{}{ - "apiVersion": "v1", - "kind": "ConfigMap", - "metadata": map[string]interface{}{"name": "cm", "namespace": "ns"}, + ClusterObjectSetContent: ocv1.ClusterObjectSetContent{ + CollisionProtection: ocv1.CollisionProtectionPrevent, + Phases: []ocv1.ClusterObjectSetPhase{{ + Name: "resources", + Objects: []ocv1.ClusterObjectSetObject{{ + Type: ocv1.ObjectTypeInlineManifest, + InlineManifest: unstructured.Unstructured{Object: map[string]interface{}{ + "apiVersion": "v1", + "kind": "ConfigMap", + "metadata": map[string]interface{}{"name": "cm", "namespace": "ns"}, + }}, }}, }}, - }}, + }, }, }, }, @@ -599,10 +601,12 @@ func revisionCOS(revision int64, lifecycle ocv1.ClusterObjectSetLifecycleState) Labels: map[string]string{labels.ManagedByKey: labels.ManagedByObjectController}, }, Spec: ocv1.ClusterObjectSetSpec{ - Group: clusterObjectDeploymentName, - Revision: revision, - CollisionProtection: ocv1.CollisionProtectionPrevent, - LifecycleState: lifecycle, + Group: clusterObjectDeploymentName, + Revision: revision, + ClusterObjectSetContent: ocv1.ClusterObjectSetContent{ + CollisionProtection: ocv1.CollisionProtectionPrevent, + }, + LifecycleState: lifecycle, }, } if lifecycle == ocv1.ClusterObjectSetLifecycleStateArchived { @@ -908,11 +912,13 @@ func Test_ClusterObjectDeploymentReconciler_RevisionBounds(t *testing.T) { }, }, Spec: ocv1.ClusterObjectSetSpec{ - Group: cod.Name, - Revision: tc.latest, - LifecycleState: tc.lifecycleState, - CollisionProtection: ocv1.CollisionProtectionPrevent, - Phases: cod.Spec.Template.Spec.Phases, + Group: cod.Name, + Revision: tc.latest, + LifecycleState: tc.lifecycleState, + ClusterObjectSetContent: ocv1.ClusterObjectSetContent{ + CollisionProtection: ocv1.CollisionProtectionPrevent, + Phases: cod.Spec.Template.Spec.Phases, + }, }, } if tc.changed { diff --git a/internal/object-controller/controllers/clusterobjectset_controller_internal_test.go b/internal/object-controller/controllers/clusterobjectset_controller_internal_test.go index e45a659bf9..7a39e19035 100644 --- a/internal/object-controller/controllers/clusterobjectset_controller_internal_test.go +++ b/internal/object-controller/controllers/clusterobjectset_controller_internal_test.go @@ -287,10 +287,12 @@ func newTestClusterObjectSetInternal(t *testing.T, name string) *ocv1.ClusterObj Group: "test-ext", LifecycleState: ocv1.ClusterObjectSetLifecycleStateActive, Revision: revNum, - Phases: []ocv1.ClusterObjectSetPhase{ - { - Name: "everything", - Objects: []ocv1.ClusterObjectSetObject{}, + ClusterObjectSetContent: ocv1.ClusterObjectSetContent{ + Phases: []ocv1.ClusterObjectSetPhase{ + { + Name: "everything", + Objects: []ocv1.ClusterObjectSetObject{}, + }, }, }, }, @@ -464,17 +466,19 @@ func TestVerifyReferencedSecretsImmutable(t *testing.T) { cos := &ocv1.ClusterObjectSet{ Spec: ocv1.ClusterObjectSetSpec{ - Phases: []ocv1.ClusterObjectSetPhase{{ - Name: "phase1", - Objects: []ocv1.ClusterObjectSetObject{{ - Type: ocv1.ObjectTypeSecretReference, - SecretRef: ocv1.SecretSourceRef{ - Name: "test-secret", - Namespace: "test-ns", - Key: "obj", - }, + ClusterObjectSetContent: ocv1.ClusterObjectSetContent{ + Phases: []ocv1.ClusterObjectSetPhase{{ + Name: "phase1", + Objects: []ocv1.ClusterObjectSetObject{{ + Type: ocv1.ObjectTypeSecretReference, + SecretRef: ocv1.SecretSourceRef{ + Name: "test-secret", + Namespace: "test-ns", + Key: "obj", + }, + }}, }}, - }}, + }, }, } @@ -495,17 +499,19 @@ func TestVerifyReferencedSecretsImmutable(t *testing.T) { cos := &ocv1.ClusterObjectSet{ Spec: ocv1.ClusterObjectSetSpec{ - Phases: []ocv1.ClusterObjectSetPhase{{ - Name: "phase1", - Objects: []ocv1.ClusterObjectSetObject{{ - Type: ocv1.ObjectTypeSecretReference, - SecretRef: ocv1.SecretSourceRef{ - Name: "test-secret", - Namespace: "test-ns", - Key: "obj", - }, + ClusterObjectSetContent: ocv1.ClusterObjectSetContent{ + Phases: []ocv1.ClusterObjectSetPhase{{ + Name: "phase1", + Objects: []ocv1.ClusterObjectSetObject{{ + Type: ocv1.ObjectTypeSecretReference, + SecretRef: ocv1.SecretSourceRef{ + Name: "test-secret", + Namespace: "test-ns", + Key: "obj", + }, + }}, }}, - }}, + }, }, } @@ -523,13 +529,15 @@ func TestVerifyReferencedSecretsImmutable(t *testing.T) { cos := &ocv1.ClusterObjectSet{ Spec: ocv1.ClusterObjectSetSpec{ - Phases: []ocv1.ClusterObjectSetPhase{{ - Name: "phase1", - Objects: []ocv1.ClusterObjectSetObject{{ - Type: ocv1.ObjectTypeInlineManifest, - // Inline object, no ref + ClusterObjectSetContent: ocv1.ClusterObjectSetContent{ + Phases: []ocv1.ClusterObjectSetPhase{{ + Name: "phase1", + Objects: []ocv1.ClusterObjectSetObject{{ + Type: ocv1.ObjectTypeInlineManifest, + // Inline object, no ref + }}, }}, - }}, + }, }, } @@ -557,13 +565,15 @@ func TestVerifyReferencedSecretsImmutable(t *testing.T) { cos := &ocv1.ClusterObjectSet{ Spec: ocv1.ClusterObjectSetSpec{ - Phases: []ocv1.ClusterObjectSetPhase{{ - Name: "phase1", - Objects: []ocv1.ClusterObjectSetObject{ - {Type: ocv1.ObjectTypeSecretReference, SecretRef: ocv1.SecretSourceRef{Name: "test-secret", Namespace: "test-ns", Key: "obj"}}, - {Type: ocv1.ObjectTypeSecretReference, SecretRef: ocv1.SecretSourceRef{Name: "test-secret", Namespace: "test-ns", Key: "obj2"}}, - }, - }}, + ClusterObjectSetContent: ocv1.ClusterObjectSetContent{ + Phases: []ocv1.ClusterObjectSetPhase{{ + Name: "phase1", + Objects: []ocv1.ClusterObjectSetObject{ + {Type: ocv1.ObjectTypeSecretReference, SecretRef: ocv1.SecretSourceRef{Name: "test-secret", Namespace: "test-ns", Key: "obj"}}, + {Type: ocv1.ObjectTypeSecretReference, SecretRef: ocv1.SecretSourceRef{Name: "test-secret", Namespace: "test-ns", Key: "obj2"}}, + }, + }}, + }, }, } diff --git a/internal/object-controller/controllers/clusterobjectset_controller_test.go b/internal/object-controller/controllers/clusterobjectset_controller_test.go index 15023546b7..75b2f8749f 100644 --- a/internal/object-controller/controllers/clusterobjectset_controller_test.go +++ b/internal/object-controller/controllers/clusterobjectset_controller_test.go @@ -1553,18 +1553,20 @@ func newTestClusterObjectSet(t *testing.T, revisionName string, ext *ocv1.Cluste Group: ext.Name, LifecycleState: ocv1.ClusterObjectSetLifecycleStateActive, Revision: revNum, - Phases: []ocv1.ClusterObjectSetPhase{ - { - Name: "everything", - Objects: []ocv1.ClusterObjectSetObject{ - { - Type: ocv1.ObjectTypeInlineManifest, - InlineManifest: unstructured.Unstructured{ - Object: map[string]interface{}{ - "apiVersion": "v1", - "kind": "ConfigMap", - "data": map[string]interface{}{ - "foo": "bar", + ClusterObjectSetContent: ocv1.ClusterObjectSetContent{ + Phases: []ocv1.ClusterObjectSetPhase{ + { + Name: "everything", + Objects: []ocv1.ClusterObjectSetObject{ + { + Type: ocv1.ObjectTypeInlineManifest, + InlineManifest: unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "v1", + "kind": "ConfigMap", + "data": map[string]interface{}{ + "foo": "bar", + }, }, }, }, diff --git a/internal/object-controller/controllers/resolve_ref_test.go b/internal/object-controller/controllers/resolve_ref_test.go index ed4bb315b0..f637c3f174 100644 --- a/internal/object-controller/controllers/resolve_ref_test.go +++ b/internal/object-controller/controllers/resolve_ref_test.go @@ -291,17 +291,19 @@ func newRefTestCOS(name string, ref ocv1.SecretSourceRef) *ocv1.ClusterObjectSet }, }, Spec: ocv1.ClusterObjectSetSpec{ - Group: "ref-test", - LifecycleState: ocv1.ClusterObjectSetLifecycleStateActive, - Revision: 1, - CollisionProtection: ocv1.CollisionProtectionPrevent, - Phases: []ocv1.ClusterObjectSetPhase{ - { - Name: "deploy", - Objects: []ocv1.ClusterObjectSetObject{ - { - Type: ocv1.ObjectTypeSecretReference, - SecretRef: ref, + Group: "ref-test", + LifecycleState: ocv1.ClusterObjectSetLifecycleStateActive, + Revision: 1, + ClusterObjectSetContent: ocv1.ClusterObjectSetContent{ + CollisionProtection: ocv1.CollisionProtectionPrevent, + Phases: []ocv1.ClusterObjectSetPhase{ + { + Name: "deploy", + Objects: []ocv1.ClusterObjectSetObject{ + { + Type: ocv1.ObjectTypeSecretReference, + SecretRef: ref, + }, }, }, }, diff --git a/internal/object-controller/status/status_test.go b/internal/object-controller/status/status_test.go index 9e269077a1..98bd76ecfb 100644 --- a/internal/object-controller/status/status_test.go +++ b/internal/object-controller/status/status_test.go @@ -1096,7 +1096,9 @@ func cosSpecWithPhaseObjects(phaseDefs ...struct { phases[i] = ocv1.ClusterObjectSetPhase{Name: pd.name, Objects: objs} } return ocv1.ClusterObjectSetSpec{ - Phases: phases, + ClusterObjectSetContent: ocv1.ClusterObjectSetContent{ + Phases: phases, + }, } } @@ -1113,7 +1115,9 @@ func cosWithPhaseObjects(phaseDefs ...struct { } return ocv1.ClusterObjectSet{ Spec: ocv1.ClusterObjectSetSpec{ - Phases: phases, + ClusterObjectSetContent: ocv1.ClusterObjectSetContent{ + Phases: phases, + }, }, Status: ocv1.ClusterObjectSetStatus{ ObservedPhases: observedPhases, diff --git a/internal/operator-controller/applier/boxcutter_test.go b/internal/operator-controller/applier/boxcutter_test.go index 3e73ed7c00..8d196370e2 100644 --- a/internal/operator-controller/applier/boxcutter_test.go +++ b/internal/operator-controller/applier/boxcutter_test.go @@ -1067,8 +1067,10 @@ func TestBoxcutterApply_RecreatesDeploymentAfterDeletion(t *testing.T) { }, Spec: ocv1.ClusterObjectDeploymentSpec{Template: ocv1.ClusterObjectDeploymentTemplate{ Spec: ocv1.ClusterObjectDeploymentTemplateSpec{ - CollisionProtection: ocv1.CollisionProtectionPrevent, - Phases: []ocv1.ClusterObjectSetPhase{{Name: "resources", Objects: []ocv1.ClusterObjectSetObject{}}}, + ClusterObjectSetContent: ocv1.ClusterObjectSetContent{ + CollisionProtection: ocv1.CollisionProtectionPrevent, + Phases: []ocv1.ClusterObjectSetPhase{{Name: "resources", Objects: []ocv1.ClusterObjectSetObject{}}}, + }, }, }}, } diff --git a/internal/operator-controller/controllers/boxcutter_reconcile_steps_test.go b/internal/operator-controller/controllers/boxcutter_reconcile_steps_test.go index 9235e6f94f..ce0b16fed6 100644 --- a/internal/operator-controller/controllers/boxcutter_reconcile_steps_test.go +++ b/internal/operator-controller/controllers/boxcutter_reconcile_steps_test.go @@ -33,8 +33,10 @@ func TestBoxcutterRevisionStatesGetter_GroupIndex(t *testing.T) { Spec: ocv1.ClusterObjectDeploymentSpec{ Template: ocv1.ClusterObjectDeploymentTemplate{ Spec: ocv1.ClusterObjectDeploymentTemplateSpec{ - CollisionProtection: ocv1.CollisionProtectionPrevent, - Phases: []ocv1.ClusterObjectSetPhase{{Name: "deploy", Objects: []ocv1.ClusterObjectSetObject{}}}, + ClusterObjectSetContent: ocv1.ClusterObjectSetContent{ + CollisionProtection: ocv1.CollisionProtectionPrevent, + Phases: []ocv1.ClusterObjectSetPhase{{Name: "deploy", Objects: []ocv1.ClusterObjectSetObject{}}}, + }, }, }, }, @@ -62,12 +64,14 @@ func TestBoxcutterRevisionStatesGetter_GroupIndex(t *testing.T) { ObjectMeta: metav1.ObjectMeta{Name: cosDef.name}, Spec: ocv1.ClusterObjectSetSpec{ Group: cosDef.group, Revision: cosDef.revision, - LifecycleState: ocv1.ClusterObjectSetLifecycleStateActive, - CollisionProtection: ocv1.CollisionProtectionPrevent, - Phases: []ocv1.ClusterObjectSetPhase{ - { - Name: "test-phase", - Objects: []ocv1.ClusterObjectSetObject{}, + LifecycleState: ocv1.ClusterObjectSetLifecycleStateActive, + ClusterObjectSetContent: ocv1.ClusterObjectSetContent{ + CollisionProtection: ocv1.CollisionProtectionPrevent, + Phases: []ocv1.ClusterObjectSetPhase{ + { + Name: "test-phase", + Objects: []ocv1.ClusterObjectSetObject{}, + }, }, }, }, diff --git a/manifests/experimental-e2e.yaml b/manifests/experimental-e2e.yaml index 7d269d17b1..af51a0d060 100644 --- a/manifests/experimental-e2e.yaml +++ b/manifests/experimental-e2e.yaml @@ -1456,9 +1456,11 @@ spec: properties: collisionProtection: description: |- - collisionProtection is the required default collision protection strategy for all objects - in the revision. Individual phases or objects may override this value. - The resolution order is: object > phase > spec. + collisionProtection is a required field that specifies the default collision protection + strategy for all objects in this revision. Individual phases or objects can override this value. + + This value is used as the default for any phase or object that does not explicitly specify + its own collisionProtection. The resolution order is: object > phase > spec. enum: - Prevent - IfNoController @@ -1466,10 +1468,21 @@ spec: type: string phases: description: |- - phases is an ordered list of 1 to 20 uniquely named phases. Each phase contains - at most 50 objects, each expressed either as an inlineManifest (an embedded - Kubernetes object) or a secretRef (a reference to an immutable Secret holding - a serialized manifest). Phase, object, and probe order participate in template identity. + phases is a required list of phases that group objects to be applied together. + + Objects are organized into phases based on their Group-Kind. Common phases include: + - namespaces: Namespace objects + - policies: ResourceQuota, LimitRange, NetworkPolicy objects + - rbac: ServiceAccount, Role, RoleBinding, ClusterRole, ClusterRoleBinding objects + - crds: CustomResourceDefinition objects + - storage: PersistentVolume, PersistentVolumeClaim, StorageClass objects + - deploy: Deployment, StatefulSet, DaemonSet, Service, ConfigMap, Secret objects + - publish: Ingress, APIService, Route, Webhook objects + + All objects in a phase are applied in no particular order. + The revision progresses to the next phase only after all objects in the current phase pass their readiness probes. + + Each phase in the list must have a unique name. The maximum number of phases is 20. items: description: |- ClusterObjectSetPhase represents a group of objects that are applied together. The phase is considered @@ -1655,9 +1668,17 @@ spec: x-kubernetes-list-type: map progressionProbes: description: |- - progressionProbes supplies custom readiness probes for objects in every phase. - The same selector and assertion semantics as ClusterObjectSet.spec.progressionProbes apply. - When set, must contain 1 to 20 probes. + progressionProbes is an optional field which provides the ability to define custom readiness probes + for objects defined within spec.phases. As documented in that field, most kubernetes-native objects + within the phases already have some kind of readiness check built-in, but this field allows for checks + which are tailored to the objects being rolled out - particularly custom resources. + + Probes defined within the progressionProbes list will apply to every phase in the revision. However, the probes will only + execute against phase objects which are a match for the provided selector type. For instance, a probe using a GroupKind selector + for ConfigMaps will automatically be considered to have passed for any non-ConfigMap object, but will halt any phase containing + a ConfigMap if that particular object does not pass the probe check. + + The maximum number of probes is 20. items: description: ProgressionProbe provides a custom probe definition, consisting of an object selection method and assertions. @@ -2141,9 +2162,6 @@ spec: - IfNoController - None type: string - x-kubernetes-validations: - - message: collisionProtection is immutable - rule: self == oldSelf group: description: |- group is a required, immutable identifier that links related revisions together. @@ -2184,7 +2202,7 @@ spec: self phases: description: |- - phases is a required, immutable list of phases that group objects to be applied together. + phases is a required list of phases that group objects to be applied together. Objects are organized into phases based on their Group-Kind. Common phases include: - namespaces: Namespace objects @@ -2198,8 +2216,6 @@ spec: All objects in a phase are applied in no particular order. The revision progresses to the next phase only after all objects in the current phase pass their readiness probes. - Once set, the phases field is immutable. - Each phase in the list must have a unique name. The maximum number of phases is 20. items: description: |- @@ -2379,9 +2395,6 @@ spec: x-kubernetes-list-map-keys: - name x-kubernetes-list-type: map - x-kubernetes-validations: - - message: phases is immutable - rule: self == oldSelf || oldSelf.size() == 0 progressionProbes: description: |- progressionProbes is an optional field which provides the ability to define custom readiness probes @@ -2395,9 +2408,6 @@ spec: a ConfigMap if that particular object does not pass the probe check. The maximum number of probes is 20. - - Once set, the progressionProbes field is immutable, as it is - part of the immutable content of a revision. items: description: ProgressionProbe provides a custom probe definition, consisting of an object selection method and assertions. @@ -2648,9 +2658,6 @@ spec: minItems: 1 type: array x-kubernetes-list-type: atomic - x-kubernetes-validations: - - message: progressionProbes is immutable - rule: self == oldSelf || oldSelf.size() == 0 revision: description: |- revision is a required, immutable sequence number identifying a specific @@ -2675,8 +2682,16 @@ spec: x-kubernetes-validations: - message: phases cannot be removed once set rule: '!has(oldSelf.phases) || has(self.phases)' - - message: progressionProbes cannot be removed once set - rule: '!has(oldSelf.progressionProbes) || has(self.progressionProbes)' + - message: phases is immutable + rule: '!has(oldSelf.phases) || self.phases == oldSelf.phases || oldSelf.phases.size() + == 0' + - message: progressionProbes is immutable + rule: '!has(oldSelf.progressionProbes) || !has(self.progressionProbes) + && oldSelf.progressionProbes.size() == 0 || has(self.progressionProbes) + && (self.progressionProbes == oldSelf.progressionProbes || oldSelf.progressionProbes.size() + == 0)' + - message: collisionProtection is immutable + rule: self.collisionProtection == oldSelf.collisionProtection status: description: |- status is optional and defines the observed state of the ClusterObjectSet. diff --git a/manifests/experimental.yaml b/manifests/experimental.yaml index bbe1fb8ef3..69fff0d791 100644 --- a/manifests/experimental.yaml +++ b/manifests/experimental.yaml @@ -1417,9 +1417,11 @@ spec: properties: collisionProtection: description: |- - collisionProtection is the required default collision protection strategy for all objects - in the revision. Individual phases or objects may override this value. - The resolution order is: object > phase > spec. + collisionProtection is a required field that specifies the default collision protection + strategy for all objects in this revision. Individual phases or objects can override this value. + + This value is used as the default for any phase or object that does not explicitly specify + its own collisionProtection. The resolution order is: object > phase > spec. enum: - Prevent - IfNoController @@ -1427,10 +1429,21 @@ spec: type: string phases: description: |- - phases is an ordered list of 1 to 20 uniquely named phases. Each phase contains - at most 50 objects, each expressed either as an inlineManifest (an embedded - Kubernetes object) or a secretRef (a reference to an immutable Secret holding - a serialized manifest). Phase, object, and probe order participate in template identity. + phases is a required list of phases that group objects to be applied together. + + Objects are organized into phases based on their Group-Kind. Common phases include: + - namespaces: Namespace objects + - policies: ResourceQuota, LimitRange, NetworkPolicy objects + - rbac: ServiceAccount, Role, RoleBinding, ClusterRole, ClusterRoleBinding objects + - crds: CustomResourceDefinition objects + - storage: PersistentVolume, PersistentVolumeClaim, StorageClass objects + - deploy: Deployment, StatefulSet, DaemonSet, Service, ConfigMap, Secret objects + - publish: Ingress, APIService, Route, Webhook objects + + All objects in a phase are applied in no particular order. + The revision progresses to the next phase only after all objects in the current phase pass their readiness probes. + + Each phase in the list must have a unique name. The maximum number of phases is 20. items: description: |- ClusterObjectSetPhase represents a group of objects that are applied together. The phase is considered @@ -1616,9 +1629,17 @@ spec: x-kubernetes-list-type: map progressionProbes: description: |- - progressionProbes supplies custom readiness probes for objects in every phase. - The same selector and assertion semantics as ClusterObjectSet.spec.progressionProbes apply. - When set, must contain 1 to 20 probes. + progressionProbes is an optional field which provides the ability to define custom readiness probes + for objects defined within spec.phases. As documented in that field, most kubernetes-native objects + within the phases already have some kind of readiness check built-in, but this field allows for checks + which are tailored to the objects being rolled out - particularly custom resources. + + Probes defined within the progressionProbes list will apply to every phase in the revision. However, the probes will only + execute against phase objects which are a match for the provided selector type. For instance, a probe using a GroupKind selector + for ConfigMaps will automatically be considered to have passed for any non-ConfigMap object, but will halt any phase containing + a ConfigMap if that particular object does not pass the probe check. + + The maximum number of probes is 20. items: description: ProgressionProbe provides a custom probe definition, consisting of an object selection method and assertions. @@ -2102,9 +2123,6 @@ spec: - IfNoController - None type: string - x-kubernetes-validations: - - message: collisionProtection is immutable - rule: self == oldSelf group: description: |- group is a required, immutable identifier that links related revisions together. @@ -2145,7 +2163,7 @@ spec: self phases: description: |- - phases is a required, immutable list of phases that group objects to be applied together. + phases is a required list of phases that group objects to be applied together. Objects are organized into phases based on their Group-Kind. Common phases include: - namespaces: Namespace objects @@ -2159,8 +2177,6 @@ spec: All objects in a phase are applied in no particular order. The revision progresses to the next phase only after all objects in the current phase pass their readiness probes. - Once set, the phases field is immutable. - Each phase in the list must have a unique name. The maximum number of phases is 20. items: description: |- @@ -2340,9 +2356,6 @@ spec: x-kubernetes-list-map-keys: - name x-kubernetes-list-type: map - x-kubernetes-validations: - - message: phases is immutable - rule: self == oldSelf || oldSelf.size() == 0 progressionProbes: description: |- progressionProbes is an optional field which provides the ability to define custom readiness probes @@ -2356,9 +2369,6 @@ spec: a ConfigMap if that particular object does not pass the probe check. The maximum number of probes is 20. - - Once set, the progressionProbes field is immutable, as it is - part of the immutable content of a revision. items: description: ProgressionProbe provides a custom probe definition, consisting of an object selection method and assertions. @@ -2609,9 +2619,6 @@ spec: minItems: 1 type: array x-kubernetes-list-type: atomic - x-kubernetes-validations: - - message: progressionProbes is immutable - rule: self == oldSelf || oldSelf.size() == 0 revision: description: |- revision is a required, immutable sequence number identifying a specific @@ -2636,8 +2643,16 @@ spec: x-kubernetes-validations: - message: phases cannot be removed once set rule: '!has(oldSelf.phases) || has(self.phases)' - - message: progressionProbes cannot be removed once set - rule: '!has(oldSelf.progressionProbes) || has(self.progressionProbes)' + - message: phases is immutable + rule: '!has(oldSelf.phases) || self.phases == oldSelf.phases || oldSelf.phases.size() + == 0' + - message: progressionProbes is immutable + rule: '!has(oldSelf.progressionProbes) || !has(self.progressionProbes) + && oldSelf.progressionProbes.size() == 0 || has(self.progressionProbes) + && (self.progressionProbes == oldSelf.progressionProbes || oldSelf.progressionProbes.size() + == 0)' + - message: collisionProtection is immutable + rule: self.collisionProtection == oldSelf.collisionProtection status: description: |- status is optional and defines the observed state of the ClusterObjectSet. From 4cfea62c1d4197b39f26dc4ab20368a00fe19d46 Mon Sep 17 00:00:00 2001 From: "Per G. da Silva" Date: Wed, 11 Nov 2026 09:42:24 +0100 Subject: [PATCH 2/2] Tighten COS content immutability and neutralize shared field docs Make COS phases and collision protection strictly immutable. Freeze both presence and values of optional progression probes from creation, while allowing lifecycle and metadata updates with unchanged content. Keep the shared COD template mutable and add API-server-backed regression coverage. Remove component-specific phase recommendations from shared field docs and regenerate apply configurations, CRDs, manifests, and API reference output. Signed-off-by: Per G. da Silva --- api/v1/clusterobjectset_types.go | 22 +++------- api/v1/clusterobjectset_types_test.go | 20 +++++++-- .../api/v1/clusterobjectsetcontent.go | 15 ++----- .../api/v1/clusterobjectsetspec.go | 2 +- docs/api-reference/olmv1-api-reference.md | 12 +++--- docs/draft/concepts/clusterobjectsets.md | 2 +- ...framework.io_clusterobjectdeployments.yaml | 15 ++----- ...peratorframework.io_clusterobjectsets.yaml | 26 +++--------- manifests/experimental-e2e.yaml | 41 ++++--------------- manifests/experimental.yaml | 41 ++++--------------- 10 files changed, 61 insertions(+), 135 deletions(-) diff --git a/api/v1/clusterobjectset_types.go b/api/v1/clusterobjectset_types.go index 473c4a0f42..9cef790dc2 100644 --- a/api/v1/clusterobjectset_types.go +++ b/api/v1/clusterobjectset_types.go @@ -47,10 +47,9 @@ const ( // ClusterObjectSetSpec defines the desired state of ClusterObjectSet. // The content fields (phases, progressionProbes, collisionProtection) are -// immutable: once set they may only be changed by creating a new revision. -// +kubebuilder:validation:XValidation:rule="!has(oldSelf.phases) || has(self.phases)",message="phases cannot be removed once set" -// +kubebuilder:validation:XValidation:rule="!has(oldSelf.phases) || self.phases == oldSelf.phases || oldSelf.phases.size() == 0",message="phases is immutable" -// +kubebuilder:validation:XValidation:rule="!has(oldSelf.progressionProbes) || !has(self.progressionProbes) && oldSelf.progressionProbes.size() == 0 || has(self.progressionProbes) && (self.progressionProbes == oldSelf.progressionProbes || oldSelf.progressionProbes.size() == 0)",message="progressionProbes is immutable" +// immutable from creation and may only be changed by creating a new revision. +// +kubebuilder:validation:XValidation:rule="self.phases == oldSelf.phases",message="phases is immutable" +// +kubebuilder:validation:XValidation:rule="has(self.progressionProbes) == has(oldSelf.progressionProbes) && (!has(self.progressionProbes) || self.progressionProbes == oldSelf.progressionProbes)",message="progressionProbes is immutable" // +kubebuilder:validation:XValidation:rule="self.collisionProtection == oldSelf.collisionProtection",message="collisionProtection is immutable" type ClusterObjectSetSpec struct { // group is a required, immutable identifier that links related revisions together. @@ -106,15 +105,6 @@ type ClusterObjectSetSpec struct { type ClusterObjectSetContent struct { // phases is a required list of phases that group objects to be applied together. // - // Objects are organized into phases based on their Group-Kind. Common phases include: - // - namespaces: Namespace objects - // - policies: ResourceQuota, LimitRange, NetworkPolicy objects - // - rbac: ServiceAccount, Role, RoleBinding, ClusterRole, ClusterRoleBinding objects - // - crds: CustomResourceDefinition objects - // - storage: PersistentVolume, PersistentVolumeClaim, StorageClass objects - // - deploy: Deployment, StatefulSet, DaemonSet, Service, ConfigMap, Secret objects - // - publish: Ingress, APIService, Route, Webhook objects - // // All objects in a phase are applied in no particular order. // The revision progresses to the next phase only after all objects in the current phase pass their readiness probes. // @@ -128,7 +118,7 @@ type ClusterObjectSetContent struct { Phases []ClusterObjectSetPhase `json:"phases,omitempty"` // progressionProbes is an optional field which provides the ability to define custom readiness probes - // for objects defined within spec.phases. As documented in that field, most kubernetes-native objects + // for objects defined within phases. As documented in that field, most kubernetes-native objects // within the phases already have some kind of readiness check built-in, but this field allows for checks // which are tailored to the objects being rolled out - particularly custom resources. // @@ -147,10 +137,10 @@ type ClusterObjectSetContent struct { ProgressionProbes []ProgressionProbe `json:"progressionProbes,omitempty"` // collisionProtection is a required field that specifies the default collision protection - // strategy for all objects in this revision. Individual phases or objects can override this value. + // strategy for all objects in this content. Individual phases or objects can override this value. // // This value is used as the default for any phase or object that does not explicitly specify - // its own collisionProtection. The resolution order is: object > phase > spec. + // its own collisionProtection. The resolution order is: object > phase > content. // // +required CollisionProtection CollisionProtection `json:"collisionProtection,omitempty"` diff --git a/api/v1/clusterobjectset_types_test.go b/api/v1/clusterobjectset_types_test.go index 49ff298e63..e40b0c433b 100644 --- a/api/v1/clusterobjectset_types_test.go +++ b/api/v1/clusterobjectset_types_test.go @@ -85,7 +85,7 @@ func TestClusterObjectSetImmutability(t *testing.T) { }, expectedError: "revision is immutable", }, - "phases are immutable once set": { + "phases are immutable": { spec: ClusterObjectSetSpec{ Group: "immutability-populated-phases", LifecycleState: ClusterObjectSetLifecycleStateActive, @@ -125,7 +125,7 @@ func TestClusterObjectSetImmutability(t *testing.T) { }, expectedError: "collisionProtection is immutable", }, - "progressionProbes may be initially unset": { + "progressionProbes cannot be added after creation": { spec: ClusterObjectSetSpec{ Group: "immutability-unset-probes", LifecycleState: ClusterObjectSetLifecycleStateActive, @@ -138,9 +138,23 @@ func TestClusterObjectSetImmutability(t *testing.T) { updateFunc: func(cos *ClusterObjectSet) { cos.Spec.ProgressionProbes = []ProgressionProbe{validProbe()} }, + expectedError: "progressionProbes is immutable", + }, + "progressionProbes cannot be removed": { + spec: probeSpec("immutability-removed-probes", validProbe()), + updateFunc: func(cos *ClusterObjectSet) { + cos.Spec.ProgressionProbes = nil + }, + expectedError: "progressionProbes is immutable", + }, + "unchanged progressionProbes permit lifecycle update": { + spec: probeSpec("immutability-unchanged-probes", validProbe()), + updateFunc: func(cos *ClusterObjectSet) { + cos.Spec.LifecycleState = ClusterObjectSetLifecycleStateArchived + }, allowed: true, }, - "progressionProbes are immutable once set": { + "progressionProbes are immutable": { spec: ClusterObjectSetSpec{ Group: "immutability-populated-probes", LifecycleState: ClusterObjectSetLifecycleStateActive, diff --git a/applyconfigurations/api/v1/clusterobjectsetcontent.go b/applyconfigurations/api/v1/clusterobjectsetcontent.go index 70fcaea93a..6a54139e6d 100644 --- a/applyconfigurations/api/v1/clusterobjectsetcontent.go +++ b/applyconfigurations/api/v1/clusterobjectsetcontent.go @@ -30,22 +30,13 @@ import ( type ClusterObjectSetContentApplyConfiguration struct { // phases is a required list of phases that group objects to be applied together. // - // Objects are organized into phases based on their Group-Kind. Common phases include: - // - namespaces: Namespace objects - // - policies: ResourceQuota, LimitRange, NetworkPolicy objects - // - rbac: ServiceAccount, Role, RoleBinding, ClusterRole, ClusterRoleBinding objects - // - crds: CustomResourceDefinition objects - // - storage: PersistentVolume, PersistentVolumeClaim, StorageClass objects - // - deploy: Deployment, StatefulSet, DaemonSet, Service, ConfigMap, Secret objects - // - publish: Ingress, APIService, Route, Webhook objects - // // All objects in a phase are applied in no particular order. // The revision progresses to the next phase only after all objects in the current phase pass their readiness probes. // // Each phase in the list must have a unique name. The maximum number of phases is 20. Phases []ClusterObjectSetPhaseApplyConfiguration `json:"phases,omitempty"` // progressionProbes is an optional field which provides the ability to define custom readiness probes - // for objects defined within spec.phases. As documented in that field, most kubernetes-native objects + // for objects defined within phases. As documented in that field, most kubernetes-native objects // within the phases already have some kind of readiness check built-in, but this field allows for checks // which are tailored to the objects being rolled out - particularly custom resources. // @@ -59,10 +50,10 @@ type ClusterObjectSetContentApplyConfiguration struct { // ProgressionProbes []ProgressionProbeApplyConfiguration `json:"progressionProbes,omitempty"` // collisionProtection is a required field that specifies the default collision protection - // strategy for all objects in this revision. Individual phases or objects can override this value. + // strategy for all objects in this content. Individual phases or objects can override this value. // // This value is used as the default for any phase or object that does not explicitly specify - // its own collisionProtection. The resolution order is: object > phase > spec. + // its own collisionProtection. The resolution order is: object > phase > content. CollisionProtection *apiv1.CollisionProtection `json:"collisionProtection,omitempty"` } diff --git a/applyconfigurations/api/v1/clusterobjectsetspec.go b/applyconfigurations/api/v1/clusterobjectsetspec.go index 28ce0e5fbb..b0590feff0 100644 --- a/applyconfigurations/api/v1/clusterobjectsetspec.go +++ b/applyconfigurations/api/v1/clusterobjectsetspec.go @@ -27,7 +27,7 @@ import ( // // ClusterObjectSetSpec defines the desired state of ClusterObjectSet. // The content fields (phases, progressionProbes, collisionProtection) are -// immutable: once set they may only be changed by creating a new revision. +// immutable from creation and may only be changed by creating a new revision. type ClusterObjectSetSpecApplyConfiguration struct { // group is a required, immutable identifier that links related revisions together. // All revisions sharing the same group form an ordered sequence, regardless of owner references. diff --git a/docs/api-reference/olmv1-api-reference.md b/docs/api-reference/olmv1-api-reference.md index 5e331a8205..267e07d8b1 100644 --- a/docs/api-reference/olmv1-api-reference.md +++ b/docs/api-reference/olmv1-api-reference.md @@ -512,9 +512,9 @@ _Appears in:_ | Field | Description | Default | Validation | | --- | --- | --- | --- | -| `phases` _[ClusterObjectSetPhase](#clusterobjectsetphase) array_ | phases is a required list of phases that group objects to be applied together.
Objects are organized into phases based on their Group-Kind. Common phases include:
- namespaces: Namespace objects
- policies: ResourceQuota, LimitRange, NetworkPolicy objects
- rbac: ServiceAccount, Role, RoleBinding, ClusterRole, ClusterRoleBinding objects
- crds: CustomResourceDefinition objects
- storage: PersistentVolume, PersistentVolumeClaim, StorageClass objects
- deploy: Deployment, StatefulSet, DaemonSet, Service, ConfigMap, Secret objects
- publish: Ingress, APIService, Route, Webhook objects
All objects in a phase are applied in no particular order.
The revision progresses to the next phase only after all objects in the current phase pass their readiness probes.
Each phase in the list must have a unique name. The maximum number of phases is 20. | | MaxItems: 20
MinItems: 1
Required: \{\}
| -| `progressionProbes` _[ProgressionProbe](#progressionprobe) array_ | progressionProbes is an optional field which provides the ability to define custom readiness probes
for objects defined within spec.phases. As documented in that field, most kubernetes-native objects
within the phases already have some kind of readiness check built-in, but this field allows for checks
which are tailored to the objects being rolled out - particularly custom resources.
Probes defined within the progressionProbes list will apply to every phase in the revision. However, the probes will only
execute against phase objects which are a match for the provided selector type. For instance, a probe using a GroupKind selector
for ConfigMaps will automatically be considered to have passed for any non-ConfigMap object, but will halt any phase containing
a ConfigMap if that particular object does not pass the probe check.
The maximum number of probes is 20.
**Experimental channel:** | | MaxItems: 20
MinItems: 1
Optional: \{\}
| -| `collisionProtection` _[CollisionProtection](#collisionprotection)_ | collisionProtection is a required field that specifies the default collision protection
strategy for all objects in this revision. Individual phases or objects can override this value.
This value is used as the default for any phase or object that does not explicitly specify
its own collisionProtection. The resolution order is: object > phase > spec. | | Enum: [Prevent IfNoController None]
Required: \{\}
| +| `phases` _[ClusterObjectSetPhase](#clusterobjectsetphase) array_ | phases is a required list of phases that group objects to be applied together.
All objects in a phase are applied in no particular order.
The revision progresses to the next phase only after all objects in the current phase pass their readiness probes.
Each phase in the list must have a unique name. The maximum number of phases is 20. | | MaxItems: 20
MinItems: 1
Required: \{\}
| +| `progressionProbes` _[ProgressionProbe](#progressionprobe) array_ | progressionProbes is an optional field which provides the ability to define custom readiness probes
for objects defined within phases. As documented in that field, most kubernetes-native objects
within the phases already have some kind of readiness check built-in, but this field allows for checks
which are tailored to the objects being rolled out - particularly custom resources.
Probes defined within the progressionProbes list will apply to every phase in the revision. However, the probes will only
execute against phase objects which are a match for the provided selector type. For instance, a probe using a GroupKind selector
for ConfigMaps will automatically be considered to have passed for any non-ConfigMap object, but will halt any phase containing
a ConfigMap if that particular object does not pass the probe check.
The maximum number of probes is 20.
**Experimental channel:** | | MaxItems: 20
MinItems: 1
Optional: \{\}
| +| `collisionProtection` _[CollisionProtection](#collisionprotection)_ | collisionProtection is a required field that specifies the default collision protection
strategy for all objects in this content. Individual phases or objects can override this value.
This value is used as the default for any phase or object that does not explicitly specify
its own collisionProtection. The resolution order is: object > phase > content. | | Enum: [Prevent IfNoController None]
Required: \{\}
| #### ClusterObjectSetContent @@ -531,9 +531,9 @@ _Appears in:_ | Field | Description | Default | Validation | | --- | --- | --- | --- | -| `phases` _[ClusterObjectSetPhase](#clusterobjectsetphase) array_ | phases is a required list of phases that group objects to be applied together.
Objects are organized into phases based on their Group-Kind. Common phases include:
- namespaces: Namespace objects
- policies: ResourceQuota, LimitRange, NetworkPolicy objects
- rbac: ServiceAccount, Role, RoleBinding, ClusterRole, ClusterRoleBinding objects
- crds: CustomResourceDefinition objects
- storage: PersistentVolume, PersistentVolumeClaim, StorageClass objects
- deploy: Deployment, StatefulSet, DaemonSet, Service, ConfigMap, Secret objects
- publish: Ingress, APIService, Route, Webhook objects
All objects in a phase are applied in no particular order.
The revision progresses to the next phase only after all objects in the current phase pass their readiness probes.
Each phase in the list must have a unique name. The maximum number of phases is 20. | | MaxItems: 20
MinItems: 1
Required: \{\}
| -| `progressionProbes` _[ProgressionProbe](#progressionprobe) array_ | progressionProbes is an optional field which provides the ability to define custom readiness probes
for objects defined within spec.phases. As documented in that field, most kubernetes-native objects
within the phases already have some kind of readiness check built-in, but this field allows for checks
which are tailored to the objects being rolled out - particularly custom resources.
Probes defined within the progressionProbes list will apply to every phase in the revision. However, the probes will only
execute against phase objects which are a match for the provided selector type. For instance, a probe using a GroupKind selector
for ConfigMaps will automatically be considered to have passed for any non-ConfigMap object, but will halt any phase containing
a ConfigMap if that particular object does not pass the probe check.
The maximum number of probes is 20.
**Experimental channel:** | | MaxItems: 20
MinItems: 1
Optional: \{\}
| -| `collisionProtection` _[CollisionProtection](#collisionprotection)_ | collisionProtection is a required field that specifies the default collision protection
strategy for all objects in this revision. Individual phases or objects can override this value.
This value is used as the default for any phase or object that does not explicitly specify
its own collisionProtection. The resolution order is: object > phase > spec. | | Enum: [Prevent IfNoController None]
Required: \{\}
| +| `phases` _[ClusterObjectSetPhase](#clusterobjectsetphase) array_ | phases is a required list of phases that group objects to be applied together.
All objects in a phase are applied in no particular order.
The revision progresses to the next phase only after all objects in the current phase pass their readiness probes.
Each phase in the list must have a unique name. The maximum number of phases is 20. | | MaxItems: 20
MinItems: 1
Required: \{\}
| +| `progressionProbes` _[ProgressionProbe](#progressionprobe) array_ | progressionProbes is an optional field which provides the ability to define custom readiness probes
for objects defined within phases. As documented in that field, most kubernetes-native objects
within the phases already have some kind of readiness check built-in, but this field allows for checks
which are tailored to the objects being rolled out - particularly custom resources.
Probes defined within the progressionProbes list will apply to every phase in the revision. However, the probes will only
execute against phase objects which are a match for the provided selector type. For instance, a probe using a GroupKind selector
for ConfigMaps will automatically be considered to have passed for any non-ConfigMap object, but will halt any phase containing
a ConfigMap if that particular object does not pass the probe check.
The maximum number of probes is 20.
**Experimental channel:** | | MaxItems: 20
MinItems: 1
Optional: \{\}
| +| `collisionProtection` _[CollisionProtection](#collisionprotection)_ | collisionProtection is a required field that specifies the default collision protection
strategy for all objects in this content. Individual phases or objects can override this value.
This value is used as the default for any phase or object that does not explicitly specify
its own collisionProtection. The resolution order is: object > phase > content. | | Enum: [Prevent IfNoController None]
Required: \{\}
| diff --git a/docs/draft/concepts/clusterobjectsets.md b/docs/draft/concepts/clusterobjectsets.md index 4456ba28ed..ffc032677b 100644 --- a/docs/draft/concepts/clusterobjectsets.md +++ b/docs/draft/concepts/clusterobjectsets.md @@ -6,7 +6,7 @@ This document explains what ClusterObjectSets are, how they enable safe phased r A `ClusterObjectSet` is a cluster-scoped Kubernetes API that represents a versioned set of Kubernetes resources organized into ordered phases. It provides a declarative way to roll out a group of related resources sequentially, with built-in readiness checks between phases. -The revision content — the `group` identifier, `revision` number, `collisionProtection` strategy, and `phases` (including all objects within them) — is immutable once set. This guarantees that the record of what was deployed at a given revision cannot change after creation. Other fields like `lifecycleState` can change over the object's lifecycle (e.g. transitioning from `Active` to `Archived`), and optional fields like `progressionProbes` can be configured independently. +The revision content — the `group` identifier, `revision` number, `collisionProtection` strategy, `phases` (including all objects within them), and optional `progressionProbes` — is immutable from creation. This guarantees that the record of what was deployed at a given revision cannot change after creation. Progression probes cannot be added, changed, or removed after creation. Other fields like `lifecycleState` can change over the object's lifecycle (e.g. transitioning from `Active` to `Archived`). Each ClusterObjectSet has: diff --git a/helm/olmv1/base/object-controller/crd/experimental/olm.operatorframework.io_clusterobjectdeployments.yaml b/helm/olmv1/base/object-controller/crd/experimental/olm.operatorframework.io_clusterobjectdeployments.yaml index d79582dcee..cccca61e67 100644 --- a/helm/olmv1/base/object-controller/crd/experimental/olm.operatorframework.io_clusterobjectdeployments.yaml +++ b/helm/olmv1/base/object-controller/crd/experimental/olm.operatorframework.io_clusterobjectdeployments.yaml @@ -128,10 +128,10 @@ spec: collisionProtection: description: |- collisionProtection is a required field that specifies the default collision protection - strategy for all objects in this revision. Individual phases or objects can override this value. + strategy for all objects in this content. Individual phases or objects can override this value. This value is used as the default for any phase or object that does not explicitly specify - its own collisionProtection. The resolution order is: object > phase > spec. + its own collisionProtection. The resolution order is: object > phase > content. enum: - Prevent - IfNoController @@ -141,15 +141,6 @@ spec: description: |- phases is a required list of phases that group objects to be applied together. - Objects are organized into phases based on their Group-Kind. Common phases include: - - namespaces: Namespace objects - - policies: ResourceQuota, LimitRange, NetworkPolicy objects - - rbac: ServiceAccount, Role, RoleBinding, ClusterRole, ClusterRoleBinding objects - - crds: CustomResourceDefinition objects - - storage: PersistentVolume, PersistentVolumeClaim, StorageClass objects - - deploy: Deployment, StatefulSet, DaemonSet, Service, ConfigMap, Secret objects - - publish: Ingress, APIService, Route, Webhook objects - All objects in a phase are applied in no particular order. The revision progresses to the next phase only after all objects in the current phase pass their readiness probes. @@ -340,7 +331,7 @@ spec: progressionProbes: description: |- progressionProbes is an optional field which provides the ability to define custom readiness probes - for objects defined within spec.phases. As documented in that field, most kubernetes-native objects + for objects defined within phases. As documented in that field, most kubernetes-native objects within the phases already have some kind of readiness check built-in, but this field allows for checks which are tailored to the objects being rolled out - particularly custom resources. diff --git a/helm/olmv1/base/object-controller/crd/experimental/olm.operatorframework.io_clusterobjectsets.yaml b/helm/olmv1/base/object-controller/crd/experimental/olm.operatorframework.io_clusterobjectsets.yaml index 6fdbae4d7b..01f909280c 100644 --- a/helm/olmv1/base/object-controller/crd/experimental/olm.operatorframework.io_clusterobjectsets.yaml +++ b/helm/olmv1/base/object-controller/crd/experimental/olm.operatorframework.io_clusterobjectsets.yaml @@ -80,10 +80,10 @@ spec: collisionProtection: description: |- collisionProtection is a required field that specifies the default collision protection - strategy for all objects in this revision. Individual phases or objects can override this value. + strategy for all objects in this content. Individual phases or objects can override this value. This value is used as the default for any phase or object that does not explicitly specify - its own collisionProtection. The resolution order is: object > phase > spec. + its own collisionProtection. The resolution order is: object > phase > content. enum: - Prevent - IfNoController @@ -131,15 +131,6 @@ spec: description: |- phases is a required list of phases that group objects to be applied together. - Objects are organized into phases based on their Group-Kind. Common phases include: - - namespaces: Namespace objects - - policies: ResourceQuota, LimitRange, NetworkPolicy objects - - rbac: ServiceAccount, Role, RoleBinding, ClusterRole, ClusterRoleBinding objects - - crds: CustomResourceDefinition objects - - storage: PersistentVolume, PersistentVolumeClaim, StorageClass objects - - deploy: Deployment, StatefulSet, DaemonSet, Service, ConfigMap, Secret objects - - publish: Ingress, APIService, Route, Webhook objects - All objects in a phase are applied in no particular order. The revision progresses to the next phase only after all objects in the current phase pass their readiness probes. @@ -325,7 +316,7 @@ spec: progressionProbes: description: |- progressionProbes is an optional field which provides the ability to define custom readiness probes - for objects defined within spec.phases. As documented in that field, most kubernetes-native objects + for objects defined within phases. As documented in that field, most kubernetes-native objects within the phases already have some kind of readiness check built-in, but this field allows for checks which are tailored to the objects being rolled out - particularly custom resources. @@ -607,16 +598,11 @@ spec: - revision type: object x-kubernetes-validations: - - message: phases cannot be removed once set - rule: '!has(oldSelf.phases) || has(self.phases)' - message: phases is immutable - rule: '!has(oldSelf.phases) || self.phases == oldSelf.phases || oldSelf.phases.size() - == 0' + rule: self.phases == oldSelf.phases - message: progressionProbes is immutable - rule: '!has(oldSelf.progressionProbes) || !has(self.progressionProbes) - && oldSelf.progressionProbes.size() == 0 || has(self.progressionProbes) - && (self.progressionProbes == oldSelf.progressionProbes || oldSelf.progressionProbes.size() - == 0)' + rule: has(self.progressionProbes) == has(oldSelf.progressionProbes) + && (!has(self.progressionProbes) || self.progressionProbes == oldSelf.progressionProbes) - message: collisionProtection is immutable rule: self.collisionProtection == oldSelf.collisionProtection status: diff --git a/manifests/experimental-e2e.yaml b/manifests/experimental-e2e.yaml index af51a0d060..70198af7f8 100644 --- a/manifests/experimental-e2e.yaml +++ b/manifests/experimental-e2e.yaml @@ -1457,10 +1457,10 @@ spec: collisionProtection: description: |- collisionProtection is a required field that specifies the default collision protection - strategy for all objects in this revision. Individual phases or objects can override this value. + strategy for all objects in this content. Individual phases or objects can override this value. This value is used as the default for any phase or object that does not explicitly specify - its own collisionProtection. The resolution order is: object > phase > spec. + its own collisionProtection. The resolution order is: object > phase > content. enum: - Prevent - IfNoController @@ -1470,15 +1470,6 @@ spec: description: |- phases is a required list of phases that group objects to be applied together. - Objects are organized into phases based on their Group-Kind. Common phases include: - - namespaces: Namespace objects - - policies: ResourceQuota, LimitRange, NetworkPolicy objects - - rbac: ServiceAccount, Role, RoleBinding, ClusterRole, ClusterRoleBinding objects - - crds: CustomResourceDefinition objects - - storage: PersistentVolume, PersistentVolumeClaim, StorageClass objects - - deploy: Deployment, StatefulSet, DaemonSet, Service, ConfigMap, Secret objects - - publish: Ingress, APIService, Route, Webhook objects - All objects in a phase are applied in no particular order. The revision progresses to the next phase only after all objects in the current phase pass their readiness probes. @@ -1669,7 +1660,7 @@ spec: progressionProbes: description: |- progressionProbes is an optional field which provides the ability to define custom readiness probes - for objects defined within spec.phases. As documented in that field, most kubernetes-native objects + for objects defined within phases. As documented in that field, most kubernetes-native objects within the phases already have some kind of readiness check built-in, but this field allows for checks which are tailored to the objects being rolled out - particularly custom resources. @@ -2153,10 +2144,10 @@ spec: collisionProtection: description: |- collisionProtection is a required field that specifies the default collision protection - strategy for all objects in this revision. Individual phases or objects can override this value. + strategy for all objects in this content. Individual phases or objects can override this value. This value is used as the default for any phase or object that does not explicitly specify - its own collisionProtection. The resolution order is: object > phase > spec. + its own collisionProtection. The resolution order is: object > phase > content. enum: - Prevent - IfNoController @@ -2204,15 +2195,6 @@ spec: description: |- phases is a required list of phases that group objects to be applied together. - Objects are organized into phases based on their Group-Kind. Common phases include: - - namespaces: Namespace objects - - policies: ResourceQuota, LimitRange, NetworkPolicy objects - - rbac: ServiceAccount, Role, RoleBinding, ClusterRole, ClusterRoleBinding objects - - crds: CustomResourceDefinition objects - - storage: PersistentVolume, PersistentVolumeClaim, StorageClass objects - - deploy: Deployment, StatefulSet, DaemonSet, Service, ConfigMap, Secret objects - - publish: Ingress, APIService, Route, Webhook objects - All objects in a phase are applied in no particular order. The revision progresses to the next phase only after all objects in the current phase pass their readiness probes. @@ -2398,7 +2380,7 @@ spec: progressionProbes: description: |- progressionProbes is an optional field which provides the ability to define custom readiness probes - for objects defined within spec.phases. As documented in that field, most kubernetes-native objects + for objects defined within phases. As documented in that field, most kubernetes-native objects within the phases already have some kind of readiness check built-in, but this field allows for checks which are tailored to the objects being rolled out - particularly custom resources. @@ -2680,16 +2662,11 @@ spec: - revision type: object x-kubernetes-validations: - - message: phases cannot be removed once set - rule: '!has(oldSelf.phases) || has(self.phases)' - message: phases is immutable - rule: '!has(oldSelf.phases) || self.phases == oldSelf.phases || oldSelf.phases.size() - == 0' + rule: self.phases == oldSelf.phases - message: progressionProbes is immutable - rule: '!has(oldSelf.progressionProbes) || !has(self.progressionProbes) - && oldSelf.progressionProbes.size() == 0 || has(self.progressionProbes) - && (self.progressionProbes == oldSelf.progressionProbes || oldSelf.progressionProbes.size() - == 0)' + rule: has(self.progressionProbes) == has(oldSelf.progressionProbes) + && (!has(self.progressionProbes) || self.progressionProbes == oldSelf.progressionProbes) - message: collisionProtection is immutable rule: self.collisionProtection == oldSelf.collisionProtection status: diff --git a/manifests/experimental.yaml b/manifests/experimental.yaml index 69fff0d791..81257cfe4d 100644 --- a/manifests/experimental.yaml +++ b/manifests/experimental.yaml @@ -1418,10 +1418,10 @@ spec: collisionProtection: description: |- collisionProtection is a required field that specifies the default collision protection - strategy for all objects in this revision. Individual phases or objects can override this value. + strategy for all objects in this content. Individual phases or objects can override this value. This value is used as the default for any phase or object that does not explicitly specify - its own collisionProtection. The resolution order is: object > phase > spec. + its own collisionProtection. The resolution order is: object > phase > content. enum: - Prevent - IfNoController @@ -1431,15 +1431,6 @@ spec: description: |- phases is a required list of phases that group objects to be applied together. - Objects are organized into phases based on their Group-Kind. Common phases include: - - namespaces: Namespace objects - - policies: ResourceQuota, LimitRange, NetworkPolicy objects - - rbac: ServiceAccount, Role, RoleBinding, ClusterRole, ClusterRoleBinding objects - - crds: CustomResourceDefinition objects - - storage: PersistentVolume, PersistentVolumeClaim, StorageClass objects - - deploy: Deployment, StatefulSet, DaemonSet, Service, ConfigMap, Secret objects - - publish: Ingress, APIService, Route, Webhook objects - All objects in a phase are applied in no particular order. The revision progresses to the next phase only after all objects in the current phase pass their readiness probes. @@ -1630,7 +1621,7 @@ spec: progressionProbes: description: |- progressionProbes is an optional field which provides the ability to define custom readiness probes - for objects defined within spec.phases. As documented in that field, most kubernetes-native objects + for objects defined within phases. As documented in that field, most kubernetes-native objects within the phases already have some kind of readiness check built-in, but this field allows for checks which are tailored to the objects being rolled out - particularly custom resources. @@ -2114,10 +2105,10 @@ spec: collisionProtection: description: |- collisionProtection is a required field that specifies the default collision protection - strategy for all objects in this revision. Individual phases or objects can override this value. + strategy for all objects in this content. Individual phases or objects can override this value. This value is used as the default for any phase or object that does not explicitly specify - its own collisionProtection. The resolution order is: object > phase > spec. + its own collisionProtection. The resolution order is: object > phase > content. enum: - Prevent - IfNoController @@ -2165,15 +2156,6 @@ spec: description: |- phases is a required list of phases that group objects to be applied together. - Objects are organized into phases based on their Group-Kind. Common phases include: - - namespaces: Namespace objects - - policies: ResourceQuota, LimitRange, NetworkPolicy objects - - rbac: ServiceAccount, Role, RoleBinding, ClusterRole, ClusterRoleBinding objects - - crds: CustomResourceDefinition objects - - storage: PersistentVolume, PersistentVolumeClaim, StorageClass objects - - deploy: Deployment, StatefulSet, DaemonSet, Service, ConfigMap, Secret objects - - publish: Ingress, APIService, Route, Webhook objects - All objects in a phase are applied in no particular order. The revision progresses to the next phase only after all objects in the current phase pass their readiness probes. @@ -2359,7 +2341,7 @@ spec: progressionProbes: description: |- progressionProbes is an optional field which provides the ability to define custom readiness probes - for objects defined within spec.phases. As documented in that field, most kubernetes-native objects + for objects defined within phases. As documented in that field, most kubernetes-native objects within the phases already have some kind of readiness check built-in, but this field allows for checks which are tailored to the objects being rolled out - particularly custom resources. @@ -2641,16 +2623,11 @@ spec: - revision type: object x-kubernetes-validations: - - message: phases cannot be removed once set - rule: '!has(oldSelf.phases) || has(self.phases)' - message: phases is immutable - rule: '!has(oldSelf.phases) || self.phases == oldSelf.phases || oldSelf.phases.size() - == 0' + rule: self.phases == oldSelf.phases - message: progressionProbes is immutable - rule: '!has(oldSelf.progressionProbes) || !has(self.progressionProbes) - && oldSelf.progressionProbes.size() == 0 || has(self.progressionProbes) - && (self.progressionProbes == oldSelf.progressionProbes || oldSelf.progressionProbes.size() - == 0)' + rule: has(self.progressionProbes) == has(oldSelf.progressionProbes) + && (!has(self.progressionProbes) || self.progressionProbes == oldSelf.progressionProbes) - message: collisionProtection is immutable rule: self.collisionProtection == oldSelf.collisionProtection status: