diff --git a/staging/src/k8s.io/apiserver/pkg/endpoints/apiserver_test.go b/staging/src/k8s.io/apiserver/pkg/endpoints/apiserver_test.go index db9ca568bd0..001fed6b249 100644 --- a/staging/src/k8s.io/apiserver/pkg/endpoints/apiserver_test.go +++ b/staging/src/k8s.io/apiserver/pkg/endpoints/apiserver_test.go @@ -56,6 +56,7 @@ import ( "k8s.io/apimachinery/pkg/runtime/serializer/streaming" "k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/util/diff" + "k8s.io/apimachinery/pkg/util/net" utilruntime "k8s.io/apimachinery/pkg/util/runtime" "k8s.io/apimachinery/pkg/util/sets" "k8s.io/apimachinery/pkg/watch" @@ -214,6 +215,10 @@ type defaultAPIServer struct { container *restful.Container } +func handleWithWarnings(storage map[string]rest.Storage) http.Handler { + return genericapifilters.WithWarningRecorder(handle(storage)) +} + // uses the default settings func handle(storage map[string]rest.Storage) http.Handler { return handleInternal(storage, admissionControl, nil) @@ -3965,7 +3970,7 @@ func TestUpdateChecksAPIVersion(t *testing.T) { // runRequest is used by TestDryRun since it runs the test twice in a // row with a slightly different URL (one has ?dryRun, one doesn't). -func runRequest(t *testing.T, path, verb string, data []byte, contentType string) *http.Response { +func runRequest(t testing.TB, path, verb string, data []byte, contentType string) *http.Response { request, err := http.NewRequest(verb, path, bytes.NewBuffer(data)) if err != nil { t.Fatalf("unexpected error: %v", err) @@ -4000,6 +4005,257 @@ func (storage *SimpleRESTStorageWithDeleteCollection) DeleteCollection(ctx conte return nil, nil } +// shared vars used by both TestFieldValidation and BenchmarkFieldValidation +var ( + strictFieldValidation = "?fieldValidation=Strict" + warnFieldValidation = "?fieldValidation=Warn" + ignoreFieldValidation = "?fieldValidation=Ignore" +) + +// TestFieldValidation tests the create, update, and patch handlers for correctness when faced with field validation errors. +func TestFieldValidation(t *testing.T) { + defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.ServerSideFieldValidation, true)() + var ( + strictDecodingErr = `strict decoding error: duplicate field \"other\", unknown field \"unknown\"` + strictDecodingWarns = []string{`duplicate field "other"`, `unknown field "unknown"`} + strictDecodingErrYAML = `strict decoding error: yaml: unmarshal errors:\n line 6: key \"other\" already set in map, unknown field \"unknown\"` + strictDecodingWarnsYAML = []string{`line 6: key "other" already set in map`, `unknown field "unknown"`} + strictDecodingErrYAMLPut = `strict decoding error: yaml: unmarshal errors:\n line 7: key \"other\" already set in map, unknown field \"unknown\"` + strictDecodingWarnsYAMLPut = []string{`line 7: key "other" already set in map`, `unknown field "unknown"`} + + invalidJSONDataPost = []byte(`{"kind":"Simple", "apiVersion":"test.group/version", "metadata":{"creationTimestamp":null}, "other":"foo","other":"bar","unknown":"baz"}`) + invalidYAMLDataPost = []byte(`apiVersion: test.group/version +kind: Simple +metadata: + creationTimestamp: null +other: foo +other: bar +unknown: baz`) + + invalidJSONDataPut = []byte(`{"kind":"Simple", "apiVersion":"test.group/version", "metadata":{"name":"id", "creationTimestamp":null}, "other":"foo","other":"bar","unknown":"baz"}`) + invalidYAMLDataPut = []byte(`apiVersion: test.group/version +kind: Simple +metadata: + name: id + creationTimestamp: null +other: foo +other: bar +unknown: baz`) + + invalidMergePatch = []byte(`{"labels":{"foo":"bar"}, "unknown": "foo", "other": "foo", "other": "bar"}`) + invalidJSONPatch = []byte(` +[ + {"op": "add", "path": "/unknown", "value": "foo"}, + {"op": "add", "path": "/other", "value": "foo"}, + {"op": "add", "path": "/other", "value": "bar"} + ] + `) + // note: duplicate fields in the patch itself + // are dropped by the + // evanphx/json-patch library and is expected. + jsonPatchStrictDecodingErr = `strict decoding error: unknown field \"unknown\"` + jsonPatchStrictDecodingWarns = []string{`unknown field "unknown"`} + + invalidSMP = []byte(`{"unknown": "foo", "other":"foo", "other": "bar"}`) + + fieldValidationTests = []struct { + name string + path string + verb string + data []byte + queryParams string + contentType string + expectedErr string + expectedWarns []string + expectedStatusCode int + }{ + // Create + {name: "post-strict-validation", path: "/namespaces/default/simples", verb: "POST", data: invalidJSONDataPost, queryParams: strictFieldValidation, expectedStatusCode: http.StatusBadRequest, expectedErr: strictDecodingErr}, + {name: "post-warn-validation", path: "/namespaces/default/simples", verb: "POST", data: invalidJSONDataPost, queryParams: warnFieldValidation, expectedStatusCode: http.StatusCreated, expectedWarns: strictDecodingWarns}, + {name: "post-ignore-validation", path: "/namespaces/default/simples", verb: "POST", data: invalidJSONDataPost, queryParams: ignoreFieldValidation, expectedStatusCode: http.StatusCreated}, + + {name: "post-strict-validation-yaml", path: "/namespaces/default/simples", verb: "POST", data: invalidYAMLDataPost, queryParams: strictFieldValidation, contentType: "application/yaml", expectedStatusCode: http.StatusBadRequest, expectedErr: strictDecodingErrYAML}, + {name: "post-warn-validation-yaml", path: "/namespaces/default/simples", verb: "POST", data: invalidYAMLDataPost, queryParams: warnFieldValidation, contentType: "application/yaml", expectedStatusCode: http.StatusCreated, expectedWarns: strictDecodingWarnsYAML}, + {name: "post-ignore-validation-yaml", path: "/namespaces/default/simples", verb: "POST", data: invalidYAMLDataPost, queryParams: ignoreFieldValidation, contentType: "application/yaml", expectedStatusCode: http.StatusCreated}, + + // Update + {name: "put-strict-validation", path: "/namespaces/default/simples/id", verb: "PUT", data: invalidJSONDataPut, queryParams: strictFieldValidation, expectedStatusCode: http.StatusBadRequest, expectedErr: strictDecodingErr}, + {name: "put-warn-validation", path: "/namespaces/default/simples/id", verb: "PUT", data: invalidJSONDataPut, queryParams: warnFieldValidation, expectedStatusCode: http.StatusOK, expectedWarns: strictDecodingWarns}, + {name: "put-ignore-validation", path: "/namespaces/default/simples/id", verb: "PUT", data: invalidJSONDataPut, queryParams: ignoreFieldValidation, expectedStatusCode: http.StatusOK}, + + {name: "put-strict-validation-yaml", path: "/namespaces/default/simples/id", verb: "PUT", data: invalidYAMLDataPut, queryParams: strictFieldValidation, contentType: "application/yaml", expectedStatusCode: http.StatusBadRequest, expectedErr: strictDecodingErrYAMLPut}, + {name: "put-warn-validation-yaml", path: "/namespaces/default/simples/id", verb: "PUT", data: invalidYAMLDataPut, queryParams: warnFieldValidation, contentType: "application/yaml", expectedStatusCode: http.StatusOK, expectedWarns: strictDecodingWarnsYAMLPut}, + {name: "put-ignore-validation-yaml", path: "/namespaces/default/simples/id", verb: "PUT", data: invalidYAMLDataPut, queryParams: ignoreFieldValidation, contentType: "application/yaml", expectedStatusCode: http.StatusOK}, + + // MergePatch + {name: "merge-patch-strict-validation", path: "/namespaces/default/simples/id", verb: "PATCH", data: invalidMergePatch, queryParams: strictFieldValidation, contentType: "application/merge-patch+json; charset=UTF-8", expectedStatusCode: http.StatusUnprocessableEntity, expectedErr: strictDecodingErr}, + {name: "merge-patch-warn-validation", path: "/namespaces/default/simples/id", verb: "PATCH", data: invalidMergePatch, queryParams: warnFieldValidation, contentType: "application/merge-patch+json; charset=UTF-8", expectedStatusCode: http.StatusOK, expectedWarns: strictDecodingWarns}, + {name: "merge-patch-ignore-validation", path: "/namespaces/default/simples/id", verb: "PATCH", data: invalidMergePatch, queryParams: ignoreFieldValidation, contentType: "application/merge-patch+json; charset=UTF-8", expectedStatusCode: http.StatusOK}, + + // JSON Patch + {name: "json-patch-strict-validation", path: "/namespaces/default/simples/id", verb: "PATCH", data: invalidJSONPatch, queryParams: strictFieldValidation, contentType: "application/json-patch+json; charset=UTF-8", expectedStatusCode: http.StatusUnprocessableEntity, expectedErr: jsonPatchStrictDecodingErr}, + {name: "json-patch-warn-validation", path: "/namespaces/default/simples/id", verb: "PATCH", data: invalidJSONPatch, queryParams: warnFieldValidation, contentType: "application/json-patch+json; charset=UTF-8", expectedStatusCode: http.StatusOK, expectedWarns: jsonPatchStrictDecodingWarns}, + {name: "json-patch-ignore-validation", path: "/namespaces/default/simples/id", verb: "PATCH", data: invalidJSONPatch, queryParams: ignoreFieldValidation, contentType: "application/json-patch+json; charset=UTF-8", expectedStatusCode: http.StatusOK}, + + // SMP + {name: "strategic-merge-patch-strict-validation", path: "/namespaces/default/simples/id", verb: "PATCH", data: invalidSMP, queryParams: strictFieldValidation, contentType: "application/strategic-merge-patch+json; charset=UTF-8", expectedStatusCode: http.StatusUnprocessableEntity, expectedErr: strictDecodingErr}, + {name: "strategic-merge-patch-warn-validation", path: "/namespaces/default/simples/id", verb: "PATCH", data: invalidSMP, queryParams: warnFieldValidation, contentType: "application/strategic-merge-patch+json; charset=UTF-8", expectedStatusCode: http.StatusOK, expectedWarns: strictDecodingWarns}, + {name: "strategic-merge-patch-ignore-validation", path: "/namespaces/default/simples/id", verb: "PATCH", data: invalidSMP, queryParams: ignoreFieldValidation, contentType: "application/strategic-merge-patch+json; charset=UTF-8", expectedStatusCode: http.StatusOK}, + } + ) + + server := httptest.NewServer(handleWithWarnings(map[string]rest.Storage{ + "simples": &SimpleRESTStorageWithDeleteCollection{ + SimpleRESTStorage{ + item: genericapitesting.Simple{ + ObjectMeta: metav1.ObjectMeta{ + Name: "id", + Namespace: "", + UID: "uid", + }, + Other: "baz", + }, + }, + }, + "simples/subsimple": &SimpleXGSubresourceRESTStorage{ + item: genericapitesting.SimpleXGSubresource{ + SubresourceInfo: "foo", + }, + itemGVK: testGroup2Version.WithKind("SimpleXGSubresource"), + }, + })) + defer server.Close() + for _, test := range fieldValidationTests { + t.Run(test.name, func(t *testing.T) { + baseURL := server.URL + "/" + prefix + "/" + testGroupVersion.Group + "/" + testGroupVersion.Version + response := runRequest(t, baseURL+test.path+test.queryParams, test.verb, test.data, test.contentType) + buf := new(bytes.Buffer) + buf.ReadFrom(response.Body) + + if response.StatusCode != test.expectedStatusCode || !strings.Contains(buf.String(), test.expectedErr) { + t.Fatalf("unexpected response: %#v, expected err: %#v", response, test.expectedErr) + } + + warnings, _ := net.ParseWarningHeaders(response.Header["Warning"]) + if len(warnings) != len(test.expectedWarns) { + t.Fatalf("unexpected number of warnings. Got count %d, expected %d. Got warnings %#v, expected %#v", len(warnings), len(test.expectedWarns), warnings, test.expectedWarns) + + } + for i, warn := range warnings { + if warn.Text != test.expectedWarns[i] { + t.Fatalf("unexpected warning: %#v, expected warning: %#v", warn.Text, test.expectedWarns[i]) + } + } + }) + } +} + +// BenchmarkFieldValidation benchmarks the create, update, and patch handlers for performance distinctions between +// strict, warn, and ignore field validation handling. +func BenchmarkFieldValidation(b *testing.B) { + defer featuregatetesting.SetFeatureGateDuringTest(b, utilfeature.DefaultFeatureGate, features.ServerSideFieldValidation, true)() + var ( + validJSONDataPost = []byte(`{"kind":"Simple", "apiVersion":"test.group/version", "metadata":{"creationTimestamp":null}, "other":"foo"}`) + validYAMLDataPost = []byte(`apiVersion: test.group/version +kind: Simple +metadata: + creationTimestamp: null +other: foo`) + + validJSONDataPut = []byte(`{"kind":"Simple", "apiVersion":"test.group/version", "metadata":{"name":"id", "creationTimestamp":null}, "other":"bar"}`) + validYAMLDataPut = []byte(`apiVersion: test.group/version +kind: Simple +metadata: + name: id + creationTimestamp: null +other: bar`) + + validMergePatch = []byte(`{"labels":{"foo":"bar"}, "other": "bar"}`) + validJSONPatch = []byte(` +[ + {"op": "add", "path": "/other", "value": "bar"} + ] + `) + validSMP = []byte(`{"other": "bar"}`) + + fieldValidationBenchmarks = []struct { + name string + path string + verb string + data []byte + queryParams string + contentType string + expectedStatusCode int + }{ + // Create + {name: "post-strict-validation", path: "/namespaces/default/simples", verb: "POST", data: validJSONDataPost, queryParams: strictFieldValidation, expectedStatusCode: http.StatusCreated}, + {name: "post-warn-validation", path: "/namespaces/default/simples", verb: "POST", data: validJSONDataPost, queryParams: warnFieldValidation, expectedStatusCode: http.StatusCreated}, + {name: "post-ignore-validation", path: "/namespaces/default/simples", verb: "POST", data: validJSONDataPost, queryParams: ignoreFieldValidation, expectedStatusCode: http.StatusCreated}, + + {name: "post-strict-validation-yaml", path: "/namespaces/default/simples", verb: "POST", data: validYAMLDataPost, queryParams: strictFieldValidation, contentType: "application/yaml", expectedStatusCode: http.StatusCreated}, + {name: "post-warn-validation-yaml", path: "/namespaces/default/simples", verb: "POST", data: validYAMLDataPost, queryParams: warnFieldValidation, contentType: "application/yaml", expectedStatusCode: http.StatusCreated}, + {name: "post-ignore-validation-yaml", path: "/namespaces/default/simples", verb: "POST", data: validYAMLDataPost, queryParams: ignoreFieldValidation, contentType: "application/yaml", expectedStatusCode: http.StatusCreated}, + + // Update + {name: "put-strict-validation", path: "/namespaces/default/simples/id", verb: "PUT", data: validJSONDataPut, queryParams: strictFieldValidation, expectedStatusCode: http.StatusOK}, + {name: "put-warn-validation", path: "/namespaces/default/simples/id", verb: "PUT", data: validJSONDataPut, queryParams: warnFieldValidation, expectedStatusCode: http.StatusOK}, + {name: "put-ignore-validation", path: "/namespaces/default/simples/id", verb: "PUT", data: validJSONDataPut, queryParams: ignoreFieldValidation, expectedStatusCode: http.StatusOK}, + + {name: "put-strict-validation-yaml", path: "/namespaces/default/simples/id", verb: "PUT", data: validYAMLDataPut, queryParams: strictFieldValidation, contentType: "application/yaml", expectedStatusCode: http.StatusOK}, + {name: "put-warn-validation-yaml", path: "/namespaces/default/simples/id", verb: "PUT", data: validYAMLDataPut, queryParams: warnFieldValidation, contentType: "application/yaml", expectedStatusCode: http.StatusOK}, + {name: "put-ignore-validation-yaml", path: "/namespaces/default/simples/id", verb: "PUT", data: validYAMLDataPut, queryParams: ignoreFieldValidation, contentType: "application/yaml", expectedStatusCode: http.StatusOK}, + + // MergePatch + {name: "merge-patch-strict-validation", path: "/namespaces/default/simples/id", verb: "PATCH", data: validMergePatch, queryParams: strictFieldValidation, contentType: "application/merge-patch+json; charset=UTF-8", expectedStatusCode: http.StatusOK}, + {name: "merge-patch-warn-validation", path: "/namespaces/default/simples/id", verb: "PATCH", data: validMergePatch, queryParams: warnFieldValidation, contentType: "application/merge-patch+json; charset=UTF-8", expectedStatusCode: http.StatusOK}, + {name: "merge-patch-ignore-validation", path: "/namespaces/default/simples/id", verb: "PATCH", data: validMergePatch, queryParams: ignoreFieldValidation, contentType: "application/merge-patch+json; charset=UTF-8", expectedStatusCode: http.StatusOK}, + + // JSON Patch + {name: "json-patch-strict-validation", path: "/namespaces/default/simples/id", verb: "PATCH", data: validJSONPatch, queryParams: strictFieldValidation, contentType: "application/json-patch+json; charset=UTF-8", expectedStatusCode: http.StatusOK}, + {name: "json-patch-warn-validation", path: "/namespaces/default/simples/id", verb: "PATCH", data: validJSONPatch, queryParams: warnFieldValidation, contentType: "application/json-patch+json; charset=UTF-8", expectedStatusCode: http.StatusOK}, + {name: "json-patch-ignore-validation", path: "/namespaces/default/simples/id", verb: "PATCH", data: validJSONPatch, queryParams: ignoreFieldValidation, contentType: "application/json-patch+json; charset=UTF-8", expectedStatusCode: http.StatusOK}, + + // SMP + {name: "strategic-merge-patch-strict-validation", path: "/namespaces/default/simples/id", verb: "PATCH", data: validSMP, queryParams: strictFieldValidation, contentType: "application/strategic-merge-patch+json; charset=UTF-8", expectedStatusCode: http.StatusOK}, + {name: "strategic-merge-patch-warn-validation", path: "/namespaces/default/simples/id", verb: "PATCH", data: validSMP, queryParams: warnFieldValidation, contentType: "application/strategic-merge-patch+json; charset=UTF-8", expectedStatusCode: http.StatusOK}, + {name: "strategic-merge-patch-ignore-validation", path: "/namespaces/default/simples/id", verb: "PATCH", data: validSMP, queryParams: ignoreFieldValidation, contentType: "application/strategic-merge-patch+json; charset=UTF-8", expectedStatusCode: http.StatusOK}, + } + ) + + server := httptest.NewServer(handleWithWarnings(map[string]rest.Storage{ + "simples": &SimpleRESTStorageWithDeleteCollection{ + SimpleRESTStorage{ + item: genericapitesting.Simple{ + ObjectMeta: metav1.ObjectMeta{ + Name: "id", + Namespace: "", + UID: "uid", + }, + Other: "bar", + }, + }, + }, + "simples/subsimple": &SimpleXGSubresourceRESTStorage{ + item: genericapitesting.SimpleXGSubresource{ + SubresourceInfo: "foo", + }, + itemGVK: testGroup2Version.WithKind("SimpleXGSubresource"), + }, + })) + defer server.Close() + for _, test := range fieldValidationBenchmarks { + b.Run(test.name, func(b *testing.B) { + for n := 0; n < b.N; n++ { + baseURL := server.URL + "/" + prefix + "/" + testGroupVersion.Group + "/" + testGroupVersion.Version + response := runRequest(b, baseURL+test.path+test.queryParams, test.verb, test.data, test.contentType) + if response.StatusCode != test.expectedStatusCode { + b.Fatalf("unexpected status code: %d, expected: %d", response.StatusCode, test.expectedStatusCode) + } + } + }) + } +} + func TestDryRunDisabled(t *testing.T) { defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.DryRun, false)() diff --git a/test/integration/apiserver/field_validation_test.go b/test/integration/apiserver/field_validation_test.go index 4487434b034..909a22b0605 100644 --- a/test/integration/apiserver/field_validation_test.go +++ b/test/integration/apiserver/field_validation_test.go @@ -19,9 +19,11 @@ package apiserver import ( "context" "encoding/json" + "flag" "fmt" "strings" "testing" + "time" apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" apiextensionsclient "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset" @@ -80,6 +82,38 @@ var ( } } ` + validBodyJSON = ` +{ + "apiVersion": "apps/v1", + "kind": "Deployment", + "metadata": { + "name": "%s", + "labels": {"app": "nginx"}, + "annotations": {"a1": "foo", "a2": "bar"} + }, + "spec": { + "selector": { + "matchLabels": { + "app": "nginx" + } + }, + "template": { + "metadata": { + "labels": { + "app": "nginx" + } + }, + "spec": { + "containers": [{ + "name": "nginx", + "image": "nginx:latest", + "imagePullPolicy": "Always" + }] + } + }, + "replicas": 2 + } +}` invalidBodyYAML = `apiVersion: apps/v1 kind: Deployment @@ -108,36 +142,31 @@ spec: imagePullPolicy: Always imagePullPolicy: Never` - validBodyJSON = ` -{ - "apiVersion": "apps/v1", - "kind": "Deployment", - "metadata": { - "name": "%s", - "labels": {"app": "nginx"} - }, - "spec": { - "selector": { - "matchLabels": { - "app": "nginx" - } - }, - "template": { - "metadata": { - "labels": { - "app": "nginx" - } - }, - "spec": { - "containers": [{ - "name": "nginx", - "image": "nginx:latest", - "imagePullPolicy": "Always" - }] - } - } - } -}` + validBodyYAML = `apiVersion: apps/v1 +kind: Deployment +metadata: + name: %s + labels: + app: nginx + annotations: + a1: foo + a2: bar +spec: + replicas: 2 + paused: true + selector: + matchLabels: + app: nginx + template: + metadata: + labels: + app: nginx + spec: + containers: + - name: nginx + image: nginx:latest + imagePullPolicy: Always` + applyInvalidBody = `{ "apiVersion": "apps/v1", "kind": "Deployment", @@ -170,6 +199,38 @@ spec: } } }` + applyValidBody = ` +{ + "apiVersion": "apps/v1", + "kind": "Deployment", + "metadata": { + "name": "%s", + "labels": {"app": "nginx"}, + "annotations": {"a1": "foo", "a2": "bar"} + }, + "spec": { + "selector": { + "matchLabels": { + "app": "nginx" + } + }, + "template": { + "metadata": { + "labels": { + "app": "nginx" + } + }, + "spec": { + "containers": [{ + "name": "nginx", + "image": "nginx:latest", + "imagePullPolicy": "Always" + }] + } + }, + "replicas": 3 + } +}` crdInvalidBody = ` { "apiVersion": "%s", @@ -458,7 +519,7 @@ func testFieldValidationPost(t *testing.T, client clientset.Interface) { bodyBase: invalidBodyJSON, }, { - name: "post-default-ignore-validation", + name: "post-no-validation", bodyBase: invalidBodyJSON, strictDecodingWarnings: []string{ `unknown field "spec.unknown1"`, @@ -1005,6 +1066,36 @@ func testFieldValidationPatchTyped(t *testing.T, client clientset.Interface) { // with unknown fields errors out when fieldValidation is strict, // but succeeds when fieldValidation is ignored. func testFieldValidationSMP(t *testing.T, client clientset.Interface) { + // non-conflicting SMP has issues with the patch (duplicate fields), + // but doesn't conflict with the existing object it's being patched to + nonconflictingSMPBody := ` + { + "spec": { + "paused": true, + "paused": false, + "selector": { + "matchLabels": { + "app": "nginx" + } + }, + "template": { + "metadata": { + "labels": { + "app": "nginx" + } + }, + "spec": { + "containers": [{ + "name": "nginx", + "imagePullPolicy": "Always", + "imagePullPolicy": "Never" + }] + } + } + } + } + ` + smpBody := ` { "spec": { @@ -1036,36 +1127,6 @@ func testFieldValidationSMP(t *testing.T, client clientset.Interface) { } } ` - // non-conflicting SMP has issues with the patch (duplicate fields), - // but doesn't conflict with the existing object it's being patched to - nonconflictingSMPBody := ` - { - "spec": { - "paused": true, - "paused": false, - "selector": { - "matchLabels": { - "app": "nginx" - } - }, - "template": { - "metadata": { - "labels": { - "app": "nginx" - } - }, - "spec": { - "containers": [{ - "name": "nginx", - "imagePullPolicy": "Always", - "imagePullPolicy": "Never" - }] - } - } - } - } - ` - var testcases = []struct { name string opts metav1.PatchOptions @@ -2893,3 +2954,526 @@ func setupCRD(t *testing.T, config *rest.Config, apiGroup string, schemaless boo return crd } + +func BenchmarkFieldValidation(b *testing.B) { + defer featuregatetesting.SetFeatureGateDuringTest(b, utilfeature.DefaultFeatureGate, features.ServerSideFieldValidation, true)() + flag.Lookup("v").Value.Set("0") + server, err := kubeapiservertesting.StartTestServer(b, kubeapiservertesting.NewDefaultTestServerOptions(), nil, framework.SharedEtcd()) + if err != nil { + b.Fatal(err) + } + config := server.ClientConfig + defer server.TearDownFn() + + // don't log warnings, tests inspect them in the responses directly + config.WarningHandler = rest.NoWarnings{} + + client := clientset.NewForConfigOrDie(config) + + b.Run("Post", func(b *testing.B) { benchFieldValidationPost(b, client) }) + b.Run("Put", func(b *testing.B) { benchFieldValidationPut(b, client) }) + b.Run("PatchTyped", func(b *testing.B) { benchFieldValidationPatchTyped(b, client) }) + b.Run("SMP", func(b *testing.B) { benchFieldValidationSMP(b, client) }) + b.Run("ApplyCreate", func(b *testing.B) { benchFieldValidationApplyCreate(b, client) }) + b.Run("ApplyUpdate", func(b *testing.B) { benchFieldValidationApplyUpdate(b, client) }) + +} + +func benchFieldValidationPost(b *testing.B, client clientset.Interface) { + var benchmarks = []struct { + name string + bodyBase string + opts metav1.CreateOptions + contentType string + }{ + { + name: "post-strict-validation", + opts: metav1.CreateOptions{ + FieldValidation: "Strict", + }, + bodyBase: validBodyJSON, + }, + { + name: "post-warn-validation", + opts: metav1.CreateOptions{ + FieldValidation: "Warn", + }, + bodyBase: validBodyJSON, + }, + { + name: "post-ignore-validation", + opts: metav1.CreateOptions{ + FieldValidation: "Ignore", + }, + bodyBase: validBodyJSON, + }, + { + name: "post-strict-validation-yaml", + opts: metav1.CreateOptions{ + FieldValidation: "Strict", + }, + bodyBase: validBodyYAML, + contentType: "application/yaml", + }, + { + name: "post-warn-validation-yaml", + opts: metav1.CreateOptions{ + FieldValidation: "Warn", + }, + bodyBase: validBodyYAML, + contentType: "application/yaml", + }, + { + name: "post-ignore-validation-yaml", + opts: metav1.CreateOptions{ + FieldValidation: "Ignore", + }, + bodyBase: validBodyYAML, + contentType: "application/yaml", + }, + } + + for _, bm := range benchmarks { + b.Run(bm.name, func(b *testing.B) { + b.ResetTimer() + b.ReportAllocs() + for n := 0; n < b.N; n++ { + body := []byte(fmt.Sprintf(bm.bodyBase, fmt.Sprintf("test-deployment-%s-%d-%d-%d", bm.name, n, b.N, time.Now().UnixNano()))) + req := client.CoreV1().RESTClient().Post(). + AbsPath("/apis/apps/v1"). + Namespace("default"). + Resource("deployments"). + SetHeader("Content-Type", bm.contentType). + VersionedParams(&bm.opts, metav1.ParameterCodec) + result := req.Body(body).Do(context.TODO()) + if result.Error() != nil { + b.Fatalf("unexpected request err: %v", result.Error()) + } + } + }) + } +} + +func benchFieldValidationPut(b *testing.B, client clientset.Interface) { + var testcases = []struct { + name string + opts metav1.UpdateOptions + putBodyBase string + contentType string + }{ + { + name: "put-strict-validation", + opts: metav1.UpdateOptions{ + FieldValidation: "Strict", + }, + putBodyBase: validBodyJSON, + }, + { + name: "put-warn-validation", + opts: metav1.UpdateOptions{ + FieldValidation: "Warn", + }, + putBodyBase: validBodyJSON, + }, + { + name: "put-ignore-validation", + opts: metav1.UpdateOptions{ + FieldValidation: "Ignore", + }, + putBodyBase: validBodyJSON, + }, + { + name: "put-strict-validation-yaml", + opts: metav1.UpdateOptions{ + FieldValidation: "Strict", + }, + putBodyBase: validBodyYAML, + contentType: "application/yaml", + }, + { + name: "put-warn-validation-yaml", + opts: metav1.UpdateOptions{ + FieldValidation: "Warn", + }, + putBodyBase: validBodyYAML, + contentType: "application/yaml", + }, + { + name: "put-ignore-validation-yaml", + opts: metav1.UpdateOptions{ + FieldValidation: "Ignore", + }, + putBodyBase: validBodyYAML, + contentType: "application/yaml", + }, + } + + for _, tc := range testcases { + b.Run(tc.name, func(b *testing.B) { + names := make([]string, b.N) + for n := 0; n < b.N; n++ { + deployName := fmt.Sprintf("%s-%d-%d-%d", tc.name, n, b.N, time.Now().UnixNano()) + names[n] = deployName + postBody := []byte(fmt.Sprintf(string(validBodyJSON), deployName)) + + if _, err := client.CoreV1().RESTClient().Post(). + AbsPath("/apis/apps/v1"). + Namespace("default"). + Resource("deployments"). + Body(postBody). + DoRaw(context.TODO()); err != nil { + b.Fatalf("failed to create initial deployment: %v", err) + } + + } + b.ResetTimer() + b.ReportAllocs() + for n := 0; n < b.N; n++ { + deployName := names[n] + putBody := []byte(fmt.Sprintf(string(tc.putBodyBase), deployName)) + req := client.CoreV1().RESTClient().Put(). + AbsPath("/apis/apps/v1"). + Namespace("default"). + Resource("deployments"). + SetHeader("Content-Type", tc.contentType). + Name(deployName). + VersionedParams(&tc.opts, metav1.ParameterCodec) + result := req.Body([]byte(putBody)).Do(context.TODO()) + if result.Error() != nil { + b.Fatalf("unexpected request err: %v", result.Error()) + } + } + }) + } +} + +func benchFieldValidationPatchTyped(b *testing.B, client clientset.Interface) { + mergePatchBodyValid := ` +{ + "spec": { + "paused": false, + "template": { + "spec": { + "containers": [{ + "name": "nginx", + "image": "nginx:latest", + "imagePullPolicy": "Always" + }] + } + }, + "replicas": 2 + } +} + ` + + jsonPatchBodyValid := ` + [ + {"op": "add", "path": "/spec/paused", "value": true}, + {"op": "add", "path": "/spec/template/spec/containers/0/imagePullPolicy", "value": "Never"}, + {"op": "add", "path": "/spec/replicas", "value": 2} + ] + ` + + var testcases = []struct { + name string + opts metav1.PatchOptions + patchType types.PatchType + body string + }{ + { + name: "merge-patch-strict-validation", + opts: metav1.PatchOptions{ + FieldValidation: "Strict", + }, + patchType: types.MergePatchType, + body: mergePatchBodyValid, + }, + { + name: "merge-patch-warn-validation", + opts: metav1.PatchOptions{ + FieldValidation: "Warn", + }, + patchType: types.MergePatchType, + body: mergePatchBodyValid, + }, + { + name: "merge-patch-ignore-validation", + opts: metav1.PatchOptions{ + FieldValidation: "Ignore", + }, + patchType: types.MergePatchType, + body: mergePatchBodyValid, + }, + { + name: "json-patch-strict-validation", + patchType: types.JSONPatchType, + opts: metav1.PatchOptions{ + FieldValidation: "Strict", + }, + body: jsonPatchBodyValid, + }, + { + name: "json-patch-warn-validation", + patchType: types.JSONPatchType, + opts: metav1.PatchOptions{ + FieldValidation: "Warn", + }, + body: jsonPatchBodyValid, + }, + { + name: "json-patch-ignore-validation", + patchType: types.JSONPatchType, + opts: metav1.PatchOptions{ + FieldValidation: "Ignore", + }, + body: jsonPatchBodyValid, + }, + } + + for _, tc := range testcases { + b.Run(tc.name, func(b *testing.B) { + names := make([]string, b.N) + for n := 0; n < b.N; n++ { + deployName := fmt.Sprintf("%s-%d-%d-%d", tc.name, n, b.N, time.Now().UnixNano()) + names[n] = deployName + postBody := []byte(fmt.Sprintf(string(validBodyJSON), deployName)) + + if _, err := client.CoreV1().RESTClient().Post(). + AbsPath("/apis/apps/v1"). + Namespace("default"). + Resource("deployments"). + Body(postBody). + DoRaw(context.TODO()); err != nil { + b.Fatalf("failed to create initial deployment: %v", err) + } + } + b.ResetTimer() + b.ReportAllocs() + for n := 0; n < b.N; n++ { + deployName := names[n] + req := client.CoreV1().RESTClient().Patch(tc.patchType). + AbsPath("/apis/apps/v1"). + Namespace("default"). + Resource("deployments"). + Name(deployName). + VersionedParams(&tc.opts, metav1.ParameterCodec) + result := req.Body([]byte(tc.body)).Do(context.TODO()) + if result.Error() != nil { + b.Fatalf("unexpected request err: %v", result.Error()) + } + } + + }) + } +} + +func benchFieldValidationSMP(b *testing.B, client clientset.Interface) { + smpBodyValid := ` + { + "spec": { + "replicas": 3, + "paused": false, + "selector": { + "matchLabels": { + "app": "nginx" + } + }, + "template": { + "metadata": { + "labels": { + "app": "nginx" + } + }, + "spec": { + "containers": [{ + "name": "nginx", + "imagePullPolicy": "Never" + }] + } + } + } + } + ` + var testcases = []struct { + name string + opts metav1.PatchOptions + body string + }{ + { + name: "smp-strict-validation", + opts: metav1.PatchOptions{ + FieldValidation: "Strict", + }, + body: smpBodyValid, + }, + { + name: "smp-warn-validation", + opts: metav1.PatchOptions{ + FieldValidation: "Warn", + }, + body: smpBodyValid, + }, + { + name: "smp-ignore-validation", + opts: metav1.PatchOptions{ + FieldValidation: "Ignore", + }, + body: smpBodyValid, + }, + } + + for _, tc := range testcases { + b.Run(tc.name, func(b *testing.B) { + names := make([]string, b.N) + for n := 0; n < b.N; n++ { + name := fmt.Sprintf("%s-%d-%d-%d", tc.name, n, b.N, time.Now().UnixNano()) + names[n] = name + body := []byte(fmt.Sprintf(validBodyJSON, name)) + _, err := client.CoreV1().RESTClient().Patch(types.ApplyPatchType). + AbsPath("/apis/apps/v1"). + Namespace("default"). + Resource("deployments"). + Name(name). + Param("fieldManager", "apply_test"). + Body(body). + Do(context.TODO()). + Get() + if err != nil { + b.Fatalf("Failed to create object using Apply patch: %v", err) + } + } + b.ResetTimer() + b.ReportAllocs() + for n := 0; n < b.N; n++ { + name := names[n] + req := client.CoreV1().RESTClient().Patch(types.StrategicMergePatchType). + AbsPath("/apis/apps/v1"). + Namespace("default"). + Resource("deployments"). + Name(name). + VersionedParams(&tc.opts, metav1.ParameterCodec) + result := req.Body([]byte(tc.body)).Do(context.TODO()) + if result.Error() != nil { + b.Fatalf("unexpected request err: %v", result.Error()) + } + } + }) + } + +} + +func benchFieldValidationApplyCreate(b *testing.B, client clientset.Interface) { + var testcases = []struct { + name string + opts metav1.PatchOptions + }{ + { + name: "strict-validation", + opts: metav1.PatchOptions{ + FieldValidation: "Strict", + FieldManager: "mgr", + }, + }, + { + name: "warn-validation", + opts: metav1.PatchOptions{ + FieldValidation: "Warn", + FieldManager: "mgr", + }, + }, + { + name: "ignore-validation", + opts: metav1.PatchOptions{ + FieldValidation: "Ignore", + FieldManager: "mgr", + }, + }, + } + + for _, tc := range testcases { + b.Run(tc.name, func(b *testing.B) { + b.ResetTimer() + b.ReportAllocs() + for n := 0; n < b.N; n++ { + name := fmt.Sprintf("apply-create-deployment-%s-%d-%d-%d", tc.name, n, b.N, time.Now().UnixNano()) + body := []byte(fmt.Sprintf(validBodyJSON, name)) + req := client.CoreV1().RESTClient().Patch(types.ApplyPatchType). + AbsPath("/apis/apps/v1"). + Namespace("default"). + Resource("deployments"). + Name(name). + VersionedParams(&tc.opts, metav1.ParameterCodec) + result := req.Body(body).Do(context.TODO()) + if result.Error() != nil { + b.Fatalf("unexpected request err: %v", result.Error()) + } + } + }) + } +} + +func benchFieldValidationApplyUpdate(b *testing.B, client clientset.Interface) { + var testcases = []struct { + name string + opts metav1.PatchOptions + }{ + { + name: "strict-validation", + opts: metav1.PatchOptions{ + FieldValidation: "Strict", + FieldManager: "mgr", + }, + }, + { + name: "warn-validation", + opts: metav1.PatchOptions{ + FieldValidation: "Warn", + FieldManager: "mgr", + }, + }, + { + name: "ignore-validation", + opts: metav1.PatchOptions{ + FieldValidation: "Ignore", + FieldManager: "mgr", + }, + }, + } + + for _, tc := range testcases { + b.Run(tc.name, func(b *testing.B) { + names := make([]string, b.N) + for n := 0; n < b.N; n++ { + name := fmt.Sprintf("apply-update-deployment-%s-%d-%d-%d", tc.name, n, b.N, time.Now().UnixNano()) + names[n] = name + createBody := []byte(fmt.Sprintf(validBodyJSON, name)) + createReq := client.CoreV1().RESTClient().Patch(types.ApplyPatchType). + AbsPath("/apis/apps/v1"). + Namespace("default"). + Resource("deployments"). + Name(name). + VersionedParams(&tc.opts, metav1.ParameterCodec) + createResult := createReq.Body(createBody).Do(context.TODO()) + if createResult.Error() != nil { + b.Fatalf("unexpected apply create err: %v", createResult.Error()) + } + } + b.ResetTimer() + b.ReportAllocs() + for n := 0; n < b.N; n++ { + name := names[n] + updateBody := []byte(fmt.Sprintf(applyValidBody, name)) + updateReq := client.CoreV1().RESTClient().Patch(types.ApplyPatchType). + AbsPath("/apis/apps/v1"). + Namespace("default"). + Resource("deployments"). + Name(name). + VersionedParams(&tc.opts, metav1.ParameterCodec) + result := updateReq.Body(updateBody).Do(context.TODO()) + if result.Error() != nil { + b.Fatalf("unexpected request err: %v", result.Error()) + } + } + }) + } +}