K8S client-go Patch example

简介: 我在本文中主要会介绍使用client-go的Patch方式,主要包括strategic merge patch和json-patch

使用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 更新 pod
func 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, annotation
func 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, annotation
func 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:如各位还有其他更好的方式,欢迎交流补充。

相关实践学习
通过Ingress进行灰度发布
本场景您将运行一个简单的应用,部署一个新的应用用于新的发布,并通过Ingress能力实现灰度发布。
容器应用与集群管理
欢迎来到《容器应用与集群管理》课程,本课程是“云原生容器Clouder认证“系列中的第二阶段。课程将向您介绍与容器集群相关的概念和技术,这些概念和技术可以帮助您了解阿里云容器服务ACK/ACK Serverless的使用。同时,本课程也会向您介绍可以采取的工具、方法和可操作步骤,以帮助您了解如何基于容器服务ACK Serverless构建和管理企业级应用。 学习完本课程后,您将能够: 掌握容器集群、容器编排的基本概念 掌握Kubernetes的基础概念及核心思想 掌握阿里云容器服务ACK/ACK Serverless概念及使用方法 基于容器服务ACK Serverless搭建和管理企业级网站应用
相关文章
|
Kubernetes Cloud Native jenkins
下篇:使用jenkins发布go项目到k8s,接上篇的手工体验改造为自动化发布
下篇:使用jenkins发布go项目到k8s,接上篇的手工体验改造为自动化发布
620 1
|
3月前
|
Kubernetes Go Docker
在K8s编程中如何使用Go
一文带你了解在K8s编程中如何使用Go
103 3
|
4月前
|
Kubernetes 监控 Cloud Native
"解锁K8s新姿势!Cobra+Client-go强强联手,打造你的专属K8s监控神器,让资源优化与性能监控尽在掌握!"
【8月更文挑战第14天】在云原生领域,Kubernetes以出色的扩展性和定制化能力引领潮流。面对独特需求,自定义插件成为必要。本文通过Cobra与Client-go两大利器,打造一款监测特定标签Pods资源使用的K8s插件。Cobra简化CLI开发,Client-go则负责与K8s API交互。从初始化项目到实现查询逻辑,一步步引导你构建个性化工具,开启K8s集群智能化管理之旅。
66 2
|
4月前
|
运维 Kubernetes Go
"解锁K8s二开新姿势!client-go:你不可不知的Go语言神器,让Kubernetes集群管理如虎添翼,秒变运维大神!"
【8月更文挑战第14天】随着云原生技术的发展,Kubernetes (K8s) 成为容器编排的首选。client-go作为K8s的官方Go语言客户端库,通过封装RESTful API,使开发者能便捷地管理集群资源,如Pods和服务。本文介绍client-go基本概念、使用方法及自定义操作。涵盖ClientSet、DynamicClient等客户端实现,以及lister、informer等组件,通过示例展示如何列出集群中的所有Pods。client-go的强大功能助力高效开发和运维。
451 1
|
7月前
|
Kubernetes Cloud Native Go
Golang深入浅出之-Go语言中的云原生开发:Kubernetes与Docker
【5月更文挑战第5天】本文探讨了Go语言在云原生开发中的应用,特别是在Kubernetes和Docker中的使用。Docker利用Go语言的性能和跨平台能力编写Dockerfile和构建镜像。Kubernetes,主要由Go语言编写,提供了方便的客户端库与集群交互。文章列举了Dockerfile编写、Kubernetes资源定义和服务发现的常见问题及解决方案,并给出了Go语言构建Docker镜像和与Kubernetes交互的代码示例。通过掌握这些技巧,开发者能更高效地进行云原生应用开发。
211 1
|
Kubernetes 监控 Cloud Native
Go微服务架构实战 中篇:2. 基于k8s部署服务和注册中心,验证服务注册和发现
Go微服务架构实战 中篇:2. 基于k8s部署服务和注册中心,验证服务注册和发现
Go微服务架构实战 中篇:2. 基于k8s部署服务和注册中心,验证服务注册和发现
|
JSON Kubernetes 测试技术
在 Go 中使用 Kubernetes 对象
通常,在某些情况下,我们需要通用的方法去使用 Kubernetes 资源对象,而不是编写代码来处理特定类型。
212 0
|
运维 Kubernetes 负载均衡
Go微服务架构实战 中篇:5. k8s基于ingress和service实现金丝雀发布和蓝绿发布
Go微服务架构实战 中篇:5. k8s基于ingress和service实现金丝雀发布和蓝绿发布
|
Kubernetes 监控 负载均衡
Go微服务架构实战-中篇 1. k8s架构介绍
Go微服务架构实战-中篇 1. k8s架构介绍
|
Kubernetes Go 开发工具
开发 k8s 管理平台 - k8sailor 03. 使用 client-go sdk 链接集群
开发 k8s 管理平台 - k8sailor 03. 使用 client-go sdk 链接集群
266 0
开发 k8s 管理平台 - k8sailor 03. 使用 client-go sdk 链接集群