Skip to content

应用部署 原创

1、功能

84434b32ab6fc6f5da1433b157ba3d83 MD5

2、数据定义

2.1、定义数据类型

internal/app/models/k8s_helm.go 文件中增加K8sHelmReleaseModels,并实现对数据库的操作,如下:

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, &param, requests.ValidK8sHelmListRequest); !ok {
		return
	}

	svc := services.New(ctx)
	list, err := svc.ListReleaseFromK8s(&param)
	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, &param, requests.ValidK8sHelmDeployRequest); !ok {
		return
	}

	svc := services.New(ctx)
	err := svc.DeployChartToK8s(&param)
	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, &param, requests.ValidK8sHelmUpdateRequest); !ok {
		return
	}

	svc := services.New(ctx)
	if err := svc.UpdateChartToK8s(&param); 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, &param, requests.ValidK8sHelmReleaseDeleteRequest); !ok {
		return
	}

	svc := services.New(ctx)
	if err := svc.DeleteReleaseFromK8s(&param); 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, &param, requests.ValidK8sHelmGetReleaseValueRequest); !ok {
		return
	}

	svc := services.New(ctx)
	content, err := svc.GetReleaseValueFromK8s(&param)
	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 章节有介绍。

这里简单测试创建应用市场接口,如下:

63aa0ed60efa3b2406a38b9cdc6e9d9f MD5

其他接口自行下去测试。

7、代码版本

本节开发完成后,记得生成 swag 和标记代码版本,如下:

go
$ swag init
$ git add .
$ git commit -m "新增k8s集群Helm部署操作"
最近更新