Merge pull request #59284 from Addepar/fix-empty-null-patch

Automatic merge from submit-queue (batch tested with PRs 59284, 63602). If you want to cherry-pick this change to another branch, please follow the instructions <a href="https://github.com/kubernetes/community/blob/master/contributors/devel/cherry-picks.md">here</a>.

Exclude keys containing empty patches in the final patch

**What this PR does / why we need it**: 
This minimizes the 3-way JSON merge patch generated when calculating the patch necessary to send to the server. It does this by removing empty maps created from deleting keys in the keepOrDeleteNullInObj method.

This is not only a slight performance improvement (less PATCH requests) but also necessary when working with custom resources that have RBAC restrictions.

**Which issue(s) this PR fixes**: N/A

**Special notes for your reviewer**: N/A

**Release note**:

```release-note
NONE
```
This commit is contained in:
Kubernetes Submit Queue 2018-05-09 13:51:09 -07:00 committed by GitHub
commit d89471c4b5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 66 additions and 48 deletions

View File

@ -1021,8 +1021,6 @@ func TestUnstructuredIdempotentApply(t *testing.T) {
} }
path := "/namespaces/test/widgets/widget" path := "/namespaces/test/widgets/widget"
verifiedPatch := false
for _, fn := range testingOpenAPISchemaFns { for _, fn := range testingOpenAPISchemaFns {
t.Run("test repeated apply operations on an unstructured object", func(t *testing.T) { t.Run("test repeated apply operations on an unstructured object", func(t *testing.T) {
tf := cmdtesting.NewTestFactory() tf := cmdtesting.NewTestFactory()
@ -1039,41 +1037,15 @@ func TestUnstructuredIdempotentApply(t *testing.T) {
Header: defaultHeader(), Header: defaultHeader(),
Body: body}, nil Body: body}, nil
case p == path && m == "PATCH": case p == path && m == "PATCH":
// In idempotent updates, kubectl sends a logically empty // In idempotent updates, kubectl will resolve to an empty patch and not send anything to the server
// request body with the PATCH request. // Thus, if we reach this branch, kubectl is unnecessarily sending a patch.
// Should look like this:
// Request Body: {"metadata":{"annotations":{}}}
patch, err := ioutil.ReadAll(req.Body) patch, err := ioutil.ReadAll(req.Body)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
t.Fatalf("Unexpected Patch: %s", patch)
contentType := req.Header.Get("Content-Type") return nil, nil
if contentType != "application/merge-patch+json" {
t.Fatalf("Unexpected Content-Type: %s", contentType)
}
patchMap := map[string]interface{}{}
if err := json.Unmarshal(patch, &patchMap); err != nil {
t.Fatal(err)
}
if len(patchMap) != 1 {
t.Fatalf("Unexpected Patch. Has more than 1 entry. path: %s", patch)
}
annotationsMap := walkMapPath(t, patchMap, []string{"metadata", "annotations"})
if len(annotationsMap) != 0 {
t.Fatalf("Unexpected Patch. Found unexpected annotation: %s", patch)
}
verifiedPatch = true
body := ioutil.NopCloser(bytes.NewReader(serversideData))
return &http.Response{
StatusCode: 200,
Header: defaultHeader(),
Body: body}, nil
default: default:
t.Fatalf("unexpected request: %#v\n%#v", req.URL, req) t.Fatalf("unexpected request: %#v\n%#v", req.URL, req)
return nil, nil return nil, nil
@ -1096,9 +1068,6 @@ func TestUnstructuredIdempotentApply(t *testing.T) {
if errBuf.String() != "" { if errBuf.String() != "" {
t.Fatalf("unexpected error output: %s", errBuf.String()) t.Fatalf("unexpected error output: %s", errBuf.String())
} }
if !verifiedPatch {
t.Fatal("No server-side patch call detected")
}
}) })
} }
} }

View File

@ -116,10 +116,26 @@ func keepOrDeleteNullInObj(m map[string]interface{}, keepNull bool) (map[string]
case val != nil: case val != nil:
switch typedVal := val.(type) { switch typedVal := val.(type) {
case map[string]interface{}: case map[string]interface{}:
filteredMap[key], err = keepOrDeleteNullInObj(typedVal, keepNull) // Explicitly-set empty maps are treated as values instead of empty patches
if len(typedVal) == 0 {
if !keepNull {
filteredMap[key] = typedVal
}
continue
}
var filteredSubMap map[string]interface{}
filteredSubMap, err = keepOrDeleteNullInObj(typedVal, keepNull)
if err != nil { if err != nil {
return nil, err return nil, err
} }
// If the returned filtered submap was empty, this is an empty patch for the entire subdict, so the key
// should not be set
if len(filteredSubMap) != 0 {
filteredMap[key] = filteredSubMap
}
case []interface{}, string, float64, bool, int, int64, nil: case []interface{}, string, float64, bool, int, int64, nil:
// Lists are always replaced in Json, no need to check each entry in the list. // Lists are always replaced in Json, no need to check each entry in the list.
if !keepNull { if !keepNull {

View File

@ -62,12 +62,12 @@ testCases:
expectedWithoutNull: {} expectedWithoutNull: {}
- description: simple map with all non-nil values - description: simple map with all non-nil values
originalObj: originalObj:
nonNilKey: foo nonNilKey1: foo
nonNilKey: bar nonNilKey2: bar
expectedWithNull: {} expectedWithNull: {}
expectedWithoutNull: expectedWithoutNull:
nonNilKey: foo nonNilKey1: foo
nonNilKey: bar nonNilKey2: bar
- description: nested map - description: nested map
originalObj: originalObj:
mapKey: mapKey:
@ -88,19 +88,52 @@ testCases:
mapKey: mapKey:
nilKey1: null nilKey1: null
nilKey2: null nilKey2: null
expectedWithoutNull: expectedWithoutNull: {}
mapKey: {}
- description: nested map that all subkeys are non-nil - description: nested map that all subkeys are non-nil
originalObj: originalObj:
mapKey: mapKey:
nonNilKey: foo nonNilKey1: foo
nonNilKey: bar nonNilKey2: bar
expectedWithNull: expectedWithNull: {}
mapKey: {}
expectedWithoutNull: expectedWithoutNull:
mapKey: mapKey:
nonNilKey: foo nonNilKey1: foo
nonNilKey: bar nonNilKey2: bar
- description: explicitly empty map as value
originalObj:
mapKey: {}
expectedWithNull: {}
expectedWithoutNull:
mapKey: {}
- description: explicitly empty nested map
originalObj:
mapKey:
nonNilKey: {}
expectedWithNull: {}
expectedWithoutNull:
mapKey:
nonNilKey: {}
- description: multiple expliclty empty nested maps
originalObj:
mapKey:
nonNilKey1: {}
nonNilKey2: {}
expectedWithNull: {}
expectedWithoutNull:
mapKey:
nonNilKey1: {}
nonNilKey2: {}
- description: nested map with non-null value as empty map
originalObj:
mapKey:
nonNilKey: {}
nilKey: null
expectedWithNull:
mapKey:
nilKey: null
expectedWithoutNull:
mapKey:
nonNilKey: {}
- description: empty list - description: empty list
originalObj: originalObj:
listKey: [] listKey: []