kubernetes/test/integration/apiserver/field_validation_test.go
2021-12-13 09:38:13 -05:00

2896 lines
88 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"
"fmt"
"strings"
"testing"
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/apiserver/pkg/features"
utilfeature "k8s.io/apiserver/pkg/util/feature"
"k8s.io/client-go/dynamic"
clientset "k8s.io/client-go/kubernetes"
"k8s.io/client-go/rest"
featuregatetesting "k8s.io/component-base/featuregate/testing"
kubeapiservertesting "k8s.io/kubernetes/cmd/kube-apiserver/app/testing"
"k8s.io/kubernetes/test/integration/framework"
)
var (
invalidBodyJSON = `
{
"apiVersion": "apps/v1",
"kind": "Deployment",
"metadata": {
"name": "%s",
"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"
}]
}
}
}
}
`
invalidBodyYAML = `apiVersion: apps/v1
kind: Deployment
metadata:
name: %s
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`
validBodyJSON = `
{
"apiVersion": "apps/v1",
"kind": "Deployment",
"metadata": {
"name": "%s",
"labels": {"app": "nginx"}
},
"spec": {
"selector": {
"matchLabels": {
"app": "nginx"
}
},
"template": {
"metadata": {
"labels": {
"app": "nginx"
}
},
"spec": {
"containers": [{
"name": "nginx",
"image": "nginx:latest",
"imagePullPolicy": "Always"
}]
}
}
}
}`
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"
}]
}
}
}
}`
crdInvalidBody = `
{
"apiVersion": "%s",
"kind": "%s",
"metadata": {
"name": "%s",
"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"
}]
}
}`
crdValidBody = `
{
"apiVersion": "%s",
"kind": "%s",
"metadata": {
"name": "%s"
},
"spec": {
"knownField1": "val1",
"ports": [{
"name": "portName",
"containerPort": 8080,
"protocol": "TCP",
"hostPort": 8081
}]
}
}
`
crdInvalidBodyYAML = `
apiVersion: "%s"
kind: "%s"
metadata:
name: "%s"
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`
crdApplyInvalidBody = `
{
"apiVersion": "%s",
"kind": "%s",
"metadata": {
"name": "%s"
},
"spec": {
"knownField1": "val1",
"knownField1": "val2",
"ports": [{
"name": "portName",
"containerPort": 8080,
"protocol": "TCP",
"hostPort": 8081,
"hostPort": 8082
}]
}
}`
crdApplyValidBody = `
{
"apiVersion": "%s",
"kind": "%s",
"metadata": {
"name": "%s"
},
"spec": {
"knownField1": "val1",
"ports": [{
"name": "portName",
"containerPort": 8080,
"protocol": "TCP",
"hostPort": 8082
}]
}
}`
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"
},
"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) {
defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.ServerSideFieldValidation, true)()
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
strictDecodingErrors []string
strictDecodingWarnings []string
}{
{
name: "post-strict-validation",
opts: metav1.CreateOptions{
FieldValidation: "Strict",
},
bodyBase: invalidBodyJSON,
strictDecodingErrors: []string{
`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-warn-validation",
opts: metav1.CreateOptions{
FieldValidation: "Warn",
},
bodyBase: invalidBodyJSON,
strictDecodingWarnings: []string{
`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-default-ignore-validation",
bodyBase: invalidBodyJSON,
strictDecodingWarnings: []string{
`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",
strictDecodingErrors: []string{
`line 10: key "unknownDupe" already set in map`,
`line 12: key "paused" already set in map`,
`line 26: key "imagePullPolicy" already set in map`,
`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 10: key "unknownDupe" already set in map`,
`line 12: key "paused" already set in map`,
`line 26: key "imagePullPolicy" already set in map`,
`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 10: key "unknownDupe" already set in map`,
`line 12: key "paused" already set in map`,
`line 26: key "imagePullPolicy" already set in map`,
`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) {
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 && len(tc.strictDecodingErrors) == 0 {
t.Fatalf("unexpected request err: %v", result.Error())
}
for _, strictErr := range tc.strictDecodingErrors {
if !strings.Contains(result.Error().Error(), strictErr) {
t.Fatalf("missing strict decoding error: %s from error: %v", strictErr, 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
strictDecodingErrors []string
strictDecodingWarnings []string
}{
{
name: "put-strict-validation",
opts: metav1.UpdateOptions{
FieldValidation: "Strict",
},
putBodyBase: invalidBodyJSON,
strictDecodingErrors: []string{
`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-warn-validation",
opts: metav1.UpdateOptions{
FieldValidation: "Warn",
},
putBodyBase: invalidBodyJSON,
strictDecodingWarnings: []string{
`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-default-ignore-validation",
opts: metav1.UpdateOptions{
FieldValidation: "Ignore",
},
putBodyBase: invalidBodyJSON,
},
{
name: "put-ignore-validation",
putBodyBase: invalidBodyJSON,
strictDecodingWarnings: []string{
`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",
strictDecodingErrors: []string{
`line 10: key "unknownDupe" already set in map`,
`line 12: key "paused" already set in map`,
`line 26: key "imagePullPolicy" already set in map`,
`unknown field "spec.unknown1"`,
`unknown field "spec.unknownDupe"`,
`unknown field "spec.template.spec.containers[0].unknownNested"`,
},
},
{
name: "put-warn-validation-yaml",
opts: metav1.UpdateOptions{
FieldValidation: "Warn",
},
putBodyBase: invalidBodyYAML,
contentType: "application/yaml",
strictDecodingWarnings: []string{
`line 10: key "unknownDupe" already set in map`,
`line 12: key "paused" already set in map`,
`line 26: key "imagePullPolicy" already set in map`,
`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 10: key "unknownDupe" already set in map`,
`line 12: key "paused" already set in map`,
`line 26: key "imagePullPolicy" already set in map`,
`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 && len(tc.strictDecodingErrors) == 0 {
t.Fatalf("unexpected request err: %v", result.Error())
}
for _, strictErr := range tc.strictDecodingErrors {
if !strings.Contains(result.Error().Error(), strictErr) {
t.Fatalf("missing strict decoding error: %s from error: %s", strictErr, result.Error().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
strictDecodingErrors []string
strictDecodingWarnings []string
}{
{
name: "merge-patch-strict-validation",
opts: metav1.PatchOptions{
FieldValidation: "Strict",
},
patchType: types.MergePatchType,
body: mergePatchBody,
strictDecodingErrors: []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-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,
strictDecodingErrors: []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-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,
strictDecodingErrors: []string{
`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 && len(tc.strictDecodingErrors) == 0 {
t.Fatalf("unexpected request err: %v", result.Error())
}
for _, strictErr := range tc.strictDecodingErrors {
if !strings.Contains(result.Error().Error(), strictErr) {
t.Fatalf("missing strict decoding error: %s from error: %s", strictErr, result.Error().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) {
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"
}]
}
}
}
}
`
// non-conflicting SMP has issues with the patch (duplicate fields),
// but doesn't conflict with the existing object it's being patched to
nonconflictingSMPBody := `
{
"spec": {
"paused": true,
"paused": false,
"selector": {
"matchLabels": {
"app": "nginx"
}
},
"template": {
"metadata": {
"labels": {
"app": "nginx"
}
},
"spec": {
"containers": [{
"name": "nginx",
"imagePullPolicy": "Always",
"imagePullPolicy": "Never"
}]
}
}
}
}
`
var testcases = []struct {
name string
opts metav1.PatchOptions
body string
strictDecodingErrors []string
strictDecodingWarnings []string
}{
{
name: "smp-strict-validation",
opts: metav1.PatchOptions{
FieldValidation: "Strict",
},
body: smpBody,
strictDecodingErrors: []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-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,
strictDecodingErrors: []string{
`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 && len(tc.strictDecodingErrors) == 0 {
t.Fatalf("unexpected patch err: %v", result.Error())
}
if result.Error() == nil && len(tc.strictDecodingErrors) > 0 {
t.Fatalf("unexpected patch succeeded")
}
for _, strictErr := range tc.strictDecodingErrors {
if !strings.Contains(result.Error().Error(), strictErr) {
t.Fatalf("missing strict decoding error: %s from error: %s", strictErr, result.Error().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
strictDecodingErrors []string
strictDecodingWarnings []string
}{
{
name: "strict-validation",
opts: metav1.PatchOptions{
FieldValidation: "Strict",
FieldManager: "mgr",
},
strictDecodingErrors: []string{
`key "paused" already set in map`,
`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 && len(tc.strictDecodingErrors) == 0 {
t.Fatalf("unexpected request err: %v", result.Error())
}
for _, strictErr := range tc.strictDecodingErrors {
if !strings.Contains(result.Error().Error(), strictErr) {
t.Fatalf("missing strict decoding error: %s from error: %s", strictErr, result.Error().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
strictDecodingErrors []string
strictDecodingWarnings []string
}{
{
name: "strict-validation",
opts: metav1.PatchOptions{
FieldValidation: "Strict",
FieldManager: "mgr",
},
strictDecodingErrors: []string{
`key "paused" already set in map`,
`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 && len(tc.strictDecodingErrors) == 0 {
t.Fatalf("unexpected apply err: %v", result.Error())
}
for _, strictErr := range tc.strictDecodingErrors {
if !strings.Contains(result.Error().Error(), strictErr) {
t.Fatalf("missing strict decoding error: %s from error: %s", strictErr, result.Error().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
strictDecodingErrors []string
strictDecodingWarnings []string
}{
{
name: "crd-post-strict-validation",
opts: metav1.PatchOptions{
FieldValidation: "Strict",
},
body: crdInvalidBody,
strictDecodingErrors: []string{
`duplicate field "spec.ports[0].hostPort"`,
`duplicate field "spec.knownField1"`,
`duplicate field "spec.unknownDupe"`,
`unknown field "spec.ports[0].unknownNested"`,
`unknown field "spec.unknown1"`,
`unknown field "spec.unknownDupe"`,
},
},
{
name: "crd-post-warn-validation",
opts: metav1.PatchOptions{
FieldValidation: "Warn",
},
body: crdInvalidBody,
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-post-ignore-validation",
opts: metav1.PatchOptions{
FieldValidation: "Ignore",
},
body: crdInvalidBody,
},
{
name: "crd-post-no-validation",
body: crdInvalidBody,
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-post-strict-validation-yaml",
opts: metav1.PatchOptions{
FieldValidation: "Strict",
},
body: crdInvalidBodyYAML,
contentType: "application/yaml",
strictDecodingErrors: []string{
`line 10: key "unknownDupe" already set in map`,
`line 12: key "knownField1" already set in map`,
`line 18: key "hostPort" already set in map`,
`unknown field "spec.ports[0].unknownNested"`,
`unknown field "spec.unknown1"`,
`unknown field "spec.unknownDupe"`,
},
},
{
name: "crd-post-warn-validation-yaml",
opts: metav1.PatchOptions{
FieldValidation: "Warn",
},
body: crdInvalidBodyYAML,
contentType: "application/yaml",
strictDecodingWarnings: []string{
`line 10: key "unknownDupe" already set in map`,
`line 12: key "knownField1" already set in map`,
`line 18: key "hostPort" already set in map`,
`unknown field "spec.ports[0].unknownNested"`,
`unknown field "spec.unknown1"`,
`unknown field "spec.unknownDupe"`,
},
},
{
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 10: key "unknownDupe" already set in map`,
`line 12: key "knownField1" already set in map`,
`line 18: key "hostPort" already set in map`,
`unknown field "spec.ports[0].unknownNested"`,
`unknown field "spec.unknown1"`,
`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 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 && len(tc.strictDecodingErrors) == 0 {
t.Fatalf("unexpected post err: %v", result.Error())
}
if result.Error() == nil && len(tc.strictDecodingErrors) > 0 {
t.Fatalf("unexpected post succeeded")
}
for _, strictErr := range tc.strictDecodingErrors {
if !strings.Contains(result.Error().Error(), strictErr) {
t.Fatalf("missing strict decoding error: %s from error: %s", strictErr, result.Error().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
strictDecodingErrors []string
strictDecodingWarnings []string
}{
{
name: "schemaless-crd-post-strict-validation",
opts: metav1.PatchOptions{
FieldValidation: "Strict",
},
body: crdInvalidBody,
strictDecodingErrors: []string{
`duplicate field "spec.ports[0].hostPort"`,
`duplicate field "spec.knownField1"`,
`duplicate field "spec.unknownDupe"`,
`unknown field "spec.ports[0].unknownNested"`,
},
},
{
name: "schemaless-crd-post-warn-validation",
opts: metav1.PatchOptions{
FieldValidation: "Warn",
},
body: crdInvalidBody,
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-post-ignore-validation",
opts: metav1.PatchOptions{
FieldValidation: "Ignore",
},
body: crdInvalidBody,
},
{
name: "schemaless-crd-post-no-validation",
body: crdInvalidBody,
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-post-strict-validation-yaml",
opts: metav1.PatchOptions{
FieldValidation: "Strict",
},
body: crdInvalidBodyYAML,
contentType: "application/yaml",
strictDecodingErrors: []string{
`line 10: key "unknownDupe" already set in map`,
`line 12: key "knownField1" already set in map`,
`line 18: key "hostPort" already set in map`,
`unknown field "spec.ports[0].unknownNested"`,
},
},
{
name: "schemaless-crd-post-warn-validation-yaml",
opts: metav1.PatchOptions{
FieldValidation: "Warn",
},
body: crdInvalidBodyYAML,
contentType: "application/yaml",
strictDecodingWarnings: []string{
`line 10: key "unknownDupe" already set in map`,
`line 12: key "knownField1" already set in map`,
`line 18: key "hostPort" already set in map`,
`unknown field "spec.ports[0].unknownNested"`,
},
},
{
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 10: key "unknownDupe" already set in map`,
`line 12: key "knownField1" already set in map`,
`line 18: key "hostPort" already set in map`,
`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 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 && len(tc.strictDecodingErrors) == 0 {
t.Fatalf("unexpected post err: %v", result.Error())
}
if result.Error() == nil && len(tc.strictDecodingErrors) > 0 {
t.Fatalf("unexpected post succeeded")
}
for _, strictErr := range tc.strictDecodingErrors {
if !strings.Contains(result.Error().Error(), strictErr) {
t.Fatalf("missing strict decoding error: %s from error: %s", strictErr, result.Error().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
strictDecodingErrors []string
strictDecodingWarnings []string
}{
{
name: "crd-put-strict-validation",
opts: metav1.PatchOptions{
FieldValidation: "Strict",
},
putBody: crdInvalidBody,
strictDecodingErrors: []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-put-warn-validation",
opts: metav1.PatchOptions{
FieldValidation: "Warn",
},
putBody: crdInvalidBody,
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-put-ignore-validation",
opts: metav1.PatchOptions{
FieldValidation: "Ignore",
},
putBody: crdInvalidBody,
},
{
name: "crd-put-no-validation",
putBody: crdInvalidBody,
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-put-strict-validation-yaml",
opts: metav1.PatchOptions{
FieldValidation: "Strict",
},
putBody: crdInvalidBodyYAML,
contentType: "application/yaml",
strictDecodingErrors: []string{
`line 10: key "unknownDupe" already set in map`,
`line 12: key "knownField1" already set in map`,
`line 18: key "hostPort" already set in map`,
`unknown field "spec.ports[0].unknownNested"`,
`unknown field "spec.unknown1"`,
`unknown field "spec.unknownDupe"`,
},
},
{
name: "crd-put-warn-validation-yaml",
opts: metav1.PatchOptions{
FieldValidation: "Warn",
},
putBody: crdInvalidBodyYAML,
contentType: "application/yaml",
strictDecodingWarnings: []string{
`line 10: key "unknownDupe" already set in map`,
`line 12: key "knownField1" already set in map`,
`line 18: key "hostPort" already set in map`,
`unknown field "spec.ports[0].unknownNested"`,
`unknown field "spec.unknown1"`,
`unknown field "spec.unknownDupe"`,
},
},
{
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 10: key "unknownDupe" already set in map`,
`line 12: key "knownField1" already set in map`,
`line 18: key "hostPort" already set in map`,
`unknown field "spec.ports[0].unknownNested"`,
`unknown field "spec.unknown1"`,
`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 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 && len(tc.strictDecodingErrors) == 0 {
t.Fatalf("unexpected put err: %v", result.Error())
}
if result.Error() == nil && len(tc.strictDecodingErrors) > 0 {
t.Fatalf("unexpected patch succeeded")
}
for _, strictErr := range tc.strictDecodingErrors {
if !strings.Contains(result.Error().Error(), strictErr) {
t.Fatalf("missing strict decoding error: %s from error: %s", strictErr, result.Error().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
strictDecodingErrors []string
strictDecodingWarnings []string
}{
{
name: "schemaless-crd-put-strict-validation",
opts: metav1.PatchOptions{
FieldValidation: "Strict",
},
putBody: crdInvalidBody,
strictDecodingErrors: []string{
`duplicate field "spec.unknownDupe"`,
`duplicate field "spec.knownField1"`,
`duplicate field "spec.ports[0].hostPort"`,
`unknown field "spec.ports[0].unknownNested"`,
},
},
{
name: "schemaless-crd-put-warn-validation",
opts: metav1.PatchOptions{
FieldValidation: "Warn",
},
putBody: crdInvalidBody,
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-put-ignore-validation",
opts: metav1.PatchOptions{
FieldValidation: "Ignore",
},
putBody: crdInvalidBody,
},
{
name: "schemaless-crd-put-no-validation",
putBody: crdInvalidBody,
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-put-strict-validation-yaml",
opts: metav1.PatchOptions{
FieldValidation: "Strict",
},
putBody: crdInvalidBodyYAML,
contentType: "application/yaml",
strictDecodingErrors: []string{
`line 10: key "unknownDupe" already set in map`,
`line 12: key "knownField1" already set in map`,
`line 18: key "hostPort" already set in map`,
`unknown field "spec.ports[0].unknownNested"`,
},
},
{
name: "schemaless-crd-put-warn-validation-yaml",
opts: metav1.PatchOptions{
FieldValidation: "Warn",
},
putBody: crdInvalidBodyYAML,
contentType: "application/yaml",
strictDecodingWarnings: []string{
`line 10: key "unknownDupe" already set in map`,
`line 12: key "knownField1" already set in map`,
`line 18: key "hostPort" already set in map`,
`unknown field "spec.ports[0].unknownNested"`,
},
},
{
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 10: key "unknownDupe" already set in map`,
`line 12: key "knownField1" already set in map`,
`line 18: key "hostPort" already set in map`,
`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 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 && len(tc.strictDecodingErrors) == 0 {
t.Fatalf("unexpected put err: %v", result.Error())
}
if result.Error() == nil && len(tc.strictDecodingErrors) > 0 {
t.Fatalf("unexpected patch succeeded")
}
for _, strictErr := range tc.strictDecodingErrors {
if !strings.Contains(result.Error().Error(), strictErr) {
t.Fatalf("missing strict decoding error: %s from error: %s", strictErr, result.Error().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
strictDecodingErrors []string
strictDecodingWarnings []string
}{
{
name: "crd-merge-patch-strict-validation",
patchType: types.MergePatchType,
opts: metav1.PatchOptions{
FieldValidation: "Strict",
},
body: mergePatchBody,
strictDecodingErrors: []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-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,
strictDecodingErrors: []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-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 && len(tc.strictDecodingErrors) == 0 {
t.Fatalf("unexpected patch err: %v", result.Error())
}
if result.Error() == nil && len(tc.strictDecodingErrors) > 0 {
t.Fatalf("unexpected patch succeeded")
}
for _, strictErr := range tc.strictDecodingErrors {
if !strings.Contains(result.Error().Error(), strictErr) {
t.Fatalf("missing strict decoding error: %s from error: %s", strictErr, result.Error().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
strictDecodingErrors []string
strictDecodingWarnings []string
}{
{
name: "schemaless-crd-merge-patch-strict-validation",
patchType: types.MergePatchType,
opts: metav1.PatchOptions{
FieldValidation: "Strict",
},
body: mergePatchBody,
strictDecodingErrors: []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-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,
strictDecodingErrors: []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-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 && len(tc.strictDecodingErrors) == 0 {
t.Fatalf("unexpected patch err: %v", result.Error())
}
if result.Error() == nil && len(tc.strictDecodingErrors) > 0 {
t.Fatalf("unexpected patch succeeded")
}
for _, strictErr := range tc.strictDecodingErrors {
if !strings.Contains(result.Error().Error(), strictErr) {
t.Fatalf("missing strict decoding error: %s from error: %s", strictErr, result.Error().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
strictDecodingErrors []string
strictDecodingWarnings []string
}{
{
name: "strict-validation",
opts: metav1.PatchOptions{
FieldValidation: "Strict",
FieldManager: "mgr",
},
strictDecodingErrors: []string{
`key "knownField1" already set in map`,
`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 && len(tc.strictDecodingErrors) == 0 {
t.Fatalf("unexpected apply err: %v", result.Error())
}
if result.Error() == nil && len(tc.strictDecodingErrors) > 0 {
t.Fatalf("unexpected apply succeeded")
}
for _, strictErr := range tc.strictDecodingErrors {
if !strings.Contains(result.Error().Error(), strictErr) {
t.Fatalf("missing strict decoding error: %s from error: %s", strictErr, result.Error().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
strictDecodingErrors []string
strictDecodingWarnings []string
}{
{
name: "schemaless-strict-validation",
opts: metav1.PatchOptions{
FieldValidation: "Strict",
FieldManager: "mgr",
},
strictDecodingErrors: []string{
`key "knownField1" already set in map`,
`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 && len(tc.strictDecodingErrors) == 0 {
t.Fatalf("unexpected apply err: %v", result.Error())
}
if result.Error() == nil && len(tc.strictDecodingErrors) > 0 {
t.Fatalf("unexpected apply succeeded")
}
for _, strictErr := range tc.strictDecodingErrors {
if !strings.Contains(result.Error().Error(), strictErr) {
t.Fatalf("missing strict decoding error: %s from error: %s", strictErr, result.Error().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
strictDecodingErrors []string
strictDecodingWarnings []string
}{
{
name: "strict-validation",
opts: metav1.PatchOptions{
FieldValidation: "Strict",
FieldManager: "mgr",
},
strictDecodingErrors: []string{
`key "knownField1" already set in map`,
`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 && len(tc.strictDecodingErrors) == 0 {
t.Fatalf("unexpected apply err: %v", result.Error())
}
if result.Error() == nil && len(tc.strictDecodingErrors) > 0 {
t.Fatalf("unexpected apply succeeded")
}
for _, strictErr := range tc.strictDecodingErrors {
if !strings.Contains(result.Error().Error(), strictErr) {
t.Fatalf("missing strict decoding error: %s from error: %s", strictErr, result.Error().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
strictDecodingErrors []string
strictDecodingWarnings []string
}{
{
name: "schemaless-strict-validation",
opts: metav1.PatchOptions{
FieldValidation: "Strict",
FieldManager: "mgr",
},
strictDecodingErrors: []string{
`key "knownField1" already set in map`,
`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 && len(tc.strictDecodingErrors) == 0 {
t.Fatalf("unexpected apply err: %v", result.Error())
}
if result.Error() == nil && len(tc.strictDecodingErrors) > 0 {
t.Fatalf("unexpected apply succeeded")
}
for _, strictErr := range tc.strictDecodingErrors {
if !strings.Contains(result.Error().Error(), strictErr) {
t.Fatalf("missing strict decoding error: %s from error: %s", strictErr, result.Error().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.T, 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
}