Patching kubernetes resources in Go
Recently I was playing around with the Kubernetes Go client when I came across the 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 the docs, I discovered that the 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 the 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 the operations list has three attributes: op
, path
, and value
. op
represents the type of the operation and its 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 with 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/hello-world",
})
// serialize defined operations
data, err := json.Marshal(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 a little bit harder for programmatic usage compared to JSON Patch.
// define partial representation 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 a usage point of view, it's the same as Merge Patch, only with a 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 is lists. Lists 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 the entire container list with the single nginx container we put into the payload. On the other hand, Strategic Merge is a little bit smarter and it uses the metadata API to determine whether a list should be merged or replaced. Our patch would result in a 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 strictly tell how objects should be handled. The operation must be provided as a $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 using JSON Patch, which is the easiest to generate and the most readable method.
Well, this is all I have. Now go patch your kube!