FieldValidation tests for endpoints apiserver and benchmarks for integration tests (#107848)

* wip, working post-strict-yaml

* wip, merge-patch and json-patch tests added

* added SMP tests

* cleanup

* add benchmarks

* more detailed test failure message

* start adding field validation integration benchmarks

* use valid input for benchmarking

* fix remaining integration benchmarks

* benchmarking feedback

* fix endpoints benchmarking

* remove unused vars
This commit is contained in:
Kevin Delgado 2022-02-16 21:19:49 -08:00 committed by GitHub
parent 912c9c46f8
commit df2768123d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 902 additions and 62 deletions

View File

@ -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)()

View File

@ -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())
}
}
})
}
}