json patch
该规范定义在 RFC 6902,定义了修改 json 格式的规范,同时还可以配合 http patch 请求一起使用,实例如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14PATCH /my/data HTTP/1.1
Host: example.org
Content-Length: 326
Content-Type: application/json-patch+json
If-Match: "abc123"
[
{ "op": "test", "path": "/a/b/c", "value": "foo" },
{ "op": "remove", "path": "/a/b/c" },
{ "op": "add", "path": "/a/b/c", "value": [ "foo", "bar" ] },
{ "op": "replace", "path": "/a/b/c", "value": 42 },
{ "op": "move", "from": "/a/b/c", "path": "/a/b/d" },
{ "op": "copy", "from": "/a/b/d", "path": "/a/b/e" }
]
支持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
2
3{
"hello": ["123"]
}
执行后结果如下:1
2
3
4
5{
"hello": [
"world"
]
}
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
2
3
4{
"hello": "world",
"hello2": "world2"
}
执行完成后的 json 如下:1
2
3
4{
"hello": "world",
"hello2": "world"
}
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
2
3
4
5
6
7
8
9
10PATCH /target HTTP/1.1
Host: example.org
Content-Type: application/merge-patch+json
{
"a":"z",
"c": {
"f": null
}
}
下面结合具体的实例来说明 json merge patch 的功能。
原始 json 格式如下:1
2
3
4
5
6
7
8
9{
"title": "Goodbye!",
"author" : {
"givenName" : "John",
"familyName" : "Doe"
},
"tags":[ "example", "sample" ],
"content": "This will be unchanged"
}
patch json 格式如下:1
2
3
4
5
6
7
8{
"title": "Hello!",
"phoneNumber": "+01-123-456-7890",
"author": {
"familyName": null
},
"tags": [ "example" ]
}
其中 null 用来表示该 key 需要删除。对于数组类型,则直接覆盖数组中的值。
patch 完成后的 json 如下:1
2
3
4
5
6
7
8
9{
"title": "Hello!",
"author" : {
"givenName" : "John"
},
"tags": [ "example" ],
"content": "This will be unchanged",
"phoneNumber": "+01-123-456-7890"
}
通过上述实例可以发现如下的功能缺陷:
- 如果某个 json 的 key 对应的值为 null,则无法表达,即不可以将某个 key 对应的value 设置为 null。
- 对于数组的处理非常弱,是直接对数组中所有元素的替换。
k8s strategic merge patch
该协议的资料较少,官方参考资料只有两篇文章,最好结合着 k8s 的代码才能完全理解:
背景
无论是 json patch,还是 json merge patch 协议,对于数组元素的支持都不够友好。
比如对于如下的 json:1
2
3
4spec:
containers:
- name: nginx
image: nginx-1.0
期望能够 patch 如下的内容1
2
3
4spec:
containers:
- name: log-tailer
image: log-tailer-1.0
从而可以实现 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
2
3
4$patch: replace # recursive and applies to all fields of the map it's in
containers:
- name: nginx
image: nginx-1.0
delete
删除数组中的特定元素,下面例子可以删除数组中包含 name: log-tailer 的元素。1
2
3
4
5containers:
- name: nginx
image: nginx-1.0
- $patch: delete
name: log-tailer # merge key and value goes here
删除 map 的特定 key,如下实例可以删除 map 中的 key rollingUpdate。1
2rollingUpdate:
$patch: delete
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
2
3
4
5finalizers:
- a
- b
- c
- b
patch yaml 如下,用来表示删除finalizers中的所有元素 b 和 c1
2
3
4
5
6# The directive includes the prefix $deleteFromPrimitiveList and
# followed by a '/' and the name of the list.
# The values in this list will be deleted after applying the patch.
$deleteFromPrimitiveList/finalizers:
- b
- c
最终得到结果:1
2finalizers:
- a
setElementOrder
用于数组中的元素排序
简单数组排序例子
原始内容如下:1
2
3
4finalizers:
- a
- b
- c
设置排序顺序:1
2
3
4
5
6# The directive includes the prefix $setElementOrder and
# followed by a '/' and the name of the list.
$setElementOrder/finalizers:
- b
- c
- a
最终得到排序顺序:1
2
3
4finalizers:
- b
- c
- a
map 类型数组排序例子
其中 patchMergeKey 为 name 字段1
2
3
4
5
6
7containers:
- name: a
...
- name: b
...
- name: c
...
patch 指令的格式:1
2
3
4
5# each map in the list should only include the mergeKey
$setElementOrder/containers:
- name: b
- name: c
- name: a
最终获得结果:1
2
3
4
5
6
7containers:
- name: b
...
- name: c
...
- name: a
...
retainKeys
用来清理 map 结构中的 key,并指定保留的 key
原始内容:1
2
3union:
foo: a
other: b
patch 内容:1
2
3
4
5
6union:
retainKeys:
- another
- bar
another: d
bar: c
最终结果,可以看到 foo 和 other 因为不在保留列表中已经被清楚了。同时新增加了字段 another 和 bar,新增加的是字段是直接 patch 的结果,同时这两个字段也在保留的列表内。1
2
3
4union:
# Field foo and other have been cleared w/o explicitly set them to null.
another: d
bar: c
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 |