diff --git a/api/v1/clusterobjectdeployment_types.go b/api/v1/clusterobjectdeployment_types.go index b7aad302c3..03bb766373 100644 --- a/api/v1/clusterobjectdeployment_types.go +++ b/api/v1/clusterobjectdeployment_types.go @@ -87,9 +87,11 @@ type ClusterObjectDeploymentTemplate struct { // ClusterObjectDeploymentTemplateMetadata contains labels and annotations // propagated to each ClusterObjectSet revision created from the template. +// +kubebuilder:validation:MinProperties=1 type ClusterObjectDeploymentTemplateMetadata struct { // labels is a set of key/value pairs propagated to each revision's metadata. // +optional + // +kubebuilder:validation:MinProperties=1 // +kubebuilder:validation:MaxProperties=32 Labels map[string]string `json:"labels,omitempty"` @@ -98,6 +100,7 @@ type ClusterObjectDeploymentTemplateMetadata struct { // rules (key format and total size) before creating a revision; invalid // annotations are reported on the deployment's Progressing condition. // +optional + // +kubebuilder:validation:MinProperties=1 // +kubebuilder:validation:MaxProperties=32 Annotations map[string]string `json:"annotations,omitempty"` } @@ -112,7 +115,7 @@ type ClusterObjectDeploymentTemplateSpec struct { // +kubebuilder:validation:MaxItems=20 // +listType=map // +listMapKey=name - Phases []ClusterObjectSetPhase `json:"phases"` //nolint:kubeapilinter // These are ordered groups of objects, not an enum describing lifecycle state. + Phases []ClusterObjectSetPhase `json:"phases,omitempty"` // These are ordered groups of objects, not an enum describing lifecycle state. // progressDeadlineMinutes is the maximum number of minutes a revision may take // to roll out, measured from the ClusterObjectSet's creation timestamp. Time @@ -120,9 +123,9 @@ type ClusterObjectDeploymentTemplateSpec struct { // deadline covers all phases together; completing a phase does not reset it. // Once a revision has completed, the deadline no longer applies, even if it // later becomes unhealthy. When omitted, no deadline is enforced. The minimum - // period is 10 minutes, and the maximum is 720 minutes (12 hours). + // period is 1 minute, and the maximum is 720 minutes (12 hours). // - // +kubebuilder:validation:Minimum:=10 + // +kubebuilder:validation:Minimum:=1 // +kubebuilder:validation:Maximum:=720 // +optional ProgressDeadlineMinutes int32 `json:"progressDeadlineMinutes,omitempty"` @@ -138,7 +141,6 @@ type ClusterObjectDeploymentTemplateSpec struct { // collisionProtection is the required default collision protection strategy for objects in the revision. // Objects and phases may override it. // +required - // +kubebuilder:validation:Enum=Prevent;IfNoController;None CollisionProtection CollisionProtection `json:"collisionProtection,omitempty"` } diff --git a/api/v1/clusterobjectset_types.go b/api/v1/clusterobjectset_types.go index c4044012d0..9568c5b519 100644 --- a/api/v1/clusterobjectset_types.go +++ b/api/v1/clusterobjectset_types.go @@ -46,6 +46,8 @@ const ( ) // ClusterObjectSetSpec defines the desired state of ClusterObjectSet. +// +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" 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. @@ -73,7 +75,6 @@ type ClusterObjectSetSpec struct { // new revision has been completely rolled out. // // +required - // +kubebuilder:validation:Enum=Active;Archived // +kubebuilder:validation:XValidation:rule="oldSelf == 'Active' || oldSelf == 'Archived' && oldSelf == self", message="cannot un-archive" LifecycleState ClusterObjectSetLifecycleState `json:"lifecycleState,omitempty"` @@ -108,11 +109,12 @@ type ClusterObjectSetSpec struct { // 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 // +listMapKey=name - // +optional - Phases []ClusterObjectSetPhase `json:"phases,omitempty"` //nolint:kubeapilinter + // +required + 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 @@ -144,7 +146,6 @@ type ClusterObjectSetSpec struct { // its own collisionProtection. The resolution order is: object > phase > spec. // // +required - // +kubebuilder:validation:Enum=Prevent;IfNoController;None // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="collisionProtection is immutable" CollisionProtection CollisionProtection `json:"collisionProtection,omitempty"` } @@ -163,7 +164,7 @@ type ProgressionProbe struct { Selector ObjectSelector `json:"selector,omitzero"` // assertions is a required list of checks which will run against the objects selected by the selector. If - // one or more assertions fail then the phase within which the object lives will be not be considered + // one or more assertions fail then the phase within which the object lives will not be considered // 'Ready', blocking rollout of all subsequent phases. // // +kubebuilder:validation:MinItems=1 @@ -192,7 +193,11 @@ type ObjectSelector struct { // Type SelectorType `json:"type,omitempty"` - // groupKind specifies the group and kind of objects to select. + // groupKind specifies the group and kind of objects to select. Note that this selector + // has a limitation in that it does not discriminate based on the version of the object; + // until this gap is closed users of this selector should take into account the possibility + // of different versions of the same groupKind not being able to pass assertions in the + // same way. // // Required when type is "GroupKind". // @@ -349,6 +354,7 @@ type FieldValueProbe struct { } // ClusterObjectSetLifecycleState specifies the lifecycle state of the ClusterObjectSet. +// +kubebuilder:validation:Enum=Active;Archived type ClusterObjectSetLifecycleState string const ( @@ -393,11 +399,10 @@ type ClusterObjectSetPhase struct { // When set, this value is used as the default for any object in this phase that does not // explicitly specify its own collisionProtection. // - // When omitted, we use .spec.collistionProtection as the default for any object in this phase that does not + // When omitted, we use .spec.collisionProtection as the default for any object in this phase that does not // explicitly specify its own collisionProtection. // // +optional - // +kubebuilder:validation:Enum=Prevent;IfNoController;None CollisionProtection CollisionProtection `json:"collisionProtection,omitempty"` } @@ -447,7 +452,6 @@ type ClusterObjectSetObject struct { // When omitted, the value is inherited from the phase, then spec. // // +optional - // +kubebuilder:validation:Enum=Prevent;IfNoController;None CollisionProtection CollisionProtection `json:"collisionProtection,omitempty"` } @@ -459,13 +463,16 @@ type ObjectSourceRef struct { // +required // +kubebuilder:validation:MinLength=1 // +kubebuilder:validation:MaxLength=253 + // +kubebuilder:validation:XValidation:rule=`!format.dns1123Label().validate(self).hasValue()`,message="the value must consist of only lowercase alphanumeric characters and hyphens, and must start and end with an alphanumeric character." Name string `json:"name,omitempty"` // namespace is the namespace of the referenced Secret. // When empty, defaults to the OLM system namespace during ref resolution. // // +optional + // +kubebuilder:validation:MinLength=1 // +kubebuilder:validation:MaxLength=63 + // +kubebuilder:validation:XValidation:rule=`!format.dns1123Label().validate(self).hasValue()`,message="the value must consist of only lowercase alphanumeric characters and hyphens, and must start and end with an alphanumeric character." Namespace string `json:"namespace,omitempty"` // key is the data key within the referenced Secret containing the @@ -475,10 +482,12 @@ type ObjectSourceRef struct { // +required // +kubebuilder:validation:MinLength=1 // +kubebuilder:validation:MaxLength=253 + // +kubebuilder:validation:XValidation:rule=`self.matches('^[-._a-zA-Z0-9]+$')`,message="must consist only of alphanumeric characters, '-', '_', or '.'" Key string `json:"key,omitempty"` } // CollisionProtection specifies if and how ownership collisions are prevented. +// +kubebuilder:validation:Enum=Prevent;IfNoController;None type CollisionProtection string const ( @@ -546,7 +555,7 @@ type ClusterObjectSetStatus struct { // +listType=map // +listMapKey=name // +optional - ObservedPhases []ObservedPhase `json:"observedPhases,omitempty"` //nolint:kubeapilinter + ObservedPhases []ObservedPhase `json:"observedPhases,omitempty"` // completedAt is the timestamp at which the revision was first observed to be // ready, meaning it had successfully rolled out and all of its objects passed @@ -554,7 +563,7 @@ type ClusterObjectSetStatus struct { // // +kubebuilder:validation:XValidation:rule="self == oldSelf || oldSelf == null",message="completedAt is immutable, once set" // +optional - CompletedAt metav1.Time `json:"completedAt,omitzero"` + CompletedAt metav1.Time `json:"completedAt,omitempty,omitzero"` } // ObservedPhase records the observed content digest of a resolved phase. @@ -579,7 +588,7 @@ type ObservedPhase struct { // Ready. // +optional // +kubebuilder:validation:XValidation:rule="self == oldSelf || oldSelf == null",message="completedAt is immutable, once set" - CompletedAt metav1.Time `json:"completedAt,omitzero"` + CompletedAt metav1.Time `json:"completedAt,omitempty,omitzero"` // message is a phase-level message providing context about the // current status (e.g. a validation error for InvalidSpecification @@ -592,6 +601,7 @@ type ObservedPhase struct { // digest is the digest of the phase's resolved object content // at first successful resolution, in the format ":". + // Currently, sha256 is the only supported algorithm. // // +required // +kubebuilder:validation:XValidation:rule="self == oldSelf || oldSelf.size() == 0",message="digest is immutable" @@ -608,7 +618,7 @@ type ObservedPhase struct { // +required // +kubebuilder:validation:Type=string // +kubebuilder:validation:Format=date-time - LastTransitionTime metav1.Time `json:"lastTransitionTime,omitzero"` + LastTransitionTime metav1.Time `json:"lastTransitionTime,omitempty,omitzero"` // objectDetails lists objects in this phase that are not yet // complete. For Reconciling, Pending, and Stabilizing phases, this @@ -660,7 +670,9 @@ type ObjectCounts struct { type ObjectStatus struct { // group is the API group of the object (empty string for core // resources). At most 253 characters (DNS subdomain max). + // +kubebuilder:validation:MinLength=1 // +kubebuilder:validation:MaxLength=253 + // +kubebuilder:validation:XValidation:rule=`!format.dns1123Subdomain().validate(self).hasValue()`,message="the value must be a valid DNS subdomain: lowercase alphanumeric characters, hyphens, and dots, starting and ending with an alphanumeric character." // +optional Group string `json:"group,omitempty"` @@ -680,7 +692,9 @@ type ObjectStatus struct { // namespace is the namespace of the object. Empty for cluster-scoped // resources. At most 253 characters (DNS subdomain max). + // +kubebuilder:validation:MinLength=1 // +kubebuilder:validation:MaxLength=253 + // +kubebuilder:validation:XValidation:rule=`!format.dns1123Label().validate(self).hasValue()`,message="the value must consist of only lowercase alphanumeric characters and hyphens, and must start and end with an alphanumeric character." // +optional Namespace string `json:"namespace,omitempty"` @@ -688,6 +702,7 @@ type ObjectStatus struct { // characters (DNS subdomain max). // +kubebuilder:validation:MinLength=1 // +kubebuilder:validation:MaxLength=253 + // +kubebuilder:validation:XValidation:rule=`self != "." && self != ".." && !self.contains("/") && !self.contains("%")`,message="name must be a valid object name: it may not be '.' or '..' or contain '/' or '%'" // +required Name string `json:"name,omitempty"` @@ -697,6 +712,7 @@ type ObjectStatus struct { // Each message is at most 1024 characters; longer messages are truncated // by the controller. // +kubebuilder:validation:MaxItems=18 + // +kubebuilder:validation:items:MinLength=1 // +kubebuilder:validation:items:MaxLength=1024 // +listType=atomic // +optional diff --git a/api/v1/clusterobjectset_types_test.go b/api/v1/clusterobjectset_types_test.go index bae5e1f08b..53fc6eecaa 100644 --- a/api/v1/clusterobjectset_types_test.go +++ b/api/v1/clusterobjectset_types_test.go @@ -31,6 +31,7 @@ func TestClusterObjectSetImmutability(t *testing.T) { LifecycleState: ClusterObjectSetLifecycleStateActive, Revision: 1, CollisionProtection: CollisionProtectionPrevent, + Phases: []ClusterObjectSetPhase{{Name: "deploy", Objects: []ClusterObjectSetObject{}}}, }, updateFunc: func(cos *ClusterObjectSet) { cos.Spec.Group = "another-group" @@ -43,6 +44,7 @@ func TestClusterObjectSetImmutability(t *testing.T) { LifecycleState: ClusterObjectSetLifecycleStateActive, Revision: 1, CollisionProtection: CollisionProtectionPrevent, + Phases: []ClusterObjectSetPhase{{Name: "deploy", Objects: []ClusterObjectSetObject{}}}, }, updateFunc: func(cos *ClusterObjectSet) { cos.Spec.Group = "" @@ -55,6 +57,7 @@ func TestClusterObjectSetImmutability(t *testing.T) { LifecycleState: ClusterObjectSetLifecycleStateActive, Revision: 1, CollisionProtection: CollisionProtectionPrevent, + Phases: []ClusterObjectSetPhase{{Name: "deploy", Objects: []ClusterObjectSetObject{}}}, }, updateFunc: func(cos *ClusterObjectSet) { cos.Spec.LifecycleState = ClusterObjectSetLifecycleStateArchived @@ -67,48 +70,14 @@ func TestClusterObjectSetImmutability(t *testing.T) { LifecycleState: ClusterObjectSetLifecycleStateActive, Revision: 1, CollisionProtection: CollisionProtectionPrevent, + Phases: []ClusterObjectSetPhase{{Name: "deploy", Objects: []ClusterObjectSetObject{}}}, }, updateFunc: func(cos *ClusterObjectSet) { cos.Spec.Revision = 2 }, expectedError: "revision is immutable", }, - "phases may be initially empty": { - spec: ClusterObjectSetSpec{ - Group: "immutability-empty-phases", - LifecycleState: ClusterObjectSetLifecycleStateActive, - Revision: 1, - CollisionProtection: CollisionProtectionPrevent, - Phases: []ClusterObjectSetPhase{}, - }, - updateFunc: func(cos *ClusterObjectSet) { - cos.Spec.Phases = []ClusterObjectSetPhase{ - { - Name: "foo", - Objects: []ClusterObjectSetObject{}, - }, - } - }, - allowed: true, - }, - "phases may be initially unset": { - spec: ClusterObjectSetSpec{ - Group: "immutability-unset-phases", - LifecycleState: ClusterObjectSetLifecycleStateActive, - Revision: 1, - CollisionProtection: CollisionProtectionPrevent, - }, - updateFunc: func(cos *ClusterObjectSet) { - cos.Spec.Phases = []ClusterObjectSetPhase{ - { - Name: "foo", - Objects: []ClusterObjectSetObject{}, - }, - } - }, - allowed: true, - }, - "phases are immutable if not empty": { + "phases are immutable once set": { spec: ClusterObjectSetSpec{ Group: "immutability-populated-phases", LifecycleState: ClusterObjectSetLifecycleStateActive, @@ -137,6 +106,7 @@ func TestClusterObjectSetImmutability(t *testing.T) { LifecycleState: ClusterObjectSetLifecycleStateActive, Revision: 1, CollisionProtection: CollisionProtectionPrevent, + Phases: []ClusterObjectSetPhase{{Name: "deploy", Objects: []ClusterObjectSetObject{}}}, }, updateFunc: func(cos *ClusterObjectSet) { cos.Spec.CollisionProtection = CollisionProtectionNone @@ -149,6 +119,7 @@ func TestClusterObjectSetImmutability(t *testing.T) { LifecycleState: ClusterObjectSetLifecycleStateActive, Revision: 1, CollisionProtection: CollisionProtectionPrevent, + Phases: []ClusterObjectSetPhase{{Name: "deploy", Objects: []ClusterObjectSetObject{}}}, }, updateFunc: func(cos *ClusterObjectSet) { cos.Spec.ProgressionProbes = []ProgressionProbe{validProbe()} @@ -161,6 +132,7 @@ func TestClusterObjectSetImmutability(t *testing.T) { LifecycleState: ClusterObjectSetLifecycleStateActive, Revision: 1, CollisionProtection: CollisionProtectionPrevent, + Phases: []ClusterObjectSetPhase{{Name: "deploy", Objects: []ClusterObjectSetObject{}}}, ProgressionProbes: []ProgressionProbe{validProbe()}, }, updateFunc: func(cos *ClusterObjectSet) { @@ -174,6 +146,7 @@ func TestClusterObjectSetImmutability(t *testing.T) { LifecycleState: ClusterObjectSetLifecycleStateArchived, Revision: 1, CollisionProtection: CollisionProtectionPrevent, + Phases: []ClusterObjectSetPhase{{Name: "deploy", Objects: []ClusterObjectSetObject{}}}, }, updateFunc: func(cos *ClusterObjectSet) { cos.Spec.LifecycleState = ClusterObjectSetLifecycleStateActive @@ -186,6 +159,7 @@ func TestClusterObjectSetImmutability(t *testing.T) { LifecycleState: ClusterObjectSetLifecycleStateActive, Revision: 1, CollisionProtection: CollisionProtectionPrevent, + Phases: []ClusterObjectSetPhase{{Name: "deploy", Objects: []ClusterObjectSetObject{}}}, }, updateFunc: func(cos *ClusterObjectSet) { cos.Spec.LifecycleState = ClusterObjectSetLifecycleStateArchived @@ -198,6 +172,7 @@ func TestClusterObjectSetImmutability(t *testing.T) { Revision: 1, LifecycleState: ClusterObjectSetLifecycleStateActive, CollisionProtection: CollisionProtectionPrevent, + Phases: []ClusterObjectSetPhase{{Name: "deploy", Objects: []ClusterObjectSetObject{}}}, }, updateFunc: func(cos *ClusterObjectSet) { cos.Labels = map[string]string{"test": "updated"} @@ -372,6 +347,7 @@ func TestClusterObjectSetValidity(t *testing.T) { LifecycleState: ClusterObjectSetLifecycleStateActive, Revision: 1, CollisionProtection: CollisionProtectionPrevent, + Phases: []ClusterObjectSetPhase{{Name: "deploy", Objects: []ClusterObjectSetObject{}}}, }, valid: true, }, @@ -381,6 +357,7 @@ func TestClusterObjectSetValidity(t *testing.T) { LifecycleState: ClusterObjectSetLifecycleStateActive, Revision: 1, CollisionProtection: CollisionProtectionIfNoController, + Phases: []ClusterObjectSetPhase{{Name: "deploy", Objects: []ClusterObjectSetObject{}}}, }, valid: true, }, @@ -390,6 +367,7 @@ func TestClusterObjectSetValidity(t *testing.T) { LifecycleState: ClusterObjectSetLifecycleStateActive, Revision: 1, CollisionProtection: CollisionProtectionNone, + Phases: []ClusterObjectSetPhase{{Name: "deploy", Objects: []ClusterObjectSetObject{}}}, }, valid: true, }, @@ -746,6 +724,7 @@ func TestClusterObjectSetRevisionBounds(t *testing.T) { "group": group, "lifecycleState": string(ClusterObjectSetLifecycleStateActive), "collisionProtection": string(CollisionProtectionPrevent), + "phases": []any{map[string]any{"name": "deploy", "objects": []any{}}}, } name := group + "-1" if tc.revision != nil { @@ -797,6 +776,7 @@ func TestClusterObjectSetCanonicalName(t *testing.T) { Revision: tc.revision, LifecycleState: ClusterObjectSetLifecycleStateActive, CollisionProtection: CollisionProtectionPrevent, + Phases: []ClusterObjectSetPhase{{Name: "deploy", Objects: []ClusterObjectSetObject{}}}, }, } err := c.Create(t.Context(), cos) @@ -845,6 +825,7 @@ func TestClusterObjectSetGroupValidation(t *testing.T) { "revision": int64(1), "lifecycleState": string(ClusterObjectSetLifecycleStateActive), "collisionProtection": string(CollisionProtectionPrevent), + "phases": []any{map[string]any{"name": "deploy", "objects": []any{}}}, } if tc.group != nil { spec["group"] = *tc.group @@ -880,6 +861,7 @@ func TestClusterObjectSetGroupFieldSelector(t *testing.T) { Revision: fixture.revision, LifecycleState: ClusterObjectSetLifecycleStateActive, CollisionProtection: CollisionProtectionPrevent, + Phases: []ClusterObjectSetPhase{{Name: "deploy", Objects: []ClusterObjectSetObject{}}}, }, } require.NoError(t, c.Create(t.Context(), cos)) @@ -913,7 +895,13 @@ func probeSpec(group string, probes ...ProgressionProbe) ClusterObjectSetSpec { LifecycleState: ClusterObjectSetLifecycleStateActive, Revision: 1, CollisionProtection: CollisionProtectionPrevent, - ProgressionProbes: probes, + Phases: []ClusterObjectSetPhase{ + { + Name: "deploy", + Objects: []ClusterObjectSetObject{}, + }, + }, + ProgressionProbes: probes, } } diff --git a/api/v1/validation_test.go b/api/v1/validation_test.go index 35446c4a16..293ea45147 100644 --- a/api/v1/validation_test.go +++ b/api/v1/validation_test.go @@ -102,15 +102,15 @@ func TestValidate(t *testing.T) { }, want: want{valid: true}, }, - "ClusterObjectDeployment: invalid progress deadline < 10": { + "ClusterObjectDeployment: valid progress deadline = 1": { args: args{ object: ClusterObjectDeploymentSpec{ Template: ClusterObjectDeploymentTemplate{ - Spec: ClusterObjectDeploymentTemplateSpec{ProgressDeadlineMinutes: 9}, + Spec: ClusterObjectDeploymentTemplateSpec{ProgressDeadlineMinutes: 1}, }, }, }, - want: want{valid: false, errContains: "spec.progressDeadlineMinutes"}, + want: want{valid: true}, }, "ClusterObjectDeployment: valid progress deadline = 10": { args: args{ @@ -287,6 +287,7 @@ func TestClusterObjectSetCompletedAtImmutable(t *testing.T) { Revision: 1, CollisionProtection: CollisionProtectionPrevent, LifecycleState: ClusterObjectSetLifecycleStateActive, + Phases: []ClusterObjectSetPhase{{Name: "deploy", Objects: []ClusterObjectSetObject{}}}, }, } require.NoError(t, c.Create(t.Context(), cos)) @@ -318,6 +319,7 @@ func TestClusterObjectSetCompletedAtCannotBeRemoved(t *testing.T) { Revision: 1, CollisionProtection: CollisionProtectionPrevent, LifecycleState: ClusterObjectSetLifecycleStateActive, + Phases: []ClusterObjectSetPhase{{Name: "deploy", Objects: []ClusterObjectSetObject{}}}, }, } require.NoError(t, c.Create(t.Context(), cos)) @@ -344,6 +346,7 @@ func TestClusterObjectSetDigestImmutable(t *testing.T) { Revision: 1, CollisionProtection: CollisionProtectionPrevent, LifecycleState: ClusterObjectSetLifecycleStateActive, + Phases: []ClusterObjectSetPhase{{Name: "deploy", Objects: []ClusterObjectSetObject{}}}, }, } require.NoError(t, c.Create(t.Context(), cos)) diff --git a/applyconfigurations/api/v1/clusterobjectdeploymenttemplatespec.go b/applyconfigurations/api/v1/clusterobjectdeploymenttemplatespec.go index 2c7bb4d347..ff5485731d 100644 --- a/applyconfigurations/api/v1/clusterobjectdeploymenttemplatespec.go +++ b/applyconfigurations/api/v1/clusterobjectdeploymenttemplatespec.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -// Code generated by controller-gen-v0.21. DO NOT EDIT. +// Code generated by controller-gen. DO NOT EDIT. package v1 @@ -37,7 +37,7 @@ type ClusterObjectDeploymentTemplateSpecApplyConfiguration struct { // deadline covers all phases together; completing a phase does not reset it. // Once a revision has completed, the deadline no longer applies, even if it // later becomes unhealthy. When omitted, no deadline is enforced. The minimum - // period is 10 minutes, and the maximum is 720 minutes (12 hours). + // period is 1 minute, and the maximum is 720 minutes (12 hours). ProgressDeadlineMinutes *int32 `json:"progressDeadlineMinutes,omitempty"` // progressionProbes supplies custom readiness probes for objects in every phase. // The existing ClusterObjectSet selector and assertion semantics apply. diff --git a/applyconfigurations/api/v1/clusterobjectsetphase.go b/applyconfigurations/api/v1/clusterobjectsetphase.go index 1d8eea68da..57c3d3a276 100644 --- a/applyconfigurations/api/v1/clusterobjectsetphase.go +++ b/applyconfigurations/api/v1/clusterobjectsetphase.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -// Code generated by controller-gen-v0.21. DO NOT EDIT. +// Code generated by controller-gen. DO NOT EDIT. package v1 @@ -48,7 +48,7 @@ type ClusterObjectSetPhaseApplyConfiguration struct { // When set, this value is used as the default for any object in this phase that does not // explicitly specify its own collisionProtection. // - // When omitted, we use .spec.collistionProtection as the default for any object in this phase that does not + // When omitted, we use .spec.collisionProtection as the default for any object in this phase that does not // explicitly specify its own collisionProtection. CollisionProtection *apiv1.CollisionProtection `json:"collisionProtection,omitempty"` } diff --git a/applyconfigurations/api/v1/objectselector.go b/applyconfigurations/api/v1/objectselector.go index 8cc2bd3762..22b1cbbff8 100644 --- a/applyconfigurations/api/v1/objectselector.go +++ b/applyconfigurations/api/v1/objectselector.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -// Code generated by controller-gen-v0.21. DO NOT EDIT. +// Code generated by controller-gen. DO NOT EDIT. package v1 @@ -38,7 +38,11 @@ type ObjectSelectorApplyConfiguration struct { // // Type *apiv1.SelectorType `json:"type,omitempty"` - // groupKind specifies the group and kind of objects to select. + // groupKind specifies the group and kind of objects to select. Note that this selector + // has a limitation in that it does not discriminate based on the version of the object; + // until this gap is closed users of this selector should take into account the possibility + // of different versions of the same groupKind not being able to pass assertions in the + // same way. // // Required when type is "GroupKind". // diff --git a/applyconfigurations/api/v1/observedphase.go b/applyconfigurations/api/v1/observedphase.go index 30decd9600..febfc37c6c 100644 --- a/applyconfigurations/api/v1/observedphase.go +++ b/applyconfigurations/api/v1/observedphase.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -// Code generated by controller-gen-v0.21. DO NOT EDIT. +// Code generated by controller-gen. DO NOT EDIT. package v1 @@ -45,6 +45,7 @@ type ObservedPhaseApplyConfiguration struct { Message *string `json:"message,omitempty"` // digest is the digest of the phase's resolved object content // at first successful resolution, in the format ":". + // Currently, sha256 is the only supported algorithm. Digest *string `json:"digest,omitempty"` // objectCounts reports the number of objects in this phase by state. ObjectCounts *ObjectCountsApplyConfiguration `json:"objectCounts,omitempty"` diff --git a/applyconfigurations/api/v1/progressionprobe.go b/applyconfigurations/api/v1/progressionprobe.go index 5564a2a1af..bd2112ca28 100644 --- a/applyconfigurations/api/v1/progressionprobe.go +++ b/applyconfigurations/api/v1/progressionprobe.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -// Code generated by controller-gen-v0.21. DO NOT EDIT. +// Code generated by controller-gen. DO NOT EDIT. package v1 @@ -33,7 +33,7 @@ type ProgressionProbeApplyConfiguration struct { // Selector *ObjectSelectorApplyConfiguration `json:"selector,omitempty"` // assertions is a required list of checks which will run against the objects selected by the selector. If - // one or more assertions fail then the phase within which the object lives will be not be considered + // one or more assertions fail then the phase within which the object lives will not be considered // 'Ready', blocking rollout of all subsequent phases. // // diff --git a/docs/api-reference/olmv1-api-reference.md b/docs/api-reference/olmv1-api-reference.md index 1e18b26772..52341070fe 100644 --- a/docs/api-reference/olmv1-api-reference.md +++ b/docs/api-reference/olmv1-api-reference.md @@ -474,7 +474,7 @@ _Appears in:_ | Field | Description | Default | Validation | | --- | --- | --- | --- | -| `metadata` _[ClusterObjectDeploymentTemplateMetadata](#clusterobjectdeploymenttemplatemetadata)_ | Refer to Kubernetes API documentation for fields of `metadata`. | | Optional: \{\}
| +| `metadata` _[ClusterObjectDeploymentTemplateMetadata](#clusterobjectdeploymenttemplatemetadata)_ | Refer to Kubernetes API documentation for fields of `metadata`. | | MinProperties: 1
Optional: \{\}
| | `spec` _[ClusterObjectDeploymentTemplateSpec](#clusterobjectdeploymenttemplatespec)_ | spec contains the desired revision content. Group, revision, lifecycle state,
and ownership are assigned by the controller and cannot be set here. | | Required: \{\}
| @@ -485,15 +485,16 @@ _Appears in:_ ClusterObjectDeploymentTemplateMetadata contains labels and annotations propagated to each ClusterObjectSet revision created from the template. - +_Validation:_ +- MinProperties: 1 _Appears in:_ - [ClusterObjectDeploymentTemplate](#clusterobjectdeploymenttemplate) | Field | Description | Default | Validation | | --- | --- | --- | --- | -| `labels` _object (keys:string, values:string)_ | labels is a set of key/value pairs propagated to each revision's metadata. | | MaxProperties: 32
Optional: \{\}
| -| `annotations` _object (keys:string, values:string)_ | annotations is a set of key/value pairs propagated to each revision's
metadata. The controller validates them against the Kubernetes annotation
rules (key format and total size) before creating a revision; invalid
annotations are reported on the deployment's Progressing condition. | | MaxProperties: 32
Optional: \{\}
| +| `labels` _object (keys:string, values:string)_ | labels is a set of key/value pairs propagated to each revision's metadata. | | MaxProperties: 32
MinProperties: 1
Optional: \{\}
| +| `annotations` _object (keys:string, values:string)_ | annotations is a set of key/value pairs propagated to each revision's
metadata. The controller validates them against the Kubernetes annotation
rules (key format and total size) before creating a revision; invalid
annotations are reported on the deployment's Progressing condition. | | MaxProperties: 32
MinProperties: 1
Optional: \{\}
| #### ClusterObjectDeploymentTemplateSpec @@ -510,7 +511,7 @@ _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 using the existing inline object or immutable Secret reference
representation. Phase, object, and probe order participate in template identity. | | MaxItems: 20
MinItems: 1
Required: \{\}
| -| `progressDeadlineMinutes` _integer_ | progressDeadlineMinutes is the maximum number of minutes a revision may take
to roll out, measured from the ClusterObjectSet's creation timestamp. Time
spent waiting for the ClusterObjectSet to be created is not counted. The
deadline covers all phases together; completing a phase does not reset it.
Once a revision has completed, the deadline no longer applies, even if it
later becomes unhealthy. When omitted, no deadline is enforced. The minimum
period is 10 minutes, and the maximum is 720 minutes (12 hours). | | Maximum: 720
Minimum: 10
Optional: \{\}
| +| `progressDeadlineMinutes` _integer_ | progressDeadlineMinutes is the maximum number of minutes a revision may take
to roll out, measured from the ClusterObjectSet's creation timestamp. Time
spent waiting for the ClusterObjectSet to be created is not counted. The
deadline covers all phases together; completing a phase does not reset it.
Once a revision has completed, the deadline no longer applies, even if it
later becomes unhealthy. When omitted, no deadline is enforced. The minimum
period is 1 minute, and the maximum is 720 minutes (12 hours). | | Maximum: 720
Minimum: 1
Optional: \{\}
| | `progressionProbes` _[ProgressionProbe](#progressionprobe) array_ | progressionProbes supplies custom readiness probes for objects in every phase.
The existing ClusterObjectSet selector and assertion semantics apply. | | MaxItems: 20
MinItems: 1
Optional: \{\}
| | `collisionProtection` _[CollisionProtection](#collisionprotection)_ | collisionProtection is the required default collision protection strategy for objects in the revision.
Objects and phases may override it. | | Enum: [Prevent IfNoController None]
Required: \{\}
| @@ -555,7 +556,7 @@ _Appears in:_ | --- | --- | --- | --- | | `name` _string_ | name is a required identifier for this phase.
phase names must follow the DNS label standard as defined in [RFC 1123].
They must contain only lowercase alphanumeric characters or hyphens (-),
start and end with an alphanumeric character, and be no longer than 63 characters.
Common phase names include: namespaces, policies, rbac, crds, storage, deploy, publish.
[RFC 1123]: https://tools.ietf.org/html/rfc1123 | | MaxLength: 63
MinLength: 1
Required: \{\}
| | `objects` _[ClusterObjectSetObject](#clusterobjectsetobject) array_ | objects is a required list of all Kubernetes objects that belong to this phase.
All objects in this list are applied to the cluster in no particular order. The maximum number of objects per phase is 50. | | ExactlyOneOf: [object ref]
MaxItems: 50
Required: \{\}
| -| `collisionProtection` _[CollisionProtection](#collisionprotection)_ | collisionProtection specifies the default collision protection strategy for all objects
in this phase. Individual objects can override this value.
When set, this value is used as the default for any object in this phase that does not
explicitly specify its own collisionProtection.
When omitted, we use .spec.collistionProtection as the default for any object in this phase that does not
explicitly specify its own collisionProtection. | | Enum: [Prevent IfNoController None]
Optional: \{\}
| +| `collisionProtection` _[CollisionProtection](#collisionprotection)_ | collisionProtection specifies the default collision protection strategy for all objects
in this phase. Individual objects can override this value.
When set, this value is used as the default for any object in this phase that does not
explicitly specify its own collisionProtection.
When omitted, we use .spec.collisionProtection as the default for any object in this phase that does not
explicitly specify its own collisionProtection. | | Enum: [Prevent IfNoController None]
Optional: \{\}
| #### CollisionProtection @@ -564,7 +565,8 @@ _Underlying type:_ _string_ CollisionProtection specifies if and how ownership collisions are prevented. - +_Validation:_ +- Enum: [Prevent IfNoController None] _Appears in:_ - [ClusterObjectDeploymentTemplateSpec](#clusterobjectdeploymenttemplatespec) @@ -665,7 +667,7 @@ _Appears in:_ | Field | Description | Default | Validation | | --- | --- | --- | --- | | `type` _[SelectorType](#selectortype)_ | type is a required field which specifies the type of selector to use.
The allowed selector types are "GroupKind" and "Label".
When set to "GroupKind", all objects which match the specified group and kind will be selected.
When set to "Label", all objects which match the specified labels and/or expressions will be selected.
**Experimental channel:** | | Enum: [GroupKind Label]
Required: \{\}
| -| `groupKind` _[GroupKind](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.31/#groupkind-v1-meta)_ | groupKind specifies the group and kind of objects to select.
Required when type is "GroupKind".
Uses the Kubernetes format specified here:
https://pkg.go.dev/k8s.io/apimachinery/pkg/apis/meta/v1#GroupKind
**Experimental channel:** | | Optional: \{\}
| +| `groupKind` _[GroupKind](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.31/#groupkind-v1-meta)_ | groupKind specifies the group and kind of objects to select. Note that this selector
has a limitation in that it does not discriminate based on the version of the object;
until this gap is closed users of this selector should take into account the possibility
of different versions of the same groupKind not being able to pass assertions in the
same way.
Required when type is "GroupKind".
Uses the Kubernetes format specified here:
https://pkg.go.dev/k8s.io/apimachinery/pkg/apis/meta/v1#GroupKind
**Experimental channel:** | | Optional: \{\}
| | `label` _[LabelSelector](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.31/#labelselector-v1-meta)_ | label is the label selector definition.
Required when type is "Label".
A probe using a Label selector will be executed against every object matching the labels or expressions; you must use care
when using this type of selector. For example, if multiple Kind objects are selected via labels then the probe is
likely to fail because the values of different Kind objects rarely share the same schema.
The LabelSelector field uses the following Kubernetes format:
https://pkg.go.dev/k8s.io/apimachinery/pkg/apis/meta/v1#LabelSelector
Requires exactly one of matchLabels or matchExpressions.
**Experimental channel:** | | Optional: \{\}
| @@ -684,7 +686,7 @@ _Appears in:_ | Field | Description | Default | Validation | | --- | --- | --- | --- | | `name` _string_ | name is the name of the referenced Secret. | | MaxLength: 253
MinLength: 1
Required: \{\}
| -| `namespace` _string_ | namespace is the namespace of the referenced Secret.
When empty, defaults to the OLM system namespace during ref resolution. | | MaxLength: 63
Optional: \{\}
| +| `namespace` _string_ | namespace is the namespace of the referenced Secret.
When empty, defaults to the OLM system namespace during ref resolution. | | MaxLength: 63
MinLength: 1
Optional: \{\}
| | `key` _string_ | key is the data key within the referenced Secret containing the
object manifest content. The value at this key must be a
JSON-serialized Kubernetes object manifest. | | MaxLength: 253
MinLength: 1
Required: \{\}
| @@ -740,7 +742,7 @@ _Appears in:_ | Field | Description | Default | Validation | | --- | --- | --- | --- | | `selector` _[ObjectSelector](#objectselector)_ | selector is a required field which defines the method by which we select objects to apply the below
assertions to. Any object which matches the defined selector will have all the associated assertions
applied against it.
If no objects within a phase are selected by the provided selector, then all assertions defined here
are considered to have succeeded.
**Experimental channel:** | | Required: \{\}
| -| `assertions` _[Assertion](#assertion) array_ | assertions is a required list of checks which will run against the objects selected by the selector. If
one or more assertions fail then the phase within which the object lives will be not be considered
'Ready', blocking rollout of all subsequent phases.
**Experimental channel:** | | MaxItems: 20
MinItems: 1
Required: \{\}
| +| `assertions` _[Assertion](#assertion) array_ | assertions is a required list of checks which will run against the objects selected by the selector. If
one or more assertions fail then the phase within which the object lives will not be considered
'Ready', blocking rollout of all subsequent phases.
**Experimental channel:** | | MaxItems: 20
MinItems: 1
Required: \{\}
| #### ResolvedCatalogSource diff --git a/hack/api-lint-diff/run.sh b/hack/api-lint-diff/run.sh index 3bf0652de9..a5771c2ed0 100755 --- a/hack/api-lint-diff/run.sh +++ b/hack/api-lint-diff/run.sh @@ -83,6 +83,7 @@ linters: linters: disable: - nonpointerstructs + - nophase lintersConfig: optionalfields: pointers: 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 4505b49d9f..918a73c34a 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 @@ -82,6 +82,7 @@ spec: olm.operatorframework.io/template-hash label, whose value is always set by the controller. Changes to metadata change template identity and therefore create a new revision. + minProperties: 1 properties: annotations: additionalProperties: @@ -92,6 +93,7 @@ spec: rules (key format and total size) before creating a revision; invalid annotations are reported on the deployment's Progressing condition. maxProperties: 32 + minProperties: 1 type: object labels: additionalProperties: @@ -99,6 +101,7 @@ spec: description: labels is a set of key/value pairs propagated to each revision's metadata. maxProperties: 32 + minProperties: 1 type: object type: object spec: @@ -133,7 +136,7 @@ spec: When set, this value is used as the default for any object in this phase that does not explicitly specify its own collisionProtection. - When omitted, we use .spec.collistionProtection as the default for any object in this phase that does not + When omitted, we use .spec.collisionProtection as the default for any object in this phase that does not explicitly specify its own collisionProtection. enum: - Prevent @@ -221,18 +224,35 @@ spec: maxLength: 253 minLength: 1 type: string + x-kubernetes-validations: + - message: must consist only of alphanumeric + characters, '-', '_', or '.' + rule: self.matches('^[-._a-zA-Z0-9]+$') name: description: name is the name of the referenced Secret. maxLength: 253 minLength: 1 type: string + x-kubernetes-validations: + - message: the value must consist of only + lowercase alphanumeric characters and + hyphens, and must start and end with an + alphanumeric character. + rule: '!format.dns1123Label().validate(self).hasValue()' namespace: description: |- namespace is the namespace of the referenced Secret. When empty, defaults to the OLM system namespace during ref resolution. maxLength: 63 + minLength: 1 type: string + x-kubernetes-validations: + - message: the value must consist of only + lowercase alphanumeric characters and + hyphens, and must start and end with an + alphanumeric character. + rule: '!format.dns1123Label().validate(self).hasValue()' required: - key - name @@ -264,10 +284,10 @@ spec: deadline covers all phases together; completing a phase does not reset it. Once a revision has completed, the deadline no longer applies, even if it later becomes unhealthy. When omitted, no deadline is enforced. The minimum - period is 10 minutes, and the maximum is 720 minutes (12 hours). + period is 1 minute, and the maximum is 720 minutes (12 hours). format: int32 maximum: 720 - minimum: 10 + minimum: 1 type: integer progressionProbes: description: |- @@ -280,7 +300,7 @@ spec: assertions: description: |- assertions is a required list of checks which will run against the objects selected by the selector. If - one or more assertions fail then the phase within which the object lives will be not be considered + one or more assertions fail then the phase within which the object lives will not be considered 'Ready', blocking rollout of all subsequent phases. items: description: Assertion is a discriminated union which @@ -416,7 +436,11 @@ spec: properties: groupKind: description: |- - groupKind specifies the group and kind of objects to select. + groupKind specifies the group and kind of objects to select. Note that this selector + has a limitation in that it does not discriminate based on the version of the object; + until this gap is closed users of this selector should take into account the possibility + of different versions of the same groupKind not being able to pass assertions in the + same way. Required when type is "GroupKind". 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 3e7fc64e85..491d5eabdd 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 @@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.22.0 + controller-gen.kubebuilder.io/version: v0.23.0 olm.operatorframework.io/generator: experimental name: clusterobjectsets.olm.operatorframework.io spec: @@ -162,7 +162,7 @@ spec: When set, this value is used as the default for any object in this phase that does not explicitly specify its own collisionProtection. - When omitted, we use .spec.collistionProtection as the default for any object in this phase that does not + When omitted, we use .spec.collisionProtection as the default for any object in this phase that does not explicitly specify its own collisionProtection. enum: - Prevent @@ -250,17 +250,32 @@ spec: maxLength: 253 minLength: 1 type: string + x-kubernetes-validations: + - message: must consist only of alphanumeric characters, + '-', '_', or '.' + rule: self.matches('^[-._a-zA-Z0-9]+$') name: description: name is the name of the referenced Secret. maxLength: 253 minLength: 1 type: string + x-kubernetes-validations: + - message: the value must consist of only lowercase + alphanumeric characters and hyphens, and must + start and end with an alphanumeric character. + rule: '!format.dns1123Label().validate(self).hasValue()' namespace: description: |- namespace is the namespace of the referenced Secret. When empty, defaults to the OLM system namespace during ref resolution. maxLength: 63 + minLength: 1 type: string + x-kubernetes-validations: + - message: the value must consist of only lowercase + alphanumeric characters and hyphens, and must + start and end with an alphanumeric character. + rule: '!format.dns1123Label().validate(self).hasValue()' required: - key - name @@ -278,6 +293,7 @@ spec: - objects type: object maxItems: 20 + minItems: 1 type: array x-kubernetes-list-map-keys: - name @@ -308,7 +324,7 @@ spec: assertions: description: |- assertions is a required list of checks which will run against the objects selected by the selector. If - one or more assertions fail then the phase within which the object lives will be not be considered + one or more assertions fail then the phase within which the object lives will not be considered 'Ready', blocking rollout of all subsequent phases. items: description: Assertion is a discriminated union which defines @@ -440,7 +456,11 @@ spec: properties: groupKind: description: |- - groupKind specifies the group and kind of objects to select. + groupKind specifies the group and kind of objects to select. Note that this selector + has a limitation in that it does not discriminate based on the version of the object; + until this gap is closed users of this selector should take into account the possibility + of different versions of the same groupKind not being able to pass assertions in the + same way. Required when type is "GroupKind". @@ -568,8 +588,14 @@ spec: - collisionProtection - group - lifecycleState + - phases - revision type: object + 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)' status: description: |- status is optional and defines the observed state of the ClusterObjectSet. @@ -734,6 +760,7 @@ spec: description: |- digest is the digest of the phase's resolved object content at first successful resolution, in the format ":". + Currently, sha256 is the only supported algorithm. maxLength: 256 minLength: 1 type: string @@ -827,7 +854,13 @@ spec: group is the API group of the object (empty string for core resources). At most 253 characters (DNS subdomain max). maxLength: 253 + minLength: 1 type: string + x-kubernetes-validations: + - message: 'the value must be a valid DNS subdomain: lowercase + alphanumeric characters, hyphens, and dots, starting + and ending with an alphanumeric character.' + rule: '!format.dns1123Subdomain().validate(self).hasValue()' kind: description: |- kind is the kind of the object. Must be between 1 and 63 @@ -844,6 +877,7 @@ spec: by the controller. items: maxLength: 1024 + minLength: 1 type: string maxItems: 18 type: array @@ -855,12 +889,23 @@ spec: maxLength: 253 minLength: 1 type: string + x-kubernetes-validations: + - message: 'name must be a valid object name: it may not + be ''.'' or ''..'' or contain ''/'' or ''%''' + rule: self != "." && self != ".." && !self.contains("/") + && !self.contains("%") namespace: description: |- namespace is the namespace of the object. Empty for cluster-scoped resources. At most 253 characters (DNS subdomain max). maxLength: 253 + minLength: 1 type: string + x-kubernetes-validations: + - message: the value must consist of only lowercase alphanumeric + characters and hyphens, and must start and end with + an alphanumeric character. + rule: '!format.dns1123Label().validate(self).hasValue()' version: description: |- version is the API version of the object. Must be between 1 and diff --git a/internal/operator-controller/controllers/boxcutter_reconcile_steps_test.go b/internal/operator-controller/controllers/boxcutter_reconcile_steps_test.go index c20a9309de..9235e6f94f 100644 --- a/internal/operator-controller/controllers/boxcutter_reconcile_steps_test.go +++ b/internal/operator-controller/controllers/boxcutter_reconcile_steps_test.go @@ -64,6 +64,12 @@ func TestBoxcutterRevisionStatesGetter_GroupIndex(t *testing.T) { Group: cosDef.group, Revision: cosDef.revision, LifecycleState: ocv1.ClusterObjectSetLifecycleStateActive, CollisionProtection: ocv1.CollisionProtectionPrevent, + Phases: []ocv1.ClusterObjectSetPhase{ + { + Name: "test-phase", + Objects: []ocv1.ClusterObjectSetObject{}, + }, + }, }, } if cosDef.group != group { diff --git a/manifests/experimental-e2e.yaml b/manifests/experimental-e2e.yaml index 02a58c730c..0d9dbe0a1e 100644 --- a/manifests/experimental-e2e.yaml +++ b/manifests/experimental-e2e.yaml @@ -1411,6 +1411,7 @@ spec: olm.operatorframework.io/template-hash label, whose value is always set by the controller. Changes to metadata change template identity and therefore create a new revision. + minProperties: 1 properties: annotations: additionalProperties: @@ -1421,6 +1422,7 @@ spec: rules (key format and total size) before creating a revision; invalid annotations are reported on the deployment's Progressing condition. maxProperties: 32 + minProperties: 1 type: object labels: additionalProperties: @@ -1428,6 +1430,7 @@ spec: description: labels is a set of key/value pairs propagated to each revision's metadata. maxProperties: 32 + minProperties: 1 type: object type: object spec: @@ -1462,7 +1465,7 @@ spec: When set, this value is used as the default for any object in this phase that does not explicitly specify its own collisionProtection. - When omitted, we use .spec.collistionProtection as the default for any object in this phase that does not + When omitted, we use .spec.collisionProtection as the default for any object in this phase that does not explicitly specify its own collisionProtection. enum: - Prevent @@ -1550,18 +1553,35 @@ spec: maxLength: 253 minLength: 1 type: string + x-kubernetes-validations: + - message: must consist only of alphanumeric + characters, '-', '_', or '.' + rule: self.matches('^[-._a-zA-Z0-9]+$') name: description: name is the name of the referenced Secret. maxLength: 253 minLength: 1 type: string + x-kubernetes-validations: + - message: the value must consist of only + lowercase alphanumeric characters and + hyphens, and must start and end with an + alphanumeric character. + rule: '!format.dns1123Label().validate(self).hasValue()' namespace: description: |- namespace is the namespace of the referenced Secret. When empty, defaults to the OLM system namespace during ref resolution. maxLength: 63 + minLength: 1 type: string + x-kubernetes-validations: + - message: the value must consist of only + lowercase alphanumeric characters and + hyphens, and must start and end with an + alphanumeric character. + rule: '!format.dns1123Label().validate(self).hasValue()' required: - key - name @@ -1593,10 +1613,10 @@ spec: deadline covers all phases together; completing a phase does not reset it. Once a revision has completed, the deadline no longer applies, even if it later becomes unhealthy. When omitted, no deadline is enforced. The minimum - period is 10 minutes, and the maximum is 720 minutes (12 hours). + period is 1 minute, and the maximum is 720 minutes (12 hours). format: int32 maximum: 720 - minimum: 10 + minimum: 1 type: integer progressionProbes: description: |- @@ -1609,7 +1629,7 @@ spec: assertions: description: |- assertions is a required list of checks which will run against the objects selected by the selector. If - one or more assertions fail then the phase within which the object lives will be not be considered + one or more assertions fail then the phase within which the object lives will not be considered 'Ready', blocking rollout of all subsequent phases. items: description: Assertion is a discriminated union which @@ -1745,7 +1765,11 @@ spec: properties: groupKind: description: |- - groupKind specifies the group and kind of objects to select. + groupKind specifies the group and kind of objects to select. Note that this selector + has a limitation in that it does not discriminate based on the version of the object; + until this gap is closed users of this selector should take into account the possibility + of different versions of the same groupKind not being able to pass assertions in the + same way. Required when type is "GroupKind". @@ -1995,7 +2019,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.22.0 + controller-gen.kubebuilder.io/version: v0.23.0 olm.operatorframework.io/generator: experimental name: clusterobjectsets.olm.operatorframework.io spec: @@ -2154,7 +2178,7 @@ spec: When set, this value is used as the default for any object in this phase that does not explicitly specify its own collisionProtection. - When omitted, we use .spec.collistionProtection as the default for any object in this phase that does not + When omitted, we use .spec.collisionProtection as the default for any object in this phase that does not explicitly specify its own collisionProtection. enum: - Prevent @@ -2242,17 +2266,32 @@ spec: maxLength: 253 minLength: 1 type: string + x-kubernetes-validations: + - message: must consist only of alphanumeric characters, + '-', '_', or '.' + rule: self.matches('^[-._a-zA-Z0-9]+$') name: description: name is the name of the referenced Secret. maxLength: 253 minLength: 1 type: string + x-kubernetes-validations: + - message: the value must consist of only lowercase + alphanumeric characters and hyphens, and must + start and end with an alphanumeric character. + rule: '!format.dns1123Label().validate(self).hasValue()' namespace: description: |- namespace is the namespace of the referenced Secret. When empty, defaults to the OLM system namespace during ref resolution. maxLength: 63 + minLength: 1 type: string + x-kubernetes-validations: + - message: the value must consist of only lowercase + alphanumeric characters and hyphens, and must + start and end with an alphanumeric character. + rule: '!format.dns1123Label().validate(self).hasValue()' required: - key - name @@ -2270,6 +2309,7 @@ spec: - objects type: object maxItems: 20 + minItems: 1 type: array x-kubernetes-list-map-keys: - name @@ -2300,7 +2340,7 @@ spec: assertions: description: |- assertions is a required list of checks which will run against the objects selected by the selector. If - one or more assertions fail then the phase within which the object lives will be not be considered + one or more assertions fail then the phase within which the object lives will not be considered 'Ready', blocking rollout of all subsequent phases. items: description: Assertion is a discriminated union which defines @@ -2432,7 +2472,11 @@ spec: properties: groupKind: description: |- - groupKind specifies the group and kind of objects to select. + groupKind specifies the group and kind of objects to select. Note that this selector + has a limitation in that it does not discriminate based on the version of the object; + until this gap is closed users of this selector should take into account the possibility + of different versions of the same groupKind not being able to pass assertions in the + same way. Required when type is "GroupKind". @@ -2560,8 +2604,14 @@ spec: - collisionProtection - group - lifecycleState + - phases - revision type: object + 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)' status: description: |- status is optional and defines the observed state of the ClusterObjectSet. @@ -2726,6 +2776,7 @@ spec: description: |- digest is the digest of the phase's resolved object content at first successful resolution, in the format ":". + Currently, sha256 is the only supported algorithm. maxLength: 256 minLength: 1 type: string @@ -2819,7 +2870,13 @@ spec: group is the API group of the object (empty string for core resources). At most 253 characters (DNS subdomain max). maxLength: 253 + minLength: 1 type: string + x-kubernetes-validations: + - message: 'the value must be a valid DNS subdomain: lowercase + alphanumeric characters, hyphens, and dots, starting + and ending with an alphanumeric character.' + rule: '!format.dns1123Subdomain().validate(self).hasValue()' kind: description: |- kind is the kind of the object. Must be between 1 and 63 @@ -2836,6 +2893,7 @@ spec: by the controller. items: maxLength: 1024 + minLength: 1 type: string maxItems: 18 type: array @@ -2847,12 +2905,23 @@ spec: maxLength: 253 minLength: 1 type: string + x-kubernetes-validations: + - message: 'name must be a valid object name: it may not + be ''.'' or ''..'' or contain ''/'' or ''%''' + rule: self != "." && self != ".." && !self.contains("/") + && !self.contains("%") namespace: description: |- namespace is the namespace of the object. Empty for cluster-scoped resources. At most 253 characters (DNS subdomain max). maxLength: 253 + minLength: 1 type: string + x-kubernetes-validations: + - message: the value must consist of only lowercase alphanumeric + characters and hyphens, and must start and end with + an alphanumeric character. + rule: '!format.dns1123Label().validate(self).hasValue()' version: description: |- version is the API version of the object. Must be between 1 and diff --git a/manifests/experimental.yaml b/manifests/experimental.yaml index 72b24b71b6..70aa114f5e 100644 --- a/manifests/experimental.yaml +++ b/manifests/experimental.yaml @@ -1372,6 +1372,7 @@ spec: olm.operatorframework.io/template-hash label, whose value is always set by the controller. Changes to metadata change template identity and therefore create a new revision. + minProperties: 1 properties: annotations: additionalProperties: @@ -1382,6 +1383,7 @@ spec: rules (key format and total size) before creating a revision; invalid annotations are reported on the deployment's Progressing condition. maxProperties: 32 + minProperties: 1 type: object labels: additionalProperties: @@ -1389,6 +1391,7 @@ spec: description: labels is a set of key/value pairs propagated to each revision's metadata. maxProperties: 32 + minProperties: 1 type: object type: object spec: @@ -1423,7 +1426,7 @@ spec: When set, this value is used as the default for any object in this phase that does not explicitly specify its own collisionProtection. - When omitted, we use .spec.collistionProtection as the default for any object in this phase that does not + When omitted, we use .spec.collisionProtection as the default for any object in this phase that does not explicitly specify its own collisionProtection. enum: - Prevent @@ -1511,18 +1514,35 @@ spec: maxLength: 253 minLength: 1 type: string + x-kubernetes-validations: + - message: must consist only of alphanumeric + characters, '-', '_', or '.' + rule: self.matches('^[-._a-zA-Z0-9]+$') name: description: name is the name of the referenced Secret. maxLength: 253 minLength: 1 type: string + x-kubernetes-validations: + - message: the value must consist of only + lowercase alphanumeric characters and + hyphens, and must start and end with an + alphanumeric character. + rule: '!format.dns1123Label().validate(self).hasValue()' namespace: description: |- namespace is the namespace of the referenced Secret. When empty, defaults to the OLM system namespace during ref resolution. maxLength: 63 + minLength: 1 type: string + x-kubernetes-validations: + - message: the value must consist of only + lowercase alphanumeric characters and + hyphens, and must start and end with an + alphanumeric character. + rule: '!format.dns1123Label().validate(self).hasValue()' required: - key - name @@ -1554,10 +1574,10 @@ spec: deadline covers all phases together; completing a phase does not reset it. Once a revision has completed, the deadline no longer applies, even if it later becomes unhealthy. When omitted, no deadline is enforced. The minimum - period is 10 minutes, and the maximum is 720 minutes (12 hours). + period is 1 minute, and the maximum is 720 minutes (12 hours). format: int32 maximum: 720 - minimum: 10 + minimum: 1 type: integer progressionProbes: description: |- @@ -1570,7 +1590,7 @@ spec: assertions: description: |- assertions is a required list of checks which will run against the objects selected by the selector. If - one or more assertions fail then the phase within which the object lives will be not be considered + one or more assertions fail then the phase within which the object lives will not be considered 'Ready', blocking rollout of all subsequent phases. items: description: Assertion is a discriminated union which @@ -1706,7 +1726,11 @@ spec: properties: groupKind: description: |- - groupKind specifies the group and kind of objects to select. + groupKind specifies the group and kind of objects to select. Note that this selector + has a limitation in that it does not discriminate based on the version of the object; + until this gap is closed users of this selector should take into account the possibility + of different versions of the same groupKind not being able to pass assertions in the + same way. Required when type is "GroupKind". @@ -1956,7 +1980,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.22.0 + controller-gen.kubebuilder.io/version: v0.23.0 olm.operatorframework.io/generator: experimental name: clusterobjectsets.olm.operatorframework.io spec: @@ -2115,7 +2139,7 @@ spec: When set, this value is used as the default for any object in this phase that does not explicitly specify its own collisionProtection. - When omitted, we use .spec.collistionProtection as the default for any object in this phase that does not + When omitted, we use .spec.collisionProtection as the default for any object in this phase that does not explicitly specify its own collisionProtection. enum: - Prevent @@ -2203,17 +2227,32 @@ spec: maxLength: 253 minLength: 1 type: string + x-kubernetes-validations: + - message: must consist only of alphanumeric characters, + '-', '_', or '.' + rule: self.matches('^[-._a-zA-Z0-9]+$') name: description: name is the name of the referenced Secret. maxLength: 253 minLength: 1 type: string + x-kubernetes-validations: + - message: the value must consist of only lowercase + alphanumeric characters and hyphens, and must + start and end with an alphanumeric character. + rule: '!format.dns1123Label().validate(self).hasValue()' namespace: description: |- namespace is the namespace of the referenced Secret. When empty, defaults to the OLM system namespace during ref resolution. maxLength: 63 + minLength: 1 type: string + x-kubernetes-validations: + - message: the value must consist of only lowercase + alphanumeric characters and hyphens, and must + start and end with an alphanumeric character. + rule: '!format.dns1123Label().validate(self).hasValue()' required: - key - name @@ -2231,6 +2270,7 @@ spec: - objects type: object maxItems: 20 + minItems: 1 type: array x-kubernetes-list-map-keys: - name @@ -2261,7 +2301,7 @@ spec: assertions: description: |- assertions is a required list of checks which will run against the objects selected by the selector. If - one or more assertions fail then the phase within which the object lives will be not be considered + one or more assertions fail then the phase within which the object lives will not be considered 'Ready', blocking rollout of all subsequent phases. items: description: Assertion is a discriminated union which defines @@ -2393,7 +2433,11 @@ spec: properties: groupKind: description: |- - groupKind specifies the group and kind of objects to select. + groupKind specifies the group and kind of objects to select. Note that this selector + has a limitation in that it does not discriminate based on the version of the object; + until this gap is closed users of this selector should take into account the possibility + of different versions of the same groupKind not being able to pass assertions in the + same way. Required when type is "GroupKind". @@ -2521,8 +2565,14 @@ spec: - collisionProtection - group - lifecycleState + - phases - revision type: object + 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)' status: description: |- status is optional and defines the observed state of the ClusterObjectSet. @@ -2687,6 +2737,7 @@ spec: description: |- digest is the digest of the phase's resolved object content at first successful resolution, in the format ":". + Currently, sha256 is the only supported algorithm. maxLength: 256 minLength: 1 type: string @@ -2780,7 +2831,13 @@ spec: group is the API group of the object (empty string for core resources). At most 253 characters (DNS subdomain max). maxLength: 253 + minLength: 1 type: string + x-kubernetes-validations: + - message: 'the value must be a valid DNS subdomain: lowercase + alphanumeric characters, hyphens, and dots, starting + and ending with an alphanumeric character.' + rule: '!format.dns1123Subdomain().validate(self).hasValue()' kind: description: |- kind is the kind of the object. Must be between 1 and 63 @@ -2797,6 +2854,7 @@ spec: by the controller. items: maxLength: 1024 + minLength: 1 type: string maxItems: 18 type: array @@ -2808,12 +2866,23 @@ spec: maxLength: 253 minLength: 1 type: string + x-kubernetes-validations: + - message: 'name must be a valid object name: it may not + be ''.'' or ''..'' or contain ''/'' or ''%''' + rule: self != "." && self != ".." && !self.contains("/") + && !self.contains("%") namespace: description: |- namespace is the namespace of the object. Empty for cluster-scoped resources. At most 253 characters (DNS subdomain max). maxLength: 253 + minLength: 1 type: string + x-kubernetes-validations: + - message: the value must consist of only lowercase alphanumeric + characters and hyphens, and must start and end with + an alphanumeric character. + rule: '!format.dns1123Label().validate(self).hasValue()' version: description: |- version is the API version of the object. Must be between 1 and diff --git a/test/e2e/features/cod_progressing_reasons.feature b/test/e2e/features/cod_progressing_reasons.feature index 8c9420d6aa..7caaa2f0cc 100644 --- a/test/e2e/features/cod_progressing_reasons.feature +++ b/test/e2e/features/cod_progressing_reasons.feature @@ -144,7 +144,6 @@ Feature: ClusterObjectDeployment impeded-rollout Progressing reasons @ProgressDeadline Scenario: Progressing is False with ProgressDeadlineExceeded when the revision never becomes ready Given namespace "${TEST_NAMESPACE}" is available - And min value for ClusterObjectDeployment .spec.template.spec.progressDeadlineMinutes is set to 1 When resource is applied """ apiVersion: olm.operatorframework.io/v1 @@ -202,7 +201,6 @@ Feature: ClusterObjectDeployment impeded-rollout Progressing reasons @ProgressDeadline Scenario: A hard stall is reported instead of the progress deadline even after it elapses Given namespace "${TEST_NAMESPACE}" is available - And min value for ClusterObjectDeployment .spec.template.spec.progressDeadlineMinutes is set to 1 And resource is applied """ apiVersion: v1 diff --git a/test/e2e/features/cos_revision_identity.feature b/test/e2e/features/cos_revision_identity.feature index 563f0b6750..e5201c7b13 100644 --- a/test/e2e/features/cos_revision_identity.feature +++ b/test/e2e/features/cos_revision_identity.feature @@ -20,6 +20,17 @@ Feature: Validate ClusterObjectSet revision identity revision: 1 lifecycleState: Active collisionProtection: Prevent + phases: + - name: resources + objects: + - object: + apiVersion: v1 + kind: ConfigMap + metadata: + name: cm + namespace: ${TEST_NAMESPACE} + data: + k: v """ Then resource "clusterobjectset/" is eventually not found @@ -41,6 +52,17 @@ Feature: Validate ClusterObjectSet revision identity revision: 1 lifecycleState: Active collisionProtection: Prevent + phases: + - name: resources + objects: + - object: + apiVersion: v1 + kind: ConfigMap + metadata: + name: cm + namespace: ${TEST_NAMESPACE} + data: + k: v """ And resource "clusterobjectset/${NAME}-1" UID is remembered # The existing COS is Active. Request Archived in the duplicate so that an @@ -57,6 +79,17 @@ Feature: Validate ClusterObjectSet revision identity revision: 1 lifecycleState: Archived collisionProtection: Prevent + phases: + - name: resources + objects: + - object: + apiVersion: v1 + kind: ConfigMap + metadata: + name: cm + namespace: ${TEST_NAMESPACE} + data: + k: v """ Then resource "clusterobjectset/${NAME}-1" keeps its UID And resource "clusterobjectset/${NAME}-1" matches @@ -65,4 +98,15 @@ Feature: Validate ClusterObjectSet revision identity group: ${NAME} revision: 1 lifecycleState: Active + phases: + - name: resources + objects: + - object: + apiVersion: v1 + kind: ConfigMap + metadata: + name: cm + namespace: ${TEST_NAMESPACE} + data: + k: v """ diff --git a/test/e2e/features/group.feature b/test/e2e/features/group.feature index 143cb33eb9..7a40517ef0 100644 --- a/test/e2e/features/group.feature +++ b/test/e2e/features/group.feature @@ -185,7 +185,18 @@ Feature: Track ClusterObjectSet revisions by group group: ${NAME}-other revision: 1 lifecycleState: Archived - collisionProtection: Prevent + collisionProtection: Prevent + phases: + - name: resources + objects: + - object: + apiVersion: v1 + kind: ConfigMap + metadata: + name: cm + namespace: ${TEST_NAMESPACE} + data: + k: v """ And ClusterExtension is applied """ diff --git a/test/e2e/features/install.feature b/test/e2e/features/install.feature index da346e1d09..5af8baee76 100644 --- a/test/e2e/features/install.feature +++ b/test/e2e/features/install.feature @@ -350,10 +350,10 @@ Feature: Install ClusterExtension Given a catalog "test" with packages: | package | version | channel | replaces | contents | | test | 1.0.2 | alpha | | BadImage | - And min value for ClusterExtension .spec.progressDeadlineMinutes is set to 1 # The applier routes progressDeadlineMinutes through the ClusterObjectDeployment template; the # deployment enforces the rollout deadline itself, so relax its CRD minimum to accept the value of 1. - And min value for ClusterObjectDeployment .spec.template.spec.progressDeadlineMinutes is set to 1 + # TODO: We should also permanently relax the ClusterExtension's field to allow a minimum of 1 + And min value for ClusterExtension .spec.progressDeadlineMinutes is set to 1 When ClusterExtension is applied """ apiVersion: olm.operatorframework.io/v1 @@ -387,10 +387,9 @@ Feature: Install ClusterExtension Given a catalog "test" with packages: | package | version | channel | replaces | contents | | test | 1.0.3 | alpha | | BadImage | - And min value for ClusterExtension .spec.progressDeadlineMinutes is set to 1 # The applier routes progressDeadlineMinutes through the ClusterObjectDeployment template; the # deployment enforces the rollout deadline itself, so relax its CRD minimum to accept the value of 1. - And min value for ClusterObjectDeployment .spec.template.spec.progressDeadlineMinutes is set to 1 + And min value for ClusterExtension .spec.progressDeadlineMinutes is set to 1 When ClusterExtension is applied """ apiVersion: olm.operatorframework.io/v1