Change:
What happened: When sending a request to the server side apply endpoint (), if the apiVersion provided in the object is different from the apiVersion in the url, it returns a 500 error, like: What you expected to happen: The request should fail if the apiVersion provided in the object is different from the apiVersion in the url, but it should fail before trying to merge, and be a 4xx error. Probably a bad request error. Anything else we need to know?: The check should probably be done somewhere near this . The issue, though, is that the patch is given as a byte array, and needs to be deserialized into an object to access the apiVersion field. How to reproduce it (as minimally and precisely as possible): With a kube-apiserver built from head, running with :
/kind bug /sig api-machinery /wg apply /priority important-longterm /assign

Discussion:
importpath = "k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager", visibility = ["//visibility:public"], deps = [ <ins> "//staging/src/k8s.io/apimachinery/pkg/api/errors:go_default_library", </ins> "//staging/src/k8s.io/apimachinery/pkg/api/meta:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library", <ins> "//staging/src/k8s.io/apimachinery/pkg/apis/meta/v1/unstructured:go_default_library", </ins> "//staging/src/k8s.io/apimachinery/pkg/runtime:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/runtime/schema:go_default_library", "//staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/internal:go_default_library", "//vendor/k8s.io/kube-openapi/pkg/util/proto:go_default_library", "//vendor/sigs.k8s.io/structured-merge-diff/fieldpath:go_default_library", "//vendor/sigs.k8s.io/structured-merge-diff/merge:go_default_library", <ins> "//vendor/sigs.k8s.io/yaml:go_default_library", </ins> ], )

embed = [":go_default_library"], deps = [ "//staging/src/k8s.io/api/core/v1:go_default_library", <ins> "//staging/src/k8s.io/apimachinery/pkg/api/errors:go_default_library", </ins> "//staging/src/k8s.io/apimachinery/pkg/api/meta:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/runtime:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/runtime/schema:go_default_library",

"fmt" "time" <ins> "k8s.io/apimachinery/pkg/api/errors" </ins> "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" <ins> "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" </ins> "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/internal" openapiproto "k8s.io/kube-openapi/pkg/util/proto" "sigs.k8s.io/structured-merge-diff/fieldpath" "sigs.k8s.io/structured-merge-diff/merge" <ins> "sigs.k8s.io/yaml" </ins> ) // FieldManager updates the managed fields and merge applied

if err != nil { return nil, fmt.Errorf("failed to decode managed fields: %v", err) } <del> // We can assume that patchObj is already on the proper version: // it shouldn't have to be converted so that it's not defaulted. // TODO (jennybuckley): Explicitly checkt that patchObj is in the proper version. </del> <ins> // Check that the patch object has the same version as the live object patchObj := &unstructured.Unstructured{Object: map[string]interface{}{}} if err := yaml.Unmarshal(patch, &patchObj.Object); err != nil { return nil, fmt.Errorf("error decoding YAML: %v", err) } if patchObj.GetAPIVersion() != f.groupVersion.String() { return nil, errors.NewBadRequest( fmt.Sprintf("Incorrect version specified in apply patch. "+ "Specified patch version: %s, expected: %s", patchObj.GetAPIVersion(), f.groupVersion.String())) } </ins> liveObjVersioned, err := f.toVersioned(liveObj) if err != nil { return nil, fmt.Errorf("failed to convert live object to proper version: %v", err)

import ( "errors" <ins> "net/http" </ins> "testing" corev1 "k8s.io/api/core/v1" <ins> apierrors "k8s.io/apimachinery/pkg/api/errors" </ins> "k8s.io/apimachinery/pkg/api/meta" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema"

obj := &corev1.Pod{} newObj, err := f.Apply(obj, []byte(`{ <del> "apiVersion": "v1", </del> <ins> "apiVersion": "apps/v1", </ins> "kind": "Deployment", "metadata": { "name": "b",

"managedFields": [{ "manager": "apply", "operation": "Apply", <del> "apiVersion": "v1", </del> <ins> "apiVersion": "apps/v1", </ins> "fields": { "f:metadata": { "f:labels": {

} } <ins> func TestVersionCheck(t *testing.T) { f := NewTestFieldManager(t) obj := &corev1.Pod{} // patch has 'apiVersion: apps/v1' and live version is apps/v1 -> no errors _, err := f.Apply(obj, []byte(`{ "apiVersion": "apps/v1", "kind": "Deployment", }`), "fieldmanager_test", false) if err != nil { t.Fatalf("failed to apply object: %v", err) } // patch has 'apiVersion: apps/v2' but live version is apps/v1 -> error _, err = f.Apply(obj, []byte(`{ "apiVersion": "apps/v2", "kind": "Deployment", }`), "fieldmanager_test", false) if err == nil { t.Fatalf("expected an error from mismatched patch and live versions") } switch typ := err.(type) { default: t.Fatalf("expected error to be of type %T was %T", apierrors.StatusError{}, typ) case apierrors.APIStatus: if typ.Status().Code != http.StatusBadRequest { t.Fatalf("expected status code to be %d but was %d", http.StatusBadRequest, typ.Status().Code) } } } </ins> func TestApplyDoesNotStripLabels(t *testing.T) { f := NewTestFieldManager(t) obj := &corev1.Pod{} newObj, err := f.Apply(obj, []byte(`{ <del> "apiVersion": "v1", </del> <ins> "apiVersion": "apps/v1", </ins> "kind": "Pod", "metadata": { "labels": {