Featured image of post 基于Go语言和Kubernetes的多集群管理平台开发实践

基于Go语言和Kubernetes的多集群管理平台开发实践

client-go是kubernetes官方提供的go语言的客户端库,go应用使用该库可以访问kubernetes的API Server,这样我们就能通过编程来对kubernetes资源进行增删改查操作;除了提供丰富的API用于操作kubernetes资源,client-go还为controller和operator提供了重要支持client-go的informer机制可以将controller关注的资源变化及时带给此controller,使controller能够及时响应变化。

项目背景

在云原生时代,Kubernetes已成为容器编排的事实标准。但随着业务规模的扩大,企业往往需要管理多个Kubernetes集群。本文通过一个实际项目,详细介绍如何基于Go语言和Gin框架开发一个支持多集群管理的Kubernetes控制平台,实现Pod资源的全生命周期管理(增删改查、日志查看),并支持数据过滤、排序和分页功能。

Client-go 介绍

  • client-go是kubernetes官方提供的go语言的客户端库,go应用使用该库可以访问kubernetes的API Server,这样我们就能通过编程来对kubernetes资源进行增删改查操作;
  • 除了提供丰富的API用于操作kubernetes资源,client-go还为controller和operator提供了重要支持client-go的informer机制可以将controller关注的资源变化及时带给此controller,使controller能够及时响应变化。
  • 通过client-go提供的客户端对象与kubernetes的API Server进行交互,而client-go提供了以下四种客户端对象: RESTClient、ClientSet、DynamicClient、DiscoveryClient

代码示例

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
package main

import (
	"context"

	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/client-go/kubernetes"

	"k8s.io/client-go/tools/clientcmd"
)

func main() {

	// 定义kubeconfig文件路径
	kubeconfig := "config/k8s.yaml"
	// 从kubeconfig文件中构建配置
	config, err := clientcmd.BuildConfigFromFlags("", kubeconfig)
	if err != nil {
		// 如果构建配置失败,则抛出错误
		panic(err.Error())
	}
	// 使用配置创建kubernetes客户端
	client, err := kubernetes.NewForConfig(config)
	if err != nil {
		// 如果创建客户端失败,则抛出错误
		panic(err.Error())
	}
	// 使用客户端获取default命名空间下的所有Pod
	pods, err := client.CoreV1().Pods("default").List(context.TODO(), metav1.ListOptions{})
	if err != nil {
		// 如果获取Pod失败,则抛出错误
		panic(err.Error())
	}
	// 遍历所有Pod,并打印Pod名称
	for _, pod := range pods.Items {
		println(pod.Name)
	}
}

打印结果 在这里插入图片描述

常用方法

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
	// 获取podList类型的pod列表
	podList, err := client.CoreV1().Pods(namespace).List(context.TODO(), metav1.ListOptions{})
   // 获取pod的详情
	pod, err := client.CoreV1().Pods(namespace).Get(context.TODO(), podName, metav1.GetOptions{})
	// 删除pod
	err := client.CoreV1().Pods(namespace).Delete(context.TODO(), podName, metav1.DeleteOptions{})
	// 更新pod
	_, err = client.CoreV1().Pods(namespace).Update(context.TODO(), pod, metav1.UpdateOptions{})
	// 获取deployment副本数
	scale, err := K8s.ClientSet.AppsV1().Deployments(namespace).GetScale(context.TODO(),deploymentName, metav1.GetOptions{})
	// 创建deployment
	deployment, err =K8s.ClientSet.AppsV1().Deployments(data.Namespace).Create(context.TODO(), deployment,metav1.CreateOptions{})
	// 更新deployment(部分yaml)
	deployment, err = K8s.ClientSet.AppsV1().Deployments(namespace).Patch(context.TODO(),deploymentName, "application/strategic-merge-patch+json", patchByte,metav1.PatchOptions{})

项目架构与核心模块

项目采用分层设计,核心模块包括:

  1. 路由层(Router):基于Gin框架定义API端点。
  2. 控制层(Controller):处理HTTP请求,参数校验,调用服务层逻辑。
  3. 服务层(Service):封装业务逻辑,与Kubernetes API交互。
  4. 数据层(Kubernetes Client):管理多集群连接,提供客户端实例。
  5. 工具模块:数据筛选、分页、日志处理等。 在这里插入图片描述

技术栈

  • Gin框架:高性能HTTP框架,用于路由和请求处理。
  • Kubernetes Client-go:官方Go语言客户端,操作Kubernetes资源。
  • 多集群管理:通过动态加载Kubeconfig支持多集群。
  • 数据分页与过滤:自定义排序、过滤和分页逻辑。

多集群管理实现

k8s_client.go中,项目通过K8s结构体实现了多集群管理:

1
2
3
4
type k8s struct {
    ClientMap    map[string]*kubernetes.Clientset  // 多集群Client
    KubeConfMap  map[string]string                 // 多集群配置
}

初始化时,从配置文件读取多个集群的kubeconfig,并为每个集群创建独立的ClientSet:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33

// Init 初始化k8s client
func (k *k8s) Init() {
	// 创建一个空的map,用于存储Kubeconfigs
	mp := make(map[string]string, 0)
	// 创建一个空的map,用于存储Kubernetes的Clientset
	k.ClientMap = make(map[string]*kubernetes.Clientset, 0)
	// 反序列化
	if err := json.Unmarshal([]byte(config.Kubeconfigs), &mp); err != nil {
		// 如果反序列化失败,则抛出异常
		panic(fmt.Sprintf("反序列化Kubeconfigs失败,%v\n", err))
	}
	// 将反序列化后的结果存储到KubeConfMap中
	k.KubeConfMap = mp
	// 初始化集群Client
	for key, value := range mp {
		// 根据Kubeconfigs中的配置,初始化集群Client
		client, err := clientcmd.BuildConfigFromFlags("", value)
		if err != nil {
			// 如果初始化失败,则抛出异常
			panic(fmt.Sprintf("初始化集群%s失败,%v\n", key, err))
		}
		clientSet, err := kubernetes.NewForConfig(client)
		if err != nil {
			// 如果初始化失败,则抛出异常
			panic(fmt.Sprintf("初始化集群%s失败,%v\n", key, err))
		}
		// 将初始化后的Clientset存储到ClientMap中
		k.ClientMap[key] = clientSet
		// 打印初始化成功的日志
		logger.Info(fmt.Sprintf("初始化集群%s成功", key))
	}
}

这种设计使得在API调用时,只需指定集群名称即可操作对应的集群资源。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14

// GetClient 根据集群名称获取Client
// 根据集群名称获取kubernetes客户端
func (k *k8s) GetClient(clusterName string) (*kubernetes.Clientset, error) {
	// 从ClientMap中获取指定集群名称的客户端
	client, ok := k.ClientMap[clusterName]
	// 如果不存在,则返回错误
	if !ok {
		logger.Error(fmt.Sprintf("集群%s不存在,无法获取Client\n", clusterName))
		return nil, errors.New(fmt.Sprintf("集群%s不存在,无法获取Client\n", clusterName))
	}
	// 返回客户端
	return client, nil
}

核心功能实现

Pod列表查询

Pod列表查询功能实现了过滤、分页和排序等高级特性,代码位于pod.godataselect.go中。

数据结构定义

首先定义了通用的数据选择器结构:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// dataSelect 用于封装排序、过滤、分页的数据类型
type dataSelector struct {
	GenericDateSelect []DataCell       // 可排序的数据集合
	dataSelectQuery   *DataSelectQuery // 查询条件
}

// DataCell 用于各种资源list的类型转换,转换后可以使用dataSelector的自定义排序方法
type DataCell interface {
	GetCreation() time.Time
	GetName() string
}

// DataSelectQuery 定义过滤和分页的属性,过滤:Name, 分页:Limit和Page
// Limit是单页的数据条数
// Page是第几页
type DataSelectQuery struct {
	FilterQuery     *FilterQuery     // 过滤条件
	PaginationQuery *PaginationQuery // 分页条件
}

type FilterQuery struct {
	Name string
}

type PaginationQuery struct {
	Limit int
	Page  int
}

过滤实现

过滤功能通过字符串匹配实现:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

// Filter 方法用于过滤元素,比较元素的Name属性,若包含,再返回
func (d *dataSelector) Filter() *dataSelector {
	//如Name的传参为空,则返回所有元素
	if d.dataSelectQuery.FilterQuery.Name == "" {
		return d
	}
	// 若Name的传参不为空,则返回元素中包含Name的元素
	var filteredList []DataCell
	for _, item := range d.GenericDateSelect {
		matched := true
		objName := item.GetName()
		if !strings.Contains(objName, d.dataSelectQuery.FilterQuery.Name) {
			matched = false
			continue
		}
		if matched {
			filteredList = append(filteredList, item)
		}
	}
	d.GenericDateSelect = filteredList // 返回过滤后的元素
	return d
}

分页实现 分页逻辑处理了边界情况:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
func (d *dataSelector) Paginate() *dataSelector {
    limit := d.dataSelectQuery.PaginationQuery.Limit
    page := d.dataSelectQuery.PaginationQuery.Page
    
    if limit < 1 || page < 1 {
        return d
    }
    
    startIndex := (page - 1) * limit
    endIndex := page * limit

    if len(d.GenericDateSelect) < endIndex {
        endIndex = len(d.GenericDateSelect)
    }
    d.GenericDateSelect = d.GenericDateSelect[startIndex:endIndex]
    return d
}

排序实现 通过实现sort.Interface接口实现排序:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
func (d *dataSelector) Len() int {
    return len(d.GenericDateSelect)
}

func (d *dataSelector) Swap(i, j int) {
    d.GenericDateSelect[i], d.GenericDateSelect[j] = d.GenericDateSelect[j], d.GenericDateSelect[i]
}

func (d *dataSelector) Less(i, j int) bool {
    a := d.GenericDateSelect[i].GetCreation()
    b := d.GenericDateSelect[j].GetCreation()
    return b.Before(a) // 降序排列
}

dataselect.go

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
package service

import (
	"sort"
	"strings"
	"time"

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

/**
 * @Author: 南宫乘风
 * @Description:定义数据结构
 * @File:  dataselect.go
 * @Email: 1794748404@qq.com
 * @Date: 2025-03-20 14:38
 */

// dataSelect 用于封装排序、过滤、分页的数据类型
type dataSelector struct {
	GenericDateSelect []DataCell       // 可排序的数据集合
	dataSelectQuery   *DataSelectQuery // 查询条件
}

// DataCell 用于各种资源list的类型转换,转换后可以使用dataSelector的自定义排序方法
type DataCell interface {
	GetCreation() time.Time
	GetName() string
}

// DataSelectQuery 定义过滤和分页的属性,过滤:Name, 分页:Limit和Page
// Limit是单页的数据条数
// Page是第几页
type DataSelectQuery struct {
	FilterQuery     *FilterQuery     // 过滤条件
	PaginationQuery *PaginationQuery // 分页条件
}

type FilterQuery struct {
	Name string
}

type PaginationQuery struct {
	Limit int
	Page  int
}

//实现自定义结构的排序,需要重写Len、Swap、Less方法

// Len 方法用于获取数据长度
func (d *dataSelector) Len() int {
	return len(d.GenericDateSelect)
}

// Swap 方法用于数组中的元素在比较大小后的位置交换,可定义升序或降序   i j 是切片的下标
func (d *dataSelector) Swap(i, j int) {
	// 交换GenericDateSelect数组中的第i个和第j个元素
	d.GenericDateSelect[i], d.GenericDateSelect[j] = d.GenericDateSelect[j], d.GenericDateSelect[i]
}

// Less 方法用于定义数组中元素排序的“大小”的比较方式
// Less 方法返回true表示第i个元素小于第j个元素,返回false表示第i个元素大于第j个元素
func (d *dataSelector) Less(i, j int) bool {
	a := d.GenericDateSelect[i].GetCreation()
	b := d.GenericDateSelect[j].GetCreation()
	return b.Before(a)
}

// Sort 重新以上3个方法,使用sort.Sort()方法进行排序
func (d *dataSelector) Sort() *dataSelector {
	// 使用sort.Sort()方法进行排序
	sort.Sort(d)
	return d
}

// 过滤

// Filter 方法用于过滤元素,比较元素的Name属性,若包含,再返回
func (d *dataSelector) Filter() *dataSelector {
	//如Name的传参为空,则返回所有元素
	if d.dataSelectQuery.FilterQuery.Name == "" {
		return d
	}
	// 若Name的传参不为空,则返回元素中包含Name的元素
	var filteredList []DataCell
	for _, item := range d.GenericDateSelect {
		matched := true
		objName := item.GetName()
		if !strings.Contains(objName, d.dataSelectQuery.FilterQuery.Name) {
			matched = false
			continue
		}
		if matched {
			filteredList = append(filteredList, item)
		}
	}
	d.GenericDateSelect = filteredList // 返回过滤后的元素
	return d
}

// 分页

// Paginate 方法用于数组分页,根据Limit和Page的传参,返回数据
func (d *dataSelector) Paginate() *dataSelector {
	limit := d.dataSelectQuery.PaginationQuery.Limit
	page := d.dataSelectQuery.PaginationQuery.Page
	// 验证参数合法,若不合法,则返回所有元素
	if limit < 1 || page < 1 {
		return d
	}
	// 举例:25个元素的数组,limit是10,page是3,startIndex是20,endIndex是30(实际上endIndex是25)、
	startIndex := (page - 1) * limit
	endIndex := page * limit

	// 处理最后一页,这时候就把endIndex由30改为25了
	if len(d.GenericDateSelect) < endIndex {
		endIndex = len(d.GenericDateSelect)
	}
	d.GenericDateSelect = d.GenericDateSelect[startIndex:endIndex]
	return d
}

// 定义podCell 类型,实现两个方法GetCreation和GetName,可进行类型转换
type podCell corev1.Pod

func (p podCell) GetCreation() time.Time {
	return p.CreationTimestamp.Time
}

func (p podCell) GetName() string {
	return p.Name
}

Pod详情查询

通过Kubernetes客户端直接获取Pod详情:

1
2
3
4
5
6
7
8
func (p *pod) GetPodDetail(client *kubernetes.Clientset, namespace, podName string) (*corev1.Pod, error) {
    pod, err := client.CoreV1().Pods(namespace).Get(context.TODO(), podName, metav1.GetOptions{})
    if err != nil {
        logger.Error(errors.New("获取Pod详情失败, " + err.Error()))
        return nil, errors.New("获取Pod详情失败, " + err.Error())
    }
    return pod, nil
}

Pod日志查询

日志查询功能支持指定容器和返回行数限制:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
func (p *pod) GetPodLog(client *kubernetes.Clientset, namespace, podName, containerName string) (log string, err error) {
    lineLimit := int64(config.PodLogTailLine)
    options := &corev1.PodLogOptions{
        Container: containerName,
        TailLines: &lineLimit,
    }
    
    req := client.CoreV1().Pods(namespace).GetLogs(podName, options)
    podLogs, err := req.Stream(context.TODO())
    if err != nil {
        logger.Error(errors.New("获取Pod日志失败, " + err.Error()))
        return "", errors.New("获取Pod日志失败, " + err.Error())
    }
    defer podLogs.Close()
    
    buf := new(bytes.Buffer)
    _, err = io.Copy(buf, podLogs)
    if err != nil {
        logger.Error(errors.New("复制PodLog失败, " + err.Error()))
        return "", errors.New("复制PodLog失败, " + err.Error())
    }
    return buf.String(), nil
}

pod.go

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
package service

import (
	"bytes"
	"context"
	"encoding/json"
	"errors"
	"io"
	"kubea-go/config"

	"github.com/aryming/logger"

	corev1 "k8s.io/api/core/v1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/client-go/kubernetes"
)

/**
 * @Author: 南宫乘风
 * @Description:
 * @File:  pod.go
 * @Email: 1794748404@qq.com
 * @Date: 2025-03-20 15:42
 */

var Pod pod

type pod struct {
}

// PodsResp 定义列表的返回内容,Items是pod元素列表,Total为pod元素数量
type PodsResp struct {
	Items []corev1.Pod `json:"items"`
	Total int          `json:"total"`
}

// GetPods 获取pod列表,支持过滤和分页,排序
func (p *pod) GetPods(client *kubernetes.Clientset, filterName, namespace string, limit, page int) (*PodsResp, error) {
	// 获取podList类型的pod列表
	podList, err := client.CoreV1().Pods(namespace).List(context.TODO(), metav1.ListOptions{})
	if err != nil {
		logger.Error(errors.New("获取Pod列表失败, " + err.Error()))
		return nil, errors.New("获取Pod列表失败, " + err.Error())
	}
	// 实例化dataSelector对象
	selectableData := &dataSelector{
		GenericDateSelect: p.toCells(podList.Items),
		dataSelectQuery: &DataSelectQuery{
			FilterQuery: &FilterQuery{Name: filterName},
			PaginationQuery: &PaginationQuery{
				Limit: limit,
				Page:  page,
			},
		},
	}
	//先过滤
	filtered := selectableData.Filter()
	total := len(filtered.GenericDateSelect)
	data := filtered.Sort().Paginate()
	//将[]DataCell类型的pod列表转为v1.pod列表
	pods := p.fromCells(data.GenericDateSelect)
	return &PodsResp{Items: pods, Total: total}, nil
}

// GetPodDetail 获取pod详情
func (p *pod) GetPodDetail(client *kubernetes.Clientset, namespace, podName string) (*corev1.Pod, error) {
	pod, err := client.CoreV1().Pods(namespace).Get(context.TODO(), podName, metav1.GetOptions{})
	if err != nil {
		logger.Error(errors.New("获取Pod详情失败, " + err.Error()))
		return nil, errors.New("获取Pod详情失败, " + err.Error())
	}
	return pod, nil
}

// DeletePod 删除POD
func (p *pod) DeletePod(client *kubernetes.Clientset, namespace, podName string) error {
	// 删除pod
	err := client.CoreV1().Pods(namespace).Delete(context.TODO(), podName, metav1.DeleteOptions{})
	if err != nil {
		logger.Error(errors.New("删除Pod失败, " + err.Error()))
		return errors.New("删除Pod失败, " + err.Error())
	}
	return nil
}

// UpdatePod 更新POD   content参数是请求中传入的pod对象的json数据
func (p *pod) UpdatePod(client *kubernetes.Clientset, namespace, podName, content string) error {
	var pod = &corev1.Pod{}
	// 将content参数的json数据解析到pod对象中
	err := json.Unmarshal([]byte(content), pod)
	if err != nil {
		logger.Error(errors.New("反序列化失败, " + err.Error()))
		return errors.New("反序列化失败, " + err.Error())
	}
	// 更新pod
	_, err = client.CoreV1().Pods(namespace).Update(context.TODO(), pod, metav1.UpdateOptions{})
	if err != nil {
		logger.Error(errors.New("更新Pod失败, " + err.Error()))
		return errors.New("更新Pod失败, " + err.Error())
	}
	return nil
}

// GetPodContainer 获取pod容器
func (p *pod) GetPodContainer(client *kubernetes.Clientset, namespace, podName string) (containers []string, err error) {
	// 获取pod详情
	pod, err := p.GetPodDetail(client, namespace, podName)
	if err != nil {
		logger.Error(errors.New("获取Pod详情失败, " + err.Error()))
		return nil, errors.New("获取Pod详情失败, " + err.Error())
	}
	// 从pod详情中获取容器列表
	for _, container := range pod.Spec.Containers {
		containers = append(containers, container.Name)
	}
	return containers, nil
}

// GetPodLog 获取pod内容器日志
func (p *pod) GetPodLog(client *kubernetes.Clientset, namespace, podName, containerName string) (log string, err error) {
	//设置日志的配置,容器名、tail的行数
	lineLimit := int64(config.PodLogTailLine)
	options := &corev1.PodLogOptions{
		Container: containerName,
		TailLines: &lineLimit,
	}
	// 获取request实例
	req := client.CoreV1().Pods(namespace).GetLogs(podName, options)
	// 发起request请求,返回一个io.ReadCloser类型(等同于response.body)
	podLogs, err := req.Stream(context.TODO())
	if err != nil {
		logger.Error(errors.New("获取Pod日志失败, " + err.Error()))
		return "", errors.New("获取Pod日志失败, " + err.Error())
	}
	defer podLogs.Close()
	//将response body写入到缓冲区,目的是为了转成string返回
	buf := new(bytes.Buffer)
	_, err = io.Copy(buf, podLogs)
	if err != nil {
		logger.Error(errors.New("复制PodLog失败, " + err.Error()))
		return "", errors.New("复制PodLog失败, " + err.Error())
	}
	return buf.String(), nil
}

// toCells 方法用于将pod类型数组,转换成DataCell类型数组
func (p *pod) toCells(std []corev1.Pod) []DataCell {
	cells := make([]DataCell, len(std))
	for i := range std {
		cells[i] = podCell(std[i])
	}
	return cells
}

// fromCells 方法用于将DataCell类型数组,转换成pod类型数组
func (p *pod) fromCells(cells []DataCell) []corev1.Pod {
	pods := make([]corev1.Pod, len(cells))
	for i := range cells {
		//cells[i].(podCell)就使用到了断言,断言后转换成了podCell类型,然后又转换成了Pod类型
		pods[i] = corev1.Pod(cells[i].(podCell))
	}
	return pods
}

Controller层面

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
package controller

import (
	"kubea-go/service"
	"net/http"

	"github.com/aryming/logger"
	"github.com/gin-gonic/gin"
)

/**
 * @Author: 南宫乘风
 * @Description:
 * @File:  pod.go.go
 * @Email: 1794748404@qq.com
 * @Date: 2025-03-25 15:37
 */

var Pod pod

type pod struct{}

//Controller中的方法入参是gin.Context,用于从上下文中获取请求参数及定义响应内容
//流程:绑定参数",调用service代码",根据调用结果响应具体内容

// GetPods 获取pod列表,支持过滤、排序、分页
func (p *pod) GetPods(c *gin.Context) {
	//匿名结构体,用于声明入参,get请求为form格式,其他请求为json格式
	params := new(
		struct {
			FilterName string `form:"filter_name"`
			Namespace  string `form:"namespace"`
			Page       int    `form:"page"`
			Limit      int    `form:"limit"`
			Cluster    string `form:"cluster"`
		})
	//绑定参数,给匿名结构体中的属性赋值,值是入参
	//	form格式使用ctx.Bind方法,json格式使用ctx.ShouldBindJSON方法
	if err := c.ShouldBind(params); err != nil {
		logger.Error("Bind请求参数失败," + err.Error())
		// ctx.JSON方法用于返回响应内容,入参是状态码和响应内容,响应内容放入gin.H的map中
		c.JSON(http.StatusInternalServerError, gin.H{
			"msg":  err.Error(),
			"data": nil,
		})
		return
	}
	// 获取k8s的连接方式
	client, err := service.K8s.GetClient(params.Cluster)
	if err != nil {
		logger.Error("获取k8s连接失败," + err.Error())
		c.JSON(http.StatusInternalServerError, gin.H{
			"msg":  err.Error(),
			"data": nil,
		})
		return
	}
	//service中的的方法通过 包名.结构体变量名.方法名 使用,serivce.Pod.GetPods()
	pods, err := service.Pod.GetPods(client, params.FilterName, params.Namespace, params.Limit, params.Page)
	if err != nil {
		logger.Error("获取pod列表失败," + err.Error())
		c.JSON(http.StatusInternalServerError, gin.H{
			"msg":  err.Error(),
			"data": nil,
		})
		return
	}
	c.JSON(http.StatusOK, gin.H{
		"msg":  "获取pod列表成功",
		"data": pods,
	})
}

// GetPodDetail 获取pod详情
func (p *pod) GetPodDetail(cxt *gin.Context) {
	params := new(struct {
		Namespace string `form:"namespace"`
		PodName   string `form:"pod_name"`
		Cluster   string `form:"cluster"`
	})
	if err := cxt.ShouldBind(params); err != nil {
		logger.Error("Bind请求参数失败," + err.Error())
		cxt.JSON(http.StatusInternalServerError, gin.H{
			"msg":  err.Error(),
			"data": nil,
		})
		return
	}
	client, err := service.K8s.GetClient(params.Cluster)
	if err != nil {
		logger.Error("获取k8s连接失败," + err.Error())
		cxt.JSON(http.StatusInternalServerError, gin.H{
			"msg":  err.Error(),
			"data": nil,
		})
		return
	}
	data, err := service.Pod.GetPodDetail(client, params.Namespace, params.PodName)
	if err != nil {
		logger.Error("获取pod详情失败," + err.Error())
		cxt.JSON(http.StatusInternalServerError, gin.H{
			"msg":  err.Error(),
			"data": nil,
		})
		return
	}
	cxt.JSON(http.StatusOK, gin.H{
		"msg":  "获取pod详情成功",
		"data": data,
	})
}

// DeletePod 删除pod
func (p *pod) DeletePod(cxt *gin.Context) {
	params := new(struct {
		Namespace string `form:"namespace"`
		PodName   string `form:"pod_name"`
		Cluster   string `form:"cluster"`
	})
	if err := cxt.ShouldBind(params); err != nil {
		logger.Error("Bind请求参数失败," + err.Error())
		cxt.JSON(http.StatusInternalServerError, gin.H{
			"msg":  err.Error(),
			"data": nil,
		})
		return
	}
	client, err := service.K8s.GetClient(params.Cluster)
	if err != nil {
		logger.Error("获取k8s连接失败," + err.Error())
		cxt.JSON(http.StatusInternalServerError, gin.H{
			"msg":  err.Error(),
			"data": nil,
		})
		return
	}
	if err := service.Pod.DeletePod(client, params.Namespace, params.PodName); err != nil {
		logger.Error("删除pod失败," + err.Error())
		cxt.JSON(http.StatusInternalServerError, gin.H{
			"msg":  err.Error(),
			"data": nil,
		})
		return
	}
	cxt.JSON(http.StatusOK, gin.H{
		"msg":  "删除pod成功",
		"data": nil,
	})
}

// UpdatePod 更新pod
func (p *pod) UpdatePod(cxt *gin.Context) {
	params := new(struct {
		Namespace string `form:"namespace"`
		PodName   string `form:"pod_name"`
		Content   string `form:"content"`
		Cluster   string `form:"cluster"`
	})
	//PUT请求,绑定参数方法改为ctx.ShouldBindJSON
	if err := cxt.ShouldBindJSON(params); err != nil {
		logger.Error("Bind请求参数失败," + err.Error())
		cxt.JSON(http.StatusInternalServerError, gin.H{
			"msg":  err.Error(),
			"data": nil,
		})
		return
	}
	client, err := service.K8s.GetClient(params.Cluster)
	if err != nil {
		logger.Error("获取k8s连接失败," + err.Error())
		cxt.JSON(http.StatusInternalServerError, gin.H{
			"msg":  err.Error(),
			"data": nil,
		})
		return
	}
	if err := service.Pod.UpdatePod(client, params.Namespace, params.PodName, params.Content); err != nil {
		logger.Error("更新pod失败," + err.Error())
		cxt.JSON(http.StatusInternalServerError, gin.H{
			"msg":  err.Error(),
			"data": nil,
		})
		return
	}
	cxt.JSON(http.StatusOK, gin.H{
		"msg":  "更新pod成功",
		"data": nil,
	})
}

// GetPodContainer 获取pod容器
func (p *pod) GetPodContainer(cxt *gin.Context) {
	params := new(struct {
		Namespace string `form:"namespace"`
		PodName   string `form:"pod_name"`
		Cluster   string `form:"cluster"`
	})
	// GET请求,绑定参数方法改为ctx.Bind
	if err := cxt.Bind(params); err != nil {
		logger.Error("Bind请求参数失败," + err.Error())
		cxt.JSON(http.StatusInternalServerError, gin.H{
			"msg":  err.Error(),
			"data": nil,
		})
		return
	}
	client, err := service.K8s.GetClient(params.Cluster)
	if err != nil {
		logger.Error("获取k8s连接失败," + err.Error())
		cxt.JSON(http.StatusInternalServerError, gin.H{
			"msg":  err.Error(),
			"data": nil,
		})
		return
	}
	containers, err := service.Pod.GetPodContainer(client, params.Namespace, params.PodName)
	if err != nil {
		logger.Error("获取pod容器失败," + err.Error())
		cxt.JSON(http.StatusInternalServerError, gin.H{
			"msg":  err.Error(),
			"data": nil,
		})
		return
	}
	cxt.JSON(http.StatusOK, gin.H{
		"msg":  "获取pod容器成功",
		"data": containers,
	})
}

// GetPodLog 获取pod中容器日志
func (p *pod) GetPodLog(cxt *gin.Context) {
	params := new(struct {
		Namespace     string `form:"namespace"`
		PodName       string `form:"pod_name"`
		ContainerName string `form:"container_name"`
		Cluster       string `form:"cluster"`
	})
	// GET请求,绑定参数方法改为ctx.Bind
	if err := cxt.Bind(params); err != nil {
		logger.Error("Bind请求参数失败," + err.Error())
		cxt.JSON(http.StatusInternalServerError, gin.H{
			"msg":  err.Error(),
			"data": nil,
		})
		return
	}
	client, err := service.K8s.GetClient(params.Cluster)
	if err != nil {
		logger.Error("获取k8s连接失败," + err.Error())
		cxt.JSON(http.StatusInternalServerError, gin.H{
			"msg":  err.Error(),
			"data": nil,
		})
		return
	}
	log, err := service.Pod.GetPodLog(client, params.Namespace, params.PodName, params.ContainerName)
	if err != nil {
		logger.Error("获取pod日志失败," + err.Error())
		cxt.JSON(http.StatusInternalServerError, gin.H{
			"msg":  err.Error(),
			"data": nil,
		})
		return
	}
	cxt.JSON(http.StatusOK, gin.H{
		"msg":  "获取pod日志成功",
		"data": log,
	})
}

API设计与路由

router.go中定义了清晰的API路由:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
func (*router) InitApiRouter(r *gin.Engine) {
    r.GET("/api/ping", func(c *gin.Context) { c.JSON(200, gin.H{"message": "pong"}) })

    // Pod 路由服务
    podGroup := r.Group(apiBasePath)
    {
        podGroup.GET("/pod", Pod.GetPods)               // 获取Pod列表
        podGroup.GET("/pod/detail", Pod.GetPodDetail)   // 获取Pod详情
        podGroup.DELETE("/pod/del", Pod.DeletePod)     // 删除Pod
        podGroup.PUT("/pod/update", Pod.UpdatePod)      // 更新Pod
        podGroup.GET("/pod/container", Pod.GetPodContainer) // 获取容器列表
        podGroup.GET("/pod/log", Pod.GetPodLog)         // 获取容器日志
    }
}

API设计遵循RESTful规范,使用合适的HTTP方法(GET/POST/PUT/DELETE)对应不同的操作类型。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
package controller

import "github.com/gin-gonic/gin"

/**
 * @Author: 南宫乘风
 * @Description:
 * @File:  router.go
 * @Email: 1794748404@qq.com
 * @Date: 2025-03-17 17:18
 */

// Router 实例化对象,可以在main.go中调用
var Router router

type router struct {
}

const apiBasePath = "/api/k8s"

// InitRouter 初始化路由

func (*router) InitApiRouter(r *gin.Engine) {
	r.GET("/api/ping", func(c *gin.Context) { c.JSON(200, gin.H{"message": "pong"}) })

	// Pod 路由服务
	podGroup := r.Group(apiBasePath)
	{
		podGroup.GET("/pod", Pod.GetPods)
		podGroup.GET("/pod/detail", Pod.GetPodDetail)
		podGroup.DELETE("/pod/del", Pod.DeletePod)
		podGroup.PUT("/pod/update", Pod.UpdatePod)
		podGroup.GET("/pod/container", Pod.GetPodContainer)
		podGroup.GET("/pod/log", Pod.GetPodLog)
	}

}

优雅关闭

main.go中实现了服务的优雅关闭:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
// 创建信号通道
quit := make(chan os.Signal)
signal.Notify(quit, os.Interrupt)
<-quit  // 阻塞等待中断信号

// 设置5秒超时关闭
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()

// 优雅关闭服务器
if err := srv.Shutdown(ctx); err != nil {
    logger.Error("Gin Server Shutdown:", err)
}
logger.Info("Gin Server exiting")

完整代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
package main

import (
	"context"
	"kubea-go/config"
	"kubea-go/controller"
	"kubea-go/service"
	"net/http"
	"os"
	"os/signal"
	"time"

	"github.com/aryming/logger"

	"github.com/gin-gonic/gin"
)

/**
 * @Author: 南宫乘风
 * @Description:
 * @File:  main.go
 * @Email: 1794748404@qq.com
 * @Date: 2025-03-17 14:49
 */
func main() {
	logger.SetLogger("config/log.json")
	// 初始化路由
	r := gin.Default()
	// 初始化K8S客户端
	service.K8s.Init()
	controller.Router.InitApiRouter(r)
	// 启动服务
	srv := &http.Server{
		Addr:    config.ListenAddress,
		Handler: r,
	}
	go func() {
		// service connections
		if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
			logger.Error("listen: %s\n", err)
		}
	}()
	//优雅关闭
	// 声明一个系统信号的channel,并监听系统信号,如果没有信号,就一直阻塞,如果有,就继续执行。当接收到中断信号时,执行cancel()
	// 创建一个用于接收OS信号的通道
	quit := make(chan os.Signal)
	// 配置信号通知,将OS中断信号通知到quit通道
	signal.Notify(quit, os.Interrupt)
	// 阻塞等待,直到从quit通道接收到信号
	<-quit

	// 设置上下文对象ctx,带有5秒的超时
	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	// 当函数返回时,调用cancel以取消上下文并释放资源
	defer cancel()

	// 尝试优雅关闭GIN服务器
	if err := srv.Shutdown(ctx); err != nil {
		// 如果关闭失败,记录致命错误
		logger.Error("Gin Server Shutdown:", err)
	}
	// 记录服务器退出信息
	logger.Info("Gin Server exiting")
}

配置管理

配置信息集中在config.go中管理:

1
2
3
4
5
6
const (
    ListenAddress = "0.0.0.0:8081"  // 服务监听地址
    // 多集群kubeconfig配置
    Kubeconfigs    = `{"TST-1":"E:\\GitHUB_Code_Check\\VUE\\kubea-go\\config\\k8s.yaml","TST-2":"E:\\GitHUB_Code_Check\\VUE\\kubea-go\\config\\k8s.yaml"}`
    PodLogTailLine = 500  // 日志默认返回行数
)

在这里插入图片描述

在这里插入图片描述