json patch
json patch
该规范定义在 RFC 6902,定义了修改 json 格式的规范,同时还可以配合 http patch 请求一起使用,实例如下:
1 | PATCH /my/data HTTP/1.1 |
支持add、remove、replace、move、copy和 test 六个patch动作。
协议规范
add
格式如下:
1 | { "op": "add", "path": "/hello", "value": [ "foo" ] } |
规范:
- 如果原始 json 中不存在 key “/hello”,则会全新创建 key。
- 如果原始 json 存在 key “/hello”,则会直接覆盖;即使”/hello”为数组,也不会在原先的基础上追加,而是直接强制覆盖;
原始 json 如下:
1 | { |
执行后结果如下:
1 | { |
remove
用来删除某个 key,格式如下:
1 | [{ "op": "remove", "path": "/hello" }] |
replace
用来替换某个 key,跟 add 动作的差异是,如果 key 不存在,则不会创建 key。
1 | { "op": "replace", "path": "/hello", "value": 42 } |
如果原始 json 格式为: {}
,执行完成后,输出 json 格式仍然为:{}
。
move
用来修改 key 的名称,格式如下:
1 | { "op": "move", "from": "/hello", "path": "/hello2" } |
如果 key 不存在,则不做任何修改。
copy
用来复制某个 key,格式如下:
1 | { "op": "copy", "from": "/hello", "path": "/hello2" } |
如果原始 key 不存在,则不复制;如果目标 key 已经存在,则仍然会复制。
原始 json 如下:
1 | { |
执行完成后的 json 如下:
1 | { |
test
用来测试 key 对应的 value 是否相等,该操作并不常用
1 | { "op": "test", "path": "/a/b/c", "value": "foo" } |
工具
- JSON Patch Builder Online 在线工具,可根据原始 json 和 patch 完成后的 json,产生 json patch
- jsonpatch.me 在线工具,可根据原始 json 和 json patch,产生 patch 完成后的 json
总结
通过上述协议可以发现如下缺点:
- 对于数组的处理不是太理想,如果要删除数组中的某个元素,或者在数组中追加某个元素,则无法表达。
- 该协议对于人类并不友好。
json merge patch
定义在 RFC 7386,由于patch 能力比较有限,使用场景较少。
同样可以配合 http patch 方法一起使用,http 请求如下:
1 | PATCH /target HTTP/1.1 |
下面结合具体的实例来说明 json merge patch 的功能。
原始 json 格式如下:
1 | { |
patch json 格式如下:
1 | { |
其中 null 用来表示该 key 需要删除。对于数组类型,则直接覆盖数组中的值。
patch 完成后的 json 如下:
1 | { |
通过上述实例可以发现如下的功能缺陷:
- 如果某个 json 的 key 对应的值为 null,则无法表达,即不可以将某个 key 对应的value 设置为 null。
- 对于数组的处理非常弱,是直接对数组中所有元素的替换。
k8s strategic merge patch
该协议的资料较少,官方参考资料只有两篇文章,最好结合着 k8s 的代码才能完全理解:
背景
无论是 json patch,还是 json merge patch 协议,对于数组元素的支持都不够友好。
比如对于如下的 json:
1 | spec: |
期望能够 patch 如下的内容
1 | spec: |
从而可以实现 containers中包含两个元素的情况,无论是 json patch 还是 json merge patch,其行为是对数组元素的直接替换,不能实现追加的功能。
协议规范
为了解决 json merge patch 的功能缺陷,strategic merge patch 通过如下两种方式来扩展功能:
- json merge patch 的 json 语法增强,增加一些额外的指令
- 通过增强原始 json 的 struct 结构实现,跟 golang 语言强绑定,通过 golang 中的 struct tag 机制实现。这样的好处是不用再扩充 json merge patch 的 json 格式了。支持如下 struct tag:
- patchStrategy: 指定策略指令,支持:replace、merge 和 delete。默认的行为为 replace,保持跟 json merge patch 的兼容性。
- patchMergeKey: 数组一个子 map 元素的主键,类似于关系型数据库中一行记录的主键。
支持如下指令:
- replace
- merge
- delete
replace
支持 go struct tag 和 在 json patch 中增加指令两种方式。
replace 是默认的指令模式,对于数组而言会直接全部替换数组内容。
如下指令用来表示,
1 | $patch: replace # recursive and applies to all fields of the map it's in |
delete
删除数组中的特定元素,下面例子可以删除数组中包含 name: log-tailer 的元素。
1 | containers: |
删除 map 的特定 key,如下实例可以删除 map 中的 key rollingUpdate。
1 | rollingUpdate: |
merge
该指令仅支持 go struct tag 模式,格式为:$deleteFromPrimitiveList/<keyOfPrimitiveList>: [a primitive list]
。
deleteFromPrimitiveList
删除数组中的某个元素
go struct 定义如下:
1 | Finalizers []string `json:"finalizers,omitempty" patchStrategy:"merge" protobuf:"bytes,14,rep,name=finalizers"` |
原始 yaml 如下:
1 | finalizers: |
patch yaml 如下,用来表示删除finalizers中的所有元素 b 和 c
1 | # The directive includes the prefix $deleteFromPrimitiveList and |
最终得到结果:
1 | finalizers: |
setElementOrder
用于数组中的元素排序
简单数组排序例子
原始内容如下:
1 | finalizers: |
设置排序顺序:
1 | # The directive includes the prefix $setElementOrder and |
最终得到排序顺序:
1 | finalizers: |
map 类型数组排序例子
其中 patchMergeKey 为 name 字段
1 | containers: |
patch 指令的格式:
1 | # each map in the list should only include the mergeKey |
最终获得结果:
1 | containers: |
retainKeys
用来清理 map 结构中的 key,并指定保留的 key
原始内容:
1 | union: |
patch 内容:
1 | union: |
最终结果,可以看到 foo 和 other 因为不在保留列表中已经被清楚了。同时新增加了字段 another 和 bar,新增加的是字段是直接 patch 的结果,同时这两个字段也在保留的列表内。
1 | union: |
strategic merge patch 在 k8s 中应用
kubectl patch 命令通过–type 参数提供了几种 patch 方法。
1 | --type='strategic': The type of patch being provided; one of [json merge strategic] |
- json:即支持 json patch 协议,例子:
kubectl patch pod valid-pod --type='json' -p='[{"op": "replace", "path": "/spec/containers/0/image", "value":"newimage"}]
- merge:对应的为 json merge patch 协议。
- stategic:k8s 特有的 patch 协议,在 json merge patch 协议基础上的扩展,可以解决 json merge patch 的缺点。
对于 k8s 的 CRD 对象,由于不存在 go struct tag,因此无法使用 stategic merge patch。
TODO:待补充具体例子
kubectl patch、replace、apply之间的区别
patch
kubectl patch 命令的实现比较简单,直接调用 kube-apiserver 的接口在 server 端实现 patch 操作。
replace
如果该命令使用 –force=true 参数,则会先删除对象,然后再提交,相当于是全新创建。
apply
apply 的实现相对比较复杂,逻辑也比较绕,可以实现 map 结构中的字段增删改操作,数组中数据的增删改操作。实现上会将多份数据进行merge 后提交,数据包含:
- 要重新 apply 的 yaml
- 对象的annotation kubectl.kubernetes.io/last-applied-configuration 包含的内容
- 运行时的 k8s 对象
具体的操作步骤:
- 要重新 apply 的 yaml 跟annotation kubectl.kubernetes.io/last-applied-configuration 包含的内容比较,获取到要删除的字段。
- 要重新 apply 的 yaml 跟运行时的 k8s 对象进行比较,获取到要增加的字段。
- 上述两个结果再进行一次 merge 操作,最终调用 kube-apiserver 的接口实现 patch 操作。
为什么一定需要用到kubectl.kubernetes.io/last-applied-configuration的数据呢?
在 yaml 对象提交到 k8s 后,k8s 会自动增加一些字段,也可以通过额外的方式修改对象增加一些字段。如果 patch 内容仅跟运行时结果比较,会导致一些运行时的k8s 自动增加的字段或者手工更新的字段被删除掉。
试验 | 上次提交 last-applied-configuration | 运行时 | patch 内容 | 结果 | 结果分析 |
---|---|---|---|---|---|
试验一 | label1: first | label1: first | label2: second | label2: second | 1. patch 内容跟上次内容比较,发现要删除字段 label1 2. patch 内容跟运行时比较,发现新增加了字段 label2 3. 最终 label1 被删除,仅保留 label2 |
试验二 | label1: first | label1: first label2: second |
label1: first | label1: first label2: second |
1. patch 内容跟上次内容比较,发现结果无变化 2. patch 内容跟运行时比较,发现要新增加字段 label2 3. 最终新增加字段 label2 |