Kubernetes Patch 小技巧
May 11, 2025 14:00 · 1031 words · 3 minute read
一个比较少见的小需求,在面向 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
}
然后就可以对它进行校验了。