From 5ec1d32fa91f1a3d690f4c7862beadd99320706a Mon Sep 17 00:00:00 2001 From: Rodrigo Silva Date: Fri, 22 Nov 2024 07:21:47 +0100 Subject: [PATCH] feat(webhook): Support for Webhook Name in Webhook Endpoints (#8203) * feat(webhooks): add by-name methods to connection helper * feat(webhooks): add connection endpoints by name * feat(webhooks): add post deployment endpoint by name * feat(webhooks): add issue endpoints by name --- .../pluginhelper/api/connection_helper.go | 27 +++++++- backend/plugins/webhook/api/connection.go | 63 ++++++++++++++++++- backend/plugins/webhook/api/deployments.go | 25 ++++++++ backend/plugins/webhook/api/issues.go | 37 +++++++++++ backend/plugins/webhook/impl/impl.go | 14 +++++ 5 files changed, 164 insertions(+), 2 deletions(-) diff --git a/backend/helpers/pluginhelper/api/connection_helper.go b/backend/helpers/pluginhelper/api/connection_helper.go index 5e306519c0f..d9630f81b03 100644 --- a/backend/helpers/pluginhelper/api/connection_helper.go +++ b/backend/helpers/pluginhelper/api/connection_helper.go @@ -99,7 +99,20 @@ func (c *ConnectionApiHelper) Patch(connection interface{}, input *plugin.ApiRes return c.save(connection, c.db.CreateOrUpdate) } -// First finds connection from db by parsing request input and decrypt it +// PatchByName (Modify) a connection record based on request body by connection name +func (c *ConnectionApiHelper) PatchByName(connection interface{}, input *plugin.ApiResourceInput) errors.Error { + err := c.FirstByName(connection, input.Params) + if err != nil { + return err + } + err = c.merge(connection, input.Body) + if err != nil { + return err + } + return c.save(connection, c.db.CreateOrUpdate) +} + +// First finds connection from db by id, parsing request input and decrypt it func (c *ConnectionApiHelper) First(connection interface{}, params map[string]string) errors.Error { connectionId := params["connectionId"] if connectionId == "" { @@ -117,6 +130,18 @@ func (c *ConnectionApiHelper) FirstById(connection interface{}, id uint64) error return CallDB(c.db.First, connection, dal.Where("id = ?", id)) } +// FirstByName finds connection from db by name, parsing request input and decrypting it +func (c *ConnectionApiHelper) FirstByName(connection interface{}, params map[string]string) errors.Error { + connectionName := params["connectionName"] + if connectionName == "" { + return errors.BadInput.New("missing connectionName") + } + if len(connectionName) > 100 { + return errors.BadInput.New("invalid connectionName") + } + return CallDB(c.db.First, connection, dal.Where("name = ?", connectionName)) +} + // List returns all connections with password/token decrypted func (c *ConnectionApiHelper) List(connections interface{}) errors.Error { return CallDB(c.db.All, connections) diff --git a/backend/plugins/webhook/api/connection.go b/backend/plugins/webhook/api/connection.go index acb8a76a593..89ca8843f7c 100644 --- a/backend/plugins/webhook/api/connection.go +++ b/backend/plugins/webhook/api/connection.go @@ -97,6 +97,24 @@ func PatchConnection(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, return &plugin.ApiResourceOutput{Body: connection}, nil } +// PatchConnectionByName +// @Summary patch webhook connection by name +// @Description Patch webhook connection +// @Tags plugins/webhook +// @Param body body models.WebhookConnection true "json body" +// @Success 200 {object} models.WebhookConnection +// @Failure 400 {string} errcode.Error "Bad Request" +// @Failure 500 {string} errcode.Error "Internal Error" +// @Router /plugins/webhook/connections/by-name/{connectionName} [PATCH] +func PatchConnectionByName(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) { + connection := &models.WebhookConnection{} + err := connectionHelper.PatchByName(connection, input) + if err != nil { + return nil, err + } + return &plugin.ApiResourceOutput{Body: connection}, nil +} + // DeleteConnection // @Summary delete a webhook connection // @Description Delete a webhook connection @@ -107,7 +125,32 @@ func PatchConnection(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, // @Failure 500 {string} errcode.Error "Internal Error" // @Router /plugins/webhook/connections/{connectionId} [DELETE] func DeleteConnection(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) { - connectionId, e := strconv.ParseInt(input.Params["connectionId"], 10, 64) + connectionId, e := strconv.ParseUint(input.Params["connectionId"], 10, 64) + return deleteConnection(e, connectionId) +} + +// DeleteConnectionByName +// @Summary delete a webhook connection by name +// @Description Delete a webhook connection +// @Tags plugins/webhook +// @Success 200 {object} models.WebhookConnection +// @Failure 400 {string} errcode.Error "Bad Request" +// @Failure 409 {object} services.BlueprintProjectPairs "References exist to this connection" +// @Failure 500 {string} errcode.Error "Internal Error" +// @Router /plugins/webhook/connections/by-name/{connectionName} [DELETE] +func DeleteConnectionByName(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) { + connection := &models.WebhookConnection{} + err := connectionHelper.FirstByName(connection, input.Params) + + if err != nil { + logger.Error(err, "query connection") + return nil, err + } + + return deleteConnection(nil, connection.ConnectionId()) +} + +func deleteConnection(e error, connectionId uint64) (*plugin.ApiResourceOutput, errors.Error) { if e != nil { return nil, errors.BadInput.WrapRaw(e) } @@ -183,6 +226,24 @@ func ListConnections(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, func GetConnection(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) { connection := &models.WebhookConnection{} err := connectionHelper.First(connection, input.Params) + return getConnection(err, connection) +} + +// GetConnectionByName +// @Summary get webhook connection detail by name +// @Description Get webhook connection detail +// @Tags plugins/webhook +// @Success 200 {object} WebhookConnectionResponse +// @Failure 400 {string} errcode.Error "Bad Request" +// @Failure 500 {string} errcode.Error "Internal Error" +// @Router /plugins/webhook/connections/by-name/{connectionName} [GET] +func GetConnectionByName(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) { + connection := &models.WebhookConnection{} + err := connectionHelper.FirstByName(connection, input.Params) + return getConnection(err, connection) +} + +func getConnection(err errors.Error, connection *models.WebhookConnection) (*plugin.ApiResourceOutput, errors.Error) { if err != nil { logger.Error(err, "query connection") return nil, err diff --git a/backend/plugins/webhook/api/deployments.go b/backend/plugins/webhook/api/deployments.go index cc5b9782732..86d01891a69 100644 --- a/backend/plugins/webhook/api/deployments.go +++ b/backend/plugins/webhook/api/deployments.go @@ -84,6 +84,31 @@ type WebhookDeploymentCommitReq struct { 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.
+// @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"}
+// @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 } diff --git a/backend/plugins/webhook/api/issues.go b/backend/plugins/webhook/api/issues.go index 88f7e859d9d..49ea72b51f1 100644 --- a/backend/plugins/webhook/api/issues.go +++ b/backend/plugins/webhook/api/issues.go @@ -94,6 +94,25 @@ func saveIncidentRelatedRecordsFromIssue(db dal.Transaction, logger log.Logger, func PostIssue(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) { connection := &models.WebhookConnection{} err := connectionHelper.First(connection, input.Params) + return postIssue(input, err, connection) +} + +// PostIssueByName +// @Summary receive a record as defined and save it +// @Description receive a record as follow and save it, example: {"url":"","issue_key":"DLK-1234","title":"a feature from DLK","description":"","epic_key":"","type":"BUG","status":"TODO","original_status":"created","story_point":0,"resolution_date":null,"created_date":"2020-01-01T12:00:00+00:00","updated_date":null,"lead_time_minutes":0,"parent_issue_key":"DLK-1200","priority":"","original_estimate_minutes":0,"time_spent_minutes":0,"time_remaining_minutes":0,"creator_id":"user1131","creator_name":"Nick name 1","assignee_id":"user1132","assignee_name":"Nick name 2","severity":"","component":""} +// @Tags plugins/webhook +// @Param body body WebhookIssueRequest true "json body" +// @Success 200 {string} noResponse "" +// @Failure 400 {string} errcode.Error "Bad Request" +// @Failure 500 {string} errcode.Error "Internal Error" +// @Router /plugins/webhook/by-name/:connectionName/issues [POST] +func PostIssueByName(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) { + connection := &models.WebhookConnection{} + err := connectionHelper.FirstByName(connection, input.Params) + return postIssue(input, err, connection) +} + +func postIssue(input *plugin.ApiResourceInput, err errors.Error, connection *models.WebhookConnection) (*plugin.ApiResourceOutput, errors.Error) { if err != nil { return nil, err } @@ -212,6 +231,24 @@ func PostIssue(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, error func CloseIssue(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) { connection := &models.WebhookConnection{} err := connectionHelper.First(connection, input.Params) + return closeIssue(input, err, connection) +} + +// CloseIssueByName +// @Summary set issue's status to DONE +// @Description set issue's status to DONE +// @Tags plugins/webhook +// @Success 200 {string} noResponse "" +// @Failure 400 {string} errcode.Error "Bad Request" +// @Failure 500 {string} errcode.Error "Internal Error" +// @Router /plugins/webhook/by-name/:connectionName/issue/:issueKey/close [POST] +func CloseIssueByName(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) { + connection := &models.WebhookConnection{} + err := connectionHelper.FirstByName(connection, input.Params) + return closeIssue(input, err, connection) +} + +func closeIssue(input *plugin.ApiResourceInput, err errors.Error, connection *models.WebhookConnection) (*plugin.ApiResourceOutput, errors.Error) { if err != nil { return nil, err } diff --git a/backend/plugins/webhook/impl/impl.go b/backend/plugins/webhook/impl/impl.go index 90e1f01986b..c0d2e70b2c4 100644 --- a/backend/plugins/webhook/impl/impl.go +++ b/backend/plugins/webhook/impl/impl.go @@ -110,5 +110,19 @@ func (p Webhook) ApiResources() map[string]map[string]plugin.ApiResourceHandler ":connectionId/issue/:issueKey/close": { "POST": api.CloseIssue, }, + "connections/by-name/:connectionName": { + "GET": api.GetConnectionByName, + "PATCH": api.PatchConnectionByName, + "DELETE": api.DeleteConnectionByName, + }, + "connections/by-name/:connectionName/deployments": { + "POST": api.PostDeploymentsByName, + }, + "connections/by-name/:connectionName/issues": { + "POST": api.PostIssueByName, + }, + "connections/by-name/:connectionName/issue/:issueKey/close": { + "POST": api.CloseIssueByName, + }, } }