背景:kubernetes-client/java升级,复杂的patch出现各种问题,并且没有找到解决方案,经过研究&测试,找到了解决方案,希望能帮助到使用kubernetes-client/java客户端的同学;
patch方法调用出现异常:
{"kind":"Status","apiVersion":"v1","metadata":{},"status":"Failure","message":"PatchOptions.meta.k8s.io "" is invalid: force: Forbidden: may not be specified for non-apply patch","reason":"Invalid","details":{"group":"meta.k8s.io","kind":"PatchOptions","causes":[{"reason":"FieldValueForbidden","message":"Forbidden: may not be specified for non-apply patch","field":"force"}]},"code":422}
参考issue:https://github.com/kubernetes-client/java/issues/958 修改,发现并没有什么作用
于是给客户端提了issue: https://github.com/kubernetes-client/java/issues/1575 并没有什么结论…
然后查看各种文档 & 社区提问,均没有回复…
查看源码发现: 1.客户端6.0.1在使用的时候指定header是merge-patch; 2.客户端11.0.0使用的时候指定header是[json-patch,merge-patch]数组,每次都会检测json-patch,且没有参数可以控制; 所以只能支持到json-patch,使用merge的时候无论传参是什么,都无法通过;
json-patch使用方法:
// patch方法1 json-patch 需要 zjsonpatch 包, 支持JsonArray,V1Patch 两种数据类型作为参数
// 禁用move
// EnumSet flags = DiffFlags.dontNormalizeOpIntoMoveAndCopy().clone();
// JsonNode patchNode = JsonDiff.asJson(source, target, flags);
// JsonArray finalBody = new JsonArray();
// patchNode.forEach((JsonNode node)->{
// JsonObject item = new JsonObject();
// String op = node.path("op").textValue();
// if(!"remove".equalsIgnoreCase(op)){
// item.add("op", new JsonPrimitive(op));
// item.add("path", new JsonPrimitive(node.path("path").textValue()));
// if(node.path("value").isNumber()){
// item.add("value", new JsonPrimitive(node.path("value").numberValue()));
// } else if(node.path("value").isBoolean()){
// item.add("value", new JsonPrimitive(node.path("value").booleanValue()));
// } else if(node.path("value").isObject()){
// // 不支持
// } else {
// item.add("value", new JsonPrimitive(node.path("value").textValue()));
// }
// finalBody.add(item);
// }
// });
// res = apiInstance.patchNamespacedCustomObject(this.apiGroup, this.apiVersion, this.apiNameSpace, this.apiPlural, name, body, dryRun, fieldManager, null);
如果只是简单的使用,上面方法完全可以支持; 逻辑也很简单,先通过对比方法找到与老的资源文件(yml/json)不一样的地方(比如多了一行,op为add,少了了一行,则op为remove,对比方法得出的就是这样一个集合),然后通过patch方法修正,达到修改资源文件的目的,这也是最符合k8s语意的方案; 注:一般的jsonPatch(标准规则)是有5种,add/remove/move/replace/copy,k8s是不支持move/copy的,所以有了上面禁用的操作;
测试下来发现需求还是不满足,因为有多个nodeGroup的场景,通过对比方法只能得到一个对象,还是没法表示一个yaml的层级关系(需求比较灵活,支持的场景过多),于是继续测试…
和k8s团队的人聊了下,一般都是通过上面的方法,先对比,再patch,也就是最符合patch语意的方案,要想支持大改资源文件的情况,就必须使用merge-patch,或者直接使用update;
于是有两个方案:1.update(直接替换)2.merge-patch;由于update可能会有并发覆盖问题,于是放弃update方案,继续测试Java客户端merge-patch;(k8s团队推荐使用go,然而切换成本过高…继续使用java客户端)
继续看源码实现: 发现最后是调用patchNamespacedCustomObjectCall方法,发出http请求到k8s。
这个方法是可以带上header参数的,即可以指定json-patch还是merge-patch;于是继续查找有没有对应的API把这个接口通过其他方式暴露出来…最后发现有个PatchUtils类是对外开放的,于是通过这个类完成实现;
逻辑很简单:绕过CustomObjectsApi这一层,调用PatchUtils自己实现;
结论// k8s-client 11.0.0 merge patch使用方法(直接跳过CustomObjectsApi这一层)
public Object patchObject(String name, Object body) {
Object res = null;
try {
// 方法2,merge-patch,json-patch都支持
res = PatchUtils.patch(
Object.class,
() -> apiInstance.patchNamespacedCustomObjectCall(
this.apiGroup, this.apiVersion, this.apiNameSpace, this.apiPlural, name,
new V1Patch(JSON.toJSONString(body)),
dryRun, fieldManager, null, null
),
V1Patch.PATCH_FORMAT_JSON_MERGE_PATCH,
apiInstance.getApiClient()
);
log.info("apiInstance.patchNamespacedCustomObject res = {}", JSON.toJSONString(res));
} catch (ApiException e) {
log.error("Exception when calling patchObject");
log.error("Reason: " + e.getResponseBody());
e.printStackTrace();
}
return res;
}