Kubernetes Patch 小技巧

May 11, 2025 14:00 · 1031 words · 3 minute read Kubernetes Golang

一个比较少见的小需求,在面向 Kubernetes 开发中,有时可能需要提前(在请求发送至 kube-apiserver 之前)得到 patch 操作后的资源对象。

首先我们要通过 clientset 或者 lister 拿到 patch 操作前的资源对象,以 Service 为例:

import (
    "sigs.k8s.io/controller-runtime/pkg/client"
)

// a lot of code here
service := &corev1.Service{}
if err := r.client.Get(req.Context(), client.ObjectKey{Namespace: namespace, Name: name}, service); err != nil {
    return err
}

patch 操作的关键内容是一个 JSON 结构体,以使用 kubectl edit 命令将某个 Service 的类型从 ClusterIP 更改为 NodePort 为例:

$ kubectl edit svc nginx-service -v 8
I0511 10:56:52.320219    7018 helper.go:269] "Request Body" body="{\"spec\":{\"type\":\"NodePort\"}}"
I0511 10:56:52.320273    7018 round_trippers.go:527] "Request" verb="PATCH" url="https://10.211.55.6:6443/api/v1/namespaces/default/services/nginx-service?fieldManager=kubectl-edit&fieldValidation=Strict" headers=<
 Accept: application/json
 Content-Type: application/strategic-merge-patch+json
 User-Agent: kubectl/v1.33.0 (darwin/arm64) kubernetes/60a317e
 >

patch 有三种类型,JSON 结构体中的内容随类型而有所不同,这里 kubectl edit 使用的是 strategic merge patch,其他两种是 json patch 和 merge patch,请阅读 https://kubernetes.io/docs/tasks/manage-kubernetes-objects/update-api-object-kubectl-patch/#use-a-json-merge-patch-to-update-a-deployment

所以 patch 类型也是一个必要的参数,kubectl 是通过 --type 选项来指定的,而 kube-apiserver 则通过 HTTP 请求中的 Content-Type 得知。

假设我们的需求是编写一个在 kubectl 和 kube-apiserver 之间的代理,它会拦截请求并校验 patch 后的资源对象是否合法。

import (
    "k8s.io/apimachinery/pkg/types"
)

    rawObj, err := io.ReadAll(req.Body)
    if err != nil {
        return nil, err
    }

    originalObjJS, err := json.Marshal(oldVService)
    if err != nil {
        return nil, err
    }
    patchType := types.PatchType(req.Header.Get("Content-Type"))
    patchedObjJS, err := applyPatch(originalObjJS, rawObj, patchType, oldVService)

首先从 HTTP 请求体中读出 patch 内容,并将原先的 Service 序列化;从 HTTP 请求头中得知 patch 类型。

import (
    jsonpatch "github.com/evanphx/json-patch"
    "k8s.io/apimachinery/pkg/util/strategicpatch"
)

// applyPatch applies the raw patch to original object and returns the patched one
func applyPatch(originalObjJS, patchRaw []byte, patchType types.PatchType, originalObject interface{}) (patchedObjJS []byte, err error) {
    switch patchType {
    case types.JSONPatchType:
        var patch jsonpatch.Patch
        if patch, err = jsonpatch.DecodePatch(patchRaw); err != nil {
            return
        }
        patchedObjJS, err = patch.Apply(originalObjJS)
    case types.MergePatchType:
        patchedObjJS, err = jsonpatch.MergePatch(originalObjJS, patchRaw)
    case types.StrategicMergePatchType:
        patchedObjJS, err = strategicpatch.StrategicMergePatch(originalObjJS, patchRaw, originalObject)
    default:
        return nil, fmt.Errorf("unexpected patch type %s", patchType)
    }
    return
}

通过 github.com/evanphx/json-patch 三方包来处理 json 和 merge path,而 strategic merge patch 则使用 k8s.io/apimachinery/pkg/util/strategicpatch 搞定。

上面参照了 kubectl patch 命令的实现 https://github.com/kubernetes/kubernetes/blob/1649f592f1909b97aa3c2a0a8f968a3fd05a7b8b/staging/src/k8s.io/kubectl/pkg/cmd/patch/patch.go#L334-L366

func getPatchedJSON(patchType types.PatchType, originalJS, patchJS []byte, gvk schema.GroupVersionKind, creater runtime.ObjectCreater) ([]byte, error) {
    switch patchType {
    case types.JSONPatchType:
        patchObj, err := jsonpatch.DecodePatch(patchJS)
        if err != nil {
            return nil, err
        }
        bytes, err := patchObj.Apply(originalJS)
        // TODO: This is pretty hacky, we need a better structured error from the json-patch
        if err != nil && strings.Contains(err.Error(), "doc is missing key") {
            msg := err.Error()
            ix := strings.Index(msg, "key:")
            key := msg[ix+5:]
            return bytes, fmt.Errorf("Object to be patched is missing field (%s)", key)
        }
        return bytes, err

    case types.MergePatchType:
        return jsonpatch.MergePatch(originalJS, patchJS)

    case types.StrategicMergePatchType:
        // get a typed object for this GVK if we need to apply a strategic merge patch
        obj, err := creater.New(gvk)
        if err != nil {
            return nil, fmt.Errorf("strategic merge patch is not supported for %s locally, try --type merge", gvk.String())
        }
        return strategicpatch.StrategicMergePatch(originalJS, patchJS, obj)

    default:
        // only here as a safety net - go-restful filters content-type
        return nil, fmt.Errorf("unknown Content-Type header for patch: %v", patchType)
    }
}

拿到 patch 后的 JSON 结构体,对其进行反序列化,得到一个新的 Service 对象:

    newVService := &corev1.Service{}
    if err := json.Unmarshal(patchedObjJS, newVService); err != nil {
        return nil, err
    }

然后就可以对它进行校验了。