Exclude keys containing empty patches in the final patch

This commit is contained in:
David Hao 2018-01-24 23:29:47 -05:00
parent 69602b4f58
commit 48ef7645df
3 changed files with 66 additions and 48 deletions

View File

@ -977,8 +977,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()
@ -995,41 +993,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
@ -1053,9 +1025,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: []