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
```
pull/8/head
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"
verifiedPatch := false
for _, fn := range testingOpenAPISchemaFns {
t.Run("test repeated apply operations on an unstructured object", func(t *testing.T) {
tf := cmdtesting.NewTestFactory()
@ -1039,41 +1037,15 @@ func TestUnstructuredIdempotentApply(t *testing.T) {
Header: defaultHeader(),
Body: body}, nil
case p == path && m == "PATCH":
// In idempotent updates, kubectl sends a logically empty
// request body with the PATCH request.
// Should look like this:
// Request Body: {"metadata":{"annotations":{}}}
// In idempotent updates, kubectl will resolve to an empty patch and not send anything to the server
// Thus, if we reach this branch, kubectl is unnecessarily sending a patch.
patch, err := ioutil.ReadAll(req.Body)
if err != nil {
t.Fatal(err)
}
contentType := req.Header.Get("Content-Type")
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
t.Fatalf("Unexpected Patch: %s", patch)
return nil, nil
default:
t.Fatalf("unexpected request: %#v\n%#v", req.URL, req)
return nil, nil
@ -1096,9 +1068,6 @@ func TestUnstructuredIdempotentApply(t *testing.T) {
if 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:
switch typedVal := val.(type) {
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 {
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:
// Lists are always replaced in Json, no need to check each entry in the list.
if !keepNull {

View File

@ -62,12 +62,12 @@ testCases:
expectedWithoutNull: {}
- description: simple map with all non-nil values
originalObj:
nonNilKey: foo
nonNilKey: bar
nonNilKey1: foo
nonNilKey2: bar
expectedWithNull: {}
expectedWithoutNull:
nonNilKey: foo
nonNilKey: bar
nonNilKey1: foo
nonNilKey2: bar
- description: nested map
originalObj:
mapKey:
@ -88,19 +88,52 @@ testCases:
mapKey:
nilKey1: null
nilKey2: null
expectedWithoutNull:
mapKey: {}
expectedWithoutNull: {}
- description: nested map that all subkeys are non-nil
originalObj:
mapKey:
nonNilKey: foo
nonNilKey: bar
expectedWithNull:
mapKey: {}
nonNilKey1: foo
nonNilKey2: bar
expectedWithNull: {}
expectedWithoutNull:
mapKey:
nonNilKey: foo
nonNilKey: bar
nonNilKey1: foo
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
originalObj:
listKey: []