Change:
Note: I'm using OpenShift. s/oc/kubectl/ below if that's how you roll. I'm using a command similar to the following to idempotently ensure a label is absent on a Namespace object: If the object's contains anything other than , this works fine: the resulting object has no key. However, if is initially absent, this command adds it with an entry with the empty string as a value: Running the command again removes the entry and the . Using this logic in a controller causes a hot loop as we thrash back and forth between the above and no labels. Using a value in a dict in a StrategicMerge patch should idempotently ensure the key is absent, even if the dict is initially absent/empty. I didn't try this on anything other than a Namespace; and I didn't try it on any dict other than . Workaround: Add any other label, as seen above. <details</details<detailsAWS </details<details</details<details</detailsI believe the issue is on the server, but... v0.9.3-0.- </details<details</details
/sig api-machinery
/assign /cc /triage accepted
can you look into it?
It looks that this idempotency problem is fixed in latest releases(after being label as you mentioned). All responses are legit; Kubernetes Version
The bug happens when there are no other labels. Are you saying the "fix" was for kubernetes to always add a label to the object?
That was an incidental change specific to namespaces. I'd expect the issue still exists for other types
you are both right. This is incidental "fix" for namespaces. I'll look at others to find the actual problem.

Discussion:
if !ok { if !isDeleteList { // If it's not in the original document, just take the patch value. <ins> if mergeOptions.IgnoreUnmatchedNulls { discardNullValuesFromPatch(patchV) } </ins> original[k] = patchV } continue

patchType := reflect.TypeOf(patchV) if originalType != patchType { if !isDeleteList { <ins> if mergeOptions.IgnoreUnmatchedNulls { discardNullValuesFromPatch(patchV) } </ins> original[k] = patchV } continue

return original, nil } <ins> // discardNullValuesFromPatch discards all null property values from patch. // It traverses all slices and map types. func discardNullValuesFromPatch(patchV interface{}) { switch patchV := patchV.(type) { case map[string]interface{}: for k, v := range patchV { if v == nil { delete(patchV, k) } else { discardNullValuesFromPatch(v) } } case []interface{}: for _, v := range patchV { discardNullValuesFromPatch(v) } } } </ins> // mergeMapHandler handles how to merge `patchV` whose key is `key` with `original` respecting // fieldPatchStrategy and mergeOptions. func mergeMapHandler(original, patch interface{}, schema LookupPatchMeta,

ExpectedThreeWay:       `{}`, ExpectedThreeWayResult: `{"array":[1,2,3],"complex":{"nested":true},"name":"foo","scalar":true}`, }, <ins> "no diff even if modified null": { Original: `{"array":[1,2,3],"complex":{"nested":true},"name":"foo","scalar":true}`, Current:  `{"array":[1,2,3],"complex":{"nested":true},"name":"foo","scalar":true}`, Modified: `{"array":[1,2,3],"complex":{"nested":true},"complex_nullable":{"key":null},"name":"foo","scalar":true}`, ExpectedTwoWay:         `{"complex_nullable":{"key":null}}`, ExpectedTwoWayResult:   `{"array":[1,2,3],"complex":{"nested":true},"complex_nullable":{},"name":"foo","scalar":true}`, ExpectedThreeWay:       `{"complex_nullable":{"key":null}}`, ExpectedThreeWayResult: `{"array":[1,2,3],"complex":{"nested":true},"complex_nullable":{},"name":"foo","scalar":true}`, }, "discard nulls in nested and adds not nulls": { Original: `{"array":[1,2,3],"complex":{"nested":true},"name":"foo","scalar":true}`, Current:  `{"array":[1,2,3],"complex":{"nested":true},"name":"foo","scalar":true}`, Modified: `{"array":[1,2,3],"complex":{"nested":true},"complex_nullable":{"key":{"keynotnull":"value","keynull":null}},"name":"foo","scalar":true}`, ExpectedTwoWay:         `{"complex_nullable":{"key":{"keynotnull":"value","keynull":null}}}`, ExpectedTwoWayResult:   `{"array":[1,2,3],"complex":{"nested":true},"complex_nullable":{"key":{"keynotnull":"value"}},"name":"foo","scalar":true}`, ExpectedThreeWay:       `{"complex_nullable":{"key":{"keynotnull":"value","keynull":null}}}`, ExpectedThreeWayResult: `{"array":[1,2,3],"complex":{"nested":true},"complex_nullable":{"key":{"keynotnull":"value"}},"name":"foo","scalar":true}`, }, "discard if modified all nulls": { Original: `{}`, Current:  `{}`, Modified: `{"complex":{"nested":null}}`, ExpectedTwoWay:         `{"complex":{"nested":null}}`, ExpectedTwoWayResult:   `{"complex":{}}`, ExpectedThreeWay:       `{"complex":{"nested":null}}`, ExpectedThreeWayResult: `{"complex":{}}`, }, "add only not nulls": { Original: `{}`, Current:  `{}`, Modified: `{"complex":{"nested":null,"nested2":"foo"}}`, ExpectedTwoWay:         `{"complex":{"nested":null,"nested2":"foo"}}`, ExpectedTwoWayResult:   `{"complex":{"nested2":"foo"}}`, ExpectedThreeWay:       `{"complex":{"nested":null,"nested2":"foo"}}`, ExpectedThreeWayResult: `{"complex":{"nested2":"foo"}}`, }, "null values in original are preserved": { Original: `{"thing":null}`, Current:  `{"thing":null}`, Modified: `{"nested":{"value":5},"thing":null}`, ExpectedTwoWay:         `{"nested":{"value":5}}`, ExpectedTwoWayResult:   `{"nested":{"value":5},"thing":null}`, ExpectedThreeWay:       `{"nested":{"value":5}}`, ExpectedThreeWayResult: `{"nested":{"value":5},"thing":null}`, }, "nested null values in original are preserved": { Original: `{"complex":{"key":null},"thing":null}`, Current:  `{"complex":{"key":null},"thing":null}`, Modified: `{"complex":{"key":null},"nested":{"value":5},"thing":null}`, ExpectedTwoWay:         `{"nested":{"value":5}}`, ExpectedTwoWayResult:   `{"complex":{"key":null},"nested":{"value":5},"thing":null}`, ExpectedThreeWay:       `{"nested":{"value":5}}`, ExpectedThreeWayResult: `{"complex":{"key":null},"nested":{"value":5},"thing":null}`, }, "add empty slices": { Original: `{"array":[1,2,3],"complex":{"nested":true},"name":"foo","scalar":true}`, Current:  `{"array":[1,2,3],"complex":{"nested":true},"name":"foo","scalar":true}`, Modified: `{"array":[1,2,3],"complex":{"nested":true},"complex_nullable":[],"name":"foo","scalar":true}`, ExpectedTwoWay:         `{"complex_nullable":[]}`, ExpectedTwoWayResult:   `{"array":[1,2,3],"complex":{"nested":true},"complex_nullable":[],"name":"foo","scalar":true}`, ExpectedThreeWay:       `{"complex_nullable":[]}`, ExpectedThreeWayResult: `{"array":[1,2,3],"complex":{"nested":true},"complex_nullable":[],"name":"foo","scalar":true}`, }, "filter nulls from nested slices": { Original: `{}`, Current:  `{}`, Modified: `{"complex_nullable":[{"inner_one":{"key_one":"foo","key_two":null}}]}`, ExpectedTwoWay:         `{"complex_nullable":[{"inner_one":{"key_one":"foo","key_two":null}}]}`, ExpectedTwoWayResult:   `{"complex_nullable":[{"inner_one":{"key_one":"foo"}}]}`, ExpectedThreeWay:       `{"complex_nullable":[{"inner_one":{"key_one":"foo","key_two":null}}]}`, ExpectedThreeWayResult: `{"complex_nullable":[{"inner_one":{"key_one":"foo"}}]}`, }, "filter if slice is all empty": { Original: `{}`, Current:  `{}`, Modified: `{"complex_nullable":[{"inner_one":{"key_one":null,"key_two":null}}]}`, ExpectedTwoWay:         `{"complex_nullable":[{"inner_one":{"key_one":null,"key_two":null}}]}`, ExpectedTwoWayResult:   `{"complex_nullable":[{"inner_one":{}}]}`, ExpectedThreeWay:       `{"complex_nullable":[{"inner_one":{"key_one":null,"key_two":null}}]}`, ExpectedThreeWayResult: `{"complex_nullable":[{"inner_one":{}}]}`, }, "not filter nulls from non-associative slice": { Original: `{}`, Current:  `{}`, Modified: `{"complex_nullable":["key1",null,"key2"]}`, ExpectedTwoWay:         `{"complex_nullable":["key1",null,"key2"]}`, ExpectedTwoWayResult:   `{"complex_nullable":["key1",null,"key2"]}`, ExpectedThreeWay:       `{"complex_nullable":["key1",null,"key2"]}`, ExpectedThreeWayResult: `{"complex_nullable":["key1",null,"key2"]}`, }, </ins> "added only": { Original: `{"name":"foo"}`, Current:  `{"name":"foo"}`,