go不定参数在k8s中的应用

本文是一篇面向go+k8s开发的文章,以k8s event对象为例提供一种比较优雅的、获取事件列表函数的封装方法,仅供大家参考。

k8s的event对象是一种常见的对象,在集群出现问题的时候经常会查看相关事件来定位问题。首先看看event对象的定义:

// k8s.io/api/core/v1/types.go

import (
    "k8s.io/apimachinery/pkg/apis/meta/v1"
    "k8s.io/apimachinery/pkg/types"
)

// ObjectReference contains enough information to let you inspect or modify the referred object.
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
type ObjectReference struct {
    // Kind of the referent.
    // More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds
    // +optional
    Kind string `json:"kind,omitempty" protobuf:"bytes,1,opt,name=kind"`
    // Namespace of the referent.
    // More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces/
    // +optional
    Namespace string `json:"namespace,omitempty" protobuf:"bytes,2,opt,name=namespace"`
    // Name of the referent.
    // More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names
    // +optional
    Name string `json:"name,omitempty" protobuf:"bytes,3,opt,name=name"`
    // UID of the referent.
    // More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#uids
    // +optional
    UID types.UID `json:"uid,omitempty" protobuf:"bytes,4,opt,name=uid,casttype=k8s.io/apimachinery/pkg/types.UID"`
    // API version of the referent.
    // +optional
    APIVersion string `json:"apiVersion,omitempty" protobuf:"bytes,5,opt,name=apiVersion"`
    // Specific resourceVersion to which this reference is made, if any.
    // More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#concurrency-control-and-consistency
    // +optional
    ResourceVersion string `json:"resourceVersion,omitempty" protobuf:"bytes,6,opt,name=resourceVersion"`

    // If referring to a piece of an object instead of an entire object, this string
    // should contain a valid JSON/Go field access statement, such as desiredState.manifest.containers[2].
    // For example, if the object reference is to a container within a pod, this would take on a value like:
    // "spec.containers{name}" (where "name" refers to the name of the container that triggered
    // the event) or if no container name is specified "spec.containers[2]" (container with
    // index 2 in this pod). This syntax is chosen only to have some well-defined way of
    // referencing a part of an object.
    // TODO: this design is not final and this field is subject to change in the future.
    // +optional
    FieldPath string `json:"fieldPath,omitempty" protobuf:"bytes,7,opt,name=fieldPath"`
}


// Event is a report of an event somewhere in the cluster.
type Event struct {
    metav1.TypeMeta `json:",inline"`
    // Standard object's metadata.
    // More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata
    metav1.ObjectMeta `json:"metadata" protobuf:"bytes,1,opt,name=metadata"`

    // The object that this event is about.
    InvolvedObject ObjectReference `json:"involvedObject" protobuf:"bytes,2,opt,name=involvedObject"`

    // This should be a short, machine understandable string that gives the reason
    // for the transition into the object's current status.
    // TODO: provide exact specification for format.
    // +optional
    Reason string `json:"reason,omitempty" protobuf:"bytes,3,opt,name=reason"`

    // A human-readable description of the status of this operation.
    // TODO: decide on maximum length.
    // +optional
    Message string `json:"message,omitempty" protobuf:"bytes,4,opt,name=message"`

    // The component reporting this event. Should be a short machine understandable string.
    // +optional
    Source EventSource `json:"source,omitempty" protobuf:"bytes,5,opt,name=source"`

    // The time at which the event was first recorded. (Time of server receipt is in TypeMeta.)
    // +optional
    FirstTimestamp metav1.Time `json:"firstTimestamp,omitempty" protobuf:"bytes,6,opt,name=firstTimestamp"`

    // The time at which the most recent occurrence of this event was recorded.
    // +optional
    LastTimestamp metav1.Time `json:"lastTimestamp,omitempty" protobuf:"bytes,7,opt,name=lastTimestamp"`

    // The number of times this event has occurred.
    // +optional
    Count int32 `json:"count,omitempty" protobuf:"varint,8,opt,name=count"`

    // Type of this event (Normal, Warning), new types could be added in the future
    // +optional
    Type string `json:"type,omitempty" protobuf:"bytes,9,opt,name=type"`

    // Time when this Event was first observed.
    // +optional
    EventTime metav1.MicroTime `json:"eventTime,omitempty" protobuf:"bytes,10,opt,name=eventTime"`

    // Data about the Event series this event represents or nil if it's a singleton Event.
    // +optional
    Series *EventSeries `json:"series,omitempty" protobuf:"bytes,11,opt,name=series"`

    // What action was taken/failed regarding to the Regarding object.
    // +optional
    Action string `json:"action,omitempty" protobuf:"bytes,12,opt,name=action"`

    // Optional secondary object for more complex actions.
    // +optional
    Related *ObjectReference `json:"related,omitempty" protobuf:"bytes,13,opt,name=related"`

    // Name of the controller that emitted this Event, e.g. `kubernetes.io/kubelet`.
    // +optional
    ReportingController string `json:"reportingComponent" protobuf:"bytes,14,opt,name=reportingComponent"`

    // ID of the controller instance, e.g. `kubelet-xyzf`.
    // +optional
    ReportingInstance string `json:"reportingInstance" protobuf:"bytes,15,opt,name=reportingInstance"`
}

基于client-go获取k8s event列表数据时,可能会有如下代码:

package main

import (
    corev1 "k8s.io/api/core/v1"
    "k8s.io/apimachinery/pkg/labels"
    listerCorev1 "k8s.io/client-go/listers/core/v1"
)

var eventLister listerCorev1.EventLister

func ListEvents(namespace string) ([]*corev1.Event, error) {
    return eventLister.Events(namespace).List(labels.Everything())
}

几天后,我们发现这个函数有了新的需求:其它模块需根据对象过滤(例如筛选pod对象的事件)、产品页面需要做事件类型(event.Type)精确匹配,以及根据事件来源类型(event.InvolvedObject.Kind)模糊匹配,而且后续还有可能增加其它筛选条件。

我们的目标是把该函数放到公共库里,这也就意味着要尽可能减少该函数的变动,包括函数名、入参、返回值,否则引用该函数的代码都得一起改动。

level1 定参

我们先直接把过滤条件作为定参进行函数封装:

import (
    "strings"

    corev1 "k8s.io/api/core/v1"
    "k8s.io/apimachinery/pkg/labels"
    listerCorev1 "k8s.io/client-go/listers/core/v1"
)

var eventLister listerCorev1.EventLister

func ListEvents(namespace string, ref *corev1.ObjectReference, tp, kind string) ([]*corev1.Event, error) {
    events, err := eventLister.Events(namespace).List(labels.Everything())
    if err != nil {
        return nil, err
    }

    ret := make([]*corev1.Event, 0, len(events))
    for _, event := range events {
        if ref != nil {
            // 过滤ref
            if ref.Namespace != "" && ref.Namespace != event.InvolvedObject.Namespace {
                continue
            }
            if ref.Name != "" && ref.Name != event.InvolvedObject.Name {
                continue
            }
            if ref.Kind != "" && ref.Kind != event.InvolvedObject.Kind {
                continue
            }
            if ref.UID != "" && ref.UID != event.InvolvedObject.UID {
                continue
            }
        }
        
        // 过滤type
        if tp != "" && tp != event.Type {
            continue
        }

        // 过滤kind
        if kind != "" && !strings.Contains(event.InvolvedObject.Kind, kind) {
            continue
        }

        ret = append(ret, event)
    }

    return ret, nil
}

但是这样写有两个明显的问题:

  1. 定参封装函数需要所有场景都传这三个参数,这并不是调用者所期望的,调用者只希望需要哪个参数传哪个参数即可;
  2. 后续如果想新增其它过滤条件,需要修改函数,会导致所有引用方都得同步修改。

level2 结构体传参

为了解决函数参数个数变动问题,我们把过滤字段封装在结构体中,这样即使后续在结构体中新增了过滤字段,对存量调用不会有影响:

import (
    "strings"

    corev1 "k8s.io/api/core/v1"
    "k8s.io/apimachinery/pkg/labels"
    listerCorev1 "k8s.io/client-go/listers/core/v1"
)

var eventLister listerCorev1.EventLister

type OptListEvents struct {
    Ref *corev1.ObjectReference
    Type string
    Kind string
}

func ListEvents(namespace string, opt OptListEvents) ([]*corev1.Event, error) {
    events, err := eventLister.Events(namespace).List(labels.Everything())
    if err != nil {
        return nil, err
    }

    ret := make([]*corev1.Event, 0, len(events))
    for _, event := range events {
        // 过滤ref
        if opt.Ref != nil {
            if opt.Ref.Namespace != "" && opt.Ref.Namespace != event.InvolvedObject.Namespace {
                continue
            }
            if opt.Ref.Name != "" && opt.Ref.Name != event.InvolvedObject.Name {
                continue
            }
            if opt.Ref.Kind != "" && opt.Ref.Kind != event.InvolvedObject.Kind {
                continue
            }
            if opt.Ref.UID != "" && opt.Ref.UID != event.InvolvedObject.UID {
                continue
            }
        }
        

        // 过滤type
        if opt.Type != "" && opt.Type != event.Type {
            continue
        }

        // 过滤kind
        if opt.Kind != "" && !strings.Contains(event.InvolvedObject.Kind, opt.Kind) {
            continue
        }

        ret = append(ret, event)
    }

    return ret, nil
}

结构体传参的方式虽然减少了函数参数变动对调用方的影响,但现在新加过滤条件时需要修改该结构体,如果该函数作为公共库函数,显然还是有缺陷的。

level3 不定参数

为了解决上述问题,我们可以基于go语言的不定参数语法来封装该函数,函数形式如下:

import (
    corev1 "k8s.io/api/core/v1"
)

func ListEventsWithOpts(namespace string, opts ...OptListEvents) ([]*corev1.Event, error) {
    // todo
}

不定参数的封装,关键在OptListEvents的定义。作为过滤条件,很明显就是对event的字段做某种规则匹配,匹配结果只需要true或false即可,因此可以把OptListEvents这么定义:

import (
    corev1 "k8s.io/api/core/v1"
)

type OptListEvents func(event *corev1.Event) bool

再把前面提到的三种过滤条件补上:

import (
    "strings"
    
    corev1 "k8s.io/api/core/v1"
)

type OptListEvents func(event *corev1.Event) bool

func OptListEventsWithRef(ref *corev1.ObjectReference) OptListEvents {
    return func(event *corev1.Event) bool {
        if ref != nil {
            if ref.Namespace != "" && ref.Namespace != event.InvolvedObject.Namespace {
                return false
            }
            if ref.Name != "" && ref.Name != event.InvolvedObject.Name {
                return false
            }
            if ref.Kind != "" && ref.Kind != event.InvolvedObject.Kind {
                return false
            }
            if ref.UID != "" && ref.UID != event.InvolvedObject.UID {
                return false
            }
        }
        return true
    }
}

func OptListEventsWithType(tp string) OptListEvents {
    return func(event *corev1.Event) bool {
        if tp != "" {
            return tp == event.Type
        }
        return true
    }
}

func OptListEventsWithKind(kind string) OptListEvents {
    return func(event *corev1.Event) bool {
        if kind != "" {
            return strings.Contains(event.InvolvedObject.Kind, kind)
        }
        return true
    }
}

于是整体函数封装如下:

import (
    "strings"

    corev1 "k8s.io/api/core/v1"
    "k8s.io/apimachinery/pkg/labels"
    listerCorev1 "k8s.io/client-go/listers/core/v1"
)

var eventLister listerCorev1.EventLister

type OptListEvents func(event *corev1.Event) bool

func OptListEventsWithRef(ref *corev1.ObjectReference) OptListEvents {
    return func(event *corev1.Event) bool {
        if ref != nil {
            if ref.Namespace != "" && ref.Namespace != event.InvolvedObject.Namespace {
                return false
            }
            if ref.Name != "" && ref.Name != event.InvolvedObject.Name {
                return false
            }
            if ref.Kind != "" && ref.Kind != event.InvolvedObject.Kind {
                return false
            }
            if ref.UID != "" && ref.UID != event.InvolvedObject.UID {
                return false
            }
        }
        return true
    }
}

func OptListEventsWithType(tp string) OptListEvents {
    return func(event *corev1.Event) bool {
        if tp != "" {
            return tp == event.Type
        }
        return true
    }
}

func OptListEventsWithKind(kind string) OptListEvents {
    return func(event *corev1.Event) bool {
        if kind != "" {
            return strings.Contains(event.InvolvedObject.Kind, kind)
        }
        return true
    }
}

func eventOptsPass(event *corev1.Event, opts ...OptListEvents) bool {
    for _, opt := range opts {
        if !opt(event) {
            return false
        }
    }
    return true
}

func ListEventsWithOpts(namespace string, opts ...OptListEvents) ([]*corev1.Event, error) {
    events, err := eventLister.Events(namespace).List(labels.Everything())
    if err != nil {
        return nil, err
    }

    ret := make([]*corev1.Event, 0, len(events))
    for _, event := range events {
        if eventOptsPass(event, opts...) {
            ret = append(ret, event)
        }
    }

    return ret, nil
}

调用方获取deployment事件:

import (
    corev1 "k8s.io/api/core/v1"
    "k8s.io/client-go/kubernetes/scheme"
    appsv1 "k8s.io/client-go/listers/apps/v1"
    "k8s.io/client-go/tools/reference"
)

var deployLister appsv1.DeploymentLister

func ListDeploymentEvents(namespace, name string) ([]*corev1.Event, error) {
    deploy, err := deployLister.Deployments(namespace).Get(name)
    if err != nil {
        return nil, err
    }
    
    ref, err := reference.GetReference(scheme.Scheme, deploy)
    if err != nil {
        return nil, err
    }
    
    return ListEventsWithOpts(namespace, OptListEventsWithRef(ref))
}

调用方根据type和kind来过滤event:

import (
    corev1 "k8s.io/api/core/v1"
)

func ListClusterEvents() ([]*corev1.Event, error) {
    var (
        tp = "Warning"
        kind = "Pod"
    )
    
    opts := []OptListEvents{
        OptListEventsWithType(tp),
        OptListEventsWithKind(kind),
    }
    return ListEventsWithOpts(corev1.NamespaceAll, opts...)
}

假设该函数已经放到公共库后又来了个新需求:需要根据事件内容(event.Message)做模糊筛选,此时调用方处理起来也很简单,只需要自己再定义一个筛选函数即可,对公共库函数没有任何影响:

import (
    "strings"

    corev1 "k8s.io/api/core/v1"
)

func OptListEventsWithMessage(message string) OptListEvents {
    return func(event *corev1.Event) bool {
        if message != "" {
            return strings.Contains(event.Message, message)
        }
        return true
    }
}

func ListClusterEvents() ([]*corev1.Event, error) {
    var (
        tp   = "Warning"
        kind = "Pod"
    )

    opts := []OptListEvents{
        OptListEventsWithType(tp),
        OptListEventsWithKind(kind),
        OptListEventsWithMessage("test"),
    }
    return ListEventsWithOpts(corev1.NamespaceAll, opts...)
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值