| /* |
| Licensed to the Apache Software Foundation (ASF) under one or more |
| contributor license agreements. See the NOTICE file distributed with |
| this work for additional information regarding copyright ownership. |
| The ASF licenses this file to You under the Apache License, Version 2.0 |
| (the "License"); you may not use this file except in compliance with |
| the License. You may obtain a copy of the License at |
| |
| http://www.apache.org/licenses/LICENSE-2.0 |
| |
| Unless required by applicable law or agreed to in writing, software |
| distributed under the License is distributed on an "AS IS" BASIS, |
| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| See the License for the specific language governing permissions and |
| limitations under the License. |
| */ |
| |
| package api |
| |
| import ( |
| "crypto/md5" |
| "fmt" |
| "net/http" |
| "time" |
| |
| "github.com/apache/incubator-devlake/core/dal" |
| "github.com/apache/incubator-devlake/core/log" |
| |
| "github.com/apache/incubator-devlake/helpers/dbhelper" |
| "github.com/go-playground/validator/v10" |
| |
| "github.com/apache/incubator-devlake/core/errors" |
| "github.com/apache/incubator-devlake/core/models/domainlayer" |
| "github.com/apache/incubator-devlake/core/models/domainlayer/devops" |
| "github.com/apache/incubator-devlake/core/plugin" |
| "github.com/apache/incubator-devlake/helpers/pluginhelper/api" |
| "github.com/apache/incubator-devlake/plugins/webhook/models" |
| ) |
| |
| type WebhookDeploymentReq struct { |
| Id string `mapstructure:"id" validate:"required"` |
| DisplayTitle string `mapstructure:"displayTitle"` |
| Result string `mapstructure:"result"` |
| Environment string `validate:"omitempty,oneof=PRODUCTION STAGING TESTING DEVELOPMENT"` |
| Name string `mapstructure:"name"` |
| Url string `mapstructure:"url"` |
| // DeploymentCommits is used for multiple commits in one deployment |
| DeploymentCommits []WebhookDeploymentCommitReq `mapstructure:"deploymentCommits" validate:"omitempty,dive"` |
| CreatedDate *time.Time `mapstructure:"createdDate"` |
| // QueuedDate *time.Time `mapstructure:"queue_time"` |
| StartedDate *time.Time `mapstructure:"startedDate" validate:"required"` |
| FinishedDate *time.Time `mapstructure:"finishedDate" validate:"required"` |
| } |
| |
| type WebhookDeploymentCommitReq struct { |
| DisplayTitle string `mapstructure:"displayTitle"` |
| RepoId string `mapstructure:"repoId"` |
| RepoUrl string `mapstructure:"repoUrl" validate:"required"` |
| Name string `mapstructure:"name"` |
| RefName string `mapstructure:"refName"` |
| CommitSha string `mapstructure:"commitSha" validate:"required"` |
| CommitMsg string `mapstructure:"commitMsg"` |
| Result string `mapstructure:"result"` |
| Status string `mapstructure:"status"` |
| CreatedDate *time.Time `mapstructure:"createdDate"` |
| // QueuedDate *time.Time `mapstructure:"queue_time"` |
| StartedDate *time.Time `mapstructure:"startedDate" validate:"required"` |
| FinishedDate *time.Time `mapstructure:"finishedDate" validate:"required"` |
| } |
| |
| // PostDeployments |
| // @Summary create deployment by webhook |
| // @Description Create deployment pipeline by webhook.<br/> |
| // @Description example1: {"repo_url":"devlake","commit_sha":"015e3d3b480e417aede5a1293bd61de9b0fd051d","start_time":"2020-01-01T12:00:00+00:00","end_time":"2020-01-01T12:59:59+00:00","environment":"PRODUCTION"}<br/> |
| // @Description So we suggest request before task after deployment pipeline finish. |
| // @Description Both cicd_pipeline and cicd_task will be created |
| // @Tags plugins/webhook |
| // @Param body body WebhookDeploymentReq true "json body" |
| // @Success 200 |
| // @Failure 400 {string} errcode.Error "Bad Request" |
| // @Failure 403 {string} errcode.Error "Forbidden" |
| // @Failure 500 {string} errcode.Error "Internal Error" |
| // @Router /plugins/webhook/connections/:connectionId/deployments [POST] |
| func PostDeployments(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) { |
| connection := &models.WebhookConnection{} |
| err := connectionHelper.First(connection, input.Params) |
| |
| return postDeployments(input, connection, err) |
| } |
| |
| // PostDeploymentsByName |
| // @Summary create deployment by webhook name |
| // @Description Create deployment pipeline by webhook name.<br/> |
| // @Description example1: {"repo_url":"devlake","commit_sha":"015e3d3b480e417aede5a1293bd61de9b0fd051d","start_time":"2020-01-01T12:00:00+00:00","end_time":"2020-01-01T12:59:59+00:00","environment":"PRODUCTION"}<br/> |
| // @Description So we suggest request before task after deployment pipeline finish. |
| // @Description Both cicd_pipeline and cicd_task will be created |
| // @Tags plugins/webhook |
| // @Param body body WebhookDeploymentReq true "json body" |
| // @Success 200 |
| // @Failure 400 {string} errcode.Error "Bad Request" |
| // @Failure 403 {string} errcode.Error "Forbidden" |
| // @Failure 500 {string} errcode.Error "Internal Error" |
| // @Router /plugins/webhook/connections/by-name/:connectionName/deployments [POST] |
| func PostDeploymentsByName(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) { |
| connection := &models.WebhookConnection{} |
| err := connectionHelper.FirstByName(connection, input.Params) |
| |
| return postDeployments(input, connection, err) |
| } |
| |
| func postDeployments(input *plugin.ApiResourceInput, connection *models.WebhookConnection, err errors.Error) (*plugin.ApiResourceOutput, errors.Error) { |
| if err != nil { |
| return nil, err |
| } |
| // get request |
| request := &WebhookDeploymentReq{} |
| err = api.DecodeMapStruct(input.Body, request, true) |
| if err != nil { |
| return &plugin.ApiResourceOutput{Body: err.Error(), Status: http.StatusBadRequest}, nil |
| } |
| // validate |
| vld = validator.New() |
| err = errors.Convert(vld.Struct(request)) |
| if err != nil { |
| return nil, errors.BadInput.Wrap(vld.Struct(request), `input json error`) |
| } |
| txHelper := dbhelper.NewTxHelper(basicRes, &err) |
| defer txHelper.End() |
| tx := txHelper.Begin() |
| if err := CreateDeploymentAndDeploymentCommits(connection, request, tx, logger); err != nil { |
| logger.Error(err, "create deployments") |
| return nil, err |
| } |
| |
| return &plugin.ApiResourceOutput{Body: nil, Status: http.StatusOK}, nil |
| } |
| |
| func CreateDeploymentAndDeploymentCommits(connection *models.WebhookConnection, request *WebhookDeploymentReq, tx dal.Transaction, logger log.Logger) errors.Error { |
| // validation |
| if request == nil { |
| return errors.BadInput.New("request body is nil") |
| } |
| if len(request.DeploymentCommits) == 0 { |
| return errors.BadInput.New("deployment_commits is empty") |
| } |
| // set default values for optional fields |
| deploymentId := request.Id |
| scopeId := fmt.Sprintf("%s:%d", "webhook", connection.ID) |
| if request.CreatedDate == nil { |
| request.CreatedDate = request.StartedDate |
| } |
| if request.FinishedDate == nil { |
| now := time.Now() |
| request.FinishedDate = &now |
| } |
| if request.Result == "" { |
| request.Result = devops.RESULT_SUCCESS |
| } |
| if request.Environment == "" { |
| request.Environment = devops.PRODUCTION |
| } |
| duration := float64(request.FinishedDate.Sub(*request.StartedDate).Milliseconds() / 1e3) |
| createdDate := time.Now() |
| if request.CreatedDate != nil { |
| createdDate = *request.CreatedDate |
| } else if request.StartedDate != nil { |
| createdDate = *request.StartedDate |
| } |
| name := request.Name |
| if name == "" { |
| name = fmt.Sprintf(`deploy to %s at %s`, request.Environment, createdDate.Format(time.RFC3339)) |
| } |
| |
| // prepare deploymentCommits and deployment records |
| // queuedDuration := dateInfo.CalculateQueueDuration() |
| deploymentCommits := make([]*devops.CicdDeploymentCommit, len(request.DeploymentCommits)) |
| for i, commit := range request.DeploymentCommits { |
| if commit.Result == "" { |
| commit.Result = devops.RESULT_SUCCESS |
| } |
| if commit.Status == "" { |
| commit.Status = devops.STATUS_DONE |
| } |
| if commit.Name == "" { |
| commit.Name = fmt.Sprintf(`deployment for %s`, commit.CommitSha) |
| } |
| if commit.CreatedDate == nil { |
| commit.CreatedDate = &createdDate |
| } |
| if commit.StartedDate == nil { |
| commit.StartedDate = request.StartedDate |
| } |
| if commit.FinishedDate == nil { |
| commit.FinishedDate = request.FinishedDate |
| } |
| // create a deployment_commit record |
| deploymentCommits[i] = &devops.CicdDeploymentCommit{ |
| DomainEntity: domainlayer.DomainEntity{ |
| Id: GenerateDeploymentCommitId(connection.ID, deploymentId, commit.RepoUrl, commit.CommitSha), |
| }, |
| CicdDeploymentId: deploymentId, |
| CicdScopeId: scopeId, |
| Result: commit.Result, |
| Status: commit.Status, |
| OriginalResult: commit.Result, |
| OriginalStatus: commit.Status, |
| TaskDatesInfo: devops.TaskDatesInfo{ |
| CreatedDate: *commit.CreatedDate, |
| StartedDate: commit.StartedDate, |
| FinishedDate: commit.FinishedDate, |
| }, |
| DurationSec: &duration, |
| RepoId: commit.RepoId, |
| Name: commit.Name, |
| DisplayTitle: commit.DisplayTitle, |
| RepoUrl: commit.RepoUrl, |
| Environment: request.Environment, |
| OriginalEnvironment: request.Environment, |
| RefName: commit.RefName, |
| CommitSha: commit.CommitSha, |
| CommitMsg: commit.CommitMsg, |
| //QueuedDurationSec: queuedDuration, |
| } |
| } |
| |
| if err := tx.CreateOrUpdate(deploymentCommits); err != nil { |
| logger.Error(err, "failed to save deployment commits") |
| return err |
| } |
| |
| // create a deployment record |
| deployment := deploymentCommits[0].ToDeploymentWithCustomDisplayTitle(request.DisplayTitle) |
| deployment.Name = name |
| deployment.CreatedDate = createdDate |
| deployment.StartedDate = request.StartedDate |
| deployment.FinishedDate = request.FinishedDate |
| deployment.Result = request.Result |
| deployment.Url = request.Url |
| if err := tx.CreateOrUpdate(deployment); err != nil { |
| logger.Error(err, "failed to save deployment") |
| return err |
| } |
| return nil |
| } |
| |
| func GenerateDeploymentCommitId(connectionId uint64, deploymentId string, repoUrl string, commitSha string) string { |
| urlHash16 := fmt.Sprintf("%x", md5.Sum([]byte(repoUrl)))[:16] |
| return fmt.Sprintf("%s:%d:%s:%s:%s", "webhook", connectionId, deploymentId, urlHash16, commitSha) |
| } |