Change:
Every time I server-side apply a with , the updates. Nothing about my manifest changes between applies; if I map to the empty map, the same manifest updates the . With client-side apply, the stays the same as long as my manifest remains constant, even if . I read in that Kubernetes should treat absent keys, empty values (in this case, an empty map), and values the same. I observe that leaving the key out and behave as expected, not modifying the and leaving the same, but empty map behaves as described above. If we cannot change this behavior, I'd at least like to understand the rationale behind it. I expected my object to stay the same between identical server-side applies, where the manifest file content was exactly the same, regardless of the value of . A different every apply indicates that the object was event type . I expect no events to fire on applying the same manifest. contains In actuality, any is fine. . Every successive repeat of steps 3-4 should show different values. Example of with : This is especially a problem when I server-side apply with code in my operator. My operator is built on and watches changes to the child objects it deploys. If I create a with , the operator constantly reconciles because every server-side apply of the manifest registers as a type event on the object, which triggers the watching operator to reconcile. N/A <details</details<details</details<details</details<details</details
/sig api-machinery
/cc /triage accepted
Potentially similar case as

Discussion:
"k8s.io/apimachinery/pkg/api/equality" "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" <ins> "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" </ins> "k8s.io/apimachinery/pkg/conversion" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apiserver/pkg/endpoints/metrics"

return newObj, nil } <ins> eqFn := equalities.DeepEqual if _, ok := newObj.(*unstructured.Unstructured); ok { // Use strict equality with unstructured eqFn = equalities.DeepEqualWithNilDifferentFromEmpty } </ins> // This condition ensures the managed fields are always compared first. If //	this check fails, the if statement will short circuit. If the check // 	succeeds the slow path is taken which compares entire objects. <del> if !equalities.DeepEqualWithNilDifferentFromEmpty(oldManagedFields, newManagedFields) { </del> <ins> if !eqFn(oldManagedFields, newManagedFields) { </ins> return newObj, nil } <del> if equalities.DeepEqualWithNilDifferentFromEmpty(newObj, oldObj) { </del> <ins> if eqFn(newObj, oldObj) { </ins> // Remove any changed timestamps, so that timestamp is not the only // change seen by etcd. //

} } <ins> // TestNoOpApplyWithEmptyMap func TestNoOpApplyWithEmptyMap(t *testing.T) { client, closeFn := setup(t) defer closeFn() deploymentName := "no-op" deploymentsResource := "deployments" deploymentBytes := []byte(`{ "apiVersion": "apps/v1", "kind": "Deployment", "metadata": { "name": "` + deploymentName + `", "labels": { "app": "nginx" } }, "spec": { "replicas": 1, "selector": { "matchLabels": { "app": "nginx" } }, "template": { "metadata": { "annotations": {}, "labels": { "app": "nginx" } }, "spec": { "containers": [{ "name": "nginx", "image": "nginx:1.14.2", "ports": [{ "containerPort": 80 }] }] } } } }`) _, err := client.AppsV1().RESTClient().Patch(types.ApplyPatchType). Namespace("default"). Param("fieldManager", "apply_test"). Resource(deploymentsResource). Name(deploymentName). Body(deploymentBytes). Do(context.TODO()). Get() if err != nil { t.Fatalf("Failed to create object: %v", err) } // This sleep is necessary to consistently produce different timestamps because the time field in managedFields has // 1 second granularity and if both apply requests happen during the same second, this test would flake. time.Sleep(1 * time.Second) createdObject, err := client.AppsV1().RESTClient().Get().Namespace("default").Resource(deploymentsResource).Name(deploymentName).Do(context.TODO()).Get() if err != nil { t.Fatalf("Failed to retrieve created object: %v", err) } createdAccessor, err := meta.Accessor(createdObject) if err != nil { t.Fatalf("Failed to get meta accessor for created object: %v", err) } createdBytes, err := json.MarshalIndent(createdObject, "t", "t") if err != nil { t.Fatalf("Failed to marshal created object: %v", err) } // Test that we can apply the same object and don't change the RV _, err = client.AppsV1().RESTClient().Patch(types.ApplyPatchType). Namespace("default"). Param("fieldManager", "apply_test"). Resource(deploymentsResource). Name(deploymentName). Body(deploymentBytes). Do(context.TODO()). Get() if err != nil { t.Fatalf("Failed to create object: %v", err) } updatedObject, err := client.AppsV1().RESTClient().Get().Namespace("default").Resource(deploymentsResource).Name(deploymentName).Do(context.TODO()).Get() if err != nil { t.Fatalf("Failed to retrieve updated object: %v", err) } updatedAccessor, err := meta.Accessor(updatedObject) if err != nil { t.Fatalf("Failed to get meta accessor for updated object: %v", err) } updatedBytes, err := json.MarshalIndent(updatedObject, "t", "t") if err != nil { t.Fatalf("Failed to marshal updated object: %v", err) } if createdAccessor.GetResourceVersion() != updatedAccessor.GetResourceVersion() { t.Fatalf("Expected same resource version to be %v but got: %vnold object:n%vnnew object:n%v", createdAccessor.GetResourceVersion(), updatedAccessor.GetResourceVersion(), string(createdBytes), string(updatedBytes), ) } } // TestApplyEmptyMarkerStructDifferentFromNil func TestApplyEmptyMarkerStructDifferentFromNil(t *testing.T) { client, closeFn := setup(t) defer closeFn() podName := "pod-with-empty-dir" podsResource := "pods" podBytesWithEmptyDir := []byte(`{ "apiVersion": "v1", "kind": "Pod", "metadata": { "name": "` + podName + `" }, "spec": { "containers": [{ "name":  "test-container-a", "image": "test-image-one", "volumeMounts": [{ "mountPath": "/cache", "name": "cache-volume" }], }], "volumes": [{ "name": "cache-volume", "emptyDir": {} }] } }`) _, err := client.CoreV1().RESTClient().Patch(types.ApplyPatchType). Namespace("default"). Param("fieldManager", "apply_test"). Resource(podsResource). Name(podName). Body(podBytesWithEmptyDir). Do(context.TODO()). Get() if err != nil { t.Fatalf("Failed to create object: %v", err) } // This sleep is necessary to consistently produce different timestamps because the time field in managedFields has // 1 second granularity and if both apply requests happen during the same second, this test would flake. time.Sleep(1 * time.Second) createdObject, err := client.CoreV1().RESTClient().Get().Namespace("default").Resource(podsResource).Name(podName).Do(context.TODO()).Get() if err != nil { t.Fatalf("Failed to retrieve created object: %v", err) } createdAccessor, err := meta.Accessor(createdObject) if err != nil { t.Fatalf("Failed to get meta accessor for created object: %v", err) } createdBytes, err := json.MarshalIndent(createdObject, "t", "t") if err != nil { t.Fatalf("Failed to marshal created object: %v", err) } podBytesNoEmptyDir := []byte(`{ "apiVersion": "v1", "kind": "Pod", "metadata": { "name": "` + podName + `" }, "spec": { "containers": [{ "name":  "test-container-a", "image": "test-image-one", "volumeMounts": [{ "mountPath": "/cache", "name": "cache-volume" }], }], "volumes": [{ "name": "cache-volume" }] } }`) // Test that an apply with no emptyDir is recognized as distinct from an empty marker struct emptyDir. _, err = client.CoreV1().RESTClient().Patch(types.ApplyPatchType). Namespace("default"). Param("fieldManager", "apply_test"). Resource(podsResource). Name(podName). Body(podBytesNoEmptyDir). Do(context.TODO()). Get() if err != nil { t.Fatalf("Failed to create object: %v", err) } updatedObject, err := client.CoreV1().RESTClient().Get().Namespace("default").Resource(podsResource).Name(podName).Do(context.TODO()).Get() if err != nil { t.Fatalf("Failed to retrieve updated object: %v", err) } updatedAccessor, err := meta.Accessor(updatedObject) if err != nil { t.Fatalf("Failed to get meta accessor for updated object: %v", err) } updatedBytes, err := json.MarshalIndent(updatedObject, "t", "t") if err != nil { t.Fatalf("Failed to marshal updated object: %v", err) } if createdAccessor.GetResourceVersion() == updatedAccessor.GetResourceVersion() { t.Fatalf("Expected different resource version to be %v but got: %vnold object:n%vnnew object:n%v", createdAccessor.GetResourceVersion(), updatedAccessor.GetResourceVersion(), string(createdBytes), string(updatedBytes), ) } } </ins> func getRV(obj runtime.Object) (string, error) { acc, err := meta.Accessor(obj) if err != nil {