mirror of
https://github.com/kubernetes/kubernetes.git
synced 2026-08-05 13:10:38 +00:00
Add declarative validation for core Volume Health API changes
This commit is contained in:
@@ -2686,57 +2686,6 @@ var resizeStatusSet = sets.New(core.PersistentVolumeClaimControllerResizeInProgr
|
||||
core.PersistentVolumeClaimNodeResizeInProgress,
|
||||
core.PersistentVolumeClaimNodeResizeInfeasible)
|
||||
|
||||
var validVolumeHealthStatusTypes = sets.New(
|
||||
core.VolumeHealthInaccessible,
|
||||
core.VolumeHealthDataLoss,
|
||||
core.VolumeHealthDegraded,
|
||||
)
|
||||
|
||||
const maxVolumeHealthConditions = 16
|
||||
|
||||
func validateVolumeHealthCondition(condition core.VolumeHealthCondition, fldPath *field.Path) field.ErrorList {
|
||||
allErrs := field.ErrorList{}
|
||||
if !validVolumeHealthStatusTypes.Has(condition.Status) {
|
||||
allErrs = append(allErrs, field.NotSupported(fldPath.Child("status"), condition.Status, sets.List(validVolumeHealthStatusTypes)))
|
||||
}
|
||||
if len(condition.Reason) == 0 {
|
||||
allErrs = append(allErrs, field.Required(fldPath.Child("reason"), ""))
|
||||
} else if len(condition.Reason) > 256 {
|
||||
allErrs = append(allErrs, field.TooLong(fldPath.Child("reason"), condition.Reason, 256))
|
||||
}
|
||||
if len(condition.Message) > 1024 {
|
||||
allErrs = append(allErrs, field.TooLong(fldPath.Child("message"), condition.Message, 1024))
|
||||
}
|
||||
return allErrs
|
||||
}
|
||||
|
||||
func validateVolumeHealthConditions(conditions []core.VolumeHealthCondition, fldPath *field.Path) field.ErrorList {
|
||||
allErrs := field.ErrorList{}
|
||||
if len(conditions) > maxVolumeHealthConditions {
|
||||
allErrs = append(allErrs, field.TooMany(fldPath, len(conditions), maxVolumeHealthConditions))
|
||||
}
|
||||
seen := sets.New[string]()
|
||||
for i, condition := range conditions {
|
||||
key := string(condition.Status) + "/" + condition.Reason
|
||||
idxPath := fldPath.Index(i)
|
||||
if seen.Has(key) {
|
||||
allErrs = append(allErrs, field.Duplicate(idxPath, key))
|
||||
}
|
||||
seen.Insert(key)
|
||||
allErrs = append(allErrs, validateVolumeHealthCondition(condition, idxPath)...)
|
||||
}
|
||||
return allErrs
|
||||
}
|
||||
|
||||
func validateVolumeHealthStatus(healthStatus *core.VolumeHealthStatus, fldPath *field.Path) field.ErrorList {
|
||||
allErrs := field.ErrorList{}
|
||||
if healthStatus == nil {
|
||||
return allErrs
|
||||
}
|
||||
allErrs = append(allErrs, validateVolumeHealthConditions(healthStatus.HealthConditions, fldPath.Child("healthConditions"))...)
|
||||
return allErrs
|
||||
}
|
||||
|
||||
func validatePodVolumeHealth(volumeHealth []core.PodVolumeHealth, spec *core.PodSpec, fldPath *field.Path) field.ErrorList {
|
||||
allErrs := field.ErrorList{}
|
||||
volumeNames := sets.New[string]()
|
||||
@@ -2745,20 +2694,11 @@ func validatePodVolumeHealth(volumeHealth []core.PodVolumeHealth, spec *core.Pod
|
||||
volumeNames.Insert(v.Name)
|
||||
}
|
||||
}
|
||||
seenNames := sets.New[string]()
|
||||
for i, vh := range volumeHealth {
|
||||
idxPath := fldPath.Index(i)
|
||||
if len(vh.Name) == 0 {
|
||||
allErrs = append(allErrs, field.Required(idxPath.Child("name"), ""))
|
||||
}
|
||||
if seenNames.Has(vh.Name) {
|
||||
allErrs = append(allErrs, field.Duplicate(idxPath.Child("name"), vh.Name))
|
||||
}
|
||||
seenNames.Insert(vh.Name)
|
||||
if len(vh.Name) > 0 && !volumeNames.Has(vh.Name) {
|
||||
allErrs = append(allErrs, field.NotFound(idxPath.Child("name"), vh.Name))
|
||||
}
|
||||
allErrs = append(allErrs, validateVolumeHealthConditions(vh.HealthConditions, idxPath.Child("healthConditions"))...)
|
||||
}
|
||||
return allErrs
|
||||
}
|
||||
@@ -2805,7 +2745,6 @@ func ValidatePersistentVolumeClaimStatusUpdate(newPvc, oldPvc *core.PersistentVo
|
||||
}
|
||||
}
|
||||
}
|
||||
allErrs = append(allErrs, validateVolumeHealthStatus(newPvc.Status.HealthStatus, field.NewPath("status", "healthStatus"))...)
|
||||
return allErrs
|
||||
}
|
||||
|
||||
|
||||
@@ -22805,137 +22805,6 @@ func TestValidatePersistentVolumeClaimStatusUpdate(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func makeVolumeHealthConditions(n int) []core.VolumeHealthCondition {
|
||||
conditions := make([]core.VolumeHealthCondition, n)
|
||||
for i := range conditions {
|
||||
conditions[i] = core.VolumeHealthCondition{
|
||||
Status: core.VolumeHealthDegraded,
|
||||
Reason: fmt.Sprintf("Reason%d", i),
|
||||
}
|
||||
}
|
||||
return conditions
|
||||
}
|
||||
|
||||
func TestValidateVolumeHealthStatus(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
healthStatus *core.VolumeHealthStatus
|
||||
isErr bool
|
||||
expectedErr string
|
||||
}{
|
||||
{
|
||||
name: "valid health status",
|
||||
healthStatus: &core.VolumeHealthStatus{
|
||||
HealthConditions: []core.VolumeHealthCondition{
|
||||
{Status: core.VolumeHealthDegraded, Reason: "DiskSlow"},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "valid health status with multiple conditions",
|
||||
healthStatus: &core.VolumeHealthStatus{
|
||||
HealthConditions: []core.VolumeHealthCondition{
|
||||
{Status: core.VolumeHealthDegraded, Reason: "DiskSlow"},
|
||||
{Status: core.VolumeHealthInaccessible, Reason: "VolumeNotFound"},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "nil health status is valid",
|
||||
healthStatus: nil,
|
||||
},
|
||||
{
|
||||
name: "invalid status type",
|
||||
healthStatus: &core.VolumeHealthStatus{
|
||||
HealthConditions: []core.VolumeHealthCondition{
|
||||
{Status: "InvalidType", Reason: "SomeReason"},
|
||||
},
|
||||
},
|
||||
isErr: true,
|
||||
expectedErr: "healthConditions[0].status",
|
||||
},
|
||||
{
|
||||
name: "empty reason",
|
||||
healthStatus: &core.VolumeHealthStatus{
|
||||
HealthConditions: []core.VolumeHealthCondition{
|
||||
{Status: core.VolumeHealthDegraded, Reason: ""},
|
||||
},
|
||||
},
|
||||
isErr: true,
|
||||
expectedErr: "healthConditions[0].reason",
|
||||
},
|
||||
{
|
||||
name: "duplicate conditions",
|
||||
healthStatus: &core.VolumeHealthStatus{
|
||||
HealthConditions: []core.VolumeHealthCondition{
|
||||
{Status: core.VolumeHealthDegraded, Reason: "DiskSlow"},
|
||||
{Status: core.VolumeHealthDegraded, Reason: "DiskSlow"},
|
||||
},
|
||||
},
|
||||
isErr: true,
|
||||
expectedErr: "healthConditions[1]",
|
||||
},
|
||||
{
|
||||
name: "reason too long",
|
||||
healthStatus: &core.VolumeHealthStatus{
|
||||
HealthConditions: []core.VolumeHealthCondition{
|
||||
{Status: core.VolumeHealthDegraded, Reason: strings.Repeat("a", 257)},
|
||||
},
|
||||
},
|
||||
isErr: true,
|
||||
expectedErr: "healthConditions[0].reason",
|
||||
},
|
||||
{
|
||||
name: "message too long",
|
||||
healthStatus: &core.VolumeHealthStatus{
|
||||
HealthConditions: []core.VolumeHealthCondition{
|
||||
{Status: core.VolumeHealthDegraded, Reason: "DiskSlow", Message: strings.Repeat("a", 1025)},
|
||||
},
|
||||
},
|
||||
isErr: true,
|
||||
expectedErr: "healthConditions[0].message",
|
||||
},
|
||||
{
|
||||
name: "exactly max health conditions is valid",
|
||||
healthStatus: &core.VolumeHealthStatus{
|
||||
HealthConditions: makeVolumeHealthConditions(maxVolumeHealthConditions),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "too many health conditions",
|
||||
healthStatus: &core.VolumeHealthStatus{
|
||||
HealthConditions: makeVolumeHealthConditions(maxVolumeHealthConditions + 1),
|
||||
},
|
||||
isErr: true,
|
||||
expectedErr: "healthConditions",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
errs := validateVolumeHealthStatus(tt.healthStatus, field.NewPath("healthStatus"))
|
||||
if tt.isErr && len(errs) == 0 {
|
||||
t.Errorf("expected error but got none")
|
||||
}
|
||||
if !tt.isErr && len(errs) > 0 {
|
||||
t.Errorf("unexpected errors: %v", errs)
|
||||
}
|
||||
if tt.isErr && len(errs) > 0 && tt.expectedErr != "" {
|
||||
found := false
|
||||
for _, err := range errs {
|
||||
if strings.Contains(err.Field, tt.expectedErr) {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Errorf("expected error containing %q but got: %v", tt.expectedErr, errs)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidatePodVolumeHealth(t *testing.T) {
|
||||
podSpec := &core.PodSpec{
|
||||
Volumes: []core.Volume{
|
||||
@@ -22973,56 +22842,6 @@ func TestValidatePodVolumeHealth(t *testing.T) {
|
||||
isErr: true,
|
||||
expectedErr: "volumeHealth[0].name",
|
||||
},
|
||||
{
|
||||
name: "duplicate volume names",
|
||||
volumeHealth: []core.PodVolumeHealth{
|
||||
{Name: "vol1", HealthConditions: []core.VolumeHealthCondition{{Status: core.VolumeHealthDegraded, Reason: "DiskSlow"}}},
|
||||
{Name: "vol1", HealthConditions: []core.VolumeHealthCondition{{Status: core.VolumeHealthInaccessible, Reason: "NotFound"}}},
|
||||
},
|
||||
isErr: true,
|
||||
expectedErr: "volumeHealth[1].name",
|
||||
},
|
||||
{
|
||||
name: "empty volume name",
|
||||
volumeHealth: []core.PodVolumeHealth{
|
||||
{Name: "", HealthConditions: []core.VolumeHealthCondition{{Status: core.VolumeHealthDegraded, Reason: "DiskSlow"}}},
|
||||
},
|
||||
isErr: true,
|
||||
expectedErr: "volumeHealth[0].name",
|
||||
},
|
||||
{
|
||||
name: "invalid condition status in pod volume health",
|
||||
volumeHealth: []core.PodVolumeHealth{
|
||||
{
|
||||
Name: "vol1",
|
||||
HealthConditions: []core.VolumeHealthCondition{
|
||||
{Status: "BadStatus", Reason: "SomeReason"},
|
||||
},
|
||||
},
|
||||
},
|
||||
isErr: true,
|
||||
expectedErr: "volumeHealth[0].healthConditions[0].status",
|
||||
},
|
||||
{
|
||||
name: "exactly max health conditions is valid",
|
||||
volumeHealth: []core.PodVolumeHealth{
|
||||
{
|
||||
Name: "vol1",
|
||||
HealthConditions: makeVolumeHealthConditions(maxVolumeHealthConditions),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "too many health conditions",
|
||||
volumeHealth: []core.PodVolumeHealth{
|
||||
{
|
||||
Name: "vol1",
|
||||
HealthConditions: makeVolumeHealthConditions(maxVolumeHealthConditions + 1),
|
||||
},
|
||||
},
|
||||
isErr: true,
|
||||
expectedErr: "volumeHealth[0].healthConditions",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
|
||||
@@ -767,6 +767,7 @@ type PersistentVolumeClaimCondition struct {
|
||||
|
||||
// VolumeHealthStatusType describes the health status category of a volume.
|
||||
// +enum
|
||||
// +k8s:enum
|
||||
type VolumeHealthStatusType string
|
||||
|
||||
const (
|
||||
@@ -782,15 +783,21 @@ const (
|
||||
type VolumeHealthCondition struct {
|
||||
// status is the machine-parseable health category.
|
||||
// One of "Inaccessible", "DataLoss", "Degraded".
|
||||
// +required
|
||||
// +k8s:required
|
||||
Status VolumeHealthStatusType `json:"status" protobuf:"bytes,1,opt,name=status,casttype=VolumeHealthStatusType"`
|
||||
// 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
|
||||
Reason string `json:"reason" protobuf:"bytes,2,opt,name=reason"`
|
||||
// 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
|
||||
Message string `json:"message,omitempty" protobuf:"bytes,3,opt,name=message"`
|
||||
}
|
||||
|
||||
@@ -806,6 +813,11 @@ type VolumeHealthStatus struct {
|
||||
// +patchMergeKey=status
|
||||
// +patchStrategy=merge
|
||||
// +listMapKey=reason
|
||||
// +k8s:optional
|
||||
// +k8s:listType=map
|
||||
// +k8s:listMapKey=status
|
||||
// +k8s:listMapKey=reason
|
||||
// +k8s:maxItems=16
|
||||
HealthConditions []VolumeHealthCondition `json:"healthConditions,omitempty" patchStrategy:"merge" patchMergeKey:"status" protobuf:"bytes,1,rep,name=healthConditions"`
|
||||
// lastTransitionTime is when the current set of conditions first appeared.
|
||||
// +optional
|
||||
@@ -816,6 +828,8 @@ type VolumeHealthStatus struct {
|
||||
// reported by the CSI node plugin via the kubelet.
|
||||
type PodVolumeHealth struct {
|
||||
// name matches an entry in pod.spec.volumes.
|
||||
// +required
|
||||
// +k8s:required
|
||||
Name string `json:"name" protobuf:"bytes,1,opt,name=name"`
|
||||
// conditions is the set of adverse conditions reported by
|
||||
// the CSI node plugin for this volume on this node.
|
||||
@@ -826,6 +840,11 @@ type PodVolumeHealth struct {
|
||||
// +patchMergeKey=status
|
||||
// +patchStrategy=merge
|
||||
// +listMapKey=reason
|
||||
// +k8s:optional
|
||||
// +k8s:listType=map
|
||||
// +k8s:listMapKey=status
|
||||
// +k8s:listMapKey=reason
|
||||
// +k8s:maxItems=16
|
||||
HealthConditions []VolumeHealthCondition `json:"healthConditions,omitempty" patchStrategy:"merge" patchMergeKey:"status" protobuf:"bytes,2,rep,name=healthConditions"`
|
||||
// lastTransitionTime is when the current set of conditions first appeared.
|
||||
// +optional
|
||||
@@ -929,6 +948,7 @@ type PersistentVolumeClaimStatus struct {
|
||||
// for the volume bound to this claim.
|
||||
// +featureGate=CSIVolumeHealth
|
||||
// +optional
|
||||
// +k8s:optional
|
||||
HealthStatus *VolumeHealthStatus `json:"healthStatus,omitempty" protobuf:"bytes,10,opt,name=healthStatus"`
|
||||
}
|
||||
|
||||
@@ -5618,6 +5638,9 @@ type PodStatus struct {
|
||||
// +optional
|
||||
// +listType=map
|
||||
// +listMapKey=name
|
||||
// +k8s:optional
|
||||
// +k8s:listType=map
|
||||
// +k8s:listMapKey=name
|
||||
VolumeHealth []PodVolumeHealth `json:"volumeHealth,omitempty" protobuf:"bytes,22,rep,name=volumeHealth"`
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user