Patching kubernetes resources in Go

2020-05-28

Recently I was playing around with kubernetes go client when I came across patch method which got me stunned for a while. It’s a little bit different approach than I am used to so I decided to write a few words about it.

The part that confused me is the patch interface which for the most resources looks like this:

Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts metav1.PatchOptions, subresources ...string) (result *v1.Pod, err error)

I had no idea how to use it at first sight so let’s take a closer look.

After a couple of minutes of digging in docs, I have discovered that kubernetes API supports 3 different patch operations which kind of explains the interface.

For the record, a patch type value is sent as Content-Type to kubernetes api server.

PATCH /api/v1/namespaces/default/pods/my-pod HTTP/1.1
Content-Type: application/json-patch+json

All patch types are provided as constants in k8s.io/apimachinery/pkg/types package.

const (
	JSONPatchType           PatchType = "application/json-patch+json"
	MergePatchType          PatchType = "application/merge-patch+json"
	StrategicMergePatchType PatchType = "application/strategic-merge-patch+json"
	ApplyPatchType          PatchType = "application/apply-patch+yaml"
)

JSON Patch

As defined in RFC 6920, JSON patch is a sequence of operations executed on the resource. Yes, data []byte are operations.

Each item in operations list has three attributes: op, path and value. Op represents a type of the operation and it’s value can be remove, replace, etc… (check out the RFC for more information). For example let’s take this operation which would replace a container image to kirecek/hello-world:

[{
    "op": "replace",
      "path": "/spec/containers/0/image",
      "value": "kirecek/hello-world"
}]

Example in Go:

type PatchOperation struct {
    Path  string      `json:"path"`
    Value interface{} `json:"value"`
    Op    string      `json:"op"`
}
ops := make([]*PatchOperation, 0)

// define operation which replaces image of a pod
ops = append(ops, &PatchOperation{
    Op: "replace",:
    Path: "/spec/containers/0/image",
    Value: "kirecek/helllo-world",
})

// serialize defined operations
data, err := json.Marshall(ops)
if err != nil {
    panic(err.Error())
}

// send the payload to kubernetes api
out, err := clientset.CoreV1().Pods("default").Patch("my-pod", types.JSONPatchType, data)
if err != nil {
    panic(err.Error())
}

fmt.Println(out)

Merge Patch

As defined in RFC7386, a Merge Patch is essentially a partial representation of the resource. The submitted JSON is “merged” with the current resource to create a new one. I think this is little bit harder for programmatic usege compared to JSONPatch.

// define partial reprezentation of an object that we want to change
podPatch := corev1.PodSpec{
    Containers: []v1.Container{
        v1.Container{
            Name: "my-pod",
            Image: "kirecek/hello-world:latest",
        },
    },
}

// serialize the object
data, err := json.Marshal(podPatch)
if err != nil {
    panic(err.Error())
}

// send the payload
out, err := clientset.CoreV1().Pods("default").Patch("my-pod", types.MergePatchType, data)
if err != nil {
    panic(err.Error())
}

fmt.Println(out)

Strategic Merge Patch

Strategic Merge Patch is a custom implementation of Merge Patch so AFAIK it does not contain any RFC. From usage point of view it’s same the Merge Patch only with different pt value.

out, err := clientset.CoreV1().Pods("default").Patch("my-pod", types.StrategicMergePatchType, data)

So what is the difference between merge and strategic merge? The most common use-case are list. List are always replaced in Merge Patch whereas they can be merged or replaced in Strategic Merge.

For example we start with the following pod

...
    "spec": {
        "containers": [
            {
                "name": "app",
                "image": "kirecek/hello-world:latest"
            }
        ]
    }
...

…and post this PATCH payload:

PATCH /api/v1/namespaces/default/pods/my-pod
{
    "spec": {
        "containers": [
            {
                "name": "new-app",
                "image": "nginx"
            }
        ]
    }
}

In classic Merge Patch the request would replace entire container list with the single nginx container we put into the playload. On the other hand, Strategic Merge is a little bit smarter and it uses metadata API to determine whether list should be merged or replaced. Our patch would result into pod with 2 containers (“app” and “new-app”).

Strategic merge also supports special operations like replace or delete which can be applied both on maps and lists. These operations tells strictly how objects should be handled. Operation must be provided as $patch field. For example:

{
    "containers": [
        {
            "name": "app",
            "image": "kirecek/hello-world:latest"
        },
        {
            "$patch": "replace"
        }
    ]
}

However, this is even harder to use in your code. These merges are mostly used as static payloads in third party libraries. For your integrations, I recommend to use JSON Patch which is the easiest to generate and the most readable method.

Well, this is all I have. Now go patch your kube!