使用Patch方式更新K8S的 API Objects 一共有三种方式:strategic merge patch, json-patch,json merge patch。关于这三种方式的文字描述区别可看官方文档update-api-object-kubectl-patch。

我在本文中主要会介绍使用client-go的Patch方式,主要包括strategic merge patchjson-patch。不介绍json merge patch的原因,是该方式使用场景比较少,因此不做介绍,如果有同学有兴趣,可做补充。

StrategicMergePatch

新增Object值

本次示例以给一个node新增一个labels为例,直接上代码:

//根据Pod Sn 更新 podfunc UpdatePodByPodSn(coreV1 v1.CoreV1Interface, podSn string, patchData map[string]interface{}) (*apiv1.Pod, error) {    v1Pod, err := coreV1.Pods("").Get(podSn, metav1.GetOptions{})    if err != nil {        logs.Error("[UpdatePodByPodSn]  get pod %v  fail %v", podSn, err)        return nil, fmt.Errorf("[UpdatePodByPodSn]  get pod %v  fail %v", podSn, err)    }    namespace := v1Pod.Namespace    podName := v1Pod.Name    playLoadBytes, _ := json.Marshal(patchData)    newV1Pod, err := coreV1.Pods(namespace).Patch(podName, types.StrategicMergePatchType, playLoadBytes)    if err != nil {        logs.Error("[UpdatePodByPodSn] %v pod Patch fail %v", podName, err)        return nil, fmt.Errorf("[UpdatePodByPodSn] %v pod Patch fail %v", podName, err)    }    return newV1Pod, nil}
注意:上面的PatchData 必须是以 {"metadata":...}的go struct, 如:`map[string]interface{}{"metadata": map[string]map[string]string{"labels": {
        "test2": "test2",    }}}`

对应单元测试用例

func pod(podName string, nodeName string, labels map[string]string, annotations map[string]string) *v1.Pod {    return &v1.Pod{ObjectMeta: metav1.ObjectMeta{Name: podName, Labels: labels, Annotations: annotations}, Spec: v1.PodSpec{NodeName: nodeName}, Status: v1.PodStatus{}}}func TestUpdatePodByPodSn(t *testing.T) {    var tests = []struct {        expectedError      interface{}        expectedAnnotation string        expectedLabel      string        podSn              string        patchData          map[string]interface{}        v1Pod              []runtime.Object    }{        {nil, "test2", "", "1.1.1.1", map[string]interface{}{"metadata": map[string]map[string]string{"annotations": {            "test2": "test2",        }}},            []runtime.Object{pod("1.1.1.1", "1.1.1.1", map[string]string{"test1": "test1"}, map[string]string{"test1": "test1"})},        },        {nil, "", "", "1.1.1.2", map[string]interface{}{"metadata": map[string]map[string]string{"labels": {            "test2": "",        }}},            []runtime.Object{pod("1.1.1.2", "1.1.1.1", map[string]string{"test1": "test1"}, map[string]string{"test1": "test1"})},        },        {nil, "", "test2", "1.1.1.3", map[string]interface{}{"metadata": map[string]map[string]string{"labels": {            "test2": "test2",        }}},            []runtime.Object{pod("1.1.1.3", "1.1.1.1", map[string]string{"test1": "test1"}, map[string]string{"test1": "test1"})},        },    }    for _, test := range tests {        client := fake.NewSimpleClientset(test.v1Pod...)        v1Pod, err := UpdatePodByPodSn(client.CoreV1(), test.podSn, test.patchData)        if err != nil {            t.Errorf("expected error  %s, got %s", test.expectedError, err)        }        assert.Equal(t, v1Pod.Annotations["test2"], test.expectedAnnotation)        assert.Equal(t, v1Pod.Labels["test2"], test.expectedLabel)    }}

修改Object的值

修改Obejct的值使用方式如下,当使用strategic merge patch的时候,如果提交的数据中键已经存在,那就会使用新提交的值替换原先的数据。依旧以修改labels的值为例。
如新提交的数据为:

{  "metadata":{      "labels":{          "test2":"test3",      },  }}

Node中已经存在的labels为:

{  "metadata":{      "labels":{          "test2":"test1",      },  }}

最终Node中labels的key为test2的值会被替换成 test3

删除Object值

当需要把某个Object的值删除的时候,当使用strategic merge patch的时候,依旧是删除labels为例提交方式是:

golang里面的表现形式是:

{  "metadata":{      "labels":{          "test2":nil      },  }}

对应从浏览器提交的数据是:

{  "metadata":{      "labels":{          "test2":null      },  }}
PS:如果不喜欢使用上面struct的方式组成数据,可以使用如下的方式 labelsPatch := fmt.Sprintf({"metadata":{"labels":{"%s":"%s"}}}, labelkey, labelvalue) 直接代替上面示例中的patchData

JSONPatch

JSONPatch的详细说明请参考文档:http://jsonpatch.com/。
JSONPatch 主要有三种操作方式:add,replace,remove。以下会以代码示例说明这三种操作在Client-go对应的代码示例来说明怎样操作K8s 的资源。

使用JSONPatch,如果Patch中带有斜杠“/”和 (~)这两个字符,不能直接传入这两个字符,需要你输入的时候就人工转换下,/转换成~1~转换成~0。以新增labels为例,如我要新增一个"test1/test2":"test3"的labels,可以把要传入的数据修改为"test1~1test2":"test3"即可。

Op:add

使用JSONPatch的方式新增一个标签,其提交的数据格式必须是[{ "op": "replace", "path": "/baz", "value": "boo" }] 这样的。代码如下:

//patchStringValue specifies a patch operation for a string.type PatchStringValue struct {    Op    string      `json:"op"`    Path  string      `json:"path"`    Value interface{} `json:"value"`}type PatchNodeParam struct {    coreV1       v1.CoreV1Interface    NodeSn       string                 `json:"nodeSn"`    OperatorType string                 `json:"operator_type"`    OperatorPath string                 `json:"operator_path"`    OperatorData map[string]interface{} `json:"operator_data"`}//patch node info, example label, annotationfunc patchNode(param PatchNodeParam) (*apiv1.Node, error) {    coreV1 := param.coreV1    nodeSn := param.NodeSn    node, err := coreV1.Nodes().Get(nodeSn, metav1.GetOptions{})    if err != nil {        return nil, err    }    operatorData := param.OperatorData    operatorType := param.OperatorType    operatorPath := param.OperatorPath    var payloads []interface{}    for key, value := range operatorData {        payload := PatchStringValue{            Op:    operatorType,            Path:  operatorPath + key,            Value: value,        }        payloads = append(payloads, payload)    }    payloadBytes, _ := json.Marshal(payloads)    newNode, err := coreV1.Nodes().Patch(nodeSn, types.JSONPatchType, payloadBytes)    if err != nil {        return nil, err    }    return newNode, err}

单元测试:

func TestPatchNode(t *testing.T) {    Convey("test patchNode", t, func() {        Convey("Patch Node fail", func() {            var tests = []struct {                nodeSn        string                operatorType  string                operatorPath  string                operatorData  map[string]interface{}                expectedError interface{}                expectedValue *v1.Node                objs          []runtime.Object            }{                {"1.1.1.1", "add", "/metadata/labels/",                    map[string]interface{}{                        "test1": "test1",                        "test2": "test2"},                    "nodes \"1.1.1.1\" not found", nil, nil},                {"1.1.1.1", "aaa", "/metadata/labels/",                    map[string]interface{}{                        "test1": "test1",                        "test2": "test2"},                    "Unexpected kind: aaa", nil, []runtime.Object{node("1.1.1.1", nil, nil)}},            }            for _, test := range tests {                client := fake.NewSimpleClientset(test.objs...)                param := PatchNodeParam{                    coreV1:       client.CoreV1(),                    NodeSn:       test.nodeSn,                    OperatorType: test.operatorType,                    OperatorPath: test.operatorPath,                    OperatorData: test.operatorData,                    EmpId:        test.empId,                }                output, err := patchNode(param)                So(output, ShouldEqual, test.expectedValue)                So(err.Error(), ShouldEqual, test.expectedError)            }        })        Convey("Patch Node success", func() {            var tests = []struct {                nodeSn        string                operatorType  string                operatorPath  string                operatorData  map[string]interface{}                expectedError interface{}                expectedValue string                objs          []runtime.Object            }{                {"1.1.1.1", "add", "/metadata/labels/",                    map[string]interface{}{                        "test1": "test1",                        "test2": "test2"},                    nil, "1.1.1.1", []runtime.Object{node("1.1.1.1", map[string]string{"test3": "test3"}, map[string]string{"test3": "test3"})}},                {"1.1.1.1", "add", "/metadata/labels/",                    map[string]interface{}{                        "test1": "test1",                        "test2": "test2"},                    nil, "1.1.1.1", []runtime.Object{node("1.1.1.1", map[string]string{"test1": "modifytest"}, map[string]string{"test1": "modifytest"})}},            }            for _, test := range tests {                client := fake.NewSimpleClientset(test.objs...)                param := PatchNodeParam{                    coreV1:       client.CoreV1(),                    NodeSn:       test.nodeSn,                    OperatorType: test.operatorType,                    OperatorPath: test.operatorPath,                    OperatorData: test.operatorData,                }                output, err := patchNode(param)                So(output, ShouldNotBeNil)                So(err, ShouldBeNil)                So(output.Name, ShouldEqual, test.expectedValue)            }        })    })}
使用add有个需要注意的地方就是,当你的Path是使用的/metadata/labels而不是/metadata/labels/labelkey的时候,那你这个add操作实际是对整个labels进行替换,而不是新增,一定要注意避免踩坑。
PS:如果不喜欢使用上面struct的方式组成数据,可以使用如下的方式 labelsPatch := fmt.Sprintf([{"op":"add","path":"/metadata/labels/%s","value":"%s" }], labelkey, labelvalue) 直接代替上面示例中的patchData

Op:remove

要删除一个标签的话,代码和增加区别不大,唯一的区别就是提交的数据要由键值对修改为提交一个string slice类型[]string,代码如下:

type PatchNodeParam struct {    coreV1       v1.CoreV1Interface    NodeSn       string                 `json:"nodeSn"`    OperatorType string                 `json:"operator_type"`    OperatorPath string                 `json:"operator_path"`    OperatorData map[string]interface{} `json:"operator_data"`}//patchStringValue specifies a remove operation for a string.type RemoveStringValue struct {    Op   string `json:"op"`    Path string `json:"path"`}//remove node info, example label, annotationfunc removeNodeInfo(param RemoveNodeInfoParam) (*apiv1.Node, error) {    coreV1 := param.coreV1    nodeSn := param.NodeSn    node, err := coreV1.Nodes().Get(nodeSn, metav1.GetOptions{})    if err != nil {        return nil, err    }    operatorKey := param.OperatorKey    operatorType := param.OperatorType    operatorPath := param.OperatorPath    var payloads []interface{}    for key := range operatorKey {        payload := RemoveStringValue{            Op:   operatorType,            Path: operatorPath + operatorKey[key],        }        payloads = append(payloads, payload)    }    payloadBytes, _ := json.Marshal(payloads)    newNode, err := coreV1.Nodes().Patch(nodeSn, types.JSONPatchType, payloadBytes)    if err != nil {        return nil, err    }    return newNode, err}

Op:replace

replace操作,会对整个的Object进行替换。所以使用replace记住要把原始的数据取出来和你要新增的数据合并后再提交,如:

type ReplaceNodeInfoParam struct {    coreV1       v1.CoreV1Interface    NodeSn       string                 `json:"nodeSn"`    OperatorType string                 `json:"operator_type"`    OperatorPath string                 `json:"operator_path"`    OperatorData map[string]interface{} `json:"operator_data"`    DataType     string                 `json:"data_type"`}//patchStringValue specifies a patch operation for a string.type PatchStringValue struct {    Op    string      `json:"op"`    Path  string      `json:"path"`    Value interface{} `json:"value"`}func replaceNodeInfo(param ReplaceNodeInfoParam) (*apiv1.Node, error) {    coreV1 := param.coreV1    nodeSn := param.NodeSn    node, err := coreV1.Nodes().Get(nodeSn, metav1.GetOptions{})    if err != nil {        return nil, err    }    var originOperatorData map[string]string    dataType := param.DataType    operatorData := param.OperatorData    operatorType := param.OperatorType    operatorPath := param.OperatorPath    switch dataType {    case "labels":        originOperatorData = node.Labels    case "annotations":        originOperatorData = node.Annotations    default:        originOperatorData = nil    }    if originOperatorData == nil {        return nil, fmt.Errorf("[replaceNodeInfo] fail, %v originOperatorData is nil", nodeSn)    }    for key, value := range originOperatorData {        operatorData[key] = value    }    var payloads []interface{}    payload := PatchStringValue{        Op:    operatorType,        Path:  operatorPath,        Value: operatorData,    }    payloads = append(payloads, payload)    payloadBytes, _ := json.Marshal(payloads)    newNode, err := coreV1.Nodes().Patch(nodeSn, types.JSONPatchType, payloadBytes)    if err != nil {        return nil, err    }    return newNode, err}

单元测试

func TestReplaceNodeInfo(t *testing.T) {    Convey("test ReplaceNodeInfo", t, func() {        Convey("Patch ReplaceNodeInfo fail", func() {            var tests = []struct {                nodeSn        string                operatorType  string                operatorPath  string                dataType      string                operatorData  map[string]interface{}                expectedError interface{}                expectedValue *v1.Node                objs          []runtime.Object            }{                {"1.1.1.1", "add", "/metadata/labels", "labels",                    map[string]interface{}{                        "test1": "test1",                        "test2": "test2"},                    "nodes \"1.1.1.1\" not found", nil, nil},                {"1.1.1.1", "aaa", "/metadata/annotations", "annotations",                    map[string]interface{}{                        "test1": "test1",                        "test2": "test2"},                    "[replaceNodeInfo] fail, 1.1.1.1 originOperatorData is nil", nil, []runtime.Object{node("1.1.1.1", nil, nil)}},            }            for _, test := range tests {                client := fake.NewSimpleClientset(test.objs...)                param := ReplaceNodeInfoParam{                    coreV1:       client.CoreV1(),                    NodeSn:       test.nodeSn,                    OperatorType: test.operatorType,                    OperatorPath: test.operatorPath,                    OperatorData: test.operatorData,                    DataType:     test.dataType,                }                output, err := replaceNodeInfo(param)                So(output, ShouldEqual, test.expectedValue)                So(err.Error(), ShouldEqual, test.expectedError)            }        })        Convey("Patch Node success", func() {            var tests = []struct {                nodeSn             string                operatorType       string                operatorPath       string                dataType           string                operatorData       map[string]interface{}                expectedError      interface{}                expectedLabel      string                expectedAnnotation string                objs               []runtime.Object            }{                {"1.1.1.1", "replace", "/metadata/labels", "labels",                    map[string]interface{}{                        "test1": "test1",                        "test2": "test2"},                    nil, "test3", "", []runtime.Object{node("1.1.1.1", map[string]string{"test3": "test3"}, map[string]string{"test3": "test3"})}},                {"1.1.1.1", "replace", "/metadata/annotations", "annotations",                    map[string]interface{}{                        "test1": "test1",                        "test2": "test2"},                    nil, "", "modifytest", []runtime.Object{node("1.1.1.1", map[string]string{"test1": "modifytest"}, map[string]string{"test1": "modifytest"})}},            }            for _, test := range tests {                client := fake.NewSimpleClientset(test.objs...)                param := ReplaceNodeInfoParam{                    coreV1:       client.CoreV1(),                    NodeSn:       test.nodeSn,                    OperatorType: test.operatorType,                    OperatorPath: test.operatorPath,                    OperatorData: test.operatorData,                    DataType:     test.dataType,                }                output, err := replaceNodeInfo(param)                So(output, ShouldNotBeNil)                So(err, ShouldBeNil)                So(output.Labels["test3"], ShouldEqual, test.expectedLabel)                So(output.Annotations["test1"], ShouldEqual, test.expectedAnnotation)            }        })    })}

PS:如各位还有其他更好的方式,欢迎交流补充。