Repository: kubernetes/kubernetes (language: go)

Issue:
QosClass of pod status shouldn't be changeable
### What happened?

According to this [code](https://github.com/kubernetes/kubernetes/blob/bbe8ca8b2ab14992389bc67e3bcfa209adcb13d4/pkg/apis/core/validation/validation.go#L4996), the QosClass of pod status is immutable. 

This [function ](https://github.com/kubernetes/kubernetes/blob/40b604e374144351eac463e7077fdb1903f5a033/pkg/apis/core/helper/qos/qos.go#L48
)also shows that QosClass is computed from the resources instead of being provided by users. 


However, we can use rest api or "kubectl edit pod <podName> --subresource='status'" to change it. 




### What did you expect to happen?

The request to change QoSClass of pod status should receive a validation error to indicate it's immutable. 

### How can we reproduce it (as minimally and precisely as possible)?

Use "kubectl edit pod <podName> --subresource='status'" to change the qosClass to a different value. It will succeed. 

### Anything else we need to know?

_No response_

### Kubernetes version

<details>

```console
$ kubectl version
Client Version: v1.29.4
Kustomize Version: v5.0.4-0.20230601165947-6ce0bf390ce3
Server Version: v1.29.4
```

</details>


### Cloud provider

<details>
Azure
</details>


### OS version

<details>

```console
# On Linux:
$ cat /etc/os-release
# paste output here
$ uname -a
# paste output here

# On Windows:
C:\> wmic os get Caption, Version, BuildNumber, OSArchitecture
# paste output here
```

</details>


### Install tools

<details>

</details>


### Container runtime (CRI) and version (if applicable)

<details>

</details>


### Related plugins (CNI, CSI, ...) and versions (if applicable)

<details>

</details>



Discussion / hints:
In fact, this value cannot be changed. It directly obtains the qos of the old pod and compares it with the calculated value of the new pod. It does not matter what value is changed, and the change will not succeed.

`edit` succeed, but get pod still the old status qos
/sig node

> In fact, this value cannot be changed. It directly obtains the qos of the old pod and compares it with the calculated value of the new pod. It does not matter what value is changed, and the change will not succeed.
> 
> `edit` succeed, but get pod still the old status qos

@chengjoey Not exactly. If you use "--subresource=status" in "kube edit po", it can change the qosClass. I verified that on 1.29.4.
/triage accepted
/assign
/unassign

I did not look into this.
/assign @kubernetes/api-reviewers 

My understanding is that this is a breaking change and is not backward compatible. So it will be hard to enforce this even if it is semantically correct. Perhaps some warning may be useful in this case.

Fix patch:
diff --git a/pkg/apis/core/validation/validation.go b/pkg/apis/core/validation/validation.go
index 01d12a036d386..488503db7f4d3 100644
--- a/pkg/apis/core/validation/validation.go
+++ b/pkg/apis/core/validation/validation.go
@@ -5373,6 +5373,9 @@ func ValidatePodStatusUpdate(newPod, oldPod *core.Pod, opts PodValidationOptions
 		}
 	}
 
+	// Pod QoS is immutable
+	allErrs = append(allErrs, ValidateImmutableField(newPod.Status.QOSClass, oldPod.Status.QOSClass, fldPath.Child("qosClass"))...)
+
 	// If pod should not restart, make sure the status update does not transition
 	// any terminated containers to a non-terminated state.
 	allErrs = append(allErrs, ValidateContainerStateTransition(newPod.Status.ContainerStatuses, oldPod.Status.ContainerStatuses, fldPath.Child("containerStatuses"), oldPod.Spec.RestartPolicy)...)
diff --git a/pkg/registry/core/pod/strategy.go b/pkg/registry/core/pod/strategy.go
index 3478fcc69b270..541fc43a2202d 100644
--- a/pkg/registry/core/pod/strategy.go
+++ b/pkg/registry/core/pod/strategy.go
@@ -226,6 +226,11 @@ func (podStatusStrategy) PrepareForUpdate(ctx context.Context, obj, old runtime.
 	// don't allow the pods/status endpoint to touch owner references since old kubelets corrupt them in a way
 	// that breaks garbage collection
 	newPod.OwnerReferences = oldPod.OwnerReferences
+	// the Pod QoS is immutable and populated at creation time by the kube-apiserver.
+	// we need to backfill it for backward compatibility because the old kubelet dropped this field when the pod was rejected.
+	if newPod.Status.QOSClass == "" {
+		newPod.Status.QOSClass = oldPod.Status.QOSClass
+	}
 }
 
 func (podStatusStrategy) ValidateUpdate(ctx context.Context, obj, old runtime.Object) field.ErrorList {


Test patch:
diff --git a/pkg/apis/core/validation/validation_test.go b/pkg/apis/core/validation/validation_test.go
index a4e9325e8d165..4a483c766de86 100644
--- a/pkg/apis/core/validation/validation_test.go
+++ b/pkg/apis/core/validation/validation_test.go
@@ -14454,6 +14454,32 @@ func TestValidatePodStatusUpdate(t *testing.T) {
 		),
 		"",
 		"restartable init container can restart if RestartPolicyAlways",
+	}, {
+		*podtest.MakePod("foo",
+			podtest.SetStatus(core.PodStatus{
+				QOSClass: core.PodQOSBurstable,
+			}),
+		),
+		*podtest.MakePod("foo",
+			podtest.SetStatus(core.PodStatus{
+				QOSClass: core.PodQOSGuaranteed,
+			}),
+		),
+		"tatus.qosClass: Invalid value: \"Burstable\": field is immutable",
+		"qosClass can not be changed",
+	}, {
+		*podtest.MakePod("foo",
+			podtest.SetStatus(core.PodStatus{
+				QOSClass: core.PodQOSBurstable,
+			}),
+		),
+		*podtest.MakePod("foo",
+			podtest.SetStatus(core.PodStatus{
+				QOSClass: core.PodQOSBurstable,
+			}),
+		),
+		"",
+		"qosClass no change",
 	},
 	}