最近因为公司的产研调整,决定将代码仓库从本地的 Gitlab 迁移到云效的 Codeup,不是 Gitlab 不够好,而是 Codeup 在度量、安全等方面比原生的 Gitlab 要好,再则公司的产研管理也迁移到了云效,也为了统一化管理。
(相关资料图)
有同学可能会问,都用云效了,为什么不直接用它的 AppStack,还要用 Zadig?
AppStack 还处于发展阶段,还有以下问题不适合我们现阶段的需求:
AppStack 不支持管理私有云 Kubernetes 集群(没有公网入口)。AppStack 不支持 Helm 类应用,改造工作比较大。再则,我也是 Zadig 开源产品的忠实粉丝~~!
但是,Zadig 对非标准的代码仓库的支持力度有限,比如:
非标准代码仓库不支持列出仓库列表,需要自己手动填写非标准代码仓库创建的流水线原生不支持 Webhook 触发经过综合考虑,手动填写代码仓库信息以及不支持 Webhook 并不影响整体的使用,只是会影响部分项目的工作效率。
但是,为了最小程度上影响原有的产研节奏,我还是准备自己实现三方仓库的 Webhook 触发 Zadig 流水线。因为本身也不复杂。
整体思路实现不复杂,也就是接收到 webhook 触发动作,解析内容,根据需要触发相应的流水线接口。截至目前(v1.17.0)zadig 的触发流水线接口已经可以正常使用了。
开始搬砖封装 Zadig API首先封装一下 Zadig 的 API。为了方便使用,之前的弄了一个 go-zadig 项目(https://github.com/joker-bai/go-zadig),这两天将其做了更新,支持最新的 1.17.0 版本的 API。
主要增加了以下内容:
// 执行工作流type ExecWorkflowTaskOptions struct { WorkflowName string `json:"workflow_name"` ProjectName string `json:"project_name"` Input WorkflowInput `json:"input"`}type WorkflowInput struct { TargetEnv string `json:"target_env,omitempty"` Build ExecBuildArgs `json:"build"` Deploy ExecDeployArgs `json:"deploy"`}type ExecBuildArgs struct { Enabled bool `json:"enabled"` ServiceList []BuildServiceInfo `json:"service_list"`}type BuildServiceInfo struct { ServiceModule string `json:"service_module"` ServiceName string `json:"service_name"` RepoInfo []RepositoryInfo `json:"repo_info"` Inputs []UserInput `json:"inputs"`}type RepositoryInfo struct { CodehostName string `json:"codehost_name"` RepoNamespace string `json:"repo_namespace"` RepoName string `json:"repo_name"` Branch string `json:"branch"` PR int `json:"pr"`}type UserInput struct { Key string `json:"key"` Value string `json:"value"`}type ExecDeployArgs struct { Enabled bool `json:"enabled"` Source string `json:"source"` ServiceList []DeployServiceInfo `json:"service_list"`}type DeployServiceInfo struct { ServiceModule string `json:"service_module"` ServiceName string `json:"service_name"` Image string `json:"image"`}type ExecWorkflowTaskResponse struct { ProjectName string `json:"project_name,omitempty"` WorkflowName string `json:"workflow_name,omitempty"` TaskID int64 `json:"task_id,omitempty"`}func (w *WorkflowService) ExecWorkflowTask(opt *ExecWorkflowTaskOptions, options ...RequestOptionFunc) (*ExecWorkflowTaskResponse, *Response, error) { path := "openapi/workflows/product/task" req, err := w.client.NewRequest(http.MethodPost, path, opt, options) if err != nil { return nil, nil, err } task := new(ExecWorkflowTaskResponse) resp, err := w.client.Do(req, &task) if err != nil { return nil, resp, err } return task, resp, err}
这部分是执行标准工作流的接口。自定义工作流之前已经实现了,并没有什么变化。
开发 Http Server由于 Zadig 原生不支持三方仓库的 Webhook,要实现不外乎两种:
自己修改 Zadig 源码,实现这部分功能。找一个中间商,由它来协调。修改源码的好处是可以不需要再单独对数据这块做太多处理,直接用现成的。但我选择了后者,主要是因为菜,源码改起来费劲。
(1)定义数据结构我使用的是 Uber 的 fx 框架。其实用什么框架不重要,本身的逻辑就很简单,我只是选了一个用起来比较简单和顺手的。
package entitytype ZadigWorkflowTask struct { ID int `json:"id"` ProjectName string `json:"project_name"` // 项目名 ServiceModule string `json:"service_module"` // 服务组件名称 ServiceName string `json:"service_name"` // 服务名 CodehostName string `json:"codehost_name"` // 代码源别名 RepoNamespace string `json:"repo_namespace"` // 仓库组名 RepoName string `json:"repo_name"` // 仓库名 WorkflowType string `json:"workflow_type"` // 工作流类型: product/custom JobName string `json:"job_name"` // 任务名 workflow_type为custom生效 JobType string `json:"job_type"` // 任务类型 workflow_type为custom生效 Registry string `json:"registry"` // 镜像仓库 workflow_type为custom生效}func (z *ZadigWorkflowTask) Table() string { return "zadig_workflow_task"}type ZadigWorkflowName struct { ID int `json:"id"` WorkflowName string `json:"workflow_name"` // workflow名 Branch string `json:"branch"` // 分支 ProjectName string `json:"project_name"` // 项目名 TargetEnv string `json:"target_env"` // 目标环境}func (z *ZadigWorkflowName) Table() string { return "zadig_workflow_name"}
我定义的比较简单,命名也比较随意。主要的字段就是 Zadig API 需要的字段,其他不需要的就没写了。
(2)实现 Zadig 触发标准和非标准流水线package zadigimport ( "github.com/joker-bai/go-zadig" "joker-bai/go-webhook/config")type Zadig struct { client *zadig.Client}func NewZadig(cfg *config.Config) *Zadig { client, err := zadig.NewClient(cfg.ZadigConfig.Token, zadig.WithBaseURL(cfg.ZadigConfig.URL)) if err != nil { panic(err) } return &Zadig{client: client}}// ExecProductWorkflowTask 执行标准工作流func (z *Zadig) ExecProductWorkflowTask(workflowName, projectName, targetEnv, serviceModule, serviceName, codehostName, repoNamespace, repoName, branch string) error { _, _, err := z.client.Workflow.ExecWorkflowTask(&zadig.ExecWorkflowTaskOptions{ WorkflowName: workflowName, ProjectName: projectName, Input: zadig.WorkflowInput{ TargetEnv: targetEnv, Build: zadig.ExecBuildArgs{ Enabled: true, ServiceList: []zadig.BuildServiceInfo{ { ServiceModule: serviceModule, ServiceName: serviceName, RepoInfo: []zadig.RepositoryInfo{ { CodehostName: codehostName, RepoNamespace: repoNamespace, RepoName: repoName, Branch: branch, }, }, }, }, }, Deploy: zadig.ExecDeployArgs{ Enabled: true, Source: "zadig", }, }, }) return err}// ExecCustomWorkflowTask 执行自定义工作流func (z *Zadig) ExecCustomWorkflowTask(projectName, workflowName, jobName, jobType, registry, serviceModule, serviceName, codehostName, repoNamespace, repoName, branch string) error { _, _, err := z.client.CustomWorkflow.CreateCustomWorkflowTask(&zadig.CreateCustomWorkflowTask{ ProjectName: projectName, WorkflowName: workflowName, Inputs: []zadig.CreateCustomWorkflowTaskInput{ { JobName: jobName, JobType: jobType, Parameters: zadig.CreateCustomWorkflowTaskParameters{ Register: registry, ServiceList: []zadig.ServiceList{ { ServiceModule: serviceModule, ServiceName: serviceName, RepoInfo: []zadig.RepoInfo{ { CodehostName: codehostName, RepoNamespace: repoNamespace, RepoName: repoName, Branch: branch, }, }, }, }, }, }, }, }) return err}
这里对工作流的 API 进行了取舍,根据实际情况选择需要的字段。
(3)实现 service 方法package serviceimport ( "context" "fmt" "github.com/sirupsen/logrus" "go.uber.org/fx" "joker-bai/go-webhook/config" "joker-bai/go-webhook/infrastructure/db" "joker-bai/go-webhook/internal/domain/entity" "joker-bai/go-webhook/pkg/zadig")// 获取并处理Codeup的Webhooktype CodeupWebhookService struct { logger *logrus.Logger cfg *config.Config db *db.DataBase}var RegCodeupWebhookService = fx.Provide(func(logger *logrus.Logger, cfg *config.Config, db *db.DataBase) *CodeupWebhookService { return &CodeupWebhookService{ logger: logger, cfg: cfg, db: db, }})// ExecZadigWorkflowTask 触发执行zadig的工作流func (c *CodeupWebhookService) ExecZadigWorkflowTask(ctx context.Context, repoName, branch string) error { client := zadig.NewZadig(c.cfg) // 从数据库中获取数据 dbClient := c.db.Master.WithContext(ctx) var workflowTask entity.ZadigWorkflowTask workflowTaskRes := dbClient.Model(&workflowTask).Where("repo_name = ?", repoName).First(&workflowTask) if workflowTaskRes.Error != nil { return workflowTaskRes.Error } // 从数据库获取项目和工作流信息 var flowName entity.ZadigWorkflowName workflowNameRes := dbClient.Model(&flowName).Where("branch = ? and project_name = ?", repoName, workflowTask.ProjectName).First(&flowName) if workflowNameRes.Error != nil { return workflowNameRes.Error } // 判断workflow的类别 if workflowTask.WorkflowType == "product" { return client.ExecProductWorkflowTask( flowName.WorkflowName, workflowTask.ProjectName, flowName.TargetEnv, workflowTask.ServiceModule, workflowTask.ServiceName, workflowTask.CodehostName, workflowTask.RepoNamespace, workflowTask.RepoName, branch, ) } else if workflowTask.WorkflowType == "custom" { return client.ExecCustomWorkflowTask( workflowTask.ProjectName, flowName.WorkflowName, workflowTask.JobName, workflowTask.JobType, workflowTask.Registry, workflowTask.ServiceModule, workflowTask.ServiceName, workflowTask.CodehostName, workflowTask.RepoNamespace, workflowTask.RepoName, branch, ) } else { return fmt.Errorf("未匹配workflow类型") }}
这里从数据库获取需要的信息,然后根据不同的工作流类型执行不同的接口。
(4)实现 controller 方法package applicationimport ( "fmt" "github.com/gin-gonic/gin" "github.com/sirupsen/logrus" "go.uber.org/fx" "joker-bai/go-webhook/internal/core/base" "joker-bai/go-webhook/domain/service" "strings" "time")var regCodeupWebhookApplication = fx.Provide(NewCodeupWebhookController)type CodeupWebhookController struct { logger *logrus.Logger service *service.CodeupWebhookService}type CodeupWebhook struct { After string `json:"after"` AliyunPk string `json:"aliyun_pk"` Before string `json:"before"` CheckoutSha string `json:"checkout_sha"` Commits []Commits `json:"commits"` ObjectKind string `json:"object_kind"` ProjectID int `json:"project_id"` Ref string `json:"ref"` Repository Repository `json:"repository"` TotalCommitsCount int `json:"total_commits_count"` UserEmail string `json:"user_email"` UserExternUID string `json:"user_extern_uid"` UserID int `json:"user_id"` UserName string `json:"user_name"`}type Author struct { Email string `json:"email"` Name string `json:"name"`}type Commits struct { Author Author `json:"author"` ID string `json:"id"` Message string `json:"message"` Timestamp time.Time `json:"timestamp"` URL string `json:"url"`}type Repository struct { Description string `json:"description"` GitHTTPURL string `json:"git_http_url"` GitSecondaryHTTPURL string `json:"git_secondary_http_url"` GitSecondarySSHURL string `json:"git_secondary_ssh_url"` GitSSHURL string `json:"git_ssh_url"` Homepage string `json:"homepage"` Name string `json:"name"` URL string `json:"url"` VisibilityLevel int `json:"visibility_level"`}func NewCodeupWebhookController(logger *logrus.Logger, service *service.CodeupWebhookService) *CodeupWebhookController { return &CodeupWebhookController{ logger: logger, service: service, }}// DoCodeupWebhook 获取Codeup 代码webhook的Bodyfunc (c *CodeupWebhookController) DoCodeupWebhook(ctx *gin.Context) { output := base.NewResponse(ctx) var param CodeupWebhook err := ctx.ShouldBindJSON(¶m) if err != nil { output.Error(10000, err.Error()) return } // 获取repo_name,repo_namespace,branch repoName := param.Repository.Name branch := strings.Split(param.Ref, "/")[2] if err := c.service.ExecZadigWorkflowTask(ctx, repoName, branch); err != nil { c.logger.Error(err.Error()) output.Error(502, "执行workflow失败") } output.Success(gin.H{ "data": "ok", })}
这部分就更简单了,从 Webhook 中获取数据,然后调 service 去执行即可。
最后就是增加路由了,这部分就不用展示了。
搬砖结束搬砖完成过后就是对自己开发的 HTTP Server 进行验证了。
整个思路和开发的工作量都不大,上面的代码还有很多地方需要去调整的,如果有相同需求的可以自己去实现,我仅仅是做了一个 demo。
通过引入中间商的方式来实现自己的需求的优点是比较简单,不需要去看或者兼容其他的代码,只需要实现自己的逻辑,缺点就是数据这一块需要单独去处理,比较麻烦。