From dbd670abfd244cc8780e66ce6a866352f81ad863 Mon Sep 17 00:00:00 2001 From: Hemant Kumar Date: Mon, 20 Jul 2026 20:46:50 -0400 Subject: [PATCH] Update generated files --- api/openapi-spec/swagger.json | 4 +- api/openapi-spec/v3/api__v1_openapi.json | 4 +- .../v3/apis__apps__v1_openapi.json | 4 +- pkg/apis/core/v1/zz_generated.validations.go | 424 +++++++++++++++++- pkg/generated/openapi/zz_generated.openapi.go | 4 +- .../src/k8s.io/api/core/v1/generated.proto | 26 +- .../core/v1/types_swagger_doc_generated.go | 4 +- .../core/v1/volumehealthcondition.go | 4 +- .../declarative_validation_test.go | 80 +++- .../zz_generated.validations.v1_test.go | 17 + .../core/pod/declarative_validation_test.go | 109 +++++ .../pod/zz_generated.validations.v1_test.go | 23 + 12 files changed, 686 insertions(+), 17 deletions(-) diff --git a/api/openapi-spec/swagger.json b/api/openapi-spec/swagger.json index ac79a71066e..d6f3cfae874 100644 --- a/api/openapi-spec/swagger.json +++ b/api/openapi-spec/swagger.json @@ -13068,11 +13068,11 @@ "description": "VolumeHealthCondition represents an adverse health condition reported for a volume.", "properties": { "message": { - "description": "message is a human-readable description. Maximum permitted length of a message is 1024 characters.", + "description": "message is a human-readable description. Maximum permitted length of a message is 1024 bytes.", "type": "string" }, "reason": { - "description": "reason is a brief CamelCase machine-parseable reason. Together with status it forms the unique identity of a condition entry. Maximum permitted length of a reason is 256 characters.a", + "description": "reason is a brief CamelCase machine-parseable reason. Together with status it forms the unique identity of a condition entry. Maximum permitted length of a reason is 256 bytes.", "type": "string" }, "status": { diff --git a/api/openapi-spec/v3/api__v1_openapi.json b/api/openapi-spec/v3/api__v1_openapi.json index 3c2fbbab92f..1b824cdc573 100644 --- a/api/openapi-spec/v3/api__v1_openapi.json +++ b/api/openapi-spec/v3/api__v1_openapi.json @@ -8340,12 +8340,12 @@ "description": "VolumeHealthCondition represents an adverse health condition reported for a volume.", "properties": { "message": { - "description": "message is a human-readable description. Maximum permitted length of a message is 1024 characters.", + "description": "message is a human-readable description. Maximum permitted length of a message is 1024 bytes.", "type": "string" }, "reason": { "default": "", - "description": "reason is a brief CamelCase machine-parseable reason. Together with status it forms the unique identity of a condition entry. Maximum permitted length of a reason is 256 characters.a", + "description": "reason is a brief CamelCase machine-parseable reason. Together with status it forms the unique identity of a condition entry. Maximum permitted length of a reason is 256 bytes.", "type": "string" }, "status": { diff --git a/api/openapi-spec/v3/apis__apps__v1_openapi.json b/api/openapi-spec/v3/apis__apps__v1_openapi.json index 9403a8d2734..a40c4462e19 100644 --- a/api/openapi-spec/v3/apis__apps__v1_openapi.json +++ b/api/openapi-spec/v3/apis__apps__v1_openapi.json @@ -5141,12 +5141,12 @@ "description": "VolumeHealthCondition represents an adverse health condition reported for a volume.", "properties": { "message": { - "description": "message is a human-readable description. Maximum permitted length of a message is 1024 characters.", + "description": "message is a human-readable description. Maximum permitted length of a message is 1024 bytes.", "type": "string" }, "reason": { "default": "", - "description": "reason is a brief CamelCase machine-parseable reason. Together with status it forms the unique identity of a condition entry. Maximum permitted length of a reason is 256 characters.a", + "description": "reason is a brief CamelCase machine-parseable reason. Together with status it forms the unique identity of a condition entry. Maximum permitted length of a reason is 256 bytes.", "type": "string" }, "status": { diff --git a/pkg/apis/core/v1/zz_generated.validations.go b/pkg/apis/core/v1/zz_generated.validations.go index be278cb43db..5a7231db86e 100644 --- a/pkg/apis/core/v1/zz_generated.validations.go +++ b/pkg/apis/core/v1/zz_generated.validations.go @@ -33,6 +33,7 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" validation "k8s.io/apimachinery/pkg/apis/meta/v1/validation" runtime "k8s.io/apimachinery/pkg/runtime" + sets "k8s.io/apimachinery/pkg/util/sets" field "k8s.io/apimachinery/pkg/util/validation/field" ) @@ -711,7 +712,77 @@ func Validate_PersistentVolumeClaim( } // field corev1.PersistentVolumeClaim.Spec has no validation - // field corev1.PersistentVolumeClaim.Status has no validation + + { // field corev1.PersistentVolumeClaim.Status + fn := func( + fldPath *field.Path, + obj, oldObj *corev1.PersistentVolumeClaimStatus, + oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update { + if equality.Semantic.DeepEqual(obj, oldObj) { + return nil + } + } + // call the type's validation function + errs = append(errs, Validate_PersistentVolumeClaimStatus(ctx, op, fldPath, obj, oldObj)...) + return + } + oldVal := safe.Field(oldObj, + func(oldObj *corev1.PersistentVolumeClaim) *corev1.PersistentVolumeClaimStatus { + return &oldObj.Status + }) + errs = append(errs, fn(fldPath.Child("status"), &obj.Status, oldVal, oldObj != nil)...) + } + + return errs +} + +// Validate_PersistentVolumeClaimStatus validates an instance of PersistentVolumeClaimStatus according +// to declarative validation rules in the API schema. +func Validate_PersistentVolumeClaimStatus( + ctx context.Context, op operation.Operation, fldPath *field.Path, + obj, oldObj *corev1.PersistentVolumeClaimStatus) (errs field.ErrorList) { + + // field corev1.PersistentVolumeClaimStatus.Phase has no validation + // field corev1.PersistentVolumeClaimStatus.AccessModes has no validation + // field corev1.PersistentVolumeClaimStatus.Capacity has no validation + // field corev1.PersistentVolumeClaimStatus.Conditions has no validation + // field corev1.PersistentVolumeClaimStatus.AllocatedResources has no validation + // field corev1.PersistentVolumeClaimStatus.AllocatedResourceStatuses has no validation + // field corev1.PersistentVolumeClaimStatus.CurrentVolumeAttributesClassName has no validation + // field corev1.PersistentVolumeClaimStatus.ModifyVolumeStatus has no validation + + { // field corev1.PersistentVolumeClaimStatus.HealthStatus + fn := func( + fldPath *field.Path, + obj, oldObj *corev1.VolumeHealthStatus, + oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update { + if equality.Semantic.DeepEqual(obj, oldObj) { + return nil + } + } + // call field-attached validations + earlyReturn := false + if e := validate.OptionalPointer(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + // call the type's validation function + errs = append(errs, Validate_VolumeHealthStatus(ctx, op, fldPath, obj, oldObj)...) + return + } + oldVal := safe.Field(oldObj, + func(oldObj *corev1.PersistentVolumeClaimStatus) *corev1.VolumeHealthStatus { + return oldObj.HealthStatus + }) + errs = append(errs, fn(fldPath.Child("healthStatus"), obj.HealthStatus, oldVal, oldObj != nil)...) + } + return errs } @@ -767,7 +838,28 @@ func Validate_Pod( errs = append(errs, fn(fldPath.Child("spec"), &obj.Spec, oldVal, oldObj != nil)...) } - // field corev1.Pod.Status has no validation + { // field corev1.Pod.Status + fn := func( + fldPath *field.Path, + obj, oldObj *corev1.PodStatus, + oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update { + if equality.Semantic.DeepEqual(obj, oldObj) { + return nil + } + } + // call the type's validation function + errs = append(errs, Validate_PodStatus(ctx, op, fldPath, obj, oldObj)...) + return + } + oldVal := safe.Field(oldObj, + func(oldObj *corev1.Pod) *corev1.PodStatus { + return &oldObj.Status + }) + errs = append(errs, fn(fldPath.Child("status"), &obj.Status, oldVal, oldObj != nil)...) + } + return errs } @@ -854,6 +946,75 @@ func Validate_PodSpec( return errs } +// Validate_PodStatus validates an instance of PodStatus according +// to declarative validation rules in the API schema. +func Validate_PodStatus( + ctx context.Context, op operation.Operation, fldPath *field.Path, + obj, oldObj *corev1.PodStatus) (errs field.ErrorList) { + + // field corev1.PodStatus.ObservedGeneration has no validation + // field corev1.PodStatus.Phase has no validation + // field corev1.PodStatus.Conditions has no validation + // field corev1.PodStatus.Message has no validation + // field corev1.PodStatus.Reason has no validation + // field corev1.PodStatus.NominatedNodeName has no validation + // field corev1.PodStatus.HostIP has no validation + // field corev1.PodStatus.HostIPs has no validation + // field corev1.PodStatus.PodIP has no validation + // field corev1.PodStatus.PodIPs has no validation + // field corev1.PodStatus.StartTime has no validation + // field corev1.PodStatus.InitContainerStatuses has no validation + // field corev1.PodStatus.ContainerStatuses has no validation + // field corev1.PodStatus.QOSClass has no validation + // field corev1.PodStatus.EphemeralContainerStatuses has no validation + // field corev1.PodStatus.Resize has no validation + // field corev1.PodStatus.ResourceClaimStatuses has no validation + // field corev1.PodStatus.ExtendedResourceClaimStatus has no validation + // field corev1.PodStatus.AllocatedResources has no validation + // field corev1.PodStatus.Resources has no validation + // field corev1.PodStatus.NodeAllocatableResourceClaimStatuses has no validation + + { // field corev1.PodStatus.VolumeHealth + fn := func( + fldPath *field.Path, + obj, oldObj []corev1.PodVolumeHealth, + oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update { + if equality.Semantic.DeepEqual(obj, oldObj) { + return nil + } + } + // call field-attached validations + earlyReturn := false + if e := validate.OptionalSlice(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + // lists with map semantics require unique keys + if e := validate.ValSliceUnique(ctx, op, fldPath, obj, oldObj, + func(a *corev1.PodVolumeHealth, b *corev1.PodVolumeHealth) bool { return a.Name == b.Name }); len(e) != 0 { + errs = append(errs, e...) + } + // iterate the list and call the type's validation function + if e := validate.EachValSliceVal(ctx, op, fldPath, obj, oldObj, + func(a *corev1.PodVolumeHealth, b *corev1.PodVolumeHealth) bool { return a.Name == b.Name }, validate.SemanticDeepEqual, Validate_PodVolumeHealth); len(e) != 0 { + errs = append(errs, e...) + } + return + } + oldVal := safe.Field(oldObj, + func(oldObj *corev1.PodStatus) []corev1.PodVolumeHealth { + return oldObj.VolumeHealth + }) + errs = append(errs, fn(fldPath.Child("volumeHealth"), obj.VolumeHealth, oldVal, oldObj != nil)...) + } + + return errs +} + // Validate_PodTemplate validates an instance of PodTemplate according // to declarative validation rules in the API schema. func Validate_PodTemplate( @@ -942,6 +1103,91 @@ func Validate_PodTemplateSpec( return errs } +// Validate_PodVolumeHealth validates an instance of PodVolumeHealth according +// to declarative validation rules in the API schema. +func Validate_PodVolumeHealth( + ctx context.Context, op operation.Operation, fldPath *field.Path, + obj, oldObj *corev1.PodVolumeHealth) (errs field.ErrorList) { + + { // field corev1.PodVolumeHealth.Name + fn := func( + fldPath *field.Path, + obj, oldObj *string, + oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update { + if obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj) { + return nil + } + } + // call field-attached validations + earlyReturn := false + if e := validate.RequiredValue(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + errs = append(errs, e...) + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + return + } + oldVal := safe.Field(oldObj, + func(oldObj *corev1.PodVolumeHealth) *string { + return &oldObj.Name + }) + errs = append(errs, fn(fldPath.Child("name"), &obj.Name, oldVal, oldObj != nil)...) + } + + { // field corev1.PodVolumeHealth.HealthConditions + fn := func( + fldPath *field.Path, + obj, oldObj []corev1.VolumeHealthCondition, + oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update { + if equality.Semantic.DeepEqual(obj, oldObj) { + return nil + } + } + // call field-attached validations + earlyReturn := false + if e := validate.MaxItems(ctx, op, fldPath, obj, oldObj, 16).MarkShortCircuit(); len(e) != 0 { + errs = append(errs, e...) + earlyReturn = true + } + if e := validate.OptionalSlice(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + // lists with map semantics require unique keys + if e := validate.ValSliceUnique(ctx, op, fldPath, obj, oldObj, + func(a *corev1.VolumeHealthCondition, b *corev1.VolumeHealthCondition) bool { + return a.Status == b.Status && a.Reason == b.Reason + }); len(e) != 0 { + errs = append(errs, e...) + } + // iterate the list and call the type's validation function + if e := validate.EachValSliceVal(ctx, op, fldPath, obj, oldObj, + func(a *corev1.VolumeHealthCondition, b *corev1.VolumeHealthCondition) bool { + return a.Status == b.Status && a.Reason == b.Reason + }, validate.DirectEqual, Validate_VolumeHealthCondition); len(e) != 0 { + errs = append(errs, e...) + } + return + } + oldVal := safe.Field(oldObj, + func(oldObj *corev1.PodVolumeHealth) []corev1.VolumeHealthCondition { + return oldObj.HealthConditions + }) + errs = append(errs, fn(fldPath.Child("healthConditions"), obj.HealthConditions, oldVal, oldObj != nil)...) + } + + // field corev1.PodVolumeHealth.LastTransitionTime has no validation + return errs +} + // Validate_ReplicationController validates an instance of ReplicationController according // to declarative validation rules in the API schema. func Validate_ReplicationController( @@ -1330,3 +1576,177 @@ func Validate_Toleration( // field corev1.Toleration.TolerationSeconds has no validation return errs } + +// Validate_VolumeHealthCondition validates an instance of VolumeHealthCondition according +// to declarative validation rules in the API schema. +func Validate_VolumeHealthCondition( + ctx context.Context, op operation.Operation, fldPath *field.Path, + obj, oldObj *corev1.VolumeHealthCondition) (errs field.ErrorList) { + + { // field corev1.VolumeHealthCondition.Status + fn := func( + fldPath *field.Path, + obj, oldObj *corev1.VolumeHealthStatusType, + oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update { + if obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj) { + return nil + } + } + // call field-attached validations + earlyReturn := false + if e := validate.RequiredValue(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + errs = append(errs, e...) + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + // call the type's validation function + errs = append(errs, Validate_VolumeHealthStatusType(ctx, op, fldPath, obj, oldObj)...) + return + } + oldVal := safe.Field(oldObj, + func(oldObj *corev1.VolumeHealthCondition) *corev1.VolumeHealthStatusType { + return &oldObj.Status + }) + errs = append(errs, fn(fldPath.Child("status"), &obj.Status, oldVal, oldObj != nil)...) + } + + { // field corev1.VolumeHealthCondition.Reason + fn := func( + fldPath *field.Path, + obj, oldObj *string, + oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update { + if obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj) { + return nil + } + } + // call field-attached validations + earlyReturn := false + if e := validate.RequiredValue(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + errs = append(errs, e...) + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + if e := validate.MaxBytes(ctx, op, fldPath, obj, oldObj, 256); len(e) != 0 { + errs = append(errs, e...) + } + return + } + oldVal := safe.Field(oldObj, + func(oldObj *corev1.VolumeHealthCondition) *string { + return &oldObj.Reason + }) + errs = append(errs, fn(fldPath.Child("reason"), &obj.Reason, oldVal, oldObj != nil)...) + } + + { // field corev1.VolumeHealthCondition.Message + fn := func( + fldPath *field.Path, + obj, oldObj *string, + oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update { + if obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj) { + return nil + } + } + // call field-attached validations + earlyReturn := false + if e := validate.OptionalValue(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + if e := validate.MaxBytes(ctx, op, fldPath, obj, oldObj, 1024); len(e) != 0 { + errs = append(errs, e...) + } + return + } + oldVal := safe.Field(oldObj, + func(oldObj *corev1.VolumeHealthCondition) *string { + return &oldObj.Message + }) + errs = append(errs, fn(fldPath.Child("message"), &obj.Message, oldVal, oldObj != nil)...) + } + + return errs +} + +// Validate_VolumeHealthStatus validates an instance of VolumeHealthStatus according +// to declarative validation rules in the API schema. +func Validate_VolumeHealthStatus( + ctx context.Context, op operation.Operation, fldPath *field.Path, + obj, oldObj *corev1.VolumeHealthStatus) (errs field.ErrorList) { + + { // field corev1.VolumeHealthStatus.HealthConditions + fn := func( + fldPath *field.Path, + obj, oldObj []corev1.VolumeHealthCondition, + oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update { + if equality.Semantic.DeepEqual(obj, oldObj) { + return nil + } + } + // call field-attached validations + earlyReturn := false + if e := validate.MaxItems(ctx, op, fldPath, obj, oldObj, 16).MarkShortCircuit(); len(e) != 0 { + errs = append(errs, e...) + earlyReturn = true + } + if e := validate.OptionalSlice(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + // lists with map semantics require unique keys + if e := validate.ValSliceUnique(ctx, op, fldPath, obj, oldObj, + func(a *corev1.VolumeHealthCondition, b *corev1.VolumeHealthCondition) bool { + return a.Status == b.Status && a.Reason == b.Reason + }); len(e) != 0 { + errs = append(errs, e...) + } + // iterate the list and call the type's validation function + if e := validate.EachValSliceVal(ctx, op, fldPath, obj, oldObj, + func(a *corev1.VolumeHealthCondition, b *corev1.VolumeHealthCondition) bool { + return a.Status == b.Status && a.Reason == b.Reason + }, validate.DirectEqual, Validate_VolumeHealthCondition); len(e) != 0 { + errs = append(errs, e...) + } + return + } + oldVal := safe.Field(oldObj, + func(oldObj *corev1.VolumeHealthStatus) []corev1.VolumeHealthCondition { + return oldObj.HealthConditions + }) + errs = append(errs, fn(fldPath.Child("healthConditions"), obj.HealthConditions, oldVal, oldObj != nil)...) + } + + // field corev1.VolumeHealthStatus.LastTransitionTime has no validation + return errs +} + +var symbolsForVolumeHealthStatusType = sets.New(corev1.VolumeHealthDataLoss, corev1.VolumeHealthDegraded, corev1.VolumeHealthInaccessible) + +// Validate_VolumeHealthStatusType validates an instance of VolumeHealthStatusType according +// to declarative validation rules in the API schema. +func Validate_VolumeHealthStatusType( + ctx context.Context, op operation.Operation, fldPath *field.Path, + obj, oldObj *corev1.VolumeHealthStatusType) (errs field.ErrorList) { + + if e := validate.Enum(ctx, op, fldPath, obj, oldObj, symbolsForVolumeHealthStatusType, nil); len(e) != 0 { + errs = append(errs, e...) + } + + return errs +} diff --git a/pkg/generated/openapi/zz_generated.openapi.go b/pkg/generated/openapi/zz_generated.openapi.go index c8f4dbdac67..4d91adae670 100644 --- a/pkg/generated/openapi/zz_generated.openapi.go +++ b/pkg/generated/openapi/zz_generated.openapi.go @@ -33251,7 +33251,7 @@ func schema_k8sio_api_core_v1_VolumeHealthCondition(ref common.ReferenceCallback }, "reason": { SchemaProps: spec.SchemaProps{ - Description: "reason is a brief CamelCase machine-parseable reason. Together with status it forms the unique identity of a condition entry. Maximum permitted length of a reason is 256 characters.a", + Description: "reason is a brief CamelCase machine-parseable reason. Together with status it forms the unique identity of a condition entry. Maximum permitted length of a reason is 256 bytes.", Default: "", Type: []string{"string"}, Format: "", @@ -33259,7 +33259,7 @@ func schema_k8sio_api_core_v1_VolumeHealthCondition(ref common.ReferenceCallback }, "message": { SchemaProps: spec.SchemaProps{ - Description: "message is a human-readable description. Maximum permitted length of a message is 1024 characters.", + Description: "message is a human-readable description. Maximum permitted length of a message is 1024 bytes.", Type: []string{"string"}, Format: "", }, diff --git a/staging/src/k8s.io/api/core/v1/generated.proto b/staging/src/k8s.io/api/core/v1/generated.proto index 12c49228467..a48d0278f9c 100644 --- a/staging/src/k8s.io/api/core/v1/generated.proto +++ b/staging/src/k8s.io/api/core/v1/generated.proto @@ -3482,6 +3482,7 @@ message PersistentVolumeClaimStatus { // for the volume bound to this claim. // +featureGate=CSIVolumeHealth // +optional + // +k8s:optional optional VolumeHealthStatus healthStatus = 10; } @@ -5092,6 +5093,9 @@ message PodStatus { // +optional // +listType=map // +listMapKey=name + // +k8s:optional + // +k8s:listType=map + // +k8s:listMapKey=name repeated PodVolumeHealth volumeHealth = 22; } @@ -5137,6 +5141,8 @@ message PodTemplateSpec { // reported by the CSI node plugin via the kubelet. message PodVolumeHealth { // name matches an entry in pod.spec.volumes. + // +required + // +k8s:required optional string name = 1; // conditions is the set of adverse conditions reported by @@ -5148,6 +5154,11 @@ message PodVolumeHealth { // +patchMergeKey=status // +patchStrategy=merge // +listMapKey=reason + // +k8s:optional + // +k8s:listType=map + // +k8s:listMapKey=status + // +k8s:listMapKey=reason + // +k8s:maxItems=16 repeated VolumeHealthCondition healthConditions = 2; // lastTransitionTime is when the current set of conditions first appeared. @@ -7030,17 +7041,23 @@ message VolumeDevice { message VolumeHealthCondition { // status is the machine-parseable health category. // One of "Inaccessible", "DataLoss", "Degraded". + // +required + // +k8s:required optional string status = 1; // reason is a brief CamelCase machine-parseable reason. // Together with status it forms the unique identity of a condition entry. - // Maximum permitted length of a reason is 256 characters.a + // Maximum permitted length of a reason is 256 bytes. // +required + // +k8s:required + // +k8s:maxBytes=256 optional string reason = 2; // message is a human-readable description. + // Maximum permitted length of a message is 1024 bytes. // +optional - // Maximum permitted length of a message is 1024 characters. + // +k8s:optional + // +k8s:maxBytes=1024 optional string message = 3; } @@ -7056,6 +7073,11 @@ message VolumeHealthStatus { // +patchMergeKey=status // +patchStrategy=merge // +listMapKey=reason + // +k8s:optional + // +k8s:listType=map + // +k8s:listMapKey=status + // +k8s:listMapKey=reason + // +k8s:maxItems=16 repeated VolumeHealthCondition healthConditions = 1; // lastTransitionTime is when the current set of conditions first appeared. diff --git a/staging/src/k8s.io/api/core/v1/types_swagger_doc_generated.go b/staging/src/k8s.io/api/core/v1/types_swagger_doc_generated.go index 547b2198fa0..253d7991624 100644 --- a/staging/src/k8s.io/api/core/v1/types_swagger_doc_generated.go +++ b/staging/src/k8s.io/api/core/v1/types_swagger_doc_generated.go @@ -2818,8 +2818,8 @@ func (VolumeDevice) SwaggerDoc() map[string]string { var map_VolumeHealthCondition = map[string]string{ "": "VolumeHealthCondition represents an adverse health condition reported for a volume.", "status": "status is the machine-parseable health category. One of \"Inaccessible\", \"DataLoss\", \"Degraded\".", - "reason": "reason is a brief CamelCase machine-parseable reason. Together with status it forms the unique identity of a condition entry. Maximum permitted length of a reason is 256 characters.a", - "message": "message is a human-readable description. Maximum permitted length of a message is 1024 characters.", + "reason": "reason is a brief CamelCase machine-parseable reason. Together with status it forms the unique identity of a condition entry. Maximum permitted length of a reason is 256 bytes.", + "message": "message is a human-readable description. Maximum permitted length of a message is 1024 bytes.", } func (VolumeHealthCondition) SwaggerDoc() map[string]string { diff --git a/staging/src/k8s.io/client-go/applyconfigurations/core/v1/volumehealthcondition.go b/staging/src/k8s.io/client-go/applyconfigurations/core/v1/volumehealthcondition.go index 245504dd652..1d7bddac7bc 100644 --- a/staging/src/k8s.io/client-go/applyconfigurations/core/v1/volumehealthcondition.go +++ b/staging/src/k8s.io/client-go/applyconfigurations/core/v1/volumehealthcondition.go @@ -32,10 +32,10 @@ type VolumeHealthConditionApplyConfiguration struct { Status *corev1.VolumeHealthStatusType `json:"status,omitempty"` // reason is a brief CamelCase machine-parseable reason. // Together with status it forms the unique identity of a condition entry. - // Maximum permitted length of a reason is 256 characters.a + // Maximum permitted length of a reason is 256 bytes. Reason *string `json:"reason,omitempty"` // message is a human-readable description. - // Maximum permitted length of a message is 1024 characters. + // Maximum permitted length of a message is 1024 bytes. Message *string `json:"message,omitempty"` } diff --git a/test/declarative_validation/core/persistentvolumeclaim/declarative_validation_test.go b/test/declarative_validation/core/persistentvolumeclaim/declarative_validation_test.go index e3d84281e0d..715e25665ba 100644 --- a/test/declarative_validation/core/persistentvolumeclaim/declarative_validation_test.go +++ b/test/declarative_validation/core/persistentvolumeclaim/declarative_validation_test.go @@ -17,12 +17,14 @@ limitations under the License. package persistentvolumeclaim import ( + "strings" "testing" "k8s.io/apimachinery/pkg/api/resource" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/util/validation/field" genericapirequest "k8s.io/apiserver/pkg/endpoints/request" + apitesting "k8s.io/kubernetes/pkg/api/testing" core "k8s.io/kubernetes/pkg/apis/core" registry "k8s.io/kubernetes/pkg/registry/core/persistentvolumeclaim" "k8s.io/kubernetes/test/declarative_validation/meta" @@ -73,6 +75,82 @@ func testDeclarativeValidateUpdate(t *testing.T, apiVersion string) { meta.RunObjectMetaUpdateTestCases(t, ctx, &updateObj, registry.Strategy, meta.WithStringentFinalizerValidation()) } +func TestDeclarativeValidateStatusUpdate(t *testing.T) { + for _, apiVersion := range apiVersions { + ctx := genericapirequest.WithNamespace(genericapirequest.WithRequestInfo(genericapirequest.NewDefaultContext(), &genericapirequest.RequestInfo{ + APIPrefix: "api", + APIGroup: "", + APIVersion: apiVersion, + Resource: "persistentvolumeclaims", + Subresource: "status", + Name: "valid-obj", + IsResourceRequest: true, + Verb: "update", + }), metav1.NamespaceDefault) + + tests := map[string]struct { + conditions []core.VolumeHealthCondition + expectedErrs field.ErrorList + }{ + "valid": { + conditions: []core.VolumeHealthCondition{{Status: core.VolumeHealthDegraded, Reason: "DiskSlow"}}, + }, + "status required": { + conditions: []core.VolumeHealthCondition{{Reason: "DiskSlow"}}, + expectedErrs: field.ErrorList{field.Required(field.NewPath("status", "healthStatus", "healthConditions").Index(0).Child("status"), "")}, + }, + "status enum": { + conditions: []core.VolumeHealthCondition{{Status: "Invalid", Reason: "DiskSlow"}}, + expectedErrs: field.ErrorList{field.NotSupported(field.NewPath("status", "healthStatus", "healthConditions").Index(0).Child("status"), core.VolumeHealthStatusType("Invalid"), []core.VolumeHealthStatusType(nil))}, + }, + "reason required": { + conditions: []core.VolumeHealthCondition{{Status: core.VolumeHealthDegraded}}, + expectedErrs: field.ErrorList{field.Required(field.NewPath("status", "healthStatus", "healthConditions").Index(0).Child("reason"), "")}, + }, + "reason max bytes": { + conditions: []core.VolumeHealthCondition{{Status: core.VolumeHealthDegraded, Reason: strings.Repeat("𝄞", 65)}}, + expectedErrs: field.ErrorList{field.TooLong(field.NewPath("status", "healthStatus", "healthConditions").Index(0).Child("reason"), "", 256).WithOrigin("maxBytes")}, + }, + "message max bytes": { + conditions: []core.VolumeHealthCondition{{Status: core.VolumeHealthDegraded, Reason: "DiskSlow", Message: strings.Repeat("𝄞", 257)}}, + expectedErrs: field.ErrorList{field.TooLong(field.NewPath("status", "healthStatus", "healthConditions").Index(0).Child("message"), "", 1024).WithOrigin("maxBytes")}, + }, + "duplicate conditions": { + conditions: []core.VolumeHealthCondition{ + {Status: core.VolumeHealthDegraded, Reason: "DiskSlow"}, + {Status: core.VolumeHealthDegraded, Reason: "DiskSlow"}, + }, + expectedErrs: field.ErrorList{field.Duplicate(field.NewPath("status", "healthStatus", "healthConditions").Index(1), nil)}, + }, + "too many conditions": { + conditions: makeVolumeHealthConditions(17), + expectedErrs: field.ErrorList{field.TooMany(field.NewPath("status", "healthStatus", "healthConditions"), 17, 16).WithOrigin("maxItems")}, + }, + } + + for name, tc := range tests { + t.Run(apiVersion+"/"+name, func(t *testing.T) { + oldObj := mkValidPersistentVolumeClaim() + oldObj.ResourceVersion = "1" + updateObj := oldObj.DeepCopy() + updateObj.Status.HealthStatus = &core.VolumeHealthStatus{HealthConditions: tc.conditions} + apitesting.VerifyUpdateValidationEquivalence(t, ctx, updateObj, &oldObj, registry.StatusStrategy, tc.expectedErrs, apitesting.WithSubResources("status")) + }) + } + } +} + +func makeVolumeHealthConditions(count int) []core.VolumeHealthCondition { + conditions := make([]core.VolumeHealthCondition, count) + for i := range conditions { + conditions[i] = core.VolumeHealthCondition{ + Status: core.VolumeHealthDegraded, + Reason: "Reason" + string(rune('A'+i)), + } + } + return conditions +} + func mkValidPersistentVolumeClaim() core.PersistentVolumeClaim { return core.PersistentVolumeClaim{ ObjectMeta: metav1.ObjectMeta{ diff --git a/test/declarative_validation/core/persistentvolumeclaim/zz_generated.validations.v1_test.go b/test/declarative_validation/core/persistentvolumeclaim/zz_generated.validations.v1_test.go index 1e80cb79d3c..5aa884092d4 100644 --- a/test/declarative_validation/core/persistentvolumeclaim/zz_generated.validations.v1_test.go +++ b/test/declarative_validation/core/persistentvolumeclaim/zz_generated.validations.v1_test.go @@ -61,6 +61,23 @@ func init() { "metadata.uid": { {ErrorType: "FieldValueInvalid", Origin: "immutable"}, }, + "status.healthStatus.healthConditions": { + {ErrorType: "FieldValueTooMany", Origin: "maxItems"}, + }, + "status.healthStatus.healthConditions[*]": { + {ErrorType: "FieldValueDuplicate"}, + }, + "status.healthStatus.healthConditions[*].message": { + {ErrorType: "FieldValueTooLong", Origin: "maxBytes"}, + }, + "status.healthStatus.healthConditions[*].reason": { + {ErrorType: "FieldValueRequired"}, + {ErrorType: "FieldValueTooLong", Origin: "maxBytes"}, + }, + "status.healthStatus.healthConditions[*].status": { + {ErrorType: "FieldValueNotSupported"}, + {ErrorType: "FieldValueRequired"}, + }, }, ) } diff --git a/test/declarative_validation/core/pod/declarative_validation_test.go b/test/declarative_validation/core/pod/declarative_validation_test.go index fbf150897b0..a3008fc4623 100644 --- a/test/declarative_validation/core/pod/declarative_validation_test.go +++ b/test/declarative_validation/core/pod/declarative_validation_test.go @@ -17,6 +17,7 @@ limitations under the License. package pod import ( + "strings" "testing" "k8s.io/apimachinery/pkg/util/validation/field" @@ -92,3 +93,111 @@ func TestDeclarativeValidateUpdate(t *testing.T) { meta.RunObjectMetaUpdateTestCases(t, ctx, &updateObj, registry.Strategy, meta.WithStringentFinalizerValidation()) } } + +func TestDeclarativeValidateStatusUpdate(t *testing.T) { + for _, apiVersion := range apiVersions { + ctx := genericapirequest.WithRequestInfo(genericapirequest.NewDefaultContext(), &genericapirequest.RequestInfo{ + APIPrefix: "api", + APIGroup: "", + APIVersion: apiVersion, + Resource: "pods", + Subresource: "status", + Name: "foo", + IsResourceRequest: true, + Verb: "update", + }) + conditionPath := field.NewPath("status", "volumeHealth").Index(0).Child("healthConditions") + tests := map[string]struct { + volumeHealth []api.PodVolumeHealth + expectedErrs field.ErrorList + }{ + "valid": { + volumeHealth: []api.PodVolumeHealth{{ + Name: "vol", + HealthConditions: []api.VolumeHealthCondition{{Status: api.VolumeHealthDegraded, Reason: "DiskSlow"}}, + }}, + }, + "name required": { + volumeHealth: []api.PodVolumeHealth{{}}, + expectedErrs: field.ErrorList{ + field.Required(field.NewPath("status", "volumeHealth").Index(0).Child("name"), ""), + }, + }, + "duplicate volumes": { + volumeHealth: []api.PodVolumeHealth{{Name: "vol"}, {Name: "vol"}}, + expectedErrs: field.ErrorList{ + field.Duplicate(field.NewPath("status", "volumeHealth").Index(1), nil), + }, + }, + "status required": { + volumeHealth: []api.PodVolumeHealth{{Name: "vol", HealthConditions: []api.VolumeHealthCondition{{Reason: "DiskSlow"}}}}, + expectedErrs: field.ErrorList{ + field.Required(conditionPath.Index(0).Child("status"), ""), + }, + }, + "status enum": { + volumeHealth: []api.PodVolumeHealth{{Name: "vol", HealthConditions: []api.VolumeHealthCondition{{Status: "Invalid", Reason: "DiskSlow"}}}}, + expectedErrs: field.ErrorList{ + field.NotSupported(conditionPath.Index(0).Child("status"), api.VolumeHealthStatusType("Invalid"), []api.VolumeHealthStatusType(nil)), + }, + }, + "reason required": { + volumeHealth: []api.PodVolumeHealth{{Name: "vol", HealthConditions: []api.VolumeHealthCondition{{Status: api.VolumeHealthDegraded}}}}, + expectedErrs: field.ErrorList{ + field.Required(conditionPath.Index(0).Child("reason"), ""), + }, + }, + "reason max bytes": { + volumeHealth: []api.PodVolumeHealth{{Name: "vol", HealthConditions: []api.VolumeHealthCondition{{Status: api.VolumeHealthDegraded, Reason: strings.Repeat("𝄞", 65)}}}}, + expectedErrs: field.ErrorList{ + field.TooLong(conditionPath.Index(0).Child("reason"), "", 256).WithOrigin("maxBytes"), + }, + }, + "message max bytes": { + volumeHealth: []api.PodVolumeHealth{{Name: "vol", HealthConditions: []api.VolumeHealthCondition{{Status: api.VolumeHealthDegraded, Reason: "DiskSlow", Message: strings.Repeat("𝄞", 257)}}}}, + expectedErrs: field.ErrorList{ + field.TooLong(conditionPath.Index(0).Child("message"), "", 1024).WithOrigin("maxBytes"), + }, + }, + "duplicate conditions": { + volumeHealth: []api.PodVolumeHealth{{ + Name: "vol", + HealthConditions: []api.VolumeHealthCondition{ + {Status: api.VolumeHealthDegraded, Reason: "DiskSlow"}, + {Status: api.VolumeHealthDegraded, Reason: "DiskSlow"}, + }, + }}, + expectedErrs: field.ErrorList{ + field.Duplicate(conditionPath.Index(1), nil), + }, + }, + "too many conditions": { + volumeHealth: []api.PodVolumeHealth{{Name: "vol", HealthConditions: makePodVolumeHealthConditions(17)}}, + expectedErrs: field.ErrorList{ + field.TooMany(conditionPath, 17, 16).WithOrigin("maxItems"), + }, + }, + } + + for name, tc := range tests { + t.Run(apiVersion+"/"+name, func(t *testing.T) { + oldObj := podtest.MakePod("foo", podtest.SetVolumes(api.Volume{Name: "vol"})) + oldObj.ResourceVersion = "1" + updateObj := oldObj.DeepCopy() + updateObj.Status.VolumeHealth = tc.volumeHealth + apitesting.VerifyUpdateValidationEquivalence(t, ctx, updateObj, oldObj, registry.StatusStrategy, tc.expectedErrs, apitesting.WithSubResources("status")) + }) + } + } +} + +func makePodVolumeHealthConditions(count int) []api.VolumeHealthCondition { + conditions := make([]api.VolumeHealthCondition, count) + for i := range conditions { + conditions[i] = api.VolumeHealthCondition{ + Status: api.VolumeHealthDegraded, + Reason: "Reason" + string(rune('A'+i)), + } + } + return conditions +} diff --git a/test/declarative_validation/core/pod/zz_generated.validations.v1_test.go b/test/declarative_validation/core/pod/zz_generated.validations.v1_test.go index 62ff75aba58..bb53f7f85ab 100644 --- a/test/declarative_validation/core/pod/zz_generated.validations.v1_test.go +++ b/test/declarative_validation/core/pod/zz_generated.validations.v1_test.go @@ -64,6 +64,29 @@ func init() { "spec.tolerations[*].key": { {ErrorType: "FieldValueInvalid", Origin: "format=k8s-label-key"}, }, + "status.volumeHealth[*]": { + {ErrorType: "FieldValueDuplicate"}, + }, + "status.volumeHealth[*].healthConditions": { + {ErrorType: "FieldValueTooMany", Origin: "maxItems"}, + }, + "status.volumeHealth[*].healthConditions[*]": { + {ErrorType: "FieldValueDuplicate"}, + }, + "status.volumeHealth[*].healthConditions[*].message": { + {ErrorType: "FieldValueTooLong", Origin: "maxBytes"}, + }, + "status.volumeHealth[*].healthConditions[*].reason": { + {ErrorType: "FieldValueRequired"}, + {ErrorType: "FieldValueTooLong", Origin: "maxBytes"}, + }, + "status.volumeHealth[*].healthConditions[*].status": { + {ErrorType: "FieldValueNotSupported"}, + {ErrorType: "FieldValueRequired"}, + }, + "status.volumeHealth[*].name": { + {ErrorType: "FieldValueRequired"}, + }, }, ) }