json patch

json patch

该规范定义在 RFC 6902,定义了修改 json 格式的规范,同时还可以配合 http patch 请求一起使用,实例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
PATCH /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" ] }

规范:

  1. 如果原始 json 中不存在 key “/hello”,则会全新创建 key。
  2. 如果原始 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

总结

通过上述协议可以发现如下缺点:

  1. 对于数组的处理不是太理想,如果要删除数组中的某个元素,或者在数组中追加某个元素,则无法表达。
  2. 该协议对于人类并不友好。

json merge patch

定义在 RFC 7386,由于patch 能力比较有限,使用场景较少。

同样可以配合 http patch 方法一起使用,http 请求如下:

1
2
3
4
5
6
7
8
9
10
PATCH /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"
}

通过上述实例可以发现如下的功能缺陷:

  1. 如果某个 json 的 key 对应的值为 null,则无法表达,即不可以将某个 key 对应的value 设置为 null。
  2. 对于数组的处理非常弱,是直接对数组中所有元素的替换。

k8s strategic merge patch

该协议的资料较少,官方参考资料只有两篇文章,最好结合着 k8s 的代码才能完全理解:

背景

无论是 json patch,还是 json merge patch 协议,对于数组元素的支持都不够友好。
比如对于如下的 json:

1
2
3
4
spec:
containers:
- name: nginx
image: nginx-1.0

期望能够 patch 如下的内容

1
2
3
4
spec:
containers:
- name: log-tailer
image: log-tailer-1.0

从而可以实现 containers中包含两个元素的情况,无论是 json patch 还是 json merge patch,其行为是对数组元素的直接替换,不能实现追加的功能。

协议规范

为了解决 json merge patch 的功能缺陷,strategic merge patch 通过如下两种方式来扩展功能:

  1. json merge patch 的 json 语法增强,增加一些额外的指令
  2. 通过增强原始 json 的 struct 结构实现,跟 golang 语言强绑定,通过 golang 中的 struct tag 机制实现。这样的好处是不用再扩充 json merge patch 的 json 格式了。支持如下 struct tag:
    1. patchStrategy: 指定策略指令,支持:replace、merge 和 delete。默认的行为为 replace,保持跟 json merge patch 的兼容性。
    2. patchMergeKey: 数组一个子 map 元素的主键,类似于关系型数据库中一行记录的主键。

支持如下指令:

  1. replace
  2. merge
  3. 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
5
containers:
- name: nginx
image: nginx-1.0
- $patch: delete
name: log-tailer # merge key and value goes here

删除 map 的特定 key,如下实例可以删除 map 中的 key rollingUpdate。

1
2
rollingUpdate:
$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
5
finalizers:
- a
- b
- c
- b

patch yaml 如下,用来表示删除finalizers中的所有元素 b 和 c

1
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
2
finalizers:
- a

setElementOrder

用于数组中的元素排序

简单数组排序例子

原始内容如下:

1
2
3
4
finalizers:
- 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
4
finalizers:
- b
- c
- a
map 类型数组排序例子

其中 patchMergeKey 为 name 字段

1
2
3
4
5
6
7
containers:
- 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
7
containers:
- name: b
...
- name: c
...
- name: a
...

retainKeys

用来清理 map 结构中的 key,并指定保留的 key
原始内容:

1
2
3
union:
foo: a
other: b

patch 内容:

1
2
3
4
5
6
union:
retainKeys:
- another
- bar
another: d
bar: c

最终结果,可以看到 foo 和 other 因为不在保留列表中已经被清楚了。同时新增加了字段 another 和 bar,新增加的是字段是直接 patch 的结果,同时这两个字段也在保留的列表内。

1
2
3
4
union:
# 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]
  1. json:即支持 json patch 协议,例子:kubectl patch pod valid-pod --type='json' -p='[{"op": "replace", "path": "/spec/containers/0/image", "value":"newimage"}]
  2. merge:对应的为 json merge patch 协议。
  3. 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 后提交,数据包含:

  1. 要重新 apply 的 yaml
  2. 对象的annotation kubectl.kubernetes.io/last-applied-configuration 包含的内容
  3. 运行时的 k8s 对象

具体的操作步骤:

  1. 要重新 apply 的 yaml 跟annotation kubectl.kubernetes.io/last-applied-configuration 包含的内容比较,获取到要删除的字段。
  2. 要重新 apply 的 yaml 跟运行时的 k8s 对象进行比较,获取到要增加的字段。
  3. 上述两个结果再进行一次 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

引用