mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-07-19 09:52:49 +00:00
3954 lines
120 KiB
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())
|
|
}
|
|
}
|
|
|
|
})
|
|
}
|
|
}
|