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 c5432cb263d..b2ea74aec0b 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 @@ -20,11 +20,26 @@ import ( structuralschema "k8s.io/apiextensions-apiserver/pkg/apiserver/schema" ) -// Prune removes object fields in obj which are not specified in s. -func Prune(obj interface{}, s *structuralschema.Structural) { +// 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 root=true. +func Prune(obj interface{}, s *structuralschema.Structural, root bool) { + if root { + if s == nil { + s = &structuralschema.Structural{} + } + clone := *s + clone.XEmbeddedResource = true + s = &clone + } prune(obj, s) } +var metaFields = map[string]bool{ + "apiVersion": true, + "kind": true, + "metadata": true, +} + func prune(x interface{}, s *structuralschema.Structural) { if s != nil && s.XPreserveUnknownFields { skipPrune(x, s) @@ -40,6 +55,9 @@ func prune(x interface{}, s *structuralschema.Structural) { return } for k, v := range x { + if s.XEmbeddedResource && metaFields[k] { + continue + } prop, ok := s.Properties[k] if ok { prune(v, &prop) @@ -72,6 +90,9 @@ func skipPrune(x interface{}, s *structuralschema.Structural) { switch x := x.(type) { case map[string]interface{}: for k, v := range x { + if s.XEmbeddedResource && metaFields[k] { + continue + } if prop, ok := s.Properties[k]; ok { prune(v, &prop) } else { diff --git a/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/schema/pruning/algorithm_test.go b/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/schema/pruning/algorithm_test.go index 94e6de774cb..bb1c494fb27 100644 --- a/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/schema/pruning/algorithm_test.go +++ b/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/schema/pruning/algorithm_test.go @@ -23,31 +23,33 @@ import ( structuralschema "k8s.io/apiextensions-apiserver/pkg/apiserver/schema" "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/util/diff" "k8s.io/apimachinery/pkg/util/json" ) func TestPrune(t *testing.T) { tests := []struct { - name string - json string - schema *structuralschema.Structural - expected string + name string + json string + dontPruneMetaAtRoot bool + schema *structuralschema.Structural + expected string }{ - {"empty", "null", nil, "null"}, - {"scalar", "4", &structuralschema.Structural{}, "4"}, - {"scalar array", "[1,2]", &structuralschema.Structural{ + {name: "empty", json: "null", expected: "null"}, + {name: "scalar", json: "4", schema: &structuralschema.Structural{}, expected: "4"}, + {name: "scalar array", json: "[1,2]", schema: &structuralschema.Structural{ Items: &structuralschema.Structural{}, - }, "[1,2]"}, - {"object array", `[{"a":1},{"b":1},{"a":1,"b":2,"c":3}]`, &structuralschema.Structural{ + }, expected: "[1,2]"}, + {name: "object array", json: `[{"a":1},{"b":1},{"a":1,"b":2,"c":3}]`, schema: &structuralschema.Structural{ Items: &structuralschema.Structural{ Properties: map[string]structuralschema.Structural{ "a": {}, "c": {}, }, }, - }, `[{"a":1},{},{"a":1,"c":3}]`}, - {"object array with nil schema", `[{"a":1},{"b":1},{"a":1,"b":2,"c":3}]`, nil, `[{},{},{}]`}, - {"object array object", `{"array":[{"a":1},{"b":1},{"a":1,"b":2,"c":3}],"unspecified":{"a":1},"specified":{"a":1,"b":2,"c":3}}`, &structuralschema.Structural{ + }, expected: `[{"a":1},{},{"a":1,"c":3}]`}, + {name: "object array with nil schema", json: `[{"a":1},{"b":1},{"a":1,"b":2,"c":3}]`, expected: `[{},{},{}]`}, + {name: "object array object", json: `{"array":[{"a":1},{"b":1},{"a":1,"b":2,"c":3}],"unspecified":{"a":1},"specified":{"a":1,"b":2,"c":3}}`, schema: &structuralschema.Structural{ Properties: map[string]structuralschema.Structural{ "array": { Items: &structuralschema.Structural{ @@ -64,8 +66,8 @@ func TestPrune(t *testing.T) { }, }, }, - }, `{"array":[{"a":1},{},{"a":1,"c":3}],"specified":{"a":1,"c":3}}`}, - {"nested x-kubernetes-preserve-unknown-fields", ` + }, expected: `{"array":[{"a":1},{},{"a":1,"c":3}],"specified":{"a":1,"c":3}}`}, + {name: "nested x-kubernetes-preserve-unknown-fields", json: ` { "unspecified":"bar", "alpha": "abc", @@ -84,7 +86,7 @@ func TestPrune(t *testing.T) { "preserving": {"unspecified": "bar"} } } -`, &structuralschema.Structural{ +`, schema: &structuralschema.Structural{ Generic: structuralschema.Generic{Type: "object"}, Extensions: structuralschema.Extensions{XPreserveUnknownFields: true}, Properties: map[string]structuralschema.Structural{ @@ -116,7 +118,7 @@ func TestPrune(t *testing.T) { }, }, }, - }, ` + }, expected: ` { "unspecified":"bar", "alpha": "abc", @@ -134,7 +136,7 @@ func TestPrune(t *testing.T) { } } `}, - {"additionalProperties with schema", `{"a":1,"b":1,"c":{"a":1,"b":2,"c":{"a":1}}}`, &structuralschema.Structural{ + {name: "additionalProperties with schema", json: `{"a":1,"b":1,"c":{"a":1,"b":2,"c":{"a":1}}}`, schema: &structuralschema.Structural{ Properties: map[string]structuralschema.Structural{ "a": {}, "c": { @@ -149,8 +151,8 @@ func TestPrune(t *testing.T) { }, }, }, - }, `{"a":1,"c":{"a":1,"b":2,"c":{}}}`}, - {"additionalProperties with bool", `{"a":1,"b":1,"c":{"a":1,"b":2,"c":{"a":1}}}`, &structuralschema.Structural{ + }, expected: `{"a":1,"c":{"a":1,"b":2,"c":{}}}`}, + {name: "additionalProperties with bool", json: `{"a":1,"b":1,"c":{"a":1,"b":2,"c":{"a":1}}}`, schema: &structuralschema.Structural{ Properties: map[string]structuralschema.Structural{ "a": {}, "c": { @@ -161,7 +163,313 @@ func TestPrune(t *testing.T) { }, }, }, - }, `{"a":1,"c":{"a":1,"b":2,"c":{}}}`}, + }, expected: `{"a":1,"c":{"a":1,"b":2,"c":{}}}`}, + {name: "x-kubernetes-embedded-resource", json: ` +{ + "apiVersion": "foo/v1", + "kind": "Foo", + "metadata": { + "name": "instance", + "unspecified": "bar" + }, + "unspecified":"bar", + "pruned": { + "apiVersion": "foo/v1", + "kind": "Foo", + "unspecified": "bar", + "metadata": { + "name": "instance", + "unspecified": "bar" + }, + "spec": { + "unspecified": "bar" + } + }, + "preserving": { + "apiVersion": "foo/v1", + "kind": "Foo", + "unspecified": "bar", + "metadata": { + "name": "instance", + "unspecified": "bar" + }, + "spec": { + "unspecified": "bar" + } + }, + "nested": { + "apiVersion": "foo/v1", + "kind": "Foo", + "unspecified": "bar", + "metadata": { + "name": "instance", + "unspecified": "bar" + }, + "spec": { + "unspecified": "bar", + "embedded": { + "apiVersion": "foo/v1", + "kind": "Foo", + "unspecified": "bar", + "metadata": { + "name": "instance", + "unspecified": "bar" + }, + "spec": { + "unspecified": "bar" + } + } + } + } +} +`, schema: &structuralschema.Structural{ + Generic: structuralschema.Generic{Type: "object"}, + Properties: map[string]structuralschema.Structural{ + "pruned": { + Generic: structuralschema.Generic{Type: "object"}, + Extensions: structuralschema.Extensions{ + XEmbeddedResource: true, + }, + Properties: map[string]structuralschema.Structural{ + "spec": { + Generic: structuralschema.Generic{Type: "object"}, + }, + }, + }, + "preserving": { + Generic: structuralschema.Generic{Type: "object"}, + Extensions: structuralschema.Extensions{ + XEmbeddedResource: true, + XPreserveUnknownFields: true, + }, + }, + "nested": { + Generic: structuralschema.Generic{Type: "object"}, + Extensions: structuralschema.Extensions{ + XEmbeddedResource: true, + }, + Properties: map[string]structuralschema.Structural{ + "spec": { + Generic: structuralschema.Generic{Type: "object"}, + Properties: map[string]structuralschema.Structural{ + "embedded": { + Generic: structuralschema.Generic{Type: "object"}, + Extensions: structuralschema.Extensions{ + XEmbeddedResource: true, + }, + Properties: map[string]structuralschema.Structural{ + "spec": { + Generic: structuralschema.Generic{Type: "object"}, + }, + }, + }, + }, + }, + }, + }, + }, + }, expected: ` +{ + "pruned": { + "apiVersion": "foo/v1", + "kind": "Foo", + "metadata": { + "name": "instance", + "unspecified": "bar" + }, + "spec": { + } + }, + "preserving": { + "apiVersion": "foo/v1", + "kind": "Foo", + "unspecified": "bar", + "metadata": { + "name": "instance", + "unspecified": "bar" + }, + "spec": { + "unspecified": "bar" + } + }, + "nested": { + "apiVersion": "foo/v1", + "kind": "Foo", + "metadata": { + "name": "instance", + "unspecified": "bar" + }, + "spec": { + "embedded": { + "apiVersion": "foo/v1", + "kind": "Foo", + "metadata": { + "name": "instance", + "unspecified": "bar" + }, + "spec": { + } + } + } + } +} +`}, + {name: "x-kubernetes-embedded-resource, with root=true", json: ` +{ + "apiVersion": "foo/v1", + "kind": "Foo", + "metadata": { + "name": "instance", + "unspecified": "bar" + }, + "unspecified":"bar", + "pruned": { + "apiVersion": "foo/v1", + "kind": "Foo", + "unspecified": "bar", + "metadata": { + "name": "instance", + "unspecified": "bar" + }, + "spec": { + "unspecified": "bar" + } + }, + "preserving": { + "apiVersion": "foo/v1", + "kind": "Foo", + "unspecified": "bar", + "metadata": { + "name": "instance", + "unspecified": "bar" + }, + "spec": { + "unspecified": "bar" + } + }, + "nested": { + "apiVersion": "foo/v1", + "kind": "Foo", + "unspecified": "bar", + "metadata": { + "name": "instance", + "unspecified": "bar" + }, + "spec": { + "unspecified": "bar", + "embedded": { + "apiVersion": "foo/v1", + "kind": "Foo", + "unspecified": "bar", + "metadata": { + "name": "instance", + "unspecified": "bar" + }, + "spec": { + "unspecified": "bar" + } + } + } + } +} +`, dontPruneMetaAtRoot: true, schema: &structuralschema.Structural{ + Generic: structuralschema.Generic{Type: "object"}, + Properties: map[string]structuralschema.Structural{ + "pruned": { + Generic: structuralschema.Generic{Type: "object"}, + Extensions: structuralschema.Extensions{ + XEmbeddedResource: true, + }, + Properties: map[string]structuralschema.Structural{ + "spec": { + Generic: structuralschema.Generic{Type: "object"}, + }, + }, + }, + "preserving": { + Generic: structuralschema.Generic{Type: "object"}, + Extensions: structuralschema.Extensions{ + XEmbeddedResource: true, + XPreserveUnknownFields: true, + }, + }, + "nested": { + Generic: structuralschema.Generic{Type: "object"}, + Extensions: structuralschema.Extensions{ + XEmbeddedResource: true, + }, + Properties: map[string]structuralschema.Structural{ + "spec": { + Generic: structuralschema.Generic{Type: "object"}, + Properties: map[string]structuralschema.Structural{ + "embedded": { + Generic: structuralschema.Generic{Type: "object"}, + Extensions: structuralschema.Extensions{ + XEmbeddedResource: true, + }, + Properties: map[string]structuralschema.Structural{ + "spec": { + Generic: structuralschema.Generic{Type: "object"}, + }, + }, + }, + }, + }, + }, + }, + }, + }, expected: ` +{ + "apiVersion": "foo/v1", + "kind": "Foo", + "metadata": { + "name": "instance", + "unspecified": "bar" + }, + "pruned": { + "apiVersion": "foo/v1", + "kind": "Foo", + "metadata": { + "name": "instance", + "unspecified": "bar" + }, + "spec": { + } + }, + "preserving": { + "apiVersion": "foo/v1", + "kind": "Foo", + "unspecified": "bar", + "metadata": { + "name": "instance", + "unspecified": "bar" + }, + "spec": { + "unspecified": "bar" + } + }, + "nested": { + "apiVersion": "foo/v1", + "kind": "Foo", + "metadata": { + "name": "instance", + "unspecified": "bar" + }, + "spec": { + "embedded": { + "apiVersion": "foo/v1", + "kind": "Foo", + "metadata": { + "name": "instance", + "unspecified": "bar" + }, + "spec": { + } + } + } + } +} +`}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -175,7 +483,7 @@ func TestPrune(t *testing.T) { t.Fatal(err) } - prune(in, tt.schema) + Prune(in, tt.schema, tt.dontPruneMetaAtRoot) if !reflect.DeepEqual(in, expected) { var buf bytes.Buffer enc := json.NewEncoder(&buf) @@ -184,7 +492,7 @@ func TestPrune(t *testing.T) { if err != nil { t.Fatalf("unexpected result mashalling error: %v", err) } - t.Errorf("expected: %s\ngot: %s", tt.expected, buf.String()) + t.Errorf("expected: %s\ngot: %s\ndiff: %s", tt.expected, buf.String(), diff.ObjectDiff(expected, in)) } }) } @@ -260,7 +568,7 @@ func BenchmarkPrune(b *testing.B) { b.StartTimer() for i := 0; i < b.N; i++ { - Prune(instances[i], schema) + Prune(instances[i], schema, true) } }