本文是一篇面向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
}
但是这样写有两个明显的问题:
- 定参封装函数需要所有场景都传这三个参数,这并不是调用者所期望的,调用者只希望需要哪个参数传哪个参数即可;
- 后续如果想新增其它过滤条件,需要修改函数,会导致所有引用方都得同步修改。
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...)
}