后端: 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)初始化数据库
6.3 Workflow
(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、总结
接口实现
// configmap
type configMapCell corev1.ConfigMapfunc (c configMapCell) GetCreation() time.Time {return c.CreationTimestamp.Time
}func (c configMapCell) GetName() string {return c.Name
}
(1)列表
(2)获取ConfigMap详情
(3)更新ConfigMap
(4) 删除ConfigMap
package serviceimport ("context""encoding/json""errors""github/wonderivan/logger"corev1 "k8s.io/api/core/v1"metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)var ConfigMap configMaptype 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
}
package controllerimport ("k8s-platform/service""net/http""github/gin-gonic/gin""github/wonderivan/logger"
)var ConfigMap configMaptype 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.ShouldBindJSONif 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.ShouldBindJSONif 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,})
}
添加路由
// // ConfigmapsGET("/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接口
接口实现
// secret
type secretCell corev1.Secretfunc (s secretCell) GetCreation() time.Time {return s.CreationTimestamp.Time
}func (s secretCell) GetName() string {return s.Name
}
(1)列表
(2)获取Secret详情
(3)更新Secret
(4) 删除Secret
package serviceimport ("context""encoding/json""errors""github/wonderivan/logger"corev1 "k8s.io/api/core/v1"metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)var Secret secrettype 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
}
package controllerimport ("k8s-platform/service""net/http""github/gin-gonic/gin""github/wonderivan/logger"
)var Secret secrettype 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.ShouldBindJSONif 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.ShouldBindJSONif 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,})
}
定义路由
// secretGET("/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接口
接口实现
// pvc
type pvcCell corev1.PersistentVolumeClaimfunc (p pvcCell) GetCreation() time.Time {return p.CreationTimestamp.Time
}func (p pvcCell) GetName() string {return p.Name
}
(1)列表
(2)获取Pvc详情
(3)更新Pvc
(4) 删除Pvc
package serviceimport ("context""encoding/json""errors""github/wonderivan/logger"corev1 "k8s.io/api/core/v1"metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)var Pvc pvctype 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
}
package controllerimport ("k8s-platform/service""net/http""github/gin-gonic/gin""github/wonderivan/logger"
)var Pvc pvctype 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.ShouldBindJSONif 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.ShouldBindJSONif 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,})
}
添加路由
//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接口
(1)初始化数据库
package dbimport ("fmt""k8s-plantform/config""time""github/wonderivan/logger""github/jinzhu/gorm"_ "github/jinzhu/gorm/dialects/mysql"
)var (isInit boolGORM *gorm.DBerr 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 = truelogger.Info("数据库初始化成功")
}// 关闭数据库连接
func Close() error {return GORM.Close()
}
加数据库配置
package configimport "time"const (ListenAddr = "0.0.0.0:9090"KubeConfig = "C:\Users\Administrator\.kube\config"// tail的日志行数// tail -n 2000PodLogTailLine = 2000// DB ConfigDbType = "mysql"DbHost = "192.168.204.129"DbPort = 3306DbName = "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
设置一个连接被使用的最长时间,即过了一段时间后会被强制回收,理论上这可以有效减少不可用连接出现的概率,当数据库方面也设置了连接的超时时间时,这个值应当不超过数据库的超时参数值。
初始化
package mainimport ("k8s-platform/config""k8s-platform/controller""k8s-platform/db""k8s-platform/service""github/gin-gonic/gin"
)func main() {// 初始化k8s clientservice.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:UsersAdministratorDesktopk8s-platform> go
2023-05-07 10:37:11 [INFO] [C:/Users/Administrator/Desktop/k8s-platform/:26] 获取K8s配置成功!
2023-05-07 10:37:11 [INFO] [C:/Users/Administrator/Desktop/k8s-platform/:33] 创建K8s client 成功!
2023-05-07 10:37:11 [INFO] [C:/Users/Administrator/Desktop/k8s-platform/: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 /github/gin-gonic/gin#readme-don-t-trust-all-proxies for details.
[GIN-debug] Listening and serving HTTP on 0.0.0.0:9090
(2)建立表的映射关系
表结构
package modelimport "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)数据库创建表
dbworkflow.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.Workflowtotal 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 daoimport ("errors""k8s-platform/db""k8s-platform/model""github/wonderivan/logger"
)var Workflow workflowtype workflow struct{}//定义列表的返回内容,Items是workflow元素列表,Total为workflow元素数量
type WorkflowResp struct {Items []*model.Workflow `json:"items"`Total int `json:"total"`
}
package serviceimport ("k8s-platform/dao""k8s-platform/model"
)var Workflow workflowtype 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 stringif 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) {//创建deploymentdc := &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 stringif data.Type != "Ingress" {serviceType = data.Type} else {serviceType = "ClusterIP"}//创建servicesc := &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}//创建ingressvar ic *IngressCreateif 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
}
package controllerimport ("k8s-platform/service""net/http""github/gin-gonic/gin""github/wonderivan/logger"
)var Workflow workflowtype 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路由
package controllerimport ("github/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.// PodsGET("/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).// workflowsGET("/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接口
中间件,英译middleware,顾名思义,放在中间的物件,那么放在谁中间呢?本来,客户端可以直接请求到服务端接口。现在,中间件横插一脚它能在请求到达接口之前拦截请求,做一些特殊处理,比如日志记录,故障处理等
因为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一定要在初始化路由的前面,否则不会生效
代码层直接处理跨域请求,不需要前面再加一层nginx处理,解决前后端域名不同、IP不同甚至端口不同导致的跨域报错。
package middleimport ("net/http""github/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()}
}
在中使用这个中间件,在初始化路由之前加
< // 加载跨域中间件r.Use(middle.Cors())
package mainimport ("k8s-platform/config""k8s-platform/controller""k8s-platform/db""k8s-platform/middle""k8s-platform/service""github/gin-gonic/gin"
)func main() {// 初始化k8s clientservice.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()
}
验证请求的合法性,前端只有在登录状态下才会生成token,请求时将token放入Header中,后端接收的请求时,先由该中间件验证token是否合法,合法时才放行,继续执行业务函数的逻辑处理。
package utilsimport ("errors""github/dgrijalva/jwt-go""github/wonderivan/logger"
)var JWTToken jwtTokentype 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失败")
}
package middleimport ("k8s-platform/utils""net/http""github/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
通过client-go提供的方法,实现通过网页进入kubernetes pod 终端操作.
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/gorilla/websocket 是 go 的一个websocket 实现,提供了全面的 websocket 相关的方法,这里使用它来实现上面所说的PtyHandler 接口。
首先定义一个TerminalSession 类,该类包含一个“websocket.Conn ,通过 websocket 连接实现PtyHandler 接口的读写方法,Next方法在 remotecommand 执行过程中会被调用
xterm.js
前端页面使用xterm.is进行模拟terminal展示,只要avascript 监听Terminal 对象的对事件及 websocket 连接的事件,进行对应的页面展示和消息推送就可以了
(1)处理终端交互
package serviceimport ("encoding/json""errors""fmt""k8s-platform/config""log""net/http""time""github/gorilla/websocket""github/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 terminaltype 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: %sn", podName, containerName, namespace)//new一个terminalsessionpty, 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.ConnsizeChan chan remotecommand.TerminalSizedoneChan chan struct{}
}// 初始化一个websocket.Upgrader类型的对象,用于http协议升级为ws协议
var upgrader = func() websocket.Upgrader {upgrader := websocket.Upgrader{}upgrader.HandshakeTimeout = time.Second * 2upgrader.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())}//newsession := &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 terminalMessageif err := json.Unmarshal(message, &msg); err != nil {log.Printf("读取消息语法错误: %v", err)return 0, err}//逻辑判断switch msg.Operation {case "stdin":return copy(p, msg.Data), nilcase "resize":t.sizeChan <- remotecommand.TerminalSize{Width: msg.Cols, Height: msg.Rows}return 0, nilcase "ping":return 0, nildefault: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 &sizecase <-t.doneChan:return nil}
}
(2)由于会将http升级为websocket协议,故需要重新监听个端口
在中启动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}
至此,K8s管理系统后端代码开发完毕,基本上开发的内容都是k8s中的原生功能,没有较为复杂的代码透辑,旨在借助K8s项目,逐渐掌握开发思路与技巧,做一个go+gin项目开发的实战入门。能够独立完成脚本/接口的开发,以及基于此项目开发更多的新功能。
本文发布于:2024-02-02 15:28:21,感谢您对本站的认可!
本文链接:https://www.4u4v.net/it/170685890244711.html
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,我们将在24小时内删除。
留言与评论(共有 0 条评论) |