后端: 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项目开发的实战入门。能够独立完成脚本/接口的开发,以及基于此项目开发更多的新功能。