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

发布于:2025-03-27 ⋅ 阅读:(30) ⋅ 点赞:(0)

项目背景

在云原生时代,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

代码示例

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)
	}
}

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

常用方法

	// 获取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结构体实现了多集群管理:

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

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


// 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调用时,只需指定集群名称即可操作对应的集群资源。


// 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中。

数据结构定义

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

// 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
}

过滤实现

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


// 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
}

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

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接口实现排序:

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

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详情:

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日志查询

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

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

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层面

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路由:

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)对应不同的操作类型。

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中实现了服务的优雅关闭:

// 创建信号通道
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")

完整代码:

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中管理:

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  // 日志默认返回行数
)

在这里插入图片描述

在这里插入图片描述