bigywater.tech
Kubernetes · 16

Patch 的三种类型:merge、strategic 与 JSON patch 的区别

kubectl patch --type 的三个取值看似差不多,改容器列表时结果却天差地别。这篇用同一个目标跑三种 patch,看列表是被合并还是被整个替换,再看删除怎么写、哪些字段会按 key 合并、自定义资源能用哪几种。

3 种结果
同一个“改容器 a 的镜像”在 merge、strategic、JSON patch 下得到的不同对象
[t3]
strategic patch 往 tolerations 写一条 t3 之后列表的内容(原来是 [t1, t2])
415
对自定义资源用 --type=strategic 的结果:application/strategic-merge-patch+json is not supported

同一个修改,三种结果

实测 · 本机执行输出
  初始:containers={"a": {"image": "busybox:1.36", "command": ["sleep", "3600"], "env": ["X"]}, "b": {"image": "busybox:1.36", "command": ["sleep", "3600"], "env": []}} tolerations=['t1', 't2']
  --type=merge(JSON merge patch,RFC 7386):退出码 0;deployment.apps/web patched
    containers={"a": {"image": "busybox:1.35", "command": null, "env": []}} tolerations=['t1', 't2']
  --type=strategic(默认,按 name 合并列表):退出码 0;deployment.apps/web patched
    containers={"a": {"image": "busybox:1.35", "command": ["sleep", "3600"], "env": ["X"]}, "b": {"image": "busybox:1.36", "command": ["sleep", "3600"], "env": []}} tolerations=['t1', 't2']
  --type=json(JSON patch,RFC 6902,按下标):退出码 0;deployment.apps/web patched
    containers={"a": {"image": "busybox:1.35", "command": ["sleep", "3600"], "env": ["X"]}, "b": {"image": "busybox:1.36", "command": ["sleep", "3600"], "env": []}} tolerations=['t1', 't2']
  merge patch 把 a 的 image 和 command 写全再试:退出码 0;deployment.apps/web patched
    containers={"a": {"image": "busybox:1.35", "command": ["sleep", "3600"], "env": []}} tolerations=['t1', 't2'](容器 b 仍然被整个列表替换掉了)

目标都是“把容器 a 的镜像改成 busybox:1.35”。merge patch(RFC 7386)把整个 containers 列表当成一个值替换:结果只剩 a,a 的 command 丢了(null),容器 b 不见了;就算把 a 的 image 和 command 写全,b 也回不来。strategic merge patch(kubectl 对内置资源的默认类型)按 name 对齐列表元素,只改 a 的 image,其余都保留。JSON patch(RFC 6902)按下标 containers/0/image 精确修改,同样只动 image。

怎么删

实测 · 本机执行输出
  merge patch 把 annotation drop 设为 null:deployment.apps/web patched;annotations={"deployment.kubernetes.io/revision":"1","keep":"yes"}
  strategic 用 $patch: delete 删掉容器 b:deployment.apps/web patched;containers={"a": {"image": "busybox:1.36", "command": ["sleep", "3600"], "env": ["X"]}} tolerations=['t1', 't2']
  strategic 删掉容器 a 的 env X:deployment.apps/web patched;containers={"a": {"image": "busybox:1.36", "command": ["sleep", "3600"], "env": []}, "b": {"image": "busybox:1.36", "command": ["sleep", "3600"], "env": []}} tolerations=['t1', 't2']
  JSON patch remove containers/0(按下标):deployment.apps/web patched;containers={"b": {"image": "busybox:1.36", "command": ["sleep", "3600"], "env": []}} tolerations=['t1', 't2']
  JSON patch 先 test containers/0/name==b 再 remove(下标和预期不符):退出码 1;The request is invalid: the server rejected our request due to an error in our request

merge patch 里把某个 key 设为 null 就是删除(这里删掉了注解 drop);strategic 里用 $patch: delete 按 key 删列表元素(删掉容器 b,或容器 a 里的 env X);JSON patch 用 remove + 下标,下标一错删掉的就是别的元素(containers/0 删掉的是 a)。JSON patch 可以先加一个 test 操作做前置条件,条件不符整个 patch 被拒绝(这次退出码 1)。

哪些列表按 key 合并

实测 · 本机执行输出
  PodSpec.containers:{'x-kubernetes-list-type': 'map', 'x-kubernetes-patch-merge-key': 'name', 'x-kubernetes-patch-strategy': 'merge'}
  PodSpec.volumes:{'x-kubernetes-list-type': 'map', 'x-kubernetes-patch-merge-key': 'name', 'x-kubernetes-patch-strategy': 'merge,retainKeys'}
  PodSpec.tolerations:{'x-kubernetes-list-type': 'atomic'}
  Container.env:{'x-kubernetes-list-type': 'map', 'x-kubernetes-patch-merge-key': 'name', 'x-kubernetes-patch-strategy': 'merge'}
  Container.ports:{'x-kubernetes-list-type': 'map', 'x-kubernetes-patch-merge-key': 'containerPort', 'x-kubernetes-patch-strategy': 'merge'}
  Container.command:{'x-kubernetes-list-type': 'atomic'}
  Container.args:{'x-kubernetes-list-type': 'atomic'}
  strategic 改 tolerations(没有 merge key)写一条 t3:deployment.apps/web patched;containers={"a": {"image": "busybox:1.36", "command": ["sleep", "3600"], "env": ["X"]}, "b": {"image": "busybox:1.36", "command": ["sleep", "3600"], "env": []}} tolerations=['t3']

策略取决于 schema 里的扩展:containers 和 env 的 x-kubernetes-patch-merge-key 是 name,ports 是 containerPort,volumes 的策略是 merge,retainKeys;而 tolerations、command、args 只有 x-kubernetes-list-type: atomic,strategic patch 写一条 t3,tolerations 就从 [t1, t2] 变成只有 [t3]。

自定义资源

实测 · 本机执行输出
  对自定义资源 --type=strategic:退出码 1;error: application/strategic-merge-patch+json is not supported by k10.example.com/v1, Kind=Box: the body of the request was in an unknown format - accepted media types include: application/json-patch+json, application/merge-patch+;spec.items=[{"name":"first","v":1},{"name":"second","v":2}]
  对自定义资源 --type=merge:退出码 0;box.k10.example.com/b1 patched;spec.items=[{"name":"first","v":9}]
  对自定义资源 --type=json:退出码 0;box.k10.example.com/b1 patched;spec.items=[{"name":"first","v":9},{"name":"second","v":2}]

CRD 没有 Go 结构体上的 patchStrategy 标记,所以不支持 strategic merge patch:--type=strategic 得到 application/strategic-merge-patch+json is not supported by k10.example.com/v1, Kind=Box,并列出可接受的媒体类型(其中有 application/json-patch+json、application/merge-patch+json)。对自定义资源,merge patch 同样整个替换列表;要精确改列表元素,用 JSON patch 或 server-side apply(前提是 CRD 里声明了 x-kubernetes-list-type: map)。

分步演示

三种 patch
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / kind(Docker 里的单节点或多节点真实集群,节点是 Docker Desktop 虚拟机里的容器)/ kubectl;用空的 kubeconfig 之外的独立 kubeconfig,不碰用户自己的集群;计时用 date +%s%N / kubectl 事件时间戳,数字每次运行会有小幅抖动。三节点 kind 集群 kx-main(Kubernetes v1.37.0);Deployment web(replicas=0,两个 busybox:1.36 容器 a、b,a 带 command 和 env X,两条 tolerations)每个用例前重新创建;patch 用 kubectl patch --type=merge|strategic|json;merge key 信息取自 /openapi/v3/apis/apps/v1 里 PodSpec 与 Container 的 schema 扩展;自定义资源是 boxes.k10.example.com(x-kubernetes-preserve-unknown-fields);每种情形各一次
    • 没有验证:strategic merge patch 的 $setElementOrder、$retainKeys、$deleteFromPrimitiveList 指令;JSON patch 的 add/move/copy 操作;patch 与 server-side apply(--type=apply-patch)的混用;对 status 子资源的 patch;大对象 patch 的性能;kubectl patch 的 --patch-file 与 -f 形式
    京ICP备2026067502号-1京公网安备11010502063487号