应用部署 原创
1、功能
2、数据定义
2.1、定义数据类型
在 internal/app/models/k8s_helm.go
文件中增加K8sHelmRelease
的Models
,并实现对数据库的操作,如下:
go
package models
import "gorm.io/gorm"
type K8sHelmRelease struct {
ReleaseName string `json:"release_name"`
Status string `json:"status"`
Namespace string `json:"namespace"`
ChartName string `json:"chart_name"`
ChartVersion string `json:"chart_version"`
*Base
}
func (k *K8sHelmRelease) TableName() string {
return "k8s_helm"
}
// Create 插入数据
func (k *K8sHelmRelease) Create(db *gorm.DB) error {
return db.Create(&k).Error
}
// GetByName 根据集群名获取集群信息
func (k *K8sHelmRelease) GetByName(db *gorm.DB) (*K8sHelmRelease, error) {
var kc *K8sHelmRelease
if k.ReleaseName != "" {
db.Where("release_name =? and is_del=?", k.ReleaseName, 0).First(&kc)
}
return kc, nil
}
// GetByID 通过ID获取集群信息
func (k *K8sHelmRelease) GetByID(db *gorm.DB) (*K8sHelmRelease, error) {
var kc *K8sHelmRelease
db.Where("id =? and is_del=?", k.ID, 0).First(&kc)
return kc, nil
}
// List 列出集群信息
func (k *K8sHelmRelease) List(db *gorm.DB, page, limit int) ([]*K8sHelmRelease, error) {
var (
kc []*K8sHelmRelease
err error
)
startIndex := (page - 1) * limit
db = db.Offset(startIndex).Limit(limit)
if k.ReleaseName != "" {
db = db.Where("release_name=?", k.ReleaseName)
}
if err = db.Where("is_del = ?", 0).Find(&kc).Error; err != nil {
return nil, err
}
return kc, nil
}
// Update 更新数据
func (k *K8sHelmRelease) Update(db *gorm.DB, values interface{}) error {
if err := db.Model(k).Where("id = ? AND is_del = ?", k.ID, 0).First(&K8sHelmRelease{}).Updates(values).Error; err != nil {
return err
}
return nil
}
// Delete 删除数据
func (k *K8sHelmRelease) Delete(db *gorm.DB) error {
var kc K8sHelmRelease
if err := db.Where("id=? AND is_del=?", k.ID, 0).First(&kc).Error; err != nil {
return err
}
kc.IsDel = 1
if err := db.Updates(&kc).Error; err != nil {
return err
}
return nil
}
// Save 保存数据
func (k *K8sHelmRelease) Save(db *gorm.DB) error {
return db.Save(k).Error
}
2.2、实现增删改查
在internal/app/dao/k8s_helm.go
文件,输入以下内容,实现对K8sHelmRelease
的数据操作。
go
package dao
import (
"github.com/joker-bai/hawkeye/internal/app/requests"
"time"
"github.com/joker-bai/hawkeye/internal/app/models"
)
// K8sHelmReleaseCreate 创建集群
func (d *Dao) K8sHelmReleaseCreate(param *requests.K8sHelmReleaseCreateRequest) error {
nowTime := uint32(time.Now().Unix())
kc := models.K8sHelmRelease{
Base: &models.Base{
CreatedAt: nowTime,
ModifiedAt: nowTime,
IsDel: 0,
},
ReleaseName: param.ReleaseName,
Namespace: param.Namespace,
ChartName: param.ChartName,
ChartVersion: param.ChartVersion,
Status: param.Status,
}
return kc.Create(d.engine)
}
// K8sHelmReleaseGetByName 通过集群名称获取集群
func (d *Dao) K8sHelmReleaseGetByName(name string) (*models.K8sHelmRelease, error) {
kc := models.K8sHelmRelease{
ReleaseName: name,
}
return kc.GetByName(d.engine)
}
func (d *Dao) K8sHelmReleaseGetById(id uint32) (*models.K8sHelmRelease, error) {
kc := models.K8sHelmRelease{
Base: &models.Base{
ID: id,
},
}
return kc.GetByID(d.engine)
}
// K8sHelmReleaseList 列出集群信息
func (d *Dao) K8sHelmReleaseList(releaseName string, page, limit int) ([]*models.K8sHelmRelease, error) {
kc := models.K8sHelmRelease{
ReleaseName: releaseName,
}
return kc.List(d.engine, page, limit)
}
// K8sHelmReleaseDelete 删除集群信息
func (d *Dao) K8sHelmReleaseDelete(id uint32) error {
kc := models.K8sHelmRelease{
Base: &models.Base{
ID: id,
},
}
return kc.Delete(d.engine)
}
func (d *Dao) K8sHelmReleaseSave(release *models.K8sHelmRelease) error {
return release.Save(d.engine)
}
3、实现 services 方法
3.1、请求参数校验
在 internal/app/requests
目录中新建 k8s_helm.go
文件,写入以下内容以完成请求参数校验:
go
package requests
import (
"github.com/gin-gonic/gin"
"github.com/joker-bai/hawkeye/pkg/app"
"github.com/thedevsaddam/govalidator"
)
type K8sHelmReleaseCreateRequest struct {
ReleaseName string `json:"release_name" form:"release_name" valid:"release_name"`
Status string `json:"status" form:"status" valid:"status"`
Namespace string `json:"namespace" form:"namespace" valid:"namespace"`
ChartName string `json:"chart_name" form:"chart_name" valid:"chart_name"`
ChartVersion string `json:"chart_version" form:"chart_version" valid:"chart_version"`
}
// K8sHelmDeployRequest 使用Helm部署应用
type K8sHelmDeployRequest struct {
ReleaseName string `json:"release_name" form:"release_name" valid:"release_name"`
ClusterName string `json:"cluster_name" form:"cluster_name" valid:"cluster_name"`
ChartName string `json:"chart_name" form:"chart_name" valid:"chart_name"`
Namespace string `json:"namespace" form:"namespace" valid:"namespace"`
}
func ValidK8sHelmDeployRequest(data interface{}, ctx *gin.Context) map[string][]string {
rules := govalidator.MapData{
"release_name": []string{"required"},
"cluster_name": []string{"required"},
"chart_name": []string{"required"},
"namespace": []string{"required"},
}
messages := govalidator.MapData{
"release_name": []string{
"required: 应用名为必填字段,字段为 release_name",
},
"cluster_name": []string{
"required: K8s集群名为必填项,字段为 cluster_name",
},
"chart_name": []string{
"required: Chart名为必填字段,字段为 chart_name",
},
"namespace": []string{
"required: 命名空间为必填字段,字段为 namespace",
},
}
// 校验入参
return app.ValidateOptions(data, rules, messages)
}
type K8sHelmUpdateRequest struct {
ReleaseName string `json:"release_name" form:"release_name" valid:"release_name"`
ClusterName string `json:"cluster_name" form:"cluster_name" valid:"cluster_name"`
ChartName string `json:"chart_name" form:"chart_name" valid:"chart_name"`
Namespace string `json:"namespace" form:"namespace" valid:"namespace"`
ValueYaml string `json:"value_yaml" valid:"value_yaml"`
}
func ValidK8sHelmUpdateRequest(data interface{}, ctx *gin.Context) map[string][]string {
rules := govalidator.MapData{
"release_name": []string{"required"},
"cluster_name": []string{"required"},
"chart_name": []string{"required"},
"namespace": []string{"required"},
"value_yaml": []string{"required"},
}
messages := govalidator.MapData{
"release_name": []string{
"required: 应用名为必填字段,字段为 release_name",
},
"cluster_name": []string{
"required: K8s集群名为必填项,字段为 cluster_name",
},
"chart_name": []string{
"required: Chart名为必填字段,字段为 chart_name",
},
"namespace": []string{
"required: 命名空间为必填字段,字段为 namespace",
},
"value_yaml": []string{
"required: Helm Value为必填字段,字段为 value_yaml",
},
}
// 校验入参
return app.ValidateOptions(data, rules, messages)
}
type K8sHelmListRequest struct {
ReleaseName string `json:"release_name,omitempty" valid:"release_name"`
Page int `json:"page,omitempty" form:"page" valid:"page"`
Limit int `json:"limit,omitempty" form:"limit" valid:"limit"`
}
func ValidK8sHelmListRequest(data interface{}, ctx *gin.Context) map[string][]string {
rules := govalidator.MapData{
"page": []string{"required"},
"limit": []string{"required"},
}
messages := govalidator.MapData{
"page": []string{
"required: 页数不能为空",
},
"limit": []string{
"required: 每页条数不能为空",
},
}
// 校验入参
return app.ValidateOptions(data, rules, messages)
}
type K8sHelmReleaseDeleteRequest struct {
ClusterName string `json:"cluster_name" valid:"cluster_name"`
Namespace string `json:"namespace" valid:"namespace"`
ReleaseName string `json:"release_name" valid:"release_name"`
ChartName string `json:"chart_name" valid:"chart_name"`
}
func ValidK8sHelmReleaseDeleteRequest(data interface{}, ctx *gin.Context) map[string][]string {
rules := govalidator.MapData{
"cluster_name": []string{"required"},
"namespace": []string{"required"},
"release_name": []string{"required"},
"chart_name": []string{"required"},
}
messages := govalidator.MapData{
"cluster_name": []string{
"required: cluster_name不能为空",
},
"namespace": []string{
"required: namespace不能为空",
},
"release_name": []string{
"required: release_name不能为空",
},
"chart_name": []string{
"required: chart_name不能为空",
},
}
// 校验入参
return app.ValidateOptions(data, rules, messages)
}
type K8sHelmGetReleaseValueRequest struct {
ClusterName string `json:"cluster_name" valid:"cluster_name"`
Namespace string `json:"namespace" valid:"namespace"`
ReleaseName string `json:"release_name" valid:"release_name"`
}
func ValidK8sHelmGetReleaseValueRequest(data interface{}, ctx *gin.Context) map[string][]string {
rules := govalidator.MapData{
"cluster_name": []string{"required"},
"release_name": []string{"required"},
"namespace": []string{"required"},
}
messages := govalidator.MapData{
"cluster_name": []string{
"required: cluster_name不能为空",
},
"release_name": []string{
"required: release_name 不能为空",
},
"namespace": []string{
"required: namespace不能为空",
},
}
// 校验入参
return app.ValidateOptions(data, rules, messages)
}
3.2、实现 services 方法
在 internal/app/services/k8s_helm.go
文件中新增 K8sHelmRelease
操作的 services 方法,如下:
go
package services
import (
"github.com/joker-bai/hawkeye/internal/app/models"
"github.com/joker-bai/hawkeye/internal/app/requests"
"github.com/joker-bai/hawkeye/internal/pkg/helm"
"time"
)
func (s *Services) DeployChartToK8s(param *requests.K8sHelmDeployRequest) error {
// 获取集群信息
k8sCluster, err := s.dao.K8sClusterGetByName(param.ClusterName)
if err != nil {
return err
}
// 初始化Helm
helmCli, err := helm.NewClient(param.Namespace, k8sCluster.KubeConfig)
if err != nil {
return err
}
// 从数据库获取chart信息
appMarket, err := s.dao.AppMarketGetByName(param.ChartName)
if err != nil {
return err
}
// 添加helm仓库
if err := helmCli.PublicRepoAdd(appMarket.RepoName, appMarket.RepoURL); err != nil {
return err
}
// 部署
if err := helmCli.InstallOrUpgradeChart(param.ReleaseName, appMarket.ChartURL, param.Namespace, appMarket.AppVersion); err != nil {
return err
}
// 部署完成后将信息存入数据库
save := &requests.K8sHelmReleaseCreateRequest{
ReleaseName: param.ReleaseName,
Status: "已部署",
Namespace: param.Namespace,
ChartName: param.ChartName,
ChartVersion: appMarket.AppVersion,
}
if err := s.dao.K8sHelmReleaseCreate(save); err != nil {
return err
}
return nil
}
func (s *Services) ListReleaseFromK8s(param *requests.K8sHelmListRequest) ([]*models.K8sHelmRelease, error) {
return s.dao.K8sHelmReleaseList(param.ReleaseName, param.Page, param.Limit)
}
func (s *Services) DeleteReleaseFromK8s(param *requests.K8sHelmReleaseDeleteRequest) error {
// 获取集群信息
k8sCluster, err := s.dao.K8sClusterGetByName(param.ClusterName)
if err != nil {
return err
}
// 初始化Helm
helmCli, err := helm.NewClient(param.Namespace, k8sCluster.KubeConfig)
if err != nil {
return err
}
if err := helmCli.UninstallRelease(param.ReleaseName, param.ChartName, param.Namespace); err != nil {
return err
}
// 数据库里标记删除
release, err := s.dao.K8sHelmReleaseGetByName(param.ReleaseName)
if err != nil {
return err
}
if err := s.dao.K8sHelmReleaseDelete(release.ID); err != nil {
return err
}
return nil
}
func (s *Services) UpdateChartToK8s(param *requests.K8sHelmUpdateRequest) error {
// 获取集群信息
k8sCluster, err := s.dao.K8sClusterGetByName(param.ClusterName)
if err != nil {
return err
}
// 初始化Helm
helmCli, err := helm.NewClient(param.Namespace, k8sCluster.KubeConfig)
if err != nil {
return err
}
// 从数据库获取chart信息
appMarket, err := s.dao.AppMarketGetByName(param.ChartName)
if err != nil {
return err
}
// 添加helm仓库
if err := helmCli.PublicRepoAdd(appMarket.RepoName, appMarket.RepoURL); err != nil {
return err
}
// 部署
if err := helmCli.UpgradeChartByValueYaml(param.ReleaseName, appMarket.ChartURL, param.Namespace, appMarket.AppVersion, param.ValueYaml); err != nil {
return err
}
// 更新完成后同步更新数据库
nowTime := uint32(time.Now().Unix())
release, err := s.dao.K8sHelmReleaseGetByName(param.ReleaseName)
if err != nil {
return err
}
release.ModifiedAt = nowTime
if err := s.dao.K8sHelmReleaseSave(release); err != nil {
return err
}
return nil
}
func (s *Services) GetReleaseValueFromK8s(param *requests.K8sHelmGetReleaseValueRequest) (map[string]interface{}, error) {
// 获取集群信息
k8sCluster, err := s.dao.K8sClusterGetByName(param.ClusterName)
if err != nil {
return nil, err
}
// 初始化Helm
helmCli, err := helm.NewClient(param.Namespace, k8sCluster.KubeConfig)
if err != nil {
return nil, err
}
return helmCli.GetReleaseValue(param.ReleaseName)
}
4、新增 controllers 方法
在 internal/app/controllers/api/v1/k8s 目录中新增 helm.go 文件,实现如下方法:
go
package k8s
import (
"encoding/json"
"github.com/gin-gonic/gin"
"github.com/joker-bai/hawkeye/global"
"github.com/joker-bai/hawkeye/internal/app/requests"
"github.com/joker-bai/hawkeye/internal/app/services"
"github.com/joker-bai/hawkeye/pkg/app"
"github.com/joker-bai/hawkeye/pkg/errorcode"
"go.uber.org/zap"
)
type HelmController struct{}
// List godoc
// @Summary 列出K8s中Helm Release
// @Description 列出K8s中Helm Release
// @Tags K8s中Helm Release管理
// @Produce json
// @Param helm_name query string false "K8s中Helm Release名" maxlength(100)
// @Param page query int true "页码"
// @Param limit query int true "每页数量"
// @Success 200 {object} string "成功"
// @Failure 400 {object} errorcode.Error "请求错误"
// @Failure 500 {object} errorcode.Error "内部错误"
// @Router /api/v1/k8s/helm/list [get]
func (u *HelmController) List(ctx *gin.Context) {
param := requests.K8sHelmListRequest{}
response := app.NewResponse(ctx)
if ok := app.Validate(ctx, ¶m, requests.ValidK8sHelmListRequest); !ok {
return
}
svc := services.New(ctx)
list, err := svc.ListReleaseFromK8s(¶m)
if err != nil {
global.Log.Error("列出K8s中的Release失败", zap.String("error", err.Error()))
response.ToErrorResponse(errorcode.ErrorK8sHelmListFail)
return
}
response.ToResponseList(list, len(list))
}
// Create godoc
// @Summary 创建K8s中Helm Release
// @Description 创建K8s中Helm Release
// @Tags K8s中Helm Release管理
// @Produce json
// @Param body body requests.K8sHelmCreateRequest true "body"
// @Success 200 {object} string "成功"
// @Failure 400 {object} errorcode.Error "请求错误"
// @Failure 500 {object} errorcode.Error "内部错误"
// @Router /api/v1/k8s/helm/create [post]
func (u *HelmController) Create(ctx *gin.Context) {
param := requests.K8sHelmDeployRequest{}
response := app.NewResponse(ctx)
if ok := app.Validate(ctx, ¶m, requests.ValidK8sHelmDeployRequest); !ok {
return
}
svc := services.New(ctx)
err := svc.DeployChartToK8s(¶m)
if err != nil {
global.Log.Error("在K8s中部署Chart失败", zap.String("error", err.Error()))
response.ToErrorResponse(errorcode.ErrorK8sHelmCreateFail)
return
}
response.ToResponse(gin.H{
"data": "在K8s中部署Chart成功",
})
}
// Update
// @Summary 修改K8s中Helm Release
// @Description 修改K8s中Helm Release
// @Tags K8s中Helm Release管理
// @Produce json
// @Param body body requests.K8sHelmUpdateRequest true "body"
// @Success 200 {object} string "成功"
// @Failure 400 {object} errorcode.Error "请求错误"
// @Failure 500 {object} errorcode.Error "内部错误"
// @Router /api/v1/k8s/helm/update [post]
func (u *HelmController) Update(ctx *gin.Context) {
param := requests.K8sHelmUpdateRequest{}
response := app.NewResponse(ctx)
if ok := app.Validate(ctx, ¶m, requests.ValidK8sHelmUpdateRequest); !ok {
return
}
svc := services.New(ctx)
if err := svc.UpdateChartToK8s(¶m); err != nil {
global.Log.Error("修改K8s中部署的Chart失败", zap.String("error", err.Error()))
response.ToErrorResponse(errorcode.ErrorK8sHelmUpdateFail)
return
}
response.ToResponse(gin.H{
"data": "修改K8s中部署的Chart成功",
})
}
// Delete
// @Summary 删除K8s中Helm Release
// @Description 删除K8s中Helm Release
// @Tags K8s中Helm Release管理
// @Produce json
// @Param body body requests.K8sHelmReleaseDeleteRequest true "body"
// @Success 200 {object} string "成功"
// @Failure 400 {object} errorcode.Error "请求错误"
// @Failure 500 {object} errorcode.Error "内部错误"
// @Router /api/v1/k8s/helm/delete [post]
func (u *HelmController) Delete(ctx *gin.Context) {
param := requests.K8sHelmReleaseDeleteRequest{}
response := app.NewResponse(ctx)
if ok := app.Validate(ctx, ¶m, requests.ValidK8sHelmReleaseDeleteRequest); !ok {
return
}
svc := services.New(ctx)
if err := svc.DeleteReleaseFromK8s(¶m); err != nil {
global.Log.Error("从K8s中删除Release失败", zap.String("error", err.Error()))
response.ToErrorResponse(errorcode.ErrorK8sHelmDeleteFail)
return
}
response.ToResponse(gin.H{
"data": "从K8s中删除Release成功",
})
}
// GetValue
// @Summary 获取K8s中Helm Release的Value配置
// @Description 获取K8s中Helm Release的Value配置
// @Tags K8s中Helm Release管理
// @Produce json
// @Param body body requests.K8sHelmGetReleaseValueRequest true "body"
// @Success 200 {object} string "成功"
// @Failure 400 {object} errorcode.Error "请求错误"
// @Failure 500 {object} errorcode.Error "内部错误"
// @Router /api/v1/k8s/helm/get_value [post]
func (u *HelmController) GetValue(ctx *gin.Context) {
param := requests.K8sHelmGetReleaseValueRequest{}
response := app.NewResponse(ctx)
if ok := app.Validate(ctx, ¶m, requests.ValidK8sHelmGetReleaseValueRequest); !ok {
return
}
svc := services.New(ctx)
content, err := svc.GetReleaseValueFromK8s(¶m)
if err != nil {
global.Log.Error("从K8s中删除Release失败", zap.String("error", err.Error()))
response.ToErrorResponse(errorcode.ErrorK8sHelmGetValueFail)
return
}
data, err := json.Marshal(content)
if err != nil {
response.ToErrorResponse(errorcode.ErrorK8sHelmGetValueFail)
return
}
response.ToResponse(gin.H{
"data": string(data),
})
}
再到 pkg/errorcode/helm.go
文件中新增如下错误代码:
go
package errorcode
var (
ErrorK8sHelmListFail = NewError(500161, "获取K8s集群中的Release失败")
ErrorK8sHelmCreateFail = NewError(500162, "创建K8s集群中的Release失败")
ErrorK8sHelmUpdateFail = NewError(500163, "更新K8s集群中的Release失败")
ErrorK8sHelmDeleteFail = NewError(500164, "删除K8s集群中的Release失败")
ErrorK8sHelmGetValueFail = NewError(500165, "获取K8s集群中的Release的Value失败")
)
5、新增路由
在 internal/app/routers/k8s_helm.go
文件中新增 K8sHelmRelease
操作的路由,如下:
go
package routers
import (
"github.com/gin-gonic/gin"
"github.com/joker-bai/hawkeye/internal/app/controllers/api/v1/k8s"
)
type K8sHelmRouter struct{}
func (a *K8sHelmRouter) Inject(r *gin.RouterGroup) {
ks := r.Group("/k8s")
{
ac := new(k8s.HelmController)
ks.GET("/helm/list", ac.List)
ks.POST("/helm/create", ac.Create)
ks.POST("/helm/update", ac.Update)
ks.POST("/helm/delete", ac.Delete)
}
}
再在initialize/router.go
中增加K8sHelmRelease
的路由。
go
package initialize
import (
"github.com/gin-gonic/gin"
_ "github.com/joker-bai/hawkeye/docs"
"github.com/joker-bai/hawkeye/internal/app/routers"
swaggerFiles "github.com/swaggo/files"
ginSwagger "github.com/swaggo/gin-swagger"
)
type injector interface {
Inject(router *gin.RouterGroup)
}
func (s *Engine) injectRouterGroup(router *gin.RouterGroup) {
{
for _, r := range []injector{
new(routers.HelloWorldRouter),
} {
r.Inject(router.Group("/api"))
}
}
// 需要鉴权
{
for _, r := range []injector{
new(routers.UserRouter),
new(routers.K8sRouter),
new(routers.AppMarketRouter),
new(routers.K8sHelmRouter),
} {
//r.Inject(router.Group("/api/v1", middleware.AuthJWT()))
r.Inject(router.Group("/api/v1"))
}
}
// 不需要鉴权
ar := new(routers.AuthRouter)
ar.Inject(router.Group("/api/v1"))
// swagger
router.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler))
}
6、测试一下
PS:测试之前都需要先初始化集群,在 4.3.1 Pod 章节有介绍。
这里简单测试创建应用市场接口,如下:
其他接口自行下去测试。
7、代码版本
本节开发完成后,记得生成 swag 和标记代码版本,如下:
go
$ swag init
$ git add .
$ git commit -m "新增k8s集群Helm部署操作"