Kubernetes中的JSON patch介绍两种对JSON进行更新的方案,然后会结合Kubernetes中的webh
我们都知道Kubernetes的资源可以用yaml和JSON表示,对于资源的添加和更新都都可以理解为对yaml或者JSON文档的操作。查看源码可以知道Kubernetes底层对资源的操作都使用JSON来表示,由于Kubernetes资源的复杂和数量众多,不可避免的遇到对复杂JSON对象的操作。下文会先介绍两种对JSON进行更新的方案,然后会结合Kubernetes中的webhook机制中涉及到的JSON操作来理解JSON Patch的原理和用法。
IETF收录的两种JSON修补方案
JSON格式文件改动的方案有很多,但是被IEFT官方收录的就两种JSON Patch和JSON Merge Patch。
这些方案声明声明性地描述了两个 JSON 文档之间的差异。既然针对修改官方发布了两种方案,那肯定两者都有优点和缺点,没有哪种方案适合所有的case。
JSON Patch
JSON Patch 定义了一个 JSON 文档结构,用于表达应用于 JavaScript Object Notation (JSON) 文档的一系列操作;它适用于 HTTP PATCH 方法,相当于这个方案扩展了http协议,比如 “application/JSON-patch+JSON”的meme.type就是用于标识此类Patch文档,它所定义的参数结构中包含对资源执行部分修改的方法。
比如对如下的JSON文档进行修改:
{
"users" : [
{ "name" : "Alice" , "email" : "alice@example.org" },
{ "name" : "Bob" , "email" : "bob@example.org" }
]
}
patch文档为:
[
{
"op" : "replace" ,
"path" : "/users/0/email" ,
"value" : "alice@wonderland.org"
},
{
"op" : "add" ,
"path" : "/users/-" ,
"value" : {
"name" : "Christine",
"email" : "christine@example.org"
}
}
]
得到的结果是:
{
"users" : [
{ "name" : "Alice" , "email" : "alice@wonderland.org" },
{ "name" : "Bob" , "email" : "bob@example.org" },
{ "name" : "Christine" , "email" : "christine@example.org" }
]
}
可以看到我们对数据进行了替换和新增。其中:
- “op”表示操作,有“test”,“remove”,“add”,“replace”,“move”,“copy”这六种操作。
- “path”表示该操作指向JSON文档的目标
- “value”表示操作的具体的值
上述三种类型的JSON Object定义没有顺序要求。上文已经做了“add","replace"操作的示范。“remove"相当于删除。“move”相当于从一个位置删除(remove)JSON Object,再从另一个位置添加(add)JSON Object,只不过这个操作是原子性的。和“move”类似的操作还有“copy”,相当于“add”,但是添加的JSON Object来自于源文件中指定路径上的JSON Object。
其中比较特殊的是“test",简单来说他就是"equal",比较值或JSON Object。
{
"op": "test",
"path": "/a/b/c",
"value": "foo"
}
如上所示,如果文档中路径“/a/b/c”下的值等于"foo"那么该操作就会成功。
错误控制
规范定义了如果一次JSON Patch操作违反了规范要求,或者操作不成功,则会终止对JSON文档的操作,且视为操作不成功。这里可以参考http patch操作的状态码。
原子性
根据http patch原子性的定义,原子性的含义就是以下操作不会对源文档产生修改,因为“test”会失败,而整个操作附带“replace”操作也会回滚。
[
{ "op": "replace", "path": "/a/b/c", "value": 42 },
{ "op": "test", "path": "/a/b/c", "value": "C" }
]
JSON Merge Patch
JSON Merge Patch和JSON Patch很像,但是他描述了JSON文档更改的版本。与上文提到的方式相比,这种方式更像git中的差异文件,而JSON PATH更像操作数据库。这种方式不包含操作,只包含文档的节点。比如对如下文档进行修改:
{
"a": "b",
"c": {
"d": "e",
"f": "g"
}
}
基于上文运行如下Patch:
{
"a":"z",
"c": {
"f": null
}
}
这种方式一看就很易于理解,但是有一些潜在的限制,比如
- 没办法将value置为null,因为null的操作在merge patch的场景下是删除的意思。当然如果在处理JSON文档的程序中null就代表资源不存在,那确实也可以避免这个问题。
- 无法追加数组,因为必须提供完整的数组才能表达元素的修改
- 一旦Patch是错误的,比如路径错误,他也会被合并到正确的数据中去,因为merge patch看起来是一种非常灵活的数据修改方式,但在JSON Patch中如果路径错误的话,会导致操作失败。
综上所述,如果面对的是结构比较简单,校验不要求很强烈的场景,可以选择JSON Merge Patch,但是更为负责的场景,建议选择JSON Patch,因为这种方式确保原子执行和错误报告。
JSON修补在Kubernetes中的应用
webhook是一种反向API,相当于服务器反向调用客户端。在云原生场景中这里的服务器就是指的api-server,客户端就是指的具体的webhook服务器,比如isito中的webhook服务器就是isitod,chaos-mesh中的webhook服务器则是chao-controller-manager,举这两个例子是因为他们是使用webhook做sidecar注入的经典场景。它们都打开了诸如443这种类型的端口用来接收api-server的请求。
因为sidecar本质上就是静默改动提交给Kubernetes的资源数据,所以这里面就涉及到了JSON修改操作。
Kubernetes中的webhook采用的就是上文提到的第一种方案(JSON Patch),具体代码位置在staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/mutating/dispatcher.go
中
func (a *mutatingDispatcher) callAttrMutatingHook(ctx context.Context, h *admissionregistrationv1.MutatingWebhook, invocation *generic.WebhookInvocation, attr *generic.VersionedAttributes, o admission.ObjectInterfaces, round, idx int) (bool, error) {
...
patchObj, err := JSONpatch.DecodePatch(result.Patch)
var patchedJS []byte
JSONSerializer := JSON.NewSerializer(JSON.DefaultMetaFactory, o.GetObjectCreater(), o.GetObjectTyper(), false)
switch result.PatchType {
case admissionv1.PatchTypeJSONPatch:
objJS, err := runtime.Encode(JSONSerializer, attr.VersionedObject)
if err != nil {
return false, apierrors.NewInternalError(err)
}
patchedJS, err = patchObj.Apply(objJS)
if err != nil {
return false, apierrors.NewInternalError(err)
}
default:
return false, &webhookutil.ErrCallingWebhook{WebhookName: h.Name, Reason: fmt.Errorf("unsupported patch type %q", result.PatchType)}
}
...
if newVersionedObject, _, err = JSONSerializer.Decode(patchedJS, nil, newVersionedObject); err != nil {
return false, apierrors.NewInternalError(err)
}
...
annotator.addPatchAnnotation(patchObj, result.PatchType)
...
}
这里的JSONpatch.DecodePatch(result.Patch)
中的JSONpatch包是一个开源的第三方库(github.com/evanphx/JSON-patch),它是一个JSONpatch库,它提供了对JSON的Patch操作和Merge Patch操作。
查看该库的代码就会发现该库对上文提到的各种操作都有对应的实现,如下图所示:
对应的做为webook的服务器,istiod和chaos-controller-manager都对修改对象资源操作做了处理逻辑,这里以修改pod的annotation这种典型场景来举例。
istiod
func updateAnnotation(target map[string]string, added map[string]string) (patch []rfc6902PatchOperation) {
var keys []string
for k := range added {
keys = append(keys, k)
}
sort.Strings(keys)
for _, key := range keys {
value := added[key]
if target == nil {
target = map[string]string{}
patch = append(patch, rfc6902PatchOperation{
Op: "add",
Path: "/metadata/annotations",
Value: map[string]string{
key: value,
},
})
} else {
op := "add"
if target[key] != "" {
op = "replace"
}
patch = append(patch, rfc6902PatchOperation{
Op: op,
Path: "/metadata/annotations/" + escapeJSONPointerValue(key),
Value: value,
})
}
}
return patch
}
func escapeJSONPointerValue(in string) string {
step := strings.Replace(in, "~", "~0", -1)
return strings.Replace(step, "/", "~1", -1)
}
chaos-controller-manager
func updateAnnotations(target map[string]string, added map[string]string) (patch []patchOperation) {
for key, value := range added {
if target == nil || target[key] == "" {
target = map[string]string{}
patch = append(patch, patchOperation{
Op: "add",
Path: "/metadata/annotations",
Value: map[string]string{
key: value,
},
})
} else {
patch = append(patch, patchOperation{
Op: "replace",
Path: "/metadata/annotations/" + key,
Value: value,
})
}
}
return patch
}
代码逻辑比较简单,可以看到都是使用的JSON Patch的“add”和“replace”修改pod中的annotation,相对来说istiod对于JSONPatch的处理更好,而且通过key的排序能够保证annotation的顺序性。经过测试,chaos-mesh的逻辑在podspec中没**有target[key]**的时候会覆盖pod原有的所有annotation。
转载自:https://juejin.cn/post/6993618347904466957