From e29d84f9a3075a6bd769e675cdada1bcabbf6da7 Mon Sep 17 00:00:00 2001 From: Kevin Delgado Date: Wed, 18 Aug 2021 02:25:36 +0000 Subject: [PATCH 01/64] Strict schema validation for POST and PUT --- .../apimachinery/pkg/runtime/interfaces.go | 2 + .../pkg/runtime/serializer/codec_factory.go | 13 ++++- .../apiserver/pkg/endpoints/apiserver_test.go | 55 +++++++++++++++++++ .../pkg/endpoints/handlers/create.go | 7 ++- .../apiserver/pkg/endpoints/handlers/patch.go | 6 ++ .../apiserver/pkg/endpoints/handlers/rest.go | 9 +++ .../pkg/endpoints/handlers/update.go | 7 ++- 7 files changed, 96 insertions(+), 3 deletions(-) diff --git a/staging/src/k8s.io/apimachinery/pkg/runtime/interfaces.go b/staging/src/k8s.io/apimachinery/pkg/runtime/interfaces.go index 3e1fab1d11019..ecb68e529d187 100644 --- a/staging/src/k8s.io/apimachinery/pkg/runtime/interfaces.go +++ b/staging/src/k8s.io/apimachinery/pkg/runtime/interfaces.go @@ -125,6 +125,8 @@ type SerializerInfo struct { // PrettySerializer, if set, can serialize this object in a form biased towards // readability. PrettySerializer Serializer + // StrictSerializer errors on unknown fields when deserializing an object + StrictSerializer Serializer // StreamSerializer, if set, describes the streaming serialization format // for this media type. StreamSerializer *StreamSerializerInfo diff --git a/staging/src/k8s.io/apimachinery/pkg/runtime/serializer/codec_factory.go b/staging/src/k8s.io/apimachinery/pkg/runtime/serializer/codec_factory.go index e55ab94d1475b..12cab8d82bd57 100644 --- a/staging/src/k8s.io/apimachinery/pkg/runtime/serializer/codec_factory.go +++ b/staging/src/k8s.io/apimachinery/pkg/runtime/serializer/codec_factory.go @@ -40,6 +40,7 @@ type serializerType struct { Serializer runtime.Serializer PrettySerializer runtime.Serializer + StrictSerializer runtime.Serializer AcceptStreamContentTypes []string StreamContentType string @@ -69,11 +70,19 @@ func newSerializersForScheme(scheme *runtime.Scheme, mf json.MetaFactory, option json.SerializerOptions{Yaml: false, Pretty: true, Strict: options.Strict}, ) } - + strictJSONSerializer := json.NewSerializerWithOptions( + mf, scheme, scheme, + json.SerializerOptions{Yaml: false, Pretty: false, Strict: true}, + ) + jsonSerializerType.StrictSerializer = strictJSONSerializer yamlSerializer := json.NewSerializerWithOptions( mf, scheme, scheme, json.SerializerOptions{Yaml: true, Pretty: false, Strict: options.Strict}, ) + strictYAMLSerializer := json.NewSerializerWithOptions( + mf, scheme, scheme, + json.SerializerOptions{Yaml: true, Pretty: false, Strict: true}, + ) protoSerializer := protobuf.NewSerializer(scheme, scheme) protoRawSerializer := protobuf.NewRawSerializer(scheme, scheme) @@ -85,6 +94,7 @@ func newSerializersForScheme(scheme *runtime.Scheme, mf json.MetaFactory, option FileExtensions: []string{"yaml"}, EncodesAsText: true, Serializer: yamlSerializer, + StrictSerializer: strictYAMLSerializer, }, { AcceptContentTypes: []string{runtime.ContentTypeProtobuf}, @@ -187,6 +197,7 @@ func newCodecFactory(scheme *runtime.Scheme, serializers []serializerType) Codec EncodesAsText: d.EncodesAsText, Serializer: d.Serializer, PrettySerializer: d.PrettySerializer, + StrictSerializer: d.StrictSerializer, } mediaType, _, err := mime.ParseMediaType(info.MediaType) diff --git a/staging/src/k8s.io/apiserver/pkg/endpoints/apiserver_test.go b/staging/src/k8s.io/apiserver/pkg/endpoints/apiserver_test.go index 3f0ee9c5c74b4..15af65c60135d 100644 --- a/staging/src/k8s.io/apiserver/pkg/endpoints/apiserver_test.go +++ b/staging/src/k8s.io/apiserver/pkg/endpoints/apiserver_test.go @@ -4388,6 +4388,61 @@ func (storage *SimpleRESTStorageWithDeleteCollection) DeleteCollection(ctx conte return nil, nil } +func TestStrictValidation(t *testing.T) { + strictDecoderErr := "strict decoder error for" + // TODO: add test cases for yaml validation and protobuf early exit. + tests := []struct { + path string + verb string + data []byte + contentType string + errContains string + }{ + + {path: "/namespaces/default/simples", verb: "POST", data: []byte(`{"kind":"Simple","apiVersion":"test.group/version","metadata":{"creationTimestamp":null},"other":"bar","unknown":"baz"}`), errContains: strictDecoderErr}, + {path: "/namespaces/default/simples/id", verb: "PUT", data: []byte(`{"kind": "Simple", "apiVersion": "test.group/version", "metadata": {"name": "id", "creationTimestamp": null}, "other": "bar", "unknown": "baz"}`), errContains: strictDecoderErr}, + // TODO: there's a bug currently where query params are being stripped so this test does not pass yet. + //{path: "/namespaces/default/simples/id", verb: "PATCH", data: []byte(`{"labels":{"foo":"bar"}}`), contentType: "application/merge-patch+json; charset=UTF-8", errContains: notImplementedErr}, + } + + server := httptest.NewServer(handle(map[string]rest.Storage{ + "simples": &SimpleRESTStorageWithDeleteCollection{ + SimpleRESTStorage{ + item: genericapitesting.Simple{ + ObjectMeta: metav1.ObjectMeta{ + Name: "id", + Namespace: "", + UID: "uid", + }, + Other: "bar", + }, + }, + }, + "simples/subsimple": &SimpleXGSubresourceRESTStorage{ + item: genericapitesting.SimpleXGSubresource{ + SubresourceInfo: "foo", + }, + itemGVK: testGroup2Version.WithKind("SimpleXGSubresource"), + }, + })) + defer server.Close() + for _, test := range tests { + baseURL := server.URL + "/" + prefix + "/" + testGroupVersion.Group + "/" + testGroupVersion.Version + response := runRequest(t, baseURL+test.path, test.verb, test.data, test.contentType) + if response.StatusCode == http.StatusBadRequest { + t.Fatalf("unexpected BadRequest: %#v", response) + } + response = runRequest(t, baseURL+test.path+"?validate=strict", test.verb, test.data, test.contentType) + buf := new(bytes.Buffer) + buf.ReadFrom(response.Body) + // TODO: better way of doing than string comparison since we are getting a response instead of a regular go error? + if response.StatusCode != http.StatusBadRequest && !strings.Contains(buf.String(), test.errContains) { + t.Fatalf("unexpected response: %#v", response) + } + } + +} + func TestDryRunDisabled(t *testing.T) { defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.DryRun, false)() diff --git a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/create.go b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/create.go index 51ad05bf16baf..de963a17bebcc 100644 --- a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/create.go +++ b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/create.go @@ -92,7 +92,12 @@ func createHandler(r rest.NamedCreater, scope *RequestScope, admit admission.Int return } - decoder := scope.Serializer.DecoderToVersion(s.Serializer, scope.HubGroupVersion) + decodeSerializer := s.Serializer + // TODO: put behind feature flag? + if strictValidation(req.URL) { + decodeSerializer = s.StrictSerializer + } + decoder := scope.Serializer.DecoderToVersion(decodeSerializer, scope.HubGroupVersion) body, err := limitedReadBody(req, scope.MaxRequestBodyBytes) if err != nil { diff --git a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/patch.go b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/patch.go index 1138bc0aee3bc..350678d43df55 100644 --- a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/patch.go +++ b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/patch.go @@ -70,6 +70,12 @@ func PatchResource(r rest.Patcher, scope *RequestScope, admit admission.Interfac return } + // TODO: put behind feature flag? + if strictValidation(req.URL) { + scope.err(errors.NewBadRequest("strict validation is not supported yet"), w, req) + return + } + // Do this first, otherwise name extraction can fail for unrecognized content types // TODO: handle this in negotiation contentType := req.Header.Get("Content-Type") diff --git a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/rest.go b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/rest.go index ef43939be0713..eba132a3e4e8b 100644 --- a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/rest.go +++ b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/rest.go @@ -457,6 +457,15 @@ func isDryRun(url *url.URL) bool { return len(url.Query()["dryRun"]) != 0 } +// TODO: currently strict validation is set via +// query param. Alternatively we could use a request header. +// TODO: maybe we should check content-type here and error for protobuf (ie take the full req instead of just the URL?) +// TODO: what do we want the query param to actually be (I went with ?validate=strict but am open to whatever) +func strictValidation(url *url.URL) bool { + validateParam := url.Query()["validate"] + return len(validateParam) == 1 && validateParam[0] == "strict" +} + type etcdError interface { Code() grpccodes.Code Error() string diff --git a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/update.go b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/update.go index bfd8cb4b51473..40a8b4bcddb56 100644 --- a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/update.go +++ b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/update.go @@ -103,7 +103,12 @@ func UpdateResource(r rest.Updater, scope *RequestScope, admit admission.Interfa original := r.New() trace.Step("About to convert to expected version") - decoder := scope.Serializer.DecoderToVersion(s.Serializer, scope.HubGroupVersion) + decodeSerializer := s.Serializer + // TODO: put behind feature flag? + if strictValidation(req.URL) { + decodeSerializer = s.StrictSerializer + } + decoder := scope.Serializer.DecoderToVersion(decodeSerializer, scope.HubGroupVersion) obj, gvk, err := decoder.Decode(body, &defaultGVK, original) if err != nil { err = transformDecodeError(scope.Typer, err, original, gvk, body) From 2591d7db8b9d75e5ac85f1521432f964277eb673 Mon Sep 17 00:00:00 2001 From: Kevin Delgado Date: Wed, 18 Aug 2021 19:57:13 +0000 Subject: [PATCH 02/64] Add benchmarking for strict validation --- .../apiserver/pkg/endpoints/apiserver_test.go | 94 ++++++++++++++++++- 1 file changed, 92 insertions(+), 2 deletions(-) diff --git a/staging/src/k8s.io/apiserver/pkg/endpoints/apiserver_test.go b/staging/src/k8s.io/apiserver/pkg/endpoints/apiserver_test.go index 15af65c60135d..04f7a958e31a3 100644 --- a/staging/src/k8s.io/apiserver/pkg/endpoints/apiserver_test.go +++ b/staging/src/k8s.io/apiserver/pkg/endpoints/apiserver_test.go @@ -4351,8 +4351,8 @@ func TestUpdateChecksAPIVersion(t *testing.T) { } } -// runRequest is used by TestDryRun since it runs the test twice in a -// row with a slightly different URL (one has ?dryRun, one doesn't). +// runRequest is used by TestDryRun and TestStrictValidation since it runs the test +// twice in a row with a slightly different URL (one has ?dryRun, one doesn't). func runRequest(t *testing.T, path, verb string, data []byte, contentType string) *http.Response { request, err := http.NewRequest(verb, path, bytes.NewBuffer(data)) if err != nil { @@ -4368,6 +4368,22 @@ func runRequest(t *testing.T, path, verb string, data []byte, contentType string return response } +// runRequestOrDie is like runRequest, but used for benchmarking. +func runRequestOrDie(path, verb string, data []byte, contentType string) *http.Response { + request, err := http.NewRequest(verb, path, bytes.NewBuffer(data)) + if err != nil { + panic(err) + } + if contentType != "" { + request.Header.Set("Content-Type", contentType) + } + response, err := http.DefaultClient.Do(request) + if err != nil { + panic(err) + } + return response +} + // encodeOrFatal is used by TestDryRun to parse an object and stop right // away if it fails. func encodeOrFatal(t *testing.T, obj runtime.Object) []byte { @@ -4443,6 +4459,80 @@ func TestStrictValidation(t *testing.T) { } +// TODO: this is a pretty crude benchmark since it only operates on the simples resource +// I imagine we want to benchmark some meatier resources too, because maybe the discrepancy in +// performance is greater for bigger objects? +func BenchmarkNonStrictValidationSimples(b *testing.B) { + server := httptest.NewServer(handle(map[string]rest.Storage{ + "simples": &SimpleRESTStorageWithDeleteCollection{ + SimpleRESTStorage{ + item: genericapitesting.Simple{ + ObjectMeta: metav1.ObjectMeta{ + Name: "id", + Namespace: "", + UID: "uid", + }, + Other: "bar", + }, + }, + }, + "simples/subsimple": &SimpleXGSubresourceRESTStorage{ + item: genericapitesting.SimpleXGSubresource{ + SubresourceInfo: "foo", + }, + itemGVK: testGroup2Version.WithKind("SimpleXGSubresource"), + }, + })) + defer server.Close() + + for n := 0; n < b.N; n++ { + postPath := "/namespaces/default/simples" + postData := []byte(`{"kind":"Simple","apiVersion":"test.group/version","metadata":{"creationTimestamp":null},"other":"bar"}`) + basePostURL := server.URL + "/" + prefix + "/" + testGroupVersion.Group + "/" + testGroupVersion.Version + _ = runRequestOrDie(basePostURL+postPath, "POST", postData, "") + + putPath := "/namespaces/default/simples/id" + putData := []byte(`{"kind": "Simple", "apiVersion": "test.group/version", "metadata": {"name": "id", "creationTimestamp": null}, "other": "bar", "unknown": "baz"}`) + basePutURL := server.URL + "/" + prefix + "/" + testGroupVersion.Group + "/" + testGroupVersion.Version + _ = runRequestOrDie(basePutURL+putPath, "POST", putData, "") + } +} +func BenchmarkStrictValidationSimples(b *testing.B) { + server := httptest.NewServer(handle(map[string]rest.Storage{ + "simples": &SimpleRESTStorageWithDeleteCollection{ + SimpleRESTStorage{ + item: genericapitesting.Simple{ + ObjectMeta: metav1.ObjectMeta{ + Name: "id", + Namespace: "", + UID: "uid", + }, + Other: "bar", + }, + }, + }, + "simples/subsimple": &SimpleXGSubresourceRESTStorage{ + item: genericapitesting.SimpleXGSubresource{ + SubresourceInfo: "foo", + }, + itemGVK: testGroup2Version.WithKind("SimpleXGSubresource"), + }, + })) + defer server.Close() + + for n := 0; n < b.N; n++ { + postPath := "/namespaces/default/simples" + postData := []byte(`{"kind":"Simple","apiVersion":"test.group/version","metadata":{"creationTimestamp":null},"other":"bar"}`) + basePostURL := server.URL + "/" + prefix + "/" + testGroupVersion.Group + "/" + testGroupVersion.Version + _ = runRequestOrDie(basePostURL+postPath+"?validate=strict", "POST", postData, "") + + putPath := "/namespaces/default/simples/id" + putData := []byte(`{"kind": "Simple", "apiVersion": "test.group/version", "metadata": {"name": "id", "creationTimestamp": null}, "other": "bar", "unknown": "baz"}`) + basePutURL := server.URL + "/" + prefix + "/" + testGroupVersion.Group + "/" + testGroupVersion.Version + _ = runRequestOrDie(basePutURL+putPath+"?validate=strict", "POST", putData, "") + } +} + func TestDryRunDisabled(t *testing.T) { defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.DryRun, false)() From f424ec6b61dd39131bd9da594625bb22684a02a9 Mon Sep 17 00:00:00 2001 From: Kevin Delgado Date: Tue, 14 Sep 2021 20:58:28 +0000 Subject: [PATCH 03/64] add benchmarking to integration tests --- test/integration/apiserver/apiserver_test.go | 106 ++++++++++++++++++- 1 file changed, 102 insertions(+), 4 deletions(-) diff --git a/test/integration/apiserver/apiserver_test.go b/test/integration/apiserver/apiserver_test.go index f40d97683f8a6..f6ab226ddd3e5 100644 --- a/test/integration/apiserver/apiserver_test.go +++ b/test/integration/apiserver/apiserver_test.go @@ -20,6 +20,7 @@ import ( "bytes" "context" "encoding/json" + "flag" "fmt" "io" "io/ioutil" @@ -71,19 +72,19 @@ import ( "k8s.io/kubernetes/test/integration/framework" ) -func setup(t *testing.T, groupVersions ...schema.GroupVersion) (*httptest.Server, clientset.Interface, framework.CloseFunc) { +func setup(t testing.TB, groupVersions ...schema.GroupVersion) (*httptest.Server, clientset.Interface, framework.CloseFunc) { return setupWithResources(t, groupVersions, nil) } -func setupWithOptions(t *testing.T, opts *framework.ControlPlaneConfigOptions, groupVersions ...schema.GroupVersion) (*httptest.Server, clientset.Interface, framework.CloseFunc) { +func setupWithOptions(t testing.TB, opts *framework.ControlPlaneConfigOptions, groupVersions ...schema.GroupVersion) (*httptest.Server, clientset.Interface, framework.CloseFunc) { return setupWithResourcesWithOptions(t, opts, groupVersions, nil) } -func setupWithResources(t *testing.T, groupVersions []schema.GroupVersion, resources []schema.GroupVersionResource) (*httptest.Server, clientset.Interface, framework.CloseFunc) { +func setupWithResources(t testing.TB, groupVersions []schema.GroupVersion, resources []schema.GroupVersionResource) (*httptest.Server, clientset.Interface, framework.CloseFunc) { return setupWithResourcesWithOptions(t, &framework.ControlPlaneConfigOptions{}, groupVersions, resources) } -func setupWithResourcesWithOptions(t *testing.T, opts *framework.ControlPlaneConfigOptions, groupVersions []schema.GroupVersion, resources []schema.GroupVersionResource) (*httptest.Server, clientset.Interface, framework.CloseFunc) { +func setupWithResourcesWithOptions(t testing.TB, opts *framework.ControlPlaneConfigOptions, groupVersions []schema.GroupVersion, resources []schema.GroupVersionResource) (*httptest.Server, clientset.Interface, framework.CloseFunc) { controlPlaneConfig := framework.NewIntegrationTestControlPlaneConfigWithOptions(opts) if len(groupVersions) > 0 || len(resources) > 0 { resourceConfig := controlplane.DefaultAPIResourceConfigSource() @@ -2329,6 +2330,103 @@ func TestDedupOwnerReferences(t *testing.T) { } } +func BenchmarkFieldValidation(b *testing.B) { + _, client, closeFn := setup(b) + defer closeFn() + flag.Lookup("v").Value.Set("0") + + baseDeploy := `{ + "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" + }] + } + } + } + }` + + // TODO: add bigger objects to benchmarks table once confirmed that this is how we want to do benchmarking. + // TODO: split POST and PUT into their own test-cases. + benchmarks := []struct { + name string + params map[string]string + }{ + { + name: "nonstrict-deployment", + params: map[string]string{}, + }, + { + name: "strict-deployment", + params: map[string]string{"validate": "strict"}, + }, + } + + for _, bm := range benchmarks { + b.Run(bm.name, func(b *testing.B) { + b.ResetTimer() + b.ReportAllocs() + for n := 0; n < b.N; n++ { + // append the timestamp to the name so that we don't hit conflicts when running the test multiple times + // (i.e. without it -count=n for n>1 will fail, this might be from not tearing stuff down properly). + deployName := fmt.Sprintf("dep-%s-%d-%d-%d", bm.name, n, b.N, time.Now().UnixNano()) + deployString := fmt.Sprintf(baseDeploy, fmt.Sprintf(`"%s"`, deployName)) + deploy := []byte(deployString) + + appsPath := "/apis/apps/v1" + postReq := client.CoreV1().RESTClient().Post(). + AbsPath(appsPath). + Namespace("default"). + Resource("deployments") + for k, v := range bm.params { + postReq = postReq.Param(k, v) + } + + _, err := postReq.Body(deploy). + DoRaw(context.TODO()) + if err != nil { + panic(err) + } + + // TODO: put PUT in a different bench case than POST (ie. have a baseReq) be a part of the test case. + putReq := client.CoreV1().RESTClient().Put(). + AbsPath(appsPath). + Namespace("default"). + Resource("deployments"). + Name(deployName) + for k, v := range bm.params { + putReq = putReq.Param(k, v) + } + + _, err = putReq.Body(deploy). + DoRaw(context.TODO()) + if err != nil { + panic(err) + } + } + }) + + } +} + type dependentClient struct { t *testing.T client dynamic.ResourceInterface From 992329052025a3a28086e05086dbfc343ce70f7d Mon Sep 17 00:00:00 2001 From: Kevin Delgado Date: Wed, 15 Sep 2021 00:33:29 +0000 Subject: [PATCH 04/64] fieldValidation returns enum, error --- .../apiserver/pkg/endpoints/apiserver_test.go | 74 ++++++------------- .../pkg/endpoints/handlers/create.go | 9 ++- .../apiserver/pkg/endpoints/handlers/patch.go | 9 ++- .../apiserver/pkg/endpoints/handlers/rest.go | 62 ++++++++++++++-- .../pkg/endpoints/handlers/update.go | 9 ++- test/integration/apiserver/apiserver_test.go | 13 +++- 6 files changed, 110 insertions(+), 66 deletions(-) diff --git a/staging/src/k8s.io/apiserver/pkg/endpoints/apiserver_test.go b/staging/src/k8s.io/apiserver/pkg/endpoints/apiserver_test.go index 04f7a958e31a3..1fedac9a29577 100644 --- a/staging/src/k8s.io/apiserver/pkg/endpoints/apiserver_test.go +++ b/staging/src/k8s.io/apiserver/pkg/endpoints/apiserver_test.go @@ -4351,7 +4351,7 @@ func TestUpdateChecksAPIVersion(t *testing.T) { } } -// runRequest is used by TestDryRun and TestStrictValidation since it runs the test +// runRequest is used by TestDryRun and TestFieldValidation since it runs the test // twice in a row with a slightly different URL (one has ?dryRun, one doesn't). func runRequest(t *testing.T, path, verb string, data []byte, contentType string) *http.Response { request, err := http.NewRequest(verb, path, bytes.NewBuffer(data)) @@ -4404,7 +4404,7 @@ func (storage *SimpleRESTStorageWithDeleteCollection) DeleteCollection(ctx conte return nil, nil } -func TestStrictValidation(t *testing.T) { +func TestFieldValidation(t *testing.T) { strictDecoderErr := "strict decoder error for" // TODO: add test cases for yaml validation and protobuf early exit. tests := []struct { @@ -4448,7 +4448,7 @@ func TestStrictValidation(t *testing.T) { if response.StatusCode == http.StatusBadRequest { t.Fatalf("unexpected BadRequest: %#v", response) } - response = runRequest(t, baseURL+test.path+"?validate=strict", test.verb, test.data, test.contentType) + response = runRequest(t, baseURL+test.path+"?fieldValidation=Strict", test.verb, test.data, test.contentType) buf := new(bytes.Buffer) buf.ReadFrom(response.Body) // TODO: better way of doing than string comparison since we are getting a response instead of a regular go error? @@ -4459,45 +4459,14 @@ func TestStrictValidation(t *testing.T) { } -// TODO: this is a pretty crude benchmark since it only operates on the simples resource -// I imagine we want to benchmark some meatier resources too, because maybe the discrepancy in -// performance is greater for bigger objects? -func BenchmarkNonStrictValidationSimples(b *testing.B) { - server := httptest.NewServer(handle(map[string]rest.Storage{ - "simples": &SimpleRESTStorageWithDeleteCollection{ - SimpleRESTStorage{ - item: genericapitesting.Simple{ - ObjectMeta: metav1.ObjectMeta{ - Name: "id", - Namespace: "", - UID: "uid", - }, - Other: "bar", - }, - }, - }, - "simples/subsimple": &SimpleXGSubresourceRESTStorage{ - item: genericapitesting.SimpleXGSubresource{ - SubresourceInfo: "foo", - }, - itemGVK: testGroup2Version.WithKind("SimpleXGSubresource"), - }, - })) - defer server.Close() - - for n := 0; n < b.N; n++ { - postPath := "/namespaces/default/simples" - postData := []byte(`{"kind":"Simple","apiVersion":"test.group/version","metadata":{"creationTimestamp":null},"other":"bar"}`) - basePostURL := server.URL + "/" + prefix + "/" + testGroupVersion.Group + "/" + testGroupVersion.Version - _ = runRequestOrDie(basePostURL+postPath, "POST", postData, "") - - putPath := "/namespaces/default/simples/id" - putData := []byte(`{"kind": "Simple", "apiVersion": "test.group/version", "metadata": {"name": "id", "creationTimestamp": null}, "other": "bar", "unknown": "baz"}`) - basePutURL := server.URL + "/" + prefix + "/" + testGroupVersion.Group + "/" + testGroupVersion.Version - _ = runRequestOrDie(basePutURL+putPath, "POST", putData, "") +func BenchmarkFieldValidation(b *testing.B) { + benchmarks := []struct { + name string + queryParam string + }{ + {"nonstrict simples", ""}, + {"strict simples", "?fieldValidation=Strict"}, } -} -func BenchmarkStrictValidationSimples(b *testing.B) { server := httptest.NewServer(handle(map[string]rest.Storage{ "simples": &SimpleRESTStorageWithDeleteCollection{ SimpleRESTStorage{ @@ -4520,16 +4489,21 @@ func BenchmarkStrictValidationSimples(b *testing.B) { })) defer server.Close() - for n := 0; n < b.N; n++ { - postPath := "/namespaces/default/simples" - postData := []byte(`{"kind":"Simple","apiVersion":"test.group/version","metadata":{"creationTimestamp":null},"other":"bar"}`) - basePostURL := server.URL + "/" + prefix + "/" + testGroupVersion.Group + "/" + testGroupVersion.Version - _ = runRequestOrDie(basePostURL+postPath+"?validate=strict", "POST", postData, "") + for _, bm := range benchmarks { + b.Run(bm.name, func(b *testing.B) { + for n := 0; n < b.N; n++ { + postPath := "/namespaces/default/simples" + postData := []byte(`{"kind":"Simple","apiVersion":"test.group/version","metadata":{"creationTimestamp":null},"other":"bar"}`) + basePostURL := server.URL + "/" + prefix + "/" + testGroupVersion.Group + "/" + testGroupVersion.Version + _ = runRequestOrDie(basePostURL+postPath+bm.queryParam, "POST", postData, "") + + putPath := "/namespaces/default/simples/id" + putData := []byte(`{"kind": "Simple", "apiVersion": "test.group/version", "metadata": {"name": "id", "creationTimestamp": null}, "other": "bar", "unknown": "baz"}`) + basePutURL := server.URL + "/" + prefix + "/" + testGroupVersion.Group + "/" + testGroupVersion.Version + _ = runRequestOrDie(basePutURL+putPath+bm.queryParam, "PUT", putData, "") + } + }) - putPath := "/namespaces/default/simples/id" - putData := []byte(`{"kind": "Simple", "apiVersion": "test.group/version", "metadata": {"name": "id", "creationTimestamp": null}, "other": "bar", "unknown": "baz"}`) - basePutURL := server.URL + "/" + prefix + "/" + testGroupVersion.Group + "/" + testGroupVersion.Version - _ = runRequestOrDie(basePutURL+putPath+"?validate=strict", "POST", putData, "") } } diff --git a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/create.go b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/create.go index de963a17bebcc..b14a9db9d27f0 100644 --- a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/create.go +++ b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/create.go @@ -93,8 +93,13 @@ func createHandler(r rest.NamedCreater, scope *RequestScope, admit admission.Int } decodeSerializer := s.Serializer - // TODO: put behind feature flag? - if strictValidation(req.URL) { + // TODO: put behind feature gate? + validationDirective, err := fieldValidation(req) + if err != nil { + scope.err(err, w, req) + return + } + if validationDirective == strictFieldValidation { decodeSerializer = s.StrictSerializer } decoder := scope.Serializer.DecoderToVersion(decodeSerializer, scope.HubGroupVersion) diff --git a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/patch.go b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/patch.go index 350678d43df55..e7964e3f857ec 100644 --- a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/patch.go +++ b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/patch.go @@ -70,8 +70,13 @@ func PatchResource(r rest.Patcher, scope *RequestScope, admit admission.Interfac return } - // TODO: put behind feature flag? - if strictValidation(req.URL) { + // TODO: put behind feature gate? + validationDirective, err := fieldValidation(req) + if err != nil { + scope.err(err, w, req) + return + } + if validationDirective == strictFieldValidation { scope.err(errors.NewBadRequest("strict validation is not supported yet"), w, req) return } diff --git a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/rest.go b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/rest.go index eba132a3e4e8b..156aecd3a7a36 100644 --- a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/rest.go +++ b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/rest.go @@ -22,6 +22,7 @@ import ( "fmt" "io" "io/ioutil" + "mime" "net/http" "net/url" "strings" @@ -457,13 +458,60 @@ func isDryRun(url *url.URL) bool { return len(url.Query()["dryRun"]) != 0 } -// TODO: currently strict validation is set via -// query param. Alternatively we could use a request header. -// TODO: maybe we should check content-type here and error for protobuf (ie take the full req instead of just the URL?) -// TODO: what do we want the query param to actually be (I went with ?validate=strict but am open to whatever) -func strictValidation(url *url.URL) bool { - validateParam := url.Query()["validate"] - return len(validateParam) == 1 && validateParam[0] == "strict" +type fieldValidationDirective int + +const ( + ignoreFieldValidation fieldValidationDirective = iota + strictFieldValidation + warnFieldValidation +) + +// fieldValidation checks if the fieldValidation query parameter is set on the request, +// and if so ensures that the parameter is valid and that the request has a valid +// media type, because the list of media types that support field validation are a subset of +// all supported media types (only json and yaml supports field validation). +func fieldValidation(req *http.Request) (fieldValidationDirective, error) { + supportedMediaTypes := []string{"application/json", "application/yaml"} + supported := false + contentType := req.Header.Get("ContentType") + // TODO: not sure if it is okay to assume empty content type is a valid one + if contentType != "" { + for _, v := range strings.Split(contentType, ",") { + t, _, err := mime.ParseMediaType(v) + if err != nil { + return ignoreFieldValidation, errors.NewBadRequest(fmt.Sprintf("could not parse media type: %v", v)) + } + for _, mt := range supportedMediaTypes { + if t == mt { + supported = true + break + } + } + } + if !supported { + return ignoreFieldValidation, errors.NewBadRequest(fmt.Sprintf("fieldValidation parameter only supports media types types %v\n content type provided: %s", supportedMediaTypes, contentType)) + } + } + + validationParam := req.URL.Query()["fieldValidation"] + switch len(validationParam) { + case 0: + return ignoreFieldValidation, nil + case 1: + switch validationParam[0] { + case "Ignore": + return ignoreFieldValidation, nil + case "Strict": + return strictFieldValidation, nil + case "Warn": + return warnFieldValidation, nil + default: + return ignoreFieldValidation, errors.NewBadRequest(fmt.Sprintf("fieldValidation parameter unsupported: %v", validationParam)) + } + default: + return ignoreFieldValidation, errors.NewBadRequest(fmt.Sprintf("fieldValidation should only be one value: %v", validationParam)) + + } } type etcdError interface { diff --git a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/update.go b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/update.go index 40a8b4bcddb56..1d012318a3f5d 100644 --- a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/update.go +++ b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/update.go @@ -104,8 +104,13 @@ func UpdateResource(r rest.Updater, scope *RequestScope, admit admission.Interfa trace.Step("About to convert to expected version") decodeSerializer := s.Serializer - // TODO: put behind feature flag? - if strictValidation(req.URL) { + // TODO: put behind feature gate? + validationDirective, err := fieldValidation(req) + if err != nil { + scope.err(err, w, req) + return + } + if validationDirective == strictFieldValidation { decodeSerializer = s.StrictSerializer } decoder := scope.Serializer.DecoderToVersion(decodeSerializer, scope.HubGroupVersion) diff --git a/test/integration/apiserver/apiserver_test.go b/test/integration/apiserver/apiserver_test.go index f6ab226ddd3e5..12152bda02526 100644 --- a/test/integration/apiserver/apiserver_test.go +++ b/test/integration/apiserver/apiserver_test.go @@ -2330,6 +2330,8 @@ func TestDedupOwnerReferences(t *testing.T) { } } +// Benchmark field validation for strict vs non-strict +// TODO: add actual correctness integration tests too func BenchmarkFieldValidation(b *testing.B) { _, client, closeFn := setup(b) defer closeFn() @@ -2366,17 +2368,22 @@ func BenchmarkFieldValidation(b *testing.B) { // TODO: add bigger objects to benchmarks table once confirmed that this is how we want to do benchmarking. // TODO: split POST and PUT into their own test-cases. + // TODO: add test for "Warn" validation once it is implemented. benchmarks := []struct { name string params map[string]string }{ { - name: "nonstrict-deployment", + name: "default-ignore-validation-deployment", params: map[string]string{}, }, { - name: "strict-deployment", - params: map[string]string{"validate": "strict"}, + name: "ignore-validation-deployment", + params: map[string]string{"fieldValidation": "Ignore"}, + }, + { + name: "strict-validation-deployment", + params: map[string]string{"fieldValidation": "Strict"}, }, } From 3cb499ef9e556e17ac63802368439400d71a56fb Mon Sep 17 00:00:00 2001 From: Kevin Delgado Date: Tue, 21 Sep 2021 17:38:58 +0000 Subject: [PATCH 05/64] WIP start addressing PR feedback --- .../k8s.io/apimachinery/pkg/runtime/types.go | 7 +++--- .../apiserver/pkg/endpoints/apiserver_test.go | 22 +++---------------- .../apiserver/pkg/endpoints/handlers/rest.go | 10 +++++---- 3 files changed, 13 insertions(+), 26 deletions(-) diff --git a/staging/src/k8s.io/apimachinery/pkg/runtime/types.go b/staging/src/k8s.io/apimachinery/pkg/runtime/types.go index 31359f35f4512..c6271e1d5f6a3 100644 --- a/staging/src/k8s.io/apimachinery/pkg/runtime/types.go +++ b/staging/src/k8s.io/apimachinery/pkg/runtime/types.go @@ -41,9 +41,10 @@ type TypeMeta struct { } const ( - ContentTypeJSON string = "application/json" - ContentTypeYAML string = "application/yaml" - ContentTypeProtobuf string = "application/vnd.kubernetes.protobuf" + ContentTypeJSON string = "application/json" + ContentTypeJSONMergePatch string = "application/merge-patch+json" + ContentTypeYAML string = "application/yaml" + ContentTypeProtobuf string = "application/vnd.kubernetes.protobuf" ) // RawExtension is used to hold extensions in external versions. diff --git a/staging/src/k8s.io/apiserver/pkg/endpoints/apiserver_test.go b/staging/src/k8s.io/apiserver/pkg/endpoints/apiserver_test.go index 1fedac9a29577..eacfdfc7be6be 100644 --- a/staging/src/k8s.io/apiserver/pkg/endpoints/apiserver_test.go +++ b/staging/src/k8s.io/apiserver/pkg/endpoints/apiserver_test.go @@ -4353,7 +4353,7 @@ func TestUpdateChecksAPIVersion(t *testing.T) { // runRequest is used by TestDryRun and TestFieldValidation since it runs the test // twice in a row with a slightly different URL (one has ?dryRun, one doesn't). -func runRequest(t *testing.T, path, verb string, data []byte, contentType string) *http.Response { +func runRequest(t testing.TB, path, verb string, data []byte, contentType string) *http.Response { request, err := http.NewRequest(verb, path, bytes.NewBuffer(data)) if err != nil { t.Fatalf("unexpected error: %v", err) @@ -4368,22 +4368,6 @@ func runRequest(t *testing.T, path, verb string, data []byte, contentType string return response } -// runRequestOrDie is like runRequest, but used for benchmarking. -func runRequestOrDie(path, verb string, data []byte, contentType string) *http.Response { - request, err := http.NewRequest(verb, path, bytes.NewBuffer(data)) - if err != nil { - panic(err) - } - if contentType != "" { - request.Header.Set("Content-Type", contentType) - } - response, err := http.DefaultClient.Do(request) - if err != nil { - panic(err) - } - return response -} - // encodeOrFatal is used by TestDryRun to parse an object and stop right // away if it fails. func encodeOrFatal(t *testing.T, obj runtime.Object) []byte { @@ -4495,12 +4479,12 @@ func BenchmarkFieldValidation(b *testing.B) { postPath := "/namespaces/default/simples" postData := []byte(`{"kind":"Simple","apiVersion":"test.group/version","metadata":{"creationTimestamp":null},"other":"bar"}`) basePostURL := server.URL + "/" + prefix + "/" + testGroupVersion.Group + "/" + testGroupVersion.Version - _ = runRequestOrDie(basePostURL+postPath+bm.queryParam, "POST", postData, "") + _ = runRequest(b, basePostURL+postPath+bm.queryParam, "POST", postData, "") putPath := "/namespaces/default/simples/id" putData := []byte(`{"kind": "Simple", "apiVersion": "test.group/version", "metadata": {"name": "id", "creationTimestamp": null}, "other": "bar", "unknown": "baz"}`) basePutURL := server.URL + "/" + prefix + "/" + testGroupVersion.Group + "/" + testGroupVersion.Version - _ = runRequestOrDie(basePutURL+putPath+bm.queryParam, "PUT", putData, "") + _ = runRequest(b, basePutURL+putPath+bm.queryParam, "PUT", putData, "") } }) diff --git a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/rest.go b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/rest.go index 156aecd3a7a36..374b3d1f43b4a 100644 --- a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/rest.go +++ b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/rest.go @@ -471,17 +471,19 @@ const ( // media type, because the list of media types that support field validation are a subset of // all supported media types (only json and yaml supports field validation). func fieldValidation(req *http.Request) (fieldValidationDirective, error) { - supportedMediaTypes := []string{"application/json", "application/yaml"} + supportedContentTypes := []string{runtime.ContentTypeJSON, runtime.ContentTypeJSONMergePatch, runtime.ContentTypeYAML} supported := false - contentType := req.Header.Get("ContentType") + contentType := req.Header.Get("Content-Type") + //contentType := req.Header.Get("ContentType") // TODO: not sure if it is okay to assume empty content type is a valid one if contentType != "" { for _, v := range strings.Split(contentType, ",") { t, _, err := mime.ParseMediaType(v) + fmt.Printf("t = %+v\n", t) if err != nil { return ignoreFieldValidation, errors.NewBadRequest(fmt.Sprintf("could not parse media type: %v", v)) } - for _, mt := range supportedMediaTypes { + for _, mt := range supportedContentTypes { if t == mt { supported = true break @@ -489,7 +491,7 @@ func fieldValidation(req *http.Request) (fieldValidationDirective, error) { } } if !supported { - return ignoreFieldValidation, errors.NewBadRequest(fmt.Sprintf("fieldValidation parameter only supports media types types %v\n content type provided: %s", supportedMediaTypes, contentType)) + return ignoreFieldValidation, errors.NewBadRequest(fmt.Sprintf("fieldValidation parameter only supports content types %v\n content type provided: %s", supportedContentTypes, contentType)) } } From 526e0cdc8822297208ead38115e6514c6ca6dcfe Mon Sep 17 00:00:00 2001 From: Kevin Delgado Date: Wed, 29 Sep 2021 23:36:26 +0000 Subject: [PATCH 06/64] Address feedback, improve integration/bench tests --- .../apiserver/pkg/endpoints/apiserver_test.go | 37 ++- .../apiserver/pkg/endpoints/handlers/rest.go | 22 +- test/integration/apiserver/apiserver_test.go | 267 ++++++++++++++++-- .../apiserver/testdata/deploy-small.json | 28 ++ .../apiserver/testdata/pod-large.json | 177 ++++++++++++ .../apiserver/testdata/pod-medium.json | 37 +++ 6 files changed, 515 insertions(+), 53 deletions(-) create mode 100644 test/integration/apiserver/testdata/deploy-small.json create mode 100644 test/integration/apiserver/testdata/pod-large.json create mode 100644 test/integration/apiserver/testdata/pod-medium.json diff --git a/staging/src/k8s.io/apiserver/pkg/endpoints/apiserver_test.go b/staging/src/k8s.io/apiserver/pkg/endpoints/apiserver_test.go index eacfdfc7be6be..1a9cbb85c5ab0 100644 --- a/staging/src/k8s.io/apiserver/pkg/endpoints/apiserver_test.go +++ b/staging/src/k8s.io/apiserver/pkg/endpoints/apiserver_test.go @@ -4390,17 +4390,26 @@ func (storage *SimpleRESTStorageWithDeleteCollection) DeleteCollection(ctx conte func TestFieldValidation(t *testing.T) { strictDecoderErr := "strict decoder error for" - // TODO: add test cases for yaml validation and protobuf early exit. + badRequestErr := "fieldValidation parameter only supports content types" + strictFieldValidation := "?fieldValidation=strict" + // TODO: add test cases for yaml validation, multiple fieldValidation values tests := []struct { + name string path string verb string data []byte + queryParams string contentType string errContains string }{ - {path: "/namespaces/default/simples", verb: "POST", data: []byte(`{"kind":"Simple","apiVersion":"test.group/version","metadata":{"creationTimestamp":null},"other":"bar","unknown":"baz"}`), errContains: strictDecoderErr}, - {path: "/namespaces/default/simples/id", verb: "PUT", data: []byte(`{"kind": "Simple", "apiVersion": "test.group/version", "metadata": {"name": "id", "creationTimestamp": null}, "other": "bar", "unknown": "baz"}`), errContains: strictDecoderErr}, + {name: "post-unknown-strict-validation", path: "/namespaces/default/simples", verb: "POST", data: []byte(`{"kind":"Simple","apiVersion":"test.group/version","metadata":{"creationTimestamp":null},"other":"bar","unknown":"baz"}`), queryParams: strictFieldValidation, errContains: strictDecoderErr}, + {name: "put-unknown-strict-validation", path: "/namespaces/default/simples/id", verb: "PUT", data: []byte(`{"kind": "Simple", "apiVersion": "test.group/version", "metadata": {"name": "id", "creationTimestamp": null}, "other": "bar", "unknown": "baz"}`), queryParams: strictFieldValidation, errContains: strictDecoderErr}, + {name: "post-unknown-ignore-validation", path: "/namespaces/default/simples", verb: "POST", data: []byte(`{"kind":"Simple","apiVersion":"test.group/version","metadata":{"creationTimestamp":null},"other":"bar","unknown":"baz"}`)}, + {name: "put-unknown-ignore-validation", path: "/namespaces/default/simples/id", verb: "PUT", data: []byte(`{"kind": "Simple", "apiVersion": "test.group/version", "metadata": {"name": "id", "creationTimestamp": null}, "other": "bar", "unknown": "baz"}`)}, + {name: "post-unknown-strict-vaidation-json", path: "/namespaces/default/simples", verb: "POST", data: []byte(`{"kind":"Simple","apiVersion":"test.group/version","metadata":{"creationTimestamp":null},"other":"bar","unknown":"baz"}`), queryParams: strictFieldValidation, errContains: strictDecoderErr, contentType: runtime.ContentTypeJSON}, + {name: "post-unknown-strict-validation-protobuf", path: "/namespaces/default/simples", verb: "POST", data: []byte(`{"kind":"Simple","apiVersion":"test.group/version","metadata":{"creationTimestamp":null},"other":"bar","unknown":"baz"}`), queryParams: strictFieldValidation, errContains: badRequestErr, contentType: runtime.ContentTypeProtobuf}, + // TODO: there's a bug currently where query params are being stripped so this test does not pass yet. //{path: "/namespaces/default/simples/id", verb: "PATCH", data: []byte(`{"labels":{"foo":"bar"}}`), contentType: "application/merge-patch+json; charset=UTF-8", errContains: notImplementedErr}, } @@ -4427,18 +4436,16 @@ func TestFieldValidation(t *testing.T) { })) defer server.Close() for _, test := range tests { - baseURL := server.URL + "/" + prefix + "/" + testGroupVersion.Group + "/" + testGroupVersion.Version - response := runRequest(t, baseURL+test.path, test.verb, test.data, test.contentType) - if response.StatusCode == http.StatusBadRequest { - t.Fatalf("unexpected BadRequest: %#v", response) - } - response = runRequest(t, baseURL+test.path+"?fieldValidation=Strict", test.verb, test.data, test.contentType) - buf := new(bytes.Buffer) - buf.ReadFrom(response.Body) - // TODO: better way of doing than string comparison since we are getting a response instead of a regular go error? - if response.StatusCode != http.StatusBadRequest && !strings.Contains(buf.String(), test.errContains) { - t.Fatalf("unexpected response: %#v", response) - } + t.Run(test.name, func(t *testing.T) { + baseURL := server.URL + "/" + prefix + "/" + testGroupVersion.Group + "/" + testGroupVersion.Version + response := runRequest(t, baseURL+test.path+test.queryParams, test.verb, test.data, test.contentType) + buf := new(bytes.Buffer) + buf.ReadFrom(response.Body) + // TODO: better way of doing than string comparison since we are getting a response instead of a regular go error? + if response.StatusCode != http.StatusBadRequest && !strings.Contains(buf.String(), test.errContains) { + t.Fatalf("unexpected response: %#v, errContains: %#v", response, test.errContains) + } + }) } } diff --git a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/rest.go b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/rest.go index 374b3d1f43b4a..83d8459ee1497 100644 --- a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/rest.go +++ b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/rest.go @@ -472,14 +472,13 @@ const ( // all supported media types (only json and yaml supports field validation). func fieldValidation(req *http.Request) (fieldValidationDirective, error) { supportedContentTypes := []string{runtime.ContentTypeJSON, runtime.ContentTypeJSONMergePatch, runtime.ContentTypeYAML} - supported := false contentType := req.Header.Get("Content-Type") - //contentType := req.Header.Get("ContentType") // TODO: not sure if it is okay to assume empty content type is a valid one + supported := true if contentType != "" { + supported = false for _, v := range strings.Split(contentType, ",") { t, _, err := mime.ParseMediaType(v) - fmt.Printf("t = %+v\n", t) if err != nil { return ignoreFieldValidation, errors.NewBadRequest(fmt.Sprintf("could not parse media type: %v", v)) } @@ -490,9 +489,6 @@ func fieldValidation(req *http.Request) (fieldValidationDirective, error) { } } } - if !supported { - return ignoreFieldValidation, errors.NewBadRequest(fmt.Sprintf("fieldValidation parameter only supports content types %v\n content type provided: %s", supportedContentTypes, contentType)) - } } validationParam := req.URL.Query()["fieldValidation"] @@ -500,12 +496,18 @@ func fieldValidation(req *http.Request) (fieldValidationDirective, error) { case 0: return ignoreFieldValidation, nil case 1: - switch validationParam[0] { - case "Ignore": + switch strings.ToLower(validationParam[0]) { + case "ignore": return ignoreFieldValidation, nil - case "Strict": + case "strict": + if !supported { + return ignoreFieldValidation, errors.NewBadRequest(fmt.Sprintf("fieldValidation parameter only supports content types %v\n content type provided: %s", supportedContentTypes, contentType)) + } return strictFieldValidation, nil - case "Warn": + case "warn": + if !supported { + return ignoreFieldValidation, errors.NewBadRequest(fmt.Sprintf("fieldValidation parameter only supports content types %v\n content type provided: %s", supportedContentTypes, contentType)) + } return warnFieldValidation, nil default: return ignoreFieldValidation, errors.NewBadRequest(fmt.Sprintf("fieldValidation parameter unsupported: %v", validationParam)) diff --git a/test/integration/apiserver/apiserver_test.go b/test/integration/apiserver/apiserver_test.go index 12152bda02526..a5383ec915341 100644 --- a/test/integration/apiserver/apiserver_test.go +++ b/test/integration/apiserver/apiserver_test.go @@ -26,6 +26,7 @@ import ( "io/ioutil" "net/http" "net/http/httptest" + "os" "path" "reflect" "strconv" @@ -2330,21 +2331,50 @@ func TestDedupOwnerReferences(t *testing.T) { } } -// Benchmark field validation for strict vs non-strict -// TODO: add actual correctness integration tests too -func BenchmarkFieldValidation(b *testing.B) { - _, client, closeFn := setup(b) +func TestFieldValidationPut(t *testing.T) { + defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.ServerSideApply, true)() + + _, client, closeFn := setup(t) defer closeFn() - flag.Lookup("v").Value.Set("0") - baseDeploy := `{ + postBody := []byte(`{ + "apiVersion": "apps/v1", + "kind": "Deployment", + "metadata": { + "name": "test-dep", + "labels": {"app": "nginx"} + }, + "spec": { + "selector": { + "matchLabels": { + "app": "nginx" + } + }, + "template": { + "metadata": { + "labels": { + "app": "nginx" + } + }, + "spec": { + "containers": [{ + "name": "nginx", + "image": "nginx:latest" + }] + } + } + } + }`) + + putBody := []byte(`{ "apiVersion": "apps/v1", "kind": "Deployment", "metadata": { - "name": %s, + "name": "test-dep", "labels": {"app": "nginx"} }, "spec": { + "foo": "bar", "selector": { "matchLabels": { "app": "nginx" @@ -2364,26 +2394,203 @@ func BenchmarkFieldValidation(b *testing.B) { } } } - }` + }`) + + var testcases = []struct { + name string + // TODO: use PostOptions for fieldValidation param instead of raw strings. + params map[string]string + errContains string + }{ + { + name: "putStrictValidation", + params: map[string]string{"fieldValidation": "Strict"}, + errContains: "found unknown field", + }, + { + name: "putDefaultIgnoreValidation", + params: map[string]string{}, + errContains: "", + }, + { + name: "putIgnoreValidation", + params: map[string]string{"fieldValidation": "Ignore"}, + errContains: "", + }, + } + + 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) + } + for _, tc := range testcases { + t.Run(tc.name, func(t *testing.T) { + req := client.CoreV1().RESTClient().Put(). + AbsPath("/apis/apps/v1"). + Namespace("default"). + Resource("deployments"). + Name("test-dep") + for k, v := range tc.params { + req.Param(k, v) + + } + result, err := req.Body(putBody).DoRaw(context.TODO()) + if err == nil && tc.errContains != "" { + t.Fatalf("unexpected post succeeded") + + } + if err != nil && !strings.Contains(string(result), tc.errContains) { + t.Fatalf("unexpected response: %v", string(result)) + + } + }) + + } + +} +func TestFieldValidationPost(t *testing.T) { + defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.ServerSideApply, true)() + + _, client, closeFn := setup(t) + defer closeFn() - // TODO: add bigger objects to benchmarks table once confirmed that this is how we want to do benchmarking. + body := []byte(`{ + "apiVersion": "apps/v1", + "kind": "Deployment", + "metadata": { + "name": "test-dep", + "labels": {"app": "nginx"} + }, + "spec": { + "foo": "bar", + "selector": { + "matchLabels": { + "app": "nginx" + } + }, + "template": { + "metadata": { + "labels": { + "app": "nginx" + } + }, + "spec": { + "containers": [{ + "name": "nginx", + "image": "nginx:latest" + }] + } + } + } + }`) + + var testcases = []struct { + name string + // TODO: use PostOptions for fieldValidation param instead of raw strings. + params map[string]string + errContains string + }{ + { + name: "postStrictValidation", + params: map[string]string{"fieldValidation": "Strict"}, + errContains: "found unknown field", + }, + { + name: "postDefaultIgnoreValidation", + params: map[string]string{}, + errContains: "", + }, + { + name: "postIgnoreValidation", + params: map[string]string{"fieldValidation": "Ignore"}, + errContains: "", + }, + } + + for _, tc := range testcases { + t.Run(tc.name, func(t *testing.T) { + req := client.CoreV1().RESTClient().Post(). + AbsPath("/apis/apps/v1"). + Namespace("default"). + Resource("deployments") + for k, v := range tc.params { + req.Param(k, v) + + } + result, err := req.Body([]byte(body)).DoRaw(context.TODO()) + if err == nil && tc.errContains != "" { + t.Fatalf("unexpected post succeeded") + + } + if err != nil && !strings.Contains(string(result), tc.errContains) { + t.Fatalf("unexpected response: %v", string(result)) + } + }) + } +} + +// Benchmark field validation for strict vs non-strict +func BenchmarkFieldValidation(b *testing.B) { + _, client, closeFn := setup(b) + defer closeFn() + + flag.Lookup("v").Value.Set("0") + corePath := "/api/v1" + appsPath := "/apis/apps/v1" // TODO: split POST and PUT into their own test-cases. // TODO: add test for "Warn" validation once it is implemented. benchmarks := []struct { - name string - params map[string]string + name string + params map[string]string + bodyFile string + resource string + absPath string }{ { - name: "default-ignore-validation-deployment", - params: map[string]string{}, + name: "ignore-validation-deployment", + params: map[string]string{"fieldValidation": "Ignore"}, + bodyFile: "./testdata/deploy-small.json", + resource: "deployments", + absPath: appsPath, + }, + { + name: "strict-validation-deployment", + params: map[string]string{"fieldValidation": "Strict"}, + bodyFile: "./testdata/deploy-small.json", + resource: "deployments", + absPath: appsPath, }, { - name: "ignore-validation-deployment", - params: map[string]string{"fieldValidation": "Ignore"}, + name: "ignore-validation-pod", + params: map[string]string{"fieldValidation": "Ignore"}, + bodyFile: "./testdata/pod-medium.json", + resource: "pods", + absPath: corePath, }, { - name: "strict-validation-deployment", - params: map[string]string{"fieldValidation": "Strict"}, + name: "strict-validation-pod", + params: map[string]string{"fieldValidation": "Strict"}, + bodyFile: "./testdata/pod-medium.json", + resource: "pods", + absPath: corePath, + }, + { + name: "ignore-validation-big-pod", + params: map[string]string{"fieldValidation": "Ignore"}, + bodyFile: "./testdata/pod-large.json", + resource: "pods", + absPath: corePath, + }, + { + name: "strict-validation-big-pod", + params: map[string]string{"fieldValidation": "Strict"}, + bodyFile: "./testdata/pod-large.json", + resource: "pods", + absPath: corePath, }, } @@ -2394,20 +2601,24 @@ func BenchmarkFieldValidation(b *testing.B) { for n := 0; n < b.N; n++ { // append the timestamp to the name so that we don't hit conflicts when running the test multiple times // (i.e. without it -count=n for n>1 will fail, this might be from not tearing stuff down properly). - deployName := fmt.Sprintf("dep-%s-%d-%d-%d", bm.name, n, b.N, time.Now().UnixNano()) - deployString := fmt.Sprintf(baseDeploy, fmt.Sprintf(`"%s"`, deployName)) - deploy := []byte(deployString) + bodyBase, err := os.ReadFile(bm.bodyFile) + if err != nil { + panic(err) + } + + objName := fmt.Sprintf("obj-%s-%d-%d-%d", bm.name, n, b.N, time.Now().UnixNano()) + objString := fmt.Sprintf(string(bodyBase), fmt.Sprintf(`"%s"`, objName)) + body := []byte(objString) - appsPath := "/apis/apps/v1" postReq := client.CoreV1().RESTClient().Post(). - AbsPath(appsPath). + AbsPath(bm.absPath). Namespace("default"). - Resource("deployments") + Resource(bm.resource) for k, v := range bm.params { postReq = postReq.Param(k, v) } - _, err := postReq.Body(deploy). + _, err = postReq.Body(body). DoRaw(context.TODO()) if err != nil { panic(err) @@ -2415,15 +2626,15 @@ func BenchmarkFieldValidation(b *testing.B) { // TODO: put PUT in a different bench case than POST (ie. have a baseReq) be a part of the test case. putReq := client.CoreV1().RESTClient().Put(). - AbsPath(appsPath). + AbsPath(bm.absPath). Namespace("default"). - Resource("deployments"). - Name(deployName) + Resource(bm.resource). + Name(objName) for k, v := range bm.params { putReq = putReq.Param(k, v) } - _, err = putReq.Body(deploy). + _, err = putReq.Body(body). DoRaw(context.TODO()) if err != nil { panic(err) diff --git a/test/integration/apiserver/testdata/deploy-small.json b/test/integration/apiserver/testdata/deploy-small.json new file mode 100644 index 0000000000000..f19a473b52752 --- /dev/null +++ b/test/integration/apiserver/testdata/deploy-small.json @@ -0,0 +1,28 @@ +{ + "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" + }] + } + } + } +} diff --git a/test/integration/apiserver/testdata/pod-large.json b/test/integration/apiserver/testdata/pod-large.json new file mode 100644 index 0000000000000..910f9407ead62 --- /dev/null +++ b/test/integration/apiserver/testdata/pod-large.json @@ -0,0 +1,177 @@ +{ + "apiVersion": "v1", + "kind": "Pod", + "metadata": { + "labels": { + "app": "some-app", + "plugin1": "some-value", + "plugin2": "some-value", + "plugin3": "some-value", + "plugin4": "some-value" + }, + "name": %s, + "namespace": "default", + "ownerReferences": [ + { + "apiVersion": "apps/v1", + "blockOwnerDeletion": true, + "controller": true, + "kind": "ReplicaSet", + "name": "some-name", + "uid": "0a9d2b9e-779e-11e7-b422-42010a8001be" + } + ] + }, + "spec": { + "containers": [ + { + "args": [ + "one", + "two", + "three", + "four", + "five", + "six", + "seven", + "eight", + "nine" + ], + "env": [ + { + "name": "VAR_3", + "valueFrom": { + "secretKeyRef": { + "key": "some-other-key", + "name": "some-oher-name" + } + } + }, + { + "name": "VAR_2", + "valueFrom": { + "secretKeyRef": { + "key": "other-key", + "name": "other-name" + } + } + }, + { + "name": "VAR_1", + "valueFrom": { + "secretKeyRef": { + "key": "some-key", + "name": "some-name" + } + } + } + ], + "image": "some-image-name", + "imagePullPolicy": "IfNotPresent", + "name": "some-name", + "resources": { + "requests": { + "cpu": "0" + } + }, + "terminationMessagePath": "/dev/termination-log", + "terminationMessagePolicy": "File", + "volumeMounts": [ + { + "mountPath": "/var/run/secrets/kubernetes.io/serviceaccount", + "name": "default-token-hu5jz", + "readOnly": true + } + ] + } + ], + "dnsPolicy": "ClusterFirst", + "nodeName": "node-name", + "priority": 0, + "restartPolicy": "Always", + "schedulerName": "default-scheduler", + "securityContext": {}, + "serviceAccount": "default", + "serviceAccountName": "default", + "terminationGracePeriodSeconds": 30, + "tolerations": [ + { + "effect": "NoExecute", + "key": "node.kubernetes.io/not-ready", + "operator": "Exists", + "tolerationSeconds": 300 + }, + { + "effect": "NoExecute", + "key": "node.kubernetes.io/unreachable", + "operator": "Exists", + "tolerationSeconds": 300 + } + ], + "volumes": [ + { + "name": "default-token-hu5jz", + "secret": { + "defaultMode": 420, + "secretName": "default-token-hu5jz" + } + } + ] + }, + "status": { + "conditions": [ + { + "lastProbeTime": null, + "lastTransitionTime": "2019-07-08T09:31:18Z", + "status": "True", + "type": "Initialized" + }, + { + "lastProbeTime": null, + "lastTransitionTime": "2019-07-08T09:41:59Z", + "status": "True", + "type": "Ready" + }, + { + "lastProbeTime": null, + "lastTransitionTime": null, + "status": "True", + "type": "ContainersReady" + }, + { + "lastProbeTime": null, + "lastTransitionTime": "2019-07-08T09:31:18Z", + "status": "True", + "type": "PodScheduled" + } + ], + "containerStatuses": [ + { + "containerID": "docker://885e82a1ed0b7356541bb410a0126921ac42439607c09875cd8097dd5d7b5376", + "image": "some-image-name", + "imageID": "docker-pullable://some-image-id", + "lastState": { + "terminated": { + "containerID": "docker://d57290f9e00fad626b20d2dd87a3cf69bbc22edae07985374f86a8b2b4e39565", + "exitCode": 255, + "finishedAt": "2019-07-08T09:39:09Z", + "reason": "Error", + "startedAt": "2019-07-08T09:38:54Z" + } + }, + "name": "name", + "ready": true, + "restartCount": 6, + "state": { + "running": { + "startedAt": "2019-07-08T09:41:59Z" + } + } + } + ], + "hostIP": "10.0.0.1", + "phase": "Running", + "podIP": "10.0.0.1", + "qosClass": "BestEffort", + "startTime": "2019-07-08T09:31:18Z" + } +} diff --git a/test/integration/apiserver/testdata/pod-medium.json b/test/integration/apiserver/testdata/pod-medium.json new file mode 100644 index 0000000000000..b456a258eacd7 --- /dev/null +++ b/test/integration/apiserver/testdata/pod-medium.json @@ -0,0 +1,37 @@ +{ + "kind": "Pod", + "apiVersion": "v1", + "metadata": { + "name": %s + }, + "spec": { + "containers": [ + { + "name": "healthz", + "image": "k8s.gcr.io/exechealthz-amd64:1.2", + "args": [ + "-cmd=nslookup localhost" + ], + "ports": [ + { + "containerPort": 8080, + "protocol": "TCP" + } + ] + }, + { + "name":"test-container", + "image":"ubuntu:14.04", + "command": ["bash", "-c", "while true; do sleep 100; done"], + "livenessProbe": { + "httpGet": { + "path": "/healthz", + "port":8080 + }, + "initialDelaySeconds": 10, + "timeoutSeconds": 2 + } + } + ] + } +} From 0564cca92641930136de89549bd73fed497652a0 Mon Sep 17 00:00:00 2001 From: Kevin Delgado Date: Thu, 26 Aug 2021 21:38:46 +0000 Subject: [PATCH 07/64] Strict Validation for CR jsonpatch --- .../pkg/apiserver/customresource_handler.go | 89 +++++++++++++------ 1 file changed, 64 insertions(+), 25 deletions(-) diff --git a/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/customresource_handler.go b/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/customresource_handler.go index c317dd4d98b9c..345a619b2c58f 100644 --- a/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/customresource_handler.go +++ b/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/customresource_handler.go @@ -20,6 +20,7 @@ import ( "fmt" "net/http" "path" + "reflect" "sort" "strings" "sync" @@ -241,6 +242,11 @@ func (r *crdHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) { ) return } + // TODO: publically expose and use handler.StrictValidation() + validateParam := req.URL.Query()["validate"] + strictValidation := len(validateParam) == 1 && validateParam[0] == "strict" + // TODO: this is just for manual testing, remove when we figure out how to test with query params + strictValidation = true if !requestInfo.IsResourceRequest { pathParts := splitPath(requestInfo.Path) // only match /apis// @@ -316,7 +322,7 @@ func (r *crdHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) { terminating := apiextensionshelpers.IsCRDConditionTrue(crd, apiextensionsv1.Terminating) - crdInfo, err := r.getOrCreateServingInfoFor(crd.UID, crd.Name) + crdInfo, err := r.getOrCreateServingInfoFor(crd.UID, crd.Name, strictValidation) if apierrors.IsNotFound(err) { r.delegate.ServeHTTP(w, req) return @@ -602,7 +608,7 @@ func (r *crdHandler) tearDown(oldInfo *crdInfo) { // GetCustomResourceListerCollectionDeleter returns the ListerCollectionDeleter of // the given crd. func (r *crdHandler) GetCustomResourceListerCollectionDeleter(crd *apiextensionsv1.CustomResourceDefinition) (finalizer.ListerCollectionDeleter, error) { - info, err := r.getOrCreateServingInfoFor(crd.UID, crd.Name) + info, err := r.getOrCreateServingInfoFor(crd.UID, crd.Name, false) if err != nil { return nil, err } @@ -611,7 +617,7 @@ func (r *crdHandler) GetCustomResourceListerCollectionDeleter(crd *apiextensions // getOrCreateServingInfoFor gets the CRD serving info for the given CRD UID if the key exists in the storage map. // Otherwise the function fetches the up-to-date CRD using the given CRD name and creates CRD serving info. -func (r *crdHandler) getOrCreateServingInfoFor(uid types.UID, name string) (*crdInfo, error) { +func (r *crdHandler) getOrCreateServingInfoFor(uid types.UID, name string, strictValidation bool) (*crdInfo, error) { storageMap := r.customStorage.Load().(crdStorageMap) if ret, ok := storageMap[uid]; ok { return ret, nil @@ -841,12 +847,12 @@ func (r *crdHandler) getOrCreateServingInfoFor(uid types.UID, name string) (*crd // CRDs explicitly do not support protobuf, but some objects returned by the API server do negotiatedSerializer := unstructuredNegotiatedSerializer{ - typer: typer, - creator: creator, - converter: safeConverter, - structuralSchemas: structuralSchemas, - structuralSchemaGK: kind.GroupKind(), - preserveUnknownFields: crd.Spec.PreserveUnknownFields, + typer: typer, + creator: creator, + converter: safeConverter, + structuralSchemas: structuralSchemas, + structuralSchemaGK: kind.GroupKind(), + unknownFieldsDirective: makeUnknownFieldsDirective(crd.Spec.PreserveUnknownFields, strictValidation), } var standardSerializers []runtime.SerializerInfo for _, s := range negotiatedSerializer.SupportedMediaTypes() { @@ -1032,9 +1038,9 @@ type unstructuredNegotiatedSerializer struct { creator runtime.ObjectCreater converter runtime.ObjectConvertor - structuralSchemas map[string]*structuralschema.Structural // by version - structuralSchemaGK schema.GroupKind - preserveUnknownFields bool + structuralSchemas map[string]*structuralschema.Structural // by version + structuralSchemaGK schema.GroupKind + unknownFieldsDirective unknownFieldsDirective } func (s unstructuredNegotiatedSerializer) SupportedMediaTypes() []runtime.SerializerInfo { @@ -1077,7 +1083,7 @@ func (s unstructuredNegotiatedSerializer) EncoderForVersion(encoder runtime.Enco } func (s unstructuredNegotiatedSerializer) DecoderToVersion(decoder runtime.Decoder, gv runtime.GroupVersioner) runtime.Decoder { - d := schemaCoercingDecoder{delegate: decoder, validator: unstructuredSchemaCoercer{structuralSchemas: s.structuralSchemas, structuralSchemaGK: s.structuralSchemaGK, preserveUnknownFields: s.preserveUnknownFields}} + d := schemaCoercingDecoder{delegate: decoder, validator: unstructuredSchemaCoercer{structuralSchemas: s.structuralSchemas, structuralSchemaGK: s.structuralSchemaGK, unknownFieldsDirective: s.unknownFieldsDirective}} return versioning.NewCodec(nil, d, runtime.UnsafeObjectConvertor(Scheme), Scheme, Scheme, unstructuredDefaulter{ delegate: Scheme, structuralSchemas: s.structuralSchemas, @@ -1191,16 +1197,16 @@ func (t crdConversionRESTOptionsGetter) GetRESTOptions(resource schema.GroupReso if err == nil { d := schemaCoercingDecoder{delegate: ret.StorageConfig.Codec, validator: unstructuredSchemaCoercer{ // drop invalid fields while decoding old CRs (before we haven't had any ObjectMeta validation) - dropInvalidMetadata: true, - repairGeneration: true, - structuralSchemas: t.structuralSchemas, - structuralSchemaGK: t.structuralSchemaGK, - preserveUnknownFields: t.preserveUnknownFields, + dropInvalidMetadata: true, + repairGeneration: true, + structuralSchemas: t.structuralSchemas, + structuralSchemaGK: t.structuralSchemaGK, + unknownFieldsDirective: makeUnknownFieldsDirective(t.preserveUnknownFields, false), }} c := schemaCoercingConverter{delegate: t.converter, validator: unstructuredSchemaCoercer{ - structuralSchemas: t.structuralSchemas, - structuralSchemaGK: t.structuralSchemaGK, - preserveUnknownFields: t.preserveUnknownFields, + structuralSchemas: t.structuralSchemas, + structuralSchemaGK: t.structuralSchemaGK, + unknownFieldsDirective: makeUnknownFieldsDirective(t.preserveUnknownFields, false), }} ret.StorageConfig.Codec = versioning.NewCodec( ret.StorageConfig.Codec, @@ -1286,6 +1292,30 @@ func (v schemaCoercingConverter) ConvertFieldLabel(gvk schema.GroupVersionKind, return v.delegate.ConvertFieldLabel(gvk, label, value) } +// unknownFieldsDirective instructs what should happen +// if a custom resource is received with unknown fields that +// are not part of the schema. The options are: +// - drop the unknown fields +// - preserve the unknown fields +// - fail to handle the request and error out. +type unknownFieldsDirective int + +const ( + drop unknownFieldsDirective = iota + preserve + fail +) + +func makeUnknownFieldsDirective(preserveUnknownFields, failOnUnknownFields bool) unknownFieldsDirective { + if failOnUnknownFields { + return fail + } + if preserveUnknownFields { + return preserve + } + return drop +} + // unstructuredSchemaCoercer adds to unstructured unmarshalling what json.Unmarshal does // in addition for native types when decoding into Golang structs: // @@ -1296,9 +1326,9 @@ type unstructuredSchemaCoercer struct { dropInvalidMetadata bool repairGeneration bool - structuralSchemas map[string]*structuralschema.Structural - structuralSchemaGK schema.GroupKind - preserveUnknownFields bool + structuralSchemas map[string]*structuralschema.Structural + structuralSchemaGK schema.GroupKind + unknownFieldsDirective unknownFieldsDirective } func (v *unstructuredSchemaCoercer) apply(u *unstructured.Unstructured) error { @@ -1322,9 +1352,18 @@ func (v *unstructuredSchemaCoercer) apply(u *unstructured.Unstructured) error { return err } if gv.Group == v.structuralSchemaGK.Group && kind == v.structuralSchemaGK.Kind { - if !v.preserveUnknownFields { + if v.unknownFieldsDirective != preserve { // TODO: switch over pruning and coercing at the root to schemaobjectmeta.Coerce too + // TODO: this seems like a very error prone way of detecting unknown fields + // copy the original object to detect whether any fields are pruned + objCopy := (&unstructured.Unstructured{ + Object: u.Object, + }).DeepCopy() structuralpruning.Prune(u.Object, v.structuralSchemas[gv.Version], false) + objCopy.Object["metadata"] = u.Object["metadata"] + if v.unknownFieldsDirective == fail && !reflect.DeepEqual(objCopy.Object, u.Object) { + return fmt.Errorf("failed with unknown fields: %v", objCopy) + } structuraldefaulting.PruneNonNullableNullsWithoutDefaults(u.Object, v.structuralSchemas[gv.Version]) } if err := schemaobjectmeta.Coerce(nil, u.Object, v.structuralSchemas[gv.Version], false, v.dropInvalidMetadata); err != nil { From 0d8d87ed1e4cce164c2fcefa0b2d166c14293a45 Mon Sep 17 00:00:00 2001 From: Kevin Delgado Date: Mon, 30 Aug 2021 23:27:42 +0000 Subject: [PATCH 08/64] determine strict serialization in the patch handler --- .../pkg/apiserver/customresource_handler.go | 23 +++++++++++-------- .../apiserver/pkg/endpoints/handlers/patch.go | 9 ++++++-- .../apiserver/pkg/endpoints/handlers/rest.go | 3 ++- 3 files changed, 23 insertions(+), 12 deletions(-) diff --git a/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/customresource_handler.go b/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/customresource_handler.go index 345a619b2c58f..131797bf51e3f 100644 --- a/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/customresource_handler.go +++ b/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/customresource_handler.go @@ -242,11 +242,6 @@ func (r *crdHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) { ) return } - // TODO: publically expose and use handler.StrictValidation() - validateParam := req.URL.Query()["validate"] - strictValidation := len(validateParam) == 1 && validateParam[0] == "strict" - // TODO: this is just for manual testing, remove when we figure out how to test with query params - strictValidation = true if !requestInfo.IsResourceRequest { pathParts := splitPath(requestInfo.Path) // only match /apis// @@ -322,7 +317,7 @@ func (r *crdHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) { terminating := apiextensionshelpers.IsCRDConditionTrue(crd, apiextensionsv1.Terminating) - crdInfo, err := r.getOrCreateServingInfoFor(crd.UID, crd.Name, strictValidation) + crdInfo, err := r.getOrCreateServingInfoFor(crd.UID, crd.Name) if apierrors.IsNotFound(err) { r.delegate.ServeHTTP(w, req) return @@ -608,7 +603,7 @@ func (r *crdHandler) tearDown(oldInfo *crdInfo) { // GetCustomResourceListerCollectionDeleter returns the ListerCollectionDeleter of // the given crd. func (r *crdHandler) GetCustomResourceListerCollectionDeleter(crd *apiextensionsv1.CustomResourceDefinition) (finalizer.ListerCollectionDeleter, error) { - info, err := r.getOrCreateServingInfoFor(crd.UID, crd.Name, false) + info, err := r.getOrCreateServingInfoFor(crd.UID, crd.Name) if err != nil { return nil, err } @@ -617,7 +612,7 @@ func (r *crdHandler) GetCustomResourceListerCollectionDeleter(crd *apiextensions // getOrCreateServingInfoFor gets the CRD serving info for the given CRD UID if the key exists in the storage map. // Otherwise the function fetches the up-to-date CRD using the given CRD name and creates CRD serving info. -func (r *crdHandler) getOrCreateServingInfoFor(uid types.UID, name string, strictValidation bool) (*crdInfo, error) { +func (r *crdHandler) getOrCreateServingInfoFor(uid types.UID, name string) (*crdInfo, error) { storageMap := r.customStorage.Load().(crdStorageMap) if ret, ok := storageMap[uid]; ok { return ret, nil @@ -846,13 +841,22 @@ func (r *crdHandler) getOrCreateServingInfoFor(uid types.UID, name string, stric clusterScoped := crd.Spec.Scope == apiextensionsv1.ClusterScoped // CRDs explicitly do not support protobuf, but some objects returned by the API server do + // TODO: we could have two serializers one with strict directive and one without? negotiatedSerializer := unstructuredNegotiatedSerializer{ typer: typer, creator: creator, converter: safeConverter, structuralSchemas: structuralSchemas, structuralSchemaGK: kind.GroupKind(), - unknownFieldsDirective: makeUnknownFieldsDirective(crd.Spec.PreserveUnknownFields, strictValidation), + unknownFieldsDirective: makeUnknownFieldsDirective(crd.Spec.PreserveUnknownFields, false), + } + strictNegotiatedSerializer := unstructuredNegotiatedSerializer{ + typer: typer, + creator: creator, + converter: safeConverter, + structuralSchemas: structuralSchemas, + structuralSchemaGK: kind.GroupKind(), + unknownFieldsDirective: makeUnknownFieldsDirective(crd.Spec.PreserveUnknownFields, true), } var standardSerializers []runtime.SerializerInfo for _, s := range negotiatedSerializer.SupportedMediaTypes() { @@ -869,6 +873,7 @@ func (r *crdHandler) getOrCreateServingInfoFor(uid types.UID, name string, stric SelfLinkPathPrefix: selfLinkPrefix, }, Serializer: negotiatedSerializer, + StrictSerializer: strictNegotiatedSerializer, ParameterCodec: parameterCodec, StandardSerializers: standardSerializers, diff --git a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/patch.go b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/patch.go index e7964e3f857ec..d43f54a984107 100644 --- a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/patch.go +++ b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/patch.go @@ -151,9 +151,14 @@ func PatchResource(r rest.Patcher, scope *RequestScope, admit admission.Interfac } gv := scope.Kind.GroupVersion() + scopeSerializer := scope.Serializer + if strictValidation(req.URL) { + scopeSerializer = scope.StrictSerializer + } + codec := runtime.NewCodec( - scope.Serializer.EncoderForVersion(s.Serializer, gv), - scope.Serializer.DecoderToVersion(s.Serializer, scope.HubGroupVersion), + scopeSerializer.EncoderForVersion(s.Serializer, gv), + scopeSerializer.DecoderToVersion(s.Serializer, scope.HubGroupVersion), ) userInfo, _ := request.UserFrom(ctx) diff --git a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/rest.go b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/rest.go index 83d8459ee1497..c5e17bdc20071 100644 --- a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/rest.go +++ b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/rest.go @@ -71,7 +71,8 @@ const ( type RequestScope struct { Namer ScopeNamer - Serializer runtime.NegotiatedSerializer + StrictSerializer runtime.NegotiatedSerializer + Serializer runtime.NegotiatedSerializer runtime.ParameterCodec // StandardSerializers, if set, restricts which serializers can be used when From 5b82235ef2032e514a96c4a77a480d160ecaabdd Mon Sep 17 00:00:00 2001 From: Kevin Delgado Date: Wed, 1 Sep 2021 23:15:52 +0000 Subject: [PATCH 09/64] First pass at integration test --- .../apiserver/apply/apply_crd_test.go | 158 ++++++++++++++++++ 1 file changed, 158 insertions(+) diff --git a/test/integration/apiserver/apply/apply_crd_test.go b/test/integration/apiserver/apply/apply_crd_test.go index 5ae861d136596..3d7448c4d0c9c 100644 --- a/test/integration/apiserver/apply/apply_crd_test.go +++ b/test/integration/apiserver/apply/apply_crd_test.go @@ -49,6 +49,164 @@ import ( "k8s.io/kubernetes/test/integration/framework" ) +func TestMergePatchDisallowUnknownFields(t *testing.T) { + //var testcases = []struct { + // name string + // patchType string + // body string + // params map[string]string + // errContains string + //}{ + // { + // name: "mergePatchStrictValidation", + // patchType: "mergePatch", + // body: `yaml`, + // params: map[string]string{"key", "val"}, + // errContains: "bad err", + // }, + //} + //for _, tc := range testcases { + // t.Run(tc.name, func(t *testing.T) { + + // }) + + //} + server, err := apiservertesting.StartTestServer(t, apiservertesting.NewDefaultTestServerOptions(), nil, framework.SharedEtcd()) + if err != nil { + t.Fatal(err) + } + defer server.TearDownFn() + config := server.ClientConfig + + apiExtensionClient, err := clientset.NewForConfig(config) + if err != nil { + t.Fatal(err) + } + dynamicClient, err := dynamic.NewForConfig(config) + if err != nil { + t.Fatal(err) + } + + noxuDefinition := fixtures.NewNoxuV1CustomResourceDefinition(apiextensionsv1.ClusterScoped) + var c apiextensionsv1.CustomResourceValidation + err = json.Unmarshal([]byte(`{ + "openAPIV3Schema": { + "type": "object", + "properties": { + "spec": { + "type": "object", + "x-kubernetes-preserve-unknown-fields": true, + "properties": { + "cronSpec": { + "type": "string", + "pattern": "^(\\d+|\\*)(/\\d+)?(\\s+(\\d+|\\*)(/\\d+)?){4}$" + }, + "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" + } + } + } + } + } + } + }`), &c) + if err != nil { + t.Fatal(err) + } + noxuDefinition.Spec.PreserveUnknownFields = false + for i := range noxuDefinition.Spec.Versions { + noxuDefinition.Spec.Versions[i].Schema = &c + fmt.Printf("noxuDefiniton.Spec.Versions[i].Schema = %+v\n", noxuDefinition.Spec.Versions[i].Schema) + } + noxuDefinition, err = fixtures.CreateNewV1CustomResourceDefinition(noxuDefinition, apiExtensionClient, dynamicClient) + if err != nil { + t.Fatal(err) + } + + kind := noxuDefinition.Spec.Names.Kind + apiVersion := noxuDefinition.Spec.Group + "/" + noxuDefinition.Spec.Versions[0].Name + name := "mytest" + + rest := apiExtensionClient.Discovery().RESTClient() + yamlBody := []byte(fmt.Sprintf(` +apiVersion: %s +kind: %s +metadata: + name: %s + finalizers: + - test-finalizer +spec: + cronSpec: "* * * * */5" + replicas: 1 + ports: + - name: x + containerPort: 80 + protocol: TCP`, apiVersion, kind, name)) + result, err := rest.Patch(types.ApplyPatchType). + AbsPath("/apis", noxuDefinition.Spec.Group, noxuDefinition.Spec.Versions[0].Name, noxuDefinition.Spec.Names.Plural). + Name(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(result)) + } + fmt.Println("apply succeeded") + fmt.Printf("result = %+v\n", string(result)) + verifyNumFinalizers(t, result, 1) + verifyFinalizersIncludes(t, result, "test-finalizer") + verifyReplicas(t, result, 1) + verifyNumPorts(t, result, 1) + fmt.Println("apply verified") + + result, err = rest.Patch(types.MergePatchType). + AbsPath("/apis", noxuDefinition.Spec.Group, noxuDefinition.Spec.Versions[0].Name, noxuDefinition.Spec.Names.Plural). + Name(name). + Param("validate", "strict"). + Body([]byte(`{"metadata":{"finalizers":["test-finalizer","another-one"]}, "spec":{"foo": "bar"}}`)). + DoRaw(context.TODO()) + if err != nil { + t.Fatalf("failed to add finalizer with merge patch: %v:\n%v", err, string(result)) + } + fmt.Println("patch succeeded") + fmt.Printf("result = %+v\n", string(result)) + verifyNumFinalizers(t, result, 2) + verifyFinalizersIncludes(t, result, "test-finalizer") + verifyFinalizersIncludes(t, result, "another-one") + fmt.Println("patch verified") + +} + // TestApplyCRDStructuralSchema tests that when a CRD has a structural schema in its validation field, // it will be used to construct the CR schema used by apply. func TestApplyCRDStructuralSchema(t *testing.T) { From 488bdf12fd22c988ea3ddb31286670a0a703e5d0 Mon Sep 17 00:00:00 2001 From: Kevin Delgado Date: Thu, 2 Sep 2021 00:00:25 +0000 Subject: [PATCH 10/64] cleanup test, move to apiserver_test.go --- .../apiserver/pkg/endpoints/handlers/patch.go | 27 +-- test/integration/apiserver/apiserver_test.go | 178 ++++++++++++++++++ .../apiserver/apply/apply_crd_test.go | 158 ---------------- 3 files changed, 194 insertions(+), 169 deletions(-) diff --git a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/patch.go b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/patch.go index d43f54a984107..3f7fe880fd36b 100644 --- a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/patch.go +++ b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/patch.go @@ -70,16 +70,16 @@ func PatchResource(r rest.Patcher, scope *RequestScope, admit admission.Interfac return } - // TODO: put behind feature gate? - validationDirective, err := fieldValidation(req) - if err != nil { - scope.err(err, w, req) - return - } - if validationDirective == strictFieldValidation { - scope.err(errors.NewBadRequest("strict validation is not supported yet"), w, req) - return - } + //// TODO: put behind feature gate? + //validationDirective, err := fieldValidation(req) + //if err != nil { + // scope.err(err, w, req) + // return + //} + //if validationDirective == strictFieldValidation { + // scope.err(errors.NewBadRequest("strict validation is not supported yet"), w, req) + // return + //} // Do this first, otherwise name extraction can fail for unrecognized content types // TODO: handle this in negotiation @@ -152,7 +152,12 @@ func PatchResource(r rest.Patcher, scope *RequestScope, admit admission.Interfac gv := scope.Kind.GroupVersion() scopeSerializer := scope.Serializer - if strictValidation(req.URL) { + validationDirective, err := fieldValidation(req) + if err != nil { + scope.err(err, w, req) + return + } + if validationDirective == strictFieldValidation { scopeSerializer = scope.StrictSerializer } diff --git a/test/integration/apiserver/apiserver_test.go b/test/integration/apiserver/apiserver_test.go index a5383ec915341..0c0bcc29a39f8 100644 --- a/test/integration/apiserver/apiserver_test.go +++ b/test/integration/apiserver/apiserver_test.go @@ -2524,7 +2524,185 @@ func TestFieldValidationPost(t *testing.T) { result, err := req.Body([]byte(body)).DoRaw(context.TODO()) if err == nil && tc.errContains != "" { t.Fatalf("unexpected post succeeded") + } + if err != nil && !strings.Contains(string(result), tc.errContains) { + t.Fatalf("unexpected response: %v", string(result)) + } + }) + } +} + +// TestPatchCRDUnknownFieldValidation tests that server-side schema validation +// works for jsonpatch and mergepatch requests. +func TestPatchCRDUnknownFieldValidation(t *testing.T) { + crdSchema := `{ + "openAPIV3Schema": { + "type": "object", + "properties": { + "spec": { + "type": "object", + "x-kubernetes-preserve-unknown-fields": true, + "properties": { + "cronSpec": { + "type": "string", + "pattern": "^(\\d+|\\*)(/\\d+)?(\\s+(\\d+|\\*)(/\\d+)?){4}$" + }, + "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" + } + } + } + } + } + } + }` + patchYAMLBody := ` +apiVersion: %s +kind: %s +metadata: + name: %s + finalizers: + - test-finalizer +spec: + cronSpec: "* * * * */5" + replicas: 1 + ports: + - name: x + containerPort: 80 + protocol: TCP` + var testcases = []struct { + name string + patchType types.PatchType + params map[string]string + body string + errContains string + }{ + { + name: "mergePatchStrictValidation", + patchType: types.MergePatchType, + params: map[string]string{"validate": "strict"}, + body: `{"metadata":{"finalizers":["test-finalizer","another-one"]}, "spec":{"foo": "bar"}}`, + errContains: "failed with unknown fields", + }, + { + name: "mergePatchNoValidation", + patchType: types.MergePatchType, + params: map[string]string{}, + body: `{"metadata":{"finalizers":["test-finalizer","another-one"]}, "spec":{"foo": "bar"}}`, + errContains: "", + }, + // TODO: figure out how to test JSONPatch + //{ + // name: "jsonPatchStrictValidation", + // patchType: types.JSONPatchType, + // params: map[string]string{"validate": "strict"}, + // body: // TODO + // errContains: "failed with unknown fields", + //}, + //{ + // name: "jsonPatchNoValidation", + // patchType: types.JSONPatchType, + // params: map[string]string{}, + // body: // TODO + // errContains: "", + //}, + } + for _, tc := range testcases { + t.Run(tc.name, func(t *testing.T) { + server, err := kubeapiservertesting.StartTestServer(t, kubeapiservertesting.NewDefaultTestServerOptions(), nil, framework.SharedEtcd()) + if err != nil { + t.Fatal(err) + } + defer server.TearDownFn() + config := server.ClientConfig + + apiExtensionClient, err := apiextensionsclient.NewForConfig(config) + if err != nil { + t.Fatal(err) + } + dynamicClient, err := dynamic.NewForConfig(config) + if err != nil { + t.Fatal(err) + } + // create the CRD + noxuDefinition := fixtures.NewNoxuV1CustomResourceDefinition(apiextensionsv1.ClusterScoped) + var c apiextensionsv1.CustomResourceValidation + err = json.Unmarshal([]byte(crdSchema), &c) + if err != nil { + t.Fatal(err) + } + // set the CRD schema + noxuDefinition.Spec.PreserveUnknownFields = false + for i := range noxuDefinition.Spec.Versions { + noxuDefinition.Spec.Versions[i].Schema = &c + fmt.Printf("noxuDefiniton.Spec.Versions[i].Schema = %+v\n", noxuDefinition.Spec.Versions[i].Schema) + } + // install the CRD + noxuDefinition, err = fixtures.CreateNewV1CustomResourceDefinition(noxuDefinition, apiExtensionClient, dynamicClient) + if err != nil { + t.Fatal(err) + } + + kind := noxuDefinition.Spec.Names.Kind + apiVersion := noxuDefinition.Spec.Group + "/" + noxuDefinition.Spec.Versions[0].Name + name := "mytest" + + // create a CR + rest := apiExtensionClient.Discovery().RESTClient() + yamlBody := []byte(fmt.Sprintf(patchYAMLBody, apiVersion, kind, name)) + result, err := rest.Patch(types.ApplyPatchType). + AbsPath("/apis", noxuDefinition.Spec.Group, noxuDefinition.Spec.Versions[0].Name, noxuDefinition.Spec.Names.Plural). + Name(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(result)) + } + + // patch the CR as specified by the test case + req := rest.Patch(tc.patchType). + AbsPath("/apis", noxuDefinition.Spec.Group, noxuDefinition.Spec.Versions[0].Name, noxuDefinition.Spec.Names.Plural). + Name(name) + for k, v := range tc.params { + req = req.Param(k, v) + } + result, err = req. + Body([]byte(tc.body)). + DoRaw(context.TODO()) + if err == nil && tc.errContains != "" { + t.Fatalf("unexpected patch succeeded") } if err != nil && !strings.Contains(string(result), tc.errContains) { t.Fatalf("unexpected response: %v", string(result)) diff --git a/test/integration/apiserver/apply/apply_crd_test.go b/test/integration/apiserver/apply/apply_crd_test.go index 3d7448c4d0c9c..5ae861d136596 100644 --- a/test/integration/apiserver/apply/apply_crd_test.go +++ b/test/integration/apiserver/apply/apply_crd_test.go @@ -49,164 +49,6 @@ import ( "k8s.io/kubernetes/test/integration/framework" ) -func TestMergePatchDisallowUnknownFields(t *testing.T) { - //var testcases = []struct { - // name string - // patchType string - // body string - // params map[string]string - // errContains string - //}{ - // { - // name: "mergePatchStrictValidation", - // patchType: "mergePatch", - // body: `yaml`, - // params: map[string]string{"key", "val"}, - // errContains: "bad err", - // }, - //} - //for _, tc := range testcases { - // t.Run(tc.name, func(t *testing.T) { - - // }) - - //} - server, err := apiservertesting.StartTestServer(t, apiservertesting.NewDefaultTestServerOptions(), nil, framework.SharedEtcd()) - if err != nil { - t.Fatal(err) - } - defer server.TearDownFn() - config := server.ClientConfig - - apiExtensionClient, err := clientset.NewForConfig(config) - if err != nil { - t.Fatal(err) - } - dynamicClient, err := dynamic.NewForConfig(config) - if err != nil { - t.Fatal(err) - } - - noxuDefinition := fixtures.NewNoxuV1CustomResourceDefinition(apiextensionsv1.ClusterScoped) - var c apiextensionsv1.CustomResourceValidation - err = json.Unmarshal([]byte(`{ - "openAPIV3Schema": { - "type": "object", - "properties": { - "spec": { - "type": "object", - "x-kubernetes-preserve-unknown-fields": true, - "properties": { - "cronSpec": { - "type": "string", - "pattern": "^(\\d+|\\*)(/\\d+)?(\\s+(\\d+|\\*)(/\\d+)?){4}$" - }, - "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" - } - } - } - } - } - } - }`), &c) - if err != nil { - t.Fatal(err) - } - noxuDefinition.Spec.PreserveUnknownFields = false - for i := range noxuDefinition.Spec.Versions { - noxuDefinition.Spec.Versions[i].Schema = &c - fmt.Printf("noxuDefiniton.Spec.Versions[i].Schema = %+v\n", noxuDefinition.Spec.Versions[i].Schema) - } - noxuDefinition, err = fixtures.CreateNewV1CustomResourceDefinition(noxuDefinition, apiExtensionClient, dynamicClient) - if err != nil { - t.Fatal(err) - } - - kind := noxuDefinition.Spec.Names.Kind - apiVersion := noxuDefinition.Spec.Group + "/" + noxuDefinition.Spec.Versions[0].Name - name := "mytest" - - rest := apiExtensionClient.Discovery().RESTClient() - yamlBody := []byte(fmt.Sprintf(` -apiVersion: %s -kind: %s -metadata: - name: %s - finalizers: - - test-finalizer -spec: - cronSpec: "* * * * */5" - replicas: 1 - ports: - - name: x - containerPort: 80 - protocol: TCP`, apiVersion, kind, name)) - result, err := rest.Patch(types.ApplyPatchType). - AbsPath("/apis", noxuDefinition.Spec.Group, noxuDefinition.Spec.Versions[0].Name, noxuDefinition.Spec.Names.Plural). - Name(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(result)) - } - fmt.Println("apply succeeded") - fmt.Printf("result = %+v\n", string(result)) - verifyNumFinalizers(t, result, 1) - verifyFinalizersIncludes(t, result, "test-finalizer") - verifyReplicas(t, result, 1) - verifyNumPorts(t, result, 1) - fmt.Println("apply verified") - - result, err = rest.Patch(types.MergePatchType). - AbsPath("/apis", noxuDefinition.Spec.Group, noxuDefinition.Spec.Versions[0].Name, noxuDefinition.Spec.Names.Plural). - Name(name). - Param("validate", "strict"). - Body([]byte(`{"metadata":{"finalizers":["test-finalizer","another-one"]}, "spec":{"foo": "bar"}}`)). - DoRaw(context.TODO()) - if err != nil { - t.Fatalf("failed to add finalizer with merge patch: %v:\n%v", err, string(result)) - } - fmt.Println("patch succeeded") - fmt.Printf("result = %+v\n", string(result)) - verifyNumFinalizers(t, result, 2) - verifyFinalizersIncludes(t, result, "test-finalizer") - verifyFinalizersIncludes(t, result, "another-one") - fmt.Println("patch verified") - -} - // TestApplyCRDStructuralSchema tests that when a CRD has a structural schema in its validation field, // it will be used to construct the CR schema used by apply. func TestApplyCRDStructuralSchema(t *testing.T) { From 853193fd490ec4b5617b89c593eb4c8a91364951 Mon Sep 17 00:00:00 2001 From: Kevin Delgado Date: Thu, 9 Sep 2021 22:02:07 +0000 Subject: [PATCH 11/64] wip, stash logging --- .../pkg/apiserver/customresource_handler.go | 16 ++++++++++++++++ .../apiserver/pkg/endpoints/handlers/patch.go | 15 +++++++++++++++ 2 files changed, 31 insertions(+) diff --git a/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/customresource_handler.go b/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/customresource_handler.go index 131797bf51e3f..f08d400302c9f 100644 --- a/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/customresource_handler.go +++ b/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/customresource_handler.go @@ -154,6 +154,9 @@ type crdInfo struct { // Storage per version storages map[string]customresource.CustomResourceStorage + // TODO: requestScopes is where we use the different serializer + // One option is to have two requestScopes (one strict and one non-strict) + // and pass it to the PatchResource call based on the query param // Request scope per version requestScopes map[string]*handlers.RequestScope @@ -233,6 +236,7 @@ var longRunningFilter = genericfilters.BasicLongRunningRequestCheck(sets.NewStri var possiblyAcrossAllNamespacesVerbs = sets.NewString("list", "watch") func (r *crdHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) { + klog.Warningf("ServeHTTP called on crdHandler") ctx := req.Context() requestInfo, ok := apirequest.RequestInfoFrom(ctx) if !ok { @@ -385,6 +389,8 @@ func (r *crdHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) { } func (r *crdHandler) serveResource(w http.ResponseWriter, req *http.Request, requestInfo *apirequest.RequestInfo, crdInfo *crdInfo, crd *apiextensionsv1.CustomResourceDefinition, terminating bool, supportedTypes []string) http.HandlerFunc { + klog.Warningf("serveResource called") + // TODO should we multiplext requestScope based on the query param? requestScope := crdInfo.requestScopes[requestInfo.APIVersion] storage := crdInfo.storages[requestInfo.APIVersion].CustomResource @@ -415,6 +421,7 @@ func (r *crdHandler) serveResource(w http.ResponseWriter, req *http.Request, req case "update": return handlers.UpdateResource(storage, requestScope, r.admission) case "patch": + klog.Warningf("returning handlers.PatchResource") return handlers.PatchResource(storage, requestScope, r.admission, supportedTypes) case "delete": allowsOptions := true @@ -1337,6 +1344,8 @@ type unstructuredSchemaCoercer struct { } func (v *unstructuredSchemaCoercer) apply(u *unstructured.Unstructured) error { + klog.Warningf("coercer apply") + klog.Warningf("uFD: %v", v.unknownFieldsDirective) // save implicit meta fields that don't have to be specified in the validation spec kind, foundKind, err := unstructured.NestedString(u.UnstructuredContent(), "kind") if err != nil { @@ -1364,16 +1373,23 @@ func (v *unstructuredSchemaCoercer) apply(u *unstructured.Unstructured) error { objCopy := (&unstructured.Unstructured{ Object: u.Object, }).DeepCopy() + klog.Warningf("obj copy pre, %v", objCopy) + klog.Warningf("schema: %v", v.structuralSchemas[gv.Version]) structuralpruning.Prune(u.Object, v.structuralSchemas[gv.Version], false) objCopy.Object["metadata"] = u.Object["metadata"] + klog.Warningf("obj copy post, %v", objCopy) + klog.Warningf("u Object, %v", u.Object) if v.unknownFieldsDirective == fail && !reflect.DeepEqual(objCopy.Object, u.Object) { + klog.Warningf("prune break") return fmt.Errorf("failed with unknown fields: %v", objCopy) } structuraldefaulting.PruneNonNullableNullsWithoutDefaults(u.Object, v.structuralSchemas[gv.Version]) + klog.Warningf("u Object post prune, %v", u.Object) } if err := schemaobjectmeta.Coerce(nil, u.Object, v.structuralSchemas[gv.Version], false, v.dropInvalidMetadata); err != nil { return err } + klog.Warningf("u Object post coerce, %v", u.Object) // fixup missing generation in very old CRs if v.repairGeneration && objectMeta.Generation == 0 { objectMeta.Generation = 1 diff --git a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/patch.go b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/patch.go index 3f7fe880fd36b..32505710affdd 100644 --- a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/patch.go +++ b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/patch.go @@ -17,8 +17,10 @@ limitations under the License. package handlers import ( + "bytes" "context" "fmt" + "io/ioutil" "net/http" "strings" "time" @@ -50,6 +52,7 @@ import ( "k8s.io/apiserver/pkg/registry/rest" "k8s.io/apiserver/pkg/util/dryrun" utilfeature "k8s.io/apiserver/pkg/util/feature" + "k8s.io/klog/v2" utiltrace "k8s.io/utils/trace" ) @@ -61,6 +64,11 @@ const ( // PatchResource returns a function that will handle a resource patch. func PatchResource(r rest.Patcher, scope *RequestScope, admit admission.Interface, patchTypes []string) http.HandlerFunc { return func(w http.ResponseWriter, req *http.Request) { + klog.Warningf("handling PatchResource") + klog.Warningf("req: %v\n", req) + bodyBytes, _ := ioutil.ReadAll(req.Body) + req.Body = ioutil.NopCloser(bytes.NewBuffer(bodyBytes)) + klog.Warningf("body: %v\n", string(bodyBytes)) // For performance tracking purposes. trace := utiltrace.New("Patch", traceFields(req)...) defer trace.LogIfLong(500 * time.Millisecond) @@ -229,6 +237,7 @@ func PatchResource(r rest.Patcher, scope *RequestScope, admit admission.Interfac updateValidation: rest.AdmissionToValidateObjectUpdateFunc(admit, staticUpdateAttributes, scope), admissionCheck: mutatingAdmission, + // TODO: define codec as strict or not-strict based on request codec: codec, options: options, @@ -242,6 +251,8 @@ func PatchResource(r rest.Patcher, scope *RequestScope, admit admission.Interfac trace: trace, } + klog.Warningf("patchType: %v", patchType) + result, wasCreated, err := p.patchResource(ctx, scope) if err != nil { scope.err(err, w, req) @@ -292,6 +303,9 @@ type patcher struct { admissionCheck admission.MutationInterface codec runtime.Codec + // don't think this is necessary because we construct the patcher on each request + // so we can multiplex the codec between/strict non-strict + //strictCodec runtime.Codec options *metav1.PatchOptions @@ -323,6 +337,7 @@ type jsonPatcher struct { } func (p *jsonPatcher) applyPatchToCurrentObject(currentObject runtime.Object) (runtime.Object, error) { + klog.Warningf("jsonPatch aPTCO") // Encode will convert & return a versioned object in JSON. currentObjJS, err := runtime.Encode(p.codec, currentObject) if err != nil { From 3229e2ae9e83a88e984c50772306f51bb46c7cb1 Mon Sep 17 00:00:00 2001 From: Kevin Delgado Date: Wed, 6 Oct 2021 17:18:15 +0000 Subject: [PATCH 12/64] fix patch crd test params --- test/integration/apiserver/apiserver_test.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/integration/apiserver/apiserver_test.go b/test/integration/apiserver/apiserver_test.go index 0c0bcc29a39f8..bf947fe4b0883 100644 --- a/test/integration/apiserver/apiserver_test.go +++ b/test/integration/apiserver/apiserver_test.go @@ -2532,9 +2532,9 @@ func TestFieldValidationPost(t *testing.T) { } } -// TestPatchCRDUnknownFieldValidation tests that server-side schema validation +// TestFieldValidationPatchCRD tests that server-side schema validation // works for jsonpatch and mergepatch requests. -func TestPatchCRDUnknownFieldValidation(t *testing.T) { +func TestFieldValidationPatchCRD(t *testing.T) { crdSchema := `{ "openAPIV3Schema": { "type": "object", @@ -2610,7 +2610,7 @@ spec: { name: "mergePatchStrictValidation", patchType: types.MergePatchType, - params: map[string]string{"validate": "strict"}, + params: map[string]string{"fieldValidation": "Strict"}, body: `{"metadata":{"finalizers":["test-finalizer","another-one"]}, "spec":{"foo": "bar"}}`, errContains: "failed with unknown fields", }, From 4194448fbace15f096b2d38174d5a50a357e7dec Mon Sep 17 00:00:00 2001 From: Kevin Delgado Date: Wed, 6 Oct 2021 20:23:58 +0000 Subject: [PATCH 13/64] benchmark working, test is broken --- .../pkg/apiserver/customresource_handler.go | 28 +-- .../apiserver/pkg/endpoints/handlers/patch.go | 11 +- test/integration/apiserver/apiserver_test.go | 162 ++++++++++++++++++ 3 files changed, 184 insertions(+), 17 deletions(-) diff --git a/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/customresource_handler.go b/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/customresource_handler.go index f08d400302c9f..0b80e1bc0bf90 100644 --- a/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/customresource_handler.go +++ b/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/customresource_handler.go @@ -236,7 +236,7 @@ var longRunningFilter = genericfilters.BasicLongRunningRequestCheck(sets.NewStri var possiblyAcrossAllNamespacesVerbs = sets.NewString("list", "watch") func (r *crdHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) { - klog.Warningf("ServeHTTP called on crdHandler") + //klog.Warningf("ServeHTTP called on crdHandler") ctx := req.Context() requestInfo, ok := apirequest.RequestInfoFrom(ctx) if !ok { @@ -389,7 +389,7 @@ func (r *crdHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) { } func (r *crdHandler) serveResource(w http.ResponseWriter, req *http.Request, requestInfo *apirequest.RequestInfo, crdInfo *crdInfo, crd *apiextensionsv1.CustomResourceDefinition, terminating bool, supportedTypes []string) http.HandlerFunc { - klog.Warningf("serveResource called") + //klog.Warningf("serveResource called") // TODO should we multiplext requestScope based on the query param? requestScope := crdInfo.requestScopes[requestInfo.APIVersion] storage := crdInfo.storages[requestInfo.APIVersion].CustomResource @@ -421,7 +421,7 @@ func (r *crdHandler) serveResource(w http.ResponseWriter, req *http.Request, req case "update": return handlers.UpdateResource(storage, requestScope, r.admission) case "patch": - klog.Warningf("returning handlers.PatchResource") + //klog.Warningf("returning handlers.PatchResource") return handlers.PatchResource(storage, requestScope, r.admission, supportedTypes) case "delete": allowsOptions := true @@ -1344,8 +1344,8 @@ type unstructuredSchemaCoercer struct { } func (v *unstructuredSchemaCoercer) apply(u *unstructured.Unstructured) error { - klog.Warningf("coercer apply") - klog.Warningf("uFD: %v", v.unknownFieldsDirective) + //klog.Warningf("coercer apply") + //klog.Warningf("uFD: %v", v.unknownFieldsDirective) // save implicit meta fields that don't have to be specified in the validation spec kind, foundKind, err := unstructured.NestedString(u.UnstructuredContent(), "kind") if err != nil { @@ -1373,23 +1373,29 @@ func (v *unstructuredSchemaCoercer) apply(u *unstructured.Unstructured) error { objCopy := (&unstructured.Unstructured{ Object: u.Object, }).DeepCopy() - klog.Warningf("obj copy pre, %v", objCopy) - klog.Warningf("schema: %v", v.structuralSchemas[gv.Version]) + //klog.Warningf("obj copy pre, %v", objCopy) + //klog.Warningf("schema: %v", v.structuralSchemas[gv.Version]) structuralpruning.Prune(u.Object, v.structuralSchemas[gv.Version], false) - objCopy.Object["metadata"] = u.Object["metadata"] - klog.Warningf("obj copy post, %v", objCopy) + delete(objCopy.Object, "kind") + delete(u.Object, "kind") + delete(objCopy.Object, "apiVersion") + delete(u.Object, "apiVersion") + delete(objCopy.Object, "metadata") + delete(u.Object, "metadata") + klog.Warningf("obj copy post, %v", objCopy.Object) klog.Warningf("u Object, %v", u.Object) + klog.Warningf("directive %v", v.unknownFieldsDirective) if v.unknownFieldsDirective == fail && !reflect.DeepEqual(objCopy.Object, u.Object) { klog.Warningf("prune break") return fmt.Errorf("failed with unknown fields: %v", objCopy) } structuraldefaulting.PruneNonNullableNullsWithoutDefaults(u.Object, v.structuralSchemas[gv.Version]) - klog.Warningf("u Object post prune, %v", u.Object) + //klog.Warningf("u Object post prune, %v", u.Object) } if err := schemaobjectmeta.Coerce(nil, u.Object, v.structuralSchemas[gv.Version], false, v.dropInvalidMetadata); err != nil { return err } - klog.Warningf("u Object post coerce, %v", u.Object) + //klog.Warningf("u Object post coerce, %v", u.Object) // fixup missing generation in very old CRs if v.repairGeneration && objectMeta.Generation == 0 { objectMeta.Generation = 1 diff --git a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/patch.go b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/patch.go index 32505710affdd..23297fea1b8b4 100644 --- a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/patch.go +++ b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/patch.go @@ -52,7 +52,6 @@ import ( "k8s.io/apiserver/pkg/registry/rest" "k8s.io/apiserver/pkg/util/dryrun" utilfeature "k8s.io/apiserver/pkg/util/feature" - "k8s.io/klog/v2" utiltrace "k8s.io/utils/trace" ) @@ -64,11 +63,11 @@ const ( // PatchResource returns a function that will handle a resource patch. func PatchResource(r rest.Patcher, scope *RequestScope, admit admission.Interface, patchTypes []string) http.HandlerFunc { return func(w http.ResponseWriter, req *http.Request) { - klog.Warningf("handling PatchResource") - klog.Warningf("req: %v\n", req) + //klog.Warningf("handling PatchResource") + //klog.Warningf("req: %v\n", req) bodyBytes, _ := ioutil.ReadAll(req.Body) req.Body = ioutil.NopCloser(bytes.NewBuffer(bodyBytes)) - klog.Warningf("body: %v\n", string(bodyBytes)) + //klog.Warningf("body: %v\n", string(bodyBytes)) // For performance tracking purposes. trace := utiltrace.New("Patch", traceFields(req)...) defer trace.LogIfLong(500 * time.Millisecond) @@ -251,7 +250,7 @@ func PatchResource(r rest.Patcher, scope *RequestScope, admit admission.Interfac trace: trace, } - klog.Warningf("patchType: %v", patchType) + //klog.Warningf("patchType: %v", patchType) result, wasCreated, err := p.patchResource(ctx, scope) if err != nil { @@ -337,7 +336,7 @@ type jsonPatcher struct { } func (p *jsonPatcher) applyPatchToCurrentObject(currentObject runtime.Object) (runtime.Object, error) { - klog.Warningf("jsonPatch aPTCO") + //klog.Warningf("jsonPatch aPTCO") // Encode will convert & return a versioned object in JSON. currentObjJS, err := runtime.Encode(p.codec, currentObject) if err != nil { diff --git a/test/integration/apiserver/apiserver_test.go b/test/integration/apiserver/apiserver_test.go index bf947fe4b0883..cb82fe611c475 100644 --- a/test/integration/apiserver/apiserver_test.go +++ b/test/integration/apiserver/apiserver_test.go @@ -2711,6 +2711,168 @@ spec: } } +func BenchmarkFieldValidationPatchCRD(b *testing.B) { + server, err := kubeapiservertesting.StartTestServer(b, kubeapiservertesting.NewDefaultTestServerOptions(), nil, framework.SharedEtcd()) + if err != nil { + panic(err) + } + defer server.TearDownFn() + config := server.ClientConfig + + apiExtensionClient, err := apiextensionsclient.NewForConfig(config) + if err != nil { + panic(err) + } + dynamicClient, err := dynamic.NewForConfig(config) + if err != nil { + panic(err) + } + + crdSchema := `{ + "openAPIV3Schema": { + "type": "object", + "properties": { + "spec": { + "type": "object", + "x-kubernetes-preserve-unknown-fields": true, + "properties": { + "cronSpec": { + "type": "string", + "pattern": "^(\\d+|\\*)(/\\d+)?(\\s+(\\d+|\\*)(/\\d+)?){4}$" + }, + "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" + } + } + } + } + } + } + }` + // create the CRD + noxuDefinition := fixtures.NewNoxuV1CustomResourceDefinition(apiextensionsv1.ClusterScoped) + var c apiextensionsv1.CustomResourceValidation + err = json.Unmarshal([]byte(crdSchema), &c) + if err != nil { + panic(err) + } + // set the CRD schema + noxuDefinition.Spec.PreserveUnknownFields = false + for i := range noxuDefinition.Spec.Versions { + noxuDefinition.Spec.Versions[i].Schema = &c + //fmt.Printf("noxuDefiniton.Spec.Versions[i].Schema = %+v\n", noxuDefinition.Spec.Versions[i].Schema) + } + // install the CRD + noxuDefinition, err = fixtures.CreateNewV1CustomResourceDefinition(noxuDefinition, apiExtensionClient, dynamicClient) + if err != nil { + panic(err) + } + + kind := noxuDefinition.Spec.Names.Kind + apiVersion := noxuDefinition.Spec.Group + "/" + noxuDefinition.Spec.Versions[0].Name + name := "mytest" + + patchYAMLBody := ` +apiVersion: %s +kind: %s +metadata: + name: %s + finalizers: + - test-finalizer +spec: + cronSpec: "* * * * */5" + replicas: 1 + ports: + - name: x + containerPort: 80 + protocol: TCP` + // create a CR + rest := apiExtensionClient.Discovery().RESTClient() + yamlBody := []byte(fmt.Sprintf(patchYAMLBody, apiVersion, kind, name)) + result, err := rest.Patch(types.ApplyPatchType). + AbsPath("/apis", noxuDefinition.Spec.Group, noxuDefinition.Spec.Versions[0].Name, noxuDefinition.Spec.Names.Plural). + Name(name). + Param("fieldManager", "apply_test"). + Body(yamlBody). + DoRaw(context.TODO()) + if err != nil { + panic(fmt.Sprintf("failed to create custom resource with apply: %v:\n%v", err, string(result))) + } + + benchmarks := []struct { + name string + patchType types.PatchType + params map[string]string + bodyBase string + }{ + { + name: "ignore-validation-crd-patch", + patchType: types.MergePatchType, + params: map[string]string{}, + bodyBase: `{"metadata":{"finalizers":["test-finalizer","finalizer-ignore-%d"]}}`, + }, + { + name: "strict-validation-crd-patch", + patchType: types.MergePatchType, + params: map[string]string{"fieldValidation": "Strict"}, + bodyBase: `{"metadata":{"finalizers":["test-finalizer","finalizer-strict-%d"]}}`, + }, + } + for _, bm := range benchmarks { + b.Run(bm.name, func(b *testing.B) { + b.ResetTimer() + b.ReportAllocs() + for n := 0; n < b.N; n++ { + body := fmt.Sprintf(bm.bodyBase, n) + //fmt.Printf("!!! body = %+v\n", body) + // patch the CR as specified by the test case + req := rest.Patch(bm.patchType). + AbsPath("/apis", noxuDefinition.Spec.Group, noxuDefinition.Spec.Versions[0].Name, noxuDefinition.Spec.Names.Plural). + Name(name) + for k, v := range bm.params { + req = req.Param(k, v) + } + result, err = req. + Body([]byte(body)). + DoRaw(context.TODO()) + if err != nil { + panic(err) + } + } + }) + } +} + // Benchmark field validation for strict vs non-strict func BenchmarkFieldValidation(b *testing.B) { _, client, closeFn := setup(b) From e5bb8653855316d2780ca4e4ed1903028edd9d5c Mon Sep 17 00:00:00 2001 From: Kevin Delgado Date: Wed, 6 Oct 2021 20:45:07 +0000 Subject: [PATCH 14/64] wip debugging --- .../pkg/apiserver/customresource_handler.go | 2 +- test/integration/apiserver/apiserver_test.go | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/customresource_handler.go b/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/customresource_handler.go index 0b80e1bc0bf90..b8b7373de6429 100644 --- a/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/customresource_handler.go +++ b/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/customresource_handler.go @@ -1382,9 +1382,9 @@ func (v *unstructuredSchemaCoercer) apply(u *unstructured.Unstructured) error { delete(u.Object, "apiVersion") delete(objCopy.Object, "metadata") delete(u.Object, "metadata") + klog.Warningf("directive %v", v.unknownFieldsDirective) klog.Warningf("obj copy post, %v", objCopy.Object) klog.Warningf("u Object, %v", u.Object) - klog.Warningf("directive %v", v.unknownFieldsDirective) if v.unknownFieldsDirective == fail && !reflect.DeepEqual(objCopy.Object, u.Object) { klog.Warningf("prune break") return fmt.Errorf("failed with unknown fields: %v", objCopy) diff --git a/test/integration/apiserver/apiserver_test.go b/test/integration/apiserver/apiserver_test.go index cb82fe611c475..b7c46539aae6b 100644 --- a/test/integration/apiserver/apiserver_test.go +++ b/test/integration/apiserver/apiserver_test.go @@ -2541,7 +2541,6 @@ func TestFieldValidationPatchCRD(t *testing.T) { "properties": { "spec": { "type": "object", - "x-kubernetes-preserve-unknown-fields": true, "properties": { "cronSpec": { "type": "string", From 8df8d69092cbc0628ad6fa336d50bab155ba7da9 Mon Sep 17 00:00:00 2001 From: Kevin Delgado Date: Wed, 6 Oct 2021 20:52:47 +0000 Subject: [PATCH 15/64] fixed it by getting rid of preserve unknown fieds in the test --- test/integration/apiserver/apiserver_test.go | 1 - 1 file changed, 1 deletion(-) diff --git a/test/integration/apiserver/apiserver_test.go b/test/integration/apiserver/apiserver_test.go index b7c46539aae6b..3aee285f4599d 100644 --- a/test/integration/apiserver/apiserver_test.go +++ b/test/integration/apiserver/apiserver_test.go @@ -2594,7 +2594,6 @@ metadata: - test-finalizer spec: cronSpec: "* * * * */5" - replicas: 1 ports: - name: x containerPort: 80 From 81f8e01c42cd4bf8538b12a6429b8cc5b26f01d5 Mon Sep 17 00:00:00 2001 From: Kevin Delgado Date: Wed, 6 Oct 2021 21:37:13 +0000 Subject: [PATCH 16/64] structural pruning now returns the pruned fields --- .../pkg/apiserver/customresource_handler.go | 34 +++++----- .../pkg/apiserver/schema/pruning/algorithm.go | 65 ++++++++++++++----- 2 files changed, 66 insertions(+), 33 deletions(-) diff --git a/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/customresource_handler.go b/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/customresource_handler.go index b8b7373de6429..7f7e4aaa5651c 100644 --- a/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/customresource_handler.go +++ b/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/customresource_handler.go @@ -20,7 +20,6 @@ import ( "fmt" "net/http" "path" - "reflect" "sort" "strings" "sync" @@ -1370,24 +1369,25 @@ func (v *unstructuredSchemaCoercer) apply(u *unstructured.Unstructured) error { // TODO: switch over pruning and coercing at the root to schemaobjectmeta.Coerce too // TODO: this seems like a very error prone way of detecting unknown fields // copy the original object to detect whether any fields are pruned - objCopy := (&unstructured.Unstructured{ - Object: u.Object, - }).DeepCopy() + //objCopy := (&unstructured.Unstructured{ + // Object: u.Object, + //}).DeepCopy() //klog.Warningf("obj copy pre, %v", objCopy) //klog.Warningf("schema: %v", v.structuralSchemas[gv.Version]) - structuralpruning.Prune(u.Object, v.structuralSchemas[gv.Version], false) - delete(objCopy.Object, "kind") - delete(u.Object, "kind") - delete(objCopy.Object, "apiVersion") - delete(u.Object, "apiVersion") - delete(objCopy.Object, "metadata") - delete(u.Object, "metadata") - klog.Warningf("directive %v", v.unknownFieldsDirective) - klog.Warningf("obj copy post, %v", objCopy.Object) - klog.Warningf("u Object, %v", u.Object) - if v.unknownFieldsDirective == fail && !reflect.DeepEqual(objCopy.Object, u.Object) { - klog.Warningf("prune break") - return fmt.Errorf("failed with unknown fields: %v", objCopy) + pruned := structuralpruning.Prune(u.Object, v.structuralSchemas[gv.Version], false) + //klog.Warningf("pruned: %v", pruned) + //delete(objCopy.Object, "kind") + //delete(u.Object, "kind") + //delete(objCopy.Object, "apiVersion") + //delete(u.Object, "apiVersion") + //delete(objCopy.Object, "metadata") + //delete(u.Object, "metadata") + //klog.Warningf("directive %v", v.unknownFieldsDirective) + //klog.Warningf("obj copy post, %v", objCopy.Object) + //klog.Warningf("u Object, %v", u.Object) + if v.unknownFieldsDirective == fail && len(pruned) > 0 { + //klog.Warningf("prune break") + return fmt.Errorf("failed with unknown fields: %v", pruned) } structuraldefaulting.PruneNonNullableNullsWithoutDefaults(u.Object, v.structuralSchemas[gv.Version]) //klog.Warningf("u Object post prune, %v", u.Object) diff --git a/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/schema/pruning/algorithm.go b/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/schema/pruning/algorithm.go index a1fd711c6a56e..fc9821d369262 100644 --- a/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/schema/pruning/algorithm.go +++ b/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/schema/pruning/algorithm.go @@ -18,12 +18,13 @@ package pruning import ( structuralschema "k8s.io/apiextensions-apiserver/pkg/apiserver/schema" + "k8s.io/klog/v2" ) // Prune removes object fields in obj which are not specified in s. It skips TypeMeta and ObjectMeta fields // if XEmbeddedResource is set to true, or for the root if isResourceRoot=true, i.e. it does not // prune unknown metadata fields. -func Prune(obj interface{}, s *structuralschema.Structural, isResourceRoot bool) { +func Prune(obj interface{}, s *structuralschema.Structural, isResourceRoot bool) map[string]bool { if isResourceRoot { if s == nil { s = &structuralschema.Structural{} @@ -34,7 +35,7 @@ func Prune(obj interface{}, s *structuralschema.Structural, isResourceRoot bool) s = &clone } } - prune(obj, s) + return prune(obj, s) } var metaFields = map[string]bool{ @@ -43,19 +44,23 @@ var metaFields = map[string]bool{ "metadata": true, } -func prune(x interface{}, s *structuralschema.Structural) { +func prune(x interface{}, s *structuralschema.Structural) map[string]bool { if s != nil && s.XPreserveUnknownFields { - skipPrune(x, s) - return + return skipPrune(x, s) } + pruning := map[string]bool{} switch x := x.(type) { case map[string]interface{}: if s == nil { for k := range x { + klog.Warningf("deleting 0 k: %v\n", k) + if !metaFields[k] { + pruning[k] = true + } delete(x, k) } - return + return pruning } for k, v := range x { if s.XEmbeddedResource && metaFields[k] { @@ -63,31 +68,49 @@ func prune(x interface{}, s *structuralschema.Structural) { } prop, ok := s.Properties[k] if ok { - prune(v, &prop) + pruned := prune(v, &prop) + for k, b := range pruned { + pruning[k] = b + } } else if s.AdditionalProperties != nil { - prune(v, s.AdditionalProperties.Structural) + pruned := prune(v, s.AdditionalProperties.Structural) + for k, b := range pruned { + pruning[k] = b + } } else { + klog.Warningf("deleting 1 k: %v\n", k) + if !metaFields[k] { + pruning[k] = true + } delete(x, k) } } case []interface{}: if s == nil { for _, v := range x { - prune(v, nil) + pruned := prune(v, nil) + for k, b := range pruned { + pruning[k] = b + } } - return + return pruning } for _, v := range x { - prune(v, s.Items) + pruned := prune(v, s.Items) + for k, b := range pruned { + pruning[k] = b + } } default: // scalars, do nothing } + return pruning } -func skipPrune(x interface{}, s *structuralschema.Structural) { +func skipPrune(x interface{}, s *structuralschema.Structural) map[string]bool { + pruning := map[string]bool{} if s == nil { - return + return pruning } switch x := x.(type) { @@ -97,16 +120,26 @@ func skipPrune(x interface{}, s *structuralschema.Structural) { continue } if prop, ok := s.Properties[k]; ok { - prune(v, &prop) + pruned := prune(v, &prop) + for k, b := range pruned { + pruning[k] = b + } } else if s.AdditionalProperties != nil { - prune(v, s.AdditionalProperties.Structural) + pruned := prune(v, s.AdditionalProperties.Structural) + for k, b := range pruned { + pruning[k] = b + } } } case []interface{}: for _, v := range x { - skipPrune(v, s.Items) + skipPruned := skipPrune(v, s.Items) + for k, b := range skipPruned { + pruning[k] = b + } } default: // scalars, do nothing } + return pruning } From 9a2e946b8aba464cb3f1753e39c1d3ffc9e709a2 Mon Sep 17 00:00:00 2001 From: Kevin Delgado Date: Wed, 6 Oct 2021 21:43:37 +0000 Subject: [PATCH 17/64] clean up debug logging in customresource_handler.go --- .../pkg/apiserver/customresource_handler.go | 29 +------------------ .../pkg/apiserver/schema/pruning/algorithm.go | 3 -- 2 files changed, 1 insertion(+), 31 deletions(-) diff --git a/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/customresource_handler.go b/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/customresource_handler.go index 7f7e4aaa5651c..339fbdd2a7fc1 100644 --- a/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/customresource_handler.go +++ b/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/customresource_handler.go @@ -235,7 +235,6 @@ var longRunningFilter = genericfilters.BasicLongRunningRequestCheck(sets.NewStri var possiblyAcrossAllNamespacesVerbs = sets.NewString("list", "watch") func (r *crdHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) { - //klog.Warningf("ServeHTTP called on crdHandler") ctx := req.Context() requestInfo, ok := apirequest.RequestInfoFrom(ctx) if !ok { @@ -388,8 +387,6 @@ func (r *crdHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) { } func (r *crdHandler) serveResource(w http.ResponseWriter, req *http.Request, requestInfo *apirequest.RequestInfo, crdInfo *crdInfo, crd *apiextensionsv1.CustomResourceDefinition, terminating bool, supportedTypes []string) http.HandlerFunc { - //klog.Warningf("serveResource called") - // TODO should we multiplext requestScope based on the query param? requestScope := crdInfo.requestScopes[requestInfo.APIVersion] storage := crdInfo.storages[requestInfo.APIVersion].CustomResource @@ -420,7 +417,6 @@ func (r *crdHandler) serveResource(w http.ResponseWriter, req *http.Request, req case "update": return handlers.UpdateResource(storage, requestScope, r.admission) case "patch": - //klog.Warningf("returning handlers.PatchResource") return handlers.PatchResource(storage, requestScope, r.admission, supportedTypes) case "delete": allowsOptions := true @@ -1343,8 +1339,6 @@ type unstructuredSchemaCoercer struct { } func (v *unstructuredSchemaCoercer) apply(u *unstructured.Unstructured) error { - //klog.Warningf("coercer apply") - //klog.Warningf("uFD: %v", v.unknownFieldsDirective) // save implicit meta fields that don't have to be specified in the validation spec kind, foundKind, err := unstructured.NestedString(u.UnstructuredContent(), "kind") if err != nil { @@ -1366,37 +1360,16 @@ func (v *unstructuredSchemaCoercer) apply(u *unstructured.Unstructured) error { } if gv.Group == v.structuralSchemaGK.Group && kind == v.structuralSchemaGK.Kind { if v.unknownFieldsDirective != preserve { - // TODO: switch over pruning and coercing at the root to schemaobjectmeta.Coerce too - // TODO: this seems like a very error prone way of detecting unknown fields - // copy the original object to detect whether any fields are pruned - //objCopy := (&unstructured.Unstructured{ - // Object: u.Object, - //}).DeepCopy() - //klog.Warningf("obj copy pre, %v", objCopy) - //klog.Warningf("schema: %v", v.structuralSchemas[gv.Version]) + // TODO: switch over pruning and coercing at the root to schemaobjectmeta.Coerce too (I don't remember what this comment means anymore) pruned := structuralpruning.Prune(u.Object, v.structuralSchemas[gv.Version], false) - //klog.Warningf("pruned: %v", pruned) - //delete(objCopy.Object, "kind") - //delete(u.Object, "kind") - //delete(objCopy.Object, "apiVersion") - //delete(u.Object, "apiVersion") - //delete(objCopy.Object, "metadata") - //delete(u.Object, "metadata") - //klog.Warningf("directive %v", v.unknownFieldsDirective) - //klog.Warningf("obj copy post, %v", objCopy.Object) - //klog.Warningf("u Object, %v", u.Object) if v.unknownFieldsDirective == fail && len(pruned) > 0 { - //klog.Warningf("prune break") return fmt.Errorf("failed with unknown fields: %v", pruned) } structuraldefaulting.PruneNonNullableNullsWithoutDefaults(u.Object, v.structuralSchemas[gv.Version]) - //klog.Warningf("u Object post prune, %v", u.Object) } if err := schemaobjectmeta.Coerce(nil, u.Object, v.structuralSchemas[gv.Version], false, v.dropInvalidMetadata); err != nil { return err } - //klog.Warningf("u Object post coerce, %v", u.Object) - // fixup missing generation in very old CRs if v.repairGeneration && objectMeta.Generation == 0 { objectMeta.Generation = 1 } diff --git a/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/schema/pruning/algorithm.go b/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/schema/pruning/algorithm.go index fc9821d369262..80986998d159b 100644 --- a/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/schema/pruning/algorithm.go +++ b/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/schema/pruning/algorithm.go @@ -18,7 +18,6 @@ package pruning import ( structuralschema "k8s.io/apiextensions-apiserver/pkg/apiserver/schema" - "k8s.io/klog/v2" ) // Prune removes object fields in obj which are not specified in s. It skips TypeMeta and ObjectMeta fields @@ -54,7 +53,6 @@ func prune(x interface{}, s *structuralschema.Structural) map[string]bool { case map[string]interface{}: if s == nil { for k := range x { - klog.Warningf("deleting 0 k: %v\n", k) if !metaFields[k] { pruning[k] = true } @@ -78,7 +76,6 @@ func prune(x interface{}, s *structuralschema.Structural) map[string]bool { pruning[k] = b } } else { - klog.Warningf("deleting 1 k: %v\n", k) if !metaFields[k] { pruning[k] = true } From 256d44271f34534ddef21b82681019227e89b214 Mon Sep 17 00:00:00 2001 From: Kevin Delgado Date: Wed, 6 Oct 2021 22:16:35 +0000 Subject: [PATCH 18/64] benchmark remaining merge-patch possibilities --- test/integration/apiserver/apiserver_test.go | 52 +++++++++++++------- 1 file changed, 34 insertions(+), 18 deletions(-) diff --git a/test/integration/apiserver/apiserver_test.go b/test/integration/apiserver/apiserver_test.go index 3aee285f4599d..91d5dba315a79 100644 --- a/test/integration/apiserver/apiserver_test.go +++ b/test/integration/apiserver/apiserver_test.go @@ -2664,7 +2664,6 @@ spec: noxuDefinition.Spec.PreserveUnknownFields = false for i := range noxuDefinition.Spec.Versions { noxuDefinition.Spec.Versions[i].Schema = &c - fmt.Printf("noxuDefiniton.Spec.Versions[i].Schema = %+v\n", noxuDefinition.Spec.Versions[i].Schema) } // install the CRD noxuDefinition, err = fixtures.CreateNewV1CustomResourceDefinition(noxuDefinition, apiExtensionClient, dynamicClient) @@ -2700,7 +2699,7 @@ spec: Body([]byte(tc.body)). DoRaw(context.TODO()) if err == nil && tc.errContains != "" { - t.Fatalf("unexpected patch succeeded") + t.Fatalf("unexpected patch succeeded, expected %s", tc.errContains) } if err != nil && !strings.Contains(string(result), tc.errContains) { t.Fatalf("unexpected response: %v", string(result)) @@ -2732,7 +2731,6 @@ func BenchmarkFieldValidationPatchCRD(b *testing.B) { "properties": { "spec": { "type": "object", - "x-kubernetes-preserve-unknown-fields": true, "properties": { "cronSpec": { "type": "string", @@ -2809,7 +2807,6 @@ metadata: - test-finalizer spec: cronSpec: "* * * * */5" - replicas: 1 ports: - name: x containerPort: 80 @@ -2828,22 +2825,39 @@ spec: } benchmarks := []struct { - name string - patchType types.PatchType - params map[string]string - bodyBase string + name string + patchType types.PatchType + params map[string]string + bodyBase string + errContains string }{ { - name: "ignore-validation-crd-patch", - patchType: types.MergePatchType, - params: map[string]string{}, - bodyBase: `{"metadata":{"finalizers":["test-finalizer","finalizer-ignore-%d"]}}`, + name: "ignore-validation-crd-patch", + patchType: types.MergePatchType, + params: map[string]string{}, + bodyBase: `{"metadata":{"finalizers":["test-finalizer","finalizer-ignore-%d"]}}`, + errContains: "", + }, + { + name: "strict-validation-crd-patch", + patchType: types.MergePatchType, + params: map[string]string{"fieldValidation": "Strict"}, + bodyBase: `{"metadata":{"finalizers":["test-finalizer","finalizer-strict-%d"]}}`, + errContains: "", + }, + { + name: "ignore-validation-crd-patch-unknown-field", + patchType: types.MergePatchType, + params: map[string]string{}, + bodyBase: `{"metadata":{"finalizers":["test-finalizer","finalizer-ignore-unknown-%d"]}, "spec":{"foo": "bar"}}`, + errContains: "", }, { - name: "strict-validation-crd-patch", - patchType: types.MergePatchType, - params: map[string]string{"fieldValidation": "Strict"}, - bodyBase: `{"metadata":{"finalizers":["test-finalizer","finalizer-strict-%d"]}}`, + name: "strict-validation-crd-patch-unknown-field", + patchType: types.MergePatchType, + params: map[string]string{"fieldValidation": "Strict"}, + bodyBase: `{"metadata":{"finalizers":["test-finalizer","finalizer-strict-unknown-%d"]}, "spec":{"foo": "bar"}}`, + errContains: "failed with unknown fields", }, } for _, bm := range benchmarks { @@ -2852,7 +2866,6 @@ spec: b.ReportAllocs() for n := 0; n < b.N; n++ { body := fmt.Sprintf(bm.bodyBase, n) - //fmt.Printf("!!! body = %+v\n", body) // patch the CR as specified by the test case req := rest.Patch(bm.patchType). AbsPath("/apis", noxuDefinition.Spec.Group, noxuDefinition.Spec.Versions[0].Name, noxuDefinition.Spec.Names.Plural). @@ -2863,7 +2876,10 @@ spec: result, err = req. Body([]byte(body)). DoRaw(context.TODO()) - if err != nil { + if err == nil && bm.errContains != "" { + panic(fmt.Sprintf("unexpected patch succeeded, expected %s", bm.errContains)) + } + if err != nil && !strings.Contains(string(result), bm.errContains) { panic(err) } } From b3e58bd15c4bc2ff2d7accc5b5efb91de739da20 Mon Sep 17 00:00:00 2001 From: Kevin Delgado Date: Thu, 9 Sep 2021 22:02:07 +0000 Subject: [PATCH 19/64] wip, stash logging --- .../pkg/apiserver/customresource_handler.go | 7 +++++++ .../apiserver/pkg/endpoints/handlers/patch.go | 14 +++----------- 2 files changed, 10 insertions(+), 11 deletions(-) diff --git a/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/customresource_handler.go b/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/customresource_handler.go index 339fbdd2a7fc1..c3a365327d81b 100644 --- a/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/customresource_handler.go +++ b/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/customresource_handler.go @@ -235,6 +235,7 @@ var longRunningFilter = genericfilters.BasicLongRunningRequestCheck(sets.NewStri var possiblyAcrossAllNamespacesVerbs = sets.NewString("list", "watch") func (r *crdHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) { + klog.Warningf("ServeHTTP called on crdHandler") ctx := req.Context() requestInfo, ok := apirequest.RequestInfoFrom(ctx) if !ok { @@ -387,6 +388,8 @@ func (r *crdHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) { } func (r *crdHandler) serveResource(w http.ResponseWriter, req *http.Request, requestInfo *apirequest.RequestInfo, crdInfo *crdInfo, crd *apiextensionsv1.CustomResourceDefinition, terminating bool, supportedTypes []string) http.HandlerFunc { + klog.Warningf("serveResource called") + // TODO should we multiplext requestScope based on the query param? requestScope := crdInfo.requestScopes[requestInfo.APIVersion] storage := crdInfo.storages[requestInfo.APIVersion].CustomResource @@ -417,6 +420,7 @@ func (r *crdHandler) serveResource(w http.ResponseWriter, req *http.Request, req case "update": return handlers.UpdateResource(storage, requestScope, r.admission) case "patch": + klog.Warningf("returning handlers.PatchResource") return handlers.PatchResource(storage, requestScope, r.admission, supportedTypes) case "delete": allowsOptions := true @@ -1339,6 +1343,8 @@ type unstructuredSchemaCoercer struct { } func (v *unstructuredSchemaCoercer) apply(u *unstructured.Unstructured) error { + klog.Warningf("coercer apply") + klog.Warningf("uFD: %v", v.unknownFieldsDirective) // save implicit meta fields that don't have to be specified in the validation spec kind, foundKind, err := unstructured.NestedString(u.UnstructuredContent(), "kind") if err != nil { @@ -1366,6 +1372,7 @@ func (v *unstructuredSchemaCoercer) apply(u *unstructured.Unstructured) error { return fmt.Errorf("failed with unknown fields: %v", pruned) } structuraldefaulting.PruneNonNullableNullsWithoutDefaults(u.Object, v.structuralSchemas[gv.Version]) + klog.Warningf("u Object post prune, %v", u.Object) } if err := schemaobjectmeta.Coerce(nil, u.Object, v.structuralSchemas[gv.Version], false, v.dropInvalidMetadata); err != nil { return err diff --git a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/patch.go b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/patch.go index 23297fea1b8b4..9ef2a841b2369 100644 --- a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/patch.go +++ b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/patch.go @@ -17,10 +17,8 @@ limitations under the License. package handlers import ( - "bytes" "context" "fmt" - "io/ioutil" "net/http" "strings" "time" @@ -52,6 +50,7 @@ import ( "k8s.io/apiserver/pkg/registry/rest" "k8s.io/apiserver/pkg/util/dryrun" utilfeature "k8s.io/apiserver/pkg/util/feature" + "k8s.io/klog/v2" utiltrace "k8s.io/utils/trace" ) @@ -63,12 +62,6 @@ const ( // PatchResource returns a function that will handle a resource patch. func PatchResource(r rest.Patcher, scope *RequestScope, admit admission.Interface, patchTypes []string) http.HandlerFunc { return func(w http.ResponseWriter, req *http.Request) { - //klog.Warningf("handling PatchResource") - //klog.Warningf("req: %v\n", req) - bodyBytes, _ := ioutil.ReadAll(req.Body) - req.Body = ioutil.NopCloser(bytes.NewBuffer(bodyBytes)) - //klog.Warningf("body: %v\n", string(bodyBytes)) - // For performance tracking purposes. trace := utiltrace.New("Patch", traceFields(req)...) defer trace.LogIfLong(500 * time.Millisecond) @@ -166,6 +159,8 @@ func PatchResource(r rest.Patcher, scope *RequestScope, admit admission.Interfac } if validationDirective == strictFieldValidation { scopeSerializer = scope.StrictSerializer + } else { + klog.Warningf("nonstrcit serializer") } codec := runtime.NewCodec( @@ -250,8 +245,6 @@ func PatchResource(r rest.Patcher, scope *RequestScope, admit admission.Interfac trace: trace, } - //klog.Warningf("patchType: %v", patchType) - result, wasCreated, err := p.patchResource(ctx, scope) if err != nil { scope.err(err, w, req) @@ -336,7 +329,6 @@ type jsonPatcher struct { } func (p *jsonPatcher) applyPatchToCurrentObject(currentObject runtime.Object) (runtime.Object, error) { - //klog.Warningf("jsonPatch aPTCO") // Encode will convert & return a versioned object in JSON. currentObjJS, err := runtime.Encode(p.codec, currentObject) if err != nil { From 32d4bc4c7c97602905edba22ad533383f52dde52 Mon Sep 17 00:00:00 2001 From: Kevin Delgado Date: Thu, 16 Sep 2021 00:53:24 +0000 Subject: [PATCH 20/64] wip more pritning --- .../apimachinery/pkg/runtime/converter.go | 45 ++++++++++++++++++- .../pkg/util/strategicpatch/patch.go | 20 ++++++++- .../apiserver/pkg/endpoints/handlers/patch.go | 15 +++++++ 3 files changed, 77 insertions(+), 3 deletions(-) diff --git a/staging/src/k8s.io/apimachinery/pkg/runtime/converter.go b/staging/src/k8s.io/apimachinery/pkg/runtime/converter.go index 4a6cc68574a45..11eeb05d02b23 100644 --- a/staging/src/k8s.io/apimachinery/pkg/runtime/converter.go +++ b/staging/src/k8s.io/apimachinery/pkg/runtime/converter.go @@ -118,6 +118,9 @@ func NewTestUnstructuredConverter(comparison conversion.Equalities) Unstructured // FromUnstructured converts an object from map[string]interface{} representation into a concrete type. // It uses encoding/json/Unmarshaler if object implements it or reflection if not. func (c *unstructuredConverter) FromUnstructured(u map[string]interface{}, obj interface{}) error { + klog.Warningf("FromUnstructured\n") + c.mismatchDetection = true + klog.Warningf("mismatchDetection?: %t", c.mismatchDetection) t := reflect.TypeOf(obj) value := reflect.ValueOf(obj) if t.Kind() != reflect.Ptr || value.IsNil() { @@ -125,8 +128,10 @@ func (c *unstructuredConverter) FromUnstructured(u map[string]interface{}, obj i } err := fromUnstructured(reflect.ValueOf(u), value.Elem()) if c.mismatchDetection { + klog.Warningf("detecting mismatches") newObj := reflect.New(t.Elem()).Interface() newErr := fromUnstructuredViaJSON(u, newObj) + klog.Warningf("newErr: %v", newErr) if (err != nil) != (newErr != nil) { klog.Fatalf("FromUnstructured unexpected error for %v: error: %v", u, err) } @@ -146,13 +151,20 @@ func fromUnstructuredViaJSON(u map[string]interface{}, obj interface{}) error { } func fromUnstructured(sv, dv reflect.Value) error { + klog.Warningf("fromUnstructured\n") + klog.Warningf("sv type: %v\n", sv.Type()) + klog.Warningf("sv: %v\n", sv) + klog.Warningf("dv type: %v\n", dv.Type()) + klog.Warningf("dv: %+v\n", dv) sv = unwrapInterface(sv) if !sv.IsValid() { dv.Set(reflect.Zero(dv.Type())) + klog.Warningf("sv not valid returning nil") return nil } st, dt := sv.Type(), dv.Type() + klog.Warningf("dt kind: %v\n", dt.Kind()) switch dt.Kind() { case reflect.Map, reflect.Slice, reflect.Ptr, reflect.Struct, reflect.Interface: // Those require non-trivial conversion. @@ -213,13 +225,17 @@ func fromUnstructured(sv, dv reflect.Value) error { switch dt.Kind() { case reflect.Map: - return mapFromUnstructured(sv, dv) + err := mapFromUnstructured(sv, dv) + klog.Warningf("map final dv: %v\n", dv) + return err case reflect.Slice: return sliceFromUnstructured(sv, dv) case reflect.Ptr: return pointerFromUnstructured(sv, dv) case reflect.Struct: - return structFromUnstructured(sv, dv) + err := structFromUnstructured(sv, dv) + klog.Warningf("struct final dv: %v\n", dv) + return err case reflect.Interface: return interfaceFromUnstructured(sv, dv) default: @@ -287,11 +303,15 @@ func mapFromUnstructured(sv, dv reflect.Value) error { if sv.IsNil() { dv.Set(reflect.Zero(dt)) + klog.Warningf("mFU sv is nil, returning nil") return nil } dv.Set(reflect.MakeMap(dt)) for _, key := range sv.MapKeys() { value := reflect.New(dt.Elem()).Elem() + klog.Warningf("mFU") + klog.Warningf("key: %v", key) + klog.Warningf("value: %v\n", value) if val := unwrapInterface(sv.MapIndex(key)); val.IsValid() { if err := fromUnstructured(val, value); err != nil { return err @@ -305,6 +325,7 @@ func mapFromUnstructured(sv, dv reflect.Value) error { dv.SetMapIndex(key.Convert(dt.Key()), value) } } + klog.Warningf("mFU done successfully") return nil } @@ -365,31 +386,51 @@ func pointerFromUnstructured(sv, dv reflect.Value) error { } func structFromUnstructured(sv, dv reflect.Value) error { + klog.Warningf("sFU!") st, dt := sv.Type(), dv.Type() if st.Kind() != reflect.Map { return fmt.Errorf("cannot restore struct from: %v", st.Kind()) } + svLength := len(sv.MapKeys()) + klog.Warningf("sv length: %d\n", svLength) + klog.Warningf("dt.NumField(): %d\n", dt.NumField()) for i := 0; i < dt.NumField(); i++ { fieldInfo := fieldInfoFromField(dt, i) fv := dv.Field(i) + klog.Warningf("sUF i: %d fieldInfo: %+v\n", i, fieldInfo) if len(fieldInfo.name) == 0 { // This field is inlined. if err := fromUnstructured(sv, fv); err != nil { return err } + klog.Warningf("pop sv len 0") + svLength-- + // else remove the field from the } else { value := unwrapInterface(sv.MapIndex(fieldInfo.nameValue)) + klog.Warningf("sFU value: %v\n", value) if value.IsValid() { + klog.Warningf("valid") if err := fromUnstructured(value, fv); err != nil { return err } + klog.Warningf("pop sv len 1") + svLength-- } else { + // TODO: this doesn't necessarily mean don't pop, + // we need to check if sv actually contains the field (but with a nil value) + // see smp-mergemap-4.log search for + // sUF i: 7 fieldInfo: &{name:creation (the nil one line 17914) + // and + // final sv length 1, value: map[creati (line 17959) + klog.Warningf("invalid, don't pop") fv.Set(reflect.Zero(fv.Type())) } } } + klog.Warningf("final sv length: %d, value: %v\n", svLength, sv) return nil } diff --git a/staging/src/k8s.io/apimachinery/pkg/util/strategicpatch/patch.go b/staging/src/k8s.io/apimachinery/pkg/util/strategicpatch/patch.go index fd2081a28d524..38af999e180d5 100644 --- a/staging/src/k8s.io/apimachinery/pkg/util/strategicpatch/patch.go +++ b/staging/src/k8s.io/apimachinery/pkg/util/strategicpatch/patch.go @@ -25,6 +25,7 @@ import ( "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/util/json" "k8s.io/apimachinery/pkg/util/mergepatch" + "k8s.io/klog/v2" ) // An alternate implementation of JSON Merge Patch @@ -854,6 +855,7 @@ func handleUnmarshal(j []byte) (map[string]interface{}, error) { // calling CreateTwoWayMergeMapPatch. // Warning: the original and patch JSONMap objects are mutated by this function and should not be reused. func StrategicMergeMapPatch(original, patch JSONMap, dataStruct interface{}) (JSONMap, error) { + klog.Warningf("SMMP") schema, err := NewPatchMetaFromStruct(dataStruct) if err != nil { return nil, err @@ -873,11 +875,14 @@ func StrategicMergeMapPatch(original, patch JSONMap, dataStruct interface{}) (JS } func StrategicMergeMapPatchUsingLookupPatchMeta(original, patch JSONMap, schema LookupPatchMeta) (JSONMap, error) { + klog.Warningf("SMMPULPM") mergeOptions := MergeOptions{ MergeParallelList: true, IgnoreUnmatchedNulls: true, } - return mergeMap(original, patch, schema, mergeOptions) + JSONMap, err := mergeMap(original, patch, schema, mergeOptions) + klog.Warningf("JSONMap: %v\n", JSONMap) + return JSONMap, err } // MergeStrategicMergeMapPatchUsingLookupPatchMeta merges strategic merge @@ -1278,6 +1283,15 @@ func partitionMapsByPresentInList(original, partitionBy []interface{}, mergeKey // present in original, then to propagate it to the end result use // mergeOptions.IgnoreUnmatchedNulls == false. func mergeMap(original, patch map[string]interface{}, schema LookupPatchMeta, mergeOptions MergeOptions) (map[string]interface{}, error) { + klog.Warningf("================ mergeMap baby!") + klog.Warningf("================ original:\n") + for k, v := range original { + klog.Warningf("k: %s\n v:%v\n", k, v) + } + klog.Warningf("================ patch:\n") + for k, v := range patch { + klog.Warningf("k: %s\n v:%v\n", k, v) + } if v, ok := patch[directiveMarker]; ok { return handleDirectiveInMergeMap(v, patch) } @@ -1368,6 +1382,10 @@ func mergeMap(original, patch map[string]interface{}, schema LookupPatchMeta, me return nil, err } } + klog.Warningf("================ final original:\n") + for k, v := range original { + klog.Warningf("k: %s\n v:%v\n", k, v) + } return original, nil } diff --git a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/patch.go b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/patch.go index 9ef2a841b2369..f86c0e4171bc9 100644 --- a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/patch.go +++ b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/patch.go @@ -89,6 +89,7 @@ func PatchResource(r rest.Patcher, scope *RequestScope, admit admission.Interfac contentType = contentType[:idx] } patchType := types.PatchType(contentType) + klog.Warningf("patchType: %v", patchType) // Ensure the patchType is one we support if !sets.NewString(patchTypes...).Has(contentType) { @@ -413,6 +414,7 @@ type smpPatcher struct { } func (p *smpPatcher) applyPatchToCurrentObject(currentObject runtime.Object) (runtime.Object, error) { + klog.Warningf("smpPatch aPTCO") // Since the patch is applied on versioned objects, we need to convert the // current object to versioned representation first. currentVersionedObject, err := p.unsafeConvertor.ConvertToVersion(currentObject, p.kind.GroupVersion()) @@ -488,6 +490,7 @@ func strategicPatchObject( objToUpdate runtime.Object, schemaReferenceObj runtime.Object, ) error { + klog.Warningf("sPO") originalObjMap, err := runtime.DefaultUnstructuredConverter.ToUnstructured(originalObject) if err != nil { return err @@ -651,19 +654,31 @@ func applyPatchToObject( objToUpdate runtime.Object, schemaReferenceObj runtime.Object, ) error { + klog.Warningf("aPTO") + // Note: objToUpdate always starts out here as an empty deployment + // even if it exists (it has to exist for this to be called because it's a PATCH, duh) + klog.Warningf("objToUpdate beginning: %v\n", objToUpdate) patchedObjMap, err := strategicpatch.StrategicMergeMapPatch(originalMap, patchMap, schemaReferenceObj) if err != nil { return interpretStrategicMergePatchError(err) } + // foo still exists here + klog.Warningf("patchedObjMap after SMMP: %v\n", patchedObjMap) // Rather than serialize the patched map to JSON, then decode it to an object, we go directly from a map to an object + // Note: this is what removes the invalid foo field if err := runtime.DefaultUnstructuredConverter.FromUnstructured(patchedObjMap, objToUpdate); err != nil { return errors.NewInvalid(schema.GroupKind{}, "", field.ErrorList{ field.Invalid(field.NewPath("patch"), fmt.Sprintf("%+v", patchMap), err.Error()), }) } + // foo still exists here + klog.Warningf("patchedObjMap after FU: %v\n", patchedObjMap) + // foo GONE HERE + klog.Warningf("objToUpdate after FU: %v\n", objToUpdate) // Decoding from JSON to a versioned object would apply defaults, so we do the same here defaulter.Default(objToUpdate) + klog.Warningf("objToUpdate after Default: %v\n", objToUpdate) return nil } From 802ec6de31b156e2c9b07db4d25eb045006dad8f Mon Sep 17 00:00:00 2001 From: Kevin Delgado Date: Mon, 20 Sep 2021 20:36:30 +0000 Subject: [PATCH 21/64] wip save pre debug cleanup --- .../apimachinery/pkg/runtime/converter.go | 125 ++++++++++++---- .../integration/apiserver/apply/apply_test.go | 139 ++++++++++++++++++ 2 files changed, 238 insertions(+), 26 deletions(-) diff --git a/staging/src/k8s.io/apimachinery/pkg/runtime/converter.go b/staging/src/k8s.io/apimachinery/pkg/runtime/converter.go index 11eeb05d02b23..b25b409388bee 100644 --- a/staging/src/k8s.io/apimachinery/pkg/runtime/converter.go +++ b/staging/src/k8s.io/apimachinery/pkg/runtime/converter.go @@ -119,14 +119,16 @@ func NewTestUnstructuredConverter(comparison conversion.Equalities) Unstructured // It uses encoding/json/Unmarshaler if object implements it or reflection if not. func (c *unstructuredConverter) FromUnstructured(u map[string]interface{}, obj interface{}) error { klog.Warningf("FromUnstructured\n") - c.mismatchDetection = true + klog.Warningf("u Map: %+v\n", u) + klog.Warningf("obj: %+v\n", obj) + //c.mismatchDetection = true klog.Warningf("mismatchDetection?: %t", c.mismatchDetection) t := reflect.TypeOf(obj) value := reflect.ValueOf(obj) if t.Kind() != reflect.Ptr || value.IsNil() { return fmt.Errorf("FromUnstructured requires a non-nil pointer to an object, got %v", t) } - err := fromUnstructured(reflect.ValueOf(u), value.Elem()) + err := fromUnstructured(reflect.ValueOf(u), value.Elem(), 0) if c.mismatchDetection { klog.Warningf("detecting mismatches") newObj := reflect.New(t.Elem()).Interface() @@ -150,8 +152,8 @@ func fromUnstructuredViaJSON(u map[string]interface{}, obj interface{}) error { return json.Unmarshal(data, obj) } -func fromUnstructured(sv, dv reflect.Value) error { - klog.Warningf("fromUnstructured\n") +func fromUnstructured(sv, dv reflect.Value, level int) error { + klog.Warningf("fromUnstructured level %d\n", level) klog.Warningf("sv type: %v\n", sv.Type()) klog.Warningf("sv: %v\n", sv) klog.Warningf("dv type: %v\n", dv.Type()) @@ -225,15 +227,15 @@ func fromUnstructured(sv, dv reflect.Value) error { switch dt.Kind() { case reflect.Map: - err := mapFromUnstructured(sv, dv) + err := mapFromUnstructured(sv, dv, level) klog.Warningf("map final dv: %v\n", dv) return err case reflect.Slice: - return sliceFromUnstructured(sv, dv) + return sliceFromUnstructured(sv, dv, level) case reflect.Ptr: - return pointerFromUnstructured(sv, dv) + return pointerFromUnstructured(sv, dv, level) case reflect.Struct: - err := structFromUnstructured(sv, dv) + err := structFromUnstructured(sv, dv, level) klog.Warningf("struct final dv: %v\n", dv) return err case reflect.Interface: @@ -291,7 +293,7 @@ func unwrapInterface(v reflect.Value) reflect.Value { return v } -func mapFromUnstructured(sv, dv reflect.Value) error { +func mapFromUnstructured(sv, dv reflect.Value, level int) error { st, dt := sv.Type(), dv.Type() if st.Kind() != reflect.Map { return fmt.Errorf("cannot restore map from %v", st.Kind()) @@ -313,7 +315,7 @@ func mapFromUnstructured(sv, dv reflect.Value) error { klog.Warningf("key: %v", key) klog.Warningf("value: %v\n", value) if val := unwrapInterface(sv.MapIndex(key)); val.IsValid() { - if err := fromUnstructured(val, value); err != nil { + if err := fromUnstructured(val, value, level+1); err != nil { return err } } else { @@ -329,7 +331,7 @@ func mapFromUnstructured(sv, dv reflect.Value) error { return nil } -func sliceFromUnstructured(sv, dv reflect.Value) error { +func sliceFromUnstructured(sv, dv reflect.Value, level int) error { st, dt := sv.Type(), dv.Type() if st.Kind() == reflect.String && dt.Elem().Kind() == reflect.Uint8 { // We store original []byte representation as string. @@ -362,14 +364,14 @@ func sliceFromUnstructured(sv, dv reflect.Value) error { } dv.Set(reflect.MakeSlice(dt, sv.Len(), sv.Cap())) for i := 0; i < sv.Len(); i++ { - if err := fromUnstructured(sv.Index(i), dv.Index(i)); err != nil { + if err := fromUnstructured(sv.Index(i), dv.Index(i), level+1); err != nil { return err } } return nil } -func pointerFromUnstructured(sv, dv reflect.Value) error { +func pointerFromUnstructured(sv, dv reflect.Value, level int) error { st, dt := sv.Type(), dv.Type() if st.Kind() == reflect.Ptr && sv.IsNil() { @@ -379,45 +381,113 @@ func pointerFromUnstructured(sv, dv reflect.Value) error { dv.Set(reflect.New(dt.Elem())) switch st.Kind() { case reflect.Ptr, reflect.Interface: - return fromUnstructured(sv.Elem(), dv.Elem()) + return fromUnstructured(sv.Elem(), dv.Elem(), level+1) default: - return fromUnstructured(sv, dv.Elem()) + return fromUnstructured(sv, dv.Elem(), level+1) } } -func structFromUnstructured(sv, dv reflect.Value) error { +func structFromUnstructured(sv, dv reflect.Value, level int) error { klog.Warningf("sFU!") st, dt := sv.Type(), dv.Type() if st.Kind() != reflect.Map { return fmt.Errorf("cannot restore struct from: %v", st.Kind()) } svLength := len(sv.MapKeys()) - klog.Warningf("sv length: %d\n", svLength) + timeCode := time.Now().UnixNano() + klog.Warningf("START sv length: %d, tc: %d, value: %v\n", svLength, timeCode, sv) + klog.Warningf("dv: %+v", dv) + klog.Warningf("tc: %d\n", timeCode) + for i, k := range sv.MapKeys() { + klog.Warningf("sv key: %+v at i: %d", k, i) + } + + keys := sv.MapKeys() + // TODO: check if this is right b/c IDK if this is actually correct + inlined := false klog.Warningf("dt.NumField(): %d\n", dt.NumField()) for i := 0; i < dt.NumField(); i++ { fieldInfo := fieldInfoFromField(dt, i) fv := dv.Field(i) klog.Warningf("sUF i: %d fieldInfo: %+v\n", i, fieldInfo) + klog.Warningf("tc: %d\n", timeCode) if len(fieldInfo.name) == 0 { + inlined = true // This field is inlined. - if err := fromUnstructured(sv, fv); err != nil { + // TODO: somehow we need to know for inlined fields how many to pop + // because it wont always be 1 I think, could be 0,1, or more? + // see metadata edge case + // Answer: I think we might want to pop based on length of fv, because that tells us + klog.Warningf("pop sv len 0") + klog.Warningf("fieldInfoNameValue, %+v\n", fieldInfo.nameValue) + klog.Warningf("fv len %d", fv.Type().NumField()) + klog.Warningf("fv: %+v\n", fv) + klog.Warningf("sv: %+v\n", sv) + svLength -= fv.Type().NumField() + for i := 0; i < fv.Type().NumField(); i++ { + curField := fv.Type().FieldByIndex([]int{i}) + klog.Warningf("field %d is %+v\n", i, curField) + jsonTag := curField.Tag.Get("json") + klog.Warningf("jsonTag %s", jsonTag) + // TODO: check for string length error if <1 + jsonName := strings.Split(jsonTag, ",")[0] + klog.Warningf("jsonName %s", jsonName) + // TODO: factor this out into deleteFromKeys() + for i := len(keys) - 1; i >= 0; i-- { + if jsonName == keys[i].String() { + // Delete from keys + klog.Warningf("pop keys inline jsonName: %s", keys[i].String()) + keys = append(keys[:i], keys[i+1:]...) + klog.Warningf("keylength inline is now %d after i %d", len(keys), i) + klog.Warningf("tc: %d\n", timeCode) + break + } + } + } + klog.Warningf("svLenght is now %d after i %d", svLength, i) + klog.Warningf("tc: %d\n", timeCode) + if err := fromUnstructured(sv, fv, level+1); err != nil { return err } - klog.Warningf("pop sv len 0") - svLength-- + klog.Warningf("fv post recurse: %+v\n", fv) + klog.Warningf("tc: %d\n", timeCode) // else remove the field from the } else { + //for i, k := range keys { + // if fieldInfo.name == k.String() { + // // Delete from keys + // klog.Warningf("pop sv len 2 the field is %s", fieldInfo.name) + // svLength-- + // klog.Warningf("svLenght is now %d after i %d", svLength, i) + // klog.Warningf("tc: %d\n", timeCode) + // break + // } + //} + + // TODO: factor this out into deleteFromKeys() + for i := len(keys) - 1; i >= 0; i-- { + if fieldInfo.name == keys[i].String() { + // Delete from keys + klog.Warningf("pop keys struct jsonName: %s", keys[i].String()) + keys = append(keys[:i], keys[i+1:]...) + klog.Warningf("keylength struct is now %d after i %d", len(keys), i) + klog.Warningf("tc: %d\n", timeCode) + break + } + } value := unwrapInterface(sv.MapIndex(fieldInfo.nameValue)) klog.Warningf("sFU value: %v\n", value) + klog.Warningf("tc: %d\n", timeCode) if value.IsValid() { - klog.Warningf("valid") - if err := fromUnstructured(value, fv); err != nil { + //klog.Warningf("valid") + if err := fromUnstructured(value, fv, level+1); err != nil { return err } - klog.Warningf("pop sv len 1") - svLength-- + //klog.Warningf("pop sv len 1") + //svLength-- + //klog.Warningf("svLenght is now %d", svLength) } else { // TODO: this doesn't necessarily mean don't pop, // we need to check if sv actually contains the field (but with a nil value) @@ -425,12 +495,15 @@ func structFromUnstructured(sv, dv reflect.Value) error { // sUF i: 7 fieldInfo: &{name:creation (the nil one line 17914) // and // final sv length 1, value: map[creati (line 17959) - klog.Warningf("invalid, don't pop") + //klog.Warningf("invalid, don't pop") fv.Set(reflect.Zero(fv.Type())) } } } - klog.Warningf("final sv length: %d, value: %v\n", svLength, sv) + klog.Warningf("END sv length: %d, value: %+v\n", svLength, sv) + klog.Warningf("end keys length: %d inlined: %t, value: %+v\n", len(keys), inlined, keys) + klog.Warningf("end dv length: %d, value: %v\n", dv.Type().NumField(), dv) + klog.Warningf("tc: %d\n", timeCode) return nil } diff --git a/test/integration/apiserver/apply/apply_test.go b/test/integration/apiserver/apply/apply_test.go index aec9e9ea1386a..2d2690b41a912 100644 --- a/test/integration/apiserver/apply/apply_test.go +++ b/test/integration/apiserver/apply/apply_test.go @@ -45,6 +45,7 @@ import ( clientset "k8s.io/client-go/kubernetes" restclient "k8s.io/client-go/rest" featuregatetesting "k8s.io/component-base/featuregate/testing" + "k8s.io/klog/v2" "k8s.io/kubernetes/pkg/controlplane" "k8s.io/kubernetes/test/integration/framework" ) @@ -1235,6 +1236,144 @@ func TestClearManagedFieldsWithMergePatch(t *testing.T) { } } +func TestSMPValidation(t *testing.T) { + defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, genericfeatures.ServerSideApply, true)() + + _, client, closeFn := setup(t) + defer closeFn() + + _, err := client.CoreV1().RESTClient().Patch(types.ApplyPatchType). + AbsPath("/apis/apps/v1"). + Namespace("default"). + Resource("deployments"). + Name("test-deployment"). + Param("fieldManager", "apply_test"). + Body([]byte(`{ + "apiVersion": "apps/v1", + "kind": "Deployment", + "metadata": { + "name": "test-deployment", + "labels": {"app": "nginx"} + }, + "spec": { + "selector": { + "matchLabels": { + "app": "nginx" + } + }, + "template": { + "metadata": { + "labels": { + "app": "nginx" + } + }, + "spec": { + "containers": [{ + "name": "nginx", + "image": "nginx:latest" + }] + } + } + } + }`)). + Do(context.TODO()). + Get() + if err != nil { + t.Fatalf("Failed to create object using Apply patch: %v", err) + } + + obj, err := client.CoreV1().RESTClient().Patch(types.StrategicMergePatchType). + AbsPath("/apis/apps/v1"). + Namespace("default"). + Resource("deployments"). + Name("test-deployment"). + Body([]byte(`{"metadata":{"labels":{"label1": "val1"}},"spec":{"foo":"bar"}}`)).Do(context.TODO()).Get() + if err != nil { + t.Fatalf("Failed to patch object: %v", err) + } + + klog.Warningf("final obj: %v\n", obj) + + //object, err := client.CoreV1().RESTClient().Get().Namespace("default").Resource("configmaps").Name("test-cm").Do(context.TODO()).Get() + //if err != nil { + // t.Fatalf("Failed to retrieve object: %v", err) + //} + + //accessor, err := meta.Accessor(object) + //if err != nil { + // t.Fatalf("Failed to get meta accessor: %v", err) + //} + + //if managedFields := accessor.GetManagedFields(); len(managedFields) != 0 { + // t.Fatalf("Failed to clear managedFields, got: %v", managedFields) + //} + + //if labels := accessor.GetLabels(); len(labels) < 1 { + // t.Fatalf("Expected other fields to stay untouched, got: %v", object) + //} +} +func TestCMSMPValidationCM(t *testing.T) { + defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, genericfeatures.ServerSideApply, true)() + + _, client, closeFn := setup(t) + defer closeFn() + + _, err := client.CoreV1().RESTClient().Patch(types.ApplyPatchType). + Namespace("default"). + Resource("configmaps"). + Name("test-cm"). + Param("fieldManager", "apply_test"). + Body([]byte(`{ + "apiVersion": "v1", + "kind": "ConfigMap", + "metadata": { + "name": "test-cm", + "namespace": "default", + "labels": { + "test-label": "test" + } + }, + "data": { + "key": "value" + } + }`)). + Do(context.TODO()). + Get() + if err != nil { + t.Fatalf("Failed to create object using Apply patch: %v", err) + } + + obj, err := client.CoreV1().RESTClient().Patch(types.StrategicMergePatchType). + Namespace("default"). + Resource("configmaps"). + Name("test-cm"). + //Body([]byte(`{"metadata":{"managedFields": [{}]}}`)).Do(context.TODO()).Get() + Body([]byte(`{"metadata":{"foo":"bar"},"data":{"key2":"value2"}}`)).Do(context.TODO()).Get() + if err != nil { + t.Fatalf("Failed to patch object: %v", err) + } + + klog.Warningf("final obj: %v\n", obj) + + //object, err := client.CoreV1().RESTClient().Get().Namespace("default").Resource("configmaps").Name("test-cm").Do(context.TODO()).Get() + //if err != nil { + // t.Fatalf("Failed to retrieve object: %v", err) + //} + + //accessor, err := meta.Accessor(object) + //if err != nil { + // t.Fatalf("Failed to get meta accessor: %v", err) + //} + + //if managedFields := accessor.GetManagedFields(); len(managedFields) != 0 { + // t.Fatalf("Failed to clear managedFields, got: %v", managedFields) + //} + + //if labels := accessor.GetLabels(); len(labels) < 1 { + // t.Fatalf("Expected other fields to stay untouched, got: %v", object) + //} +} + // TestClearManagedFieldsWithStrategicMergePatch verifies it's possible to clear the managedFields func TestClearManagedFieldsWithStrategicMergePatch(t *testing.T) { defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, genericfeatures.ServerSideApply, true)() From 9da68d705b058b9a3d6d8cb010d2b1200d576635 Mon Sep 17 00:00:00 2001 From: Kevin Delgado Date: Mon, 20 Sep 2021 21:20:46 +0000 Subject: [PATCH 22/64] WIP factor deleteFromKeys, stash pre help chat --- .../apimachinery/pkg/runtime/converter.go | 109 ++++++------------ 1 file changed, 35 insertions(+), 74 deletions(-) diff --git a/staging/src/k8s.io/apimachinery/pkg/runtime/converter.go b/staging/src/k8s.io/apimachinery/pkg/runtime/converter.go index b25b409388bee..8144a00a1ea68 100644 --- a/staging/src/k8s.io/apimachinery/pkg/runtime/converter.go +++ b/staging/src/k8s.io/apimachinery/pkg/runtime/converter.go @@ -388,125 +388,86 @@ func pointerFromUnstructured(sv, dv reflect.Value, level int) error { } func structFromUnstructured(sv, dv reflect.Value, level int) error { - klog.Warningf("sFU!") st, dt := sv.Type(), dv.Type() if st.Kind() != reflect.Map { return fmt.Errorf("cannot restore struct from: %v", st.Kind()) } + // DEBUG printing svLength := len(sv.MapKeys()) + keys := sv.MapKeys() timeCode := time.Now().UnixNano() klog.Warningf("START sv length: %d, tc: %d, value: %v\n", svLength, timeCode, sv) klog.Warningf("dv: %+v", dv) + klog.Warningf("dt.NumField(): %d\n", dt.NumField()) klog.Warningf("tc: %d\n", timeCode) - for i, k := range sv.MapKeys() { + for i, k := range keys { klog.Warningf("sv key: %+v at i: %d", k, i) } - keys := sv.MapKeys() - // TODO: check if this is right b/c IDK if this is actually correct inlined := false - klog.Warningf("dt.NumField(): %d\n", dt.NumField()) for i := 0; i < dt.NumField(); i++ { + // + // flatten field check + // fieldInfo := fieldInfoFromField(dt, i) fv := dv.Field(i) - klog.Warningf("sUF i: %d fieldInfo: %+v\n", i, fieldInfo) + klog.Warningf("dt field i: %d fieldInfo: %+v\n", i, fieldInfo) klog.Warningf("tc: %d\n", timeCode) if len(fieldInfo.name) == 0 { + // This field is inlined inlined = true - // This field is inlined. - // TODO: somehow we need to know for inlined fields how many to pop - // because it wont always be 1 I think, could be 0,1, or more? - // see metadata edge case - // Answer: I think we might want to pop based on length of fv, because that tells us - klog.Warningf("pop sv len 0") - klog.Warningf("fieldInfoNameValue, %+v\n", fieldInfo.nameValue) - klog.Warningf("fv len %d", fv.Type().NumField()) - klog.Warningf("fv: %+v\n", fv) - klog.Warningf("sv: %+v\n", sv) - svLength -= fv.Type().NumField() + // get the name of the field and delete it from the source's keys for i := 0; i < fv.Type().NumField(); i++ { curField := fv.Type().FieldByIndex([]int{i}) - klog.Warningf("field %d is %+v\n", i, curField) jsonTag := curField.Tag.Get("json") - klog.Warningf("jsonTag %s", jsonTag) - // TODO: check for string length error if <1 jsonName := strings.Split(jsonTag, ",")[0] - klog.Warningf("jsonName %s", jsonName) - // TODO: factor this out into deleteFromKeys() - for i := len(keys) - 1; i >= 0; i-- { - if jsonName == keys[i].String() { - // Delete from keys - klog.Warningf("pop keys inline jsonName: %s", keys[i].String()) - keys = append(keys[:i], keys[i+1:]...) - klog.Warningf("keylength inline is now %d after i %d", len(keys), i) - klog.Warningf("tc: %d\n", timeCode) - break - } - } + klog.Warningf("deleting jsonName %s from keys at tc: %d", jsonName, timeCode) + deleteFromKeys(jsonName, &keys) + klog.Warningf("post keylength %d", len(keys)) } - klog.Warningf("svLenght is now %d after i %d", svLength, i) - klog.Warningf("tc: %d\n", timeCode) + if err := fromUnstructured(sv, fv, level+1); err != nil { return err } - klog.Warningf("fv post recurse: %+v\n", fv) - klog.Warningf("tc: %d\n", timeCode) - // else remove the field from the } else { - //for i, k := range keys { - // if fieldInfo.name == k.String() { - // // Delete from keys - // klog.Warningf("pop sv len 2 the field is %s", fieldInfo.name) - // svLength-- - // klog.Warningf("svLenght is now %d after i %d", svLength, i) - // klog.Warningf("tc: %d\n", timeCode) - // break - // } - //} - - // TODO: factor this out into deleteFromKeys() - for i := len(keys) - 1; i >= 0; i-- { - if fieldInfo.name == keys[i].String() { - // Delete from keys - klog.Warningf("pop keys struct jsonName: %s", keys[i].String()) - keys = append(keys[:i], keys[i+1:]...) - klog.Warningf("keylength struct is now %d after i %d", len(keys), i) - klog.Warningf("tc: %d\n", timeCode) - break - } - } + // delete from the source's keys + klog.Warningf("deleting fInV %s from keys at tc: %d", fieldInfo.nameValue, timeCode) + deleteFromKeys(fieldInfo.name, &keys) + klog.Warningf("post keylength %d", len(keys)) + value := unwrapInterface(sv.MapIndex(fieldInfo.nameValue)) - klog.Warningf("sFU value: %v\n", value) - klog.Warningf("tc: %d\n", timeCode) if value.IsValid() { - //klog.Warningf("valid") if err := fromUnstructured(value, fv, level+1); err != nil { return err } - //klog.Warningf("pop sv len 1") - //svLength-- - //klog.Warningf("svLenght is now %d", svLength) } else { - // TODO: this doesn't necessarily mean don't pop, - // we need to check if sv actually contains the field (but with a nil value) - // see smp-mergemap-4.log search for - // sUF i: 7 fieldInfo: &{name:creation (the nil one line 17914) - // and - // final sv length 1, value: map[creati (line 17959) - //klog.Warningf("invalid, don't pop") fv.Set(reflect.Zero(fv.Type())) } } } - klog.Warningf("END sv length: %d, value: %+v\n", svLength, sv) - klog.Warningf("end keys length: %d inlined: %t, value: %+v\n", len(keys), inlined, keys) + // TODO: error here if len(keys) > 0 meaning there is are unknown fields + klog.Warningf("END keys length: %d inlined: %t, value: %+v\n", len(keys), inlined, keys) klog.Warningf("end dv length: %d, value: %v\n", dv.Type().NumField(), dv) klog.Warningf("tc: %d\n", timeCode) return nil } +func deleteFromKeys(name string, keys *[]reflect.Value) { + klog.Warningf("dFK starting keylength: %d", len(*keys)) + for i := len(*keys) - 1; i >= 0; i-- { + if name == (*keys)[i].String() { + // Delete from keys + klog.Warningf("pop keys struct name: %s", (*keys)[i].String()) + *keys = append((*keys)[:i], (*keys)[i+1:]...) + klog.Warningf("keyslength is now %d after i %d", len(*keys), i) + //klog.Warningf("tc: %d\n", timeCode) + return + } + } +} + func interfaceFromUnstructured(sv, dv reflect.Value) error { // TODO: Is this conversion safe? dv.Set(sv) From e19c59137ec78c3d9c73497f564b1cc09a345adb Mon Sep 17 00:00:00 2001 From: Kevin Delgado Date: Tue, 21 Sep 2021 22:37:48 +0000 Subject: [PATCH 23/64] IT WORKS --- .../apimachinery/pkg/runtime/converter.go | 34 ++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/staging/src/k8s.io/apimachinery/pkg/runtime/converter.go b/staging/src/k8s.io/apimachinery/pkg/runtime/converter.go index 8144a00a1ea68..8431bca9cb24f 100644 --- a/staging/src/k8s.io/apimachinery/pkg/runtime/converter.go +++ b/staging/src/k8s.io/apimachinery/pkg/runtime/converter.go @@ -406,6 +406,21 @@ func structFromUnstructured(sv, dv reflect.Value, level int) error { inlined := false + // flatten fields check + // 1. create map of fields (set) + // 2. non-inlined fields: add to map + // 3. inlined fields: take to lower level add to map, recurse + //for i := 0; i < dt.NumField(); i++ { + // fieldInfo := fieldInfoFromField(dt, i) + // fv := dv.Field(i) + // klog.Warningf("dt field i: %d fieldInfo: %+v\n", i, fieldInfo) + // klog.Warningf("tc: %d\n", timeCode) + // if len(fieldInfo.name) == 0 { + // destFieldsSet := getFieldsSet(fv) + // } + + //} + for i := 0; i < dt.NumField(); i++ { // // flatten field check @@ -415,6 +430,7 @@ func structFromUnstructured(sv, dv reflect.Value, level int) error { klog.Warningf("dt field i: %d fieldInfo: %+v\n", i, fieldInfo) klog.Warningf("tc: %d\n", timeCode) + inlinedValues := map[string]interface{}{} if len(fieldInfo.name) == 0 { // This field is inlined inlined = true @@ -426,9 +442,25 @@ func structFromUnstructured(sv, dv reflect.Value, level int) error { klog.Warningf("deleting jsonName %s from keys at tc: %d", jsonName, timeCode) deleteFromKeys(jsonName, &keys) klog.Warningf("post keylength %d", len(keys)) + svMap, ok := (sv.Interface()).(map[string]interface{}) + if !ok { + klog.Warningf("couldn't cast sv map: %+v", sv) + } + if val, ok := svMap[jsonName]; ok { + inlinedValues[jsonName] = val + klog.Warningf("set inlinedValues of jsonName %s to value %+v", jsonName, val) + } else { + klog.Warningf("couldn't hydrate inlinedValues for jsonName %s", jsonName) + } } - if err := fromUnstructured(sv, fv, level+1); err != nil { + // this is problem is that we pass the whole sv into fromUnstructured, we should just pass the + // value + //if err := fromUnstructured(sv, fv, level+1); err != nil { + // return err + //} + klog.Warningf("recursing into from unstructred with subset of sv: %+v", inlinedValues) + if err := fromUnstructured(reflect.ValueOf(inlinedValues), fv, level+1); err != nil { return err } } else { From d45a665d081210f3170b776acdb344ce161a29ee Mon Sep 17 00:00:00 2001 From: Kevin Delgado Date: Tue, 21 Sep 2021 23:50:59 +0000 Subject: [PATCH 24/64] wip, hacky test working pre cleanup --- .../apimachinery/pkg/runtime/converter.go | 58 +++++++++++++------ .../apiserver/pkg/endpoints/handlers/patch.go | 3 +- .../integration/apiserver/apply/apply_test.go | 5 +- 3 files changed, 46 insertions(+), 20 deletions(-) diff --git a/staging/src/k8s.io/apimachinery/pkg/runtime/converter.go b/staging/src/k8s.io/apimachinery/pkg/runtime/converter.go index 8431bca9cb24f..6dc680e41e756 100644 --- a/staging/src/k8s.io/apimachinery/pkg/runtime/converter.go +++ b/staging/src/k8s.io/apimachinery/pkg/runtime/converter.go @@ -81,6 +81,16 @@ var ( }, ), } + + StrictUnstructuredConverter = &unstructuredConverter{ + mismatchDetection: parseBool(os.Getenv("KUBE_PATCH_CONVERSION_DETECTOR")), + comparison: conversion.EqualitiesOrDie( + func(a, b time.Time) bool { + return a.UTC() == b.UTC() + }, + ), + strictFieldValidation: true, + } ) func parseBool(key string) bool { @@ -102,7 +112,8 @@ type unstructuredConverter struct { // This is supposed to be set only in tests. mismatchDetection bool // comparison is the default test logic used to compare - comparison conversion.Equalities + comparison conversion.Equalities + strictFieldValidation bool } // NewTestUnstructuredConverter creates an UnstructuredConverter that accepts JSON typed maps and translates them @@ -128,7 +139,7 @@ func (c *unstructuredConverter) FromUnstructured(u map[string]interface{}, obj i if t.Kind() != reflect.Ptr || value.IsNil() { return fmt.Errorf("FromUnstructured requires a non-nil pointer to an object, got %v", t) } - err := fromUnstructured(reflect.ValueOf(u), value.Elem(), 0) + err := c.fromUnstructured(reflect.ValueOf(u), value.Elem(), 0) if c.mismatchDetection { klog.Warningf("detecting mismatches") newObj := reflect.New(t.Elem()).Interface() @@ -152,7 +163,7 @@ func fromUnstructuredViaJSON(u map[string]interface{}, obj interface{}) error { return json.Unmarshal(data, obj) } -func fromUnstructured(sv, dv reflect.Value, level int) error { +func (c *unstructuredConverter) fromUnstructured(sv, dv reflect.Value, level int) error { klog.Warningf("fromUnstructured level %d\n", level) klog.Warningf("sv type: %v\n", sv.Type()) klog.Warningf("sv: %v\n", sv) @@ -227,15 +238,15 @@ func fromUnstructured(sv, dv reflect.Value, level int) error { switch dt.Kind() { case reflect.Map: - err := mapFromUnstructured(sv, dv, level) + err := c.mapFromUnstructured(sv, dv, level) klog.Warningf("map final dv: %v\n", dv) return err case reflect.Slice: - return sliceFromUnstructured(sv, dv, level) + return c.sliceFromUnstructured(sv, dv, level) case reflect.Ptr: - return pointerFromUnstructured(sv, dv, level) + return c.pointerFromUnstructured(sv, dv, level) case reflect.Struct: - err := structFromUnstructured(sv, dv, level) + err := c.structFromUnstructured(sv, dv, level) klog.Warningf("struct final dv: %v\n", dv) return err case reflect.Interface: @@ -293,7 +304,7 @@ func unwrapInterface(v reflect.Value) reflect.Value { return v } -func mapFromUnstructured(sv, dv reflect.Value, level int) error { +func (c *unstructuredConverter) mapFromUnstructured(sv, dv reflect.Value, level int) error { st, dt := sv.Type(), dv.Type() if st.Kind() != reflect.Map { return fmt.Errorf("cannot restore map from %v", st.Kind()) @@ -315,7 +326,7 @@ func mapFromUnstructured(sv, dv reflect.Value, level int) error { klog.Warningf("key: %v", key) klog.Warningf("value: %v\n", value) if val := unwrapInterface(sv.MapIndex(key)); val.IsValid() { - if err := fromUnstructured(val, value, level+1); err != nil { + if err := c.fromUnstructured(val, value, level+1); err != nil { return err } } else { @@ -331,7 +342,7 @@ func mapFromUnstructured(sv, dv reflect.Value, level int) error { return nil } -func sliceFromUnstructured(sv, dv reflect.Value, level int) error { +func (c *unstructuredConverter) sliceFromUnstructured(sv, dv reflect.Value, level int) error { st, dt := sv.Type(), dv.Type() if st.Kind() == reflect.String && dt.Elem().Kind() == reflect.Uint8 { // We store original []byte representation as string. @@ -364,14 +375,14 @@ func sliceFromUnstructured(sv, dv reflect.Value, level int) error { } dv.Set(reflect.MakeSlice(dt, sv.Len(), sv.Cap())) for i := 0; i < sv.Len(); i++ { - if err := fromUnstructured(sv.Index(i), dv.Index(i), level+1); err != nil { + if err := c.fromUnstructured(sv.Index(i), dv.Index(i), level+1); err != nil { return err } } return nil } -func pointerFromUnstructured(sv, dv reflect.Value, level int) error { +func (c *unstructuredConverter) pointerFromUnstructured(sv, dv reflect.Value, level int) error { st, dt := sv.Type(), dv.Type() if st.Kind() == reflect.Ptr && sv.IsNil() { @@ -381,13 +392,13 @@ func pointerFromUnstructured(sv, dv reflect.Value, level int) error { dv.Set(reflect.New(dt.Elem())) switch st.Kind() { case reflect.Ptr, reflect.Interface: - return fromUnstructured(sv.Elem(), dv.Elem(), level+1) + return c.fromUnstructured(sv.Elem(), dv.Elem(), level+1) default: - return fromUnstructured(sv, dv.Elem(), level+1) + return c.fromUnstructured(sv, dv.Elem(), level+1) } } -func structFromUnstructured(sv, dv reflect.Value, level int) error { +func (c *unstructuredConverter) structFromUnstructured(sv, dv reflect.Value, level int) error { st, dt := sv.Type(), dv.Type() if st.Kind() != reflect.Map { return fmt.Errorf("cannot restore struct from: %v", st.Kind()) @@ -460,7 +471,7 @@ func structFromUnstructured(sv, dv reflect.Value, level int) error { // return err //} klog.Warningf("recursing into from unstructred with subset of sv: %+v", inlinedValues) - if err := fromUnstructured(reflect.ValueOf(inlinedValues), fv, level+1); err != nil { + if err := c.fromUnstructured(reflect.ValueOf(inlinedValues), fv, level+1); err != nil { return err } } else { @@ -471,7 +482,7 @@ func structFromUnstructured(sv, dv reflect.Value, level int) error { value := unwrapInterface(sv.MapIndex(fieldInfo.nameValue)) if value.IsValid() { - if err := fromUnstructured(value, fv, level+1); err != nil { + if err := c.fromUnstructured(value, fv, level+1); err != nil { return err } } else { @@ -483,9 +494,22 @@ func structFromUnstructured(sv, dv reflect.Value, level int) error { klog.Warningf("END keys length: %d inlined: %t, value: %+v\n", len(keys), inlined, keys) klog.Warningf("end dv length: %d, value: %v\n", dv.Type().NumField(), dv) klog.Warningf("tc: %d\n", timeCode) + if len(keys) > 0 && c.strictFieldValidation { + return &UnknownFieldError{ + invalidFields: keys, + } + } return nil } +type UnknownFieldError struct { + invalidFields []reflect.Value +} + +func (fe *UnknownFieldError) Error() string { + return fmt.Sprintf("unknown fields when converting from unstructured: %+v", fe.invalidFields) +} + func deleteFromKeys(name string, keys *[]reflect.Value) { klog.Warningf("dFK starting keylength: %d", len(*keys)) for i := len(*keys) - 1; i >= 0; i-- { diff --git a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/patch.go b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/patch.go index f86c0e4171bc9..5d40da0314cb5 100644 --- a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/patch.go +++ b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/patch.go @@ -667,7 +667,8 @@ func applyPatchToObject( // Rather than serialize the patched map to JSON, then decode it to an object, we go directly from a map to an object // Note: this is what removes the invalid foo field - if err := runtime.DefaultUnstructuredConverter.FromUnstructured(patchedObjMap, objToUpdate); err != nil { + //if err := runtime.DefaultUnstructuredConverter.FromUnstructured(patchedObjMap, objToUpdate); err != nil { + if err := runtime.StrictUnstructuredConverter.FromUnstructured(patchedObjMap, objToUpdate); err != nil { return errors.NewInvalid(schema.GroupKind{}, "", field.ErrorList{ field.Invalid(field.NewPath("patch"), fmt.Sprintf("%+v", patchMap), err.Error()), }) diff --git a/test/integration/apiserver/apply/apply_test.go b/test/integration/apiserver/apply/apply_test.go index 2d2690b41a912..feab6fa6461dc 100644 --- a/test/integration/apiserver/apply/apply_test.go +++ b/test/integration/apiserver/apply/apply_test.go @@ -1288,8 +1288,9 @@ func TestSMPValidation(t *testing.T) { Resource("deployments"). Name("test-deployment"). Body([]byte(`{"metadata":{"labels":{"label1": "val1"}},"spec":{"foo":"bar"}}`)).Do(context.TODO()).Get() - if err != nil { - t.Fatalf("Failed to patch object: %v", err) + if !strings.Contains(err.Error(), "unknown fields when converting from unstructured") { + // TODO: use errors.Is or As instead if we can + t.Fatalf("unexpected err: %v", err) } klog.Warningf("final obj: %v\n", obj) From 42ccfc98950ca51fd90de879358d9a1f9592551e Mon Sep 17 00:00:00 2001 From: Kevin Delgado Date: Wed, 22 Sep 2021 19:29:17 +0000 Subject: [PATCH 25/64] WIP cleanup, move test out of apply --- .../pkg/apiserver/customresource_handler.go | 10 -- .../pkg/util/strategicpatch/patch.go | 20 +-- test/integration/apiserver/apiserver_test.go | 58 ++++++++ .../integration/apiserver/apply/apply_test.go | 140 ------------------ 4 files changed, 59 insertions(+), 169 deletions(-) diff --git a/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/customresource_handler.go b/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/customresource_handler.go index c3a365327d81b..e12f506ff5012 100644 --- a/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/customresource_handler.go +++ b/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/customresource_handler.go @@ -153,9 +153,6 @@ type crdInfo struct { // Storage per version storages map[string]customresource.CustomResourceStorage - // TODO: requestScopes is where we use the different serializer - // One option is to have two requestScopes (one strict and one non-strict) - // and pass it to the PatchResource call based on the query param // Request scope per version requestScopes map[string]*handlers.RequestScope @@ -235,7 +232,6 @@ var longRunningFilter = genericfilters.BasicLongRunningRequestCheck(sets.NewStri var possiblyAcrossAllNamespacesVerbs = sets.NewString("list", "watch") func (r *crdHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) { - klog.Warningf("ServeHTTP called on crdHandler") ctx := req.Context() requestInfo, ok := apirequest.RequestInfoFrom(ctx) if !ok { @@ -388,8 +384,6 @@ func (r *crdHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) { } func (r *crdHandler) serveResource(w http.ResponseWriter, req *http.Request, requestInfo *apirequest.RequestInfo, crdInfo *crdInfo, crd *apiextensionsv1.CustomResourceDefinition, terminating bool, supportedTypes []string) http.HandlerFunc { - klog.Warningf("serveResource called") - // TODO should we multiplext requestScope based on the query param? requestScope := crdInfo.requestScopes[requestInfo.APIVersion] storage := crdInfo.storages[requestInfo.APIVersion].CustomResource @@ -420,7 +414,6 @@ func (r *crdHandler) serveResource(w http.ResponseWriter, req *http.Request, req case "update": return handlers.UpdateResource(storage, requestScope, r.admission) case "patch": - klog.Warningf("returning handlers.PatchResource") return handlers.PatchResource(storage, requestScope, r.admission, supportedTypes) case "delete": allowsOptions := true @@ -1343,8 +1336,6 @@ type unstructuredSchemaCoercer struct { } func (v *unstructuredSchemaCoercer) apply(u *unstructured.Unstructured) error { - klog.Warningf("coercer apply") - klog.Warningf("uFD: %v", v.unknownFieldsDirective) // save implicit meta fields that don't have to be specified in the validation spec kind, foundKind, err := unstructured.NestedString(u.UnstructuredContent(), "kind") if err != nil { @@ -1372,7 +1363,6 @@ func (v *unstructuredSchemaCoercer) apply(u *unstructured.Unstructured) error { return fmt.Errorf("failed with unknown fields: %v", pruned) } structuraldefaulting.PruneNonNullableNullsWithoutDefaults(u.Object, v.structuralSchemas[gv.Version]) - klog.Warningf("u Object post prune, %v", u.Object) } if err := schemaobjectmeta.Coerce(nil, u.Object, v.structuralSchemas[gv.Version], false, v.dropInvalidMetadata); err != nil { return err diff --git a/staging/src/k8s.io/apimachinery/pkg/util/strategicpatch/patch.go b/staging/src/k8s.io/apimachinery/pkg/util/strategicpatch/patch.go index 38af999e180d5..fd2081a28d524 100644 --- a/staging/src/k8s.io/apimachinery/pkg/util/strategicpatch/patch.go +++ b/staging/src/k8s.io/apimachinery/pkg/util/strategicpatch/patch.go @@ -25,7 +25,6 @@ import ( "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/util/json" "k8s.io/apimachinery/pkg/util/mergepatch" - "k8s.io/klog/v2" ) // An alternate implementation of JSON Merge Patch @@ -855,7 +854,6 @@ func handleUnmarshal(j []byte) (map[string]interface{}, error) { // calling CreateTwoWayMergeMapPatch. // Warning: the original and patch JSONMap objects are mutated by this function and should not be reused. func StrategicMergeMapPatch(original, patch JSONMap, dataStruct interface{}) (JSONMap, error) { - klog.Warningf("SMMP") schema, err := NewPatchMetaFromStruct(dataStruct) if err != nil { return nil, err @@ -875,14 +873,11 @@ func StrategicMergeMapPatch(original, patch JSONMap, dataStruct interface{}) (JS } func StrategicMergeMapPatchUsingLookupPatchMeta(original, patch JSONMap, schema LookupPatchMeta) (JSONMap, error) { - klog.Warningf("SMMPULPM") mergeOptions := MergeOptions{ MergeParallelList: true, IgnoreUnmatchedNulls: true, } - JSONMap, err := mergeMap(original, patch, schema, mergeOptions) - klog.Warningf("JSONMap: %v\n", JSONMap) - return JSONMap, err + return mergeMap(original, patch, schema, mergeOptions) } // MergeStrategicMergeMapPatchUsingLookupPatchMeta merges strategic merge @@ -1283,15 +1278,6 @@ func partitionMapsByPresentInList(original, partitionBy []interface{}, mergeKey // present in original, then to propagate it to the end result use // mergeOptions.IgnoreUnmatchedNulls == false. func mergeMap(original, patch map[string]interface{}, schema LookupPatchMeta, mergeOptions MergeOptions) (map[string]interface{}, error) { - klog.Warningf("================ mergeMap baby!") - klog.Warningf("================ original:\n") - for k, v := range original { - klog.Warningf("k: %s\n v:%v\n", k, v) - } - klog.Warningf("================ patch:\n") - for k, v := range patch { - klog.Warningf("k: %s\n v:%v\n", k, v) - } if v, ok := patch[directiveMarker]; ok { return handleDirectiveInMergeMap(v, patch) } @@ -1382,10 +1368,6 @@ func mergeMap(original, patch map[string]interface{}, schema LookupPatchMeta, me return nil, err } } - klog.Warningf("================ final original:\n") - for k, v := range original { - klog.Warningf("k: %s\n v:%v\n", k, v) - } return original, nil } diff --git a/test/integration/apiserver/apiserver_test.go b/test/integration/apiserver/apiserver_test.go index 91d5dba315a79..2ef3c2e578919 100644 --- a/test/integration/apiserver/apiserver_test.go +++ b/test/integration/apiserver/apiserver_test.go @@ -2999,6 +2999,64 @@ func BenchmarkFieldValidation(b *testing.B) { } } +func TestSMPFieldValidation(t *testing.T) { + defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.ServerSideApply, true)() + + _, client, closeFn := setup(t) + defer closeFn() + + _, err := client.CoreV1().RESTClient().Patch(types.ApplyPatchType). + AbsPath("/apis/apps/v1"). + Namespace("default"). + Resource("deployments"). + Name("test-deployment"). + Param("fieldManager", "apply_test"). + Body([]byte(`{ + "apiVersion": "apps/v1", + "kind": "Deployment", + "metadata": { + "name": "test-deployment", + "labels": {"app": "nginx"} + }, + "spec": { + "selector": { + "matchLabels": { + "app": "nginx" + } + }, + "template": { + "metadata": { + "labels": { + "app": "nginx" + } + }, + "spec": { + "containers": [{ + "name": "nginx", + "image": "nginx:latest" + }] + } + } + } + }`)). + Do(context.TODO()). + Get() + if err != nil { + t.Fatalf("Failed to create object using Apply patch: %v", err) + } + + _, err = client.CoreV1().RESTClient().Patch(types.StrategicMergePatchType). + AbsPath("/apis/apps/v1"). + Namespace("default"). + Resource("deployments"). + Name("test-deployment"). + Body([]byte(`{"metadata":{"labels":{"label1": "val1"}},"spec":{"foo":"bar"}}`)).Do(context.TODO()).Get() + if !strings.Contains(err.Error(), "unknown fields when converting from unstructured") { + // TODO: use errors.Is or As instead if we can + t.Fatalf("unexpected err: %v", err) + } +} + type dependentClient struct { t *testing.T client dynamic.ResourceInterface diff --git a/test/integration/apiserver/apply/apply_test.go b/test/integration/apiserver/apply/apply_test.go index feab6fa6461dc..aec9e9ea1386a 100644 --- a/test/integration/apiserver/apply/apply_test.go +++ b/test/integration/apiserver/apply/apply_test.go @@ -45,7 +45,6 @@ import ( clientset "k8s.io/client-go/kubernetes" restclient "k8s.io/client-go/rest" featuregatetesting "k8s.io/component-base/featuregate/testing" - "k8s.io/klog/v2" "k8s.io/kubernetes/pkg/controlplane" "k8s.io/kubernetes/test/integration/framework" ) @@ -1236,145 +1235,6 @@ func TestClearManagedFieldsWithMergePatch(t *testing.T) { } } -func TestSMPValidation(t *testing.T) { - defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, genericfeatures.ServerSideApply, true)() - - _, client, closeFn := setup(t) - defer closeFn() - - _, err := client.CoreV1().RESTClient().Patch(types.ApplyPatchType). - AbsPath("/apis/apps/v1"). - Namespace("default"). - Resource("deployments"). - Name("test-deployment"). - Param("fieldManager", "apply_test"). - Body([]byte(`{ - "apiVersion": "apps/v1", - "kind": "Deployment", - "metadata": { - "name": "test-deployment", - "labels": {"app": "nginx"} - }, - "spec": { - "selector": { - "matchLabels": { - "app": "nginx" - } - }, - "template": { - "metadata": { - "labels": { - "app": "nginx" - } - }, - "spec": { - "containers": [{ - "name": "nginx", - "image": "nginx:latest" - }] - } - } - } - }`)). - Do(context.TODO()). - Get() - if err != nil { - t.Fatalf("Failed to create object using Apply patch: %v", err) - } - - obj, err := client.CoreV1().RESTClient().Patch(types.StrategicMergePatchType). - AbsPath("/apis/apps/v1"). - Namespace("default"). - Resource("deployments"). - Name("test-deployment"). - Body([]byte(`{"metadata":{"labels":{"label1": "val1"}},"spec":{"foo":"bar"}}`)).Do(context.TODO()).Get() - if !strings.Contains(err.Error(), "unknown fields when converting from unstructured") { - // TODO: use errors.Is or As instead if we can - t.Fatalf("unexpected err: %v", err) - } - - klog.Warningf("final obj: %v\n", obj) - - //object, err := client.CoreV1().RESTClient().Get().Namespace("default").Resource("configmaps").Name("test-cm").Do(context.TODO()).Get() - //if err != nil { - // t.Fatalf("Failed to retrieve object: %v", err) - //} - - //accessor, err := meta.Accessor(object) - //if err != nil { - // t.Fatalf("Failed to get meta accessor: %v", err) - //} - - //if managedFields := accessor.GetManagedFields(); len(managedFields) != 0 { - // t.Fatalf("Failed to clear managedFields, got: %v", managedFields) - //} - - //if labels := accessor.GetLabels(); len(labels) < 1 { - // t.Fatalf("Expected other fields to stay untouched, got: %v", object) - //} -} -func TestCMSMPValidationCM(t *testing.T) { - defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, genericfeatures.ServerSideApply, true)() - - _, client, closeFn := setup(t) - defer closeFn() - - _, err := client.CoreV1().RESTClient().Patch(types.ApplyPatchType). - Namespace("default"). - Resource("configmaps"). - Name("test-cm"). - Param("fieldManager", "apply_test"). - Body([]byte(`{ - "apiVersion": "v1", - "kind": "ConfigMap", - "metadata": { - "name": "test-cm", - "namespace": "default", - "labels": { - "test-label": "test" - } - }, - "data": { - "key": "value" - } - }`)). - Do(context.TODO()). - Get() - if err != nil { - t.Fatalf("Failed to create object using Apply patch: %v", err) - } - - obj, err := client.CoreV1().RESTClient().Patch(types.StrategicMergePatchType). - Namespace("default"). - Resource("configmaps"). - Name("test-cm"). - //Body([]byte(`{"metadata":{"managedFields": [{}]}}`)).Do(context.TODO()).Get() - Body([]byte(`{"metadata":{"foo":"bar"},"data":{"key2":"value2"}}`)).Do(context.TODO()).Get() - if err != nil { - t.Fatalf("Failed to patch object: %v", err) - } - - klog.Warningf("final obj: %v\n", obj) - - //object, err := client.CoreV1().RESTClient().Get().Namespace("default").Resource("configmaps").Name("test-cm").Do(context.TODO()).Get() - //if err != nil { - // t.Fatalf("Failed to retrieve object: %v", err) - //} - - //accessor, err := meta.Accessor(object) - //if err != nil { - // t.Fatalf("Failed to get meta accessor: %v", err) - //} - - //if managedFields := accessor.GetManagedFields(); len(managedFields) != 0 { - // t.Fatalf("Failed to clear managedFields, got: %v", managedFields) - //} - - //if labels := accessor.GetLabels(); len(labels) < 1 { - // t.Fatalf("Expected other fields to stay untouched, got: %v", object) - //} -} - // TestClearManagedFieldsWithStrategicMergePatch verifies it's possible to clear the managedFields func TestClearManagedFieldsWithStrategicMergePatch(t *testing.T) { defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, genericfeatures.ServerSideApply, true)() From 3a5136e3a99d20826b943c970253b9eba5c27302 Mon Sep 17 00:00:00 2001 From: Kevin Delgado Date: Thu, 23 Sep 2021 00:52:14 +0000 Subject: [PATCH 26/64] fix SMP test --- .../apimachinery/pkg/runtime/converter.go | 14 +--- .../apiserver/pkg/endpoints/handlers/patch.go | 73 +++++++++---------- .../apiserver/pkg/endpoints/installer.go | 19 +++-- test/integration/apiserver/apiserver_test.go | 42 ++++++++--- 4 files changed, 81 insertions(+), 67 deletions(-) diff --git a/staging/src/k8s.io/apimachinery/pkg/runtime/converter.go b/staging/src/k8s.io/apimachinery/pkg/runtime/converter.go index 6dc680e41e756..426de92ad663f 100644 --- a/staging/src/k8s.io/apimachinery/pkg/runtime/converter.go +++ b/staging/src/k8s.io/apimachinery/pkg/runtime/converter.go @@ -81,16 +81,6 @@ var ( }, ), } - - StrictUnstructuredConverter = &unstructuredConverter{ - mismatchDetection: parseBool(os.Getenv("KUBE_PATCH_CONVERSION_DETECTOR")), - comparison: conversion.EqualitiesOrDie( - func(a, b time.Time) bool { - return a.UTC() == b.UTC() - }, - ), - strictFieldValidation: true, - } ) func parseBool(key string) bool { @@ -126,6 +116,10 @@ func NewTestUnstructuredConverter(comparison conversion.Equalities) Unstructured } } +func (c *unstructuredConverter) SetStrictFieldValidation(strict bool) { + c.strictFieldValidation = strict +} + // FromUnstructured converts an object from map[string]interface{} representation into a concrete type. // It uses encoding/json/Unmarshaler if object implements it or reflection if not. func (c *unstructuredConverter) FromUnstructured(u map[string]interface{}, obj interface{}) error { diff --git a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/patch.go b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/patch.go index 5d40da0314cb5..ff4e802fe0dc6 100644 --- a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/patch.go +++ b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/patch.go @@ -62,6 +62,7 @@ const ( // PatchResource returns a function that will handle a resource patch. func PatchResource(r rest.Patcher, scope *RequestScope, admit admission.Interface, patchTypes []string) http.HandlerFunc { return func(w http.ResponseWriter, req *http.Request) { + // For performance tracking purposes. trace := utiltrace.New("Patch", traceFields(req)...) defer trace.LogIfLong(500 * time.Millisecond) @@ -89,7 +90,6 @@ func PatchResource(r rest.Patcher, scope *RequestScope, admit admission.Interfac contentType = contentType[:idx] } patchType := types.PatchType(contentType) - klog.Warningf("patchType: %v", patchType) // Ensure the patchType is one we support if !sets.NewString(patchTypes...).Has(contentType) { @@ -151,6 +151,7 @@ func PatchResource(r rest.Patcher, scope *RequestScope, admit admission.Interfac return } gv := scope.Kind.GroupVersion() + strictValidation := false scopeSerializer := scope.Serializer validationDirective, err := fieldValidation(req) @@ -160,6 +161,10 @@ func PatchResource(r rest.Patcher, scope *RequestScope, admit admission.Interfac } if validationDirective == strictFieldValidation { scopeSerializer = scope.StrictSerializer + strictValidation = true + klog.Warningf("scpeSerializer %+v", scopeSerializer) + klog.Warningf("scope dot Serializer %+v", scope.Serializer) + klog.Warningf("sSerializer %+v", s.Serializer) } else { klog.Warningf("nonstrcit serializer") } @@ -214,15 +219,16 @@ func PatchResource(r rest.Patcher, scope *RequestScope, admit admission.Interfac } p := patcher{ - namer: scope.Namer, - creater: scope.Creater, - defaulter: scope.Defaulter, - typer: scope.Typer, - unsafeConvertor: scope.UnsafeConvertor, - kind: scope.Kind, - resource: scope.Resource, - subresource: scope.Subresource, - dryRun: dryrun.IsDryRun(options.DryRun), + namer: scope.Namer, + creater: scope.Creater, + defaulter: scope.Defaulter, + typer: scope.Typer, + unsafeConvertor: scope.UnsafeConvertor, + kind: scope.Kind, + resource: scope.Resource, + subresource: scope.Subresource, + dryRun: dryrun.IsDryRun(options.DryRun), + strictFieldValidation: strictValidation, objectInterfaces: scope, @@ -276,15 +282,16 @@ type mutateObjectUpdateFunc func(ctx context.Context, obj, old runtime.Object) e // moved into this type. type patcher struct { // Pieces of RequestScope - namer ScopeNamer - creater runtime.ObjectCreater - defaulter runtime.ObjectDefaulter - typer runtime.ObjectTyper - unsafeConvertor runtime.ObjectConvertor - resource schema.GroupVersionResource - kind schema.GroupVersionKind - subresource string - dryRun bool + namer ScopeNamer + creater runtime.ObjectCreater + defaulter runtime.ObjectDefaulter + typer runtime.ObjectTyper + unsafeConvertor runtime.ObjectConvertor + resource schema.GroupVersionResource + kind schema.GroupVersionKind + subresource string + dryRun bool + strictFieldValidation bool objectInterfaces admission.ObjectInterfaces @@ -296,9 +303,6 @@ type patcher struct { admissionCheck admission.MutationInterface codec runtime.Codec - // don't think this is necessary because we construct the patcher on each request - // so we can multiplex the codec between/strict non-strict - //strictCodec runtime.Codec options *metav1.PatchOptions @@ -414,7 +418,6 @@ type smpPatcher struct { } func (p *smpPatcher) applyPatchToCurrentObject(currentObject runtime.Object) (runtime.Object, error) { - klog.Warningf("smpPatch aPTCO") // Since the patch is applied on versioned objects, we need to convert the // current object to versioned representation first. currentVersionedObject, err := p.unsafeConvertor.ConvertToVersion(currentObject, p.kind.GroupVersion()) @@ -425,7 +428,7 @@ func (p *smpPatcher) applyPatchToCurrentObject(currentObject runtime.Object) (ru if err != nil { return nil, err } - if err := strategicPatchObject(p.defaulter, currentVersionedObject, p.patchBytes, versionedObjToUpdate, p.schemaReferenceObj); err != nil { + if err := strategicPatchObject(p.defaulter, currentVersionedObject, p.patchBytes, versionedObjToUpdate, p.schemaReferenceObj, p.strictFieldValidation); err != nil { return nil, err } // Convert the object back to the hub version @@ -489,8 +492,8 @@ func strategicPatchObject( patchBytes []byte, objToUpdate runtime.Object, schemaReferenceObj runtime.Object, + strictFieldValidation bool, ) error { - klog.Warningf("sPO") originalObjMap, err := runtime.DefaultUnstructuredConverter.ToUnstructured(originalObject) if err != nil { return err @@ -501,7 +504,7 @@ func strategicPatchObject( return errors.NewBadRequest(err.Error()) } - if err := applyPatchToObject(defaulter, originalObjMap, patchMap, objToUpdate, schemaReferenceObj); err != nil { + if err := applyPatchToObject(defaulter, originalObjMap, patchMap, objToUpdate, schemaReferenceObj, strictFieldValidation); err != nil { return err } return nil @@ -653,33 +656,23 @@ func applyPatchToObject( patchMap map[string]interface{}, objToUpdate runtime.Object, schemaReferenceObj runtime.Object, + strictFieldValidation bool, ) error { - klog.Warningf("aPTO") - // Note: objToUpdate always starts out here as an empty deployment - // even if it exists (it has to exist for this to be called because it's a PATCH, duh) - klog.Warningf("objToUpdate beginning: %v\n", objToUpdate) patchedObjMap, err := strategicpatch.StrategicMergeMapPatch(originalMap, patchMap, schemaReferenceObj) if err != nil { return interpretStrategicMergePatchError(err) } - // foo still exists here - klog.Warningf("patchedObjMap after SMMP: %v\n", patchedObjMap) // Rather than serialize the patched map to JSON, then decode it to an object, we go directly from a map to an object - // Note: this is what removes the invalid foo field - //if err := runtime.DefaultUnstructuredConverter.FromUnstructured(patchedObjMap, objToUpdate); err != nil { - if err := runtime.StrictUnstructuredConverter.FromUnstructured(patchedObjMap, objToUpdate); err != nil { + converter := runtime.DefaultUnstructuredConverter + converter.SetStrictFieldValidation(strictFieldValidation) + if err := converter.FromUnstructured(patchedObjMap, objToUpdate); err != nil { return errors.NewInvalid(schema.GroupKind{}, "", field.ErrorList{ field.Invalid(field.NewPath("patch"), fmt.Sprintf("%+v", patchMap), err.Error()), }) } - // foo still exists here - klog.Warningf("patchedObjMap after FU: %v\n", patchedObjMap) - // foo GONE HERE - klog.Warningf("objToUpdate after FU: %v\n", objToUpdate) // Decoding from JSON to a versioned object would apply defaults, so we do the same here defaulter.Default(objToUpdate) - klog.Warningf("objToUpdate after Default: %v\n", objToUpdate) return nil } diff --git a/staging/src/k8s.io/apiserver/pkg/endpoints/installer.go b/staging/src/k8s.io/apiserver/pkg/endpoints/installer.go index 0902add4823c3..65189960a2c02 100644 --- a/staging/src/k8s.io/apiserver/pkg/endpoints/installer.go +++ b/staging/src/k8s.io/apiserver/pkg/endpoints/installer.go @@ -46,6 +46,7 @@ import ( "k8s.io/apiserver/pkg/storageversion" utilfeature "k8s.io/apiserver/pkg/util/feature" versioninfo "k8s.io/component-base/version" + "k8s.io/klog/v2" "sigs.k8s.io/structured-merge-diff/v4/fieldpath" ) @@ -569,14 +570,15 @@ func (a *APIInstaller) registerResourceHandlers(path string, storage rest.Storag kubeVerbs := map[string]struct{}{} reqScope := handlers.RequestScope{ - Serializer: a.group.Serializer, - ParameterCodec: a.group.ParameterCodec, - Creater: a.group.Creater, - Convertor: a.group.Convertor, - Defaulter: a.group.Defaulter, - Typer: a.group.Typer, - UnsafeConvertor: a.group.UnsafeConvertor, - Authorizer: a.group.Authorizer, + Serializer: a.group.Serializer, + StrictSerializer: a.group.Serializer, + ParameterCodec: a.group.ParameterCodec, + Creater: a.group.Creater, + Convertor: a.group.Convertor, + Defaulter: a.group.Defaulter, + Typer: a.group.Typer, + UnsafeConvertor: a.group.UnsafeConvertor, + Authorizer: a.group.Authorizer, EquivalentResourceMapper: a.group.EquivalentResourceRegistry, @@ -1206,6 +1208,7 @@ func restfulUpdateResource(r rest.Updater, scope handlers.RequestScope, admit ad } func restfulPatchResource(r rest.Patcher, scope handlers.RequestScope, admit admission.Interface, supportedTypes []string) restful.RouteFunction { + klog.Warningf("rPR scope: %+v", scope) return func(req *restful.Request, res *restful.Response) { handlers.PatchResource(r, &scope, admit, supportedTypes)(res.ResponseWriter, req.Request) } diff --git a/test/integration/apiserver/apiserver_test.go b/test/integration/apiserver/apiserver_test.go index 2ef3c2e578919..2453d892b3e05 100644 --- a/test/integration/apiserver/apiserver_test.go +++ b/test/integration/apiserver/apiserver_test.go @@ -3045,15 +3045,39 @@ func TestSMPFieldValidation(t *testing.T) { t.Fatalf("Failed to create object using Apply patch: %v", err) } - _, err = client.CoreV1().RESTClient().Patch(types.StrategicMergePatchType). - AbsPath("/apis/apps/v1"). - Namespace("default"). - Resource("deployments"). - Name("test-deployment"). - Body([]byte(`{"metadata":{"labels":{"label1": "val1"}},"spec":{"foo":"bar"}}`)).Do(context.TODO()).Get() - if !strings.Contains(err.Error(), "unknown fields when converting from unstructured") { - // TODO: use errors.Is or As instead if we can - t.Fatalf("unexpected err: %v", err) + var testcases = []struct { + name string + params map[string]string + errContains string + }{ + { + name: "strategicMergePatchStrictValidation", + params: map[string]string{"validate": "strict"}, + errContains: "unknown fields when converting from unstructured", + }, + { + name: "strategicMergePatchIgnoreValidation", + params: map[string]string{}, + errContains: "", + }, + } + + for _, tc := range testcases { + req := client.CoreV1().RESTClient().Patch(types.StrategicMergePatchType). + AbsPath("/apis/apps/v1"). + Namespace("default"). + Resource("deployments"). + Name("test-deployment") + for k, v := range tc.params { + req.Param(k, v) + } + result, err := req.Body([]byte(`{"metadata":{"labels":{"label1": "val1"}},"spec":{"foo":"bar"}}`)).DoRaw(context.TODO()) + if err == nil && tc.errContains != "" { + t.Fatalf("unexpected patch succeeded") + } + if err != nil && !strings.Contains(string(result), tc.errContains) { + t.Fatalf("unexpected response: %v", string(result)) + } } } From a09a237b61c06ecc99ca2cc16801ac5af80ab8b8 Mon Sep 17 00:00:00 2001 From: Kevin Delgado Date: Thu, 23 Sep 2021 01:32:19 +0000 Subject: [PATCH 27/64] cleanup converter debug printing --- .../apimachinery/pkg/runtime/converter.go | 99 ++++--------------- .../apiserver/pkg/endpoints/installer.go | 2 - 2 files changed, 21 insertions(+), 80 deletions(-) diff --git a/staging/src/k8s.io/apimachinery/pkg/runtime/converter.go b/staging/src/k8s.io/apimachinery/pkg/runtime/converter.go index 426de92ad663f..dddfdceeca2c1 100644 --- a/staging/src/k8s.io/apimachinery/pkg/runtime/converter.go +++ b/staging/src/k8s.io/apimachinery/pkg/runtime/converter.go @@ -123,22 +123,15 @@ func (c *unstructuredConverter) SetStrictFieldValidation(strict bool) { // FromUnstructured converts an object from map[string]interface{} representation into a concrete type. // It uses encoding/json/Unmarshaler if object implements it or reflection if not. func (c *unstructuredConverter) FromUnstructured(u map[string]interface{}, obj interface{}) error { - klog.Warningf("FromUnstructured\n") - klog.Warningf("u Map: %+v\n", u) - klog.Warningf("obj: %+v\n", obj) - //c.mismatchDetection = true - klog.Warningf("mismatchDetection?: %t", c.mismatchDetection) t := reflect.TypeOf(obj) value := reflect.ValueOf(obj) if t.Kind() != reflect.Ptr || value.IsNil() { return fmt.Errorf("FromUnstructured requires a non-nil pointer to an object, got %v", t) } - err := c.fromUnstructured(reflect.ValueOf(u), value.Elem(), 0) + err := c.fromUnstructured(reflect.ValueOf(u), value.Elem()) if c.mismatchDetection { - klog.Warningf("detecting mismatches") newObj := reflect.New(t.Elem()).Interface() newErr := fromUnstructuredViaJSON(u, newObj) - klog.Warningf("newErr: %v", newErr) if (err != nil) != (newErr != nil) { klog.Fatalf("FromUnstructured unexpected error for %v: error: %v", u, err) } @@ -157,21 +150,14 @@ func fromUnstructuredViaJSON(u map[string]interface{}, obj interface{}) error { return json.Unmarshal(data, obj) } -func (c *unstructuredConverter) fromUnstructured(sv, dv reflect.Value, level int) error { - klog.Warningf("fromUnstructured level %d\n", level) - klog.Warningf("sv type: %v\n", sv.Type()) - klog.Warningf("sv: %v\n", sv) - klog.Warningf("dv type: %v\n", dv.Type()) - klog.Warningf("dv: %+v\n", dv) +func (c *unstructuredConverter) fromUnstructured(sv, dv reflect.Value) error { sv = unwrapInterface(sv) if !sv.IsValid() { dv.Set(reflect.Zero(dv.Type())) - klog.Warningf("sv not valid returning nil") return nil } st, dt := sv.Type(), dv.Type() - klog.Warningf("dt kind: %v\n", dt.Kind()) switch dt.Kind() { case reflect.Map, reflect.Slice, reflect.Ptr, reflect.Struct, reflect.Interface: // Those require non-trivial conversion. @@ -232,16 +218,14 @@ func (c *unstructuredConverter) fromUnstructured(sv, dv reflect.Value, level int switch dt.Kind() { case reflect.Map: - err := c.mapFromUnstructured(sv, dv, level) - klog.Warningf("map final dv: %v\n", dv) + err := c.mapFromUnstructured(sv, dv) return err case reflect.Slice: - return c.sliceFromUnstructured(sv, dv, level) + return c.sliceFromUnstructured(sv, dv) case reflect.Ptr: - return c.pointerFromUnstructured(sv, dv, level) + return c.pointerFromUnstructured(sv, dv) case reflect.Struct: - err := c.structFromUnstructured(sv, dv, level) - klog.Warningf("struct final dv: %v\n", dv) + err := c.structFromUnstructured(sv, dv) return err case reflect.Interface: return interfaceFromUnstructured(sv, dv) @@ -298,7 +282,7 @@ func unwrapInterface(v reflect.Value) reflect.Value { return v } -func (c *unstructuredConverter) mapFromUnstructured(sv, dv reflect.Value, level int) error { +func (c *unstructuredConverter) mapFromUnstructured(sv, dv reflect.Value) error { st, dt := sv.Type(), dv.Type() if st.Kind() != reflect.Map { return fmt.Errorf("cannot restore map from %v", st.Kind()) @@ -310,17 +294,13 @@ func (c *unstructuredConverter) mapFromUnstructured(sv, dv reflect.Value, level if sv.IsNil() { dv.Set(reflect.Zero(dt)) - klog.Warningf("mFU sv is nil, returning nil") return nil } dv.Set(reflect.MakeMap(dt)) for _, key := range sv.MapKeys() { value := reflect.New(dt.Elem()).Elem() - klog.Warningf("mFU") - klog.Warningf("key: %v", key) - klog.Warningf("value: %v\n", value) if val := unwrapInterface(sv.MapIndex(key)); val.IsValid() { - if err := c.fromUnstructured(val, value, level+1); err != nil { + if err := c.fromUnstructured(val, value); err != nil { return err } } else { @@ -332,11 +312,10 @@ func (c *unstructuredConverter) mapFromUnstructured(sv, dv reflect.Value, level dv.SetMapIndex(key.Convert(dt.Key()), value) } } - klog.Warningf("mFU done successfully") return nil } -func (c *unstructuredConverter) sliceFromUnstructured(sv, dv reflect.Value, level int) error { +func (c *unstructuredConverter) sliceFromUnstructured(sv, dv reflect.Value) error { st, dt := sv.Type(), dv.Type() if st.Kind() == reflect.String && dt.Elem().Kind() == reflect.Uint8 { // We store original []byte representation as string. @@ -369,14 +348,14 @@ func (c *unstructuredConverter) sliceFromUnstructured(sv, dv reflect.Value, leve } dv.Set(reflect.MakeSlice(dt, sv.Len(), sv.Cap())) for i := 0; i < sv.Len(); i++ { - if err := c.fromUnstructured(sv.Index(i), dv.Index(i), level+1); err != nil { + if err := c.fromUnstructured(sv.Index(i), dv.Index(i)); err != nil { return err } } return nil } -func (c *unstructuredConverter) pointerFromUnstructured(sv, dv reflect.Value, level int) error { +func (c *unstructuredConverter) pointerFromUnstructured(sv, dv reflect.Value) error { st, dt := sv.Type(), dv.Type() if st.Kind() == reflect.Ptr && sv.IsNil() { @@ -386,59 +365,36 @@ func (c *unstructuredConverter) pointerFromUnstructured(sv, dv reflect.Value, le dv.Set(reflect.New(dt.Elem())) switch st.Kind() { case reflect.Ptr, reflect.Interface: - return c.fromUnstructured(sv.Elem(), dv.Elem(), level+1) + return c.fromUnstructured(sv.Elem(), dv.Elem()) default: - return c.fromUnstructured(sv, dv.Elem(), level+1) + return c.fromUnstructured(sv, dv.Elem()) } } -func (c *unstructuredConverter) structFromUnstructured(sv, dv reflect.Value, level int) error { +func (c *unstructuredConverter) structFromUnstructured(sv, dv reflect.Value) error { st, dt := sv.Type(), dv.Type() if st.Kind() != reflect.Map { return fmt.Errorf("cannot restore struct from: %v", st.Kind()) } - // DEBUG printing + //// DEBUG printing svLength := len(sv.MapKeys()) keys := sv.MapKeys() timeCode := time.Now().UnixNano() klog.Warningf("START sv length: %d, tc: %d, value: %v\n", svLength, timeCode, sv) - klog.Warningf("dv: %+v", dv) - klog.Warningf("dt.NumField(): %d\n", dt.NumField()) - klog.Warningf("tc: %d\n", timeCode) + klog.Warningf("dt.NumField(): %d value: %+v\n", dt.NumField(), dv) for i, k := range keys { klog.Warningf("sv key: %+v at i: %d", k, i) } - - inlined := false - - // flatten fields check - // 1. create map of fields (set) - // 2. non-inlined fields: add to map - // 3. inlined fields: take to lower level add to map, recurse - //for i := 0; i < dt.NumField(); i++ { - // fieldInfo := fieldInfoFromField(dt, i) - // fv := dv.Field(i) - // klog.Warningf("dt field i: %d fieldInfo: %+v\n", i, fieldInfo) - // klog.Warningf("tc: %d\n", timeCode) - // if len(fieldInfo.name) == 0 { - // destFieldsSet := getFieldsSet(fv) - // } - - //} + //// for i := 0; i < dt.NumField(); i++ { - // - // flatten field check - // fieldInfo := fieldInfoFromField(dt, i) fv := dv.Field(i) - klog.Warningf("dt field i: %d fieldInfo: %+v\n", i, fieldInfo) - klog.Warningf("tc: %d\n", timeCode) + klog.Warningf("dt field i: %d fieldInfo: %+v, tc: %d\n", i, fieldInfo, timeCode) inlinedValues := map[string]interface{}{} if len(fieldInfo.name) == 0 { // This field is inlined - inlined = true // get the name of the field and delete it from the source's keys for i := 0; i < fv.Type().NumField(); i++ { curField := fv.Type().FieldByIndex([]int{i}) @@ -446,7 +402,6 @@ func (c *unstructuredConverter) structFromUnstructured(sv, dv reflect.Value, lev jsonName := strings.Split(jsonTag, ",")[0] klog.Warningf("deleting jsonName %s from keys at tc: %d", jsonName, timeCode) deleteFromKeys(jsonName, &keys) - klog.Warningf("post keylength %d", len(keys)) svMap, ok := (sv.Interface()).(map[string]interface{}) if !ok { klog.Warningf("couldn't cast sv map: %+v", sv) @@ -454,29 +409,21 @@ func (c *unstructuredConverter) structFromUnstructured(sv, dv reflect.Value, lev if val, ok := svMap[jsonName]; ok { inlinedValues[jsonName] = val klog.Warningf("set inlinedValues of jsonName %s to value %+v", jsonName, val) - } else { - klog.Warningf("couldn't hydrate inlinedValues for jsonName %s", jsonName) } } - // this is problem is that we pass the whole sv into fromUnstructured, we should just pass the - // value - //if err := fromUnstructured(sv, fv, level+1); err != nil { - // return err - //} klog.Warningf("recursing into from unstructred with subset of sv: %+v", inlinedValues) - if err := c.fromUnstructured(reflect.ValueOf(inlinedValues), fv, level+1); err != nil { + if err := c.fromUnstructured(reflect.ValueOf(inlinedValues), fv); err != nil { return err } } else { // delete from the source's keys klog.Warningf("deleting fInV %s from keys at tc: %d", fieldInfo.nameValue, timeCode) deleteFromKeys(fieldInfo.name, &keys) - klog.Warningf("post keylength %d", len(keys)) value := unwrapInterface(sv.MapIndex(fieldInfo.nameValue)) if value.IsValid() { - if err := c.fromUnstructured(value, fv, level+1); err != nil { + if err := c.fromUnstructured(value, fv); err != nil { return err } } else { @@ -485,9 +432,7 @@ func (c *unstructuredConverter) structFromUnstructured(sv, dv reflect.Value, lev } } // TODO: error here if len(keys) > 0 meaning there is are unknown fields - klog.Warningf("END keys length: %d inlined: %t, value: %+v\n", len(keys), inlined, keys) - klog.Warningf("end dv length: %d, value: %v\n", dv.Type().NumField(), dv) - klog.Warningf("tc: %d\n", timeCode) + klog.Warningf("END keys length: %d, value: %+v, tc: %d\n", len(keys), keys, timeCode) if len(keys) > 0 && c.strictFieldValidation { return &UnknownFieldError{ invalidFields: keys, @@ -505,14 +450,12 @@ func (fe *UnknownFieldError) Error() string { } func deleteFromKeys(name string, keys *[]reflect.Value) { - klog.Warningf("dFK starting keylength: %d", len(*keys)) for i := len(*keys) - 1; i >= 0; i-- { if name == (*keys)[i].String() { // Delete from keys klog.Warningf("pop keys struct name: %s", (*keys)[i].String()) *keys = append((*keys)[:i], (*keys)[i+1:]...) klog.Warningf("keyslength is now %d after i %d", len(*keys), i) - //klog.Warningf("tc: %d\n", timeCode) return } } diff --git a/staging/src/k8s.io/apiserver/pkg/endpoints/installer.go b/staging/src/k8s.io/apiserver/pkg/endpoints/installer.go index 65189960a2c02..14bc185aceb78 100644 --- a/staging/src/k8s.io/apiserver/pkg/endpoints/installer.go +++ b/staging/src/k8s.io/apiserver/pkg/endpoints/installer.go @@ -46,7 +46,6 @@ import ( "k8s.io/apiserver/pkg/storageversion" utilfeature "k8s.io/apiserver/pkg/util/feature" versioninfo "k8s.io/component-base/version" - "k8s.io/klog/v2" "sigs.k8s.io/structured-merge-diff/v4/fieldpath" ) @@ -1208,7 +1207,6 @@ func restfulUpdateResource(r rest.Updater, scope handlers.RequestScope, admit ad } func restfulPatchResource(r rest.Patcher, scope handlers.RequestScope, admit admission.Interface, supportedTypes []string) restful.RouteFunction { - klog.Warningf("rPR scope: %+v", scope) return func(req *restful.Request, res *restful.Response) { handlers.PatchResource(r, &scope, admit, supportedTypes)(res.ResponseWriter, req.Request) } From e6d2249aa9debc75998ea8a2f095b52bdf994c69 Mon Sep 17 00:00:00 2001 From: Kevin Delgado Date: Tue, 28 Sep 2021 20:05:02 +0000 Subject: [PATCH 28/64] WIP stash --- .../apimachinery/pkg/runtime/converter.go | 90 +++++++++++-------- 1 file changed, 55 insertions(+), 35 deletions(-) diff --git a/staging/src/k8s.io/apimachinery/pkg/runtime/converter.go b/staging/src/k8s.io/apimachinery/pkg/runtime/converter.go index dddfdceeca2c1..e5b5dc4cca159 100644 --- a/staging/src/k8s.io/apimachinery/pkg/runtime/converter.go +++ b/staging/src/k8s.io/apimachinery/pkg/runtime/converter.go @@ -120,6 +120,14 @@ func (c *unstructuredConverter) SetStrictFieldValidation(strict bool) { c.strictFieldValidation = strict } +func makeFields(u map[string]interface{}) map[string]struct{} { + fields := make(map[string]struct{}, len(u)) + for k, _ := range u { + fields[k] = struct{}{} + } + return fields +} + // FromUnstructured converts an object from map[string]interface{} representation into a concrete type. // It uses encoding/json/Unmarshaler if object implements it or reflection if not. func (c *unstructuredConverter) FromUnstructured(u map[string]interface{}, obj interface{}) error { @@ -128,7 +136,8 @@ func (c *unstructuredConverter) FromUnstructured(u map[string]interface{}, obj i if t.Kind() != reflect.Ptr || value.IsNil() { return fmt.Errorf("FromUnstructured requires a non-nil pointer to an object, got %v", t) } - err := c.fromUnstructured(reflect.ValueOf(u), value.Elem()) + fields := makeFields(u) + err := c.fromUnstructured(reflect.ValueOf(u), value.Elem(), fields) if c.mismatchDetection { newObj := reflect.New(t.Elem()).Interface() newErr := fromUnstructuredViaJSON(u, newObj) @@ -150,7 +159,7 @@ func fromUnstructuredViaJSON(u map[string]interface{}, obj interface{}) error { return json.Unmarshal(data, obj) } -func (c *unstructuredConverter) fromUnstructured(sv, dv reflect.Value) error { +func (c *unstructuredConverter) fromUnstructured(sv, dv reflect.Value, fields map[string]struct{}) error { sv = unwrapInterface(sv) if !sv.IsValid() { dv.Set(reflect.Zero(dv.Type())) @@ -218,14 +227,14 @@ func (c *unstructuredConverter) fromUnstructured(sv, dv reflect.Value) error { switch dt.Kind() { case reflect.Map: - err := c.mapFromUnstructured(sv, dv) + err := c.mapFromUnstructured(sv, dv, fields) return err case reflect.Slice: - return c.sliceFromUnstructured(sv, dv) + return c.sliceFromUnstructured(sv, dv, fields) case reflect.Ptr: - return c.pointerFromUnstructured(sv, dv) + return c.pointerFromUnstructured(sv, dv, fields) case reflect.Struct: - err := c.structFromUnstructured(sv, dv) + err := c.structFromUnstructured(sv, dv, fields) return err case reflect.Interface: return interfaceFromUnstructured(sv, dv) @@ -282,7 +291,7 @@ func unwrapInterface(v reflect.Value) reflect.Value { return v } -func (c *unstructuredConverter) mapFromUnstructured(sv, dv reflect.Value) error { +func (c *unstructuredConverter) mapFromUnstructured(sv, dv reflect.Value, fields map[string]struct{}) error { st, dt := sv.Type(), dv.Type() if st.Kind() != reflect.Map { return fmt.Errorf("cannot restore map from %v", st.Kind()) @@ -300,7 +309,7 @@ func (c *unstructuredConverter) mapFromUnstructured(sv, dv reflect.Value) error for _, key := range sv.MapKeys() { value := reflect.New(dt.Elem()).Elem() if val := unwrapInterface(sv.MapIndex(key)); val.IsValid() { - if err := c.fromUnstructured(val, value); err != nil { + if err := c.fromUnstructured(val, value, fields); err != nil { return err } } else { @@ -315,7 +324,7 @@ func (c *unstructuredConverter) mapFromUnstructured(sv, dv reflect.Value) error return nil } -func (c *unstructuredConverter) sliceFromUnstructured(sv, dv reflect.Value) error { +func (c *unstructuredConverter) sliceFromUnstructured(sv, dv reflect.Value, fields map[string]struct{}) error { st, dt := sv.Type(), dv.Type() if st.Kind() == reflect.String && dt.Elem().Kind() == reflect.Uint8 { // We store original []byte representation as string. @@ -348,14 +357,14 @@ func (c *unstructuredConverter) sliceFromUnstructured(sv, dv reflect.Value) erro } dv.Set(reflect.MakeSlice(dt, sv.Len(), sv.Cap())) for i := 0; i < sv.Len(); i++ { - if err := c.fromUnstructured(sv.Index(i), dv.Index(i)); err != nil { + if err := c.fromUnstructured(sv.Index(i), dv.Index(i), fields); err != nil { return err } } return nil } -func (c *unstructuredConverter) pointerFromUnstructured(sv, dv reflect.Value) error { +func (c *unstructuredConverter) pointerFromUnstructured(sv, dv reflect.Value, fields map[string]struct{}) error { st, dt := sv.Type(), dv.Type() if st.Kind() == reflect.Ptr && sv.IsNil() { @@ -365,13 +374,13 @@ func (c *unstructuredConverter) pointerFromUnstructured(sv, dv reflect.Value) er dv.Set(reflect.New(dt.Elem())) switch st.Kind() { case reflect.Ptr, reflect.Interface: - return c.fromUnstructured(sv.Elem(), dv.Elem()) + return c.fromUnstructured(sv.Elem(), dv.Elem(), fields) default: - return c.fromUnstructured(sv, dv.Elem()) + return c.fromUnstructured(sv, dv.Elem(), fields) } } -func (c *unstructuredConverter) structFromUnstructured(sv, dv reflect.Value) error { +func (c *unstructuredConverter) structFromUnstructured(sv, dv reflect.Value, fields map[string]struct{}) error { st, dt := sv.Type(), dv.Type() if st.Kind() != reflect.Map { return fmt.Errorf("cannot restore struct from: %v", st.Kind()) @@ -385,6 +394,9 @@ func (c *unstructuredConverter) structFromUnstructured(sv, dv reflect.Value) err for i, k := range keys { klog.Warningf("sv key: %+v at i: %d", k, i) } + for k, v := range fields { + klog.Warningf("fields key: %+v is %t\n", k, v) + } //// for i := 0; i < dt.NumField(); i++ { @@ -392,38 +404,45 @@ func (c *unstructuredConverter) structFromUnstructured(sv, dv reflect.Value) err fv := dv.Field(i) klog.Warningf("dt field i: %d fieldInfo: %+v, tc: %d\n", i, fieldInfo, timeCode) - inlinedValues := map[string]interface{}{} + //inlinedValues := map[string]interface{}{} if len(fieldInfo.name) == 0 { - // This field is inlined - // get the name of the field and delete it from the source's keys - for i := 0; i < fv.Type().NumField(); i++ { - curField := fv.Type().FieldByIndex([]int{i}) - jsonTag := curField.Tag.Get("json") - jsonName := strings.Split(jsonTag, ",")[0] - klog.Warningf("deleting jsonName %s from keys at tc: %d", jsonName, timeCode) - deleteFromKeys(jsonName, &keys) - svMap, ok := (sv.Interface()).(map[string]interface{}) - if !ok { - klog.Warningf("couldn't cast sv map: %+v", sv) - } - if val, ok := svMap[jsonName]; ok { - inlinedValues[jsonName] = val - klog.Warningf("set inlinedValues of jsonName %s to value %+v", jsonName, val) - } - } - klog.Warningf("recursing into from unstructred with subset of sv: %+v", inlinedValues) - if err := c.fromUnstructured(reflect.ValueOf(inlinedValues), fv); err != nil { + //// This field is inlined + //// get the name of the field and delete it from the source's keys + //for i := 0; i < fv.Type().NumField(); i++ { + // curField := fv.Type().FieldByIndex([]int{i}) + // jsonTag := curField.Tag.Get("json") + // jsonName := strings.Split(jsonTag, ",")[0] + // klog.Warningf("deleting jsonName %s from keys at tc: %d", jsonName, timeCode) + // deleteFromKeys(jsonName, &keys) + // svMap, ok := (sv.Interface()).(map[string]interface{}) + // if !ok { + // klog.Warningf("couldn't cast sv map: %+v", sv) + // } + // if val, ok := svMap[jsonName]; ok { + // inlinedValues[jsonName] = val + // klog.Warningf("set inlinedValues of jsonName %s to value %+v", jsonName, val) + // } + //} + + //klog.Warningf("recursing into from unstructred with subset of sv: %+v", inlinedValues) + //if err := c.fromUnstructured(reflect.ValueOf(inlinedValues), fv); err != nil { + // return err + //} + klog.Warningf("recursing into from unstructred at tc %d, with sv: %+v", timeCode, sv) + if err := c.fromUnstructured(sv, fv, fields); err != nil { return err } } else { // delete from the source's keys klog.Warningf("deleting fInV %s from keys at tc: %d", fieldInfo.nameValue, timeCode) deleteFromKeys(fieldInfo.name, &keys) + klog.Warningf("from fields %+v", fields) + delete(fields, fieldInfo.name) value := unwrapInterface(sv.MapIndex(fieldInfo.nameValue)) if value.IsValid() { - if err := c.fromUnstructured(value, fv); err != nil { + if err := c.fromUnstructured(value, fv, fields); err != nil { return err } } else { @@ -433,6 +452,7 @@ func (c *unstructuredConverter) structFromUnstructured(sv, dv reflect.Value) err } // TODO: error here if len(keys) > 0 meaning there is are unknown fields klog.Warningf("END keys length: %d, value: %+v, tc: %d\n", len(keys), keys, timeCode) + klog.Warningf("fields length %d, value: %+v", len(fields), fields) if len(keys) > 0 && c.strictFieldValidation { return &UnknownFieldError{ invalidFields: keys, From 904316f860d15c2b34d0cd78c322ab30636a87a5 Mon Sep 17 00:00:00 2001 From: Kevin Delgado Date: Thu, 7 Oct 2021 20:40:28 +0000 Subject: [PATCH 29/64] cleanup debug logging --- .../apimachinery/pkg/runtime/converter.go | 44 ------------------- 1 file changed, 44 deletions(-) diff --git a/staging/src/k8s.io/apimachinery/pkg/runtime/converter.go b/staging/src/k8s.io/apimachinery/pkg/runtime/converter.go index e5b5dc4cca159..da22bb379454f 100644 --- a/staging/src/k8s.io/apimachinery/pkg/runtime/converter.go +++ b/staging/src/k8s.io/apimachinery/pkg/runtime/converter.go @@ -385,59 +385,18 @@ func (c *unstructuredConverter) structFromUnstructured(sv, dv reflect.Value, fie if st.Kind() != reflect.Map { return fmt.Errorf("cannot restore struct from: %v", st.Kind()) } - //// DEBUG printing - svLength := len(sv.MapKeys()) keys := sv.MapKeys() - timeCode := time.Now().UnixNano() - klog.Warningf("START sv length: %d, tc: %d, value: %v\n", svLength, timeCode, sv) - klog.Warningf("dt.NumField(): %d value: %+v\n", dt.NumField(), dv) - for i, k := range keys { - klog.Warningf("sv key: %+v at i: %d", k, i) - } - for k, v := range fields { - klog.Warningf("fields key: %+v is %t\n", k, v) - } - //// for i := 0; i < dt.NumField(); i++ { fieldInfo := fieldInfoFromField(dt, i) fv := dv.Field(i) - klog.Warningf("dt field i: %d fieldInfo: %+v, tc: %d\n", i, fieldInfo, timeCode) - - //inlinedValues := map[string]interface{}{} if len(fieldInfo.name) == 0 { - - //// This field is inlined - //// get the name of the field and delete it from the source's keys - //for i := 0; i < fv.Type().NumField(); i++ { - // curField := fv.Type().FieldByIndex([]int{i}) - // jsonTag := curField.Tag.Get("json") - // jsonName := strings.Split(jsonTag, ",")[0] - // klog.Warningf("deleting jsonName %s from keys at tc: %d", jsonName, timeCode) - // deleteFromKeys(jsonName, &keys) - // svMap, ok := (sv.Interface()).(map[string]interface{}) - // if !ok { - // klog.Warningf("couldn't cast sv map: %+v", sv) - // } - // if val, ok := svMap[jsonName]; ok { - // inlinedValues[jsonName] = val - // klog.Warningf("set inlinedValues of jsonName %s to value %+v", jsonName, val) - // } - //} - - //klog.Warningf("recursing into from unstructred with subset of sv: %+v", inlinedValues) - //if err := c.fromUnstructured(reflect.ValueOf(inlinedValues), fv); err != nil { - // return err - //} - klog.Warningf("recursing into from unstructred at tc %d, with sv: %+v", timeCode, sv) if err := c.fromUnstructured(sv, fv, fields); err != nil { return err } } else { // delete from the source's keys - klog.Warningf("deleting fInV %s from keys at tc: %d", fieldInfo.nameValue, timeCode) deleteFromKeys(fieldInfo.name, &keys) - klog.Warningf("from fields %+v", fields) delete(fields, fieldInfo.name) value := unwrapInterface(sv.MapIndex(fieldInfo.nameValue)) @@ -450,9 +409,6 @@ func (c *unstructuredConverter) structFromUnstructured(sv, dv reflect.Value, fie } } } - // TODO: error here if len(keys) > 0 meaning there is are unknown fields - klog.Warningf("END keys length: %d, value: %+v, tc: %d\n", len(keys), keys, timeCode) - klog.Warningf("fields length %d, value: %+v", len(fields), fields) if len(keys) > 0 && c.strictFieldValidation { return &UnknownFieldError{ invalidFields: keys, From 92755984fe39a44b1abb6e89b732e49a7241a4b5 Mon Sep 17 00:00:00 2001 From: Kevin Delgado Date: Mon, 11 Oct 2021 17:18:04 +0000 Subject: [PATCH 30/64] consistent test naming --- test/integration/apiserver/apiserver_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/integration/apiserver/apiserver_test.go b/test/integration/apiserver/apiserver_test.go index 2453d892b3e05..665b092630598 100644 --- a/test/integration/apiserver/apiserver_test.go +++ b/test/integration/apiserver/apiserver_test.go @@ -2999,7 +2999,7 @@ func BenchmarkFieldValidation(b *testing.B) { } } -func TestSMPFieldValidation(t *testing.T) { +func TestFieldValidationSMP(t *testing.T) { defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.ServerSideApply, true)() _, client, closeFn := setup(t) From f06b5826b2fa7fc243a940ae09258595aaad5ce4 Mon Sep 17 00:00:00 2001 From: Kevin Delgado Date: Mon, 11 Oct 2021 19:25:08 +0000 Subject: [PATCH 31/64] fix fieldValidation for SMP after unifying --- staging/src/k8s.io/apimachinery/pkg/runtime/types.go | 9 +++++---- .../src/k8s.io/apiserver/pkg/endpoints/handlers/rest.go | 2 +- test/integration/apiserver/apiserver_test.go | 2 +- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/staging/src/k8s.io/apimachinery/pkg/runtime/types.go b/staging/src/k8s.io/apimachinery/pkg/runtime/types.go index c6271e1d5f6a3..ac01833304593 100644 --- a/staging/src/k8s.io/apimachinery/pkg/runtime/types.go +++ b/staging/src/k8s.io/apimachinery/pkg/runtime/types.go @@ -41,10 +41,11 @@ type TypeMeta struct { } const ( - ContentTypeJSON string = "application/json" - ContentTypeJSONMergePatch string = "application/merge-patch+json" - ContentTypeYAML string = "application/yaml" - ContentTypeProtobuf string = "application/vnd.kubernetes.protobuf" + ContentTypeJSON string = "application/json" + ContentTypeJSONMergePatch string = "application/merge-patch+json" + ContentTypeJSONStrategicMergePatch string = "application/strategic-merge-patch+json" + ContentTypeYAML string = "application/yaml" + ContentTypeProtobuf string = "application/vnd.kubernetes.protobuf" ) // RawExtension is used to hold extensions in external versions. diff --git a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/rest.go b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/rest.go index c5e17bdc20071..9da6aa4570bc8 100644 --- a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/rest.go +++ b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/rest.go @@ -472,7 +472,7 @@ const ( // media type, because the list of media types that support field validation are a subset of // all supported media types (only json and yaml supports field validation). func fieldValidation(req *http.Request) (fieldValidationDirective, error) { - supportedContentTypes := []string{runtime.ContentTypeJSON, runtime.ContentTypeJSONMergePatch, runtime.ContentTypeYAML} + supportedContentTypes := []string{runtime.ContentTypeJSON, runtime.ContentTypeJSONMergePatch, runtime.ContentTypeJSONStrategicMergePatch, runtime.ContentTypeYAML} contentType := req.Header.Get("Content-Type") // TODO: not sure if it is okay to assume empty content type is a valid one supported := true diff --git a/test/integration/apiserver/apiserver_test.go b/test/integration/apiserver/apiserver_test.go index 665b092630598..c5b89962641a7 100644 --- a/test/integration/apiserver/apiserver_test.go +++ b/test/integration/apiserver/apiserver_test.go @@ -3052,7 +3052,7 @@ func TestFieldValidationSMP(t *testing.T) { }{ { name: "strategicMergePatchStrictValidation", - params: map[string]string{"validate": "strict"}, + params: map[string]string{"fieldValidation": "Strict"}, errContains: "unknown fields when converting from unstructured", }, { From 62ead3986b8ccf04fa8552b325607079b7b1f62c Mon Sep 17 00:00:00 2001 From: Kevin Delgado Date: Tue, 12 Oct 2021 03:54:49 +0000 Subject: [PATCH 32/64] Add SMP benchmark test --- test/integration/apiserver/apiserver_test.go | 90 ++++++++++++++++++++ 1 file changed, 90 insertions(+) diff --git a/test/integration/apiserver/apiserver_test.go b/test/integration/apiserver/apiserver_test.go index c5b89962641a7..a2c10497eb33a 100644 --- a/test/integration/apiserver/apiserver_test.go +++ b/test/integration/apiserver/apiserver_test.go @@ -3081,6 +3081,96 @@ func TestFieldValidationSMP(t *testing.T) { } } +// Benchmark field validation for strict vs non-strict +func BenchmarkFieldValidationSMP(b *testing.B) { + defer featuregatetesting.SetFeatureGateDuringTest(b, utilfeature.DefaultFeatureGate, features.ServerSideApply, true)() + + _, client, closeFn := setup(b) + defer closeFn() + + _, err := client.CoreV1().RESTClient().Patch(types.ApplyPatchType). + AbsPath("/apis/apps/v1"). + Namespace("default"). + Resource("deployments"). + Name("test-deployment"). + Param("fieldManager", "apply_test"). + Body([]byte(`{ + "apiVersion": "apps/v1", + "kind": "Deployment", + "metadata": { + "name": "test-deployment", + "labels": {"app": "nginx"} + }, + "spec": { + "selector": { + "matchLabels": { + "app": "nginx" + } + }, + "template": { + "metadata": { + "labels": { + "app": "nginx" + } + }, + "spec": { + "containers": [{ + "name": "nginx", + "image": "nginx:latest" + }] + } + } + } + }`)). + Do(context.TODO()). + Get() + if err != nil { + b.Fatalf("Failed to create object using Apply patch: %v", err) + } + + var benchmarks = []struct { + name string + params map[string]string + errContains string + }{ + { + name: "strategicMergePatchStrictValidation", + params: map[string]string{"fieldValidation": "Strict"}, + errContains: "unknown fields when converting from unstructured", + }, + { + name: "strategicMergePatchIgnoreValidation", + params: map[string]string{}, + errContains: "", + }, + } + + for _, bm := range benchmarks { + b.Run(bm.name, func(b *testing.B) { + b.ResetTimer() + b.ReportAllocs() + for n := 0; n < b.N; n++ { + req := client.CoreV1().RESTClient().Patch(types.StrategicMergePatchType). + AbsPath("/apis/apps/v1"). + Namespace("default"). + Resource("deployments"). + Name("test-deployment") + for k, v := range bm.params { + req.Param(k, v) + } + result, err := req.Body([]byte(`{"metadata":{"labels":{"label1": "val1"}},"spec":{"foo":"bar"}}`)).DoRaw(context.TODO()) + if err == nil && bm.errContains != "" { + b.Fatalf("unexpected patch succeeded") + } + if err != nil && !strings.Contains(string(result), bm.errContains) { + b.Fatalf("unexpected response: %v", string(result)) + } + } + + }) + } +} + type dependentClient struct { t *testing.T client dynamic.ResourceInterface From 712623eabb4480e01544171ec4e9f633f10f5872 Mon Sep 17 00:00:00 2001 From: Kevin Delgado Date: Tue, 12 Oct 2021 06:10:53 +0000 Subject: [PATCH 33/64] remove converter debug logging --- staging/src/k8s.io/apimachinery/pkg/runtime/converter.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/staging/src/k8s.io/apimachinery/pkg/runtime/converter.go b/staging/src/k8s.io/apimachinery/pkg/runtime/converter.go index da22bb379454f..24d0dd42b78f5 100644 --- a/staging/src/k8s.io/apimachinery/pkg/runtime/converter.go +++ b/staging/src/k8s.io/apimachinery/pkg/runtime/converter.go @@ -429,9 +429,7 @@ func deleteFromKeys(name string, keys *[]reflect.Value) { for i := len(*keys) - 1; i >= 0; i-- { if name == (*keys)[i].String() { // Delete from keys - klog.Warningf("pop keys struct name: %s", (*keys)[i].String()) *keys = append((*keys)[:i], (*keys)[i+1:]...) - klog.Warningf("keyslength is now %d after i %d", len(*keys), i) return } } From 58fffc87cc198d0f5e8cc47f3145ffa0fc07a016 Mon Sep 17 00:00:00 2001 From: Kevin Delgado Date: Tue, 12 Oct 2021 06:59:07 +0000 Subject: [PATCH 34/64] Refactor SMP tests into separate file --- test/integration/apiserver/apiserver_test.go | 172 ------------------ .../apiserver/field_validation_test.go | 143 +++++++++++++++ 2 files changed, 143 insertions(+), 172 deletions(-) create mode 100644 test/integration/apiserver/field_validation_test.go diff --git a/test/integration/apiserver/apiserver_test.go b/test/integration/apiserver/apiserver_test.go index a2c10497eb33a..91d5dba315a79 100644 --- a/test/integration/apiserver/apiserver_test.go +++ b/test/integration/apiserver/apiserver_test.go @@ -2999,178 +2999,6 @@ func BenchmarkFieldValidation(b *testing.B) { } } -func TestFieldValidationSMP(t *testing.T) { - defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.ServerSideApply, true)() - - _, client, closeFn := setup(t) - defer closeFn() - - _, err := client.CoreV1().RESTClient().Patch(types.ApplyPatchType). - AbsPath("/apis/apps/v1"). - Namespace("default"). - Resource("deployments"). - Name("test-deployment"). - Param("fieldManager", "apply_test"). - Body([]byte(`{ - "apiVersion": "apps/v1", - "kind": "Deployment", - "metadata": { - "name": "test-deployment", - "labels": {"app": "nginx"} - }, - "spec": { - "selector": { - "matchLabels": { - "app": "nginx" - } - }, - "template": { - "metadata": { - "labels": { - "app": "nginx" - } - }, - "spec": { - "containers": [{ - "name": "nginx", - "image": "nginx:latest" - }] - } - } - } - }`)). - Do(context.TODO()). - Get() - if err != nil { - t.Fatalf("Failed to create object using Apply patch: %v", err) - } - - var testcases = []struct { - name string - params map[string]string - errContains string - }{ - { - name: "strategicMergePatchStrictValidation", - params: map[string]string{"fieldValidation": "Strict"}, - errContains: "unknown fields when converting from unstructured", - }, - { - name: "strategicMergePatchIgnoreValidation", - params: map[string]string{}, - errContains: "", - }, - } - - for _, tc := range testcases { - req := client.CoreV1().RESTClient().Patch(types.StrategicMergePatchType). - AbsPath("/apis/apps/v1"). - Namespace("default"). - Resource("deployments"). - Name("test-deployment") - for k, v := range tc.params { - req.Param(k, v) - } - result, err := req.Body([]byte(`{"metadata":{"labels":{"label1": "val1"}},"spec":{"foo":"bar"}}`)).DoRaw(context.TODO()) - if err == nil && tc.errContains != "" { - t.Fatalf("unexpected patch succeeded") - } - if err != nil && !strings.Contains(string(result), tc.errContains) { - t.Fatalf("unexpected response: %v", string(result)) - } - } -} - -// Benchmark field validation for strict vs non-strict -func BenchmarkFieldValidationSMP(b *testing.B) { - defer featuregatetesting.SetFeatureGateDuringTest(b, utilfeature.DefaultFeatureGate, features.ServerSideApply, true)() - - _, client, closeFn := setup(b) - defer closeFn() - - _, err := client.CoreV1().RESTClient().Patch(types.ApplyPatchType). - AbsPath("/apis/apps/v1"). - Namespace("default"). - Resource("deployments"). - Name("test-deployment"). - Param("fieldManager", "apply_test"). - Body([]byte(`{ - "apiVersion": "apps/v1", - "kind": "Deployment", - "metadata": { - "name": "test-deployment", - "labels": {"app": "nginx"} - }, - "spec": { - "selector": { - "matchLabels": { - "app": "nginx" - } - }, - "template": { - "metadata": { - "labels": { - "app": "nginx" - } - }, - "spec": { - "containers": [{ - "name": "nginx", - "image": "nginx:latest" - }] - } - } - } - }`)). - Do(context.TODO()). - Get() - if err != nil { - b.Fatalf("Failed to create object using Apply patch: %v", err) - } - - var benchmarks = []struct { - name string - params map[string]string - errContains string - }{ - { - name: "strategicMergePatchStrictValidation", - params: map[string]string{"fieldValidation": "Strict"}, - errContains: "unknown fields when converting from unstructured", - }, - { - name: "strategicMergePatchIgnoreValidation", - params: map[string]string{}, - errContains: "", - }, - } - - for _, bm := range benchmarks { - b.Run(bm.name, func(b *testing.B) { - b.ResetTimer() - b.ReportAllocs() - for n := 0; n < b.N; n++ { - req := client.CoreV1().RESTClient().Patch(types.StrategicMergePatchType). - AbsPath("/apis/apps/v1"). - Namespace("default"). - Resource("deployments"). - Name("test-deployment") - for k, v := range bm.params { - req.Param(k, v) - } - result, err := req.Body([]byte(`{"metadata":{"labels":{"label1": "val1"}},"spec":{"foo":"bar"}}`)).DoRaw(context.TODO()) - if err == nil && bm.errContains != "" { - b.Fatalf("unexpected patch succeeded") - } - if err != nil && !strings.Contains(string(result), bm.errContains) { - b.Fatalf("unexpected response: %v", string(result)) - } - } - - }) - } -} - type dependentClient struct { t *testing.T client dynamic.ResourceInterface diff --git a/test/integration/apiserver/field_validation_test.go b/test/integration/apiserver/field_validation_test.go new file mode 100644 index 0000000000000..e055e11c0df10 --- /dev/null +++ b/test/integration/apiserver/field_validation_test.go @@ -0,0 +1,143 @@ +/* +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" + "fmt" + "os" + "strings" + "testing" + + "k8s.io/apimachinery/pkg/types" + "k8s.io/apiserver/pkg/features" + utilfeature "k8s.io/apiserver/pkg/util/feature" + clientset "k8s.io/client-go/kubernetes" + featuregatetesting "k8s.io/component-base/featuregate/testing" +) + +// smpTestSetup applies an object that will later be patched +// in the actual test/benchmark. +func smpTestSetup(t testing.TB, client clientset.Interface) { + bodyBase, err := os.ReadFile("./testdata/deploy-small.json") + if err != nil { + t.Fatalf("failed to read file: %v", err) + } + _, err = client.CoreV1().RESTClient().Patch(types.ApplyPatchType). + AbsPath("/apis/apps/v1"). + Namespace("default"). + Resource("deployments"). + Name("test-deployment"). + Param("fieldManager", "apply_test"). + Body([]byte(fmt.Sprintf(string(bodyBase), "test-deployment"))). + Do(context.TODO()). + Get() + if err != nil { + t.Fatalf("Failed to create object using Apply patch: %v", err) + } +} + +// smpRunTest attempts to patch an object via strategic-merge-patch +// with params given from the testcase. +func smpRunTest(t testing.TB, client clientset.Interface, tc smpTestCase) { + req := client.CoreV1().RESTClient().Patch(types.StrategicMergePatchType). + AbsPath("/apis/apps/v1"). + Namespace("default"). + Resource("deployments"). + Name("test-deployment") + for k, v := range tc.params { + req.Param(k, v) + } + result, err := req.Body([]byte(`{"metadata":{"labels":{"label1": "val1"}},"spec":{"foo":"bar"}}`)).DoRaw(context.TODO()) + if err == nil && tc.errContains != "" { + t.Fatalf("unexpected patch succeeded") + } + if err != nil && !strings.Contains(string(result), tc.errContains) { + t.Fatalf("unexpected response: %v", string(result)) + } +} + +type smpTestCase struct { + name string + params map[string]string + errContains string +} + +// 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) { + defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.ServerSideApply, true)() + + _, client, closeFn := setup(t) + defer closeFn() + + smpTestSetup(t, client) + + var testcases = []smpTestCase{ + { + name: "smp-strict-validation", + params: map[string]string{"fieldValidation": "Strict"}, + errContains: "unknown fields when converting from unstructured", + }, + { + name: "smp-ignore-validation", + params: map[string]string{}, + errContains: "", + }, + } + + for _, tc := range testcases { + t.Run(tc.name, func(t *testing.T) { + smpRunTest(t, client, tc) + }) + } +} + +// Benchmark strategic-merge-patch field validation for strict vs non-strict +func BenchmarkFieldValidationSMP(b *testing.B) { + defer featuregatetesting.SetFeatureGateDuringTest(b, utilfeature.DefaultFeatureGate, features.ServerSideApply, true)() + + _, client, closeFn := setup(b) + defer closeFn() + + smpTestSetup(b, client) + + var benchmarks = []smpTestCase{ + { + name: "smp-strict-validation", + params: map[string]string{"fieldValidation": "Strict"}, + errContains: "unknown fields when converting from unstructured", + }, + { + name: "smp-ignore-validation", + params: map[string]string{}, + errContains: "", + }, + } + + for _, bm := range benchmarks { + b.Run(bm.name, func(b *testing.B) { + b.ResetTimer() + b.ReportAllocs() + for n := 0; n < b.N; n++ { + smpRunTest(b, client, bm) + } + + }) + } +} From 772b5c65d91fe3121558d9b9e40f97fc4b2981b3 Mon Sep 17 00:00:00 2001 From: Kevin Delgado Date: Tue, 12 Oct 2021 20:04:24 +0000 Subject: [PATCH 35/64] using helper breaks patch CRD test for some reason --- test/integration/apiserver/apiserver_test.go | 355 ------------------ .../apiserver/field_validation_test.go | 319 ++++++++++++++++ 2 files changed, 319 insertions(+), 355 deletions(-) diff --git a/test/integration/apiserver/apiserver_test.go b/test/integration/apiserver/apiserver_test.go index 91d5dba315a79..16d0f96ccf3c2 100644 --- a/test/integration/apiserver/apiserver_test.go +++ b/test/integration/apiserver/apiserver_test.go @@ -2532,361 +2532,6 @@ func TestFieldValidationPost(t *testing.T) { } } -// TestFieldValidationPatchCRD tests that server-side schema validation -// works for jsonpatch and mergepatch requests. -func TestFieldValidationPatchCRD(t *testing.T) { - crdSchema := `{ - "openAPIV3Schema": { - "type": "object", - "properties": { - "spec": { - "type": "object", - "properties": { - "cronSpec": { - "type": "string", - "pattern": "^(\\d+|\\*)(/\\d+)?(\\s+(\\d+|\\*)(/\\d+)?){4}$" - }, - "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" - } - } - } - } - } - } - }` - patchYAMLBody := ` -apiVersion: %s -kind: %s -metadata: - name: %s - finalizers: - - test-finalizer -spec: - cronSpec: "* * * * */5" - ports: - - name: x - containerPort: 80 - protocol: TCP` - var testcases = []struct { - name string - patchType types.PatchType - params map[string]string - body string - errContains string - }{ - { - name: "mergePatchStrictValidation", - patchType: types.MergePatchType, - params: map[string]string{"fieldValidation": "Strict"}, - body: `{"metadata":{"finalizers":["test-finalizer","another-one"]}, "spec":{"foo": "bar"}}`, - errContains: "failed with unknown fields", - }, - { - name: "mergePatchNoValidation", - patchType: types.MergePatchType, - params: map[string]string{}, - body: `{"metadata":{"finalizers":["test-finalizer","another-one"]}, "spec":{"foo": "bar"}}`, - errContains: "", - }, - // TODO: figure out how to test JSONPatch - //{ - // name: "jsonPatchStrictValidation", - // patchType: types.JSONPatchType, - // params: map[string]string{"validate": "strict"}, - // body: // TODO - // errContains: "failed with unknown fields", - //}, - //{ - // name: "jsonPatchNoValidation", - // patchType: types.JSONPatchType, - // params: map[string]string{}, - // body: // TODO - // errContains: "", - //}, - } - for _, tc := range testcases { - t.Run(tc.name, func(t *testing.T) { - server, err := kubeapiservertesting.StartTestServer(t, kubeapiservertesting.NewDefaultTestServerOptions(), nil, framework.SharedEtcd()) - if err != nil { - t.Fatal(err) - } - defer server.TearDownFn() - config := server.ClientConfig - - apiExtensionClient, err := apiextensionsclient.NewForConfig(config) - if err != nil { - t.Fatal(err) - } - dynamicClient, err := dynamic.NewForConfig(config) - if err != nil { - t.Fatal(err) - } - - // create the CRD - noxuDefinition := fixtures.NewNoxuV1CustomResourceDefinition(apiextensionsv1.ClusterScoped) - var c apiextensionsv1.CustomResourceValidation - err = json.Unmarshal([]byte(crdSchema), &c) - if err != nil { - t.Fatal(err) - } - // set the CRD schema - noxuDefinition.Spec.PreserveUnknownFields = false - for i := range noxuDefinition.Spec.Versions { - noxuDefinition.Spec.Versions[i].Schema = &c - } - // install the CRD - noxuDefinition, err = fixtures.CreateNewV1CustomResourceDefinition(noxuDefinition, apiExtensionClient, dynamicClient) - if err != nil { - t.Fatal(err) - } - - kind := noxuDefinition.Spec.Names.Kind - apiVersion := noxuDefinition.Spec.Group + "/" + noxuDefinition.Spec.Versions[0].Name - name := "mytest" - - // create a CR - rest := apiExtensionClient.Discovery().RESTClient() - yamlBody := []byte(fmt.Sprintf(patchYAMLBody, apiVersion, kind, name)) - result, err := rest.Patch(types.ApplyPatchType). - AbsPath("/apis", noxuDefinition.Spec.Group, noxuDefinition.Spec.Versions[0].Name, noxuDefinition.Spec.Names.Plural). - Name(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(result)) - } - - // patch the CR as specified by the test case - req := rest.Patch(tc.patchType). - AbsPath("/apis", noxuDefinition.Spec.Group, noxuDefinition.Spec.Versions[0].Name, noxuDefinition.Spec.Names.Plural). - Name(name) - for k, v := range tc.params { - req = req.Param(k, v) - } - result, err = req. - Body([]byte(tc.body)). - DoRaw(context.TODO()) - if err == nil && tc.errContains != "" { - t.Fatalf("unexpected patch succeeded, expected %s", tc.errContains) - } - if err != nil && !strings.Contains(string(result), tc.errContains) { - t.Fatalf("unexpected response: %v", string(result)) - } - }) - } -} - -func BenchmarkFieldValidationPatchCRD(b *testing.B) { - server, err := kubeapiservertesting.StartTestServer(b, kubeapiservertesting.NewDefaultTestServerOptions(), nil, framework.SharedEtcd()) - if err != nil { - panic(err) - } - defer server.TearDownFn() - config := server.ClientConfig - - apiExtensionClient, err := apiextensionsclient.NewForConfig(config) - if err != nil { - panic(err) - } - dynamicClient, err := dynamic.NewForConfig(config) - if err != nil { - panic(err) - } - - crdSchema := `{ - "openAPIV3Schema": { - "type": "object", - "properties": { - "spec": { - "type": "object", - "properties": { - "cronSpec": { - "type": "string", - "pattern": "^(\\d+|\\*)(/\\d+)?(\\s+(\\d+|\\*)(/\\d+)?){4}$" - }, - "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" - } - } - } - } - } - } - }` - // create the CRD - noxuDefinition := fixtures.NewNoxuV1CustomResourceDefinition(apiextensionsv1.ClusterScoped) - var c apiextensionsv1.CustomResourceValidation - err = json.Unmarshal([]byte(crdSchema), &c) - if err != nil { - panic(err) - } - // set the CRD schema - noxuDefinition.Spec.PreserveUnknownFields = false - for i := range noxuDefinition.Spec.Versions { - noxuDefinition.Spec.Versions[i].Schema = &c - //fmt.Printf("noxuDefiniton.Spec.Versions[i].Schema = %+v\n", noxuDefinition.Spec.Versions[i].Schema) - } - // install the CRD - noxuDefinition, err = fixtures.CreateNewV1CustomResourceDefinition(noxuDefinition, apiExtensionClient, dynamicClient) - if err != nil { - panic(err) - } - - kind := noxuDefinition.Spec.Names.Kind - apiVersion := noxuDefinition.Spec.Group + "/" + noxuDefinition.Spec.Versions[0].Name - name := "mytest" - - patchYAMLBody := ` -apiVersion: %s -kind: %s -metadata: - name: %s - finalizers: - - test-finalizer -spec: - cronSpec: "* * * * */5" - ports: - - name: x - containerPort: 80 - protocol: TCP` - // create a CR - rest := apiExtensionClient.Discovery().RESTClient() - yamlBody := []byte(fmt.Sprintf(patchYAMLBody, apiVersion, kind, name)) - result, err := rest.Patch(types.ApplyPatchType). - AbsPath("/apis", noxuDefinition.Spec.Group, noxuDefinition.Spec.Versions[0].Name, noxuDefinition.Spec.Names.Plural). - Name(name). - Param("fieldManager", "apply_test"). - Body(yamlBody). - DoRaw(context.TODO()) - if err != nil { - panic(fmt.Sprintf("failed to create custom resource with apply: %v:\n%v", err, string(result))) - } - - benchmarks := []struct { - name string - patchType types.PatchType - params map[string]string - bodyBase string - errContains string - }{ - { - name: "ignore-validation-crd-patch", - patchType: types.MergePatchType, - params: map[string]string{}, - bodyBase: `{"metadata":{"finalizers":["test-finalizer","finalizer-ignore-%d"]}}`, - errContains: "", - }, - { - name: "strict-validation-crd-patch", - patchType: types.MergePatchType, - params: map[string]string{"fieldValidation": "Strict"}, - bodyBase: `{"metadata":{"finalizers":["test-finalizer","finalizer-strict-%d"]}}`, - errContains: "", - }, - { - name: "ignore-validation-crd-patch-unknown-field", - patchType: types.MergePatchType, - params: map[string]string{}, - bodyBase: `{"metadata":{"finalizers":["test-finalizer","finalizer-ignore-unknown-%d"]}, "spec":{"foo": "bar"}}`, - errContains: "", - }, - { - name: "strict-validation-crd-patch-unknown-field", - patchType: types.MergePatchType, - params: map[string]string{"fieldValidation": "Strict"}, - bodyBase: `{"metadata":{"finalizers":["test-finalizer","finalizer-strict-unknown-%d"]}, "spec":{"foo": "bar"}}`, - errContains: "failed with unknown fields", - }, - } - for _, bm := range benchmarks { - b.Run(bm.name, func(b *testing.B) { - b.ResetTimer() - b.ReportAllocs() - for n := 0; n < b.N; n++ { - body := fmt.Sprintf(bm.bodyBase, n) - // patch the CR as specified by the test case - req := rest.Patch(bm.patchType). - AbsPath("/apis", noxuDefinition.Spec.Group, noxuDefinition.Spec.Versions[0].Name, noxuDefinition.Spec.Names.Plural). - Name(name) - for k, v := range bm.params { - req = req.Param(k, v) - } - result, err = req. - Body([]byte(body)). - DoRaw(context.TODO()) - if err == nil && bm.errContains != "" { - panic(fmt.Sprintf("unexpected patch succeeded, expected %s", bm.errContains)) - } - if err != nil && !strings.Contains(string(result), bm.errContains) { - panic(err) - } - } - }) - } -} - // Benchmark field validation for strict vs non-strict func BenchmarkFieldValidation(b *testing.B) { _, client, closeFn := setup(b) diff --git a/test/integration/apiserver/field_validation_test.go b/test/integration/apiserver/field_validation_test.go index e055e11c0df10..9ddb985cca54a 100644 --- a/test/integration/apiserver/field_validation_test.go +++ b/test/integration/apiserver/field_validation_test.go @@ -18,16 +18,25 @@ package apiserver import ( "context" + "encoding/json" "fmt" "os" "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" "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" + restclient "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" ) // smpTestSetup applies an object that will later be patched @@ -117,6 +126,7 @@ func BenchmarkFieldValidationSMP(b *testing.B) { smpTestSetup(b, client) + // TODO: add more benchmarks to test bigger objects var benchmarks = []smpTestCase{ { name: "smp-strict-validation", @@ -141,3 +151,312 @@ func BenchmarkFieldValidationSMP(b *testing.B) { }) } } + +func patchCRDTestSetup(t testing.TB, name string) (restclient.Interface, *apiextensionsv1.CustomResourceDefinition) { + server, err := kubeapiservertesting.StartTestServer(t, kubeapiservertesting.NewDefaultTestServerOptions(), nil, framework.SharedEtcd()) + if err != nil { + t.Fatal(err) + } + defer server.TearDownFn() + config := server.ClientConfig + + apiExtensionClient, err := apiextensionsclient.NewForConfig(config) + if err != nil { + t.Fatal(err) + } + dynamicClient, err := dynamic.NewForConfig(config) + if err != nil { + t.Fatal(err) + } + crdSchema, err := os.ReadFile("./testdata/crd-schema.json") + if err != nil { + t.Fatalf("failed to read file: %v", err) + } + patchYAMLBody, err := os.ReadFile("./testdata/noxu-cr-shell.yaml") + if err != nil { + t.Fatalf("failed to read file: %v", err) + } + + // create the CRD + noxuDefinition := fixtures.NewNoxuV1CustomResourceDefinition(apiextensionsv1.ClusterScoped) + var c apiextensionsv1.CustomResourceValidation + err = json.Unmarshal(crdSchema, &c) + if err != nil { + t.Fatal(err) + } + // set the CRD schema + noxuDefinition.Spec.PreserveUnknownFields = false + for i := range noxuDefinition.Spec.Versions { + noxuDefinition.Spec.Versions[i].Schema = &c + } + // install the CRD + noxuDefinition, err = fixtures.CreateNewV1CustomResourceDefinition(noxuDefinition, apiExtensionClient, dynamicClient) + if err != nil { + t.Fatal(err) + } + + kind := noxuDefinition.Spec.Names.Kind + apiVersion := noxuDefinition.Spec.Group + "/" + noxuDefinition.Spec.Versions[0].Name + + // create a CR + rest := apiExtensionClient.Discovery().RESTClient() + yamlBody := []byte(fmt.Sprintf(string(patchYAMLBody), apiVersion, kind, name)) + result, err := rest.Patch(types.ApplyPatchType). + AbsPath("/apis", noxuDefinition.Spec.Group, noxuDefinition.Spec.Versions[0].Name, noxuDefinition.Spec.Names.Plural). + Name(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(result)) + } + + return rest, noxuDefinition +} + +// TestFieldValidationPatchCRD tests that server-side schema validation +// works for jsonpatch and mergepatch requests. +func TestFieldValidationPatchCRD(t *testing.T) { + var testcases = []struct { + name string + patchType types.PatchType + params map[string]string + body string + errContains string + }{ + { + name: "merge-patch-strict-validation", + patchType: types.MergePatchType, + params: map[string]string{"fieldValidation": "Strict"}, + body: `{"metadata":{"finalizers":["test-finalizer","another-one"]}, "spec":{"foo": "bar"}}`, + errContains: "failed with unknown fields", + }, + { + name: "merge-patch-no-validation", + patchType: types.MergePatchType, + params: map[string]string{}, + body: `{"metadata":{"finalizers":["test-finalizer","another-one"]}, "spec":{"foo": "bar"}}`, + errContains: "", + }, + // TODO: figure out how to test JSONPatch + //{ + // name: "jsonPatchStrictValidation", + // patchType: types.JSONPatchType, + // params: map[string]string{"validate": "strict"}, + // body: // TODO + // errContains: "failed with unknown fields", + //}, + //{ + // name: "jsonPatchNoValidation", + // patchType: types.JSONPatchType, + // params: map[string]string{}, + // body: // TODO + // errContains: "", + //}, + } + for _, tc := range testcases { + t.Run(tc.name, func(t *testing.T) { + name := "testname" + //rest, noxuDefinition := patchCRDTestSetup(t, name) + server, err := kubeapiservertesting.StartTestServer(t, kubeapiservertesting.NewDefaultTestServerOptions(), nil, framework.SharedEtcd()) + if err != nil { + t.Fatal(err) + } + defer server.TearDownFn() + config := server.ClientConfig + + apiExtensionClient, err := apiextensionsclient.NewForConfig(config) + if err != nil { + t.Fatal(err) + } + dynamicClient, err := dynamic.NewForConfig(config) + if err != nil { + t.Fatal(err) + } + crdSchema, err := os.ReadFile("./testdata/crd-schema.json") + if err != nil { + t.Fatalf("failed to read file: %v", err) + } + patchYAMLBody, err := os.ReadFile("./testdata/noxu-cr-shell.yaml") + if err != nil { + t.Fatalf("failed to read file: %v", err) + } + + // create the CRD + noxuDefinition := fixtures.NewNoxuV1CustomResourceDefinition(apiextensionsv1.ClusterScoped) + var c apiextensionsv1.CustomResourceValidation + err = json.Unmarshal(crdSchema, &c) + if err != nil { + t.Fatal(err) + } + // set the CRD schema + noxuDefinition.Spec.PreserveUnknownFields = false + for i := range noxuDefinition.Spec.Versions { + noxuDefinition.Spec.Versions[i].Schema = &c + } + // install the CRD + noxuDefinition, err = fixtures.CreateNewV1CustomResourceDefinition(noxuDefinition, apiExtensionClient, dynamicClient) + if err != nil { + t.Fatal(err) + } + + kind := noxuDefinition.Spec.Names.Kind + apiVersion := noxuDefinition.Spec.Group + "/" + noxuDefinition.Spec.Versions[0].Name + + // create a CR + rest := apiExtensionClient.Discovery().RESTClient() + yamlBody := []byte(fmt.Sprintf(string(patchYAMLBody), apiVersion, kind, name)) + result, err := rest.Patch(types.ApplyPatchType). + AbsPath("/apis", noxuDefinition.Spec.Group, noxuDefinition.Spec.Versions[0].Name, noxuDefinition.Spec.Names.Plural). + Name(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(result)) + } + + // patch the CR as specified by the test case + req := rest.Patch(tc.patchType). + AbsPath("/apis", noxuDefinition.Spec.Group, noxuDefinition.Spec.Versions[0].Name, noxuDefinition.Spec.Names.Plural). + Name(name) + for k, v := range tc.params { + req = req.Param(k, v) + } + result, err = req. + Body([]byte(tc.body)). + DoRaw(context.TODO()) + if err == nil && tc.errContains != "" { + t.Fatalf("unexpected patch succeeded, expected %s", tc.errContains) + } + if err != nil && !strings.Contains(string(result), tc.errContains) { + t.Errorf("bad err: %v", err) + t.Fatalf("unexpected response: %v", string(result)) + } + }) + } +} + +func BenchmarkFieldValidationPatchCRD(b *testing.B) { + server, err := kubeapiservertesting.StartTestServer(b, kubeapiservertesting.NewDefaultTestServerOptions(), nil, framework.SharedEtcd()) + if err != nil { + panic(err) + } + defer server.TearDownFn() + config := server.ClientConfig + + apiExtensionClient, err := apiextensionsclient.NewForConfig(config) + if err != nil { + panic(err) + } + dynamicClient, err := dynamic.NewForConfig(config) + if err != nil { + panic(err) + } + crdSchema, err := os.ReadFile("./testdata/crd-schema.json") + if err != nil { + b.Fatalf("failed to read file: %v", err) + } + patchYAMLBody, err := os.ReadFile("./testdata/noxu-cr-shell.yaml") + if err != nil { + b.Fatalf("failed to read file: %v", err) + } + + // create the CRD + noxuDefinition := fixtures.NewNoxuV1CustomResourceDefinition(apiextensionsv1.ClusterScoped) + var c apiextensionsv1.CustomResourceValidation + err = json.Unmarshal(crdSchema, &c) + if err != nil { + panic(err) + } + // set the CRD schema + noxuDefinition.Spec.PreserveUnknownFields = false + for i := range noxuDefinition.Spec.Versions { + noxuDefinition.Spec.Versions[i].Schema = &c + //fmt.Printf("noxuDefiniton.Spec.Versions[i].Schema = %+v\n", noxuDefinition.Spec.Versions[i].Schema) + } + // install the CRD + noxuDefinition, err = fixtures.CreateNewV1CustomResourceDefinition(noxuDefinition, apiExtensionClient, dynamicClient) + if err != nil { + panic(err) + } + + kind := noxuDefinition.Spec.Names.Kind + apiVersion := noxuDefinition.Spec.Group + "/" + noxuDefinition.Spec.Versions[0].Name + name := "mytest" + + // create a CR + rest := apiExtensionClient.Discovery().RESTClient() + yamlBody := []byte(fmt.Sprintf(string(patchYAMLBody), apiVersion, kind, name)) + result, err := rest.Patch(types.ApplyPatchType). + AbsPath("/apis", noxuDefinition.Spec.Group, noxuDefinition.Spec.Versions[0].Name, noxuDefinition.Spec.Names.Plural). + Name(name). + Param("fieldManager", "apply_test"). + Body(yamlBody). + DoRaw(context.TODO()) + if err != nil { + panic(fmt.Sprintf("failed to create custom resource with apply: %v:\n%v", err, string(result))) + } + + benchmarks := []struct { + name string + patchType types.PatchType + params map[string]string + bodyBase string + errContains string + }{ + { + name: "ignore-validation-crd-patch", + patchType: types.MergePatchType, + params: map[string]string{}, + bodyBase: `{"metadata":{"finalizers":["test-finalizer","finalizer-ignore-%d"]}}`, + errContains: "", + }, + { + name: "strict-validation-crd-patch", + patchType: types.MergePatchType, + params: map[string]string{"fieldValidation": "Strict"}, + bodyBase: `{"metadata":{"finalizers":["test-finalizer","finalizer-strict-%d"]}}`, + errContains: "", + }, + { + name: "ignore-validation-crd-patch-unknown-field", + patchType: types.MergePatchType, + params: map[string]string{}, + bodyBase: `{"metadata":{"finalizers":["test-finalizer","finalizer-ignore-unknown-%d"]}, "spec":{"foo": "bar"}}`, + errContains: "", + }, + { + name: "strict-validation-crd-patch-unknown-field", + patchType: types.MergePatchType, + params: map[string]string{"fieldValidation": "Strict"}, + bodyBase: `{"metadata":{"finalizers":["test-finalizer","finalizer-strict-unknown-%d"]}, "spec":{"foo": "bar"}}`, + errContains: "failed with unknown fields", + }, + } + for _, bm := range benchmarks { + b.Run(bm.name, func(b *testing.B) { + b.ResetTimer() + b.ReportAllocs() + for n := 0; n < b.N; n++ { + body := fmt.Sprintf(bm.bodyBase, n) + // patch the CR as specified by the test case + req := rest.Patch(bm.patchType). + AbsPath("/apis", noxuDefinition.Spec.Group, noxuDefinition.Spec.Versions[0].Name, noxuDefinition.Spec.Names.Plural). + Name(name) + for k, v := range bm.params { + req = req.Param(k, v) + } + result, err = req. + Body([]byte(body)). + DoRaw(context.TODO()) + if err == nil && bm.errContains != "" { + panic(fmt.Sprintf("unexpected patch succeeded, expected %s", bm.errContains)) + } + if err != nil && !strings.Contains(string(result), bm.errContains) { + panic(err) + } + } + }) + } +} From 2685b5722b0e0a66727bbb852a864a26f837b8e0 Mon Sep 17 00:00:00 2001 From: Kevin Delgado Date: Tue, 12 Oct 2021 20:32:08 +0000 Subject: [PATCH 36/64] refactor patchCRD to properly use helper --- .../apiserver/field_validation_test.go | 140 ++---------------- 1 file changed, 16 insertions(+), 124 deletions(-) diff --git a/test/integration/apiserver/field_validation_test.go b/test/integration/apiserver/field_validation_test.go index 9ddb985cca54a..2c8ce2a6277e3 100644 --- a/test/integration/apiserver/field_validation_test.go +++ b/test/integration/apiserver/field_validation_test.go @@ -152,12 +152,7 @@ func BenchmarkFieldValidationSMP(b *testing.B) { } } -func patchCRDTestSetup(t testing.TB, name string) (restclient.Interface, *apiextensionsv1.CustomResourceDefinition) { - server, err := kubeapiservertesting.StartTestServer(t, kubeapiservertesting.NewDefaultTestServerOptions(), nil, framework.SharedEtcd()) - if err != nil { - t.Fatal(err) - } - defer server.TearDownFn() +func patchCRDTestSetup(t testing.TB, server kubeapiservertesting.TestServer, name string) (restclient.Interface, *apiextensionsv1.CustomResourceDefinition) { config := server.ClientConfig apiExtensionClient, err := apiextensionsclient.NewForConfig(config) @@ -256,74 +251,22 @@ func TestFieldValidationPatchCRD(t *testing.T) { } for _, tc := range testcases { t.Run(tc.name, func(t *testing.T) { - name := "testname" - //rest, noxuDefinition := patchCRDTestSetup(t, name) + // setup the testerver and install the CRD server, err := kubeapiservertesting.StartTestServer(t, kubeapiservertesting.NewDefaultTestServerOptions(), nil, framework.SharedEtcd()) if err != nil { t.Fatal(err) } defer server.TearDownFn() - config := server.ClientConfig - - apiExtensionClient, err := apiextensionsclient.NewForConfig(config) - if err != nil { - t.Fatal(err) - } - dynamicClient, err := dynamic.NewForConfig(config) - if err != nil { - t.Fatal(err) - } - crdSchema, err := os.ReadFile("./testdata/crd-schema.json") - if err != nil { - t.Fatalf("failed to read file: %v", err) - } - patchYAMLBody, err := os.ReadFile("./testdata/noxu-cr-shell.yaml") - if err != nil { - t.Fatalf("failed to read file: %v", err) - } - - // create the CRD - noxuDefinition := fixtures.NewNoxuV1CustomResourceDefinition(apiextensionsv1.ClusterScoped) - var c apiextensionsv1.CustomResourceValidation - err = json.Unmarshal(crdSchema, &c) - if err != nil { - t.Fatal(err) - } - // set the CRD schema - noxuDefinition.Spec.PreserveUnknownFields = false - for i := range noxuDefinition.Spec.Versions { - noxuDefinition.Spec.Versions[i].Schema = &c - } - // install the CRD - noxuDefinition, err = fixtures.CreateNewV1CustomResourceDefinition(noxuDefinition, apiExtensionClient, dynamicClient) - if err != nil { - t.Fatal(err) - } - - kind := noxuDefinition.Spec.Names.Kind - apiVersion := noxuDefinition.Spec.Group + "/" + noxuDefinition.Spec.Versions[0].Name - - // create a CR - rest := apiExtensionClient.Discovery().RESTClient() - yamlBody := []byte(fmt.Sprintf(string(patchYAMLBody), apiVersion, kind, name)) - result, err := rest.Patch(types.ApplyPatchType). - AbsPath("/apis", noxuDefinition.Spec.Group, noxuDefinition.Spec.Versions[0].Name, noxuDefinition.Spec.Names.Plural). - Name(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(result)) - } + rest, noxuDefinition := patchCRDTestSetup(t, server, tc.name) // patch the CR as specified by the test case req := rest.Patch(tc.patchType). AbsPath("/apis", noxuDefinition.Spec.Group, noxuDefinition.Spec.Versions[0].Name, noxuDefinition.Spec.Names.Plural). - Name(name) + Name(tc.name) for k, v := range tc.params { req = req.Param(k, v) } - result, err = req. + result, err := req. Body([]byte(tc.body)). DoRaw(context.TODO()) if err == nil && tc.errContains != "" { @@ -338,66 +281,6 @@ func TestFieldValidationPatchCRD(t *testing.T) { } func BenchmarkFieldValidationPatchCRD(b *testing.B) { - server, err := kubeapiservertesting.StartTestServer(b, kubeapiservertesting.NewDefaultTestServerOptions(), nil, framework.SharedEtcd()) - if err != nil { - panic(err) - } - defer server.TearDownFn() - config := server.ClientConfig - - apiExtensionClient, err := apiextensionsclient.NewForConfig(config) - if err != nil { - panic(err) - } - dynamicClient, err := dynamic.NewForConfig(config) - if err != nil { - panic(err) - } - crdSchema, err := os.ReadFile("./testdata/crd-schema.json") - if err != nil { - b.Fatalf("failed to read file: %v", err) - } - patchYAMLBody, err := os.ReadFile("./testdata/noxu-cr-shell.yaml") - if err != nil { - b.Fatalf("failed to read file: %v", err) - } - - // create the CRD - noxuDefinition := fixtures.NewNoxuV1CustomResourceDefinition(apiextensionsv1.ClusterScoped) - var c apiextensionsv1.CustomResourceValidation - err = json.Unmarshal(crdSchema, &c) - if err != nil { - panic(err) - } - // set the CRD schema - noxuDefinition.Spec.PreserveUnknownFields = false - for i := range noxuDefinition.Spec.Versions { - noxuDefinition.Spec.Versions[i].Schema = &c - //fmt.Printf("noxuDefiniton.Spec.Versions[i].Schema = %+v\n", noxuDefinition.Spec.Versions[i].Schema) - } - // install the CRD - noxuDefinition, err = fixtures.CreateNewV1CustomResourceDefinition(noxuDefinition, apiExtensionClient, dynamicClient) - if err != nil { - panic(err) - } - - kind := noxuDefinition.Spec.Names.Kind - apiVersion := noxuDefinition.Spec.Group + "/" + noxuDefinition.Spec.Versions[0].Name - name := "mytest" - - // create a CR - rest := apiExtensionClient.Discovery().RESTClient() - yamlBody := []byte(fmt.Sprintf(string(patchYAMLBody), apiVersion, kind, name)) - result, err := rest.Patch(types.ApplyPatchType). - AbsPath("/apis", noxuDefinition.Spec.Group, noxuDefinition.Spec.Versions[0].Name, noxuDefinition.Spec.Names.Plural). - Name(name). - Param("fieldManager", "apply_test"). - Body(yamlBody). - DoRaw(context.TODO()) - if err != nil { - panic(fmt.Sprintf("failed to create custom resource with apply: %v:\n%v", err, string(result))) - } - benchmarks := []struct { name string patchType types.PatchType @@ -439,15 +322,24 @@ func BenchmarkFieldValidationPatchCRD(b *testing.B) { b.ResetTimer() b.ReportAllocs() for n := 0; n < b.N; n++ { + + // setup the testerver and install the CRD + server, err := kubeapiservertesting.StartTestServer(b, kubeapiservertesting.NewDefaultTestServerOptions(), nil, framework.SharedEtcd()) + if err != nil { + b.Fatal(err) + } + defer server.TearDownFn() + rest, noxuDefinition := patchCRDTestSetup(b, server, bm.name) + body := fmt.Sprintf(bm.bodyBase, n) // patch the CR as specified by the test case req := rest.Patch(bm.patchType). AbsPath("/apis", noxuDefinition.Spec.Group, noxuDefinition.Spec.Versions[0].Name, noxuDefinition.Spec.Names.Plural). - Name(name) + Name(bm.name) for k, v := range bm.params { req = req.Param(k, v) } - result, err = req. + result, err := req. Body([]byte(body)). DoRaw(context.TODO()) if err == nil && bm.errContains != "" { From 9bb72d53031aade27e56d3140d26ccbe45f8b3d5 Mon Sep 17 00:00:00 2001 From: Kevin Delgado Date: Tue, 12 Oct 2021 22:11:55 +0000 Subject: [PATCH 37/64] refactor fieldValidation post test --- test/integration/apiserver/apiserver_test.go | 315 ------------------ .../apiserver/field_validation_test.go | 245 ++++++++++++++ 2 files changed, 245 insertions(+), 315 deletions(-) diff --git a/test/integration/apiserver/apiserver_test.go b/test/integration/apiserver/apiserver_test.go index 16d0f96ccf3c2..dd4c61732ad16 100644 --- a/test/integration/apiserver/apiserver_test.go +++ b/test/integration/apiserver/apiserver_test.go @@ -20,13 +20,11 @@ import ( "bytes" "context" "encoding/json" - "flag" "fmt" "io" "io/ioutil" "net/http" "net/http/httptest" - "os" "path" "reflect" "strconv" @@ -2331,319 +2329,6 @@ func TestDedupOwnerReferences(t *testing.T) { } } -func TestFieldValidationPut(t *testing.T) { - defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.ServerSideApply, true)() - - _, client, closeFn := setup(t) - defer closeFn() - - postBody := []byte(`{ - "apiVersion": "apps/v1", - "kind": "Deployment", - "metadata": { - "name": "test-dep", - "labels": {"app": "nginx"} - }, - "spec": { - "selector": { - "matchLabels": { - "app": "nginx" - } - }, - "template": { - "metadata": { - "labels": { - "app": "nginx" - } - }, - "spec": { - "containers": [{ - "name": "nginx", - "image": "nginx:latest" - }] - } - } - } - }`) - - putBody := []byte(`{ - "apiVersion": "apps/v1", - "kind": "Deployment", - "metadata": { - "name": "test-dep", - "labels": {"app": "nginx"} - }, - "spec": { - "foo": "bar", - "selector": { - "matchLabels": { - "app": "nginx" - } - }, - "template": { - "metadata": { - "labels": { - "app": "nginx" - } - }, - "spec": { - "containers": [{ - "name": "nginx", - "image": "nginx:latest" - }] - } - } - } - }`) - - var testcases = []struct { - name string - // TODO: use PostOptions for fieldValidation param instead of raw strings. - params map[string]string - errContains string - }{ - { - name: "putStrictValidation", - params: map[string]string{"fieldValidation": "Strict"}, - errContains: "found unknown field", - }, - { - name: "putDefaultIgnoreValidation", - params: map[string]string{}, - errContains: "", - }, - { - name: "putIgnoreValidation", - params: map[string]string{"fieldValidation": "Ignore"}, - errContains: "", - }, - } - - 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) - } - for _, tc := range testcases { - t.Run(tc.name, func(t *testing.T) { - req := client.CoreV1().RESTClient().Put(). - AbsPath("/apis/apps/v1"). - Namespace("default"). - Resource("deployments"). - Name("test-dep") - for k, v := range tc.params { - req.Param(k, v) - - } - result, err := req.Body(putBody).DoRaw(context.TODO()) - if err == nil && tc.errContains != "" { - t.Fatalf("unexpected post succeeded") - - } - if err != nil && !strings.Contains(string(result), tc.errContains) { - t.Fatalf("unexpected response: %v", string(result)) - - } - }) - - } - -} -func TestFieldValidationPost(t *testing.T) { - defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.ServerSideApply, true)() - - _, client, closeFn := setup(t) - defer closeFn() - - body := []byte(`{ - "apiVersion": "apps/v1", - "kind": "Deployment", - "metadata": { - "name": "test-dep", - "labels": {"app": "nginx"} - }, - "spec": { - "foo": "bar", - "selector": { - "matchLabels": { - "app": "nginx" - } - }, - "template": { - "metadata": { - "labels": { - "app": "nginx" - } - }, - "spec": { - "containers": [{ - "name": "nginx", - "image": "nginx:latest" - }] - } - } - } - }`) - - var testcases = []struct { - name string - // TODO: use PostOptions for fieldValidation param instead of raw strings. - params map[string]string - errContains string - }{ - { - name: "postStrictValidation", - params: map[string]string{"fieldValidation": "Strict"}, - errContains: "found unknown field", - }, - { - name: "postDefaultIgnoreValidation", - params: map[string]string{}, - errContains: "", - }, - { - name: "postIgnoreValidation", - params: map[string]string{"fieldValidation": "Ignore"}, - errContains: "", - }, - } - - for _, tc := range testcases { - t.Run(tc.name, func(t *testing.T) { - req := client.CoreV1().RESTClient().Post(). - AbsPath("/apis/apps/v1"). - Namespace("default"). - Resource("deployments") - for k, v := range tc.params { - req.Param(k, v) - - } - result, err := req.Body([]byte(body)).DoRaw(context.TODO()) - if err == nil && tc.errContains != "" { - t.Fatalf("unexpected post succeeded") - } - if err != nil && !strings.Contains(string(result), tc.errContains) { - t.Fatalf("unexpected response: %v", string(result)) - } - }) - } -} - -// Benchmark field validation for strict vs non-strict -func BenchmarkFieldValidation(b *testing.B) { - _, client, closeFn := setup(b) - defer closeFn() - - flag.Lookup("v").Value.Set("0") - corePath := "/api/v1" - appsPath := "/apis/apps/v1" - // TODO: split POST and PUT into their own test-cases. - // TODO: add test for "Warn" validation once it is implemented. - benchmarks := []struct { - name string - params map[string]string - bodyFile string - resource string - absPath string - }{ - { - name: "ignore-validation-deployment", - params: map[string]string{"fieldValidation": "Ignore"}, - bodyFile: "./testdata/deploy-small.json", - resource: "deployments", - absPath: appsPath, - }, - { - name: "strict-validation-deployment", - params: map[string]string{"fieldValidation": "Strict"}, - bodyFile: "./testdata/deploy-small.json", - resource: "deployments", - absPath: appsPath, - }, - { - name: "ignore-validation-pod", - params: map[string]string{"fieldValidation": "Ignore"}, - bodyFile: "./testdata/pod-medium.json", - resource: "pods", - absPath: corePath, - }, - { - name: "strict-validation-pod", - params: map[string]string{"fieldValidation": "Strict"}, - bodyFile: "./testdata/pod-medium.json", - resource: "pods", - absPath: corePath, - }, - { - name: "ignore-validation-big-pod", - params: map[string]string{"fieldValidation": "Ignore"}, - bodyFile: "./testdata/pod-large.json", - resource: "pods", - absPath: corePath, - }, - { - name: "strict-validation-big-pod", - params: map[string]string{"fieldValidation": "Strict"}, - bodyFile: "./testdata/pod-large.json", - resource: "pods", - absPath: corePath, - }, - } - - for _, bm := range benchmarks { - b.Run(bm.name, func(b *testing.B) { - b.ResetTimer() - b.ReportAllocs() - for n := 0; n < b.N; n++ { - // append the timestamp to the name so that we don't hit conflicts when running the test multiple times - // (i.e. without it -count=n for n>1 will fail, this might be from not tearing stuff down properly). - bodyBase, err := os.ReadFile(bm.bodyFile) - if err != nil { - panic(err) - } - - objName := fmt.Sprintf("obj-%s-%d-%d-%d", bm.name, n, b.N, time.Now().UnixNano()) - objString := fmt.Sprintf(string(bodyBase), fmt.Sprintf(`"%s"`, objName)) - body := []byte(objString) - - postReq := client.CoreV1().RESTClient().Post(). - AbsPath(bm.absPath). - Namespace("default"). - Resource(bm.resource) - for k, v := range bm.params { - postReq = postReq.Param(k, v) - } - - _, err = postReq.Body(body). - DoRaw(context.TODO()) - if err != nil { - panic(err) - } - - // TODO: put PUT in a different bench case than POST (ie. have a baseReq) be a part of the test case. - putReq := client.CoreV1().RESTClient().Put(). - AbsPath(bm.absPath). - Namespace("default"). - Resource(bm.resource). - Name(objName) - for k, v := range bm.params { - putReq = putReq.Param(k, v) - } - - _, err = putReq.Body(body). - DoRaw(context.TODO()) - if err != nil { - panic(err) - } - } - }) - - } -} - type dependentClient struct { t *testing.T client dynamic.ResourceInterface diff --git a/test/integration/apiserver/field_validation_test.go b/test/integration/apiserver/field_validation_test.go index 2c8ce2a6277e3..0c99b6dc91d5f 100644 --- a/test/integration/apiserver/field_validation_test.go +++ b/test/integration/apiserver/field_validation_test.go @@ -19,10 +19,12 @@ package apiserver import ( "context" "encoding/json" + "flag" "fmt" "os" "strings" "testing" + "time" apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" apiextensionsclient "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset" @@ -39,6 +41,249 @@ import ( "k8s.io/kubernetes/test/integration/framework" ) +func TestFieldValidationPut(t *testing.T) { + defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.ServerSideApply, true)() + + _, client, closeFn := setup(t) + defer closeFn() + + deployName := `"test-deployment"` + postBytes, err := os.ReadFile("./testdata/deploy-small.json") + if err != nil { + t.Fatalf("failed to read file: %v", err) + } + postBody := []byte(fmt.Sprintf(string(postBytes), 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) + } + + putBytes, err := os.ReadFile("./testdata/deploy-small-unknown-field.json") + if err != nil { + t.Fatalf("failed to read file: %v", err) + } + putBody := []byte(fmt.Sprintf(string(putBytes), deployName)) + var testcases = []struct { + name string + // TODO: use PostOptions for fieldValidation param instead of raw strings. + params map[string]string + errContains string + }{ + { + name: "put-strict-validation", + params: map[string]string{"fieldValidation": "Strict"}, + errContains: "found unknown field", + }, + { + name: "put-default-ignore-validation", + params: map[string]string{}, + errContains: "", + }, + { + name: "put-ignore-validation", + params: map[string]string{"fieldValidation": "Ignore"}, + errContains: "", + }, + } + + for _, tc := range testcases { + t.Run(tc.name, func(t *testing.T) { + req := client.CoreV1().RESTClient().Put(). + AbsPath("/apis/apps/v1"). + Namespace("default"). + Resource("deployments"). + Name("test-dep") + for k, v := range tc.params { + req.Param(k, v) + + } + result, err := req.Body(putBody).DoRaw(context.TODO()) + if err == nil && tc.errContains != "" { + t.Fatalf("unexpected post succeeded") + + } + if err != nil && !strings.Contains(string(result), tc.errContains) { + t.Fatalf("unexpected response: %v", string(result)) + + } + }) + + } + +} +func TestFieldValidationPost(t *testing.T) { + defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.ServerSideApply, true)() + + _, client, closeFn := setup(t) + defer closeFn() + + bodyBytes, err := os.ReadFile("./testdata/deploy-small-unknown-field.json") + if err != nil { + t.Fatalf("failed to read file: %v", err) + } + body := []byte(fmt.Sprintf(string(bodyBytes), `"test-deployment"`)) + + var testcases = []struct { + name string + // TODO: use PostOptions for fieldValidation param instead of raw strings. + params map[string]string + errContains string + }{ + { + name: "post-strict-validation", + params: map[string]string{"fieldValidation": "Strict"}, + errContains: "found unknown field", + }, + { + name: "post-default-ignore-validation", + params: map[string]string{}, + errContains: "", + }, + { + name: "post-ignore-validation", + params: map[string]string{"fieldValidation": "Ignore"}, + errContains: "", + }, + } + + for _, tc := range testcases { + t.Run(tc.name, func(t *testing.T) { + req := client.CoreV1().RESTClient().Post(). + AbsPath("/apis/apps/v1"). + Namespace("default"). + Resource("deployments") + for k, v := range tc.params { + req.Param(k, v) + + } + result, err := req.Body([]byte(body)).DoRaw(context.TODO()) + if err == nil && tc.errContains != "" { + t.Fatalf("unexpected post succeeded") + } + if err != nil && !strings.Contains(string(result), tc.errContains) { + t.Fatalf("unexpected response: %v", string(result)) + } + }) + } +} + +// Benchmark field validation for strict vs non-strict +func BenchmarkFieldValidation(b *testing.B) { + _, client, closeFn := setup(b) + defer closeFn() + + flag.Lookup("v").Value.Set("0") + corePath := "/api/v1" + appsPath := "/apis/apps/v1" + // TODO: split POST and PUT into their own test-cases. + // TODO: add test for "Warn" validation once it is implemented. + benchmarks := []struct { + name string + params map[string]string + bodyFile string + resource string + absPath string + }{ + { + name: "ignore-validation-deployment", + params: map[string]string{"fieldValidation": "Ignore"}, + bodyFile: "./testdata/deploy-small.json", + resource: "deployments", + absPath: appsPath, + }, + { + name: "strict-validation-deployment", + params: map[string]string{"fieldValidation": "Strict"}, + bodyFile: "./testdata/deploy-small.json", + resource: "deployments", + absPath: appsPath, + }, + { + name: "ignore-validation-pod", + params: map[string]string{"fieldValidation": "Ignore"}, + bodyFile: "./testdata/pod-medium.json", + resource: "pods", + absPath: corePath, + }, + { + name: "strict-validation-pod", + params: map[string]string{"fieldValidation": "Strict"}, + bodyFile: "./testdata/pod-medium.json", + resource: "pods", + absPath: corePath, + }, + { + name: "ignore-validation-big-pod", + params: map[string]string{"fieldValidation": "Ignore"}, + bodyFile: "./testdata/pod-large.json", + resource: "pods", + absPath: corePath, + }, + { + name: "strict-validation-big-pod", + params: map[string]string{"fieldValidation": "Strict"}, + bodyFile: "./testdata/pod-large.json", + resource: "pods", + absPath: corePath, + }, + } + + for _, bm := range benchmarks { + b.Run(bm.name, func(b *testing.B) { + b.ResetTimer() + b.ReportAllocs() + for n := 0; n < b.N; n++ { + // append the timestamp to the name so that we don't hit conflicts when running the test multiple times + // (i.e. without it -count=n for n>1 will fail, this might be from not tearing stuff down properly). + bodyBase, err := os.ReadFile(bm.bodyFile) + if err != nil { + panic(err) + } + + objName := fmt.Sprintf("obj-%s-%d-%d-%d", bm.name, n, b.N, time.Now().UnixNano()) + objString := fmt.Sprintf(string(bodyBase), fmt.Sprintf(`"%s"`, objName)) + body := []byte(objString) + + postReq := client.CoreV1().RESTClient().Post(). + AbsPath(bm.absPath). + Namespace("default"). + Resource(bm.resource) + for k, v := range bm.params { + postReq = postReq.Param(k, v) + } + + _, err = postReq.Body(body). + DoRaw(context.TODO()) + if err != nil { + panic(err) + } + + // TODO: put PUT in a different bench case than POST (ie. have a baseReq) be a part of the test case. + putReq := client.CoreV1().RESTClient().Put(). + AbsPath(bm.absPath). + Namespace("default"). + Resource(bm.resource). + Name(objName) + for k, v := range bm.params { + putReq = putReq.Param(k, v) + } + + _, err = putReq.Body(body). + DoRaw(context.TODO()) + if err != nil { + panic(err) + } + } + }) + + } +} + // smpTestSetup applies an object that will later be patched // in the actual test/benchmark. func smpTestSetup(t testing.TB, client clientset.Interface) { From a2f0549b8932fe884d0deef8716d140f28513f62 Mon Sep 17 00:00:00 2001 From: Kevin Delgado Date: Tue, 12 Oct 2021 22:12:52 +0000 Subject: [PATCH 38/64] rename BenchmarkFieldValidationPostPut --- test/integration/apiserver/field_validation_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/integration/apiserver/field_validation_test.go b/test/integration/apiserver/field_validation_test.go index 0c99b6dc91d5f..8b9aad701e4fe 100644 --- a/test/integration/apiserver/field_validation_test.go +++ b/test/integration/apiserver/field_validation_test.go @@ -173,7 +173,7 @@ func TestFieldValidationPost(t *testing.T) { } // Benchmark field validation for strict vs non-strict -func BenchmarkFieldValidation(b *testing.B) { +func BenchmarkFieldValidationPostPut(b *testing.B) { _, client, closeFn := setup(b) defer closeFn() From d60725883917ce086537ab0add6c102ab491cc1e Mon Sep 17 00:00:00 2001 From: Kevin Delgado Date: Tue, 12 Oct 2021 22:18:11 +0000 Subject: [PATCH 39/64] fix panics, add commentary --- .../apiserver/field_validation_test.go | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/test/integration/apiserver/field_validation_test.go b/test/integration/apiserver/field_validation_test.go index 8b9aad701e4fe..4dd9b386e10f0 100644 --- a/test/integration/apiserver/field_validation_test.go +++ b/test/integration/apiserver/field_validation_test.go @@ -41,6 +41,8 @@ import ( "k8s.io/kubernetes/test/integration/framework" ) +// TestFieldValidationPut tests PUT requests containing unknown fields with +// strict and non-strict field validation. func TestFieldValidationPut(t *testing.T) { defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.ServerSideApply, true)() @@ -116,6 +118,9 @@ func TestFieldValidationPut(t *testing.T) { } } + +// TestFieldValidationPost tests POST requests containing unknown fields with +// strict and non-strict field validation. func TestFieldValidationPost(t *testing.T) { defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.ServerSideApply, true)() @@ -242,7 +247,7 @@ func BenchmarkFieldValidationPostPut(b *testing.B) { // (i.e. without it -count=n for n>1 will fail, this might be from not tearing stuff down properly). bodyBase, err := os.ReadFile(bm.bodyFile) if err != nil { - panic(err) + b.Fatal(err) } objName := fmt.Sprintf("obj-%s-%d-%d-%d", bm.name, n, b.N, time.Now().UnixNano()) @@ -260,7 +265,7 @@ func BenchmarkFieldValidationPostPut(b *testing.B) { _, err = postReq.Body(body). DoRaw(context.TODO()) if err != nil { - panic(err) + b.Fatal(err) } // TODO: put PUT in a different bench case than POST (ie. have a baseReq) be a part of the test case. @@ -276,7 +281,7 @@ func BenchmarkFieldValidationPostPut(b *testing.B) { _, err = putReq.Body(body). DoRaw(context.TODO()) if err != nil { - panic(err) + b.Fatal(err) } } }) @@ -525,6 +530,7 @@ func TestFieldValidationPatchCRD(t *testing.T) { } } +// Benchmark patch CRD for strict vs non-strict func BenchmarkFieldValidationPatchCRD(b *testing.B) { benchmarks := []struct { name string @@ -588,10 +594,10 @@ func BenchmarkFieldValidationPatchCRD(b *testing.B) { Body([]byte(body)). DoRaw(context.TODO()) if err == nil && bm.errContains != "" { - panic(fmt.Sprintf("unexpected patch succeeded, expected %s", bm.errContains)) + b.Fatalf("unexpected patch succeeded, expected %s", bm.errContains) } if err != nil && !strings.Contains(string(result), bm.errContains) { - panic(err) + b.Fatal(err) } } }) From d5fec063c77da87e483eaf6eb1e40633275b2e1f Mon Sep 17 00:00:00 2001 From: Kevin Delgado Date: Wed, 13 Oct 2021 20:25:35 +0000 Subject: [PATCH 40/64] default to strict decoder, use in create handler --- .../pkg/runtime/serializer/codec_factory.go | 4 ++- .../pkg/endpoints/handlers/create.go | 29 ++++++++++--------- .../apiserver/field_validation_test.go | 4 +-- 3 files changed, 20 insertions(+), 17 deletions(-) diff --git a/staging/src/k8s.io/apimachinery/pkg/runtime/serializer/codec_factory.go b/staging/src/k8s.io/apimachinery/pkg/runtime/serializer/codec_factory.go index 12cab8d82bd57..28146e74365f9 100644 --- a/staging/src/k8s.io/apimachinery/pkg/runtime/serializer/codec_factory.go +++ b/staging/src/k8s.io/apimachinery/pkg/runtime/serializer/codec_factory.go @@ -169,7 +169,9 @@ func DisableStrict(options *CodecFactoryOptions) { // TODO: allow other codecs to be compiled in? // TODO: accept a scheme interface func NewCodecFactory(scheme *runtime.Scheme, mutators ...CodecFactoryOptionsMutator) CodecFactory { - options := CodecFactoryOptions{Pretty: true} + // default to strict decoding, callers of decode are responsible for + // distinguishing fatal decoding errors from strict decoding errors. + options := CodecFactoryOptions{Pretty: true, Strict: true} for _, fn := range mutators { fn(&options) } diff --git a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/create.go b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/create.go index b14a9db9d27f0..a74dfa043b72a 100644 --- a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/create.go +++ b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/create.go @@ -92,17 +92,7 @@ func createHandler(r rest.NamedCreater, scope *RequestScope, admit admission.Int return } - decodeSerializer := s.Serializer - // TODO: put behind feature gate? - validationDirective, err := fieldValidation(req) - if err != nil { - scope.err(err, w, req) - return - } - if validationDirective == strictFieldValidation { - decodeSerializer = s.StrictSerializer - } - decoder := scope.Serializer.DecoderToVersion(decodeSerializer, scope.HubGroupVersion) + decoder := scope.Serializer.DecoderToVersion(s.Serializer, scope.HubGroupVersion) body, err := limitedReadBody(req, scope.MaxRequestBodyBytes) if err != nil { @@ -126,14 +116,25 @@ func createHandler(r rest.NamedCreater, scope *RequestScope, admit admission.Int defaultGVK := scope.Kind original := r.New() - trace.Step("About to convert to expected version") - obj, gvk, err := decoder.Decode(body, &defaultGVK, original) + validationDirective, err := fieldValidation(req) if err != nil { - err = transformDecodeError(scope.Typer, err, original, gvk, body) scope.err(err, w, req) return } + trace.Step("About to convert to expected version") + obj, gvk, err := decoder.Decode(body, &defaultGVK, original) + if err != nil { + if !runtime.IsStrictDecodingError(err) || validationDirective == strictFieldValidation { + err = transformDecodeError(scope.Typer, err, original, gvk, body) + scope.err(err, w, req) + return + } + if validationDirective == warnFieldValidation { + // TODO: throw a warning here + } + } + objGV := gvk.GroupVersion() if !scope.AcceptsGroupVersion(objGV) { err = errors.NewBadRequest(fmt.Sprintf("the API version in the data (%s) does not match the expected API version (%v)", objGV.String(), gv.String())) diff --git a/test/integration/apiserver/field_validation_test.go b/test/integration/apiserver/field_validation_test.go index 4dd9b386e10f0..f4220a2745458 100644 --- a/test/integration/apiserver/field_validation_test.go +++ b/test/integration/apiserver/field_validation_test.go @@ -79,7 +79,7 @@ func TestFieldValidationPut(t *testing.T) { { name: "put-strict-validation", params: map[string]string{"fieldValidation": "Strict"}, - errContains: "found unknown field", + errContains: "unknown field", }, { name: "put-default-ignore-validation", @@ -142,7 +142,7 @@ func TestFieldValidationPost(t *testing.T) { { name: "post-strict-validation", params: map[string]string{"fieldValidation": "Strict"}, - errContains: "found unknown field", + errContains: "unknown field", }, { name: "post-default-ignore-validation", From c077c26c2e01ba632e8d67382473d0ba8368e211 Mon Sep 17 00:00:00 2001 From: Kevin Delgado Date: Wed, 13 Oct 2021 21:15:18 +0000 Subject: [PATCH 41/64] default strict decoder in update handler --- .../pkg/endpoints/handlers/create.go | 5 +++-- .../pkg/endpoints/handlers/update.go | 21 +++++++++++-------- 2 files changed, 15 insertions(+), 11 deletions(-) diff --git a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/create.go b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/create.go index a74dfa043b72a..400a93297a184 100644 --- a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/create.go +++ b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/create.go @@ -92,8 +92,6 @@ func createHandler(r rest.NamedCreater, scope *RequestScope, admit admission.Int return } - decoder := scope.Serializer.DecoderToVersion(s.Serializer, scope.HubGroupVersion) - body, err := limitedReadBody(req, scope.MaxRequestBodyBytes) if err != nil { scope.err(err, w, req) @@ -116,12 +114,15 @@ func createHandler(r rest.NamedCreater, scope *RequestScope, admit admission.Int defaultGVK := scope.Kind original := r.New() + + // TODO: put behind feature gate? validationDirective, err := fieldValidation(req) if err != nil { scope.err(err, w, req) return } + decoder := scope.Serializer.DecoderToVersion(s.Serializer, scope.HubGroupVersion) trace.Step("About to convert to expected version") obj, gvk, err := decoder.Decode(body, &defaultGVK, original) if err != nil { diff --git a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/update.go b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/update.go index 1d012318a3f5d..cece289c24946 100644 --- a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/update.go +++ b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/update.go @@ -102,24 +102,27 @@ func UpdateResource(r rest.Updater, scope *RequestScope, admit admission.Interfa defaultGVK := scope.Kind original := r.New() - trace.Step("About to convert to expected version") - decodeSerializer := s.Serializer // TODO: put behind feature gate? validationDirective, err := fieldValidation(req) if err != nil { scope.err(err, w, req) return } - if validationDirective == strictFieldValidation { - decodeSerializer = s.StrictSerializer - } - decoder := scope.Serializer.DecoderToVersion(decodeSerializer, scope.HubGroupVersion) + + decoder := scope.Serializer.DecoderToVersion(s.Serializer, scope.HubGroupVersion) + trace.Step("About to convert to expected version") obj, gvk, err := decoder.Decode(body, &defaultGVK, original) if err != nil { - err = transformDecodeError(scope.Typer, err, original, gvk, body) - scope.err(err, w, req) - return + if !runtime.IsStrictDecodingError(err) || validationDirective == strictFieldValidation { + err = transformDecodeError(scope.Typer, err, original, gvk, body) + scope.err(err, w, req) + return + } + if validationDirective == warnFieldValidation { + // TODO: throw a warning here + } } + objGV := gvk.GroupVersion() if !scope.AcceptsGroupVersion(objGV) { err = errors.NewBadRequest(fmt.Sprintf("the API version in the data (%s) does not match the expected API version (%s)", objGV, defaultGVK.GroupVersion())) From 68fa22c5e87bcc0f80c3c931bef111dd43d05349 Mon Sep 17 00:00:00 2001 From: Kevin Delgado Date: Wed, 13 Oct 2021 21:48:13 +0000 Subject: [PATCH 42/64] bisect 1 --- staging/src/k8s.io/apimachinery/pkg/runtime/interfaces.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/staging/src/k8s.io/apimachinery/pkg/runtime/interfaces.go b/staging/src/k8s.io/apimachinery/pkg/runtime/interfaces.go index ecb68e529d187..56b28905429aa 100644 --- a/staging/src/k8s.io/apimachinery/pkg/runtime/interfaces.go +++ b/staging/src/k8s.io/apimachinery/pkg/runtime/interfaces.go @@ -126,7 +126,7 @@ type SerializerInfo struct { // readability. PrettySerializer Serializer // StrictSerializer errors on unknown fields when deserializing an object - StrictSerializer Serializer + //StrictSerializer Serializer // StreamSerializer, if set, describes the streaming serialization format // for this media type. StreamSerializer *StreamSerializerInfo From acc474a4d4b75f1e936bc863d71cd4ee49c3124d Mon Sep 17 00:00:00 2001 From: Kevin Delgado Date: Wed, 13 Oct 2021 21:48:28 +0000 Subject: [PATCH 43/64] bisect 2 --- .../pkg/runtime/serializer/codec_factory.go | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/staging/src/k8s.io/apimachinery/pkg/runtime/serializer/codec_factory.go b/staging/src/k8s.io/apimachinery/pkg/runtime/serializer/codec_factory.go index 28146e74365f9..77c329209bb43 100644 --- a/staging/src/k8s.io/apimachinery/pkg/runtime/serializer/codec_factory.go +++ b/staging/src/k8s.io/apimachinery/pkg/runtime/serializer/codec_factory.go @@ -40,7 +40,7 @@ type serializerType struct { Serializer runtime.Serializer PrettySerializer runtime.Serializer - StrictSerializer runtime.Serializer + //StrictSerializer runtime.Serializer AcceptStreamContentTypes []string StreamContentType string @@ -70,19 +70,19 @@ func newSerializersForScheme(scheme *runtime.Scheme, mf json.MetaFactory, option json.SerializerOptions{Yaml: false, Pretty: true, Strict: options.Strict}, ) } - strictJSONSerializer := json.NewSerializerWithOptions( - mf, scheme, scheme, - json.SerializerOptions{Yaml: false, Pretty: false, Strict: true}, - ) - jsonSerializerType.StrictSerializer = strictJSONSerializer + //strictJSONSerializer := json.NewSerializerWithOptions( + // mf, scheme, scheme, + // json.SerializerOptions{Yaml: false, Pretty: false, Strict: true}, + //) + //jsonSerializerType.StrictSerializer = strictJSONSerializer yamlSerializer := json.NewSerializerWithOptions( mf, scheme, scheme, json.SerializerOptions{Yaml: true, Pretty: false, Strict: options.Strict}, ) - strictYAMLSerializer := json.NewSerializerWithOptions( - mf, scheme, scheme, - json.SerializerOptions{Yaml: true, Pretty: false, Strict: true}, - ) + //strictYAMLSerializer := json.NewSerializerWithOptions( + // mf, scheme, scheme, + // json.SerializerOptions{Yaml: true, Pretty: false, Strict: true}, + //) protoSerializer := protobuf.NewSerializer(scheme, scheme) protoRawSerializer := protobuf.NewRawSerializer(scheme, scheme) @@ -94,7 +94,7 @@ func newSerializersForScheme(scheme *runtime.Scheme, mf json.MetaFactory, option FileExtensions: []string{"yaml"}, EncodesAsText: true, Serializer: yamlSerializer, - StrictSerializer: strictYAMLSerializer, + //StrictSerializer: strictYAMLSerializer, }, { AcceptContentTypes: []string{runtime.ContentTypeProtobuf}, @@ -199,7 +199,7 @@ func newCodecFactory(scheme *runtime.Scheme, serializers []serializerType) Codec EncodesAsText: d.EncodesAsText, Serializer: d.Serializer, PrettySerializer: d.PrettySerializer, - StrictSerializer: d.StrictSerializer, + //StrictSerializer: d.StrictSerializer, } mediaType, _, err := mime.ParseMediaType(info.MediaType) From 7a42d3ac6c938837d4c7c8869cfbba9cea70c685 Mon Sep 17 00:00:00 2001 From: Kevin Delgado Date: Wed, 13 Oct 2021 21:48:38 +0000 Subject: [PATCH 44/64] bisect 3 --- .../apiserver/pkg/endpoints/installer.go | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/staging/src/k8s.io/apiserver/pkg/endpoints/installer.go b/staging/src/k8s.io/apiserver/pkg/endpoints/installer.go index 14bc185aceb78..ad8bdba3c9656 100644 --- a/staging/src/k8s.io/apiserver/pkg/endpoints/installer.go +++ b/staging/src/k8s.io/apiserver/pkg/endpoints/installer.go @@ -569,15 +569,15 @@ func (a *APIInstaller) registerResourceHandlers(path string, storage rest.Storag kubeVerbs := map[string]struct{}{} reqScope := handlers.RequestScope{ - Serializer: a.group.Serializer, - StrictSerializer: a.group.Serializer, - ParameterCodec: a.group.ParameterCodec, - Creater: a.group.Creater, - Convertor: a.group.Convertor, - Defaulter: a.group.Defaulter, - Typer: a.group.Typer, - UnsafeConvertor: a.group.UnsafeConvertor, - Authorizer: a.group.Authorizer, + Serializer: a.group.Serializer, + //StrictSerializer: a.group.Serializer, + ParameterCodec: a.group.ParameterCodec, + Creater: a.group.Creater, + Convertor: a.group.Convertor, + Defaulter: a.group.Defaulter, + Typer: a.group.Typer, + UnsafeConvertor: a.group.UnsafeConvertor, + Authorizer: a.group.Authorizer, EquivalentResourceMapper: a.group.EquivalentResourceRegistry, From 5d75f7a9f4532694495c7f1d74793c02e06ea058 Mon Sep 17 00:00:00 2001 From: Kevin Delgado Date: Wed, 13 Oct 2021 22:14:54 +0000 Subject: [PATCH 45/64] revert removal of strict serializer because it breaks patch CRD --- .../apiserver/pkg/endpoints/installer.go | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/staging/src/k8s.io/apiserver/pkg/endpoints/installer.go b/staging/src/k8s.io/apiserver/pkg/endpoints/installer.go index ad8bdba3c9656..14bc185aceb78 100644 --- a/staging/src/k8s.io/apiserver/pkg/endpoints/installer.go +++ b/staging/src/k8s.io/apiserver/pkg/endpoints/installer.go @@ -569,15 +569,15 @@ func (a *APIInstaller) registerResourceHandlers(path string, storage rest.Storag kubeVerbs := map[string]struct{}{} reqScope := handlers.RequestScope{ - Serializer: a.group.Serializer, - //StrictSerializer: a.group.Serializer, - ParameterCodec: a.group.ParameterCodec, - Creater: a.group.Creater, - Convertor: a.group.Convertor, - Defaulter: a.group.Defaulter, - Typer: a.group.Typer, - UnsafeConvertor: a.group.UnsafeConvertor, - Authorizer: a.group.Authorizer, + Serializer: a.group.Serializer, + StrictSerializer: a.group.Serializer, + ParameterCodec: a.group.ParameterCodec, + Creater: a.group.Creater, + Convertor: a.group.Convertor, + Defaulter: a.group.Defaulter, + Typer: a.group.Typer, + UnsafeConvertor: a.group.UnsafeConvertor, + Authorizer: a.group.Authorizer, EquivalentResourceMapper: a.group.EquivalentResourceRegistry, From 15e6eb5be6c992183f62fe53bb3f75d77bc45d2f Mon Sep 17 00:00:00 2001 From: Kevin Delgado Date: Wed, 13 Oct 2021 22:59:34 +0000 Subject: [PATCH 46/64] bisect 1 --- .../pkg/apiserver/customresource_handler.go | 23 +++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/customresource_handler.go b/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/customresource_handler.go index e12f506ff5012..f76347fca2dce 100644 --- a/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/customresource_handler.go +++ b/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/customresource_handler.go @@ -17,6 +17,7 @@ limitations under the License. package apiserver import ( + "errors" "fmt" "net/http" "path" @@ -1247,6 +1248,9 @@ func (d schemaCoercingDecoder) Decode(data []byte, defaults *schema.GroupVersion } if u, ok := obj.(*unstructured.Unstructured); ok { if err := d.validator.apply(u); err != nil { + if runtime.IsStrictDecodingError(err) { + return obj, gvk, err + } return nil, gvk, err } } @@ -1355,13 +1359,15 @@ func (v *unstructuredSchemaCoercer) apply(u *unstructured.Unstructured) error { if err != nil { return err } + + pruned := map[string]bool{} if gv.Group == v.structuralSchemaGK.Group && kind == v.structuralSchemaGK.Kind { if v.unknownFieldsDirective != preserve { // TODO: switch over pruning and coercing at the root to schemaobjectmeta.Coerce too (I don't remember what this comment means anymore) - pruned := structuralpruning.Prune(u.Object, v.structuralSchemas[gv.Version], false) - if v.unknownFieldsDirective == fail && len(pruned) > 0 { - return fmt.Errorf("failed with unknown fields: %v", pruned) - } + pruned = structuralpruning.Prune(u.Object, v.structuralSchemas[gv.Version], false) + //if v.unknownFieldsDirective == fail && len(pruned) > 0 { + // return fmt.Errorf("failed with unknown fields: %v", pruned) + //} structuraldefaulting.PruneNonNullableNullsWithoutDefaults(u.Object, v.structuralSchemas[gv.Version]) } if err := schemaobjectmeta.Coerce(nil, u.Object, v.structuralSchemas[gv.Version], false, v.dropInvalidMetadata); err != nil { @@ -1384,6 +1390,15 @@ func (v *unstructuredSchemaCoercer) apply(u *unstructured.Unstructured) error { return err } } + if len(pruned) > 0 { + allStrictErrs := make([]error, len(pruned)) + i := 0 + for unknownField, _ := range pruned { + allStrictErrs[i] = errors.New(unknownField) + } + return runtime.NewStrictDecodingError(allStrictErrs) + + } return nil } From 892b2c0eb860d4b9b55a5fa80acdcec9d69c09eb Mon Sep 17 00:00:00 2001 From: Kevin Delgado Date: Wed, 13 Oct 2021 22:59:49 +0000 Subject: [PATCH 47/64] bisect 2 --- .../apiserver/pkg/endpoints/handlers/patch.go | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/patch.go b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/patch.go index ff4e802fe0dc6..e3ed6679d99be 100644 --- a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/patch.go +++ b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/patch.go @@ -229,6 +229,7 @@ func PatchResource(r rest.Patcher, scope *RequestScope, admit admission.Interfac subresource: scope.Subresource, dryRun: dryrun.IsDryRun(options.DryRun), strictFieldValidation: strictValidation, + validationDirective: validationDirective, objectInterfaces: scope, @@ -292,6 +293,7 @@ type patcher struct { subresource string dryRun bool strictFieldValidation bool + validationDirective fieldValidationDirective objectInterfaces admission.ObjectInterfaces @@ -349,9 +351,15 @@ func (p *jsonPatcher) applyPatchToCurrentObject(currentObject runtime.Object) (r // Construct the resulting typed, unversioned object. objToUpdate := p.restPatcher.New() if err := runtime.DecodeInto(p.codec, patchedObjJS, objToUpdate); err != nil { - return nil, errors.NewInvalid(schema.GroupKind{}, "", field.ErrorList{ - field.Invalid(field.NewPath("patch"), string(patchedObjJS), err.Error()), - }) + klog.Warningf("decode into err: %v", err) + if !runtime.IsStrictDecodingError(err) || p.validationDirective == strictFieldValidation { + return nil, errors.NewInvalid(schema.GroupKind{}, "", field.ErrorList{ + field.Invalid(field.NewPath("patch"), string(patchedObjJS), err.Error()), + }) + } + if p.validationDirective == warnFieldValidation { + // TODO: throw a warning here + } } if p.fieldManager != nil { From 17327a54c4b97a1e5baa29af5b68234e87d1a091 Mon Sep 17 00:00:00 2001 From: Kevin Delgado Date: Thu, 14 Oct 2021 01:25:03 +0000 Subject: [PATCH 48/64] wip, patch crd kinda working --- .../pkg/apiserver/customresource_handler.go | 36 +++++++++++++------ .../k8s.io/apimachinery/pkg/runtime/codec.go | 12 +++++++ .../serializer/versioning/versioning.go | 21 ++++++++--- .../apiserver/pkg/endpoints/handlers/patch.go | 1 + .../apiserver/field_validation_test.go | 2 +- 5 files changed, 56 insertions(+), 16 deletions(-) diff --git a/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/customresource_handler.go b/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/customresource_handler.go index f76347fca2dce..29d479a444706 100644 --- a/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/customresource_handler.go +++ b/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/customresource_handler.go @@ -17,10 +17,10 @@ limitations under the License. package apiserver import ( - "errors" "fmt" "net/http" "path" + goruntime "runtime" "sort" "strings" "sync" @@ -843,12 +843,13 @@ func (r *crdHandler) getOrCreateServingInfoFor(uid types.UID, name string) (*crd // CRDs explicitly do not support protobuf, but some objects returned by the API server do // TODO: we could have two serializers one with strict directive and one without? negotiatedSerializer := unstructuredNegotiatedSerializer{ - typer: typer, - creator: creator, - converter: safeConverter, - structuralSchemas: structuralSchemas, - structuralSchemaGK: kind.GroupKind(), - unknownFieldsDirective: makeUnknownFieldsDirective(crd.Spec.PreserveUnknownFields, false), + typer: typer, + creator: creator, + converter: safeConverter, + structuralSchemas: structuralSchemas, + structuralSchemaGK: kind.GroupKind(), + //unknownFieldsDirective: makeUnknownFieldsDirective(crd.Spec.PreserveUnknownFields, false), + unknownFieldsDirective: makeUnknownFieldsDirective(crd.Spec.PreserveUnknownFields, true), } strictNegotiatedSerializer := unstructuredNegotiatedSerializer{ typer: typer, @@ -1242,6 +1243,10 @@ type schemaCoercingDecoder struct { var _ runtime.Decoder = schemaCoercingDecoder{} func (d schemaCoercingDecoder) Decode(data []byte, defaults *schema.GroupVersionKind, into runtime.Object) (runtime.Object, *schema.GroupVersionKind, error) { + _, file, no, ok := goruntime.Caller(1) + if ok { + klog.Warningf("called from %s#%d\n", file, no) + } obj, gvk, err := d.delegate.Decode(data, defaults, into) if err != nil { return nil, gvk, err @@ -1249,6 +1254,7 @@ func (d schemaCoercingDecoder) Decode(data []byte, defaults *schema.GroupVersion if u, ok := obj.(*unstructured.Unstructured); ok { if err := d.validator.apply(u); err != nil { if runtime.IsStrictDecodingError(err) { + klog.Warningf("sCD Decode err %v", err) return obj, gvk, err } return nil, gvk, err @@ -1361,10 +1367,12 @@ func (v *unstructuredSchemaCoercer) apply(u *unstructured.Unstructured) error { } pruned := map[string]bool{} + klog.Warningf("applying directive: %d", v.unknownFieldsDirective) if gv.Group == v.structuralSchemaGK.Group && kind == v.structuralSchemaGK.Kind { if v.unknownFieldsDirective != preserve { // TODO: switch over pruning and coercing at the root to schemaobjectmeta.Coerce too (I don't remember what this comment means anymore) pruned = structuralpruning.Prune(u.Object, v.structuralSchemas[gv.Version], false) + klog.Warningf("pruned: %v", pruned) //if v.unknownFieldsDirective == fail && len(pruned) > 0 { // return fmt.Errorf("failed with unknown fields: %v", pruned) //} @@ -1390,14 +1398,20 @@ func (v *unstructuredSchemaCoercer) apply(u *unstructured.Unstructured) error { return err } } - if len(pruned) > 0 { + if len(pruned) > 0 && v.unknownFieldsDirective == fail { + klog.Warningf("end of apply pruned len: %d", len(pruned)) + klog.Warningf("pruned: %v", pruned) + klog.Warningf("directive: %d", v.unknownFieldsDirective) allStrictErrs := make([]error, len(pruned)) i := 0 for unknownField, _ := range pruned { - allStrictErrs[i] = errors.New(unknownField) + allStrictErrs[i] = fmt.Errorf("unknown field: %s", unknownField) + i++ } - return runtime.NewStrictDecodingError(allStrictErrs) - + klog.Warningf("all strict errs: %v", allStrictErrs) + err := runtime.NewStrictDecodingError(allStrictErrs) + klog.Warningf("apply return decoding err: %v", err) + return err } return nil diff --git a/staging/src/k8s.io/apimachinery/pkg/runtime/codec.go b/staging/src/k8s.io/apimachinery/pkg/runtime/codec.go index a92863139ede3..baf878c69967a 100644 --- a/staging/src/k8s.io/apimachinery/pkg/runtime/codec.go +++ b/staging/src/k8s.io/apimachinery/pkg/runtime/codec.go @@ -55,14 +55,26 @@ func Encode(e Encoder, obj Object) ([]byte, error) { // Decode is a convenience wrapper for decoding data into an Object. func Decode(d Decoder, data []byte) (Object, error) { + klog.Warningf("Decode wrapper") obj, _, err := d.Decode(data, nil, nil) + klog.Warningf("back from decode wrapper") + if IsStrictDecodingError(err) { + klog.Warningf("got a strict error") + return obj, nil + } return obj, err } // DecodeInto performs a Decode into the provided object. func DecodeInto(d Decoder, data []byte, into Object) error { + klog.Warningf("DecodeInto") out, gvk, err := d.Decode(data, nil, into) + klog.Warningf("back from DecodeInto") if err != nil { + klog.Warningf("this the one 1") + if IsStrictDecodingError(err) { + klog.Warningf("strict decode err") + } return err } if out != into { diff --git a/staging/src/k8s.io/apimachinery/pkg/runtime/serializer/versioning/versioning.go b/staging/src/k8s.io/apimachinery/pkg/runtime/serializer/versioning/versioning.go index 718c5dfb7df75..d27232723c47d 100644 --- a/staging/src/k8s.io/apimachinery/pkg/runtime/serializer/versioning/versioning.go +++ b/staging/src/k8s.io/apimachinery/pkg/runtime/serializer/versioning/versioning.go @@ -20,6 +20,7 @@ import ( "encoding/json" "io" "reflect" + goruntime "runtime" "sync" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" @@ -124,6 +125,10 @@ func identifier(encodeGV runtime.GroupVersioner, encoder runtime.Encoder) runtim // successful, the returned runtime.Object will be the value passed as into. Note that this may bypass conversion if you pass an // into that matches the serialized version. func (c *codec) Decode(data []byte, defaultGVK *schema.GroupVersionKind, into runtime.Object) (runtime.Object, *schema.GroupVersionKind, error) { + _, file, no, ok := goruntime.Caller(1) + if ok { + klog.Warningf("called from %s#%d\n", file, no) + } // If the into object is unstructured and expresses an opinion about its group/version, // create a new instance of the type so we always exercise the conversion path (skips short-circuiting on `into == obj`) decodeInto := into @@ -133,10 +138,15 @@ func (c *codec) Decode(data []byte, defaultGVK *schema.GroupVersionKind, into ru } } + klog.Warningf("versionging decode") obj, gvk, err := c.decoder.Decode(data, defaultGVK, decodeInto) - if err != nil { + klog.Warningf("back from versionging decode: %v", err) + if err != nil && !runtime.IsStrictDecodingError(err) { + klog.Warningf("not a strict err") return nil, gvk, err } + strictDecodingErr := err + klog.Warningf("set SDE: %v", strictDecodingErr) if d, ok := obj.(runtime.NestedObjectDecoder); ok { if err := d.DecodeNestedObjects(runtime.WithoutVersionDecoder{c.decoder}); err != nil { @@ -153,14 +163,16 @@ func (c *codec) Decode(data []byte, defaultGVK *schema.GroupVersionKind, into ru // Short-circuit conversion if the into object is same object if into == obj { - return into, gvk, nil + klog.Warningf("short circuit into loop 1 sDE: %v", strictDecodingErr) + return into, gvk, strictDecodingErr } if err := c.convertor.Convert(obj, into, c.decodeVersion); err != nil { return nil, gvk, err } - return into, gvk, nil + klog.Warningf("short circuit into loop 2 sDE: %v", strictDecodingErr) + return into, gvk, strictDecodingErr } // perform defaulting if requested @@ -172,7 +184,8 @@ func (c *codec) Decode(data []byte, defaultGVK *schema.GroupVersionKind, into ru if err != nil { return nil, gvk, err } - return out, gvk, nil + klog.Warningf("returning with sDE: %v", strictDecodingErr) + return out, gvk, strictDecodingErr } // Encode ensures the provided object is output in the appropriate group and version, invoking diff --git a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/patch.go b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/patch.go index e3ed6679d99be..8f0ac8dc1e2c0 100644 --- a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/patch.go +++ b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/patch.go @@ -353,6 +353,7 @@ func (p *jsonPatcher) applyPatchToCurrentObject(currentObject runtime.Object) (r if err := runtime.DecodeInto(p.codec, patchedObjJS, objToUpdate); err != nil { klog.Warningf("decode into err: %v", err) if !runtime.IsStrictDecodingError(err) || p.validationDirective == strictFieldValidation { + klog.Warningf("yes invalid err") return nil, errors.NewInvalid(schema.GroupKind{}, "", field.ErrorList{ field.Invalid(field.NewPath("patch"), string(patchedObjJS), err.Error()), }) diff --git a/test/integration/apiserver/field_validation_test.go b/test/integration/apiserver/field_validation_test.go index f4220a2745458..69dcf43104352 100644 --- a/test/integration/apiserver/field_validation_test.go +++ b/test/integration/apiserver/field_validation_test.go @@ -474,7 +474,7 @@ func TestFieldValidationPatchCRD(t *testing.T) { patchType: types.MergePatchType, params: map[string]string{"fieldValidation": "Strict"}, body: `{"metadata":{"finalizers":["test-finalizer","another-one"]}, "spec":{"foo": "bar"}}`, - errContains: "failed with unknown fields", + errContains: "unknown field", }, { name: "merge-patch-no-validation", From 8c67e1ea4a9ba439c5af395c2b75c7672865a4f8 Mon Sep 17 00:00:00 2001 From: Kevin Delgado Date: Thu, 14 Oct 2021 02:00:49 +0000 Subject: [PATCH 49/64] wip, checkpoint patch crd cleanup --- .../src/k8s.io/apimachinery/pkg/runtime/codec.go | 12 ------------ .../pkg/runtime/serializer/versioning/versioning.go | 13 +------------ .../apiserver/pkg/endpoints/handlers/patch.go | 2 -- 3 files changed, 1 insertion(+), 26 deletions(-) diff --git a/staging/src/k8s.io/apimachinery/pkg/runtime/codec.go b/staging/src/k8s.io/apimachinery/pkg/runtime/codec.go index baf878c69967a..a92863139ede3 100644 --- a/staging/src/k8s.io/apimachinery/pkg/runtime/codec.go +++ b/staging/src/k8s.io/apimachinery/pkg/runtime/codec.go @@ -55,26 +55,14 @@ func Encode(e Encoder, obj Object) ([]byte, error) { // Decode is a convenience wrapper for decoding data into an Object. func Decode(d Decoder, data []byte) (Object, error) { - klog.Warningf("Decode wrapper") obj, _, err := d.Decode(data, nil, nil) - klog.Warningf("back from decode wrapper") - if IsStrictDecodingError(err) { - klog.Warningf("got a strict error") - return obj, nil - } return obj, err } // DecodeInto performs a Decode into the provided object. func DecodeInto(d Decoder, data []byte, into Object) error { - klog.Warningf("DecodeInto") out, gvk, err := d.Decode(data, nil, into) - klog.Warningf("back from DecodeInto") if err != nil { - klog.Warningf("this the one 1") - if IsStrictDecodingError(err) { - klog.Warningf("strict decode err") - } return err } if out != into { diff --git a/staging/src/k8s.io/apimachinery/pkg/runtime/serializer/versioning/versioning.go b/staging/src/k8s.io/apimachinery/pkg/runtime/serializer/versioning/versioning.go index d27232723c47d..a8d43493e9064 100644 --- a/staging/src/k8s.io/apimachinery/pkg/runtime/serializer/versioning/versioning.go +++ b/staging/src/k8s.io/apimachinery/pkg/runtime/serializer/versioning/versioning.go @@ -20,7 +20,6 @@ import ( "encoding/json" "io" "reflect" - goruntime "runtime" "sync" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" @@ -125,10 +124,6 @@ func identifier(encodeGV runtime.GroupVersioner, encoder runtime.Encoder) runtim // successful, the returned runtime.Object will be the value passed as into. Note that this may bypass conversion if you pass an // into that matches the serialized version. func (c *codec) Decode(data []byte, defaultGVK *schema.GroupVersionKind, into runtime.Object) (runtime.Object, *schema.GroupVersionKind, error) { - _, file, no, ok := goruntime.Caller(1) - if ok { - klog.Warningf("called from %s#%d\n", file, no) - } // If the into object is unstructured and expresses an opinion about its group/version, // create a new instance of the type so we always exercise the conversion path (skips short-circuiting on `into == obj`) decodeInto := into @@ -138,15 +133,12 @@ func (c *codec) Decode(data []byte, defaultGVK *schema.GroupVersionKind, into ru } } - klog.Warningf("versionging decode") obj, gvk, err := c.decoder.Decode(data, defaultGVK, decodeInto) - klog.Warningf("back from versionging decode: %v", err) if err != nil && !runtime.IsStrictDecodingError(err) { - klog.Warningf("not a strict err") return nil, gvk, err } + // save the strictDecodingError and the caller decide what to do with it strictDecodingErr := err - klog.Warningf("set SDE: %v", strictDecodingErr) if d, ok := obj.(runtime.NestedObjectDecoder); ok { if err := d.DecodeNestedObjects(runtime.WithoutVersionDecoder{c.decoder}); err != nil { @@ -163,7 +155,6 @@ func (c *codec) Decode(data []byte, defaultGVK *schema.GroupVersionKind, into ru // Short-circuit conversion if the into object is same object if into == obj { - klog.Warningf("short circuit into loop 1 sDE: %v", strictDecodingErr) return into, gvk, strictDecodingErr } @@ -171,7 +162,6 @@ func (c *codec) Decode(data []byte, defaultGVK *schema.GroupVersionKind, into ru return nil, gvk, err } - klog.Warningf("short circuit into loop 2 sDE: %v", strictDecodingErr) return into, gvk, strictDecodingErr } @@ -184,7 +174,6 @@ func (c *codec) Decode(data []byte, defaultGVK *schema.GroupVersionKind, into ru if err != nil { return nil, gvk, err } - klog.Warningf("returning with sDE: %v", strictDecodingErr) return out, gvk, strictDecodingErr } diff --git a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/patch.go b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/patch.go index 8f0ac8dc1e2c0..893e147833e8e 100644 --- a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/patch.go +++ b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/patch.go @@ -351,9 +351,7 @@ func (p *jsonPatcher) applyPatchToCurrentObject(currentObject runtime.Object) (r // Construct the resulting typed, unversioned object. objToUpdate := p.restPatcher.New() if err := runtime.DecodeInto(p.codec, patchedObjJS, objToUpdate); err != nil { - klog.Warningf("decode into err: %v", err) if !runtime.IsStrictDecodingError(err) || p.validationDirective == strictFieldValidation { - klog.Warningf("yes invalid err") return nil, errors.NewInvalid(schema.GroupKind{}, "", field.ErrorList{ field.Invalid(field.NewPath("patch"), string(patchedObjJS), err.Error()), }) From 98e0901c21b560aa734da903ffac284fb322c1a2 Mon Sep 17 00:00:00 2001 From: Kevin Delgado Date: Thu, 14 Oct 2021 03:05:57 +0000 Subject: [PATCH 50/64] cleanup crd-patch --- .../pkg/apiserver/customresource_handler.go | 104 +++++------------- .../apiserver/pkg/endpoints/handlers/patch.go | 14 +-- 2 files changed, 32 insertions(+), 86 deletions(-) diff --git a/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/customresource_handler.go b/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/customresource_handler.go index 29d479a444706..451b44324a8b3 100644 --- a/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/customresource_handler.go +++ b/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/customresource_handler.go @@ -20,7 +20,6 @@ import ( "fmt" "net/http" "path" - goruntime "runtime" "sort" "strings" "sync" @@ -841,23 +840,14 @@ func (r *crdHandler) getOrCreateServingInfoFor(uid types.UID, name string) (*crd clusterScoped := crd.Spec.Scope == apiextensionsv1.ClusterScoped // CRDs explicitly do not support protobuf, but some objects returned by the API server do - // TODO: we could have two serializers one with strict directive and one without? negotiatedSerializer := unstructuredNegotiatedSerializer{ - typer: typer, - creator: creator, - converter: safeConverter, - structuralSchemas: structuralSchemas, - structuralSchemaGK: kind.GroupKind(), - //unknownFieldsDirective: makeUnknownFieldsDirective(crd.Spec.PreserveUnknownFields, false), - unknownFieldsDirective: makeUnknownFieldsDirective(crd.Spec.PreserveUnknownFields, true), - } - strictNegotiatedSerializer := unstructuredNegotiatedSerializer{ - typer: typer, - creator: creator, - converter: safeConverter, - structuralSchemas: structuralSchemas, - structuralSchemaGK: kind.GroupKind(), - unknownFieldsDirective: makeUnknownFieldsDirective(crd.Spec.PreserveUnknownFields, true), + typer: typer, + creator: creator, + converter: safeConverter, + structuralSchemas: structuralSchemas, + structuralSchemaGK: kind.GroupKind(), + preserveUnknownFields: crd.Spec.PreserveUnknownFields, + persistStrictDecodingErrors: true, } var standardSerializers []runtime.SerializerInfo for _, s := range negotiatedSerializer.SupportedMediaTypes() { @@ -874,7 +864,6 @@ func (r *crdHandler) getOrCreateServingInfoFor(uid types.UID, name string) (*crd SelfLinkPathPrefix: selfLinkPrefix, }, Serializer: negotiatedSerializer, - StrictSerializer: strictNegotiatedSerializer, ParameterCodec: parameterCodec, StandardSerializers: standardSerializers, @@ -1044,9 +1033,10 @@ type unstructuredNegotiatedSerializer struct { creator runtime.ObjectCreater converter runtime.ObjectConvertor - structuralSchemas map[string]*structuralschema.Structural // by version - structuralSchemaGK schema.GroupKind - unknownFieldsDirective unknownFieldsDirective + structuralSchemas map[string]*structuralschema.Structural // by version + structuralSchemaGK schema.GroupKind + preserveUnknownFields bool + persistStrictDecodingErrors bool } func (s unstructuredNegotiatedSerializer) SupportedMediaTypes() []runtime.SerializerInfo { @@ -1089,7 +1079,7 @@ func (s unstructuredNegotiatedSerializer) EncoderForVersion(encoder runtime.Enco } func (s unstructuredNegotiatedSerializer) DecoderToVersion(decoder runtime.Decoder, gv runtime.GroupVersioner) runtime.Decoder { - d := schemaCoercingDecoder{delegate: decoder, validator: unstructuredSchemaCoercer{structuralSchemas: s.structuralSchemas, structuralSchemaGK: s.structuralSchemaGK, unknownFieldsDirective: s.unknownFieldsDirective}} + d := schemaCoercingDecoder{delegate: decoder, validator: unstructuredSchemaCoercer{structuralSchemas: s.structuralSchemas, structuralSchemaGK: s.structuralSchemaGK, preserveUnknownFields: s.preserveUnknownFields, persistStrictDecodingErrors: s.persistStrictDecodingErrors}} return versioning.NewCodec(nil, d, runtime.UnsafeObjectConvertor(Scheme), Scheme, Scheme, unstructuredDefaulter{ delegate: Scheme, structuralSchemas: s.structuralSchemas, @@ -1203,16 +1193,16 @@ func (t crdConversionRESTOptionsGetter) GetRESTOptions(resource schema.GroupReso if err == nil { d := schemaCoercingDecoder{delegate: ret.StorageConfig.Codec, validator: unstructuredSchemaCoercer{ // drop invalid fields while decoding old CRs (before we haven't had any ObjectMeta validation) - dropInvalidMetadata: true, - repairGeneration: true, - structuralSchemas: t.structuralSchemas, - structuralSchemaGK: t.structuralSchemaGK, - unknownFieldsDirective: makeUnknownFieldsDirective(t.preserveUnknownFields, false), + dropInvalidMetadata: true, + repairGeneration: true, + structuralSchemas: t.structuralSchemas, + structuralSchemaGK: t.structuralSchemaGK, + preserveUnknownFields: t.preserveUnknownFields, }} c := schemaCoercingConverter{delegate: t.converter, validator: unstructuredSchemaCoercer{ - structuralSchemas: t.structuralSchemas, - structuralSchemaGK: t.structuralSchemaGK, - unknownFieldsDirective: makeUnknownFieldsDirective(t.preserveUnknownFields, false), + structuralSchemas: t.structuralSchemas, + structuralSchemaGK: t.structuralSchemaGK, + preserveUnknownFields: t.preserveUnknownFields, }} ret.StorageConfig.Codec = versioning.NewCodec( ret.StorageConfig.Codec, @@ -1243,10 +1233,6 @@ type schemaCoercingDecoder struct { var _ runtime.Decoder = schemaCoercingDecoder{} func (d schemaCoercingDecoder) Decode(data []byte, defaults *schema.GroupVersionKind, into runtime.Object) (runtime.Object, *schema.GroupVersionKind, error) { - _, file, no, ok := goruntime.Caller(1) - if ok { - klog.Warningf("called from %s#%d\n", file, no) - } obj, gvk, err := d.delegate.Decode(data, defaults, into) if err != nil { return nil, gvk, err @@ -1254,7 +1240,6 @@ func (d schemaCoercingDecoder) Decode(data []byte, defaults *schema.GroupVersion if u, ok := obj.(*unstructured.Unstructured); ok { if err := d.validator.apply(u); err != nil { if runtime.IsStrictDecodingError(err) { - klog.Warningf("sCD Decode err %v", err) return obj, gvk, err } return nil, gvk, err @@ -1306,30 +1291,6 @@ func (v schemaCoercingConverter) ConvertFieldLabel(gvk schema.GroupVersionKind, return v.delegate.ConvertFieldLabel(gvk, label, value) } -// unknownFieldsDirective instructs what should happen -// if a custom resource is received with unknown fields that -// are not part of the schema. The options are: -// - drop the unknown fields -// - preserve the unknown fields -// - fail to handle the request and error out. -type unknownFieldsDirective int - -const ( - drop unknownFieldsDirective = iota - preserve - fail -) - -func makeUnknownFieldsDirective(preserveUnknownFields, failOnUnknownFields bool) unknownFieldsDirective { - if failOnUnknownFields { - return fail - } - if preserveUnknownFields { - return preserve - } - return drop -} - // unstructuredSchemaCoercer adds to unstructured unmarshalling what json.Unmarshal does // in addition for native types when decoding into Golang structs: // @@ -1340,9 +1301,10 @@ type unstructuredSchemaCoercer struct { dropInvalidMetadata bool repairGeneration bool - structuralSchemas map[string]*structuralschema.Structural - structuralSchemaGK schema.GroupKind - unknownFieldsDirective unknownFieldsDirective + structuralSchemas map[string]*structuralschema.Structural + structuralSchemaGK schema.GroupKind + preserveUnknownFields bool + persistStrictDecodingErrors bool } func (v *unstructuredSchemaCoercer) apply(u *unstructured.Unstructured) error { @@ -1367,20 +1329,16 @@ func (v *unstructuredSchemaCoercer) apply(u *unstructured.Unstructured) error { } pruned := map[string]bool{} - klog.Warningf("applying directive: %d", v.unknownFieldsDirective) if gv.Group == v.structuralSchemaGK.Group && kind == v.structuralSchemaGK.Kind { - if v.unknownFieldsDirective != preserve { - // TODO: switch over pruning and coercing at the root to schemaobjectmeta.Coerce too (I don't remember what this comment means anymore) + if !v.preserveUnknownFields { + // TODO: switch over pruning and coercing at the root to schemaobjectmeta.Coerce too pruned = structuralpruning.Prune(u.Object, v.structuralSchemas[gv.Version], false) - klog.Warningf("pruned: %v", pruned) - //if v.unknownFieldsDirective == fail && len(pruned) > 0 { - // return fmt.Errorf("failed with unknown fields: %v", pruned) - //} structuraldefaulting.PruneNonNullableNullsWithoutDefaults(u.Object, v.structuralSchemas[gv.Version]) } if err := schemaobjectmeta.Coerce(nil, u.Object, v.structuralSchemas[gv.Version], false, v.dropInvalidMetadata); err != nil { return err } + // fixup missing generation in very old CRs if v.repairGeneration && objectMeta.Generation == 0 { objectMeta.Generation = 1 } @@ -1398,19 +1356,15 @@ func (v *unstructuredSchemaCoercer) apply(u *unstructured.Unstructured) error { return err } } - if len(pruned) > 0 && v.unknownFieldsDirective == fail { - klog.Warningf("end of apply pruned len: %d", len(pruned)) - klog.Warningf("pruned: %v", pruned) - klog.Warningf("directive: %d", v.unknownFieldsDirective) + // collect all the strict decoding errors and return them + if len(pruned) > 0 && v.persistStrictDecodingErrors { allStrictErrs := make([]error, len(pruned)) i := 0 for unknownField, _ := range pruned { allStrictErrs[i] = fmt.Errorf("unknown field: %s", unknownField) i++ } - klog.Warningf("all strict errs: %v", allStrictErrs) err := runtime.NewStrictDecodingError(allStrictErrs) - klog.Warningf("apply return decoding err: %v", err) return err } diff --git a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/patch.go b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/patch.go index 893e147833e8e..f4f924a0c7c37 100644 --- a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/patch.go +++ b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/patch.go @@ -50,7 +50,6 @@ import ( "k8s.io/apiserver/pkg/registry/rest" "k8s.io/apiserver/pkg/util/dryrun" utilfeature "k8s.io/apiserver/pkg/util/feature" - "k8s.io/klog/v2" utiltrace "k8s.io/utils/trace" ) @@ -151,27 +150,20 @@ func PatchResource(r rest.Patcher, scope *RequestScope, admit admission.Interfac return } gv := scope.Kind.GroupVersion() - strictValidation := false - scopeSerializer := scope.Serializer validationDirective, err := fieldValidation(req) if err != nil { scope.err(err, w, req) return } + strictValidation := false if validationDirective == strictFieldValidation { - scopeSerializer = scope.StrictSerializer strictValidation = true - klog.Warningf("scpeSerializer %+v", scopeSerializer) - klog.Warningf("scope dot Serializer %+v", scope.Serializer) - klog.Warningf("sSerializer %+v", s.Serializer) - } else { - klog.Warningf("nonstrcit serializer") } codec := runtime.NewCodec( - scopeSerializer.EncoderForVersion(s.Serializer, gv), - scopeSerializer.DecoderToVersion(s.Serializer, scope.HubGroupVersion), + scope.Serializer.EncoderForVersion(s.Serializer, gv), + scope.Serializer.DecoderToVersion(s.Serializer, scope.HubGroupVersion), ) userInfo, _ := request.UserFrom(ctx) From cc039623d9201548e0a9b0c07c8df8e23d09e087 Mon Sep 17 00:00:00 2001 From: Kevin Delgado Date: Thu, 14 Oct 2021 03:38:26 +0000 Subject: [PATCH 51/64] start smp cleanup --- .../src/k8s.io/apimachinery/pkg/runtime/converter.go | 1 + .../k8s.io/apimachinery/pkg/runtime/interfaces.go | 2 -- .../pkg/runtime/serializer/codec_factory.go | 12 ------------ .../k8s.io/apiserver/pkg/endpoints/handlers/patch.go | 12 ------------ 4 files changed, 1 insertion(+), 26 deletions(-) diff --git a/staging/src/k8s.io/apimachinery/pkg/runtime/converter.go b/staging/src/k8s.io/apimachinery/pkg/runtime/converter.go index 24d0dd42b78f5..242eefaf63e5a 100644 --- a/staging/src/k8s.io/apimachinery/pkg/runtime/converter.go +++ b/staging/src/k8s.io/apimachinery/pkg/runtime/converter.go @@ -391,6 +391,7 @@ func (c *unstructuredConverter) structFromUnstructured(sv, dv reflect.Value, fie fieldInfo := fieldInfoFromField(dt, i) fv := dv.Field(i) if len(fieldInfo.name) == 0 { + // This field is inlined. if err := c.fromUnstructured(sv, fv, fields); err != nil { return err } diff --git a/staging/src/k8s.io/apimachinery/pkg/runtime/interfaces.go b/staging/src/k8s.io/apimachinery/pkg/runtime/interfaces.go index 56b28905429aa..3e1fab1d11019 100644 --- a/staging/src/k8s.io/apimachinery/pkg/runtime/interfaces.go +++ b/staging/src/k8s.io/apimachinery/pkg/runtime/interfaces.go @@ -125,8 +125,6 @@ type SerializerInfo struct { // PrettySerializer, if set, can serialize this object in a form biased towards // readability. PrettySerializer Serializer - // StrictSerializer errors on unknown fields when deserializing an object - //StrictSerializer Serializer // StreamSerializer, if set, describes the streaming serialization format // for this media type. StreamSerializer *StreamSerializerInfo diff --git a/staging/src/k8s.io/apimachinery/pkg/runtime/serializer/codec_factory.go b/staging/src/k8s.io/apimachinery/pkg/runtime/serializer/codec_factory.go index 77c329209bb43..545d694211e09 100644 --- a/staging/src/k8s.io/apimachinery/pkg/runtime/serializer/codec_factory.go +++ b/staging/src/k8s.io/apimachinery/pkg/runtime/serializer/codec_factory.go @@ -40,7 +40,6 @@ type serializerType struct { Serializer runtime.Serializer PrettySerializer runtime.Serializer - //StrictSerializer runtime.Serializer AcceptStreamContentTypes []string StreamContentType string @@ -70,19 +69,10 @@ func newSerializersForScheme(scheme *runtime.Scheme, mf json.MetaFactory, option json.SerializerOptions{Yaml: false, Pretty: true, Strict: options.Strict}, ) } - //strictJSONSerializer := json.NewSerializerWithOptions( - // mf, scheme, scheme, - // json.SerializerOptions{Yaml: false, Pretty: false, Strict: true}, - //) - //jsonSerializerType.StrictSerializer = strictJSONSerializer yamlSerializer := json.NewSerializerWithOptions( mf, scheme, scheme, json.SerializerOptions{Yaml: true, Pretty: false, Strict: options.Strict}, ) - //strictYAMLSerializer := json.NewSerializerWithOptions( - // mf, scheme, scheme, - // json.SerializerOptions{Yaml: true, Pretty: false, Strict: true}, - //) protoSerializer := protobuf.NewSerializer(scheme, scheme) protoRawSerializer := protobuf.NewRawSerializer(scheme, scheme) @@ -94,7 +84,6 @@ func newSerializersForScheme(scheme *runtime.Scheme, mf json.MetaFactory, option FileExtensions: []string{"yaml"}, EncodesAsText: true, Serializer: yamlSerializer, - //StrictSerializer: strictYAMLSerializer, }, { AcceptContentTypes: []string{runtime.ContentTypeProtobuf}, @@ -199,7 +188,6 @@ func newCodecFactory(scheme *runtime.Scheme, serializers []serializerType) Codec EncodesAsText: d.EncodesAsText, Serializer: d.Serializer, PrettySerializer: d.PrettySerializer, - //StrictSerializer: d.StrictSerializer, } mediaType, _, err := mime.ParseMediaType(info.MediaType) diff --git a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/patch.go b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/patch.go index f4f924a0c7c37..ff1c381a50dcf 100644 --- a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/patch.go +++ b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/patch.go @@ -70,17 +70,6 @@ func PatchResource(r rest.Patcher, scope *RequestScope, admit admission.Interfac return } - //// TODO: put behind feature gate? - //validationDirective, err := fieldValidation(req) - //if err != nil { - // scope.err(err, w, req) - // return - //} - //if validationDirective == strictFieldValidation { - // scope.err(errors.NewBadRequest("strict validation is not supported yet"), w, req) - // return - //} - // Do this first, otherwise name extraction can fail for unrecognized content types // TODO: handle this in negotiation contentType := req.Header.Get("Content-Type") @@ -231,7 +220,6 @@ func PatchResource(r rest.Patcher, scope *RequestScope, admit admission.Interfac updateValidation: rest.AdmissionToValidateObjectUpdateFunc(admit, staticUpdateAttributes, scope), admissionCheck: mutatingAdmission, - // TODO: define codec as strict or not-strict based on request codec: codec, options: options, From 35399e476ee54abf6335ad02d9f5ef773d205f40 Mon Sep 17 00:00:00 2001 From: Kevin Delgado Date: Thu, 14 Oct 2021 04:08:05 +0000 Subject: [PATCH 52/64] move fieldValidationDirective enum to runtime pkg --- .../apimachinery/pkg/runtime/converter.go | 20 ++++-- .../pkg/endpoints/handlers/create.go | 4 +- .../apiserver/pkg/endpoints/handlers/patch.go | 61 +++++++++---------- .../apiserver/pkg/endpoints/handlers/rest.go | 28 +++------ .../pkg/endpoints/handlers/update.go | 4 +- 5 files changed, 57 insertions(+), 60 deletions(-) diff --git a/staging/src/k8s.io/apimachinery/pkg/runtime/converter.go b/staging/src/k8s.io/apimachinery/pkg/runtime/converter.go index 242eefaf63e5a..c54816244a8ed 100644 --- a/staging/src/k8s.io/apimachinery/pkg/runtime/converter.go +++ b/staging/src/k8s.io/apimachinery/pkg/runtime/converter.go @@ -102,8 +102,8 @@ type unstructuredConverter struct { // This is supposed to be set only in tests. mismatchDetection bool // comparison is the default test logic used to compare - comparison conversion.Equalities - strictFieldValidation bool + comparison conversion.Equalities + fieldValidationDirective FieldValidationDirective } // NewTestUnstructuredConverter creates an UnstructuredConverter that accepts JSON typed maps and translates them @@ -116,8 +116,18 @@ func NewTestUnstructuredConverter(comparison conversion.Equalities) Unstructured } } -func (c *unstructuredConverter) SetStrictFieldValidation(strict bool) { - c.strictFieldValidation = strict +// FieldValidationDirective indicates what the apiserver should +// do with unknown fields (ignore, strict/error, or warn). +type FieldValidationDirective int + +const ( + IgnoreFieldValidation FieldValidationDirective = iota + StrictFieldValidation + WarnFieldValidation +) + +func (c *unstructuredConverter) SetFieldValidationDirective(directive FieldValidationDirective) { + c.fieldValidationDirective = directive } func makeFields(u map[string]interface{}) map[string]struct{} { @@ -410,7 +420,7 @@ func (c *unstructuredConverter) structFromUnstructured(sv, dv reflect.Value, fie } } } - if len(keys) > 0 && c.strictFieldValidation { + if len(keys) > 0 && c.fieldValidationDirective == StrictFieldValidation { return &UnknownFieldError{ invalidFields: keys, } diff --git a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/create.go b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/create.go index 400a93297a184..58b82a258a883 100644 --- a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/create.go +++ b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/create.go @@ -126,12 +126,12 @@ func createHandler(r rest.NamedCreater, scope *RequestScope, admit admission.Int trace.Step("About to convert to expected version") obj, gvk, err := decoder.Decode(body, &defaultGVK, original) if err != nil { - if !runtime.IsStrictDecodingError(err) || validationDirective == strictFieldValidation { + if !runtime.IsStrictDecodingError(err) || validationDirective == runtime.StrictFieldValidation { err = transformDecodeError(scope.Typer, err, original, gvk, body) scope.err(err, w, req) return } - if validationDirective == warnFieldValidation { + if validationDirective == runtime.WarnFieldValidation { // TODO: throw a warning here } } diff --git a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/patch.go b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/patch.go index ff1c381a50dcf..69ae96b051b13 100644 --- a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/patch.go +++ b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/patch.go @@ -145,10 +145,6 @@ func PatchResource(r rest.Patcher, scope *RequestScope, admit admission.Interfac scope.err(err, w, req) return } - strictValidation := false - if validationDirective == strictFieldValidation { - strictValidation = true - } codec := runtime.NewCodec( scope.Serializer.EncoderForVersion(s.Serializer, gv), @@ -200,17 +196,16 @@ func PatchResource(r rest.Patcher, scope *RequestScope, admit admission.Interfac } p := patcher{ - namer: scope.Namer, - creater: scope.Creater, - defaulter: scope.Defaulter, - typer: scope.Typer, - unsafeConvertor: scope.UnsafeConvertor, - kind: scope.Kind, - resource: scope.Resource, - subresource: scope.Subresource, - dryRun: dryrun.IsDryRun(options.DryRun), - strictFieldValidation: strictValidation, - validationDirective: validationDirective, + namer: scope.Namer, + creater: scope.Creater, + defaulter: scope.Defaulter, + typer: scope.Typer, + unsafeConvertor: scope.UnsafeConvertor, + kind: scope.Kind, + resource: scope.Resource, + subresource: scope.Subresource, + dryRun: dryrun.IsDryRun(options.DryRun), + validationDirective: validationDirective, objectInterfaces: scope, @@ -263,17 +258,17 @@ type mutateObjectUpdateFunc func(ctx context.Context, obj, old runtime.Object) e // moved into this type. type patcher struct { // Pieces of RequestScope - namer ScopeNamer - creater runtime.ObjectCreater - defaulter runtime.ObjectDefaulter - typer runtime.ObjectTyper - unsafeConvertor runtime.ObjectConvertor - resource schema.GroupVersionResource - kind schema.GroupVersionKind - subresource string - dryRun bool - strictFieldValidation bool - validationDirective fieldValidationDirective + namer ScopeNamer + creater runtime.ObjectCreater + defaulter runtime.ObjectDefaulter + typer runtime.ObjectTyper + unsafeConvertor runtime.ObjectConvertor + resource schema.GroupVersionResource + kind schema.GroupVersionKind + subresource string + dryRun bool + //strictFieldValidation bool + validationDirective runtime.FieldValidationDirective objectInterfaces admission.ObjectInterfaces @@ -331,12 +326,12 @@ func (p *jsonPatcher) applyPatchToCurrentObject(currentObject runtime.Object) (r // Construct the resulting typed, unversioned object. objToUpdate := p.restPatcher.New() if err := runtime.DecodeInto(p.codec, patchedObjJS, objToUpdate); err != nil { - if !runtime.IsStrictDecodingError(err) || p.validationDirective == strictFieldValidation { + if !runtime.IsStrictDecodingError(err) || p.validationDirective == runtime.StrictFieldValidation { return nil, errors.NewInvalid(schema.GroupKind{}, "", field.ErrorList{ field.Invalid(field.NewPath("patch"), string(patchedObjJS), err.Error()), }) } - if p.validationDirective == warnFieldValidation { + if p.validationDirective == runtime.WarnFieldValidation { // TODO: throw a warning here } } @@ -415,7 +410,7 @@ func (p *smpPatcher) applyPatchToCurrentObject(currentObject runtime.Object) (ru if err != nil { return nil, err } - if err := strategicPatchObject(p.defaulter, currentVersionedObject, p.patchBytes, versionedObjToUpdate, p.schemaReferenceObj, p.strictFieldValidation); err != nil { + if err := strategicPatchObject(p.defaulter, currentVersionedObject, p.patchBytes, versionedObjToUpdate, p.schemaReferenceObj, p.validationDirective); err != nil { return nil, err } // Convert the object back to the hub version @@ -479,7 +474,7 @@ func strategicPatchObject( patchBytes []byte, objToUpdate runtime.Object, schemaReferenceObj runtime.Object, - strictFieldValidation bool, + validationDirective runtime.FieldValidationDirective, ) error { originalObjMap, err := runtime.DefaultUnstructuredConverter.ToUnstructured(originalObject) if err != nil { @@ -491,7 +486,7 @@ func strategicPatchObject( return errors.NewBadRequest(err.Error()) } - if err := applyPatchToObject(defaulter, originalObjMap, patchMap, objToUpdate, schemaReferenceObj, strictFieldValidation); err != nil { + if err := applyPatchToObject(defaulter, originalObjMap, patchMap, objToUpdate, schemaReferenceObj, validationDirective); err != nil { return err } return nil @@ -643,7 +638,7 @@ func applyPatchToObject( patchMap map[string]interface{}, objToUpdate runtime.Object, schemaReferenceObj runtime.Object, - strictFieldValidation bool, + validationDirective runtime.FieldValidationDirective, ) error { patchedObjMap, err := strategicpatch.StrategicMergeMapPatch(originalMap, patchMap, schemaReferenceObj) if err != nil { @@ -652,7 +647,7 @@ func applyPatchToObject( // Rather than serialize the patched map to JSON, then decode it to an object, we go directly from a map to an object converter := runtime.DefaultUnstructuredConverter - converter.SetStrictFieldValidation(strictFieldValidation) + converter.SetFieldValidationDirective(validationDirective) if err := converter.FromUnstructured(patchedObjMap, objToUpdate); err != nil { return errors.NewInvalid(schema.GroupKind{}, "", field.ErrorList{ field.Invalid(field.NewPath("patch"), fmt.Sprintf("%+v", patchMap), err.Error()), diff --git a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/rest.go b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/rest.go index 9da6aa4570bc8..117c29a4e5b71 100644 --- a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/rest.go +++ b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/rest.go @@ -459,19 +459,11 @@ func isDryRun(url *url.URL) bool { return len(url.Query()["dryRun"]) != 0 } -type fieldValidationDirective int - -const ( - ignoreFieldValidation fieldValidationDirective = iota - strictFieldValidation - warnFieldValidation -) - // fieldValidation checks if the fieldValidation query parameter is set on the request, // and if so ensures that the parameter is valid and that the request has a valid // media type, because the list of media types that support field validation are a subset of // all supported media types (only json and yaml supports field validation). -func fieldValidation(req *http.Request) (fieldValidationDirective, error) { +func fieldValidation(req *http.Request) (runtime.FieldValidationDirective, error) { supportedContentTypes := []string{runtime.ContentTypeJSON, runtime.ContentTypeJSONMergePatch, runtime.ContentTypeJSONStrategicMergePatch, runtime.ContentTypeYAML} contentType := req.Header.Get("Content-Type") // TODO: not sure if it is okay to assume empty content type is a valid one @@ -481,7 +473,7 @@ func fieldValidation(req *http.Request) (fieldValidationDirective, error) { for _, v := range strings.Split(contentType, ",") { t, _, err := mime.ParseMediaType(v) if err != nil { - return ignoreFieldValidation, errors.NewBadRequest(fmt.Sprintf("could not parse media type: %v", v)) + return runtime.IgnoreFieldValidation, errors.NewBadRequest(fmt.Sprintf("could not parse media type: %v", v)) } for _, mt := range supportedContentTypes { if t == mt { @@ -495,26 +487,26 @@ func fieldValidation(req *http.Request) (fieldValidationDirective, error) { validationParam := req.URL.Query()["fieldValidation"] switch len(validationParam) { case 0: - return ignoreFieldValidation, nil + return runtime.IgnoreFieldValidation, nil case 1: switch strings.ToLower(validationParam[0]) { case "ignore": - return ignoreFieldValidation, nil + return runtime.IgnoreFieldValidation, nil case "strict": if !supported { - return ignoreFieldValidation, errors.NewBadRequest(fmt.Sprintf("fieldValidation parameter only supports content types %v\n content type provided: %s", supportedContentTypes, contentType)) + return runtime.IgnoreFieldValidation, errors.NewBadRequest(fmt.Sprintf("fieldValidation parameter only supports content types %v\n content type provided: %s", supportedContentTypes, contentType)) } - return strictFieldValidation, nil + return runtime.StrictFieldValidation, nil case "warn": if !supported { - return ignoreFieldValidation, errors.NewBadRequest(fmt.Sprintf("fieldValidation parameter only supports content types %v\n content type provided: %s", supportedContentTypes, contentType)) + return runtime.IgnoreFieldValidation, errors.NewBadRequest(fmt.Sprintf("fieldValidation parameter only supports content types %v\n content type provided: %s", supportedContentTypes, contentType)) } - return warnFieldValidation, nil + return runtime.WarnFieldValidation, nil default: - return ignoreFieldValidation, errors.NewBadRequest(fmt.Sprintf("fieldValidation parameter unsupported: %v", validationParam)) + return runtime.IgnoreFieldValidation, errors.NewBadRequest(fmt.Sprintf("fieldValidation parameter unsupported: %v", validationParam)) } default: - return ignoreFieldValidation, errors.NewBadRequest(fmt.Sprintf("fieldValidation should only be one value: %v", validationParam)) + return runtime.IgnoreFieldValidation, errors.NewBadRequest(fmt.Sprintf("fieldValidation should only be one value: %v", validationParam)) } } diff --git a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/update.go b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/update.go index cece289c24946..2d65d858e896c 100644 --- a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/update.go +++ b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/update.go @@ -113,12 +113,12 @@ func UpdateResource(r rest.Updater, scope *RequestScope, admit admission.Interfa trace.Step("About to convert to expected version") obj, gvk, err := decoder.Decode(body, &defaultGVK, original) if err != nil { - if !runtime.IsStrictDecodingError(err) || validationDirective == strictFieldValidation { + if !runtime.IsStrictDecodingError(err) || validationDirective == runtime.StrictFieldValidation { err = transformDecodeError(scope.Typer, err, original, gvk, body) scope.err(err, w, req) return } - if validationDirective == warnFieldValidation { + if validationDirective == runtime.WarnFieldValidation { // TODO: throw a warning here } } From 73eb90ace7a9b1ad95cdb3db44a2e7790793492a Mon Sep 17 00:00:00 2001 From: Kevin Delgado Date: Thu, 14 Oct 2021 04:13:49 +0000 Subject: [PATCH 53/64] remove StrictSerializer from scope --- .../pkg/runtime/serializer/codec_factory.go | 1 + .../apiserver/pkg/endpoints/handlers/rest.go | 3 +-- .../k8s.io/apiserver/pkg/endpoints/installer.go | 17 ++++++++--------- 3 files changed, 10 insertions(+), 11 deletions(-) diff --git a/staging/src/k8s.io/apimachinery/pkg/runtime/serializer/codec_factory.go b/staging/src/k8s.io/apimachinery/pkg/runtime/serializer/codec_factory.go index 545d694211e09..ceca12fac7aff 100644 --- a/staging/src/k8s.io/apimachinery/pkg/runtime/serializer/codec_factory.go +++ b/staging/src/k8s.io/apimachinery/pkg/runtime/serializer/codec_factory.go @@ -69,6 +69,7 @@ func newSerializersForScheme(scheme *runtime.Scheme, mf json.MetaFactory, option json.SerializerOptions{Yaml: false, Pretty: true, Strict: options.Strict}, ) } + yamlSerializer := json.NewSerializerWithOptions( mf, scheme, scheme, json.SerializerOptions{Yaml: true, Pretty: false, Strict: options.Strict}, diff --git a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/rest.go b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/rest.go index 117c29a4e5b71..185621f2cfc59 100644 --- a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/rest.go +++ b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/rest.go @@ -71,8 +71,7 @@ const ( type RequestScope struct { Namer ScopeNamer - StrictSerializer runtime.NegotiatedSerializer - Serializer runtime.NegotiatedSerializer + Serializer runtime.NegotiatedSerializer runtime.ParameterCodec // StandardSerializers, if set, restricts which serializers can be used when diff --git a/staging/src/k8s.io/apiserver/pkg/endpoints/installer.go b/staging/src/k8s.io/apiserver/pkg/endpoints/installer.go index 14bc185aceb78..0902add4823c3 100644 --- a/staging/src/k8s.io/apiserver/pkg/endpoints/installer.go +++ b/staging/src/k8s.io/apiserver/pkg/endpoints/installer.go @@ -569,15 +569,14 @@ func (a *APIInstaller) registerResourceHandlers(path string, storage rest.Storag kubeVerbs := map[string]struct{}{} reqScope := handlers.RequestScope{ - Serializer: a.group.Serializer, - StrictSerializer: a.group.Serializer, - ParameterCodec: a.group.ParameterCodec, - Creater: a.group.Creater, - Convertor: a.group.Convertor, - Defaulter: a.group.Defaulter, - Typer: a.group.Typer, - UnsafeConvertor: a.group.UnsafeConvertor, - Authorizer: a.group.Authorizer, + Serializer: a.group.Serializer, + ParameterCodec: a.group.ParameterCodec, + Creater: a.group.Creater, + Convertor: a.group.Convertor, + Defaulter: a.group.Defaulter, + Typer: a.group.Typer, + UnsafeConvertor: a.group.UnsafeConvertor, + Authorizer: a.group.Authorizer, EquivalentResourceMapper: a.group.EquivalentResourceRegistry, From 5ff723b5702fe9da47a34cf3398a6d229c990bdb Mon Sep 17 00:00:00 2001 From: Kevin Delgado Date: Thu, 14 Oct 2021 04:30:02 +0000 Subject: [PATCH 54/64] refactor SMP --- .../k8s.io/apimachinery/pkg/runtime/converter.go | 16 ++++++---------- .../apiserver/pkg/endpoints/handlers/patch.go | 11 ++++++++--- .../apiserver/field_validation_test.go | 6 +++--- 3 files changed, 17 insertions(+), 16 deletions(-) diff --git a/staging/src/k8s.io/apimachinery/pkg/runtime/converter.go b/staging/src/k8s.io/apimachinery/pkg/runtime/converter.go index c54816244a8ed..66af8b62fa5fb 100644 --- a/staging/src/k8s.io/apimachinery/pkg/runtime/converter.go +++ b/staging/src/k8s.io/apimachinery/pkg/runtime/converter.go @@ -421,21 +421,17 @@ func (c *unstructuredConverter) structFromUnstructured(sv, dv reflect.Value, fie } } if len(keys) > 0 && c.fieldValidationDirective == StrictFieldValidation { - return &UnknownFieldError{ - invalidFields: keys, + allStrictErrs := make([]error, len(keys)) + for i, unknownField := range keys { + allStrictErrs[i] = fmt.Errorf("unknown field: %s", unknownField.String()) + i++ } + err := NewStrictDecodingError(allStrictErrs) + return err } return nil } -type UnknownFieldError struct { - invalidFields []reflect.Value -} - -func (fe *UnknownFieldError) Error() string { - return fmt.Sprintf("unknown fields when converting from unstructured: %+v", fe.invalidFields) -} - func deleteFromKeys(name string, keys *[]reflect.Value) { for i := len(*keys) - 1; i >= 0; i-- { if name == (*keys)[i].String() { diff --git a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/patch.go b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/patch.go index 69ae96b051b13..ea6610db72a64 100644 --- a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/patch.go +++ b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/patch.go @@ -649,9 +649,14 @@ func applyPatchToObject( converter := runtime.DefaultUnstructuredConverter converter.SetFieldValidationDirective(validationDirective) if err := converter.FromUnstructured(patchedObjMap, objToUpdate); err != nil { - return errors.NewInvalid(schema.GroupKind{}, "", field.ErrorList{ - field.Invalid(field.NewPath("patch"), fmt.Sprintf("%+v", patchMap), err.Error()), - }) + if !runtime.IsStrictDecodingError(err) || validationDirective == runtime.StrictFieldValidation { + return errors.NewInvalid(schema.GroupKind{}, "", field.ErrorList{ + field.Invalid(field.NewPath("patch"), fmt.Sprintf("%+v", patchMap), err.Error()), + }) + } + if validationDirective == runtime.WarnFieldValidation { + // TODO: throw a warning here + } } // Decoding from JSON to a versioned object would apply defaults, so we do the same here defaulter.Default(objToUpdate) diff --git a/test/integration/apiserver/field_validation_test.go b/test/integration/apiserver/field_validation_test.go index 69dcf43104352..46839cc87e899 100644 --- a/test/integration/apiserver/field_validation_test.go +++ b/test/integration/apiserver/field_validation_test.go @@ -351,7 +351,7 @@ func TestFieldValidationSMP(t *testing.T) { { name: "smp-strict-validation", params: map[string]string{"fieldValidation": "Strict"}, - errContains: "unknown fields when converting from unstructured", + errContains: "unknown field", }, { name: "smp-ignore-validation", @@ -381,7 +381,7 @@ func BenchmarkFieldValidationSMP(b *testing.B) { { name: "smp-strict-validation", params: map[string]string{"fieldValidation": "Strict"}, - errContains: "unknown fields when converting from unstructured", + errContains: "unknown field", }, { name: "smp-ignore-validation", @@ -565,7 +565,7 @@ func BenchmarkFieldValidationPatchCRD(b *testing.B) { patchType: types.MergePatchType, params: map[string]string{"fieldValidation": "Strict"}, bodyBase: `{"metadata":{"finalizers":["test-finalizer","finalizer-strict-unknown-%d"]}, "spec":{"foo": "bar"}}`, - errContains: "failed with unknown fields", + errContains: "unknown field", }, } for _, bm := range benchmarks { From 8339937265499029c4afa1eadbf1c01bbe7c13a1 Mon Sep 17 00:00:00 2001 From: Kevin Delgado Date: Thu, 14 Oct 2021 16:26:32 +0000 Subject: [PATCH 55/64] Add warning for POST/PUT --- .../k8s.io/apimachinery/pkg/util/net/http.go | 4 + .../pkg/endpoints/handlers/create.go | 3 +- .../pkg/endpoints/handlers/update.go | 3 +- .../apiserver/field_validation_test.go | 142 +++++++++++------- 4 files changed, 93 insertions(+), 59 deletions(-) diff --git a/staging/src/k8s.io/apimachinery/pkg/util/net/http.go b/staging/src/k8s.io/apimachinery/pkg/util/net/http.go index 42d66d3164ab3..2106ccf9c2047 100644 --- a/staging/src/k8s.io/apimachinery/pkg/util/net/http.go +++ b/staging/src/k8s.io/apimachinery/pkg/util/net/http.go @@ -556,6 +556,10 @@ type WarningHeader struct { Text string } +func (w WarningHeader) String() string { + return fmt.Sprintf("%03d %s %s", w.Code, w.Agent, w.Text) +} + // ParseWarningHeaders extract RFC2616 14.46 warnings headers from the specified set of header values. // Multiple comma-separated warnings per header are supported. // If errors are encountered on a header, the remainder of that header are skipped and subsequent headers are parsed. diff --git a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/create.go b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/create.go index 58b82a258a883..2fc4596f9156a 100644 --- a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/create.go +++ b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/create.go @@ -43,6 +43,7 @@ import ( "k8s.io/apiserver/pkg/registry/rest" "k8s.io/apiserver/pkg/util/dryrun" utilfeature "k8s.io/apiserver/pkg/util/feature" + "k8s.io/apiserver/pkg/warning" utiltrace "k8s.io/utils/trace" ) @@ -132,7 +133,7 @@ func createHandler(r rest.NamedCreater, scope *RequestScope, admit admission.Int return } if validationDirective == runtime.WarnFieldValidation { - // TODO: throw a warning here + warning.AddWarning(req.Context(), "", err.Error()) } } diff --git a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/update.go b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/update.go index 2d65d858e896c..76ec874c9dee2 100644 --- a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/update.go +++ b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/update.go @@ -41,6 +41,7 @@ import ( "k8s.io/apiserver/pkg/registry/rest" "k8s.io/apiserver/pkg/util/dryrun" utilfeature "k8s.io/apiserver/pkg/util/feature" + "k8s.io/apiserver/pkg/warning" utiltrace "k8s.io/utils/trace" ) @@ -119,7 +120,7 @@ func UpdateResource(r rest.Updater, scope *RequestScope, admit admission.Interfa return } if validationDirective == runtime.WarnFieldValidation { - // TODO: throw a warning here + warning.AddWarning(req.Context(), "", err.Error()) } } diff --git a/test/integration/apiserver/field_validation_test.go b/test/integration/apiserver/field_validation_test.go index 46839cc87e899..41c376584c4fd 100644 --- a/test/integration/apiserver/field_validation_test.go +++ b/test/integration/apiserver/field_validation_test.go @@ -41,137 +41,165 @@ import ( "k8s.io/kubernetes/test/integration/framework" ) -// TestFieldValidationPut tests PUT requests containing unknown fields with +// TestFieldValidationPost tests POST requests containing unknown fields with // strict and non-strict field validation. -func TestFieldValidationPut(t *testing.T) { +func TestFieldValidationPost(t *testing.T) { defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.ServerSideApply, true)() _, client, closeFn := setup(t) defer closeFn() - deployName := `"test-deployment"` - postBytes, err := os.ReadFile("./testdata/deploy-small.json") + bodyBytes, err := os.ReadFile("./testdata/deploy-small-unknown-field.json") if err != nil { t.Fatalf("failed to read file: %v", err) } - postBody := []byte(fmt.Sprintf(string(postBytes), 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) - } + body := []byte(fmt.Sprintf(string(bodyBytes), `"test-deployment"`)) - putBytes, err := os.ReadFile("./testdata/deploy-small-unknown-field.json") - if err != nil { - t.Fatalf("failed to read file: %v", err) - } - putBody := []byte(fmt.Sprintf(string(putBytes), deployName)) var testcases = []struct { name string // TODO: use PostOptions for fieldValidation param instead of raw strings. - params map[string]string - errContains string + params map[string]string + errContains string + warnContains string }{ { - name: "put-strict-validation", + name: "post-strict-validation", params: map[string]string{"fieldValidation": "Strict"}, errContains: "unknown field", }, { - name: "put-default-ignore-validation", - params: map[string]string{}, - errContains: "", + name: "post-ignore-validation", + params: map[string]string{"fieldValidation": "Warn"}, + warnContains: "unknown field", }, { - name: "put-ignore-validation", - params: map[string]string{"fieldValidation": "Ignore"}, - errContains: "", + name: "post-default-ignore-validation", + params: map[string]string{}, + }, + { + name: "post-ignore-validation", + params: map[string]string{"fieldValidation": "Ignore"}, }, } for _, tc := range testcases { t.Run(tc.name, func(t *testing.T) { - req := client.CoreV1().RESTClient().Put(). + req := client.CoreV1().RESTClient().Post(). AbsPath("/apis/apps/v1"). Namespace("default"). - Resource("deployments"). - Name("test-dep") + Resource("deployments") for k, v := range tc.params { req.Param(k, v) } - result, err := req.Body(putBody).DoRaw(context.TODO()) + result := req.Body([]byte(body)).Do(context.TODO()) + if tc.warnContains != "" { + warningMatched := false + for _, w := range result.Warnings() { + if strings.Contains(w.String(), tc.warnContains) { + warningMatched = true + } + } + if !warningMatched { + t.Fatalf("expected warning to contain: %s, got warnings: %v", tc.warnContains, result.Warnings()) + } + } + resBody, err := result.Raw() if err == nil && tc.errContains != "" { t.Fatalf("unexpected post succeeded") - } - if err != nil && !strings.Contains(string(result), tc.errContains) { - t.Fatalf("unexpected response: %v", string(result)) - + if err != nil && !strings.Contains(string(resBody), tc.errContains) { + t.Fatalf("unexpected response: %v", string(resBody)) } }) - } - } -// TestFieldValidationPost tests POST requests containing unknown fields with +// TestFieldValidationPut tests PUT requests containing unknown fields with // strict and non-strict field validation. -func TestFieldValidationPost(t *testing.T) { +func TestFieldValidationPut(t *testing.T) { defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.ServerSideApply, true)() _, client, closeFn := setup(t) defer closeFn() - bodyBytes, err := os.ReadFile("./testdata/deploy-small-unknown-field.json") + deployName := `"test-deployment"` + postBytes, err := os.ReadFile("./testdata/deploy-small.json") if err != nil { t.Fatalf("failed to read file: %v", err) } - body := []byte(fmt.Sprintf(string(bodyBytes), `"test-deployment"`)) + postBody := []byte(fmt.Sprintf(string(postBytes), 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) + } + putBytes, err := os.ReadFile("./testdata/deploy-small-unknown-field.json") + if err != nil { + t.Fatalf("failed to read file: %v", err) + } + putBody := []byte(fmt.Sprintf(string(putBytes), deployName)) var testcases = []struct { name string // TODO: use PostOptions for fieldValidation param instead of raw strings. - params map[string]string - errContains string + params map[string]string + errContains string + warnContains string }{ { - name: "post-strict-validation", + name: "put-strict-validation", params: map[string]string{"fieldValidation": "Strict"}, errContains: "unknown field", }, { - name: "post-default-ignore-validation", - params: map[string]string{}, - errContains: "", + name: "put-strict-validation", + params: map[string]string{"fieldValidation": "Warn"}, + warnContains: "unknown field", }, { - name: "post-ignore-validation", - params: map[string]string{"fieldValidation": "Ignore"}, - errContains: "", + name: "put-default-ignore-validation", + params: map[string]string{}, + }, + { + name: "put-ignore-validation", + params: map[string]string{"fieldValidation": "Ignore"}, }, } for _, tc := range testcases { t.Run(tc.name, func(t *testing.T) { - req := client.CoreV1().RESTClient().Post(). + req := client.CoreV1().RESTClient().Put(). AbsPath("/apis/apps/v1"). Namespace("default"). - Resource("deployments") + Resource("deployments"). + Name("test-dep") for k, v := range tc.params { req.Param(k, v) } - result, err := req.Body([]byte(body)).DoRaw(context.TODO()) + result := req.Body([]byte(putBody)).Do(context.TODO()) + if tc.warnContains != "" { + warningMatched := false + for _, w := range result.Warnings() { + if strings.Contains(w.String(), tc.warnContains) { + warningMatched = true + } + } + if !warningMatched { + t.Fatalf("expected warning to contain: %s, got warnings: %v", tc.warnContains, result.Warnings()) + } + } + resBody, err := result.Raw() if err == nil && tc.errContains != "" { - t.Fatalf("unexpected post succeeded") + t.Fatalf("unexpected put succeeded") } - if err != nil && !strings.Contains(string(result), tc.errContains) { - t.Fatalf("unexpected response: %v", string(result)) + if err != nil && !strings.Contains(string(resBody), tc.errContains) { + t.Fatalf("unexpected response: %v", string(resBody)) } }) } From 67ddac6ec8b868c8823acc842e8557fe1149ff9d Mon Sep 17 00:00:00 2001 From: Kevin Delgado Date: Thu, 14 Oct 2021 17:10:25 +0000 Subject: [PATCH 56/64] Add warning for SMP Patch --- .../apimachinery/pkg/runtime/converter.go | 2 +- .../apiserver/pkg/endpoints/handlers/patch.go | 25 ++++++------ .../apiserver/field_validation_test.go | 38 ++++++++++++++----- 3 files changed, 43 insertions(+), 22 deletions(-) diff --git a/staging/src/k8s.io/apimachinery/pkg/runtime/converter.go b/staging/src/k8s.io/apimachinery/pkg/runtime/converter.go index 66af8b62fa5fb..1731cc1d4a2d5 100644 --- a/staging/src/k8s.io/apimachinery/pkg/runtime/converter.go +++ b/staging/src/k8s.io/apimachinery/pkg/runtime/converter.go @@ -420,7 +420,7 @@ func (c *unstructuredConverter) structFromUnstructured(sv, dv reflect.Value, fie } } } - if len(keys) > 0 && c.fieldValidationDirective == StrictFieldValidation { + if len(keys) > 0 && c.fieldValidationDirective == StrictFieldValidation || c.fieldValidationDirective == WarnFieldValidation { allStrictErrs := make([]error, len(keys)) for i, unknownField := range keys { allStrictErrs[i] = fmt.Errorf("unknown field: %s", unknownField.String()) diff --git a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/patch.go b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/patch.go index ea6610db72a64..0927d1673a417 100644 --- a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/patch.go +++ b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/patch.go @@ -50,6 +50,7 @@ import ( "k8s.io/apiserver/pkg/registry/rest" "k8s.io/apiserver/pkg/util/dryrun" utilfeature "k8s.io/apiserver/pkg/util/feature" + "k8s.io/apiserver/pkg/warning" utiltrace "k8s.io/utils/trace" ) @@ -300,7 +301,7 @@ type patcher struct { } type patchMechanism interface { - applyPatchToCurrentObject(currentObject runtime.Object) (runtime.Object, error) + applyPatchToCurrentObject(requextContext context.Context, currentObject runtime.Object) (runtime.Object, error) createNewObject() (runtime.Object, error) } @@ -310,7 +311,7 @@ type jsonPatcher struct { fieldManager *fieldmanager.FieldManager } -func (p *jsonPatcher) applyPatchToCurrentObject(currentObject runtime.Object) (runtime.Object, error) { +func (p *jsonPatcher) applyPatchToCurrentObject(requestContext context.Context, currentObject runtime.Object) (runtime.Object, error) { // Encode will convert & return a versioned object in JSON. currentObjJS, err := runtime.Encode(p.codec, currentObject) if err != nil { @@ -332,7 +333,7 @@ func (p *jsonPatcher) applyPatchToCurrentObject(currentObject runtime.Object) (r }) } if p.validationDirective == runtime.WarnFieldValidation { - // TODO: throw a warning here + warning.AddWarning(requestContext, "", err.Error()) } } @@ -399,7 +400,7 @@ type smpPatcher struct { fieldManager *fieldmanager.FieldManager } -func (p *smpPatcher) applyPatchToCurrentObject(currentObject runtime.Object) (runtime.Object, error) { +func (p *smpPatcher) applyPatchToCurrentObject(requestContext context.Context, currentObject runtime.Object) (runtime.Object, error) { // Since the patch is applied on versioned objects, we need to convert the // current object to versioned representation first. currentVersionedObject, err := p.unsafeConvertor.ConvertToVersion(currentObject, p.kind.GroupVersion()) @@ -410,7 +411,7 @@ func (p *smpPatcher) applyPatchToCurrentObject(currentObject runtime.Object) (ru if err != nil { return nil, err } - if err := strategicPatchObject(p.defaulter, currentVersionedObject, p.patchBytes, versionedObjToUpdate, p.schemaReferenceObj, p.validationDirective); err != nil { + if err := strategicPatchObject(requestContext, p.defaulter, currentVersionedObject, p.patchBytes, versionedObjToUpdate, p.schemaReferenceObj, p.validationDirective); err != nil { return nil, err } // Convert the object back to the hub version @@ -438,7 +439,7 @@ type applyPatcher struct { userAgent string } -func (p *applyPatcher) applyPatchToCurrentObject(obj runtime.Object) (runtime.Object, error) { +func (p *applyPatcher) applyPatchToCurrentObject(_ context.Context, obj runtime.Object) (runtime.Object, error) { force := false if p.options.Force != nil { force = *p.options.Force @@ -460,7 +461,7 @@ func (p *applyPatcher) createNewObject() (runtime.Object, error) { if err != nil { return nil, fmt.Errorf("failed to create new object: %v", err) } - return p.applyPatchToCurrentObject(obj) + return p.applyPatchToCurrentObject(context.TODO(), obj) } // strategicPatchObject applies a strategic merge patch of to @@ -469,6 +470,7 @@ func (p *applyPatcher) createNewObject() (runtime.Object, error) { // and . // NOTE: Both and are supposed to be versioned. func strategicPatchObject( + requestContext context.Context, defaulter runtime.ObjectDefaulter, originalObject runtime.Object, patchBytes []byte, @@ -486,7 +488,7 @@ func strategicPatchObject( return errors.NewBadRequest(err.Error()) } - if err := applyPatchToObject(defaulter, originalObjMap, patchMap, objToUpdate, schemaReferenceObj, validationDirective); err != nil { + if err := applyPatchToObject(requestContext, defaulter, originalObjMap, patchMap, objToUpdate, schemaReferenceObj, validationDirective); err != nil { return err } return nil @@ -495,7 +497,7 @@ func strategicPatchObject( // applyPatch is called every time GuaranteedUpdate asks for the updated object, // and is given the currently persisted object as input. // TODO: rename this function because the name implies it is related to applyPatcher -func (p *patcher) applyPatch(_ context.Context, _, currentObject runtime.Object) (objToUpdate runtime.Object, patchErr error) { +func (p *patcher) applyPatch(ctx context.Context, _, currentObject runtime.Object) (objToUpdate runtime.Object, patchErr error) { // Make sure we actually have a persisted currentObject p.trace.Step("About to apply patch") currentObjectHasUID, err := hasUID(currentObject) @@ -504,7 +506,7 @@ func (p *patcher) applyPatch(_ context.Context, _, currentObject runtime.Object) } else if !currentObjectHasUID { objToUpdate, patchErr = p.mechanism.createNewObject() } else { - objToUpdate, patchErr = p.mechanism.applyPatchToCurrentObject(currentObject) + objToUpdate, patchErr = p.mechanism.applyPatchToCurrentObject(ctx, currentObject) } if patchErr != nil { @@ -633,6 +635,7 @@ func (p *patcher) patchResource(ctx context.Context, scope *RequestScope) (runti // and stores the result in . // NOTE: must be a versioned object. func applyPatchToObject( + requestContext context.Context, defaulter runtime.ObjectDefaulter, originalMap map[string]interface{}, patchMap map[string]interface{}, @@ -655,7 +658,7 @@ func applyPatchToObject( }) } if validationDirective == runtime.WarnFieldValidation { - // TODO: throw a warning here + warning.AddWarning(requestContext, "", err.Error()) } } // Decoding from JSON to a versioned object would apply defaults, so we do the same here diff --git a/test/integration/apiserver/field_validation_test.go b/test/integration/apiserver/field_validation_test.go index 41c376584c4fd..f1c925fe6af38 100644 --- a/test/integration/apiserver/field_validation_test.go +++ b/test/integration/apiserver/field_validation_test.go @@ -349,19 +349,33 @@ func smpRunTest(t testing.TB, client clientset.Interface, tc smpTestCase) { for k, v := range tc.params { req.Param(k, v) } - result, err := req.Body([]byte(`{"metadata":{"labels":{"label1": "val1"}},"spec":{"foo":"bar"}}`)).DoRaw(context.TODO()) + smpBody := `{"metadata":{"labels":{"label1": "val1"}},"spec":{"foo":"bar"}}` + result := req.Body([]byte(smpBody)).Do(context.TODO()) + if tc.warnContains != "" { + warningMatched := false + for _, w := range result.Warnings() { + if strings.Contains(w.String(), tc.warnContains) { + warningMatched = true + } + } + if !warningMatched { + t.Fatalf("expected warning to contain: %s, got warnings: %v", tc.warnContains, result.Warnings()) + } + } + resBody, err := result.Raw() if err == nil && tc.errContains != "" { - t.Fatalf("unexpected patch succeeded") + t.Fatalf("unexpected put succeeded") } - if err != nil && !strings.Contains(string(result), tc.errContains) { - t.Fatalf("unexpected response: %v", string(result)) + if err != nil && !strings.Contains(string(resBody), tc.errContains) { + t.Fatalf("unexpected response: %v", string(resBody)) } } type smpTestCase struct { - name string - params map[string]string - errContains string + name string + params map[string]string + errContains string + warnContains string } // TestFieldValidationSMP tests that attempting a strategic-merge-patch @@ -382,9 +396,13 @@ func TestFieldValidationSMP(t *testing.T) { errContains: "unknown field", }, { - name: "smp-ignore-validation", - params: map[string]string{}, - errContains: "", + name: "smp-strict-validation", + params: map[string]string{"fieldValidation": "Warn"}, + warnContains: "unknown field", + }, + { + name: "smp-ignore-validation", + params: map[string]string{}, }, } From 2e084cb72a694911094cebe3555801735a1f6d03 Mon Sep 17 00:00:00 2001 From: Kevin Delgado Date: Thu, 14 Oct 2021 17:19:12 +0000 Subject: [PATCH 57/64] Add warn for PatchCRD --- .../apiserver/field_validation_test.go | 56 ++++++++++++------- 1 file changed, 36 insertions(+), 20 deletions(-) diff --git a/test/integration/apiserver/field_validation_test.go b/test/integration/apiserver/field_validation_test.go index f1c925fe6af38..c3e23bfb6ccb8 100644 --- a/test/integration/apiserver/field_validation_test.go +++ b/test/integration/apiserver/field_validation_test.go @@ -68,7 +68,7 @@ func TestFieldValidationPost(t *testing.T) { errContains: "unknown field", }, { - name: "post-ignore-validation", + name: "post-warn-validation", params: map[string]string{"fieldValidation": "Warn"}, warnContains: "unknown field", }, @@ -157,7 +157,7 @@ func TestFieldValidationPut(t *testing.T) { errContains: "unknown field", }, { - name: "put-strict-validation", + name: "put-warn-validation", params: map[string]string{"fieldValidation": "Warn"}, warnContains: "unknown field", }, @@ -396,7 +396,7 @@ func TestFieldValidationSMP(t *testing.T) { errContains: "unknown field", }, { - name: "smp-strict-validation", + name: "smp-warn-validation", params: map[string]string{"fieldValidation": "Warn"}, warnContains: "unknown field", }, @@ -509,11 +509,12 @@ func patchCRDTestSetup(t testing.TB, server kubeapiservertesting.TestServer, nam // works for jsonpatch and mergepatch requests. func TestFieldValidationPatchCRD(t *testing.T) { var testcases = []struct { - name string - patchType types.PatchType - params map[string]string - body string - errContains string + name string + patchType types.PatchType + params map[string]string + body string + errContains string + warnContains string }{ { name: "merge-patch-strict-validation", @@ -523,11 +524,17 @@ func TestFieldValidationPatchCRD(t *testing.T) { errContains: "unknown field", }, { - name: "merge-patch-no-validation", - patchType: types.MergePatchType, - params: map[string]string{}, - body: `{"metadata":{"finalizers":["test-finalizer","another-one"]}, "spec":{"foo": "bar"}}`, - errContains: "", + name: "merge-patch-warn-validation", + patchType: types.MergePatchType, + params: map[string]string{"fieldValidation": "Warn"}, + body: `{"metadata":{"finalizers":["test-finalizer","another-one"]}, "spec":{"foo": "bar"}}`, + warnContains: "unknown field", + }, + { + name: "merge-patch-no-validation", + patchType: types.MergePatchType, + params: map[string]string{}, + body: `{"metadata":{"finalizers":["test-finalizer","another-one"]}, "spec":{"foo": "bar"}}`, }, // TODO: figure out how to test JSONPatch //{ @@ -562,15 +569,24 @@ func TestFieldValidationPatchCRD(t *testing.T) { for k, v := range tc.params { req = req.Param(k, v) } - result, err := req. - Body([]byte(tc.body)). - DoRaw(context.TODO()) + result := req.Body([]byte(tc.body)).Do(context.TODO()) + if tc.warnContains != "" { + warningMatched := false + for _, w := range result.Warnings() { + if strings.Contains(w.String(), tc.warnContains) { + warningMatched = true + } + } + if !warningMatched { + t.Fatalf("expected warning to contain: %s, got warnings: %v", tc.warnContains, result.Warnings()) + } + } + resBody, err := result.Raw() if err == nil && tc.errContains != "" { - t.Fatalf("unexpected patch succeeded, expected %s", tc.errContains) + t.Fatalf("unexpected put succeeded") } - if err != nil && !strings.Contains(string(result), tc.errContains) { - t.Errorf("bad err: %v", err) - t.Fatalf("unexpected response: %v", string(result)) + if err != nil && !strings.Contains(string(resBody), tc.errContains) { + t.Fatalf("unexpected response: %v", string(resBody)) } }) } From 9e24323b693dea689253c26f3d898fc8e57a800e Mon Sep 17 00:00:00 2001 From: Kevin Delgado Date: Thu, 14 Oct 2021 17:48:26 +0000 Subject: [PATCH 58/64] cleanup comment --- .../pkg/apiserver/schema/pruning/algorithm.go | 1 + .../apimachinery/pkg/runtime/converter.go | 6 +++++- .../pkg/endpoints/handlers/create.go | 1 - .../apiserver/pkg/endpoints/handlers/patch.go | 19 +++++++++---------- .../pkg/endpoints/handlers/update.go | 1 - 5 files changed, 15 insertions(+), 13 deletions(-) diff --git a/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/schema/pruning/algorithm.go b/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/schema/pruning/algorithm.go index 80986998d159b..9a17b22b7979c 100644 --- a/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/schema/pruning/algorithm.go +++ b/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/schema/pruning/algorithm.go @@ -23,6 +23,7 @@ import ( // Prune removes object fields in obj which are not specified in s. It skips TypeMeta and ObjectMeta fields // if XEmbeddedResource is set to true, or for the root if isResourceRoot=true, i.e. it does not // prune unknown metadata fields. +// It returns the set of fields that it prunes. func Prune(obj interface{}, s *structuralschema.Structural, isResourceRoot bool) map[string]bool { if isResourceRoot { if s == nil { diff --git a/staging/src/k8s.io/apimachinery/pkg/runtime/converter.go b/staging/src/k8s.io/apimachinery/pkg/runtime/converter.go index 1731cc1d4a2d5..879c13b026ef3 100644 --- a/staging/src/k8s.io/apimachinery/pkg/runtime/converter.go +++ b/staging/src/k8s.io/apimachinery/pkg/runtime/converter.go @@ -102,7 +102,9 @@ type unstructuredConverter struct { // This is supposed to be set only in tests. mismatchDetection bool // comparison is the default test logic used to compare - comparison conversion.Equalities + comparison conversion.Equalities + // fieldValidationDirective indicates what to do with unknown + // fields. It defaults to ignoring unkown fields. fieldValidationDirective FieldValidationDirective } @@ -126,6 +128,8 @@ const ( WarnFieldValidation ) +// SetFieldValidationDirective sets the directive for what the converter should do with +// unknown fields. func (c *unstructuredConverter) SetFieldValidationDirective(directive FieldValidationDirective) { c.fieldValidationDirective = directive } diff --git a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/create.go b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/create.go index 2fc4596f9156a..b7541ab90f8bc 100644 --- a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/create.go +++ b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/create.go @@ -116,7 +116,6 @@ func createHandler(r rest.NamedCreater, scope *RequestScope, admit admission.Int defaultGVK := scope.Kind original := r.New() - // TODO: put behind feature gate? validationDirective, err := fieldValidation(req) if err != nil { scope.err(err, w, req) diff --git a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/patch.go b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/patch.go index 0927d1673a417..cd4b19db8fe25 100644 --- a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/patch.go +++ b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/patch.go @@ -259,16 +259,15 @@ type mutateObjectUpdateFunc func(ctx context.Context, obj, old runtime.Object) e // moved into this type. type patcher struct { // Pieces of RequestScope - namer ScopeNamer - creater runtime.ObjectCreater - defaulter runtime.ObjectDefaulter - typer runtime.ObjectTyper - unsafeConvertor runtime.ObjectConvertor - resource schema.GroupVersionResource - kind schema.GroupVersionKind - subresource string - dryRun bool - //strictFieldValidation bool + namer ScopeNamer + creater runtime.ObjectCreater + defaulter runtime.ObjectDefaulter + typer runtime.ObjectTyper + unsafeConvertor runtime.ObjectConvertor + resource schema.GroupVersionResource + kind schema.GroupVersionKind + subresource string + dryRun bool validationDirective runtime.FieldValidationDirective objectInterfaces admission.ObjectInterfaces diff --git a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/update.go b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/update.go index 76ec874c9dee2..eae5efb2e0cc4 100644 --- a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/update.go +++ b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/update.go @@ -103,7 +103,6 @@ func UpdateResource(r rest.Updater, scope *RequestScope, admit admission.Interfa defaultGVK := scope.Kind original := r.New() - // TODO: put behind feature gate? validationDirective, err := fieldValidation(req) if err != nil { scope.err(err, w, req) From d57bcc94ddedec5fa394c7c9cf74228a47ed70ba Mon Sep 17 00:00:00 2001 From: Kevin Delgado Date: Tue, 19 Oct 2021 00:03:27 +0000 Subject: [PATCH 59/64] Implement {Create,Update,Patch}Options for field validation --- .../apimachinery/pkg/apis/meta/v1/types.go | 30 ++++ .../apiserver/pkg/endpoints/handlers/rest.go | 2 +- .../apiserver/field_validation_test.go | 143 +++++++++--------- 3 files changed, 106 insertions(+), 69 deletions(-) diff --git a/staging/src/k8s.io/apimachinery/pkg/apis/meta/v1/types.go b/staging/src/k8s.io/apimachinery/pkg/apis/meta/v1/types.go index 9660282c48bc1..b273b32cb4d3e 100644 --- a/staging/src/k8s.io/apimachinery/pkg/apis/meta/v1/types.go +++ b/staging/src/k8s.io/apimachinery/pkg/apis/meta/v1/types.go @@ -544,6 +544,16 @@ type CreateOptions struct { // as defined by https://golang.org/pkg/unicode/#IsPrint. // +optional FieldManager string `json:"fieldManager,omitempty" protobuf:"bytes,3,name=fieldManager"` + + // fieldValidation determines how the server should respond to + // unknown/duplicate fields. + // TODO: Do we still need a protobuf tag if protobuf is not supported? + // Valid values are: + // - Ignore: ignore's unknown/duplicate fields + // - Strict: fail the request on unknown/duplicate fields + // - Warn: respond with a warning, but successfully serve the request. + // +optional + FieldValidation string `json:"fieldValidation,omitempty"` } // +k8s:conversion-gen:explicit-from=net/url.Values @@ -577,6 +587,16 @@ type PatchOptions struct { // types (JsonPatch, MergePatch, StrategicMergePatch). // +optional FieldManager string `json:"fieldManager,omitempty" protobuf:"bytes,3,name=fieldManager"` + + // fieldValidation determines how the server should respond to + // unknown/duplicate fields. + // TODO: Do we still need a protobuf tag if protobuf is not supported? + // Valid values are: + // - Ignore: ignore's unknown/duplicate fields + // - Strict: fail the request on unknown/duplicate fields + // - Warn: respond with a warning, but successfully serve the request. + // +optional + FieldValidation string `json:"fieldValidation,omitempty"` } // ApplyOptions may be provided when applying an API object. @@ -632,6 +652,16 @@ type UpdateOptions struct { // as defined by https://golang.org/pkg/unicode/#IsPrint. // +optional FieldManager string `json:"fieldManager,omitempty" protobuf:"bytes,2,name=fieldManager"` + + // fieldValidation determines how the server should respond to + // unknown/duplicate fields. + // TODO: Do we still need a protobuf tag if protobuf is not supported? + // Valid values are: + // - Ignore: ignore's unknown/duplicate fields + // - Strict: fail the request on unknown/duplicate fields + // - Warn: respond with a warning, but successfully serve the request. + // +optional + FieldValidation string `json:"fieldValidation,omitempty"` } // Preconditions must be fulfilled before an operation (update, delete, etc.) is carried out. diff --git a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/rest.go b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/rest.go index 185621f2cfc59..9b8fca91f0c05 100644 --- a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/rest.go +++ b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/rest.go @@ -461,7 +461,7 @@ func isDryRun(url *url.URL) bool { // fieldValidation checks if the fieldValidation query parameter is set on the request, // and if so ensures that the parameter is valid and that the request has a valid // media type, because the list of media types that support field validation are a subset of -// all supported media types (only json and yaml supports field validation). +// all supported media types (protobuf does not support field validation). func fieldValidation(req *http.Request) (runtime.FieldValidationDirective, error) { supportedContentTypes := []string{runtime.ContentTypeJSON, runtime.ContentTypeJSONMergePatch, runtime.ContentTypeJSONStrategicMergePatch, runtime.ContentTypeYAML} contentType := req.Header.Get("Content-Type") diff --git a/test/integration/apiserver/field_validation_test.go b/test/integration/apiserver/field_validation_test.go index c3e23bfb6ccb8..24d275de7582e 100644 --- a/test/integration/apiserver/field_validation_test.go +++ b/test/integration/apiserver/field_validation_test.go @@ -29,6 +29,7 @@ import ( 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/types" "k8s.io/apiserver/pkg/features" utilfeature "k8s.io/apiserver/pkg/util/feature" @@ -56,29 +57,33 @@ func TestFieldValidationPost(t *testing.T) { body := []byte(fmt.Sprintf(string(bodyBytes), `"test-deployment"`)) var testcases = []struct { - name string - // TODO: use PostOptions for fieldValidation param instead of raw strings. - params map[string]string + name string + opts metav1.CreateOptions errContains string warnContains string }{ { name: "post-strict-validation", - params: map[string]string{"fieldValidation": "Strict"}, errContains: "unknown field", + opts: metav1.CreateOptions{ + FieldValidation: "Strict", + }, }, { name: "post-warn-validation", - params: map[string]string{"fieldValidation": "Warn"}, warnContains: "unknown field", + opts: metav1.CreateOptions{ + FieldValidation: "Warn", + }, }, { - name: "post-default-ignore-validation", - params: map[string]string{}, + name: "post-ignore-validation", + opts: metav1.CreateOptions{ + FieldValidation: "Ignore", + }, }, { - name: "post-ignore-validation", - params: map[string]string{"fieldValidation": "Ignore"}, + name: "post-default-ignore-validation", }, } @@ -87,11 +92,8 @@ func TestFieldValidationPost(t *testing.T) { req := client.CoreV1().RESTClient().Post(). AbsPath("/apis/apps/v1"). Namespace("default"). - Resource("deployments") - for k, v := range tc.params { - req.Param(k, v) - - } + Resource("deployments"). + VersionedParams(&tc.opts, metav1.ParameterCodec) result := req.Body([]byte(body)).Do(context.TODO()) if tc.warnContains != "" { warningMatched := false @@ -145,29 +147,33 @@ func TestFieldValidationPut(t *testing.T) { } putBody := []byte(fmt.Sprintf(string(putBytes), deployName)) var testcases = []struct { - name string - // TODO: use PostOptions for fieldValidation param instead of raw strings. - params map[string]string + name string + opts metav1.UpdateOptions errContains string warnContains string }{ { - name: "put-strict-validation", - params: map[string]string{"fieldValidation": "Strict"}, + name: "put-strict-validation", + opts: metav1.UpdateOptions{ + FieldValidation: "Strict", + }, errContains: "unknown field", }, { - name: "put-warn-validation", - params: map[string]string{"fieldValidation": "Warn"}, + name: "put-warn-validation", + opts: metav1.UpdateOptions{ + FieldValidation: "Warn", + }, warnContains: "unknown field", }, { - name: "put-default-ignore-validation", - params: map[string]string{}, + name: "put-default-ignore-validation", + opts: metav1.UpdateOptions{ + FieldValidation: "Ignore", + }, }, { - name: "put-ignore-validation", - params: map[string]string{"fieldValidation": "Ignore"}, + name: "put-ignore-validation", }, } @@ -177,11 +183,8 @@ func TestFieldValidationPut(t *testing.T) { AbsPath("/apis/apps/v1"). Namespace("default"). Resource("deployments"). - Name("test-dep") - for k, v := range tc.params { - req.Param(k, v) - - } + Name("test-dep"). + VersionedParams(&tc.opts, metav1.ParameterCodec) result := req.Body([]byte(putBody)).Do(context.TODO()) if tc.warnContains != "" { warningMatched := false @@ -345,10 +348,8 @@ func smpRunTest(t testing.TB, client clientset.Interface, tc smpTestCase) { AbsPath("/apis/apps/v1"). Namespace("default"). Resource("deployments"). - Name("test-deployment") - for k, v := range tc.params { - req.Param(k, v) - } + Name("test-deployment"). + VersionedParams(&tc.opts, metav1.ParameterCodec) smpBody := `{"metadata":{"labels":{"label1": "val1"}},"spec":{"foo":"bar"}}` result := req.Body([]byte(smpBody)).Do(context.TODO()) if tc.warnContains != "" { @@ -373,7 +374,7 @@ func smpRunTest(t testing.TB, client clientset.Interface, tc smpTestCase) { type smpTestCase struct { name string - params map[string]string + opts metav1.PatchOptions errContains string warnContains string } @@ -391,18 +392,21 @@ func TestFieldValidationSMP(t *testing.T) { var testcases = []smpTestCase{ { - name: "smp-strict-validation", - params: map[string]string{"fieldValidation": "Strict"}, + name: "smp-strict-validation", + opts: metav1.PatchOptions{ + FieldValidation: "Strict", + }, errContains: "unknown field", }, { - name: "smp-warn-validation", - params: map[string]string{"fieldValidation": "Warn"}, + name: "smp-warn-validation", + opts: metav1.PatchOptions{ + FieldValidation: "Warn", + }, warnContains: "unknown field", }, { - name: "smp-ignore-validation", - params: map[string]string{}, + name: "smp-ignore-validation", }, } @@ -425,13 +429,14 @@ func BenchmarkFieldValidationSMP(b *testing.B) { // TODO: add more benchmarks to test bigger objects var benchmarks = []smpTestCase{ { - name: "smp-strict-validation", - params: map[string]string{"fieldValidation": "Strict"}, + name: "smp-strict-validation", + opts: metav1.PatchOptions{ + FieldValidation: "Strict", + }, errContains: "unknown field", }, { name: "smp-ignore-validation", - params: map[string]string{}, errContains: "", }, } @@ -511,29 +516,32 @@ func TestFieldValidationPatchCRD(t *testing.T) { var testcases = []struct { name string patchType types.PatchType - params map[string]string + opts metav1.PatchOptions body string errContains string warnContains string }{ { - name: "merge-patch-strict-validation", - patchType: types.MergePatchType, - params: map[string]string{"fieldValidation": "Strict"}, + name: "merge-patch-strict-validation", + patchType: types.MergePatchType, + opts: metav1.PatchOptions{ + FieldValidation: "Strict", + }, body: `{"metadata":{"finalizers":["test-finalizer","another-one"]}, "spec":{"foo": "bar"}}`, errContains: "unknown field", }, { - name: "merge-patch-warn-validation", - patchType: types.MergePatchType, - params: map[string]string{"fieldValidation": "Warn"}, + name: "merge-patch-warn-validation", + patchType: types.MergePatchType, + opts: metav1.PatchOptions{ + FieldValidation: "Warn", + }, body: `{"metadata":{"finalizers":["test-finalizer","another-one"]}, "spec":{"foo": "bar"}}`, warnContains: "unknown field", }, { name: "merge-patch-no-validation", patchType: types.MergePatchType, - params: map[string]string{}, body: `{"metadata":{"finalizers":["test-finalizer","another-one"]}, "spec":{"foo": "bar"}}`, }, // TODO: figure out how to test JSONPatch @@ -565,10 +573,8 @@ func TestFieldValidationPatchCRD(t *testing.T) { // patch the CR as specified by the test case req := rest.Patch(tc.patchType). AbsPath("/apis", noxuDefinition.Spec.Group, noxuDefinition.Spec.Versions[0].Name, noxuDefinition.Spec.Names.Plural). - Name(tc.name) - for k, v := range tc.params { - req = req.Param(k, v) - } + Name(tc.name). + VersionedParams(&tc.opts, metav1.ParameterCodec) result := req.Body([]byte(tc.body)).Do(context.TODO()) if tc.warnContains != "" { warningMatched := false @@ -597,6 +603,7 @@ func BenchmarkFieldValidationPatchCRD(b *testing.B) { benchmarks := []struct { name string patchType types.PatchType + opts metav1.PatchOptions params map[string]string bodyBase string errContains string @@ -604,28 +611,30 @@ func BenchmarkFieldValidationPatchCRD(b *testing.B) { { name: "ignore-validation-crd-patch", patchType: types.MergePatchType, - params: map[string]string{}, bodyBase: `{"metadata":{"finalizers":["test-finalizer","finalizer-ignore-%d"]}}`, errContains: "", }, { - name: "strict-validation-crd-patch", - patchType: types.MergePatchType, - params: map[string]string{"fieldValidation": "Strict"}, + name: "strict-validation-crd-patch", + patchType: types.MergePatchType, + opts: metav1.PatchOptions{ + FieldValidation: "Strict", + }, bodyBase: `{"metadata":{"finalizers":["test-finalizer","finalizer-strict-%d"]}}`, errContains: "", }, { name: "ignore-validation-crd-patch-unknown-field", patchType: types.MergePatchType, - params: map[string]string{}, bodyBase: `{"metadata":{"finalizers":["test-finalizer","finalizer-ignore-unknown-%d"]}, "spec":{"foo": "bar"}}`, errContains: "", }, { - name: "strict-validation-crd-patch-unknown-field", - patchType: types.MergePatchType, - params: map[string]string{"fieldValidation": "Strict"}, + name: "strict-validation-crd-patch-unknown-field", + patchType: types.MergePatchType, + opts: metav1.PatchOptions{ + FieldValidation: "Strict", + }, bodyBase: `{"metadata":{"finalizers":["test-finalizer","finalizer-strict-unknown-%d"]}, "spec":{"foo": "bar"}}`, errContains: "unknown field", }, @@ -648,10 +657,8 @@ func BenchmarkFieldValidationPatchCRD(b *testing.B) { // patch the CR as specified by the test case req := rest.Patch(bm.patchType). AbsPath("/apis", noxuDefinition.Spec.Group, noxuDefinition.Spec.Versions[0].Name, noxuDefinition.Spec.Names.Plural). - Name(bm.name) - for k, v := range bm.params { - req = req.Param(k, v) - } + Name(bm.name). + VersionedParams(&bm.opts, metav1.ParameterCodec) result, err := req. Body([]byte(body)). DoRaw(context.TODO()) From fb910cd593e57d45940a9a78939b3650ac386dc6 Mon Sep 17 00:00:00 2001 From: Kevin Delgado Date: Tue, 19 Oct 2021 18:49:44 +0000 Subject: [PATCH 60/64] Add feature gate for FieldValidation --- pkg/features/kube_features.go | 1 + .../k8s.io/apiserver/pkg/endpoints/handlers/rest.go | 4 ++++ .../k8s.io/apiserver/pkg/features/kube_features.go | 8 ++++++++ test/integration/apiserver/field_validation_test.go | 12 ++++++++++-- 4 files changed, 23 insertions(+), 2 deletions(-) diff --git a/pkg/features/kube_features.go b/pkg/features/kube_features.go index 8c4d8253b0d97..e5d0ba2eebc34 100644 --- a/pkg/features/kube_features.go +++ b/pkg/features/kube_features.go @@ -904,6 +904,7 @@ var defaultKubernetesFeatureGates = map[featuregate.Feature]featuregate.FeatureS genericfeatures.WarningHeaders: {Default: true, PreRelease: featuregate.GA, LockToDefault: true}, // remove in 1.24 genericfeatures.OpenAPIEnums: {Default: false, PreRelease: featuregate.Alpha}, genericfeatures.CustomResourceValidationExpressions: {Default: false, PreRelease: featuregate.Alpha}, + genericfeatures.FieldValidation: {Default: false, PreRelease: featuregate.Alpha}, // features that enable backwards compatibility but are scheduled to be removed // ... HPAScaleToZero: {Default: false, PreRelease: featuregate.Alpha}, diff --git a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/rest.go b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/rest.go index 9b8fca91f0c05..f079f4eb2a288 100644 --- a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/rest.go +++ b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/rest.go @@ -463,6 +463,10 @@ func isDryRun(url *url.URL) bool { // media type, because the list of media types that support field validation are a subset of // all supported media types (protobuf does not support field validation). func fieldValidation(req *http.Request) (runtime.FieldValidationDirective, error) { + if !utilfeature.DefaultFeatureGate.Enabled(features.FieldValidation) { + return runtime.IgnoreFieldValidation, nil + } + supportedContentTypes := []string{runtime.ContentTypeJSON, runtime.ContentTypeJSONMergePatch, runtime.ContentTypeJSONStrategicMergePatch, runtime.ContentTypeYAML} contentType := req.Header.Get("Content-Type") // TODO: not sure if it is okay to assume empty content type is a valid one diff --git a/staging/src/k8s.io/apiserver/pkg/features/kube_features.go b/staging/src/k8s.io/apiserver/pkg/features/kube_features.go index d0da61e57b955..29768ea9015f0 100644 --- a/staging/src/k8s.io/apiserver/pkg/features/kube_features.go +++ b/staging/src/k8s.io/apiserver/pkg/features/kube_features.go @@ -185,6 +185,13 @@ const ( // // Enables expression validation for Custom Resource CustomResourceValidationExpressions featuregate.Feature = "CustomResourceValidationExpressions" + + // owner: @kevindelgado + // kep: http://kep.k8s.io/2885 + // alpha: v1.23 + // + // Enables server-side field validation. + FieldValidation featuregate.Feature = "FieldValidation" ) func init() { @@ -215,4 +222,5 @@ var defaultKubernetesFeatureGates = map[featuregate.Feature]featuregate.FeatureS APIServerTracing: {Default: false, PreRelease: featuregate.Alpha}, OpenAPIEnums: {Default: false, PreRelease: featuregate.Alpha}, CustomResourceValidationExpressions: {Default: false, PreRelease: featuregate.Alpha}, + FieldValidation: {Default: false, PreRelease: featuregate.Alpha}, } diff --git a/test/integration/apiserver/field_validation_test.go b/test/integration/apiserver/field_validation_test.go index 24d275de7582e..52a88258d1ad5 100644 --- a/test/integration/apiserver/field_validation_test.go +++ b/test/integration/apiserver/field_validation_test.go @@ -45,7 +45,7 @@ import ( // TestFieldValidationPost tests POST requests containing unknown fields with // strict and non-strict field validation. func TestFieldValidationPost(t *testing.T) { - defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.ServerSideApply, true)() + defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.FieldValidation, true)() _, client, closeFn := setup(t) defer closeFn() @@ -120,7 +120,7 @@ func TestFieldValidationPost(t *testing.T) { // TestFieldValidationPut tests PUT requests containing unknown fields with // strict and non-strict field validation. func TestFieldValidationPut(t *testing.T) { - defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.ServerSideApply, true)() + defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.FieldValidation, true)() _, client, closeFn := setup(t) defer closeFn() @@ -210,6 +210,8 @@ func TestFieldValidationPut(t *testing.T) { // Benchmark field validation for strict vs non-strict func BenchmarkFieldValidationPostPut(b *testing.B) { + defer featuregatetesting.SetFeatureGateDuringTest(b, utilfeature.DefaultFeatureGate, features.FieldValidation, true)() + _, client, closeFn := setup(b) defer closeFn() @@ -384,6 +386,7 @@ type smpTestCase struct { // but succeeds when fieldValidation is ignored. func TestFieldValidationSMP(t *testing.T) { defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.ServerSideApply, true)() + defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.FieldValidation, true)() _, client, closeFn := setup(t) defer closeFn() @@ -420,6 +423,7 @@ func TestFieldValidationSMP(t *testing.T) { // Benchmark strategic-merge-patch field validation for strict vs non-strict func BenchmarkFieldValidationSMP(b *testing.B) { defer featuregatetesting.SetFeatureGateDuringTest(b, utilfeature.DefaultFeatureGate, features.ServerSideApply, true)() + defer featuregatetesting.SetFeatureGateDuringTest(b, utilfeature.DefaultFeatureGate, features.FieldValidation, true)() _, client, closeFn := setup(b) defer closeFn() @@ -513,6 +517,8 @@ func patchCRDTestSetup(t testing.TB, server kubeapiservertesting.TestServer, nam // TestFieldValidationPatchCRD tests that server-side schema validation // works for jsonpatch and mergepatch requests. func TestFieldValidationPatchCRD(t *testing.T) { + defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.FieldValidation, true)() + var testcases = []struct { name string patchType types.PatchType @@ -600,6 +606,8 @@ func TestFieldValidationPatchCRD(t *testing.T) { // Benchmark patch CRD for strict vs non-strict func BenchmarkFieldValidationPatchCRD(b *testing.B) { + defer featuregatetesting.SetFeatureGateDuringTest(b, utilfeature.DefaultFeatureGate, features.FieldValidation, true)() + benchmarks := []struct { name string patchType types.PatchType From 8bc736ab3284cc9d47ea2b57502b6ab3757d6075 Mon Sep 17 00:00:00 2001 From: Kevin Delgado Date: Wed, 20 Oct 2021 20:44:40 +0000 Subject: [PATCH 61/64] Add JSONPatch tests --- .../k8s.io/apimachinery/pkg/runtime/types.go | 1 + .../apiserver/pkg/endpoints/handlers/rest.go | 7 +++-- .../apiserver/field_validation_test.go | 29 +++++++++---------- 3 files changed, 20 insertions(+), 17 deletions(-) diff --git a/staging/src/k8s.io/apimachinery/pkg/runtime/types.go b/staging/src/k8s.io/apimachinery/pkg/runtime/types.go index ac01833304593..d8d8bf633ed28 100644 --- a/staging/src/k8s.io/apimachinery/pkg/runtime/types.go +++ b/staging/src/k8s.io/apimachinery/pkg/runtime/types.go @@ -42,6 +42,7 @@ type TypeMeta struct { const ( ContentTypeJSON string = "application/json" + ContentTypeJSONPatch string = "application/json-patch+json" ContentTypeJSONMergePatch string = "application/merge-patch+json" ContentTypeJSONStrategicMergePatch string = "application/strategic-merge-patch+json" ContentTypeYAML string = "application/yaml" diff --git a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/rest.go b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/rest.go index f079f4eb2a288..58596b8608ae6 100644 --- a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/rest.go +++ b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/rest.go @@ -467,9 +467,12 @@ func fieldValidation(req *http.Request) (runtime.FieldValidationDirective, error return runtime.IgnoreFieldValidation, nil } - supportedContentTypes := []string{runtime.ContentTypeJSON, runtime.ContentTypeJSONMergePatch, runtime.ContentTypeJSONStrategicMergePatch, runtime.ContentTypeYAML} + // TODO: Should we blocklist unsupportedContentTypes (just protobuf) rather than allowlisting everything that isn't protobuf? + // TODO: Is there a better way to determine if something is JSON or YAML by its media type suffix rather than adding all these + // ContentTypes to the runtime package? + supportedContentTypes := []string{runtime.ContentTypeJSON, runtime.ContentTypeJSONPatch, runtime.ContentTypeJSONMergePatch, runtime.ContentTypeJSONStrategicMergePatch, runtime.ContentTypeYAML} contentType := req.Header.Get("Content-Type") - // TODO: not sure if it is okay to assume empty content type is a valid one + // TODO: Is it okay to assume empty content type is a valid one? supported := true if contentType != "" { supported = false diff --git a/test/integration/apiserver/field_validation_test.go b/test/integration/apiserver/field_validation_test.go index 52a88258d1ad5..e11c641085935 100644 --- a/test/integration/apiserver/field_validation_test.go +++ b/test/integration/apiserver/field_validation_test.go @@ -550,21 +550,20 @@ func TestFieldValidationPatchCRD(t *testing.T) { patchType: types.MergePatchType, body: `{"metadata":{"finalizers":["test-finalizer","another-one"]}, "spec":{"foo": "bar"}}`, }, - // TODO: figure out how to test JSONPatch - //{ - // name: "jsonPatchStrictValidation", - // patchType: types.JSONPatchType, - // params: map[string]string{"validate": "strict"}, - // body: // TODO - // errContains: "failed with unknown fields", - //}, - //{ - // name: "jsonPatchNoValidation", - // patchType: types.JSONPatchType, - // params: map[string]string{}, - // body: // TODO - // errContains: "", - //}, + { + name: "json-patch-strict-validation", + patchType: types.JSONPatchType, + opts: metav1.PatchOptions{ + FieldValidation: "Strict", + }, + body: `[{"op": "add", "path": "/spec/foo", "value": "bar"}]`, + errContains: "unknown field", + }, + { + name: "json-patch-strict-validation", + patchType: types.JSONPatchType, + body: `[{"op": "add", "path": "/spec/foo", "value": "bar"}]`, + }, } for _, tc := range testcases { t.Run(tc.name, func(t *testing.T) { From d445e144d3fd9efae0e147943df729b3309be3a5 Mon Sep 17 00:00:00 2001 From: Kevin Delgado Date: Wed, 20 Oct 2021 21:24:50 +0000 Subject: [PATCH 62/64] wip: debug smp --- .../apimachinery/pkg/runtime/converter.go | 1 + .../apiserver/field_validation_test.go | 20 ++++++++++++------- 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/staging/src/k8s.io/apimachinery/pkg/runtime/converter.go b/staging/src/k8s.io/apimachinery/pkg/runtime/converter.go index 879c13b026ef3..9ce3a22316419 100644 --- a/staging/src/k8s.io/apimachinery/pkg/runtime/converter.go +++ b/staging/src/k8s.io/apimachinery/pkg/runtime/converter.go @@ -425,6 +425,7 @@ func (c *unstructuredConverter) structFromUnstructured(sv, dv reflect.Value, fie } } if len(keys) > 0 && c.fieldValidationDirective == StrictFieldValidation || c.fieldValidationDirective == WarnFieldValidation { + klog.Warningf("keys are: %v", keys) allStrictErrs := make([]error, len(keys)) for i, unknownField := range keys { allStrictErrs[i] = fmt.Errorf("unknown field: %s", unknownField.String()) diff --git a/test/integration/apiserver/field_validation_test.go b/test/integration/apiserver/field_validation_test.go index e11c641085935..3e9ed9cab26d4 100644 --- a/test/integration/apiserver/field_validation_test.go +++ b/test/integration/apiserver/field_validation_test.go @@ -37,6 +37,7 @@ import ( clientset "k8s.io/client-go/kubernetes" restclient "k8s.io/client-go/rest" featuregatetesting "k8s.io/component-base/featuregate/testing" + "k8s.io/klog/v2" kubeapiservertesting "k8s.io/kubernetes/cmd/kube-apiserver/app/testing" "k8s.io/kubernetes/test/integration/framework" @@ -54,7 +55,6 @@ func TestFieldValidationPost(t *testing.T) { if err != nil { t.Fatalf("failed to read file: %v", err) } - body := []byte(fmt.Sprintf(string(bodyBytes), `"test-deployment"`)) var testcases = []struct { name string @@ -89,6 +89,7 @@ func TestFieldValidationPost(t *testing.T) { for _, tc := range testcases { t.Run(tc.name, func(t *testing.T) { + body := []byte(fmt.Sprintf(string(bodyBytes), fmt.Sprintf(`"test-deployment-%s"`, tc.name))) req := client.CoreV1().RESTClient().Post(). AbsPath("/apis/apps/v1"). Namespace("default"). @@ -110,7 +111,8 @@ func TestFieldValidationPost(t *testing.T) { if err == nil && tc.errContains != "" { t.Fatalf("unexpected post succeeded") } - if err != nil && !strings.Contains(string(resBody), tc.errContains) { + //if err != nil && !strings.Contains(string(resBody), tc.errContains) { + if err != nil && (tc.errContains == "" || !strings.Contains(string(resBody), tc.errContains)) { t.Fatalf("unexpected response: %v", string(resBody)) } }) @@ -183,7 +185,7 @@ func TestFieldValidationPut(t *testing.T) { AbsPath("/apis/apps/v1"). Namespace("default"). Resource("deployments"). - Name("test-dep"). + Name("test-deployment"). VersionedParams(&tc.opts, metav1.ParameterCodec) result := req.Body([]byte(putBody)).Do(context.TODO()) if tc.warnContains != "" { @@ -201,7 +203,8 @@ func TestFieldValidationPut(t *testing.T) { if err == nil && tc.errContains != "" { t.Fatalf("unexpected put succeeded") } - if err != nil && !strings.Contains(string(resBody), tc.errContains) { + //if err != nil && !strings.Contains(string(resBody), tc.errContains) { + if err != nil && (tc.errContains == "" || !strings.Contains(string(resBody), tc.errContains)) { t.Fatalf("unexpected response: %v", string(resBody)) } }) @@ -335,7 +338,7 @@ func smpTestSetup(t testing.TB, client clientset.Interface) { Resource("deployments"). Name("test-deployment"). Param("fieldManager", "apply_test"). - Body([]byte(fmt.Sprintf(string(bodyBase), "test-deployment"))). + Body([]byte(fmt.Sprintf(string(bodyBase), `"test-deployment"`))). Do(context.TODO()). Get() if err != nil { @@ -366,10 +369,12 @@ func smpRunTest(t testing.TB, client clientset.Interface, tc smpTestCase) { } } resBody, err := result.Raw() + klog.Warningf("result: %v", string(resBody)) if err == nil && tc.errContains != "" { t.Fatalf("unexpected put succeeded") } - if err != nil && !strings.Contains(string(resBody), tc.errContains) { + //if err != nil && !strings.Contains(string(resBody), tc.errContains) { + if err != nil && (tc.errContains == "" || !strings.Contains(string(resBody), tc.errContains)) { t.Fatalf("unexpected response: %v", string(resBody)) } } @@ -593,10 +598,11 @@ func TestFieldValidationPatchCRD(t *testing.T) { } } resBody, err := result.Raw() + fmt.Printf("tname %s, resBody: %s, err: %v", tc.name, string(resBody), err) if err == nil && tc.errContains != "" { t.Fatalf("unexpected put succeeded") } - if err != nil && !strings.Contains(string(resBody), tc.errContains) { + if err != nil && (tc.errContains == "" || !strings.Contains(string(resBody), tc.errContains)) { t.Fatalf("unexpected response: %v", string(resBody)) } }) From 773122061551f5b180c29b5e22ccb62a3e742e14 Mon Sep 17 00:00:00 2001 From: Kevin Delgado Date: Thu, 21 Oct 2021 23:54:29 +0000 Subject: [PATCH 63/64] fix unstructured converter --- .../apimachinery/pkg/runtime/converter.go | 129 +++++++++++------- 1 file changed, 77 insertions(+), 52 deletions(-) diff --git a/staging/src/k8s.io/apimachinery/pkg/runtime/converter.go b/staging/src/k8s.io/apimachinery/pkg/runtime/converter.go index 9ce3a22316419..1e5db11b09f07 100644 --- a/staging/src/k8s.io/apimachinery/pkg/runtime/converter.go +++ b/staging/src/k8s.io/apimachinery/pkg/runtime/converter.go @@ -134,14 +134,6 @@ func (c *unstructuredConverter) SetFieldValidationDirective(directive FieldValid c.fieldValidationDirective = directive } -func makeFields(u map[string]interface{}) map[string]struct{} { - fields := make(map[string]struct{}, len(u)) - for k, _ := range u { - fields[k] = struct{}{} - } - return fields -} - // FromUnstructured converts an object from map[string]interface{} representation into a concrete type. // It uses encoding/json/Unmarshaler if object implements it or reflection if not. func (c *unstructuredConverter) FromUnstructured(u map[string]interface{}, obj interface{}) error { @@ -150,8 +142,7 @@ func (c *unstructuredConverter) FromUnstructured(u map[string]interface{}, obj i if t.Kind() != reflect.Ptr || value.IsNil() { return fmt.Errorf("FromUnstructured requires a non-nil pointer to an object, got %v", t) } - fields := makeFields(u) - err := c.fromUnstructured(reflect.ValueOf(u), value.Elem(), fields) + err := c.fromUnstructured(reflect.ValueOf(u), value.Elem()) if c.mismatchDetection { newObj := reflect.New(t.Elem()).Interface() newErr := fromUnstructuredViaJSON(u, newObj) @@ -173,7 +164,7 @@ func fromUnstructuredViaJSON(u map[string]interface{}, obj interface{}) error { return json.Unmarshal(data, obj) } -func (c *unstructuredConverter) fromUnstructured(sv, dv reflect.Value, fields map[string]struct{}) error { +func (c *unstructuredConverter) fromUnstructured(sv, dv reflect.Value) error { sv = unwrapInterface(sv) if !sv.IsValid() { dv.Set(reflect.Zero(dv.Type())) @@ -241,14 +232,14 @@ func (c *unstructuredConverter) fromUnstructured(sv, dv reflect.Value, fields ma switch dt.Kind() { case reflect.Map: - err := c.mapFromUnstructured(sv, dv, fields) + err := c.mapFromUnstructured(sv, dv) return err case reflect.Slice: - return c.sliceFromUnstructured(sv, dv, fields) + return c.sliceFromUnstructured(sv, dv) case reflect.Ptr: - return c.pointerFromUnstructured(sv, dv, fields) + return c.pointerFromUnstructured(sv, dv) case reflect.Struct: - err := c.structFromUnstructured(sv, dv, fields) + err := c.structFromUnstructured(sv, dv) return err case reflect.Interface: return interfaceFromUnstructured(sv, dv) @@ -305,7 +296,7 @@ func unwrapInterface(v reflect.Value) reflect.Value { return v } -func (c *unstructuredConverter) mapFromUnstructured(sv, dv reflect.Value, fields map[string]struct{}) error { +func (c *unstructuredConverter) mapFromUnstructured(sv, dv reflect.Value) error { st, dt := sv.Type(), dv.Type() if st.Kind() != reflect.Map { return fmt.Errorf("cannot restore map from %v", st.Kind()) @@ -323,7 +314,7 @@ func (c *unstructuredConverter) mapFromUnstructured(sv, dv reflect.Value, fields for _, key := range sv.MapKeys() { value := reflect.New(dt.Elem()).Elem() if val := unwrapInterface(sv.MapIndex(key)); val.IsValid() { - if err := c.fromUnstructured(val, value, fields); err != nil { + if err := c.fromUnstructured(val, value); err != nil { return err } } else { @@ -338,7 +329,7 @@ func (c *unstructuredConverter) mapFromUnstructured(sv, dv reflect.Value, fields return nil } -func (c *unstructuredConverter) sliceFromUnstructured(sv, dv reflect.Value, fields map[string]struct{}) error { +func (c *unstructuredConverter) sliceFromUnstructured(sv, dv reflect.Value) error { st, dt := sv.Type(), dv.Type() if st.Kind() == reflect.String && dt.Elem().Kind() == reflect.Uint8 { // We store original []byte representation as string. @@ -371,14 +362,14 @@ func (c *unstructuredConverter) sliceFromUnstructured(sv, dv reflect.Value, fiel } dv.Set(reflect.MakeSlice(dt, sv.Len(), sv.Cap())) for i := 0; i < sv.Len(); i++ { - if err := c.fromUnstructured(sv.Index(i), dv.Index(i), fields); err != nil { + if err := c.fromUnstructured(sv.Index(i), dv.Index(i)); err != nil { return err } } return nil } -func (c *unstructuredConverter) pointerFromUnstructured(sv, dv reflect.Value, fields map[string]struct{}) error { +func (c *unstructuredConverter) pointerFromUnstructured(sv, dv reflect.Value) error { st, dt := sv.Type(), dv.Type() if st.Kind() == reflect.Ptr && sv.IsNil() { @@ -388,53 +379,87 @@ func (c *unstructuredConverter) pointerFromUnstructured(sv, dv reflect.Value, fi dv.Set(reflect.New(dt.Elem())) switch st.Kind() { case reflect.Ptr, reflect.Interface: - return c.fromUnstructured(sv.Elem(), dv.Elem(), fields) + return c.fromUnstructured(sv.Elem(), dv.Elem()) default: - return c.fromUnstructured(sv, dv.Elem(), fields) + return c.fromUnstructured(sv, dv.Elem()) } } -func (c *unstructuredConverter) structFromUnstructured(sv, dv reflect.Value, fields map[string]struct{}) error { - st, dt := sv.Type(), dv.Type() - if st.Kind() != reflect.Map { - return fmt.Errorf("cannot restore struct from: %v", st.Kind()) - } - keys := sv.MapKeys() - +func flattenedFields(dv reflect.Value) map[reflect.Value]reflect.Value { + klog.Warningf("fF called: %v", dv) + m := map[reflect.Value]reflect.Value{} + dt := dv.Type() for i := 0; i < dt.NumField(); i++ { + klog.Warningf("fF i: %d", i) fieldInfo := fieldInfoFromField(dt, i) fv := dv.Field(i) + if len(fieldInfo.name) == 0 { - // This field is inlined. - if err := c.fromUnstructured(sv, fv, fields); err != nil { - return err + //inlined, recurse + klog.Warningf("fF inlined") + inlinedFields := flattenedFields(fv) + for k, v := range inlinedFields { + m[k] = v } } else { - // delete from the source's keys - deleteFromKeys(fieldInfo.name, &keys) - delete(fields, fieldInfo.name) - - value := unwrapInterface(sv.MapIndex(fieldInfo.nameValue)) - if value.IsValid() { - if err := c.fromUnstructured(value, fv, fields); err != nil { - return err - } - } else { - fv.Set(reflect.Zero(fv.Type())) + klog.Warningf("fF field: %s", fieldInfo.nameValue.String()) + m[fieldInfo.nameValue] = fv + } + } + return m +} + +func (c *unstructuredConverter) structFromUnstructured(sv, dv reflect.Value) error { + st := sv.Type() + if st.Kind() != reflect.Map { + return fmt.Errorf("cannot restore struct from: %v", st.Kind()) + } + + // TODO: benchmark whether this flatten step actually is less performant + // and thus we need to only conditionally do it for non-ignore case + dtFieldsForFieldName := flattenedFields(dv) + var strictDecodingErr error + if c.fieldValidationDirective != IgnoreFieldValidation { + fieldNameStrings := map[string]struct{}{} + for nameValue := range dtFieldsForFieldName { + fieldNameStrings[nameValue.String()] = struct{}{} + } + + klog.Warningf("dFields: %v\n", dtFieldsForFieldName) + unknownFields := []reflect.Value{} + for _, key := range sv.MapKeys() { + klog.Warningf("sv key %s", key.String()) + if _, ok := fieldNameStrings[key.String()]; !ok { + klog.Warningf("found unknown field: %s", key.String()) + unknownFields = append(unknownFields, key) + } + } + + if len(unknownFields) > 0 { + klog.Warningf("directive is %v", c.fieldValidationDirective) + klog.Warningf("uFs are: %v", unknownFields) + allStrictErrs := make([]error, len(unknownFields)) + for i, unknownField := range unknownFields { + allStrictErrs[i] = fmt.Errorf("unknown field: %s", unknownField.String()) + } + strictDecodingErr = NewStrictDecodingError(allStrictErrs) + if c.fieldValidationDirective == StrictFieldValidation { + return strictDecodingErr } } } - if len(keys) > 0 && c.fieldValidationDirective == StrictFieldValidation || c.fieldValidationDirective == WarnFieldValidation { - klog.Warningf("keys are: %v", keys) - allStrictErrs := make([]error, len(keys)) - for i, unknownField := range keys { - allStrictErrs[i] = fmt.Errorf("unknown field: %s", unknownField.String()) - i++ + + for fieldName, fv := range dtFieldsForFieldName { + value := unwrapInterface(sv.MapIndex(fieldName)) + if value.IsValid() { + if err := c.fromUnstructured(value, fv); err != nil { + return err + } + } else { + fv.Set(reflect.Zero(fv.Type())) } - err := NewStrictDecodingError(allStrictErrs) - return err } - return nil + return strictDecodingErr } func deleteFromKeys(name string, keys *[]reflect.Value) { From adcd819c54caf6212a0708bceb56a0a30590ea8b Mon Sep 17 00:00:00 2001 From: Kevin Delgado Date: Fri, 22 Oct 2021 00:00:16 +0000 Subject: [PATCH 64/64] SMP converter, remove debug printing and add commentary --- .../apimachinery/pkg/runtime/converter.go | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/staging/src/k8s.io/apimachinery/pkg/runtime/converter.go b/staging/src/k8s.io/apimachinery/pkg/runtime/converter.go index 1e5db11b09f07..9f5a5824e18ba 100644 --- a/staging/src/k8s.io/apimachinery/pkg/runtime/converter.go +++ b/staging/src/k8s.io/apimachinery/pkg/runtime/converter.go @@ -385,24 +385,22 @@ func (c *unstructuredConverter) pointerFromUnstructured(sv, dv reflect.Value) er } } +// flattenedFields takes a value and returns all inlined fields as top +// level fields in order to determine which fields are invalid. func flattenedFields(dv reflect.Value) map[reflect.Value]reflect.Value { - klog.Warningf("fF called: %v", dv) m := map[reflect.Value]reflect.Value{} dt := dv.Type() for i := 0; i < dt.NumField(); i++ { - klog.Warningf("fF i: %d", i) fieldInfo := fieldInfoFromField(dt, i) fv := dv.Field(i) if len(fieldInfo.name) == 0 { //inlined, recurse - klog.Warningf("fF inlined") inlinedFields := flattenedFields(fv) for k, v := range inlinedFields { m[k] = v } } else { - klog.Warningf("fF field: %s", fieldInfo.nameValue.String()) m[fieldInfo.nameValue] = fv } } @@ -425,19 +423,20 @@ func (c *unstructuredConverter) structFromUnstructured(sv, dv reflect.Value) err fieldNameStrings[nameValue.String()] = struct{}{} } - klog.Warningf("dFields: %v\n", dtFieldsForFieldName) + // for every field in sv confirm that it exists in the + // flattened fields set of dv. + // If not, add it to the slice of unknown fields. unknownFields := []reflect.Value{} for _, key := range sv.MapKeys() { - klog.Warningf("sv key %s", key.String()) if _, ok := fieldNameStrings[key.String()]; !ok { - klog.Warningf("found unknown field: %s", key.String()) unknownFields = append(unknownFields, key) } } + // if there are unknown fields in sv + // return the decoding error immediately for the strict directive, + // but for the warn directive, save the error and return it after conversion. if len(unknownFields) > 0 { - klog.Warningf("directive is %v", c.fieldValidationDirective) - klog.Warningf("uFs are: %v", unknownFields) allStrictErrs := make([]error, len(unknownFields)) for i, unknownField := range unknownFields { allStrictErrs[i] = fmt.Errorf("unknown field: %s", unknownField.String())