kubernetes/apimachinery

[Question] why does it not preserve changes when invoke `CreateThreeWayMergePatch` function with nil annotations

chengjingtao opened this issue · 4 comments

When I using CreateThreeWayMergePatch to genernate patch, I found a strange behaviors

// original

{
  "kind": "Deployment",
  "apiVersion": "apps/v1",
  "metadata": {
    "name": "test",
    "namespace": "ns",
    "annotations": null
  }
}

// modified // same as original
{
  "kind": "Deployment",
  "apiVersion": "apps/v1",
  "metadata": {
    "name": "test",
    "namespace": "ns",
    "annotations": null
  }
}

// current  // do some change based on original
{
  "kind": "Deployment",
  "apiVersion": "apps/v1",
  "metadata": {
    "name": "test",
    "namespace": "ns",
    "annotations": {
         "a": "a1"
     }
  }
}

after invoke CreateThreeWayMergePatch, it will generate a patch like that {"metadata":{"annotations":null}}, just not preserve the change on annotations.

My Question is: "why does it not preserve annotations changes? is this expected?"
This seems to easily lead to the following conflicts

  • controller 1: reconcile resources as expected (same as original)
  • controller 2: add some annotations
  • controller 1: reconcile resources as expected (same as original) : clean all annotations.

the code is here

package main

import (
	"encoding/json"
	"k8s.io/api/apps/v1"
	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
	"k8s.io/apimachinery/pkg/util/strategicpatch"
	"log"
)

func main() {
	patch := createPatch(nil)
	log.Printf("when expected annotations is nil:   patch: %s \n", string(patch))

	patch = createPatch(map[string]string{})
	log.Printf("when expected annotations is empty: patch: %s \n", string(patch))
}

func createPatch(annotations map[string]string) []byte {
	expected := &unstructured.Unstructured{
		Object: map[string]interface{}{
			"kind":       "Deployment",
			"apiVersion": "apps/v1",
			"metadata": map[string]interface{}{
				"name":        "test",
				"namespace":   "ns",
				"annotations": annotations,
			},
		},
	}

	versionedObject := &v1.Deployment{}
	bts, _ := json.Marshal(expected)
	_ = json.Unmarshal(bts, versionedObject)

	existing := &unstructured.Unstructured{
		Object: map[string]interface{}{
			"kind":       "Deployment",
			"apiVersion": "apps/v1",
			"metadata": map[string]interface{}{
				"name":      "test",
				"namespace": "ns",
				"annotations": map[string]string{
					"a": "a1",
					"b": "b1",
				},
			},
		},
	}

	patchMeta, err := strategicpatch.NewPatchMetaFromStruct(versionedObject)
	if err != nil {
		log.Fatal("new patch meta:" + err.Error())
	}

	expectedJSON, _ := json.Marshal(expected)
	existingJSON, _ := json.Marshal(existing)
	patch, err := strategicpatch.CreateThreeWayMergePatch(expectedJSON, expectedJSON, existingJSON, patchMeta, true)

	if err != nil {
		log.Fatal(err)
	}

	return patch
}

result is :

when expected annotations is nil:      patch: {"metadata":{"annotations":null}} 
when expected annotations is empty:    patch: {}