项目背景
在云原生时代,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
代码示例
go
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)
}
}
打印结果
常用方法
go
// 获取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{})
项目架构与核心模块
项目采用分层设计,核心模块包括:
- 路由层(Router):基于Gin框架定义API端点。
- 控制层(Controller):处理HTTP请求,参数校验,调用服务层逻辑。
- 服务层(Service):封装业务逻辑,与Kubernetes API交互。
- 数据层(Kubernetes Client):管理多集群连接,提供客户端实例。
- 工具模块 :数据筛选、分页、日志处理等。
技术栈
- Gin框架:高性能HTTP框架,用于路由和请求处理。
- Kubernetes Client-go:官方Go语言客户端,操作Kubernetes资源。
- 多集群管理:通过动态加载Kubeconfig支持多集群。
- 数据分页与过滤:自定义排序、过滤和分页逻辑。
多集群管理实现
在k8s_client.go
中,项目通过K8s
结构体实现了多集群管理:
go
type k8s struct {
ClientMap map[string]*kubernetes.Clientset // 多集群Client
KubeConfMap map[string]string // 多集群配置
}
初始化时,从配置文件读取多个集群的kubeconfig,并为每个集群创建独立的ClientSet:
go
// 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调用时,只需指定集群名称即可操作对应的集群资源。
go
// 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.go
和dataselect.go
中。
数据结构定义
首先定义了通用的数据选择器结构:
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
}
过滤实现
过滤功能通过字符串匹配实现:
go
// 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
}
分页实现
分页逻辑处理了边界情况:
go
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接口实现排序:
go
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
go
package service
import (
"sort"
"strings"
"time"
corev1 "k8s.io/api/core/v1"
)
/**
* @Author: 南宫乘风
* @Description:定义数据结构
* @File: dataselect.go
* @Email: [email protected]
* @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详情:
go
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日志查询
日志查询功能支持指定容器和返回行数限制:
go
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
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: [email protected]
* @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层面
go
package controller
import (
"kubea-go/service"
"net/http"
"github.com/aryming/logger"
"github.com/gin-gonic/gin"
)
/**
* @Author: 南宫乘风
* @Description:
* @File: pod.go.go
* @Email: [email protected]
* @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路由:
go
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)对应不同的操作类型。
go
package controller
import "github.com/gin-gonic/gin"
/**
* @Author: 南宫乘风
* @Description:
* @File: router.go
* @Email: [email protected]
* @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
中实现了服务的优雅关闭:
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")
完整代码:
go
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: [email protected]
* @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
中管理:
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 // 日志默认返回行数
)

