kubernetes/test/integration/apiserver/field_validation_test.go

3954 lines
120 KiB
Go

/*
Copyright 2021 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
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"
"k8s.io/apiextensions-apiserver/test/integration/fixtures"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/types"
"k8s.io/client-go/dynamic"
clientset "k8s.io/client-go/kubernetes"
"k8s.io/client-go/rest"
"k8s.io/klog/v2"
kubeapiservertesting "k8s.io/kubernetes/cmd/kube-apiserver/app/testing"
"k8s.io/kubernetes/test/integration/framework"
)
var (
invalidBodyJSON = `
{
"apiVersion": "apps/v1",
"kind": "Deployment",
"metadata": {
"name": "dupename",
"name": "%s",
"labels": {"app": "nginx"},
"unknownMeta": "metaVal"
},
"spec": {
"unknown1": "val1",
"unknownDupe": "valDupe",
"unknownDupe": "valDupe2",
"paused": true,
"paused": false,
"selector": {
"matchLabels": {
"app": "nginx"
}
},
"template": {
"metadata": {
"labels": {
"app": "nginx"
}
},
"spec": {
"containers": [{
"name": "nginx",
"image": "nginx:latest",
"unknownNested": "val1",
"imagePullPolicy": "Always",
"imagePullPolicy": "Never"
}]
}
}
}
}
`
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
metadata:
name: dupename
name: %s
unknownMeta: metaVal
labels:
app: nginx
spec:
unknown1: val1
unknownDupe: valDupe
unknownDupe: valDupe2
paused: true
paused: false
selector:
matchLabels:
app: nginx
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx:latest
unknownNested: val1
imagePullPolicy: Always
imagePullPolicy: Never`
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",
"metadata": {
"name": "%s",
"labels": {"app": "nginx"}
},
"spec": {
"paused": false,
"paused": true,
"selector": {
"matchLabels": {
"app": "nginx"
}
},
"template": {
"metadata": {
"labels": {
"app": "nginx"
}
},
"spec": {
"containers": [{
"name": "nginx",
"image": "nginx:latest",
"imagePullPolicy": "Never",
"imagePullPolicy": "Always"
}]
}
}
}
}`
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",
"kind": "%s",
"metadata": {
"name": "dupename",
"name": "%s",
"unknownMeta": "metaVal",
"resourceVersion": "%s"
},
"spec": {
"unknown1": "val1",
"unknownDupe": "valDupe",
"unknownDupe": "valDupe2",
"knownField1": "val1",
"knownField1": "val2",
"ports": [{
"name": "portName",
"containerPort": 8080,
"protocol": "TCP",
"hostPort": 8081,
"hostPort": 8082,
"unknownNested": "val"
}],
"embeddedObj": {
"apiVersion": "v1",
"kind": "ConfigMap",
"metadata": {
"name": "my-cm",
"namespace": "my-ns",
"unknownEmbeddedMeta": "foo"
}
}
}
}`
crdValidBody = `
{
"apiVersion": "%s",
"kind": "%s",
"metadata": {
"name": "%s",
"resourceVersion": "%s"
},
"spec": {
"knownField1": "val1",
"ports": [{
"name": "portName",
"containerPort": 8080,
"protocol": "TCP",
"hostPort": 8081
}],
"embeddedObj": {
"apiVersion": "v1",
"kind": "ConfigMap",
"metadata": {
"name": "my-cm"
}
}
}
}
`
crdInvalidBodyYAML = `
apiVersion: "%s"
kind: "%s"
metadata:
name: dupename
name: "%s"
resourceVersion: "%s"
unknownMeta: metaVal
spec:
unknown1: val1
unknownDupe: valDupe
unknownDupe: valDupe2
knownField1: val1
knownField1: val2
ports:
- name: portName
containerPort: 8080
protocol: TCP
hostPort: 8081
hostPort: 8082
unknownNested: val
embeddedObj:
apiVersion: v1
kind: ConfigMap
metadata:
name: my-cm
namespace: my-ns
unknownEmbeddedMeta: foo`
crdValidBodyYAML = `
apiVersion: "%s"
kind: "%s"
metadata:
name: "%s"
resourceVersion: "%s"
spec:
knownField1: val1
ports:
- name: portName
containerPort: 8080
protocol: TCP
hostPort: 8081
embeddedObj:
apiVersion: v1
kind: ConfigMap
metadata:
name: my-cm
namespace: my-ns`
crdApplyInvalidBody = `
{
"apiVersion": "%s",
"kind": "%s",
"metadata": {
"name": "%s"
},
"spec": {
"knownField1": "val1",
"knownField1": "val2",
"ports": [{
"name": "portName",
"containerPort": 8080,
"protocol": "TCP",
"hostPort": 8081,
"hostPort": 8082
}],
"embeddedObj": {
"apiVersion": "v1",
"kind": "ConfigMap",
"metadata": {
"name": "my-cm",
"namespace": "my-ns"
}
}
}
}`
crdApplyValidBody = `
{
"apiVersion": "%s",
"kind": "%s",
"metadata": {
"name": "%s"
},
"spec": {
"knownField1": "val1",
"ports": [{
"name": "portName",
"containerPort": 8080,
"protocol": "TCP",
"hostPort": 8082
}],
"embeddedObj": {
"apiVersion": "v1",
"kind": "ConfigMap",
"metadata": {
"name": "my-cm",
"namespace": "my-ns"
}
}
}
}`
crdApplyValidBody2 = `
{
"apiVersion": "%s",
"kind": "%s",
"metadata": {
"name": "%s"
},
"spec": {
"knownField1": "val2",
"ports": [{
"name": "portName",
"containerPort": 8080,
"protocol": "TCP",
"hostPort": 8083
}],
"embeddedObj": {
"apiVersion": "v1",
"kind": "ConfigMap",
"metadata": {
"name": "my-cm",
"namespace": "my-ns"
}
}
}
}`
patchYAMLBody = `
apiVersion: %s
kind: %s
metadata:
name: %s
finalizers:
- test-finalizer
spec:
cronSpec: "* * * * */5"
ports:
- name: x
containerPort: 80
protocol: TCP
`
crdSchemaBase = `
{
"openAPIV3Schema": {
"type": "object",
"properties": {
"spec": {
"type": "object",
%s
"properties": {
"cronSpec": {
"type": "string",
"pattern": "^(\\d+|\\*)(/\\d+)?(\\s+(\\d+|\\*)(/\\d+)?){4}$"
},
"knownField1": {
"type": "string"
},
"embeddedObj": {
"x-kubernetes-embedded-resource": true,
"type": "object",
"properties": {
"apiversion": {
"type": "string"
},
"kind": {
"type": "string"
},
"metadata": {
"type": "object"
}
}
},
"ports": {
"type": "array",
"x-kubernetes-list-map-keys": [
"containerPort",
"protocol"
],
"x-kubernetes-list-type": "map",
"items": {
"properties": {
"containerPort": {
"format": "int32",
"type": "integer"
},
"hostIP": {
"type": "string"
},
"hostPort": {
"format": "int32",
"type": "integer"
},
"name": {
"type": "string"
},
"protocol": {
"type": "string"
}
},
"required": [
"containerPort",
"protocol"
],
"type": "object"
}
}
}
}
}
}
}
`
)
func TestFieldValidation(t *testing.T) {
server, err := kubeapiservertesting.StartTestServer(t, kubeapiservertesting.NewDefaultTestServerOptions(), nil, framework.SharedEtcd())
if err != nil {
t.Fatal(err)
}
config := server.ClientConfig
defer server.TearDownFn()
// don't log warnings, tests inspect them in the responses directly
config.WarningHandler = rest.NoWarnings{}
schemaCRD := setupCRD(t, config, "schema.example.com", false)
schemaGVR := schema.GroupVersionResource{
Group: schemaCRD.Spec.Group,
Version: schemaCRD.Spec.Versions[0].Name,
Resource: schemaCRD.Spec.Names.Plural,
}
schemaGVK := schema.GroupVersionKind{
Group: schemaCRD.Spec.Group,
Version: schemaCRD.Spec.Versions[0].Name,
Kind: schemaCRD.Spec.Names.Kind,
}
schemalessCRD := setupCRD(t, config, "schemaless.example.com", true)
schemalessGVR := schema.GroupVersionResource{
Group: schemalessCRD.Spec.Group,
Version: schemalessCRD.Spec.Versions[0].Name,
Resource: schemalessCRD.Spec.Names.Plural,
}
schemalessGVK := schema.GroupVersionKind{
Group: schemalessCRD.Spec.Group,
Version: schemalessCRD.Spec.Versions[0].Name,
Kind: schemalessCRD.Spec.Names.Kind,
}
client := clientset.NewForConfigOrDie(config)
rest := client.Discovery().RESTClient()
t.Run("Post", func(t *testing.T) { testFieldValidationPost(t, client) })
t.Run("Put", func(t *testing.T) { testFieldValidationPut(t, client) })
t.Run("PatchTyped", func(t *testing.T) { testFieldValidationPatchTyped(t, client) })
t.Run("SMP", func(t *testing.T) { testFieldValidationSMP(t, client) })
t.Run("ApplyCreate", func(t *testing.T) { testFieldValidationApplyCreate(t, client) })
t.Run("ApplyUpdate", func(t *testing.T) { testFieldValidationApplyUpdate(t, client) })
t.Run("PostCRD", func(t *testing.T) { testFieldValidationPostCRD(t, rest, schemaGVK, schemaGVR) })
t.Run("PutCRD", func(t *testing.T) { testFieldValidationPutCRD(t, rest, schemaGVK, schemaGVR) })
t.Run("PatchCRD", func(t *testing.T) { testFieldValidationPatchCRD(t, rest, schemaGVK, schemaGVR) })
t.Run("ApplyCreateCRD", func(t *testing.T) { testFieldValidationApplyCreateCRD(t, rest, schemaGVK, schemaGVR) })
t.Run("ApplyUpdateCRD", func(t *testing.T) { testFieldValidationApplyUpdateCRD(t, rest, schemaGVK, schemaGVR) })
t.Run("PostCRDSchemaless", func(t *testing.T) { testFieldValidationPostCRDSchemaless(t, rest, schemalessGVK, schemalessGVR) })
t.Run("PutCRDSchemaless", func(t *testing.T) { testFieldValidationPutCRDSchemaless(t, rest, schemalessGVK, schemalessGVR) })
t.Run("PatchCRDSchemaless", func(t *testing.T) { testFieldValidationPatchCRDSchemaless(t, rest, schemalessGVK, schemalessGVR) })
t.Run("ApplyCreateCRDSchemaless", func(t *testing.T) { testFieldValidationApplyCreateCRDSchemaless(t, rest, schemalessGVK, schemalessGVR) })
t.Run("ApplyUpdateCRDSchemaless", func(t *testing.T) { testFieldValidationApplyUpdateCRDSchemaless(t, rest, schemalessGVK, schemalessGVR) })
}
// testFieldValidationPost tests POST requests containing unknown fields with
// strict and non-strict field validation.
func testFieldValidationPost(t *testing.T, client clientset.Interface) {
var testcases = []struct {
name string
bodyBase string
opts metav1.CreateOptions
contentType string
strictDecodingError string
strictDecodingWarnings []string
}{
{
name: "post-strict-validation",
opts: metav1.CreateOptions{
FieldValidation: "Strict",
},
bodyBase: invalidBodyJSON,
strictDecodingError: `strict decoding error: duplicate field "metadata.name", unknown field "metadata.unknownMeta", unknown field "spec.unknown1", unknown field "spec.unknownDupe", duplicate field "spec.paused", unknown field "spec.template.spec.containers[0].unknownNested", duplicate field "spec.template.spec.containers[0].imagePullPolicy"`,
},
{
name: "post-warn-validation",
opts: metav1.CreateOptions{
FieldValidation: "Warn",
},
bodyBase: invalidBodyJSON,
strictDecodingWarnings: []string{
`duplicate field "metadata.name"`,
`unknown field "metadata.unknownMeta"`,
`unknown field "spec.unknown1"`,
`unknown field "spec.unknownDupe"`,
`duplicate field "spec.paused"`,
// note: fields that are both unknown
// and duplicated will only be detected
// as unknown for typed resources.
`unknown field "spec.template.spec.containers[0].unknownNested"`,
`duplicate field "spec.template.spec.containers[0].imagePullPolicy"`,
},
},
{
name: "post-ignore-validation",
opts: metav1.CreateOptions{
FieldValidation: "Ignore",
},
bodyBase: invalidBodyJSON,
},
{
name: "post-no-validation",
bodyBase: invalidBodyJSON,
strictDecodingWarnings: []string{
`duplicate field "metadata.name"`,
`unknown field "metadata.unknownMeta"`,
`unknown field "spec.unknown1"`,
`unknown field "spec.unknownDupe"`,
`duplicate field "spec.paused"`,
// note: fields that are both unknown
// and duplicated will only be detected
// as unknown for typed resources.
`unknown field "spec.template.spec.containers[0].unknownNested"`,
`duplicate field "spec.template.spec.containers[0].imagePullPolicy"`,
},
},
{
name: "post-strict-validation-yaml",
opts: metav1.CreateOptions{
FieldValidation: "Strict",
},
bodyBase: invalidBodyYAML,
contentType: "application/yaml",
strictDecodingError: `strict decoding error: yaml: unmarshal errors:
line 5: key "name" already set in map
line 12: key "unknownDupe" already set in map
line 14: key "paused" already set in map
line 28: key "imagePullPolicy" already set in map, unknown field "metadata.unknownMeta", unknown field "spec.template.spec.containers[0].unknownNested", unknown field "spec.unknown1", unknown field "spec.unknownDupe"`,
},
{
name: "post-warn-validation-yaml",
opts: metav1.CreateOptions{
FieldValidation: "Warn",
},
bodyBase: invalidBodyYAML,
contentType: "application/yaml",
strictDecodingWarnings: []string{
`line 5: key "name" already set in map`,
`line 12: key "unknownDupe" already set in map`,
`line 14: key "paused" already set in map`,
`line 28: key "imagePullPolicy" already set in map`,
`unknown field "metadata.unknownMeta"`,
`unknown field "spec.template.spec.containers[0].unknownNested"`,
`unknown field "spec.unknown1"`,
`unknown field "spec.unknownDupe"`,
},
},
{
name: "post-ignore-validation-yaml",
opts: metav1.CreateOptions{
FieldValidation: "Ignore",
},
bodyBase: invalidBodyYAML,
contentType: "application/yaml",
},
{
name: "post-no-validation-yaml",
bodyBase: invalidBodyYAML,
contentType: "application/yaml",
strictDecodingWarnings: []string{
`line 5: key "name" already set in map`,
`line 12: key "unknownDupe" already set in map`,
`line 14: key "paused" already set in map`,
`line 28: key "imagePullPolicy" already set in map`,
`unknown field "metadata.unknownMeta"`,
`unknown field "spec.template.spec.containers[0].unknownNested"`,
`unknown field "spec.unknown1"`,
`unknown field "spec.unknownDupe"`,
},
},
}
for _, tc := range testcases {
t.Run(tc.name, func(t *testing.T) {
klog.Warningf("running tc named: %s", tc.name)
body := []byte(fmt.Sprintf(tc.bodyBase, fmt.Sprintf("test-deployment-%s", tc.name)))
req := client.CoreV1().RESTClient().Post().
AbsPath("/apis/apps/v1").
Namespace("default").
Resource("deployments").
SetHeader("Content-Type", tc.contentType).
VersionedParams(&tc.opts, metav1.ParameterCodec)
result := req.Body(body).Do(context.TODO())
if result.Error() == nil && tc.strictDecodingError != "" {
t.Fatalf("received nil error when expecting: %q", tc.strictDecodingError)
}
if result.Error() != nil && (tc.strictDecodingError == "" || !strings.HasSuffix(result.Error().Error(), tc.strictDecodingError)) {
t.Fatalf("expected error: %q, got: %v", tc.strictDecodingError, result.Error())
}
if len(result.Warnings()) != len(tc.strictDecodingWarnings) {
t.Fatalf("unexpected number of warnings, expected: %d, got: %d", len(tc.strictDecodingWarnings), len(result.Warnings()))
}
for i, strictWarn := range tc.strictDecodingWarnings {
if strictWarn != result.Warnings()[i].Text {
t.Fatalf("expected warning: %s, got warning: %s", strictWarn, result.Warnings()[i].Text)
}
}
})
}
}
// testFieldValidationPut tests PUT requests
// that update existing objects with unknown fields
// for both strict and non-strict field validation.
func testFieldValidationPut(t *testing.T, client clientset.Interface) {
deployName := "test-deployment-put"
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 {
t.Fatalf("failed to create initial deployment: %v", err)
}
var testcases = []struct {
name string
opts metav1.UpdateOptions
putBodyBase string
contentType string
strictDecodingError string
strictDecodingWarnings []string
}{
{
name: "put-strict-validation",
opts: metav1.UpdateOptions{
FieldValidation: "Strict",
},
putBodyBase: invalidBodyJSON,
strictDecodingError: `strict decoding error: duplicate field "metadata.name", unknown field "metadata.unknownMeta", unknown field "spec.unknown1", unknown field "spec.unknownDupe", duplicate field "spec.paused", unknown field "spec.template.spec.containers[0].unknownNested", duplicate field "spec.template.spec.containers[0].imagePullPolicy"`,
},
{
name: "put-warn-validation",
opts: metav1.UpdateOptions{
FieldValidation: "Warn",
},
putBodyBase: invalidBodyJSON,
strictDecodingWarnings: []string{
`duplicate field "metadata.name"`,
`unknown field "metadata.unknownMeta"`,
`unknown field "spec.unknown1"`,
`unknown field "spec.unknownDupe"`,
`duplicate field "spec.paused"`,
// note: fields that are both unknown
// and duplicated will only be detected
// as unknown for typed resources.
`unknown field "spec.template.spec.containers[0].unknownNested"`,
`duplicate field "spec.template.spec.containers[0].imagePullPolicy"`,
},
},
{
name: "put-ignore-validation",
opts: metav1.UpdateOptions{
FieldValidation: "Ignore",
},
putBodyBase: invalidBodyJSON,
},
{
name: "put-no-validation",
putBodyBase: invalidBodyJSON,
strictDecodingWarnings: []string{
`duplicate field "metadata.name"`,
`unknown field "metadata.unknownMeta"`,
`unknown field "spec.unknown1"`,
`unknown field "spec.unknownDupe"`,
`duplicate field "spec.paused"`,
// note: fields that are both unknown
// and duplicated will only be detected
// as unknown for typed resources.
`unknown field "spec.template.spec.containers[0].unknownNested"`,
`duplicate field "spec.template.spec.containers[0].imagePullPolicy"`,
},
},
{
name: "put-strict-validation-yaml",
opts: metav1.UpdateOptions{
FieldValidation: "Strict",
},
putBodyBase: invalidBodyYAML,
contentType: "application/yaml",
strictDecodingError: `strict decoding error: yaml: unmarshal errors:
line 5: key "name" already set in map
line 12: key "unknownDupe" already set in map
line 14: key "paused" already set in map
line 28: key "imagePullPolicy" already set in map, unknown field "metadata.unknownMeta", unknown field "spec.template.spec.containers[0].unknownNested", unknown field "spec.unknown1", unknown field "spec.unknownDupe"`,
},
{
name: "put-warn-validation-yaml",
opts: metav1.UpdateOptions{
FieldValidation: "Warn",
},
putBodyBase: invalidBodyYAML,
contentType: "application/yaml",
strictDecodingWarnings: []string{
`line 5: key "name" already set in map`,
`line 12: key "unknownDupe" already set in map`,
`line 14: key "paused" already set in map`,
`line 28: key "imagePullPolicy" already set in map`,
`unknown field "metadata.unknownMeta"`,
`unknown field "spec.template.spec.containers[0].unknownNested"`,
`unknown field "spec.unknown1"`,
`unknown field "spec.unknownDupe"`,
},
},
{
name: "put-ignore-validation-yaml",
opts: metav1.UpdateOptions{
FieldValidation: "Ignore",
},
putBodyBase: invalidBodyYAML,
contentType: "application/yaml",
},
{
name: "put-no-validation-yaml",
putBodyBase: invalidBodyYAML,
contentType: "application/yaml",
strictDecodingWarnings: []string{
`line 5: key "name" already set in map`,
`line 12: key "unknownDupe" already set in map`,
`line 14: key "paused" already set in map`,
`line 28: key "imagePullPolicy" already set in map`,
`unknown field "metadata.unknownMeta"`,
`unknown field "spec.template.spec.containers[0].unknownNested"`,
`unknown field "spec.unknown1"`,
`unknown field "spec.unknownDupe"`,
},
},
}
for _, tc := range testcases {
t.Run(tc.name, func(t *testing.T) {
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 && tc.strictDecodingError != "" {
t.Fatalf("received nil error when expecting: %q", tc.strictDecodingError)
}
if result.Error() != nil && (tc.strictDecodingError == "" || !strings.HasSuffix(result.Error().Error(), tc.strictDecodingError)) {
t.Fatalf("expected error: %q, got: %v", tc.strictDecodingError, result.Error())
}
if len(result.Warnings()) != len(tc.strictDecodingWarnings) {
t.Fatalf("unexpected number of warnings, expected: %d, got: %d", len(tc.strictDecodingWarnings), len(result.Warnings()))
}
for i, strictWarn := range tc.strictDecodingWarnings {
if strictWarn != result.Warnings()[i].Text {
t.Fatalf("expected warning: %s, got warning: %s", strictWarn, result.Warnings()[i].Text)
}
}
})
}
}
// testFieldValidationPatchTyped tests merge-patch and json-patch requests containing unknown fields with
// strict and non-strict field validation for typed objects.
func testFieldValidationPatchTyped(t *testing.T, client clientset.Interface) {
deployName := "test-deployment-patch-typed"
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 {
t.Fatalf("failed to create initial deployment: %v", err)
}
mergePatchBody := `
{
"spec": {
"unknown1": "val1",
"unknownDupe": "valDupe",
"unknownDupe": "valDupe2",
"paused": true,
"paused": false,
"template": {
"spec": {
"containers": [{
"name": "nginx",
"image": "nginx:latest",
"unknownNested": "val1",
"imagePullPolicy": "Always",
"imagePullPolicy": "Never"
}]
}
}
}
}
`
jsonPatchBody := `
[
{"op": "add", "path": "/spec/unknown1", "value": "val1", "foo":"bar"},
{"op": "add", "path": "/spec/unknown2", "path": "/spec/unknown3", "value": "val1"},
{"op": "add", "path": "/spec/unknownDupe", "value": "valDupe"},
{"op": "add", "path": "/spec/unknownDupe", "value": "valDupe2"},
{"op": "add", "path": "/spec/paused", "value": true},
{"op": "add", "path": "/spec/paused", "value": false},
{"op": "add", "path": "/spec/template/spec/containers/0/unknownNested", "value": "val1"},
{"op": "add", "path": "/spec/template/spec/containers/0/imagePullPolicy", "value": "Always"},
{"op": "add", "path": "/spec/template/spec/containers/0/imagePullPolicy", "value": "Never"}
]
`
// non-conflicting mergePatch has issues with the patch (duplicate fields),
// but doesn't conflict with the existing object it's being patched to
nonconflictingMergePatchBody := `
{
"spec": {
"paused": true,
"paused": false,
"template": {
"spec": {
"containers": [{
"name": "nginx",
"image": "nginx:latest",
"imagePullPolicy": "Always",
"imagePullPolicy": "Never"
}]
}
}
}
}
`
var testcases = []struct {
name string
opts metav1.PatchOptions
patchType types.PatchType
body string
strictDecodingError string
strictDecodingWarnings []string
}{
{
name: "merge-patch-strict-validation",
opts: metav1.PatchOptions{
FieldValidation: "Strict",
},
patchType: types.MergePatchType,
body: mergePatchBody,
strictDecodingError: `strict decoding error: duplicate field "spec.unknownDupe", duplicate field "spec.paused", duplicate field "spec.template.spec.containers[0].imagePullPolicy", unknown field "spec.template.spec.containers[0].unknownNested", unknown field "spec.unknown1", unknown field "spec.unknownDupe"`,
},
{
name: "merge-patch-warn-validation",
opts: metav1.PatchOptions{
FieldValidation: "Warn",
},
patchType: types.MergePatchType,
body: mergePatchBody,
strictDecodingWarnings: []string{
`duplicate field "spec.unknownDupe"`,
`duplicate field "spec.paused"`,
`duplicate field "spec.template.spec.containers[0].imagePullPolicy"`,
`unknown field "spec.template.spec.containers[0].unknownNested"`,
`unknown field "spec.unknown1"`,
`unknown field "spec.unknownDupe"`,
},
},
{
name: "merge-patch-ignore-validation",
opts: metav1.PatchOptions{
FieldValidation: "Ignore",
},
patchType: types.MergePatchType,
body: mergePatchBody,
},
{
name: "merge-patch-no-validation",
patchType: types.MergePatchType,
body: mergePatchBody,
strictDecodingWarnings: []string{
`duplicate field "spec.unknownDupe"`,
`duplicate field "spec.paused"`,
`duplicate field "spec.template.spec.containers[0].imagePullPolicy"`,
`unknown field "spec.template.spec.containers[0].unknownNested"`,
`unknown field "spec.unknown1"`,
`unknown field "spec.unknownDupe"`,
},
},
{
name: "json-patch-strict-validation",
patchType: types.JSONPatchType,
opts: metav1.PatchOptions{
FieldValidation: "Strict",
},
body: jsonPatchBody,
strictDecodingError: `strict decoding error: json patch unknown field "[0].foo", json patch duplicate field "[1].path", unknown field "spec.template.spec.containers[0].unknownNested", unknown field "spec.unknown1", unknown field "spec.unknown3", unknown field "spec.unknownDupe"`,
},
{
name: "json-patch-warn-validation",
patchType: types.JSONPatchType,
opts: metav1.PatchOptions{
FieldValidation: "Warn",
},
body: jsonPatchBody,
strictDecodingWarnings: []string{
// note: duplicate fields in the patch itself
// are dropped by the
// evanphx/json-patch library and is expected.
// Duplicate fields in the json patch ops
// themselves can be detected though
`json patch unknown field "[0].foo"`,
`json patch duplicate field "[1].path"`,
`unknown field "spec.template.spec.containers[0].unknownNested"`,
`unknown field "spec.unknown1"`,
`unknown field "spec.unknown3"`,
`unknown field "spec.unknownDupe"`,
},
},
{
name: "json-patch-ignore-validation",
patchType: types.JSONPatchType,
opts: metav1.PatchOptions{
FieldValidation: "Ignore",
},
body: jsonPatchBody,
},
{
name: "json-patch-no-validation",
patchType: types.JSONPatchType,
body: jsonPatchBody,
strictDecodingWarnings: []string{
// note: duplicate fields in the patch itself
// are dropped by the
// evanphx/json-patch library and is expected.
// Duplicate fields in the json patch ops
// themselves can be detected though
`json patch unknown field "[0].foo"`,
`json patch duplicate field "[1].path"`,
`unknown field "spec.template.spec.containers[0].unknownNested"`,
`unknown field "spec.unknown1"`,
`unknown field "spec.unknown3"`,
`unknown field "spec.unknownDupe"`,
},
},
{
name: "nonconflicting-merge-patch-strict-validation",
opts: metav1.PatchOptions{
FieldValidation: "Strict",
},
patchType: types.MergePatchType,
body: nonconflictingMergePatchBody,
strictDecodingError: `strict decoding error: duplicate field "spec.paused", duplicate field "spec.template.spec.containers[0].imagePullPolicy"`,
},
{
name: "nonconflicting-merge-patch-warn-validation",
opts: metav1.PatchOptions{
FieldValidation: "Warn",
},
patchType: types.MergePatchType,
body: nonconflictingMergePatchBody,
strictDecodingWarnings: []string{
`duplicate field "spec.paused"`,
`duplicate field "spec.template.spec.containers[0].imagePullPolicy"`,
},
},
{
name: "nonconflicting-merge-patch-ignore-validation",
opts: metav1.PatchOptions{
FieldValidation: "Ignore",
},
patchType: types.MergePatchType,
body: nonconflictingMergePatchBody,
},
{
name: "nonconflicting-merge-patch-no-validation",
patchType: types.MergePatchType,
body: nonconflictingMergePatchBody,
strictDecodingWarnings: []string{
`duplicate field "spec.paused"`,
`duplicate field "spec.template.spec.containers[0].imagePullPolicy"`,
},
},
}
for _, tc := range testcases {
t.Run(tc.name, func(t *testing.T) {
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 && tc.strictDecodingError != "" {
t.Fatalf("received nil error when expecting: %q", tc.strictDecodingError)
}
if result.Error() != nil && (tc.strictDecodingError == "" || !strings.HasSuffix(result.Error().Error(), tc.strictDecodingError)) {
t.Fatalf("expected error: %q, got: %v", tc.strictDecodingError, result.Error())
}
if len(result.Warnings()) != len(tc.strictDecodingWarnings) {
t.Fatalf("unexpected number of warnings, expected: %d, got: %d", len(tc.strictDecodingWarnings), len(result.Warnings()))
}
for i, strictWarn := range tc.strictDecodingWarnings {
if strictWarn != result.Warnings()[i].Text {
t.Fatalf("expected warning: %s, got warning: %s", strictWarn, result.Warnings()[i].Text)
}
}
})
}
}
// testFieldValidationSMP tests that attempting a strategic-merge-patch
// 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": {
"unknown1": "val1",
"unknownDupe": "valDupe",
"unknownDupe": "valDupe2",
"paused": true,
"paused": false,
"selector": {
"matchLabels": {
"app": "nginx"
}
},
"template": {
"metadata": {
"labels": {
"app": "nginx"
}
},
"spec": {
"containers": [{
"name": "nginx",
"unknownNested": "val1",
"imagePullPolicy": "Always",
"imagePullPolicy": "Never"
}]
}
}
}
}
`
var testcases = []struct {
name string
opts metav1.PatchOptions
body string
strictDecodingError string
strictDecodingWarnings []string
}{
{
name: "smp-strict-validation",
opts: metav1.PatchOptions{
FieldValidation: "Strict",
},
body: smpBody,
strictDecodingError: `strict decoding error: duplicate field "spec.unknownDupe", duplicate field "spec.paused", duplicate field "spec.template.spec.containers[0].imagePullPolicy", unknown field "spec.template.spec.containers[0].unknownNested", unknown field "spec.unknown1", unknown field "spec.unknownDupe"`,
},
{
name: "smp-warn-validation",
opts: metav1.PatchOptions{
FieldValidation: "Warn",
},
body: smpBody,
strictDecodingWarnings: []string{
`duplicate field "spec.unknownDupe"`,
`duplicate field "spec.paused"`,
`duplicate field "spec.template.spec.containers[0].imagePullPolicy"`,
`unknown field "spec.template.spec.containers[0].unknownNested"`,
`unknown field "spec.unknown1"`,
`unknown field "spec.unknownDupe"`,
},
},
{
name: "smp-ignore-validation",
opts: metav1.PatchOptions{
FieldValidation: "Ignore",
},
body: smpBody,
},
{
name: "smp-no-validation",
body: smpBody,
strictDecodingWarnings: []string{
`duplicate field "spec.unknownDupe"`,
`duplicate field "spec.paused"`,
`duplicate field "spec.template.spec.containers[0].imagePullPolicy"`,
`unknown field "spec.template.spec.containers[0].unknownNested"`,
`unknown field "spec.unknown1"`,
`unknown field "spec.unknownDupe"`,
},
},
{
name: "nonconflicting-smp-strict-validation",
opts: metav1.PatchOptions{
FieldValidation: "Strict",
},
body: nonconflictingSMPBody,
strictDecodingError: `strict decoding error: duplicate field "spec.paused", duplicate field "spec.template.spec.containers[0].imagePullPolicy"`,
},
{
name: "nonconflicting-smp-warn-validation",
opts: metav1.PatchOptions{
FieldValidation: "Warn",
},
body: nonconflictingSMPBody,
strictDecodingWarnings: []string{
`duplicate field "spec.paused"`,
`duplicate field "spec.template.spec.containers[0].imagePullPolicy"`,
},
},
{
name: "nonconflicting-smp-ignore-validation",
opts: metav1.PatchOptions{
FieldValidation: "Ignore",
},
body: nonconflictingSMPBody,
},
{
name: "nonconflicting-smp-no-validation",
body: nonconflictingSMPBody,
strictDecodingWarnings: []string{
`duplicate field "spec.paused"`,
`duplicate field "spec.template.spec.containers[0].imagePullPolicy"`,
},
},
}
for _, tc := range testcases {
t.Run(tc.name, func(t *testing.T) {
body := []byte(fmt.Sprintf(validBodyJSON, tc.name))
_, err := client.CoreV1().RESTClient().Patch(types.ApplyPatchType).
AbsPath("/apis/apps/v1").
Namespace("default").
Resource("deployments").
Name(tc.name).
Param("fieldManager", "apply_test").
Body(body).
Do(context.TODO()).
Get()
if err != nil {
t.Fatalf("Failed to create object using Apply patch: %v", err)
}
req := client.CoreV1().RESTClient().Patch(types.StrategicMergePatchType).
AbsPath("/apis/apps/v1").
Namespace("default").
Resource("deployments").
Name(tc.name).
VersionedParams(&tc.opts, metav1.ParameterCodec)
result := req.Body([]byte(tc.body)).Do(context.TODO())
if result.Error() == nil && tc.strictDecodingError != "" {
t.Fatalf("received nil error when expecting: %q", tc.strictDecodingError)
}
if result.Error() != nil && (tc.strictDecodingError == "" || !strings.HasSuffix(result.Error().Error(), tc.strictDecodingError)) {
t.Fatalf("expected error: %q, got: %v", tc.strictDecodingError, result.Error())
}
if len(result.Warnings()) != len(tc.strictDecodingWarnings) {
t.Fatalf("unexpected number of warnings, expected: %d, got: %d", len(tc.strictDecodingWarnings), len(result.Warnings()))
}
for i, strictWarn := range tc.strictDecodingWarnings {
if strictWarn != result.Warnings()[i].Text {
t.Fatalf("expected warning: %s, got warning: %s", strictWarn, result.Warnings()[i].Text)
}
}
})
}
}
// testFieldValidationApplyCreate tests apply patch requests containing unknown fields
// on newly created objects, with strict and non-strict field validation.
func testFieldValidationApplyCreate(t *testing.T, client clientset.Interface) {
var testcases = []struct {
name string
opts metav1.PatchOptions
strictDecodingError string
strictDecodingWarnings []string
}{
{
name: "strict-validation",
opts: metav1.PatchOptions{
FieldValidation: "Strict",
FieldManager: "mgr",
},
strictDecodingError: `error strict decoding YAML: error converting YAML to JSON: yaml: unmarshal errors:
line 10: key "paused" already set in map
line 27: key "imagePullPolicy" already set in map`,
},
{
name: "warn-validation",
opts: metav1.PatchOptions{
FieldValidation: "Warn",
FieldManager: "mgr",
},
strictDecodingWarnings: []string{
`line 10: key "paused" already set in map`,
`line 27: key "imagePullPolicy" already set in map`,
},
},
{
name: "ignore-validation",
opts: metav1.PatchOptions{
FieldValidation: "Ignore",
FieldManager: "mgr",
},
},
{
name: "no-validation",
opts: metav1.PatchOptions{
FieldManager: "mgr",
},
strictDecodingWarnings: []string{
`line 10: key "paused" already set in map`,
`line 27: key "imagePullPolicy" already set in map`,
},
},
}
for _, tc := range testcases {
t.Run(tc.name, func(t *testing.T) {
name := fmt.Sprintf("apply-create-deployment-%s", tc.name)
body := []byte(fmt.Sprintf(applyInvalidBody, 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 && tc.strictDecodingError != "" {
t.Fatalf("received nil error when expecting: %q", tc.strictDecodingError)
}
if result.Error() != nil && (tc.strictDecodingError == "" || !strings.HasSuffix(result.Error().Error(), tc.strictDecodingError)) {
t.Fatalf("expected error: %q, got: %v", tc.strictDecodingError, result.Error())
}
if len(result.Warnings()) != len(tc.strictDecodingWarnings) {
t.Fatalf("unexpected number of warnings, expected: %d, got: %d", len(tc.strictDecodingWarnings), len(result.Warnings()))
}
for i, strictWarn := range tc.strictDecodingWarnings {
if strictWarn != result.Warnings()[i].Text {
t.Fatalf("expected warning: %s, got warning: %s", strictWarn, result.Warnings()[i].Text)
}
}
})
}
}
// testFieldValidationApplyUpdate tests apply patch requests containing unknown fields
// on apply requests to existing objects, with strict and non-strict field validation.
func testFieldValidationApplyUpdate(t *testing.T, client clientset.Interface) {
var testcases = []struct {
name string
opts metav1.PatchOptions
strictDecodingError string
strictDecodingWarnings []string
}{
{
name: "strict-validation",
opts: metav1.PatchOptions{
FieldValidation: "Strict",
FieldManager: "mgr",
},
strictDecodingError: `error strict decoding YAML: error converting YAML to JSON: yaml: unmarshal errors:
line 10: key "paused" already set in map
line 27: key "imagePullPolicy" already set in map`,
},
{
name: "warn-validation",
opts: metav1.PatchOptions{
FieldValidation: "Warn",
FieldManager: "mgr",
},
strictDecodingWarnings: []string{
`line 10: key "paused" already set in map`,
`line 27: key "imagePullPolicy" already set in map`,
},
},
{
name: "ignore-validation",
opts: metav1.PatchOptions{
FieldValidation: "Ignore",
FieldManager: "mgr",
},
},
{
name: "no-validation",
opts: metav1.PatchOptions{
FieldManager: "mgr",
},
strictDecodingWarnings: []string{
`line 10: key "paused" already set in map`,
`line 27: key "imagePullPolicy" already set in map`,
},
},
}
for _, tc := range testcases {
t.Run(tc.name, func(t *testing.T) {
name := fmt.Sprintf("apply-update-deployment-%s", tc.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 {
t.Fatalf("unexpected apply create err: %v", createResult.Error())
}
updateBody := []byte(fmt.Sprintf(applyInvalidBody, 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 && tc.strictDecodingError != "" {
t.Fatalf("received nil error when expecting: %q", tc.strictDecodingError)
}
if result.Error() != nil && (tc.strictDecodingError == "" || !strings.HasSuffix(result.Error().Error(), tc.strictDecodingError)) {
t.Fatalf("expected error: %q, got: %v", tc.strictDecodingError, result.Error())
}
if len(result.Warnings()) != len(tc.strictDecodingWarnings) {
t.Fatalf("unexpected number of warnings, expected: %d, got: %d", len(tc.strictDecodingWarnings), len(result.Warnings()))
}
for i, strictWarn := range tc.strictDecodingWarnings {
if strictWarn != result.Warnings()[i].Text {
t.Fatalf("expected warning: %s, got warning: %s", strictWarn, result.Warnings()[i].Text)
}
}
})
}
}
// testFieldValidationPostCRD tests that server-side schema validation
// works for CRD create requests for CRDs with schemas
func testFieldValidationPostCRD(t *testing.T, rest rest.Interface, gvk schema.GroupVersionKind, gvr schema.GroupVersionResource) {
var testcases = []struct {
name string
opts metav1.PatchOptions
body string
contentType string
strictDecodingError string
strictDecodingWarnings []string
}{
{
name: "crd-post-strict-validation",
opts: metav1.PatchOptions{
FieldValidation: "Strict",
},
body: crdInvalidBody,
strictDecodingError: `strict decoding error: duplicate field "metadata.name", duplicate field "spec.unknownDupe", duplicate field "spec.knownField1", duplicate field "spec.ports[0].hostPort", unknown field "metadata.unknownMeta", unknown field "spec.ports[0].unknownNested", unknown field "spec.unknown1", unknown field "spec.unknownDupe", unknown field "spec.embeddedObj.metadata.unknownEmbeddedMeta"`,
},
{
name: "crd-post-warn-validation",
opts: metav1.PatchOptions{
FieldValidation: "Warn",
},
body: crdInvalidBody,
strictDecodingWarnings: []string{
`duplicate field "metadata.name"`,
`duplicate field "spec.unknownDupe"`,
`duplicate field "spec.knownField1"`,
`duplicate field "spec.ports[0].hostPort"`,
`unknown field "metadata.unknownMeta"`,
`unknown field "spec.ports[0].unknownNested"`,
`unknown field "spec.unknown1"`,
`unknown field "spec.unknownDupe"`,
`unknown field "spec.embeddedObj.metadata.unknownEmbeddedMeta"`,
},
},
{
name: "crd-post-ignore-validation",
opts: metav1.PatchOptions{
FieldValidation: "Ignore",
},
body: crdInvalidBody,
},
{
name: "crd-post-no-validation",
body: crdInvalidBody,
strictDecodingWarnings: []string{
`duplicate field "metadata.name"`,
`duplicate field "spec.unknownDupe"`,
`duplicate field "spec.knownField1"`,
`duplicate field "spec.ports[0].hostPort"`,
`unknown field "metadata.unknownMeta"`,
`unknown field "spec.ports[0].unknownNested"`,
`unknown field "spec.unknown1"`,
`unknown field "spec.unknownDupe"`,
`unknown field "spec.embeddedObj.metadata.unknownEmbeddedMeta"`,
},
},
{
name: "crd-post-strict-validation-yaml",
opts: metav1.PatchOptions{
FieldValidation: "Strict",
},
body: crdInvalidBodyYAML,
contentType: "application/yaml",
strictDecodingError: `strict decoding error: yaml: unmarshal errors:
line 6: key "name" already set in map
line 12: key "unknownDupe" already set in map
line 14: key "knownField1" already set in map
line 20: key "hostPort" already set in map, unknown field "metadata.unknownMeta", unknown field "spec.ports[0].unknownNested", unknown field "spec.unknown1", unknown field "spec.unknownDupe", unknown field "spec.embeddedObj.metadata.unknownEmbeddedMeta"`,
},
{
name: "crd-post-warn-validation-yaml",
opts: metav1.PatchOptions{
FieldValidation: "Warn",
},
body: crdInvalidBodyYAML,
contentType: "application/yaml",
strictDecodingWarnings: []string{
`line 6: key "name" already set in map`,
`line 12: key "unknownDupe" already set in map`,
`line 14: key "knownField1" already set in map`,
`line 20: key "hostPort" already set in map`,
`unknown field "metadata.unknownMeta"`,
`unknown field "spec.ports[0].unknownNested"`,
`unknown field "spec.unknown1"`,
`unknown field "spec.unknownDupe"`,
`unknown field "spec.embeddedObj.metadata.unknownEmbeddedMeta"`,
},
},
{
name: "crd-post-ignore-validation-yaml",
opts: metav1.PatchOptions{
FieldValidation: "Ignore",
},
body: crdInvalidBodyYAML,
contentType: "application/yaml",
},
{
name: "crd-post-no-validation-yaml",
body: crdInvalidBodyYAML,
contentType: "application/yaml",
strictDecodingWarnings: []string{
`line 6: key "name" already set in map`,
`line 12: key "unknownDupe" already set in map`,
`line 14: key "knownField1" already set in map`,
`line 20: key "hostPort" already set in map`,
`unknown field "metadata.unknownMeta"`,
`unknown field "spec.ports[0].unknownNested"`,
`unknown field "spec.unknown1"`,
`unknown field "spec.unknownDupe"`,
`unknown field "spec.embeddedObj.metadata.unknownEmbeddedMeta"`,
},
},
}
for _, tc := range testcases {
t.Run(tc.name, func(t *testing.T) {
klog.Warningf("running tc named: %s", tc.name)
kind := gvk.Kind
apiVersion := gvk.Group + "/" + gvk.Version
// create the CR as specified by the test case
jsonBody := []byte(fmt.Sprintf(tc.body, apiVersion, kind, tc.name))
req := rest.Post().
AbsPath("/apis", gvr.Group, gvr.Version, gvr.Resource).
SetHeader("Content-Type", tc.contentType).
VersionedParams(&tc.opts, metav1.ParameterCodec)
result := req.Body([]byte(jsonBody)).Do(context.TODO())
if result.Error() == nil && tc.strictDecodingError != "" {
t.Fatalf("received nil error when expecting: %q", tc.strictDecodingError)
}
if result.Error() != nil && (tc.strictDecodingError == "" || !strings.HasSuffix(result.Error().Error(), tc.strictDecodingError)) {
t.Fatalf("expected error: %q, got: %v", tc.strictDecodingError, result.Error())
}
if len(result.Warnings()) != len(tc.strictDecodingWarnings) {
t.Fatalf("unexpected number of warnings, expected: %d, got: %d", len(tc.strictDecodingWarnings), len(result.Warnings()))
}
for i, strictWarn := range tc.strictDecodingWarnings {
if strictWarn != result.Warnings()[i].Text {
t.Fatalf("expected warning: %s, got warning: %s", strictWarn, result.Warnings()[i].Text)
}
}
})
}
}
// testFieldValidationPostCRDSchemaless tests that server-side schema validation
// works for CRD create requests for CRDs that have schemas
// with x-kubernetes-preserve-unknown-field set
func testFieldValidationPostCRDSchemaless(t *testing.T, rest rest.Interface, gvk schema.GroupVersionKind, gvr schema.GroupVersionResource) {
var testcases = []struct {
name string
opts metav1.PatchOptions
body string
contentType string
strictDecodingError string
strictDecodingWarnings []string
}{
{
name: "schemaless-crd-post-strict-validation",
opts: metav1.PatchOptions{
FieldValidation: "Strict",
},
body: crdInvalidBody,
strictDecodingError: `strict decoding error: duplicate field "metadata.name", duplicate field "spec.unknownDupe", duplicate field "spec.knownField1", duplicate field "spec.ports[0].hostPort", unknown field "metadata.unknownMeta", unknown field "spec.ports[0].unknownNested", unknown field "spec.embeddedObj.metadata.unknownEmbeddedMeta"`,
},
{
name: "schemaless-crd-post-warn-validation",
opts: metav1.PatchOptions{
FieldValidation: "Warn",
},
body: crdInvalidBody,
strictDecodingWarnings: []string{
`duplicate field "metadata.name"`,
`duplicate field "spec.unknownDupe"`,
`duplicate field "spec.knownField1"`,
`duplicate field "spec.ports[0].hostPort"`,
`unknown field "metadata.unknownMeta"`,
`unknown field "spec.ports[0].unknownNested"`,
`unknown field "spec.embeddedObj.metadata.unknownEmbeddedMeta"`,
},
},
{
name: "schemaless-crd-post-ignore-validation",
opts: metav1.PatchOptions{
FieldValidation: "Ignore",
},
body: crdInvalidBody,
},
{
name: "schemaless-crd-post-no-validation",
body: crdInvalidBody,
strictDecodingWarnings: []string{
`duplicate field "metadata.name"`,
`duplicate field "spec.unknownDupe"`,
`duplicate field "spec.knownField1"`,
`duplicate field "spec.ports[0].hostPort"`,
`unknown field "metadata.unknownMeta"`,
`unknown field "spec.ports[0].unknownNested"`,
`unknown field "spec.embeddedObj.metadata.unknownEmbeddedMeta"`,
},
},
{
name: "schemaless-crd-post-strict-validation-yaml",
opts: metav1.PatchOptions{
FieldValidation: "Strict",
},
body: crdInvalidBodyYAML,
contentType: "application/yaml",
strictDecodingError: `strict decoding error: yaml: unmarshal errors:
line 6: key "name" already set in map
line 12: key "unknownDupe" already set in map
line 14: key "knownField1" already set in map
line 20: key "hostPort" already set in map, unknown field "metadata.unknownMeta", unknown field "spec.ports[0].unknownNested", unknown field "spec.embeddedObj.metadata.unknownEmbeddedMeta"`,
},
{
name: "schemaless-crd-post-warn-validation-yaml",
opts: metav1.PatchOptions{
FieldValidation: "Warn",
},
body: crdInvalidBodyYAML,
contentType: "application/yaml",
strictDecodingWarnings: []string{
`line 6: key "name" already set in map`,
`line 12: key "unknownDupe" already set in map`,
`line 14: key "knownField1" already set in map`,
`line 20: key "hostPort" already set in map`,
`unknown field "metadata.unknownMeta"`,
`unknown field "spec.ports[0].unknownNested"`,
`unknown field "spec.embeddedObj.metadata.unknownEmbeddedMeta"`,
},
},
{
name: "schemaless-crd-post-ignore-validation-yaml",
opts: metav1.PatchOptions{
FieldValidation: "Ignore",
},
body: crdInvalidBodyYAML,
contentType: "application/yaml",
},
{
name: "schemaless-crd-post-no-validation-yaml",
body: crdInvalidBodyYAML,
contentType: "application/yaml",
strictDecodingWarnings: []string{
`line 6: key "name" already set in map`,
`line 12: key "unknownDupe" already set in map`,
`line 14: key "knownField1" already set in map`,
`line 20: key "hostPort" already set in map`,
`unknown field "metadata.unknownMeta"`,
`unknown field "spec.ports[0].unknownNested"`,
`unknown field "spec.embeddedObj.metadata.unknownEmbeddedMeta"`,
},
},
}
for _, tc := range testcases {
t.Run(tc.name, func(t *testing.T) {
kind := gvk.Kind
apiVersion := gvk.Group + "/" + gvk.Version
// create the CR as specified by the test case
jsonBody := []byte(fmt.Sprintf(tc.body, apiVersion, kind, tc.name))
req := rest.Post().
AbsPath("/apis", gvr.Group, gvr.Version, gvr.Resource).
SetHeader("Content-Type", tc.contentType).
VersionedParams(&tc.opts, metav1.ParameterCodec)
result := req.Body([]byte(jsonBody)).Do(context.TODO())
if result.Error() == nil && tc.strictDecodingError != "" {
t.Fatalf("received nil error when expecting: %q", tc.strictDecodingError)
}
if result.Error() != nil && (tc.strictDecodingError == "" || !strings.HasSuffix(result.Error().Error(), tc.strictDecodingError)) {
t.Fatalf("expected error: %q, got: %v", tc.strictDecodingError, result.Error())
}
if len(result.Warnings()) != len(tc.strictDecodingWarnings) {
t.Logf("expected:")
for _, w := range tc.strictDecodingWarnings {
t.Logf("\t%v", w)
}
t.Logf("got:")
for _, w := range result.Warnings() {
t.Logf("\t%v", w.Text)
}
t.Fatalf("unexpected number of warnings, expected: %d, got: %d", len(tc.strictDecodingWarnings), len(result.Warnings()))
}
for i, strictWarn := range tc.strictDecodingWarnings {
if strictWarn != result.Warnings()[i].Text {
t.Fatalf("expected warning: %s, got warning: %s", strictWarn, result.Warnings()[i].Text)
}
}
})
}
}
// testFieldValidationPutCRD tests that server-side schema validation
// works for CRD update requests for CRDs with schemas.
func testFieldValidationPutCRD(t *testing.T, rest rest.Interface, gvk schema.GroupVersionKind, gvr schema.GroupVersionResource) {
var testcases = []struct {
name string
opts metav1.PatchOptions
putBody string
contentType string
strictDecodingError string
strictDecodingWarnings []string
}{
{
name: "crd-put-strict-validation",
opts: metav1.PatchOptions{
FieldValidation: "Strict",
},
putBody: crdInvalidBody,
strictDecodingError: `strict decoding error: duplicate field "metadata.name", duplicate field "spec.unknownDupe", duplicate field "spec.knownField1", duplicate field "spec.ports[0].hostPort", unknown field "metadata.unknownMeta", unknown field "spec.ports[0].unknownNested", unknown field "spec.unknown1", unknown field "spec.unknownDupe", unknown field "spec.embeddedObj.metadata.unknownEmbeddedMeta"`,
},
{
name: "crd-put-warn-validation",
opts: metav1.PatchOptions{
FieldValidation: "Warn",
},
putBody: crdInvalidBody,
strictDecodingWarnings: []string{
`duplicate field "metadata.name"`,
`duplicate field "spec.unknownDupe"`,
`duplicate field "spec.knownField1"`,
`duplicate field "spec.ports[0].hostPort"`,
`unknown field "metadata.unknownMeta"`,
`unknown field "spec.ports[0].unknownNested"`,
`unknown field "spec.unknown1"`,
`unknown field "spec.unknownDupe"`,
`unknown field "spec.embeddedObj.metadata.unknownEmbeddedMeta"`,
},
},
{
name: "crd-put-ignore-validation",
opts: metav1.PatchOptions{
FieldValidation: "Ignore",
},
putBody: crdInvalidBody,
},
{
name: "crd-put-no-validation",
putBody: crdInvalidBody,
strictDecodingWarnings: []string{
`duplicate field "metadata.name"`,
`duplicate field "spec.unknownDupe"`,
`duplicate field "spec.knownField1"`,
`duplicate field "spec.ports[0].hostPort"`,
`unknown field "metadata.unknownMeta"`,
`unknown field "spec.ports[0].unknownNested"`,
`unknown field "spec.unknown1"`,
`unknown field "spec.unknownDupe"`,
`unknown field "spec.embeddedObj.metadata.unknownEmbeddedMeta"`,
},
},
{
name: "crd-put-strict-validation-yaml",
opts: metav1.PatchOptions{
FieldValidation: "Strict",
},
putBody: crdInvalidBodyYAML,
contentType: "application/yaml",
strictDecodingError: `strict decoding error: yaml: unmarshal errors:
line 6: key "name" already set in map
line 12: key "unknownDupe" already set in map
line 14: key "knownField1" already set in map
line 20: key "hostPort" already set in map, unknown field "metadata.unknownMeta", unknown field "spec.ports[0].unknownNested", unknown field "spec.unknown1", unknown field "spec.unknownDupe", unknown field "spec.embeddedObj.metadata.unknownEmbeddedMeta"`,
},
{
name: "crd-put-warn-validation-yaml",
opts: metav1.PatchOptions{
FieldValidation: "Warn",
},
putBody: crdInvalidBodyYAML,
contentType: "application/yaml",
strictDecodingWarnings: []string{
`line 6: key "name" already set in map`,
`line 12: key "unknownDupe" already set in map`,
`line 14: key "knownField1" already set in map`,
`line 20: key "hostPort" already set in map`,
`unknown field "metadata.unknownMeta"`,
`unknown field "spec.ports[0].unknownNested"`,
`unknown field "spec.unknown1"`,
`unknown field "spec.unknownDupe"`,
`unknown field "spec.embeddedObj.metadata.unknownEmbeddedMeta"`,
},
},
{
name: "crd-put-ignore-validation-yaml",
opts: metav1.PatchOptions{
FieldValidation: "Ignore",
},
putBody: crdInvalidBodyYAML,
contentType: "application/yaml",
},
{
name: "crd-put-no-validation-yaml",
putBody: crdInvalidBodyYAML,
contentType: "application/yaml",
strictDecodingWarnings: []string{
`line 6: key "name" already set in map`,
`line 12: key "unknownDupe" already set in map`,
`line 14: key "knownField1" already set in map`,
`line 20: key "hostPort" already set in map`,
`unknown field "metadata.unknownMeta"`,
`unknown field "spec.ports[0].unknownNested"`,
`unknown field "spec.unknown1"`,
`unknown field "spec.unknownDupe"`,
`unknown field "spec.embeddedObj.metadata.unknownEmbeddedMeta"`,
},
},
}
for _, tc := range testcases {
t.Run(tc.name, func(t *testing.T) {
kind := gvk.Kind
apiVersion := gvk.Group + "/" + gvk.Version
// create the CR as specified by the test case
jsonPostBody := []byte(fmt.Sprintf(crdValidBody, apiVersion, kind, tc.name))
postReq := rest.Post().
AbsPath("/apis", gvr.Group, gvr.Version, gvr.Resource).
VersionedParams(&tc.opts, metav1.ParameterCodec)
postResult, err := postReq.Body([]byte(jsonPostBody)).Do(context.TODO()).Raw()
if err != nil {
t.Fatalf("unexpeted error on CR creation: %v", err)
}
postUnstructured := &unstructured.Unstructured{}
if err := postUnstructured.UnmarshalJSON(postResult); err != nil {
t.Fatalf("unexpeted error unmarshalling created CR: %v", err)
}
// update the CR as specified by the test case
putBody := []byte(fmt.Sprintf(tc.putBody, apiVersion, kind, tc.name, postUnstructured.GetResourceVersion()))
putReq := rest.Put().
AbsPath("/apis", gvr.Group, gvr.Version, gvr.Resource).
Name(tc.name).
SetHeader("Content-Type", tc.contentType).
VersionedParams(&tc.opts, metav1.ParameterCodec)
result := putReq.Body([]byte(putBody)).Do(context.TODO())
if result.Error() == nil && tc.strictDecodingError != "" {
t.Fatalf("received nil error when expecting: %q", tc.strictDecodingError)
}
if result.Error() != nil && (tc.strictDecodingError == "" || !strings.HasSuffix(result.Error().Error(), tc.strictDecodingError)) {
t.Fatalf("expected error: %q, got: %v", tc.strictDecodingError, result.Error())
}
if len(result.Warnings()) != len(tc.strictDecodingWarnings) {
t.Fatalf("unexpected number of warnings, expected: %d, got: %d", len(tc.strictDecodingWarnings), len(result.Warnings()))
}
for i, strictWarn := range tc.strictDecodingWarnings {
if strictWarn != result.Warnings()[i].Text {
t.Fatalf("expected warning: %s, got warning: %s", strictWarn, result.Warnings()[i].Text)
}
}
})
}
}
// testFieldValidationPutCRDSchemaless tests that server-side schema validation
// works for CRD update requests for CRDs that have schemas
// with x-kubernetes-preserve-unknown-field set
func testFieldValidationPutCRDSchemaless(t *testing.T, rest rest.Interface, gvk schema.GroupVersionKind, gvr schema.GroupVersionResource) {
var testcases = []struct {
name string
opts metav1.PatchOptions
putBody string
contentType string
strictDecodingError string
strictDecodingWarnings []string
}{
{
name: "schemaless-crd-put-strict-validation",
opts: metav1.PatchOptions{
FieldValidation: "Strict",
},
putBody: crdInvalidBody,
strictDecodingError: `strict decoding error: duplicate field "metadata.name", duplicate field "spec.unknownDupe", duplicate field "spec.knownField1", duplicate field "spec.ports[0].hostPort", unknown field "metadata.unknownMeta", unknown field "spec.ports[0].unknownNested", unknown field "spec.embeddedObj.metadata.unknownEmbeddedMeta"`,
},
{
name: "schemaless-crd-put-warn-validation",
opts: metav1.PatchOptions{
FieldValidation: "Warn",
},
putBody: crdInvalidBody,
strictDecodingWarnings: []string{
`duplicate field "metadata.name"`,
`duplicate field "spec.unknownDupe"`,
`duplicate field "spec.knownField1"`,
`duplicate field "spec.ports[0].hostPort"`,
`unknown field "metadata.unknownMeta"`,
`unknown field "spec.ports[0].unknownNested"`,
`unknown field "spec.embeddedObj.metadata.unknownEmbeddedMeta"`,
},
},
{
name: "schemaless-crd-put-ignore-validation",
opts: metav1.PatchOptions{
FieldValidation: "Ignore",
},
putBody: crdInvalidBody,
},
{
name: "schemaless-crd-put-no-validation",
putBody: crdInvalidBody,
strictDecodingWarnings: []string{
`duplicate field "metadata.name"`,
`duplicate field "spec.unknownDupe"`,
`duplicate field "spec.knownField1"`,
`duplicate field "spec.ports[0].hostPort"`,
`unknown field "metadata.unknownMeta"`,
`unknown field "spec.ports[0].unknownNested"`,
`unknown field "spec.embeddedObj.metadata.unknownEmbeddedMeta"`,
},
},
{
name: "schemaless-crd-put-strict-validation-yaml",
opts: metav1.PatchOptions{
FieldValidation: "Strict",
},
putBody: crdInvalidBodyYAML,
contentType: "application/yaml",
strictDecodingError: `strict decoding error: yaml: unmarshal errors:
line 6: key "name" already set in map
line 12: key "unknownDupe" already set in map
line 14: key "knownField1" already set in map
line 20: key "hostPort" already set in map, unknown field "metadata.unknownMeta", unknown field "spec.ports[0].unknownNested", unknown field "spec.embeddedObj.metadata.unknownEmbeddedMeta"`,
},
{
name: "schemaless-crd-put-warn-validation-yaml",
opts: metav1.PatchOptions{
FieldValidation: "Warn",
},
putBody: crdInvalidBodyYAML,
contentType: "application/yaml",
strictDecodingWarnings: []string{
`line 6: key "name" already set in map`,
`line 12: key "unknownDupe" already set in map`,
`line 14: key "knownField1" already set in map`,
`line 20: key "hostPort" already set in map`,
`unknown field "metadata.unknownMeta"`,
`unknown field "spec.ports[0].unknownNested"`,
`unknown field "spec.embeddedObj.metadata.unknownEmbeddedMeta"`,
},
},
{
name: "schemaless-crd-put-ignore-validation-yaml",
opts: metav1.PatchOptions{
FieldValidation: "Ignore",
},
putBody: crdInvalidBodyYAML,
contentType: "application/yaml",
},
{
name: "schemaless-crd-put-no-validation-yaml",
putBody: crdInvalidBodyYAML,
contentType: "application/yaml",
strictDecodingWarnings: []string{
`line 6: key "name" already set in map`,
`line 12: key "unknownDupe" already set in map`,
`line 14: key "knownField1" already set in map`,
`line 20: key "hostPort" already set in map`,
`unknown field "metadata.unknownMeta"`,
`unknown field "spec.ports[0].unknownNested"`,
`unknown field "spec.embeddedObj.metadata.unknownEmbeddedMeta"`,
},
},
}
for _, tc := range testcases {
t.Run(tc.name, func(t *testing.T) {
kind := gvk.Kind
apiVersion := gvk.Group + "/" + gvk.Version
// create the CR as specified by the test case
jsonPostBody := []byte(fmt.Sprintf(crdValidBody, apiVersion, kind, tc.name))
postReq := rest.Post().
AbsPath("/apis", gvr.Group, gvr.Version, gvr.Resource).
VersionedParams(&tc.opts, metav1.ParameterCodec)
postResult, err := postReq.Body([]byte(jsonPostBody)).Do(context.TODO()).Raw()
if err != nil {
t.Fatalf("unexpeted error on CR creation: %v", err)
}
postUnstructured := &unstructured.Unstructured{}
if err := postUnstructured.UnmarshalJSON(postResult); err != nil {
t.Fatalf("unexpeted error unmarshalling created CR: %v", err)
}
// update the CR as specified by the test case
putBody := []byte(fmt.Sprintf(tc.putBody, apiVersion, kind, tc.name, postUnstructured.GetResourceVersion()))
putReq := rest.Put().
AbsPath("/apis", gvr.Group, gvr.Version, gvr.Resource).
Name(tc.name).
SetHeader("Content-Type", tc.contentType).
VersionedParams(&tc.opts, metav1.ParameterCodec)
result := putReq.Body([]byte(putBody)).Do(context.TODO())
if result.Error() == nil && tc.strictDecodingError != "" {
t.Fatalf("received nil error when expecting: %q", tc.strictDecodingError)
}
if result.Error() != nil && (tc.strictDecodingError == "" || !strings.HasSuffix(result.Error().Error(), tc.strictDecodingError)) {
t.Fatalf("expected error: %q, got: %v", tc.strictDecodingError, result.Error())
}
if len(result.Warnings()) != len(tc.strictDecodingWarnings) {
t.Logf("expected:")
for _, w := range tc.strictDecodingWarnings {
t.Logf("\t%v", w)
}
t.Logf("got:")
for _, w := range result.Warnings() {
t.Logf("\t%v", w.Text)
}
t.Fatalf("unexpected number of warnings, expected: %d, got: %d", len(tc.strictDecodingWarnings), len(result.Warnings()))
}
for i, strictWarn := range tc.strictDecodingWarnings {
if strictWarn != result.Warnings()[i].Text {
t.Fatalf("expected warning: %s, got warning: %s", strictWarn, result.Warnings()[i].Text)
}
}
})
}
}
// testFieldValidationPatchCRD tests that server-side schema validation
// works for jsonpatch and mergepatch requests
// for custom resources that have schemas.
func testFieldValidationPatchCRD(t *testing.T, rest rest.Interface, gvk schema.GroupVersionKind, gvr schema.GroupVersionResource) {
patchYAMLBody := `
apiVersion: %s
kind: %s
metadata:
name: %s
finalizers:
- test-finalizer
spec:
cronSpec: "* * * * */5"
ports:
- name: x
containerPort: 80
protocol: TCP`
mergePatchBody := `
{
"spec": {
"unknown1": "val1",
"unknownDupe": "valDupe",
"unknownDupe": "valDupe2",
"knownField1": "val1",
"knownField1": "val2",
"ports": [{
"name": "portName",
"containerPort": 8080,
"protocol": "TCP",
"hostPort": 8081,
"hostPort": 8082,
"unknownNested": "val"
}]
}
}
`
jsonPatchBody := `
[
{"op": "add", "path": "/spec/unknown1", "value": "val1", "foo": "bar"},
{"op": "add", "path": "/spec/unknown2", "path": "/spec/unknown3", "value": "val2"},
{"op": "add", "path": "/spec/unknownDupe", "value": "valDupe"},
{"op": "add", "path": "/spec/unknownDupe", "value": "valDupe2"},
{"op": "add", "path": "/spec/knownField1", "value": "val1"},
{"op": "add", "path": "/spec/knownField1", "value": "val2"},
{"op": "add", "path": "/spec/ports/0/name", "value": "portName"},
{"op": "add", "path": "/spec/ports/0/containerPort", "value": 8080},
{"op": "add", "path": "/spec/ports/0/protocol", "value": "TCP"},
{"op": "add", "path": "/spec/ports/0/hostPort", "value": 8081},
{"op": "add", "path": "/spec/ports/0/hostPort", "value": 8082},
{"op": "add", "path": "/spec/ports/0/unknownNested", "value": "val"}
]
`
var testcases = []struct {
name string
patchType types.PatchType
opts metav1.PatchOptions
body string
strictDecodingError string
strictDecodingWarnings []string
}{
{
name: "crd-merge-patch-strict-validation",
patchType: types.MergePatchType,
opts: metav1.PatchOptions{
FieldValidation: "Strict",
},
body: mergePatchBody,
strictDecodingError: `strict decoding error: duplicate field "spec.unknownDupe", duplicate field "spec.knownField1", duplicate field "spec.ports[0].hostPort", unknown field "spec.ports[0].unknownNested", unknown field "spec.unknown1", unknown field "spec.unknownDupe"`,
},
{
name: "crd-merge-patch-warn-validation",
patchType: types.MergePatchType,
opts: metav1.PatchOptions{
FieldValidation: "Warn",
},
body: mergePatchBody,
strictDecodingWarnings: []string{
`duplicate field "spec.unknownDupe"`,
`duplicate field "spec.knownField1"`,
`duplicate field "spec.ports[0].hostPort"`,
`unknown field "spec.ports[0].unknownNested"`,
`unknown field "spec.unknown1"`,
`unknown field "spec.unknownDupe"`,
},
},
{
name: "crd-merge-patch-ignore-validation",
patchType: types.MergePatchType,
opts: metav1.PatchOptions{
FieldValidation: "Ignore",
},
body: mergePatchBody,
},
{
name: "crd-merge-patch-no-validation",
patchType: types.MergePatchType,
body: mergePatchBody,
strictDecodingWarnings: []string{
`duplicate field "spec.unknownDupe"`,
`duplicate field "spec.knownField1"`,
`duplicate field "spec.ports[0].hostPort"`,
`unknown field "spec.ports[0].unknownNested"`,
`unknown field "spec.unknown1"`,
`unknown field "spec.unknownDupe"`,
},
},
{
name: "crd-json-patch-strict-validation",
patchType: types.JSONPatchType,
opts: metav1.PatchOptions{
FieldValidation: "Strict",
},
body: jsonPatchBody,
// note: duplicate fields in the patch itself
// are dropped by the
// evanphx/json-patch library and is expected.
// Duplicate fields in the json patch ops
// themselves can be detected though
strictDecodingError: `strict decoding error: json patch unknown field "[0].foo", json patch duplicate field "[1].path", unknown field "spec.ports[0].unknownNested", unknown field "spec.unknown1", unknown field "spec.unknown3", unknown field "spec.unknownDupe"`,
},
{
name: "crd-json-patch-warn-validation",
patchType: types.JSONPatchType,
opts: metav1.PatchOptions{
FieldValidation: "Warn",
},
body: jsonPatchBody,
strictDecodingWarnings: []string{
// note: duplicate fields in the patch itself
// are dropped by the
// evanphx/json-patch library and is expected.
// Duplicate fields in the json patch ops
// themselves can be detected though
`json patch unknown field "[0].foo"`,
`json patch duplicate field "[1].path"`,
`unknown field "spec.ports[0].unknownNested"`,
`unknown field "spec.unknown1"`,
`unknown field "spec.unknown3"`,
`unknown field "spec.unknownDupe"`,
},
},
{
name: "crd-json-patch-ignore-validation",
patchType: types.JSONPatchType,
opts: metav1.PatchOptions{
FieldValidation: "Ignore",
},
body: jsonPatchBody,
},
{
name: "crd-json-patch-no-validation",
patchType: types.JSONPatchType,
body: jsonPatchBody,
strictDecodingWarnings: []string{
// note: duplicate fields in the patch itself
// are dropped by the
// evanphx/json-patch library and is expected.
// Duplicate fields in the json patch ops
// themselves can be detected though
`json patch unknown field "[0].foo"`,
`json patch duplicate field "[1].path"`,
`unknown field "spec.ports[0].unknownNested"`,
`unknown field "spec.unknown1"`,
`unknown field "spec.unknown3"`,
`unknown field "spec.unknownDupe"`,
},
},
}
for _, tc := range testcases {
t.Run(tc.name, func(t *testing.T) {
kind := gvk.Kind
apiVersion := gvk.Group + "/" + gvk.Version
// create a CR
yamlBody := []byte(fmt.Sprintf(string(patchYAMLBody), apiVersion, kind, tc.name))
createResult, err := rest.Patch(types.ApplyPatchType).
AbsPath("/apis", gvr.Group, gvr.Version, gvr.Resource).
Name(tc.name).
Param("fieldManager", "apply_test").
Body(yamlBody).
DoRaw(context.TODO())
if err != nil {
t.Fatalf("failed to create custom resource with apply: %v:\n%v", err, string(createResult))
}
// patch the CR as specified by the test case
req := rest.Patch(tc.patchType).
AbsPath("/apis", gvr.Group, gvr.Version, gvr.Resource).
Name(tc.name).
VersionedParams(&tc.opts, metav1.ParameterCodec)
result := req.Body([]byte(tc.body)).Do(context.TODO())
if result.Error() == nil && tc.strictDecodingError != "" {
t.Fatalf("received nil error when expecting: %q", tc.strictDecodingError)
}
if result.Error() != nil && (tc.strictDecodingError == "" || !strings.HasSuffix(result.Error().Error(), tc.strictDecodingError)) {
t.Fatalf("expected error: %q, got: %v", tc.strictDecodingError, result.Error())
}
if len(result.Warnings()) != len(tc.strictDecodingWarnings) {
t.Fatalf("unexpected number of warnings, expected: %d, got: %d", len(tc.strictDecodingWarnings), len(result.Warnings()))
}
for i, strictWarn := range tc.strictDecodingWarnings {
if strictWarn != result.Warnings()[i].Text {
t.Fatalf("expected warning: %s, got warning: %s", strictWarn, result.Warnings()[i].Text)
}
}
})
}
}
// testFieldValidationPatchCRDSchemaless tests that server-side schema validation
// works for jsonpatch and mergepatch requests
// for custom resources that have schemas
// with x-kubernetes-preserve-unknown-field set
func testFieldValidationPatchCRDSchemaless(t *testing.T, rest rest.Interface, gvk schema.GroupVersionKind, gvr schema.GroupVersionResource) {
mergePatchBody := `
{
"spec": {
"unknown1": "val1",
"unknownDupe": "valDupe",
"unknownDupe": "valDupe2",
"knownField1": "val1",
"knownField1": "val2",
"ports": [{
"name": "portName",
"containerPort": 8080,
"protocol": "TCP",
"hostPort": 8081,
"hostPort": 8082,
"unknownNested": "val"
}]
}
}
`
jsonPatchBody := `
[
{"op": "add", "path": "/spec/unknown1", "value": "val1", "foo": "bar"},
{"op": "add", "path": "/spec/unknown2", "path": "/spec/unknown3", "value": "val2"},
{"op": "add", "path": "/spec/unknownDupe", "value": "valDupe"},
{"op": "add", "path": "/spec/unknownDupe", "value": "valDupe2"},
{"op": "add", "path": "/spec/knownField1", "value": "val1"},
{"op": "add", "path": "/spec/knownField1", "value": "val2"},
{"op": "add", "path": "/spec/ports/0/name", "value": "portName"},
{"op": "add", "path": "/spec/ports/0/containerPort", "value": 8080},
{"op": "add", "path": "/spec/ports/0/protocol", "value": "TCP"},
{"op": "add", "path": "/spec/ports/0/hostPort", "value": 8081},
{"op": "add", "path": "/spec/ports/0/hostPort", "value": 8082},
{"op": "add", "path": "/spec/ports/0/unknownNested", "value": "val"}
]
`
var testcases = []struct {
name string
patchType types.PatchType
opts metav1.PatchOptions
body string
strictDecodingError string
strictDecodingWarnings []string
}{
{
name: "schemaless-crd-merge-patch-strict-validation",
patchType: types.MergePatchType,
opts: metav1.PatchOptions{
FieldValidation: "Strict",
},
body: mergePatchBody,
strictDecodingError: `strict decoding error: duplicate field "spec.unknownDupe", duplicate field "spec.knownField1", duplicate field "spec.ports[0].hostPort", unknown field "spec.ports[0].unknownNested"`,
},
{
name: "schemaless-crd-merge-patch-warn-validation",
patchType: types.MergePatchType,
opts: metav1.PatchOptions{
FieldValidation: "Warn",
},
body: mergePatchBody,
strictDecodingWarnings: []string{
`duplicate field "spec.unknownDupe"`,
`duplicate field "spec.knownField1"`,
`duplicate field "spec.ports[0].hostPort"`,
`unknown field "spec.ports[0].unknownNested"`,
},
},
{
name: "schemaless-crd-merge-patch-ignore-validation",
patchType: types.MergePatchType,
opts: metav1.PatchOptions{
FieldValidation: "Ignore",
},
body: mergePatchBody,
},
{
name: "schemaless-crd-merge-patch-no-validation",
patchType: types.MergePatchType,
body: mergePatchBody,
strictDecodingWarnings: []string{
`duplicate field "spec.unknownDupe"`,
`duplicate field "spec.knownField1"`,
`duplicate field "spec.ports[0].hostPort"`,
`unknown field "spec.ports[0].unknownNested"`,
},
},
{
name: "schemaless-crd-json-patch-strict-validation",
patchType: types.JSONPatchType,
opts: metav1.PatchOptions{
FieldValidation: "Strict",
},
body: jsonPatchBody,
// note: duplicate fields in the patch itself
// are dropped by the
// evanphx/json-patch library and is expected.
// Duplicate fields in the json patch ops
// themselves can be detected though
strictDecodingError: `strict decoding error: json patch unknown field "[0].foo", json patch duplicate field "[1].path", unknown field "spec.ports[0].unknownNested"`,
},
{
name: "schemaless-crd-json-patch-warn-validation",
patchType: types.JSONPatchType,
opts: metav1.PatchOptions{
FieldValidation: "Warn",
},
body: jsonPatchBody,
strictDecodingWarnings: []string{
// note: duplicate fields in the patch itself
// are dropped by the
// evanphx/json-patch library and is expected.
// Duplicate fields in the json patch ops
// themselves can be detected though
`json patch unknown field "[0].foo"`,
`json patch duplicate field "[1].path"`,
`unknown field "spec.ports[0].unknownNested"`,
},
},
{
name: "schemaless-crd-json-patch-ignore-validation",
patchType: types.JSONPatchType,
opts: metav1.PatchOptions{
FieldValidation: "Ignore",
},
body: jsonPatchBody,
},
{
name: "schemaless-crd-json-patch-no-validation",
patchType: types.JSONPatchType,
body: jsonPatchBody,
strictDecodingWarnings: []string{
// note: duplicate fields in the patch itself
// are dropped by the
// evanphx/json-patch library and is expected.
// Duplicate fields in the json patch ops
// themselves can be detected though
`json patch unknown field "[0].foo"`,
`json patch duplicate field "[1].path"`,
`unknown field "spec.ports[0].unknownNested"`,
},
},
}
for _, tc := range testcases {
t.Run(tc.name, func(t *testing.T) {
kind := gvk.Kind
apiVersion := gvk.Group + "/" + gvk.Version
// create a CR
yamlBody := []byte(fmt.Sprintf(string(patchYAMLBody), apiVersion, kind, tc.name))
createResult, err := rest.Patch(types.ApplyPatchType).
AbsPath("/apis", gvr.Group, gvr.Version, gvr.Resource).
Name(tc.name).
Param("fieldManager", "apply_test").
Body(yamlBody).
DoRaw(context.TODO())
if err != nil {
t.Fatalf("failed to create custom resource with apply: %v:\n%v", err, string(createResult))
}
// patch the CR as specified by the test case
req := rest.Patch(tc.patchType).
AbsPath("/apis", gvr.Group, gvr.Version, gvr.Resource).
Name(tc.name).
VersionedParams(&tc.opts, metav1.ParameterCodec)
result := req.Body([]byte(tc.body)).Do(context.TODO())
if result.Error() == nil && tc.strictDecodingError != "" {
t.Fatalf("received nil error when expecting: %q", tc.strictDecodingError)
}
if result.Error() != nil && (tc.strictDecodingError == "" || !strings.HasSuffix(result.Error().Error(), tc.strictDecodingError)) {
t.Fatalf("expected error: %q, got: %v", tc.strictDecodingError, result.Error())
}
if len(result.Warnings()) != len(tc.strictDecodingWarnings) {
t.Fatalf("unexpected number of warnings, expected: %d, got: %d", len(tc.strictDecodingWarnings), len(result.Warnings()))
}
for i, strictWarn := range tc.strictDecodingWarnings {
if strictWarn != result.Warnings()[i].Text {
t.Fatalf("expected warning: %s, got warning: %s", strictWarn, result.Warnings()[i].Text)
}
}
})
}
}
// testFieldValidationApplyCreateCRD tests apply patch requests containing duplicate fields
// on newly created objects, for CRDs that have schemas
// Note that even prior to server-side validation, unknown fields were treated as
// errors in apply-patch and are not tested here.
func testFieldValidationApplyCreateCRD(t *testing.T, rest rest.Interface, gvk schema.GroupVersionKind, gvr schema.GroupVersionResource) {
var testcases = []struct {
name string
opts metav1.PatchOptions
strictDecodingError string
strictDecodingWarnings []string
}{
{
name: "strict-validation",
opts: metav1.PatchOptions{
FieldValidation: "Strict",
FieldManager: "mgr",
},
strictDecodingError: `error strict decoding YAML: error converting YAML to JSON: yaml: unmarshal errors:
line 10: key "knownField1" already set in map
line 16: key "hostPort" already set in map`,
},
{
name: "warn-validation",
opts: metav1.PatchOptions{
FieldValidation: "Warn",
FieldManager: "mgr",
},
strictDecodingWarnings: []string{
`line 10: key "knownField1" already set in map`,
`line 16: key "hostPort" already set in map`,
},
},
{
name: "ignore-validation",
opts: metav1.PatchOptions{
FieldValidation: "Ignore",
FieldManager: "mgr",
},
},
{
name: "no-validation",
opts: metav1.PatchOptions{
FieldManager: "mgr",
},
strictDecodingWarnings: []string{
`line 10: key "knownField1" already set in map`,
`line 16: key "hostPort" already set in map`,
},
},
}
for _, tc := range testcases {
t.Run(tc.name, func(t *testing.T) {
kind := gvk.Kind
apiVersion := gvk.Group + "/" + gvk.Version
// create the CR as specified by the test case
name := fmt.Sprintf("apply-create-crd-%s", tc.name)
applyCreateBody := []byte(fmt.Sprintf(crdApplyInvalidBody, apiVersion, kind, name))
req := rest.Patch(types.ApplyPatchType).
AbsPath("/apis", gvr.Group, gvr.Version, gvr.Resource).
Name(name).
VersionedParams(&tc.opts, metav1.ParameterCodec)
result := req.Body(applyCreateBody).Do(context.TODO())
if result.Error() == nil && tc.strictDecodingError != "" {
t.Fatalf("received nil error when expecting: %q", tc.strictDecodingError)
}
if result.Error() != nil && (tc.strictDecodingError == "" || !strings.HasSuffix(result.Error().Error(), tc.strictDecodingError)) {
t.Fatalf("expected error: %q, got: %v", tc.strictDecodingError, result.Error())
}
if len(result.Warnings()) != len(tc.strictDecodingWarnings) {
t.Fatalf("unexpected number of warnings, expected: %d, got: %d", len(tc.strictDecodingWarnings), len(result.Warnings()))
}
for i, strictWarn := range tc.strictDecodingWarnings {
if strictWarn != result.Warnings()[i].Text {
t.Fatalf("expected warning: %s, got warning: %s", strictWarn, result.Warnings()[i].Text)
}
}
})
}
}
// testFieldValidationApplyCreateCRDSchemaless tests apply patch requests containing duplicate fields
// on newly created objects, for CRDs that have schemas
// with x-kubernetes-preserve-unknown-field set
// Note that even prior to server-side validation, unknown fields were treated as
// errors in apply-patch and are not tested here.
func testFieldValidationApplyCreateCRDSchemaless(t *testing.T, rest rest.Interface, gvk schema.GroupVersionKind, gvr schema.GroupVersionResource) {
var testcases = []struct {
name string
opts metav1.PatchOptions
strictDecodingError string
strictDecodingWarnings []string
}{
{
name: "schemaless-strict-validation",
opts: metav1.PatchOptions{
FieldValidation: "Strict",
FieldManager: "mgr",
},
strictDecodingError: `error strict decoding YAML: error converting YAML to JSON: yaml: unmarshal errors:
line 10: key "knownField1" already set in map
line 16: key "hostPort" already set in map`,
},
{
name: "schemaless-warn-validation",
opts: metav1.PatchOptions{
FieldValidation: "Warn",
FieldManager: "mgr",
},
strictDecodingWarnings: []string{
`line 10: key "knownField1" already set in map`,
`line 16: key "hostPort" already set in map`,
},
},
{
name: "schemaless-ignore-validation",
opts: metav1.PatchOptions{
FieldValidation: "Ignore",
FieldManager: "mgr",
},
},
{
name: "schemaless-no-validation",
opts: metav1.PatchOptions{
FieldManager: "mgr",
},
strictDecodingWarnings: []string{
`line 10: key "knownField1" already set in map`,
`line 16: key "hostPort" already set in map`,
},
},
}
for _, tc := range testcases {
t.Run(tc.name, func(t *testing.T) {
kind := gvk.Kind
apiVersion := gvk.Group + "/" + gvk.Version
// create the CR as specified by the test case
name := fmt.Sprintf("apply-create-crd-schemaless-%s", tc.name)
applyCreateBody := []byte(fmt.Sprintf(crdApplyInvalidBody, apiVersion, kind, name))
req := rest.Patch(types.ApplyPatchType).
AbsPath("/apis", gvr.Group, gvr.Version, gvr.Resource).
Name(name).
VersionedParams(&tc.opts, metav1.ParameterCodec)
result := req.Body(applyCreateBody).Do(context.TODO())
if result.Error() == nil && tc.strictDecodingError != "" {
t.Fatalf("received nil error when expecting: %q", tc.strictDecodingError)
}
if result.Error() != nil && (tc.strictDecodingError == "" || !strings.HasSuffix(result.Error().Error(), tc.strictDecodingError)) {
t.Fatalf("expected error: %q, got: %v", tc.strictDecodingError, result.Error())
}
if len(result.Warnings()) != len(tc.strictDecodingWarnings) {
t.Fatalf("unexpected number of warnings, expected: %d, got: %d", len(tc.strictDecodingWarnings), len(result.Warnings()))
}
for i, strictWarn := range tc.strictDecodingWarnings {
if strictWarn != result.Warnings()[i].Text {
t.Fatalf("expected warning: %s, got warning: %s", strictWarn, result.Warnings()[i].Text)
}
}
})
}
}
// testFieldValidationApplyUpdateCRD tests apply patch requests containing duplicate fields
// on existing objects, for CRDs with schemas
// Note that even prior to server-side validation, unknown fields were treated as
// errors in apply-patch and are not tested here.
func testFieldValidationApplyUpdateCRD(t *testing.T, rest rest.Interface, gvk schema.GroupVersionKind, gvr schema.GroupVersionResource) {
var testcases = []struct {
name string
opts metav1.PatchOptions
strictDecodingError string
strictDecodingWarnings []string
}{
{
name: "strict-validation",
opts: metav1.PatchOptions{
FieldValidation: "Strict",
FieldManager: "mgr",
},
strictDecodingError: `error strict decoding YAML: error converting YAML to JSON: yaml: unmarshal errors:
line 10: key "knownField1" already set in map
line 16: key "hostPort" already set in map`,
},
{
name: "warn-validation",
opts: metav1.PatchOptions{
FieldValidation: "Warn",
FieldManager: "mgr",
},
strictDecodingWarnings: []string{
`line 10: key "knownField1" already set in map`,
`line 16: key "hostPort" already set in map`,
},
},
{
name: "ignore-validation",
opts: metav1.PatchOptions{
FieldValidation: "Ignore",
FieldManager: "mgr",
},
},
{
name: "no-validation",
opts: metav1.PatchOptions{
FieldManager: "mgr",
},
strictDecodingWarnings: []string{
`line 10: key "knownField1" already set in map`,
`line 16: key "hostPort" already set in map`,
},
},
}
for _, tc := range testcases {
t.Run(tc.name, func(t *testing.T) {
kind := gvk.Kind
apiVersion := gvk.Group + "/" + gvk.Version
// create the CR as specified by the test case
name := fmt.Sprintf("apply-update-crd-%s", tc.name)
applyCreateBody := []byte(fmt.Sprintf(crdApplyValidBody, apiVersion, kind, name))
createReq := rest.Patch(types.ApplyPatchType).
AbsPath("/apis", gvr.Group, gvr.Version, gvr.Resource).
Name(name).
VersionedParams(&tc.opts, metav1.ParameterCodec)
createResult := createReq.Body(applyCreateBody).Do(context.TODO())
if createResult.Error() != nil {
t.Fatalf("unexpected apply create err: %v", createResult.Error())
}
applyUpdateBody := []byte(fmt.Sprintf(crdApplyInvalidBody, apiVersion, kind, name))
updateReq := rest.Patch(types.ApplyPatchType).
AbsPath("/apis", gvr.Group, gvr.Version, gvr.Resource).
Name(name).
VersionedParams(&tc.opts, metav1.ParameterCodec)
result := updateReq.Body(applyUpdateBody).Do(context.TODO())
if result.Error() == nil && tc.strictDecodingError != "" {
t.Fatalf("received nil error when expecting: %q", tc.strictDecodingError)
}
if result.Error() != nil && (tc.strictDecodingError == "" || !strings.HasSuffix(result.Error().Error(), tc.strictDecodingError)) {
t.Fatalf("expected error: %q, got: %v", tc.strictDecodingError, result.Error())
}
if len(result.Warnings()) != len(tc.strictDecodingWarnings) {
t.Fatalf("unexpected number of warnings, expected: %d, got: %d", len(tc.strictDecodingWarnings), len(result.Warnings()))
}
for i, strictWarn := range tc.strictDecodingWarnings {
if strictWarn != result.Warnings()[i].Text {
t.Fatalf("expected warning: %s, got warning: %s", strictWarn, result.Warnings()[i].Text)
}
}
})
}
}
// testFieldValidationApplyUpdateCRDSchemaless tests apply patch requests containing duplicate fields
// on existing objects, for CRDs with schemas
// with x-kubernetes-preserve-unknown-field set
// Note that even prior to server-side validation, unknown fields were treated as
// errors in apply-patch and are not tested here.
func testFieldValidationApplyUpdateCRDSchemaless(t *testing.T, rest rest.Interface, gvk schema.GroupVersionKind, gvr schema.GroupVersionResource) {
var testcases = []struct {
name string
opts metav1.PatchOptions
strictDecodingError string
strictDecodingWarnings []string
}{
{
name: "schemaless-strict-validation",
opts: metav1.PatchOptions{
FieldValidation: "Strict",
FieldManager: "mgr",
},
strictDecodingError: `error strict decoding YAML: error converting YAML to JSON: yaml: unmarshal errors:
line 10: key "knownField1" already set in map
line 16: key "hostPort" already set in map`,
},
{
name: "schemaless-warn-validation",
opts: metav1.PatchOptions{
FieldValidation: "Warn",
FieldManager: "mgr",
},
strictDecodingWarnings: []string{
`line 10: key "knownField1" already set in map`,
`line 16: key "hostPort" already set in map`,
},
},
{
name: "schemaless-ignore-validation",
opts: metav1.PatchOptions{
FieldValidation: "Ignore",
FieldManager: "mgr",
},
},
{
name: "schemaless-no-validation",
opts: metav1.PatchOptions{
FieldManager: "mgr",
},
strictDecodingWarnings: []string{
`line 10: key "knownField1" already set in map`,
`line 16: key "hostPort" already set in map`,
},
},
}
for _, tc := range testcases {
t.Run(tc.name, func(t *testing.T) {
kind := gvk.Kind
apiVersion := gvk.Group + "/" + gvk.Version
// create the CR as specified by the test case
name := fmt.Sprintf("apply-update-crd-schemaless-%s", tc.name)
applyCreateBody := []byte(fmt.Sprintf(crdApplyValidBody, apiVersion, kind, name))
createReq := rest.Patch(types.ApplyPatchType).
AbsPath("/apis", gvr.Group, gvr.Version, gvr.Resource).
Name(name).
VersionedParams(&tc.opts, metav1.ParameterCodec)
createResult := createReq.Body(applyCreateBody).Do(context.TODO())
if createResult.Error() != nil {
t.Fatalf("unexpected apply create err: %v", createResult.Error())
}
applyUpdateBody := []byte(fmt.Sprintf(crdApplyInvalidBody, apiVersion, kind, name))
updateReq := rest.Patch(types.ApplyPatchType).
AbsPath("/apis", gvr.Group, gvr.Version, gvr.Resource).
Name(name).
VersionedParams(&tc.opts, metav1.ParameterCodec)
result := updateReq.Body(applyUpdateBody).Do(context.TODO())
if result.Error() == nil && tc.strictDecodingError != "" {
t.Fatalf("received nil error when expecting: %q", tc.strictDecodingError)
}
if result.Error() != nil && (tc.strictDecodingError == "" || !strings.HasSuffix(result.Error().Error(), tc.strictDecodingError)) {
t.Fatalf("expected error: %q, got: %v", tc.strictDecodingError, result.Error())
}
if len(result.Warnings()) != len(tc.strictDecodingWarnings) {
t.Fatalf("unexpected number of warnings, expected: %d, got: %d", len(tc.strictDecodingWarnings), len(result.Warnings()))
}
for i, strictWarn := range tc.strictDecodingWarnings {
if strictWarn != result.Warnings()[i].Text {
t.Fatalf("expected warning: %s, got warning: %s", strictWarn, result.Warnings()[i].Text)
}
}
})
}
}
func setupCRD(t testing.TB, config *rest.Config, apiGroup string, schemaless bool) *apiextensionsv1.CustomResourceDefinition {
apiExtensionClient, err := apiextensionsclient.NewForConfig(config)
if err != nil {
t.Fatal(err)
}
dynamicClient, err := dynamic.NewForConfig(config)
if err != nil {
t.Fatal(err)
}
preserveUnknownFields := ""
if schemaless {
preserveUnknownFields = `"x-kubernetes-preserve-unknown-fields": true,`
}
crdSchema := fmt.Sprintf(crdSchemaBase, preserveUnknownFields)
// create the CRD
crd := fixtures.NewNoxuV1CustomResourceDefinition(apiextensionsv1.ClusterScoped)
// adjust the API group
crd.Name = crd.Spec.Names.Plural + "." + apiGroup
crd.Spec.Group = apiGroup
var c apiextensionsv1.CustomResourceValidation
err = json.Unmarshal([]byte(crdSchema), &c)
if err != nil {
t.Fatal(err)
}
//crd.Spec.PreserveUnknownFields = false
for i := range crd.Spec.Versions {
crd.Spec.Versions[i].Schema = &c
}
// install the CRD
crd, err = fixtures.CreateNewV1CustomResourceDefinition(crd, apiExtensionClient, dynamicClient)
if err != nil {
t.Fatal(err)
}
return crd
}
func BenchmarkFieldValidation(b *testing.B) {
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)
schemaCRD := setupCRD(b, config, "schema.example.com", false)
schemaGVR := schema.GroupVersionResource{
Group: schemaCRD.Spec.Group,
Version: schemaCRD.Spec.Versions[0].Name,
Resource: schemaCRD.Spec.Names.Plural,
}
schemaGVK := schema.GroupVersionKind{
Group: schemaCRD.Spec.Group,
Version: schemaCRD.Spec.Versions[0].Name,
Kind: schemaCRD.Spec.Names.Kind,
}
schemalessCRD := setupCRD(b, config, "schemaless.example.com", true)
schemalessGVR := schema.GroupVersionResource{
Group: schemalessCRD.Spec.Group,
Version: schemalessCRD.Spec.Versions[0].Name,
Resource: schemalessCRD.Spec.Names.Plural,
}
schemalessGVK := schema.GroupVersionKind{
Group: schemalessCRD.Spec.Group,
Version: schemalessCRD.Spec.Versions[0].Name,
Kind: schemalessCRD.Spec.Names.Kind,
}
rest := client.Discovery().RESTClient()
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) })
b.Run("PostCRD", func(b *testing.B) { benchFieldValidationPostCRD(b, rest, schemaGVK, schemaGVR) })
b.Run("PutCRD", func(b *testing.B) { benchFieldValidationPutCRD(b, rest, schemaGVK, schemaGVR) })
b.Run("PatchCRD", func(b *testing.B) { benchFieldValidationPatchCRD(b, rest, schemaGVK, schemaGVR) })
b.Run("ApplyCreateCRD", func(b *testing.B) { benchFieldValidationApplyCreateCRD(b, rest, schemaGVK, schemaGVR) })
b.Run("ApplyUpdateCRD", func(b *testing.B) { benchFieldValidationApplyUpdateCRD(b, rest, schemaGVK, schemaGVR) })
b.Run("PostCRDSchemaless", func(b *testing.B) { benchFieldValidationPostCRD(b, rest, schemalessGVK, schemalessGVR) })
b.Run("PutCRDSchemaless", func(b *testing.B) { benchFieldValidationPutCRD(b, rest, schemalessGVK, schemalessGVR) })
b.Run("PatchCRDSchemaless", func(b *testing.B) { benchFieldValidationPatchCRD(b, rest, schemalessGVK, schemalessGVR) })
b.Run("ApplyCreateCRDSchemaless", func(b *testing.B) { benchFieldValidationApplyCreateCRD(b, rest, schemalessGVK, schemalessGVR) })
b.Run("ApplyUpdateCRDSchemaless", func(b *testing.B) { benchFieldValidationApplyUpdateCRD(b, rest, schemalessGVK, schemalessGVR) })
}
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())
}
}
})
}
}
func benchFieldValidationPostCRD(b *testing.B, rest rest.Interface, gvk schema.GroupVersionKind, gvr schema.GroupVersionResource) {
var testcases = []struct {
name string
opts metav1.PatchOptions
body string
contentType string
}{
{
name: "crd-post-strict-validation",
opts: metav1.PatchOptions{
FieldValidation: "Strict",
},
body: crdValidBody,
},
{
name: "crd-post-warn-validation",
opts: metav1.PatchOptions{
FieldValidation: "Warn",
},
body: crdValidBody,
},
{
name: "crd-post-ignore-validation",
opts: metav1.PatchOptions{
FieldValidation: "Ignore",
},
body: crdValidBody,
},
{
name: "crd-post-no-validation",
body: crdValidBody,
},
{
name: "crd-post-strict-validation-yaml",
opts: metav1.PatchOptions{
FieldValidation: "Strict",
},
body: crdValidBodyYAML,
contentType: "application/yaml",
},
{
name: "crd-post-warn-validation-yaml",
opts: metav1.PatchOptions{
FieldValidation: "Warn",
},
body: crdValidBodyYAML,
contentType: "application/yaml",
},
{
name: "crd-post-ignore-validation-yaml",
opts: metav1.PatchOptions{
FieldValidation: "Ignore",
},
body: crdValidBodyYAML,
contentType: "application/yaml",
},
{
name: "crd-post-no-validation-yaml",
body: crdValidBodyYAML,
contentType: "application/yaml",
},
}
for _, tc := range testcases {
b.Run(tc.name, func(b *testing.B) {
b.ResetTimer()
b.ReportAllocs()
for n := 0; n < b.N; n++ {
kind := gvk.Kind
apiVersion := gvk.Group + "/" + gvk.Version
// create the CR as specified by the test case
jsonBody := []byte(fmt.Sprintf(tc.body, apiVersion, kind, fmt.Sprintf("test-dep-%s-%d-%d-%d", tc.name, n, b.N, time.Now().UnixNano())))
req := rest.Post().
AbsPath("/apis", gvr.Group, gvr.Version, gvr.Resource).
SetHeader("Content-Type", tc.contentType).
VersionedParams(&tc.opts, metav1.ParameterCodec)
result := req.Body([]byte(jsonBody)).Do(context.TODO())
if result.Error() != nil {
b.Fatalf("unexpected post err: %v", result.Error())
}
}
})
}
}
func benchFieldValidationPutCRD(b *testing.B, rest rest.Interface, gvk schema.GroupVersionKind, gvr schema.GroupVersionResource) {
var testcases = []struct {
name string
opts metav1.PatchOptions
putBody string
contentType string
}{
{
name: "crd-put-strict-validation",
opts: metav1.PatchOptions{
FieldValidation: "Strict",
},
putBody: crdValidBody,
},
{
name: "crd-put-warn-validation",
opts: metav1.PatchOptions{
FieldValidation: "Warn",
},
putBody: crdValidBody,
},
{
name: "crd-put-ignore-validation",
opts: metav1.PatchOptions{
FieldValidation: "Ignore",
},
putBody: crdValidBody,
},
{
name: "crd-put-no-validation",
putBody: crdValidBody,
},
{
name: "crd-put-strict-validation-yaml",
opts: metav1.PatchOptions{
FieldValidation: "Strict",
},
putBody: crdValidBodyYAML,
contentType: "application/yaml",
},
{
name: "crd-put-warn-validation-yaml",
opts: metav1.PatchOptions{
FieldValidation: "Warn",
},
putBody: crdValidBodyYAML,
contentType: "application/yaml",
},
{
name: "crd-put-ignore-validation-yaml",
opts: metav1.PatchOptions{
FieldValidation: "Ignore",
},
putBody: crdValidBodyYAML,
contentType: "application/yaml",
},
{
name: "crd-put-no-validation-yaml",
putBody: crdValidBodyYAML,
contentType: "application/yaml",
},
}
for _, tc := range testcases {
b.Run(tc.name, func(b *testing.B) {
kind := gvk.Kind
apiVersion := gvk.Group + "/" + gvk.Version
names := make([]string, b.N)
resourceVersions := make([]string, b.N)
for n := 0; n < b.N; n++ {
deployName := fmt.Sprintf("test-dep-%s-%d-%d-%d", tc.name, n, b.N, time.Now().UnixNano())
names[n] = deployName
// create the CR as specified by the test case
jsonPostBody := []byte(fmt.Sprintf(crdValidBody, apiVersion, kind, deployName))
postReq := rest.Post().
AbsPath("/apis", gvr.Group, gvr.Version, gvr.Resource).
VersionedParams(&tc.opts, metav1.ParameterCodec)
postResult, err := postReq.Body([]byte(jsonPostBody)).Do(context.TODO()).Raw()
if err != nil {
b.Fatalf("unexpeted error on CR creation: %v", err)
}
postUnstructured := &unstructured.Unstructured{}
if err := postUnstructured.UnmarshalJSON(postResult); err != nil {
b.Fatalf("unexpeted error unmarshalling created CR: %v", err)
}
resourceVersions[n] = postUnstructured.GetResourceVersion()
}
b.ResetTimer()
b.ReportAllocs()
for n := 0; n < b.N; n++ {
// update the CR as specified by the test case
putBody := []byte(fmt.Sprintf(tc.putBody, apiVersion, kind, names[n], resourceVersions[n]))
putReq := rest.Put().
AbsPath("/apis", gvr.Group, gvr.Version, gvr.Resource).
Name(names[n]).
SetHeader("Content-Type", tc.contentType).
VersionedParams(&tc.opts, metav1.ParameterCodec)
result := putReq.Body([]byte(putBody)).Do(context.TODO())
if result.Error() != nil {
b.Fatalf("unexpected put err: %v", result.Error())
}
}
})
}
}
func benchFieldValidationPatchCRD(b *testing.B, rest rest.Interface, gvk schema.GroupVersionKind, gvr schema.GroupVersionResource) {
patchYAMLBody := `
apiVersion: %s
kind: %s
metadata:
name: %s
finalizers:
- test-finalizer
spec:
cronSpec: "* * * * */5"
ports:
- name: x
containerPort: 80
protocol: TCP`
mergePatchBody := `
{
"spec": {
"knownField1": "val1",
"ports": [{
"name": "portName",
"containerPort": 8080,
"protocol": "TCP",
"hostPort": 8081
}]
}
}
`
jsonPatchBody := `
[
{"op": "add", "path": "/spec/knownField1", "value": "val1"},
{"op": "add", "path": "/spec/ports/0/name", "value": "portName"},
{"op": "add", "path": "/spec/ports/0/containerPort", "value": 8080},
{"op": "add", "path": "/spec/ports/0/protocol", "value": "TCP"},
{"op": "add", "path": "/spec/ports/0/hostPort", "value": 8081}
]
`
var testcases = []struct {
name string
patchType types.PatchType
opts metav1.PatchOptions
body string
}{
{
name: "crd-merge-patch-strict-validation",
patchType: types.MergePatchType,
opts: metav1.PatchOptions{
FieldValidation: "Strict",
},
body: mergePatchBody,
},
{
name: "crd-merge-patch-warn-validation",
patchType: types.MergePatchType,
opts: metav1.PatchOptions{
FieldValidation: "Warn",
},
body: mergePatchBody,
},
{
name: "crd-merge-patch-ignore-validation",
patchType: types.MergePatchType,
opts: metav1.PatchOptions{
FieldValidation: "Ignore",
},
body: mergePatchBody,
},
{
name: "crd-merge-patch-no-validation",
patchType: types.MergePatchType,
body: mergePatchBody,
},
{
name: "crd-json-patch-strict-validation",
patchType: types.JSONPatchType,
opts: metav1.PatchOptions{
FieldValidation: "Strict",
},
body: jsonPatchBody,
},
{
name: "crd-json-patch-warn-validation",
patchType: types.JSONPatchType,
opts: metav1.PatchOptions{
FieldValidation: "Warn",
},
body: jsonPatchBody,
},
{
name: "crd-json-patch-ignore-validation",
patchType: types.JSONPatchType,
opts: metav1.PatchOptions{
FieldValidation: "Ignore",
},
body: jsonPatchBody,
},
{
name: "crd-json-patch-no-validation",
patchType: types.JSONPatchType,
body: jsonPatchBody,
},
}
for _, tc := range testcases {
b.Run(tc.name, func(b *testing.B) {
kind := gvk.Kind
apiVersion := gvk.Group + "/" + gvk.Version
names := make([]string, b.N)
for n := 0; n < b.N; n++ {
deployName := fmt.Sprintf("test-dep-%s-%d-%d-%d", tc.name, n, b.N, time.Now().UnixNano())
names[n] = deployName
// create a CR
yamlBody := []byte(fmt.Sprintf(string(patchYAMLBody), apiVersion, kind, deployName))
createResult, err := rest.Patch(types.ApplyPatchType).
AbsPath("/apis", gvr.Group, gvr.Version, gvr.Resource).
Name(deployName).
Param("fieldManager", "apply_test").
Body(yamlBody).
DoRaw(context.TODO())
if err != nil {
b.Fatalf("failed to create custom resource with apply: %v:\n%v", err, string(createResult))
}
}
b.ResetTimer()
b.ReportAllocs()
for n := 0; n < b.N; n++ {
// patch the CR as specified by the test case
req := rest.Patch(tc.patchType).
AbsPath("/apis", gvr.Group, gvr.Version, gvr.Resource).
Name(names[n]).
VersionedParams(&tc.opts, metav1.ParameterCodec)
result := req.Body([]byte(tc.body)).Do(context.TODO())
if result.Error() != nil {
b.Fatalf("unexpected patch err: %v", result.Error())
}
}
})
}
}
func benchFieldValidationApplyCreateCRD(b *testing.B, rest rest.Interface, gvk schema.GroupVersionKind, gvr schema.GroupVersionResource) {
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",
},
},
{
name: "no-validation",
opts: metav1.PatchOptions{
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++ {
kind := gvk.Kind
apiVersion := gvk.Group + "/" + gvk.Version
name := fmt.Sprintf("test-dep-%s-%d-%d-%d", tc.name, n, b.N, time.Now().UnixNano())
// create the CR as specified by the test case
applyCreateBody := []byte(fmt.Sprintf(crdApplyValidBody, apiVersion, kind, name))
req := rest.Patch(types.ApplyPatchType).
AbsPath("/apis", gvr.Group, gvr.Version, gvr.Resource).
Name(name).
VersionedParams(&tc.opts, metav1.ParameterCodec)
result := req.Body(applyCreateBody).Do(context.TODO())
if result.Error() != nil {
b.Fatalf("unexpected apply err: %v", result.Error())
}
}
})
}
}
func benchFieldValidationApplyUpdateCRD(b *testing.B, rest rest.Interface, gvk schema.GroupVersionKind, gvr schema.GroupVersionResource) {
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",
},
},
{
name: "no-validation",
opts: metav1.PatchOptions{
FieldManager: "mgr",
},
},
}
for _, tc := range testcases {
b.Run(tc.name, func(b *testing.B) {
kind := gvk.Kind
apiVersion := gvk.Group + "/" + gvk.Version
names := make([]string, b.N)
for n := 0; n < b.N; n++ {
names[n] = fmt.Sprintf("apply-update-crd-%s-%d-%d-%d", tc.name, n, b.N, time.Now().UnixNano())
applyCreateBody := []byte(fmt.Sprintf(crdApplyValidBody, apiVersion, kind, names[n]))
createReq := rest.Patch(types.ApplyPatchType).
AbsPath("/apis", gvr.Group, gvr.Version, gvr.Resource).
Name(names[n]).
VersionedParams(&tc.opts, metav1.ParameterCodec)
createResult := createReq.Body(applyCreateBody).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++ {
applyUpdateBody := []byte(fmt.Sprintf(crdApplyValidBody2, apiVersion, kind, names[n]))
updateReq := rest.Patch(types.ApplyPatchType).
AbsPath("/apis", gvr.Group, gvr.Version, gvr.Resource).
Name(names[n]).
VersionedParams(&tc.opts, metav1.ParameterCodec)
result := updateReq.Body(applyUpdateBody).Do(context.TODO())
if result.Error() != nil {
b.Fatalf("unexpected apply err: %v", result.Error())
}
}
})
}
}