K8S管理系统项目实战[API开发]-2

2023-11-07

后端: go+gin

后端代码地址GitHub - yunixiangfeng/k8s-platform: K8s管理系统后端: go+gin

kubernetes v1.24.2

golang v1.18.3

5、存储与配置

5.1 ConfigMap

5.2 Secret

5.3 PersistentVolumeClaims

6、工作流

6.1 流程设计

6.2 数据库操作(GORM)

(1)初始化数据库

db/db.go

6.3 Workflow

service/workflow.go

(1)列表

(2)获取Workflow详情

(3)新增Workflow

(4)表数据列表

7、中间件

7.1 什么是中间件

7.2 gin中间件用法

7.2 Cors跨域

7.3 JWT token验证

8、WebShell终端

8.1 kubectl exec原理

8.2 实现思路

8.3 代码实现

9、总结

API开发:存储与配置资源

5、存储与配置

5.1 ConfigMap

接口实现

service/dataselector.go

// configmap
type configMapCell corev1.ConfigMap

func (c configMapCell) GetCreation() time.Time {
	return c.CreationTimestamp.Time
}

func (c configMapCell) GetName() string {
	return c.Name
}

(1)列表

(2)获取ConfigMap详情

(3)更新ConfigMap

(4)   删除ConfigMap

service/configmap.go

package service

import (
	"context"
	"encoding/json"
	"errors"

	"github.com/wonderivan/logger"
	corev1 "k8s.io/api/core/v1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

var ConfigMap configMap

type configMap struct{}

type ConfigMapsResp struct {
	Items []corev1.ConfigMap `json:"items"`
	Total int                `json:"total"`
}

// 获取configmap列表,支持过滤、排序、分页
func (c *configMap) GetConfigMaps(filterName, namespace string, limit, page int) (configMapsResp *ConfigMapsResp, err error) {
	//获取configMapList类型的configMap列表
	configMapList, err := K8s.ClientSet.CoreV1().ConfigMaps(namespace).List(context.TODO(), metav1.ListOptions{})
	if err != nil {
		logger.Error(errors.New("获取ConfigMap列表失败, " + err.Error()))
		return nil, errors.New("获取ConfigMap列表失败, " + err.Error())
	}
	//将configMapList中的configMap列表(Items),放进dataselector对象中,进行排序
	selectableData := &dataSelector{
		GenericDataList: c.toCells(configMapList.Items),
		DataSelect: &DataSelectQuery{
			Filter: &FilterQuery{Name: filterName},
			Paginate: &PaginateQuery{
				Limit: limit,
				Page:  page,
			},
		},
	}

	filtered := selectableData.Filter()
	total := len(filtered.GenericDataList)
	data := filtered.Sort().Paginate()

	//将[]DataCell类型的configmap列表转为v1.configmap列表
	configMaps := c.fromCells(data.GenericDataList)

	return &ConfigMapsResp{
		Items: configMaps,
		Total: total,
	}, nil
}

// 获取configmap详情
func (c *configMap) GetConfigMapDetail(configMapName, namespace string) (configMap *corev1.ConfigMap, err error) {
	configMap, err = K8s.ClientSet.CoreV1().ConfigMaps(namespace).Get(context.TODO(), configMapName, metav1.GetOptions{})
	if err != nil {
		logger.Error(errors.New("获取ConfigMap详情失败, " + err.Error()))
		return nil, errors.New("获取ConfigMap详情失败, " + err.Error())
	}

	return configMap, nil
}

// 删除configmap
func (c *configMap) DeleteConfigMap(configMapName, namespace string) (err error) {
	err = K8s.ClientSet.CoreV1().ConfigMaps(namespace).Delete(context.TODO(), configMapName, metav1.DeleteOptions{})
	if err != nil {
		logger.Error(errors.New("删除ConfigMap失败, " + err.Error()))
		return errors.New("删除ConfigMap失败, " + err.Error())
	}

	return nil
}

// 更新configmap
func (c *configMap) UpdateConfigMap(namespace, content string) (err error) {
	var configMap = &corev1.ConfigMap{}

	err = json.Unmarshal([]byte(content), configMap)
	if err != nil {
		logger.Error(errors.New("反序列化失败, " + err.Error()))
		return errors.New("反序列化失败, " + err.Error())
	}

	_, err = K8s.ClientSet.CoreV1().ConfigMaps(namespace).Update(context.TODO(), configMap, metav1.UpdateOptions{})
	if err != nil {
		logger.Error(errors.New("更新ConfigMap失败, " + err.Error()))
		return errors.New("更新ConfigMap失败, " + err.Error())
	}
	return nil
}

func (c *configMap) toCells(std []corev1.ConfigMap) []DataCell {
	cells := make([]DataCell, len(std))
	for i := range std {
		cells[i] = configMapCell(std[i])
	}
	return cells
}

func (c *configMap) fromCells(cells []DataCell) []corev1.ConfigMap {
	configMaps := make([]corev1.ConfigMap, len(cells))
	for i := range cells {
		configMaps[i] = corev1.ConfigMap(cells[i].(configMapCell))
	}

	return configMaps
}

controller/configmap.go

package controller

import (
	"k8s-platform/service"
	"net/http"

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

var ConfigMap configMap

type configMap struct{}

// 获取configmap列表,支持过滤、排序、分页
func (c *configMap) GetConfigMaps(ctx *gin.Context) {
	params := new(struct {
		FilterName string `form:"filter_name"`
		Namespace  string `form:"namespace"`
		Page       int    `form:"page"`
		Limit      int    `form:"limit"`
	})
	if err := ctx.Bind(params); err != nil {
		logger.Error("Bind请求参数失败, " + err.Error())
		ctx.JSON(http.StatusInternalServerError, gin.H{
			"msg":  err.Error(),
			"data": nil,
		})
		return
	}

	data, err := service.ConfigMap.GetConfigMaps(params.FilterName, params.Namespace, params.Limit, params.Page)
	if err != nil {
		ctx.JSON(http.StatusInternalServerError, gin.H{
			"msg":  err.Error(),
			"data": nil,
		})
		return
	}

	ctx.JSON(http.StatusOK, gin.H{
		"msg":  "获取ConfigMap列表成功",
		"data": data,
	})
}

// 获取configmap详情
func (c *configMap) GetConfigMapDetail(ctx *gin.Context) {
	params := new(struct {
		ConfigMapName string `form:"configmap_name"`
		Namespace     string `form:"namespace"`
	})
	if err := ctx.Bind(params); err != nil {
		logger.Error("Bind请求参数失败, " + err.Error())
		ctx.JSON(http.StatusInternalServerError, gin.H{
			"msg":  err.Error(),
			"data": nil,
		})
		return
	}

	data, err := service.ConfigMap.GetConfigMapDetail(params.ConfigMapName, params.Namespace)
	if err != nil {
		ctx.JSON(http.StatusInternalServerError, gin.H{
			"msg":  err.Error(),
			"data": nil,
		})
		return
	}

	ctx.JSON(http.StatusOK, gin.H{
		"msg":  "获取ConfigMap详情成功",
		"data": data,
	})
}

// 删除configmap
func (c *configMap) DeleteConfigMap(ctx *gin.Context) {
	params := new(struct {
		ConfigMapName string `json:"configmap_name"`
		Namespace     string `json:"namespace"`
	})
	//DELETE请求,绑定参数方法改为ctx.ShouldBindJSON
	if err := ctx.ShouldBindJSON(params); err != nil {
		logger.Error("Bind请求参数失败, " + err.Error())
		ctx.JSON(http.StatusInternalServerError, gin.H{
			"msg":  err.Error(),
			"data": nil,
		})
		return
	}

	err := service.ConfigMap.DeleteConfigMap(params.ConfigMapName, params.Namespace)
	if err != nil {
		ctx.JSON(http.StatusInternalServerError, gin.H{
			"msg":  err.Error(),
			"data": nil,
		})
		return
	}
	ctx.JSON(http.StatusOK, gin.H{
		"msg":  "删除ConfigMap成功",
		"data": nil,
	})
}

// 更新configmap
func (c *configMap) UpdateConfigMap(ctx *gin.Context) {
	params := new(struct {
		Namespace string `json:"namespace"`
		Content   string `json:"content"`
	})
	//PUT请求,绑定参数方法改为ctx.ShouldBindJSON
	if err := ctx.ShouldBindJSON(params); err != nil {
		logger.Error("Bind请求参数失败, " + err.Error())
		ctx.JSON(http.StatusInternalServerError, gin.H{
			"msg":  err.Error(),
			"data": nil,
		})
		return
	}

	err := service.ConfigMap.UpdateConfigMap(params.Namespace, params.Content)
	if err != nil {
		ctx.JSON(http.StatusInternalServerError, gin.H{
			"msg":  err.Error(),
			"data": nil,
		})
		return
	}
	ctx.JSON(http.StatusOK, gin.H{
		"msg":  "更新ConfigMap成功",
		"data": nil,
	})
}

添加路由

// controller/router.go
		// Configmaps
		GET("/api/k8s/configmaps", ConfigMap.GetConfigMaps).
		GET("/api/k8s/configmap/detail", ConfigMap.GetConfigMapDetail).
		DELETE("/api/k8s/configmap/del", ConfigMap.DeleteConfigMap).
		PUT("/api/k8s/configmap/update", ConfigMap.UpdateConfigMap)

 测试api接口

5.2 Secret

接口实现

service/dataselector.go

// secret
type secretCell corev1.Secret

func (s secretCell) GetCreation() time.Time {
	return s.CreationTimestamp.Time
}

func (s secretCell) GetName() string {
	return s.Name
}

(1)列表

(2)获取Secret详情

(3)更新Secret

(4)   删除Secret

service/secret.go

package service

import (
	"context"
	"encoding/json"
	"errors"

	"github.com/wonderivan/logger"
	corev1 "k8s.io/api/core/v1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

var Secret secret

type secret struct{}

type SecretsResp struct {
	Items []corev1.Secret `json:"items"`
	Total int             `json:"total"`
}

// 获取secret列表,支持过滤、排序、分页
func (s *secret) GetSecrets(filterName, namespace string, limit, page int) (secretsResp *SecretsResp, err error) {
	//获取secretList类型的secret列表
	secretList, err := K8s.ClientSet.CoreV1().Secrets(namespace).List(context.TODO(), metav1.ListOptions{})
	if err != nil {
		logger.Error(errors.New("获取Secret列表失败, " + err.Error()))
		return nil, errors.New("获取Secret列表失败, " + err.Error())
	}
	//将secretList中的secret列表(Items),放进dataselector对象中,进行排序
	selectableData := &dataSelector{
		GenericDataList: s.toCells(secretList.Items),
		DataSelect: &DataSelectQuery{
			Filter: &FilterQuery{Name: filterName},
			Paginate: &PaginateQuery{
				Limit: limit,
				Page:  page,
			},
		},
	}

	filtered := selectableData.Filter()
	total := len(filtered.GenericDataList)
	data := filtered.Sort().Paginate()

	//将[]DataCell类型的secret列表转为v1.secret列表
	secrets := s.fromCells(data.GenericDataList)

	return &SecretsResp{
		Items: secrets,
		Total: total,
	}, nil
}

// 获取secret详情
func (s *secret) GetSecretDetail(secretName, namespace string) (secret *corev1.Secret, err error) {
	secret, err = K8s.ClientSet.CoreV1().Secrets(namespace).Get(context.TODO(), secretName, metav1.GetOptions{})
	if err != nil {
		logger.Error(errors.New("获取Secret详情失败, " + err.Error()))
		return nil, errors.New("获取Secret详情失败, " + err.Error())
	}

	return secret, nil
}

// 删除secret
func (s *secret) DeleteSecret(secretName, namespace string) (err error) {
	err = K8s.ClientSet.CoreV1().Secrets(namespace).Delete(context.TODO(), secretName, metav1.DeleteOptions{})
	if err != nil {
		logger.Error(errors.New("删除Secret失败, " + err.Error()))
		return errors.New("删除Secret失败, " + err.Error())
	}

	return nil
}

// 更新secret
func (s *secret) UpdateSecret(namespace, content string) (err error) {
	var secret = &corev1.Secret{}

	err = json.Unmarshal([]byte(content), secret)
	if err != nil {
		logger.Error(errors.New("反序列化失败, " + err.Error()))
		return errors.New("反序列化失败, " + err.Error())
	}

	_, err = K8s.ClientSet.CoreV1().Secrets(namespace).Update(context.TODO(), secret, metav1.UpdateOptions{})
	if err != nil {
		logger.Error(errors.New("更新Secret失败, " + err.Error()))
		return errors.New("更新Secret失败, " + err.Error())
	}
	return nil
}

func (s *secret) toCells(std []corev1.Secret) []DataCell {
	cells := make([]DataCell, len(std))
	for i := range std {
		cells[i] = secretCell(std[i])
	}
	return cells
}

func (s *secret) fromCells(cells []DataCell) []corev1.Secret {
	secrets := make([]corev1.Secret, len(cells))
	for i := range cells {
		secrets[i] = corev1.Secret(cells[i].(secretCell))
	}

	return secrets
}

 controller/secret.go

package controller

import (
	"k8s-platform/service"
	"net/http"

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

var Secret secret

type secret struct{}

// 获取secret列表,支持过滤、排序、分页
func (s *secret) GetSecrets(ctx *gin.Context) {
	params := new(struct {
		FilterName string `form:"filter_name"`
		Namespace  string `form:"namespace"`
		Page       int    `form:"page"`
		Limit      int    `form:"limit"`
	})
	if err := ctx.Bind(params); err != nil {
		logger.Error("Bind请求参数失败, " + err.Error())
		ctx.JSON(http.StatusInternalServerError, gin.H{
			"msg":  err.Error(),
			"data": nil,
		})
		return
	}

	data, err := service.Secret.GetSecrets(params.FilterName, params.Namespace, params.Limit, params.Page)
	if err != nil {
		ctx.JSON(http.StatusInternalServerError, gin.H{
			"msg":  err.Error(),
			"data": nil,
		})
		return
	}

	ctx.JSON(http.StatusOK, gin.H{
		"msg":  "获取Secret列表成功",
		"data": data,
	})
}

// 获取secret详情
func (s *secret) GetSecretDetail(ctx *gin.Context) {
	params := new(struct {
		SecretName string `form:"secret_name"`
		Namespace  string `form:"namespace"`
	})
	if err := ctx.Bind(params); err != nil {
		logger.Error("Bind请求参数失败, " + err.Error())
		ctx.JSON(http.StatusInternalServerError, gin.H{
			"msg":  err.Error(),
			"data": nil,
		})
		return
	}

	data, err := service.Secret.GetSecretDetail(params.SecretName, params.Namespace)
	if err != nil {
		ctx.JSON(http.StatusInternalServerError, gin.H{
			"msg":  err.Error(),
			"data": nil,
		})
		return
	}

	ctx.JSON(http.StatusOK, gin.H{
		"msg":  "获取Secret详情成功",
		"data": data,
	})
}

// 删除secret
func (s *secret) DeleteSecret(ctx *gin.Context) {
	params := new(struct {
		SecretName string `json:"secret_name"`
		Namespace  string `json:"namespace"`
	})
	//DELETE请求,绑定参数方法改为ctx.ShouldBindJSON
	if err := ctx.ShouldBindJSON(params); err != nil {
		logger.Error("Bind请求参数失败, " + err.Error())
		ctx.JSON(http.StatusInternalServerError, gin.H{
			"msg":  err.Error(),
			"data": nil,
		})
		return
	}

	err := service.Secret.DeleteSecret(params.SecretName, params.Namespace)
	if err != nil {
		ctx.JSON(http.StatusInternalServerError, gin.H{
			"msg":  err.Error(),
			"data": nil,
		})
		return
	}
	ctx.JSON(http.StatusOK, gin.H{
		"msg":  "删除Secret成功",
		"data": nil,
	})
}

// 更新secret
func (s *secret) UpdateSecret(ctx *gin.Context) {
	params := new(struct {
		Namespace string `json:"namespace"`
		Content   string `json:"content"`
	})
	//PUT请求,绑定参数方法改为ctx.ShouldBindJSON
	if err := ctx.ShouldBindJSON(params); err != nil {
		logger.Error("Bind请求参数失败, " + err.Error())
		ctx.JSON(http.StatusInternalServerError, gin.H{
			"msg":  err.Error(),
			"data": nil,
		})
		return
	}

	err := service.Secret.UpdateSecret(params.Namespace, params.Content)
	if err != nil {
		ctx.JSON(http.StatusInternalServerError, gin.H{
			"msg":  err.Error(),
			"data": nil,
		})
		return
	}
	ctx.JSON(http.StatusOK, gin.H{
		"msg":  "更新Secret成功",
		"data": nil,
	})
}

 定义路由

controller/router.go

		// secret
		GET("/api/k8s/secrets", Secret.GetSecrets).
		GET("/api/k8s/secret/detail", Secret.GetSecretDetail).
		DELETE("/api/k8s/secret/del", Secret.DeleteSecret).
		PUT("/api/k8s/secret/update", Secret.UpdateSecret)

 测试api接口

5.3 PersistentVolumeClaims

接口实现

service/dataselector.go

// pvc
type pvcCell corev1.PersistentVolumeClaim

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

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

(1)列表

(2)获取Pvc详情

(3)更新Pvc

(4)   删除Pvc
service/pvc.go

package service

import (
	"context"
	"encoding/json"
	"errors"

	"github.com/wonderivan/logger"
	corev1 "k8s.io/api/core/v1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

var Pvc pvc

type pvc struct{}

type PvcsResp struct {
	Items []corev1.PersistentVolumeClaim `json:"items"`
	Total int                            `json:"total"`
}

// 获取pvc列表,支持过滤、排序、分页
func (p *pvc) GetPvcs(filterName, namespace string, limit, page int) (pvcsResp *PvcsResp, err error) {
	//获取pvcList类型的pvc列表
	pvcList, err := K8s.ClientSet.CoreV1().PersistentVolumeClaims(namespace).List(context.TODO(), metav1.ListOptions{})
	if err != nil {
		logger.Error(errors.New("获取Pvc列表失败, " + err.Error()))
		return nil, errors.New("获取Pvc列表失败, " + err.Error())
	}
	//将pvcList中的pvc列表(Items),放进dataselector对象中,进行排序
	selectableData := &dataSelector{
		GenericDataList: p.toCells(pvcList.Items),
		DataSelect: &DataSelectQuery{
			Filter: &FilterQuery{Name: filterName},
			Paginate: &PaginateQuery{
				Limit: limit,
				Page:  page,
			},
		},
	}

	filtered := selectableData.Filter()
	total := len(filtered.GenericDataList)
	data := filtered.Sort().Paginate()

	//将[]DataCell类型的pvc列表转为v1.pvc列表
	pvcs := p.fromCells(data.GenericDataList)

	return &PvcsResp{
		Items: pvcs,
		Total: total,
	}, nil
}

// 获取pvc详情
func (p *pvc) GetPvcDetail(pvcName, namespace string) (pvc *corev1.PersistentVolumeClaim, err error) {
	pvc, err = K8s.ClientSet.CoreV1().PersistentVolumeClaims(namespace).Get(context.TODO(), pvcName, metav1.GetOptions{})
	if err != nil {
		logger.Error(errors.New("获取Pvc详情失败, " + err.Error()))
		return nil, errors.New("获取Pvc详情失败, " + err.Error())
	}

	return pvc, nil
}

// 删除pvc
func (p *pvc) DeletePvc(pvcName, namespace string) (err error) {
	err = K8s.ClientSet.CoreV1().PersistentVolumeClaims(namespace).Delete(context.TODO(), pvcName, metav1.DeleteOptions{})
	if err != nil {
		logger.Error(errors.New("删除Pvc失败, " + err.Error()))
		return errors.New("删除Pvc失败, " + err.Error())
	}

	return nil
}

// 更新pvc
func (p *pvc) UpdatePvc(namespace, content string) (err error) {
	var pvc = &corev1.PersistentVolumeClaim{}

	err = json.Unmarshal([]byte(content), pvc)
	if err != nil {
		logger.Error(errors.New("反序列化失败, " + err.Error()))
		return errors.New("反序列化失败, " + err.Error())
	}

	_, err = K8s.ClientSet.CoreV1().PersistentVolumeClaims(namespace).Update(context.TODO(), pvc, metav1.UpdateOptions{})
	if err != nil {
		logger.Error(errors.New("更新Pvc失败, " + err.Error()))
		return errors.New("更新Pvc失败, " + err.Error())
	}
	return nil
}

func (p *pvc) toCells(std []corev1.PersistentVolumeClaim) []DataCell {
	cells := make([]DataCell, len(std))
	for i := range std {
		cells[i] = pvcCell(std[i])
	}
	return cells
}

func (p *pvc) fromCells(cells []DataCell) []corev1.PersistentVolumeClaim {
	pvcs := make([]corev1.PersistentVolumeClaim, len(cells))
	for i := range cells {
		pvcs[i] = corev1.PersistentVolumeClaim(cells[i].(pvcCell))
	}

	return pvcs
}

 controller/pvc.go

package controller

import (
	"k8s-platform/service"
	"net/http"

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

var Pvc pvc

type pvc struct{}

// 获取pvc列表,支持过滤、排序、分页
func (p *pvc) GetPvcs(ctx *gin.Context) {
	params := new(struct {
		FilterName string `form:"filter_name"`
		Namespace  string `form:"namespace"`
		Page       int    `form:"page"`
		Limit      int    `form:"limit"`
	})
	if err := ctx.Bind(params); err != nil {
		logger.Error("Bind请求参数失败, " + err.Error())
		ctx.JSON(http.StatusInternalServerError, gin.H{
			"msg":  err.Error(),
			"data": nil,
		})
		return
	}

	data, err := service.Pvc.GetPvcs(params.FilterName, params.Namespace, params.Limit, params.Page)
	if err != nil {
		ctx.JSON(http.StatusInternalServerError, gin.H{
			"msg":  err.Error(),
			"data": nil,
		})
		return
	}

	ctx.JSON(http.StatusOK, gin.H{
		"msg":  "获取Pvc列表成功",
		"data": data,
	})
}

// 获取pvc详情
func (p *pvc) GetPvcDetail(ctx *gin.Context) {
	params := new(struct {
		PvcName   string `form:"pvc_name"`
		Namespace string `form:"namespace"`
	})
	if err := ctx.Bind(params); err != nil {
		logger.Error("Bind请求参数失败, " + err.Error())
		ctx.JSON(http.StatusInternalServerError, gin.H{
			"msg":  err.Error(),
			"data": nil,
		})
		return
	}

	data, err := service.Pvc.GetPvcDetail(params.PvcName, params.Namespace)
	if err != nil {
		ctx.JSON(http.StatusInternalServerError, gin.H{
			"msg":  err.Error(),
			"data": nil,
		})
		return
	}

	ctx.JSON(http.StatusOK, gin.H{
		"msg":  "获取Pvc详情成功",
		"data": data,
	})
}

// 删除pvc
func (p *pvc) DeletePvc(ctx *gin.Context) {
	params := new(struct {
		PvcName   string `json:"pvc_name"`
		Namespace string `json:"namespace"`
	})
	//DELETE请求,绑定参数方法改为ctx.ShouldBindJSON
	if err := ctx.ShouldBindJSON(params); err != nil {
		logger.Error("Bind请求参数失败, " + err.Error())
		ctx.JSON(http.StatusInternalServerError, gin.H{
			"msg":  err.Error(),
			"data": nil,
		})
		return
	}

	err := service.Pvc.DeletePvc(params.PvcName, params.Namespace)
	if err != nil {
		ctx.JSON(http.StatusInternalServerError, gin.H{
			"msg":  err.Error(),
			"data": nil,
		})
		return
	}
	ctx.JSON(http.StatusOK, gin.H{
		"msg":  "删除Pvc成功",
		"data": nil,
	})
}

// 更新pvc
func (p *pvc) UpdatePvc(ctx *gin.Context) {
	params := new(struct {
		Namespace string `json:"namespace"`
		Content   string `json:"content"`
	})
	//PUT请求,绑定参数方法改为ctx.ShouldBindJSON
	if err := ctx.ShouldBindJSON(params); err != nil {
		logger.Error("Bind请求参数失败, " + err.Error())
		ctx.JSON(http.StatusInternalServerError, gin.H{
			"msg":  err.Error(),
			"data": nil,
		})
		return
	}

	err := service.Pvc.UpdatePvc(params.Namespace, params.Content)
	if err != nil {
		ctx.JSON(http.StatusInternalServerError, gin.H{
			"msg":  err.Error(),
			"data": nil,
		})
		return
	}
	ctx.JSON(http.StatusOK, gin.H{
		"msg":  "更新Pvc成功",
		"data": nil,
	})
}

 添加路由

controller/router.go

		//pvc操作
		GET("/api/k8s/pvcs", Pvc.GetPvcs).
		GET("/api/k8s/pvc/detail", Pvc.GetPvcDetail).
		DELETE("/api/k8s/pvc/del", Pvc.DeletePvc).
		PUT("/api/k8s/pvc/update", Pvc.UpdatePvc)

 测试api接口

API开发:部署工作流

6、工作流

6.1 流程设计

6.2 数据库操作(GORM)

(1)初始化数据库

db/init.go

package db

import (
	"fmt"
	"k8s-plantform/config"
	"time"

	"github.com/wonderivan/logger"

	"github.com/jinzhu/gorm"
	_ "github.com/jinzhu/gorm/dialects/mysql"
)

var (
	isInit bool
	GORM   *gorm.DB
	err    error
)

// DB的初始化函数,与数据库建立连接
func Init() {
	// 判断是否已经初始化
	if isInit {
		return
	}
	// 组装连接配置
	dsn := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=utf8&parseTime=True&loc=Local",
		config.DbUser,
		config.DbPass,
		config.DbHost,
		config.DbPort,
		config.DbName)
	GORM, err := gorm.Open(config.DbType, dsn)
	if err != nil {
		panic("数据库连接失败," + err.Error())
	}
	// 打印sql语句
	GORM.LogMode(config.LogMode)
	// 开启连接池
	GORM.DB().SetMaxIdleConns(config.MaxIdleConns)
	GORM.DB().SetMaxOpenConns(config.MaxOpenConns)
	GORM.DB().SetConnMaxLifetime(time.Duration(config.MaxLifeTime))
	isInit = true
	logger.Info("数据库初始化成功")
}

// 关闭数据库连接
func Close() error {
	return GORM.Close()
}

加数据库配置

service/config.go

package config

import "time"

const (
	ListenAddr = "0.0.0.0:9090"
	KubeConfig = "C:\\Users\\Administrator\\.kube\\config"
	// tail的日志行数
	// tail -n 2000
	PodLogTailLine = 2000

	// DB Config
	DbType = "mysql"
	DbHost = "192.168.204.129"
	DbPort = 3306
	DbName = "k8s_dashboard"
	DbUser = "root"
	DbPass = ""
	// 打印mysql debug的sql日志
	LogMode = false
	// 连接池配置
	MaxIdleConns = 10               // 最大空闲连接
	MaxOpenConns = 100              // 最大连接数
	MaxLifeTime  = 30 * time.Second // 会话时间
)

SetMaxOpenConns
默认情况下,连接池的最大数量是没有限制的,一般来说,连接数越多,访问数据库的性能越高,但是系统资源不是无限的,数据库的并发能力也不是无限的,因此为了减少系统和数据据库崩溃的风险,可以给并发连接教设置一个上限,这个数值一般不超过进程的最大文件句柄打开数,不超过数据库服务自身支持的并发连接数,比如1000。
SetMaxldleConns
理论上maxldleConns连接的上限越高,也即允许在连接池中的空闲连接最大值越大,可以有效减少连接创建和消毁的次数,提高程序的性能,但是连接对象也是占用内存资源的,而且如果空闲连接越多,存在于连接池内的时间可能越长,连接在经过一段时间后有可能会变得不可用,而这时连接还在连接池内没有回收的话,后续被征用的时候就会出问题,一般建议maxidleConns的值为MaxOpenConns的1/2仅供参考。
SetConnMaxLifetime
设置一个连接被使用的最长时间,即过了一段时间后会被强制回收,理论上这可以有效减少不可用连接出现的概率,当数据库方面也设置了连接的超时时间时,这个值应当不超过数据库的超时参数值。

main.go

初始化

package main

import (
	"k8s-platform/config"
	"k8s-platform/controller"
	"k8s-platform/db"
	"k8s-platform/service"

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

func main() {
	// 初始化k8s client
	service.K8s.Init() // 可以使用service.K8s.clientset 进行跨包调用

	// 初始化数据库
	db.Init()
	// 初始化gin对象/路由配置
	r := gin.Default()
	// 初始化路由规则
	controller.Router.InitApiRouter(r)
	// gin程序启动
	r.Run(config.ListenAddr)

	// 关闭数据库
	db.Close()
}

创建数据库k8s_dashboard

PS C:\Users\Administrator\Desktop\k8s-platform> go run main.go
2023-05-07 10:37:11 [INFO] [C:/Users/Administrator/Desktop/k8s-platform/service/init.go:26] 获取K8s配置成功!
2023-05-07 10:37:11 [INFO] [C:/Users/Administrator/Desktop/k8s-platform/service/init.go:33] 创建K8s client 成功!
2023-05-07 10:37:11 [INFO] [C:/Users/Administrator/Desktop/k8s-platform/db/init.go:44] 数据库初始化成功
[GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.

[GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production.
 - using env:   export GIN_MODE=release
 - using code:  gin.SetMode(gin.ReleaseMode)

[GIN-debug] GET    /api/k8s/pods             --> k8s-platform/controller.(*pod).GetPods-fm (3 handlers)
[GIN-debug] GET    /api/k8s/pod/detail       --> k8s-platform/controller.(*pod).GetPodDetail-fm (3 handlers)
[GIN-debug] DELETE /api/k8s/pod/del          --> k8s-platform/controller.(*pod).DeletePod-fm (3 handlers)
[GIN-debug] PUT    /api/k8s/pod/update       --> k8s-platform/controller.(*pod).UpdatePod-fm (3 handlers)
[GIN-debug] GET    /api/k8s/pod/container    --> k8s-platform/controller.(*pod).GetPodContainer-fm (3 handlers)
[GIN-debug] GET    /api/k8s/pod/log          --> k8s-platform/controller.(*pod).GetPodLog-fm (3 handlers)
[GIN-debug] GET    /api/k8s/pod/numnp        --> k8s-platform/controller.(*pod).GetPodNumPerNp-fm (3 handlers)
[GIN-debug] GET    /api/k8s/deployments      --> k8s-platform/controller.(*deployment).GetDeployments-fm (3 handlers)
[GIN-debug] GET    /api/k8s/deployment/detail --> k8s-platform/controller.(*deployment).GetDeploymentDetail-fm (3 handlers)
[GIN-debug] PUT    /api/k8s/deployment/scale --> k8s-platform/controller.(*deployment).ScaleDeployment-fm (3 handlers)
[GIN-debug] DELETE /api/k8s/deployment/del   --> k8s-platform/controller.(*deployment).DeleteDeployment-fm (3 handlers)
[GIN-debug] PUT    /api/k8s/deployment/restart --> k8s-platform/controller.(*deployment).RestartDeployment-fm (3 handlers)
[GIN-debug] PUT    /api/k8s/deployment/update --> k8s-platform/controller.(*deployment).UpdateDeployment-fm (3 handlers)
[GIN-debug] GET    /api/k8s/deployment/numnp --> k8s-platform/controller.(*deployment).GetDeployNumPerNp-fm (3 handlers)
[GIN-debug] POST   /api/k8s/deployment/create --> k8s-platform/controller.(*deployment).CreateDeployment-fm (3 handlers)
[GIN-debug] [WARNING] You trusted all proxies, this is NOT safe. We recommend you to set a value.Please check https://pkg.go.dev/github.com/gin-gonic/gin#readme-don-t-trust-all-proxies for details.
[GIN-debug] Listening and serving HTTP on 0.0.0.0:9090

(2)建立表的映射关系

表结构

model/workflow.go

package model

import "time"

// 定义结构体,属性与mysql表字段对齐
type Workflow struct {
	// gorm:"primarykey"用于声明主键
	ID       uint       `json:"id" gorm:"primaryKey"`
	CreateAt *time.Time `json:"created_at"`
	UpdateAt *time.Time `json:"update_at"`
	DeleteAt *time.Time `json:"deleted_at"`

	Name       string `json:"name"`
	Namespace  string `json:"namespace"`
	Replicas   int32  `json:"replicas"`
	Deployment string `json:"deployment"`
	Service    string `json:"service"`
	Ingress    string `json:"ingress"`
	// gorm:"column:type"用于声明mysql中表的字段名
	Type string `json:"type" gorm:"column:type"`
}

// 定义TableName方法,返回mysql表名,以次定义mysql中的表名
func (*Workflow) TableName() string {
	return "workflow"
}

(3)数据库创建表

db\workflow.sql

CREATE TABLE `workflow` ( 
	`id` int NOT NULL AUTO_INCREMENT,
	`name` varchar(32) COLLATE utf8mb4_general_ci NOT NULL,
	`namespace` varchar(32) COLLATE utf8mb4_general_ci DEFAULT NULL,
	`replicas` int DEFAULT NULL,
	`deployment` varchar(32) COLLATE utf8mb4_general_ci DEFAULT NULL,
	`service` varchar(32) COLLATE utf8mb4_general_ci DEFAULT NULL,
	`ingress` varchar(32) COLLATE utf8mb4_general_ci DEFAULT NULL,
	`type` varchar(32) COLLATE utf8mb4_general_ci DEFAULT NULL,
	`created_at` datetime DEFAULT NULL,
	`updated_at` datetime DEFAULT NULL,
	`deleted_at` datetime DEFAULT NULL,
	PRIMARY KEY (`id`) USING BTREE,
	UNIQUE KEY `name` (`name`)
	) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;

(4)表数据列表

// 获取workflow列表/获取列表分页查询GetList
func (w *workflow) GetWorkflows(filterName, namespace string, limit, page int) (data *WorkflowResp, err error) {
	//定义分页的起始位置
	startSet := (page - 1) * limit
	//定义数据库查询返回的内容
	var (
		workflowList []*model.Workflow
		total        int
	)
	//数据库查询,Limit方法用于限制条数,Offset方法用于设置起始位置
	tx := db.GORM.
		Model(&model.Workflow{}).
		Where("name like ?", "%"+filterName+"%").
		Count(&total).
		Limit(limit).
		Offset(startSet).
		Order("id desc").
		Find(&workflowList)
	if tx.Error != nil && tx.Error.Error() != "record not found" {
		logger.Error("获取Workflow列表失败, " + tx.Error.Error())
		return nil, errors.New("获取Workflow列表失败, " + tx.Error.Error())
	}
	return &WorkflowResp{
		Items: workflowList,
		Total: total,
	}, nil
}

(5)获取单条

// 获取详情
func (w *workflow) GetById(id int) (workflow *model.Workflow, err error) {
	workflow = &model.Workflow{}
	tx := db.GORM.Where("id = ?", id).First(&workflow)
	if tx.Error != nil && tx.Error.Error() != "record not found" {
		logger.Error("获取Workflow详情失败, " + tx.Error.Error())
		return nil, errors.New("获取Workflow详情失败, " + tx.Error.Error())
	}
	return workflow, nil
}

(6)表数据新增

// 创建
func (w *workflow) Add(workflow *model.Workflow) (err error) {
	tx := db.GORM.Create(&workflow)
	if tx.Error != nil && tx.Error.Error() != "record not found" {
		logger.Error("创建Workflow失败, " + tx.Error.Error())
		return errors.New("创建Workflow失败, " + tx.Error.Error())
	}
	return nil
}

(7)表数据删除

// 删除
func (w *workflow) DelById(id int) (err error) {
	tx := db.GORM.Where("id = ?", id).Delete(&model.Workflow{})
	if tx.Error != nil && tx.Error.Error() != "record not found" {
		logger.Error("获取Workflow详情失败, " + tx.Error.Error())
		return errors.New("获取Workflow详情失败, " + tx.Error.Error())
	}
	return nil
}
package dao

import (
	"errors"
	"k8s-platform/db"
	"k8s-platform/model"

	"github.com/wonderivan/logger"
)

var Workflow workflow

type workflow struct{}

//定义列表的返回内容,Items是workflow元素列表,Total为workflow元素数量
type WorkflowResp struct {
	Items []*model.Workflow `json:"items"`
	Total int               `json:"total"`
}

6.3 Workflow

service/workflow.go

package service

import (
	"k8s-platform/dao"
	"k8s-platform/model"
)

var Workflow workflow

type workflow struct{}

//定义workflowCreate类型
type WorkflowCreate struct {
	Name          string                 `json:"name"`
	Namespace     string                 `json:"namespace"`
	Replicas      int32                  `json:"replicas"`
	Image         string                 `json:"image"`
	Label         map[string]string      `json:"label"`
	Cpu           string                 `json:"cpu"`
	Memory        string                 `json:"memory"`
	ContainerPort int32                  `json:"container_port"`
	HealthCheck   bool                   `json:"health_check"`
	HealthPath    string                 `json:"health_path"`
	Type          string                 `json:"type"`
	Port          int32                  `json:"port"`
	NodePort      int32                  `json:"node_port"`
	Hosts         map[string][]*HttpPath `json:"hosts"`
}

(1)列表

//获取列表分页查询
func(w *workflow) GetList(name, namespace string, page, limit int) (data *dao.WorkflowResp, err error) {
	data, err = dao.Workflow.GetWorkflows(name, namespace, page, limit)
	if err != nil {
		return nil, err
	}
	return data, nil
}

(2)获取Workflow详情

//查询workflow单条数据
func(w *workflow) GetById(id int) (data *model.Workflow, err error) {
	data, err = dao.Workflow.GetById(id)
	if err != nil {
		return nil, err
	}
	return data, nil
}

(3)新增Workflow

//创建workflow
func(w *workflow) CreateWorkFlow(data *WorkflowCreate) (err error) {
	//定义ingress名字
	var ingressName string
	if data.Type == "Ingress" {
		ingressName = getIngressName(data.Name)
	} else {
		ingressName = ""
	}

	//workflow数据落库
	workflow := &model.Workflow{
		Name:       data.Name,
		Namespace:  data.Namespace,
		Replicas:   data.Replicas,
		Deployment: data.Name,
		Service:    getServiceName(data.Name),
		Ingress:    ingressName,
		Type:       data.Type,
	}
	err = dao.Workflow.Add(workflow)
	if err != nil {
		return err
	}
	//创建k8s资源
	err = createWorkflowRes(data)
	if err != nil {
		return err
	}

	return err
}

//创建k8s资源 deployment service ingress
func createWorkflowRes(data *WorkflowCreate) (err error) {

	//创建deployment
	dc := &DeployCreate{
		Name:          data.Name,
		Namespace:     data.Namespace,
		Replicas:      data.Replicas,
		Image:         data.Image,
		Label:         data.Label,
		Cpu:           data.Cpu,
		Memory:        data.Memory,
		ContainerPort: data.ContainerPort,
		HealthCheck:   data.HealthCheck,
		HealthPath:    data.HealthPath,
	}
	err = Deployment.CreateDeployment(dc)
	if err != nil {
		return err
	}
	var serviceType string
	if data.Type != "Ingress" {
		serviceType = data.Type
	} else {
		serviceType = "ClusterIP"
	}
	//创建service
	sc := &ServiceCreate{
		Name:          getServiceName(data.Name),
		Namespace:     data.Namespace,
		Type:          serviceType,
		ContainerPort: data.ContainerPort,
		Port:          data.Port,
		NodePort:      data.NodePort,
		Label:         data.Label,
	}
	if err := Servicev1.CreateService(sc); err != nil {
		return err
	}
	//创建ingress
	var ic *IngressCreate
	if data.Type == "Ingress" {
		ic = &IngressCreate{
			Name:      getIngressName(data.Name),
			Namespace: data.Namespace,
			Label:     data.Label,
			Hosts:     data.Hosts,
		}
		err = Ingress.CreateIngress(ic)
		if err != nil {
			return err
		}
	}

	return nil
}

//workflow名字转换成service名字,添加-svc后缀
func getServiceName(workflowName string) (serviceName string) {
	return workflowName + "-svc"
}
//workflow名字转换成ingress名字,添加-ing后缀
func getIngressName(workflowName string) (ingressName string) {
	return workflowName + "-ing"
}

(4)删除workflow

//删除workflow
func(w *workflow) DelById(id int) (err error) {
	//获取数据库数据
	workflow, err := dao.Workflow.GetById(id)
	if err != nil {
		return err
	}
	//删除k8s资源
	err = delWorkflowRes(workflow)
	if err != nil {
		return err
	}
	//删除数据库数据
	err = dao.Workflow.DelById(id)
	if err != nil {
		return err
	}

	return
}

//删除k8s资源 deployment service ingress
func delWorkflowRes(workflow *model.Workflow) (err error) {
	err = Deployment.DeleteDeployment(workflow.Name, workflow.Namespace)
	if err != nil {
		return err
	}
	err = Servicev1.DeleteService(getServiceName(workflow.Name), workflow.Namespace)
	if err != nil {
		return err
	}

	if workflow.Type == "Ingress" {
		err = Ingress.DeleteIngress(getIngressName(workflow.Name), workflow.Namespace)
		if err != nil {
			return err
		}
	}

	return nil
}

controller/workflow.go

package controller

import (
	"k8s-platform/service"
	"net/http"

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

var Workflow workflow

type workflow struct{}

// 获取列表分页查询
func (w *workflow) GetList(ctx *gin.Context) {
	params := new(struct {
		Name      string `form:"name"`
		Namespace string `form:"namespace"`
		Page      int    `form:"page"`
		Limit     int    `form:"limit"`
	})
	if err := ctx.Bind(params); err != nil {
		logger.Error("Bind请求参数失败, " + err.Error())
		ctx.JSON(http.StatusInternalServerError, gin.H{
			"msg":  err.Error(),
			"data": nil,
		})
		return
	}

	data, err := service.Workflow.GetList(params.Name, params.Namespace, params.Limit, params.Page)
	if err != nil {
		logger.Error("获取Workflow列表失败, " + err.Error())
		ctx.JSON(http.StatusInternalServerError, gin.H{
			"msg":  err.Error(),
			"data": nil,
		})
		return
	}

	ctx.JSON(http.StatusOK, gin.H{
		"msg":  "获取Workflow列表成功",
		"data": data,
	})
}

// 查询workflow单条数据
func (w *workflow) GetById(ctx *gin.Context) {
	params := new(struct {
		ID int `form:"id"`
	})
	if err := ctx.Bind(params); err != nil {
		logger.Error("Bind请求参数失败, " + err.Error())
		ctx.JSON(http.StatusInternalServerError, gin.H{
			"msg":  err.Error(),
			"data": nil,
		})
		return
	}

	data, err := service.Workflow.GetById(params.ID)
	if err != nil {
		logger.Error("查询Workflow单条数据失败, " + err.Error())
		ctx.JSON(http.StatusInternalServerError, gin.H{
			"msg":  err.Error(),
			"data": nil,
		})
		return
	}

	ctx.JSON(http.StatusOK, gin.H{
		"msg":  "查询Workflow单条数据成功",
		"data": data,
	})
}

// 创建workflow
func (w *workflow) Create(ctx *gin.Context) {
	var (
		wc  = &service.WorkflowCreate{}
		err error
	)

	if err = ctx.ShouldBindJSON(wc); err != nil {
		logger.Error("Bind请求参数dc失败, " + err.Error())
		ctx.JSON(http.StatusInternalServerError, gin.H{
			"msg":  err.Error(),
			"data": nil,
		})
		return
	}
	if err = service.Workflow.CreateWorkFlow(wc); err != nil {
		logger.Error("创建Workflow失败, " + err.Error())
		ctx.JSON(http.StatusInternalServerError, gin.H{
			"msg":  err.Error(),
			"data": nil,
		})
		return
	}

	ctx.JSON(http.StatusOK, gin.H{
		"msg":  "创建Workflow成功",
		"data": nil,
	})

}

// 删除workflow
func (w *workflow) DelById(ctx *gin.Context) {
	params := new(struct {
		ID int `json:"id"`
	})
	if err := ctx.ShouldBindJSON(params); err != nil {
		logger.Error("Bind请求参数失败, " + err.Error())
		ctx.JSON(http.StatusInternalServerError, gin.H{
			"msg":  err.Error(),
			"data": nil,
		})
		return
	}

	if err := service.Workflow.DelById(params.ID); err != nil {
		logger.Error("删除Workflow失败, " + err.Error())
		ctx.JSON(http.StatusInternalServerError, gin.H{
			"msg":  err.Error(),
			"data": nil,
		})
		return
	}

	ctx.JSON(http.StatusOK, gin.H{
		"msg":  "删除Workflow成功",
		"data": nil,
	})
}

配置workflow路由

controller/router.go

package controller

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

// // 初始化router类型对象,首字母大写,用于跨包调用
// var Router router

// // 声明一个router的结构体
// type router struct{}

// func (r *router) InitApiRouter(router *gin.Engine) {
// 	router.GET("/", Index)
// }

// func Index(ctx *gin.Context) {
// 	ctx.JSON(200, gin.H{
// 		"code": 200,
// 		"msg":  "In index",
// 	})
// }

// 实例化router结构体,可使用该对象点出首字母大写的方法(包外调用)
var Router router

// 创建router的结构体
type router struct{}

// // 初始化路由规则,创建测试api接口
// func (r *router) InitApiRouter(router *gin.Engine) {
// 	router.GET("/testapi", func(ctx *gin.Context) {
// 		ctx.JSON(http.StatusOK, gin.H{
// 			"msg":  "testapi success!",
// 			"data": nil,
// 		})
// 	})
// }
// 初始化路由规则
// func (r *router) InitApiRouter(router *gin.Engine) {
// 	router.
// 		GET("/api/k8s/pods", Pod.GetPods).
// 		GET("/api/k8s/pod/detail", Pod.GetPodDetail).
// 		POST("/api/k8s/pods", Pod.DeletePod).
func (r *router) InitApiRouter(router *gin.Engine) {
	router.
		// Pods
		GET("/api/k8s/pods", Pod.GetPods).
		GET("/api/k8s/pod/detail", Pod.GetPodDetail).
		DELETE("/api/k8s/pod/del", Pod.DeletePod).
		PUT("/api/k8s/pod/update", Pod.UpdatePod).
		GET("/api/k8s/pod/container", Pod.GetPodContainer).
		GET("/api/k8s/pod/log", Pod.GetPodLog).
		GET("/api/k8s/pod/numnp", Pod.GetPodNumPerNp).
		//deployment操作
		GET("/api/k8s/deployments", Deployment.GetDeployments).
		GET("/api/k8s/deployment/detail", Deployment.GetDeploymentDetail).
		PUT("/api/k8s/deployment/scale", Deployment.ScaleDeployment).
		DELETE("/api/k8s/deployment/del", Deployment.DeleteDeployment).
		PUT("/api/k8s/deployment/restart", Deployment.RestartDeployment).
		PUT("/api/k8s/deployment/update", Deployment.UpdateDeployment).
		GET("/api/k8s/deployment/numnp", Deployment.GetDeployNumPerNp).
		POST("/api/k8s/deployment/create", Deployment.CreateDeployment).
		// workflows
		GET("/api/k8s/workflows", Workflow.GetList).
		GET("/api/k8s/workflow/detail", Workflow.GetById).
		POST("/api/k8s/workflow/create", Workflow.Create).
		DELETE("/api/k8s/workflow/del", Workflow.DelById)

}

测试api接口

API开发:跨域、JWT Token验证

7、中间件

7.1 什么是中间件

中间件,英译middleware,顾名思义,放在中间的物件,那么放在谁中间呢?本来,客户端可以直接请求到服务端接口。现在,中间件横插一脚它能在请求到达接口之前拦截请求,做一些特殊处理,比如日志记录,故障处理等

7.2 gin中间件用法

 因为gin的中间件函数与业务逻辑处理函数是放到gin的队列中的,所以当一个中间件函数执行return语句时只代表当前中间件函数执行完了,框架会驱动index++,然后执行队列中后续的中间件函数或逻辑处理函数,当在中间件函数中执行context.Next()时,gin框架也会驱动index++,执行下一个函数。当执行context.Abort()时,会修改c.index =63.5,由于该索引不存在,所以队列中后面的的中间件函数和逻辑处理函数就不会执行了。

(1)定义一个返回值是gin.HandlerFunc的方法
(2)在方法中根据context上下文添加中间件逻辑
(3)中间件逻辑未通过,使用context.Abort()和return停止下个函数的执行
(4)中间件逻辑通过时,使用contextNext()继续执行下个函数
(5)定义好中间件函数后,在main中使用use()将其加入到队列中,注意use一定要在初始化路由的前面,否则不会生效

7.2 Cors跨域

middle/cors.go

代码层直接处理跨域请求,不需要前面再加一层nginx处理,解决前后端域名不同、IP不同甚至端口不同导致的跨域报错。

package middle

import (
	"net/http"

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

func Cors() gin.HandlerFunc {
	return func(ctx *gin.Context) {
		// 获取请求方法
		method := ctx.Request.Method

		// 添加跨域响应头
		ctx.Header("Content-Type", "application/json")
		ctx.Header("Access-Control-Allow-Origin", "*")
		ctx.Header("Access-Control-Max-Age", "86400")
		ctx.Header("Access-Control-Allow-Methods", "POST, GET, OPTIONS, PUT, DELETE, UPDATE")
		ctx.Header("Access-Control-Allow-Headers", "X-Token, Content-Type, Context-Length, Accept-Encoding, X-CSRF-Token, Authorization, X-MAX")
		ctx.Header("Access-Control-Allow-Credentials", "false")

		// 放行OPTIONS方法
		if method == "OPTIONS" {
			ctx.AbortWithStatus(http.StatusNoContent)
		}

		// 处理请求
		ctx.Next()
	}
}

在main.go中使用这个中间件,在初始化路由之前加

main.go

    // 加载跨域中间件

    r.Use(middle.Cors())
package main

import (
	"k8s-platform/config"
	"k8s-platform/controller"
	"k8s-platform/db"
	"k8s-platform/middle"
	"k8s-platform/service"

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

func main() {
	// 初始化k8s client
	service.K8s.Init() // 可以使用service.K8s.clientset 进行跨包调用

	// 初始化数据库
	db.Init()
	// 初始化gin对象/路由配置
	r := gin.Default()
	// 加载跨域中间件
	r.Use(middle.Cors())
	// 初始化路由规则
	controller.Router.InitApiRouter(r)
	// gin程序启动
	r.Run(config.ListenAddr)

	// 关闭数据库
	db.Close()
}

7.3 JWT token验证

验证请求的合法性,前端只有在登录状态下才会生成token,请求时将token放入Header中,后端接收的请求时,先由该中间件验证token是否合法,合法时才放行,继续执行业务函数的逻辑处理。

utils/jwt.go

package utils

import (
	"errors"

	"github.com/dgrijalva/jwt-go"
	"github.com/wonderivan/logger"
)

var JWTToken jwtToken

type jwtToken struct{}

//定义token中携带的信息
type CustomClaims struct {
	Username string `json:"username"`
	Password string `json:"password"`
	jwt.StandardClaims
}

//加解密因子
const (
	SECRET = "adoodevops"
)

//解析Token
func (*jwtToken) ParseToken(tokenString string) (claims *CustomClaims, err error) {
	token, err := jwt.ParseWithClaims(tokenString, &CustomClaims{}, func(token *jwt.Token) (interface{}, error) {
		return []byte(SECRET), nil
	})
	if err != nil {
		logger.Error("parse token failed ", err)
		//处理token解析后的各种错误
		if ve, ok := err.(*jwt.ValidationError); ok {
			if ve.Errors&jwt.ValidationErrorMalformed != 0 {
				return nil, errors.New("TokenMalformed")
			} else if ve.Errors&jwt.ValidationErrorExpired != 0 {
				return nil, errors.New("TokenExpired")
			} else if ve.Errors&jwt.ValidationErrorNotValidYet != 0 {
				return nil, errors.New("TokenNotValidYet")
			} else {
				return nil, errors.New("TokenInvalid")
			}
		}
	}

	if claims, ok := token.Claims.(*CustomClaims); ok && token.Valid {
		return claims, nil
	}
	return nil, errors.New("解析Token失败")
}

middle/jwt.go

package middle

import (
	"k8s-platform/utils"
	"net/http"

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

func JWTAuth() gin.HandlerFunc {
	return func(ctx *gin.Context) {
		// 对登录接口放行
		if len(ctx.Request.URL.String()) >= 10 && ctx.Request.URL.String()[0:10] == "/api/login" {
			ctx.Next()
		} else {
			// 处理验证逻辑
			token := ctx.Request.Header.Get("Authorization")
			if token == "" {
				ctx.JSON(http.StatusBadRequest, gin.H{
					"msg":  "请求未携带token,无权限访问",
					"data": nil,
				})
				ctx.Abort()
				return
			}
			// 解析token内容
			claims, err := utils.JWTToken.ParseToken(token)
			if err != nil {
				// token过期错误
				if err.Error() == "TokenExpired" {
					ctx.JSON(http.StatusBadRequest, gin.H{
						"msg":  "授权已过期",
						"data": nil,
					})
					ctx.Abort()
					return
				}
				// 其他解析错误
				ctx.JSON(http.StatusBadRequest, gin.H{
					"msg":  err.Error(),
					"data": nil,
				})
				ctx.Abort()
				return
			}
			ctx.Set("claims", claims)
			ctx.Next()
		}

	}
}

 在main中使用这个中间件

	// 加载jwt中间件
	r.Use(middle.JWTAuth())

 测试api接口,提示“请求未携带token,无权限访问”。

需要在Header加token才能调接口,防止别人刷接口。

Authrization: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

API开发:Web容器终端

8、WebShell终端 

8.1 kubectl exec原理

8.2 实现思路

通过client-go提供的方法,实现通过网页进入kubernetes pod 终端操作.

  • client-go remotecommand
  • websocket
  • xterm.js

remotecommand
k8s.io/client-go/tools/remotecommand kubernetes client-go 提供的remoteCommand包,提供了方法与集群中的容器建立长连接,并设置容器的 stdin,stdout 等。
remotecommand 包提供基于 SPDY 协议的 Executor interface,进行和 pod 终端的流的传输。初始化一个Executor 很简单,只需要调用remotecommand 的NewSPDYExecutor 并传入对应参数。Exeutor的 Stream 方法,会建立一个流传输的连接,真到服务端和调用端一端关闭连接,才会停止传输,常用的做法是定义一个如下PtyHandler 的interface,然后使用你想用的客户端实现该 interface 对应的Read(p []byte) (int,error)和write(p []byte)(int,error)方法即可,调用Stream 方时,只要将 StreamOptions 的 Stdin Stdout 都设为 ptyHandler ,Executor 就会通过你定义的 write 和 read 方法来传输数据。

websocket
github.com/gorilla/websocket 是 go 的一个websocket 实现,提供了全面的 websocket 相关的方法,这里使用它来实现上面所说的PtyHandler 接口。

首先定义一个TerminalSession 类,该类包含一个“websocket.Conn ,通过 websocket 连接实现PtyHandler 接口的读写方法,Next方法在 remotecommand 执行过程中会被调用
xterm.js
前端页面使用xterm.is进行模拟terminal展示,只要avascript 监听Terminal 对象的对事件及 websocket 连接的事件,进行对应的页面展示和消息推送就可以了

8.3 代码实现

(1)处理终端交互

service/terminal.go

package service

import (
	"encoding/json"
	"errors"
	"fmt"
	"k8s-platform/config"
	"log"
	"net/http"
	"time"

	"github.com/gorilla/websocket"
	"github.com/wonderivan/logger"
	v1 "k8s.io/api/core/v1"
	"k8s.io/client-go/kubernetes/scheme"
	"k8s.io/client-go/tools/clientcmd"
	"k8s.io/client-go/tools/remotecommand"
)

var Terminal terminal

type terminal struct{}

// wshanlder
func (t *terminal) WsHandler(w http.ResponseWriter, r *http.Request) {
	//加载k8s配置
	conf, err := clientcmd.BuildConfigFromFlags("", config.KubeConfig)
	if err != nil {
		logger.Error("加载k8s配置失败, " + err.Error())
		return
	}
	//解析form入参,获取namespace,pod,container参数
	if err := r.ParseForm(); err != nil {
		logger.Error("解析参数失败, " + err.Error())
		return
	}
	namespace := r.Form.Get("namespace")
	podName := r.Form.Get("pod_name")
	containerName := r.Form.Get("container_name")
	logger.Info("exec pod: %s, container: %s, namespace: %s\n", podName, containerName, namespace)

	//new一个terminalsession
	pty, err := NewTerminalSession(w, r, nil)
	if err != nil {
		logger.Error("实例化TerminalSession失败, " + err.Error())
		return
	}
	//处理关闭
	defer func() {
		logger.Info("关闭TerminalSession")
		pty.Close()
	}()
	//组装post请求
	req := K8s.ClientSet.CoreV1().RESTClient().Post().
		Resource("pods").
		Name(podName).
		Namespace(namespace).
		SubResource("exec").
		VersionedParams(&v1.PodExecOptions{
			Stdin:     true,
			Stdout:    true,
			Stderr:    true,
			TTY:       true,
			Container: containerName,
			Command:   []string{"/bin/bash"},
		}, scheme.ParameterCodec)
	logger.Info("exec post request url: ", req)

	//升级SPDY协议
	executor, err := remotecommand.NewSPDYExecutor(conf, "POST", req.URL())
	if err != nil {
		logger.Error("建立SPDY连接失败, " + err.Error())
		return
	}
	//与kubelet建立stream连接
	err = executor.Stream(remotecommand.StreamOptions{
		Stdin:             pty,
		Stdout:            pty,
		Stderr:            pty,
		Tty:               true,
		TerminalSizeQueue: pty,
	})

	if err != nil {
		logger.Error("执行 pod 命令失败, " + err.Error())
		//将报错返回给web端
		pty.Write([]byte("执行 pod 命令失败, " + err.Error()))
		//标记关闭
		pty.Done()
	}
}

// 消息内容
type terminalMessage struct {
	Operation string `json:"operation"`
	Data      string `json:"data"`
	Rows      uint16 `json:"rows"`
	Cols      uint16 `json:"cols"`
}

// 交互的结构体,接管输入和输出
type TerminalSession struct {
	wsConn   *websocket.Conn
	sizeChan chan remotecommand.TerminalSize
	doneChan chan struct{}
}

// 初始化一个websocket.Upgrader类型的对象,用于http协议升级为ws协议
var upgrader = func() websocket.Upgrader {
	upgrader := websocket.Upgrader{}
	upgrader.HandshakeTimeout = time.Second * 2
	upgrader.CheckOrigin = func(r *http.Request) bool {
		return true
	}
	return upgrader
}()

// 创建TerminalSession类型的对象并返回
func NewTerminalSession(w http.ResponseWriter, r *http.Request, responseHeader http.Header) (*TerminalSession, error) {
	//升级ws协议
	conn, err := upgrader.Upgrade(w, r, responseHeader)
	if err != nil {
		return nil, errors.New("升级websocket失败," + err.Error())
	}
	//new
	session := &TerminalSession{
		wsConn:   conn,
		sizeChan: make(chan remotecommand.TerminalSize),
		doneChan: make(chan struct{}),
	}

	return session, nil
}

// 读数据的方法
// 返回值int是读成功了多少数据
func (t *TerminalSession) Read(p []byte) (int, error) {
	//从ws中读取消息
	_, message, err := t.wsConn.ReadMessage()
	if err != nil {
		log.Printf("读取消息错误: %v", err)
		return 0, err
	}
	//反序列化
	var msg terminalMessage
	if err := json.Unmarshal(message, &msg); err != nil {
		log.Printf("读取消息语法错误: %v", err)
		return 0, err
	}
	//逻辑判断
	switch msg.Operation {
	case "stdin":
		return copy(p, msg.Data), nil
	case "resize":
		t.sizeChan <- remotecommand.TerminalSize{Width: msg.Cols, Height: msg.Rows}
		return 0, nil
	case "ping":
		return 0, nil
	default:
		log.Printf("消息类型错误'%s'", msg.Operation)
		return 0, fmt.Errorf("消息类型错误'%s'", msg.Operation)
	}
}

// 写数据的方法,拿到apiserver的返回内容,向web端输出
func (t *TerminalSession) Write(p []byte) (int, error) {
	msg, err := json.Marshal(terminalMessage{
		Operation: "stdout",
		Data:      string(p),
	})
	if err != nil {
		log.Printf("写消息语法错误: %v", err)
		return 0, err
	}
	if err := t.wsConn.WriteMessage(websocket.TextMessage, msg); err != nil {
		log.Printf("写消息错误: %v", err)
		return 0, err
	}

	return len(p), nil
}

// 标记关闭的方法
func (t *TerminalSession) Done() {
	close(t.doneChan)
}

// 关闭的方法
func (t *TerminalSession) Close() {
	t.wsConn.Close()
}

// resize方法,以及是否退出终端
func (t *TerminalSession) Next() *remotecommand.TerminalSize {
	select {
	case size := <-t.sizeChan:
		return &size
	case <-t.doneChan:
		return nil
	}
}

 (2)由于会将http升级为websocket协议,故需要重新监听个端口

在main.go中启动websocket,写在启动gin server前面是为什么?启动websocket是异步方法,写在启动gin server后面执行不到。

启动gin server方法 r.Run(config.ListenAddr)阻塞一直监听

(3)websocket测试

测试ws ws://localhost:8081/ws?pod_name=xxx&container_name=xxx&namespace=default

发送消息 {"operation":"stdin","data":"ls -l","rows":0,"cols":0}

发送消息 {"operation":"stdin","data":"\r","rows":0,"cols":0}

9、总结

至此,K8s管理系统后端代码开发完毕,基本上开发的内容都是k8s中的原生功能,没有较为复杂的代码透辑,旨在借助K8s项目,逐渐掌握开发思路与技巧,做一个go+gin项目开发的实战入门。能够独立完成脚本/接口的开发,以及基于此项目开发更多的新功能。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

K8S管理系统项目实战[API开发]-2 的相关文章

  • nslookup 无法在最新的 busybox 上获取服务 ip

    重现步骤 kubectl run busybox1 generator run pod v1 image busybox 1 28 sleep 3600 kubectl run busybox2 generator run pod v1 i
  • 从动态服务器中抓取 html 列表数据

    哈喽大家好 抱歉提出转储问题 这是我最后的手段 我发誓我尝试了无数其他 Stackoverflow 问题 不同的框架等 但这些似乎没有帮助 我有以下问题 一个网站显示一个数据列表 前面有大量的 div li span 等标签 它是一个很大的
  • Windows 持久卷上的 Kubernetes

    Windows minikube 是否支持带有主机路径的持久卷 如果是这样 语法是什么 I tried apiVersion v1 kind PersistentVolume metadata name kbmongo002 labels
  • 向下滚动时如何使图像移动?

    这是我想要实现的目标的示例 https www flambette com en https www flambette com en 我尝试过更改图像的 css 属性 但效果不能满足我的需求 我尝试过以下代码 mydocument on
  • 在 Kubernetes 中连接前端和后端

    我的前端设置为 this http post
  • 为什么ReadWriteOnce在不同的节点上工作?

    我们在 K8s 上运行的平台有不同的组件 我们需要在其中两个组件 comp A 和 comp B 之间共享存储 但我们错误地将 PV 和 PVC 定义为ReadWriteOnce即使这两个组件在不同的节点上运行 一切都正常 我们能够从两个组
  • 如何生成大型网站的图形站点地图[关闭]

    Closed 这个问题正在寻求书籍 工具 软件库等的推荐 不满足堆栈溢出指南 help closed questions 目前不接受答案 我想为我的网站生成图形站点地图 据我所知 有两个阶段 抓取网站并分析链接关系 提取树形结构 生成视觉上
  • Istio:RequestAuthentication jwksUri 无法解析内部服务名称

    Notice 其根本原因与Istio 当我启用 JWT RequestAuthentication 时 运行状况检查 sidecar 失败 https stackoverflow com questions 66446178 istio h
  • 如果谷歌的主页如此之小,为什么源代码只有数百行代码? [关闭]

    Closed 这个问题是基于意见的 help closed questions 目前不接受答案 代码被缩小了 但重新格式化后只有几百行代码 我想象这样一个最小的页面也有最少的代码 源代码这么长 Google 到底在做什么 我可以看到很多都是
  • Kubernetes:没有定义端口的服务

    我想确认我们是否可以在 pod 中没有容器端口的情况下公开服务 在下面的示例中 pod 没有容器端口 但服务已定义端口 apiVersion v1 metadata name mypod namespace ggckad s8 labels
  • Kubernetes 服务 IP 会变化吗?

    我对 kubernetes docker 非常陌生 所以如果这是一个愚蠢的问题 我深表歉意 我有一个正在访问一些服务的 Pod 在我的容器中 我正在运行 python 脚本并需要访问该服务 目前我正在使用服务的 IP 地址来执行此操作 服务
  • 如何在minikube中创建多个集群

    我需要在 minikube 中创建额外的集群 我搜索了一段时间没有找到任何这方面的资源 如何在 minikube 中创建集群 创建第一个名为cluster 1 minikube start p cluster 1 创建第二个集群 名称为cl
  • 如何使用 nginx 入口控制器仅允许每个 pod 建立一个连接

    我的 Kubernetes 集群使用副本集来运行 N 个相似的 pod 由于资源限制 每个 Pod 只能处理一个 WebSocket 连接 我的 Kubernetes 使用 nginx 入口控制器 有没有办法让 nginx 每个 pod 仅
  • kubernetes master 的 x509 证书无效

    我正在尝试从我的工作站访问我的 k8s master 我可以从 LAN 很好地访问主站 但不能从我的工作站访问 错误信息是 kubectl context employee context get pods Unable to connec
  • kubernetes 上的 gitlab-ci 缓存与 minio-service 不再工作

    我正在运行 gitlab 10 4 3 和 gitlab runner 10 4 0 作为 kubernetes 部署 带有 kubernetes runner 和一个用于缓存的 minio server 我是按照安装的gitlab 文档
  • 列出命名空间中的所有资源

    我想查看命名空间中的所有资源 Doing kubectl get all尽管有名称 但不会列出服务和入口等内容 如果我知道类型 我可以明确要求该特定类型 但似乎也没有用于列出所有可能类型的命令 尤其kubectl get例如 不列出自定义类
  • kubernetes + coreos 集群 - 替换证书

    我有一个 coreos kubernetes 集群 我是按照这篇文章开始的 AWS 上的 kubernetes coreos 集群 https coreos com kubernetes docs latest kubernetes on
  • 开发工具在表达式上中断

    当给定的 Javascript 表达式为 true 时 我想暂停一切 我见过条件断点 但这对我不起作用 因为这需要首先在某处设置断点 或者首先基于某个标准 然后向其添加附加条件 相反 我想要的是能够在给定表达式为真时中断 无论在哪里 在 D
  • kubernetes kubectl 中的生成器是什么?

    当我想通过运行生成 yaml 时kubectl 它表示我应该表示 generator something命令内的标志 例如 要通过以下方式获取部署模板kubectl 我应该运行以下命令 kubectl run generator deplo
  • 如何在不手动修改文件的情况下编辑部署?

    我已经为我的应用程序定义了一个部署 apiVersion extensions v1beta1 kind Deployment metadata name myapp deployment spec replicas 2 template

随机推荐

  • Spring 根据Bean注册的名称获取Bean对象

    根据Bean注册的名称获取Bean对象 一个通过Bean名称获取Bean的对象实例的一个类 现在复习下Spring 再此处记录下 package net shopxx util import org springframework bean
  • 二叉树层次遍历如何判断当前结点是哪层的?

    二叉树层次遍历就是按每层从左到右 一般是从左到右 若想从右到左也很简单 的次序遍历结点 下面是一个简单的例子 这棵二叉树层次遍历的结果是 1 2 3 4 5 实现层次遍历一般是用队列 思路还是比较简单 1 首先把根结点入队 2 若队列不为空
  • mac 完全卸载python

    这里主要是卸载pkg安装的python 第一步 删除框架 sudo rm rf Library Frameworks Python framework Versions 3 11 第二步 删除应用目录 sudo rm rf Applicat
  • 解决Module not found: Error: ‘element-plus/lib/theme-chalk/index.css‘,通过下载插件,使用的是vue ui项目仪表盘

    1 首先在package json中查看vue版本和element ui版本 2 找到element ui官网https element eleme cn zh CN component quickstart 点击element ui 3
  • STM32定时器-基本定时器

    目录 定时器分类 基本定时器功能框图讲解 基本定时器功能 时钟源 计数器时钟 计数器 自动重装载寄存器 定时时间的计算 定时器初始化结构体详解 实验 定时器分类 STM32F1 系列中 除了互联型的产品 共有 8 个定时器 分为基本定时器
  • 初识Electron开发桌面应用

    Electron是什么 Electron 基于 Chromium 和 Node js 让你可以使用 HTML CSS 和 JavaScript 构建跨平台 mac window linux 桌面应用 Electron开发环境的搭建 首先安装
  • 数据预测之BP神经网络具体应用以及matlab代码(转)

    1 具体应用实例 根据表2 预测序号15的跳高成绩 表2 国内男子跳高运动员各项素质指标 序号 跳高成绩 30行进跑 s 立定三级跳远 助跑摸高 助跑4 6步跳高 负重深蹲杠铃 杠铃半蹲系数 100 s 抓举 1 2 24 3 2 9 6
  • 优先队列(堆)

    设计一个程序模仿操作系统的进程管理问题 进 程服务按优先级高的先服务 同优先级的先到先服务的管理 原则 设文件task txt中存放了仿真进程服务请求 其中第 一列是进程任务号 第二列是进程的优先级 1 30 2 20 3 40 4 20
  • React 与Spring Json通信

    前端 post方法 对应post请求 param String url 请求的url地址 param Object params 请求时携带的参数 param Boolean json true json格式请求头 false FormDa
  • Nothing to be done for `install-exec-am' while compile zabbix

    编译zabbix报错 make 3 Leaving directory home sysadmin zabbix zabbix 1 8 src libs zbxjson make 3 Entering directory home sysa
  • 利用scrapy框架进行数据的爬取

    首先介绍一下什么是scrapy框架 爬虫中封装好的一个明星框架 代表性功能 高性能的持久化存储 异步的数据下载 高性能的数据解析 分布式 Scrapy是一个为了爬取网站数据 提取结构性数据而编写的应用框架 非常出名 非常强悍 其内部已经被集
  • 《UE4开发笔记》Tip 0 Rider的五个必备技巧

    在UE4的C 工程和源码开发中 过去常使用Visual Studio系列来作为IDE 但VS始终对UE4工程支持的并不友好 小番茄Visual Assist X系列插件的安装也是十分玄学 时不时就失灵 JetBrains推出了Rider F
  • Selenium WebDriver使用IE浏览器

    IEdriver插件下载地址 http www cr173 com soft 195732 html 1 配置系统环境变量 我的电脑 右键 属性 高级系统设置 环境变量 系统变量 path 注 先把IEDriverServer exe放在C
  • HTML <title> 标签

    实例 一个简单的 HTML 文档 带有尽可能少的必需的标签 The content of the document 定义和用法
  • Elasticsearch概述和DSL查询总结

    目录 Elasticsearch概述 1 什么是Elasticsearch 2 作用 3 特点 DSL Domain Specifit Language 特定领域语言 概念和作用 查询代码总结 最后附项目准备 1 创建搜索工程 maven工
  • Jmeter Springboot Redisson分布式锁并发订单操作

    Jmeter Springboot Redisson分布式锁并发订单操作 下单 取消单 完成单 加库存 br 涉及知识点 br gt java springboot mybatis开发 br gt redis分布式锁 Redisson客户端
  • Web3 用例全解析:传统品牌加速进入 Web3 的原因?

    Web3 有能力彻底改变品牌和客户相互联系的方式 许多品牌已经在尝试使用 NFT 和元宇宙来提高品牌知名度和消费者忠诚度 这是传统社交媒体和电子商务渠道根本无法做到的 NIKE Panini 和 Vodafone nbsp 是最早认识到 N
  • sql2005中的简单分页方法

    select top 10 from select top 30 ROW NUMBER over order by readerid desc as rows1 from reader as a1 where rows1 gt 21 转载于
  • 教育服务机器人总结

    现在有种新名词 用机器人来服务教育 顾名思义 指的是在人工智能领域 机器人助力中小学学生各课程从研究到开展授课过程 其中需要老师在教学环节中的集体辅助 格物斯坦表示 机器人全面帮助教育业完传道授业解课的作用 是功不可没的 随着市场需求具体化
  • K8S管理系统项目实战[API开发]-2

    后端 go gin 后端代码地址GitHub yunixiangfeng k8s platform K8s管理系统后端 go gin kubernetes v1 24 2 golang v1 18 3 5 存储与配置 5 1 ConfigM