Skip to content

Commit de7be95

Browse files
mieliespoord4x1
andauthored
feat: add support for creating deployments by project name (#8611)
* feat: add support for creating deployments by project name and retrieving connections by project and plugin name * refactor: replace FirstByProjectName with findByProjectName for improved clarity and maintainability * chore: add gitattributes to help windows developers * fix(webhook): add project deployments endpoint for webhook plugin * chore(build): add .gitattributes to paths-ignore in .licenserc.yaml * fix: create connection if not exist * fix: allow for null apiKeys * feat: create webhook and blueprint connection if not exist * refactor: create specific name for project deployment webhooks * chore: revert change to webhook routing * chore: pass in webhook name * chore: improve logging for webhook connection creation by using projectName variable * refactor: replace gorm error check with custom error handling for webhook connection lookup * refactor: enhance connection handling in PostDeploymentsByProjectName function --------- Co-authored-by: Lynwee <1507509064@qq.com>
1 parent 920bc62 commit de7be95

File tree

5 files changed

+175
-1
lines changed

5 files changed

+175
-1
lines changed

.gitattributes

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
# Ensure all text files use LF (Linux) line endings
2+
* text=auto eol=lf
3+
4+
# Treat shell scripts as text and enforce LF
5+
*.sh text eol=lf
6+
7+
# Treat Go files as text and enforce LF
8+
*.go text eol=lf
9+
10+
# Treat Python files as text and enforce LF
11+
*.py text eol=lf
12+
13+
# Treat JavaScript files as text and enforce LF
14+
*.js text eol=lf
15+
16+
# Treat Markdown files as text and enforce LF
17+
*.md text eol=lf
18+
19+
# Treat configuration files as text and enforce LF
20+
*.yml text eol=lf
21+
*.yaml text eol=lf
22+
*.json text eol=lf
23+
24+
# Prevent CRLF normalization for binary files
25+
*.png binary
26+
*.jpg binary
27+
*.jpeg binary
28+
*.gif binary
29+
*.pdf binary
30+
*.zip binary
31+
*.tar binary
32+
*.gz binary
33+
*.bz2 binary
34+
*.xz binary

.licenserc.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ header:
3535
- "**/*.svg"
3636
- "**/*.png"
3737
- ".editorconfig"
38+
- "**/.gitattributes"
3839
- "**/.gitignore"
3940
- "**/.helmignore"
4041
- "**/.dockerignore"

backend/plugins/webhook/api/deployments.go

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,11 +26,13 @@ import (
2626

2727
"github.com/apache/incubator-devlake/core/dal"
2828
"github.com/apache/incubator-devlake/core/log"
29+
"github.com/apache/incubator-devlake/server/services"
2930

3031
"github.com/apache/incubator-devlake/helpers/dbhelper"
3132
"github.com/go-playground/validator/v10"
3233

3334
"github.com/apache/incubator-devlake/core/errors"
35+
coremodels "github.com/apache/incubator-devlake/core/models"
3436
"github.com/apache/incubator-devlake/core/models/domainlayer"
3537
"github.com/apache/incubator-devlake/core/models/domainlayer/devops"
3638
"github.com/apache/incubator-devlake/core/plugin"
@@ -109,6 +111,105 @@ func PostDeploymentsByName(input *plugin.ApiResourceInput) (*plugin.ApiResourceO
109111
return postDeployments(input, connection, err)
110112
}
111113

114+
// PostDeploymentsByProjectName
115+
// @Summary create deployment by project name
116+
// @Description Create deployment pipeline by project name.<br/>
117+
// @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/>
118+
// @Description So we suggest request before task after deployment pipeline finish.
119+
// @Description Both cicd_pipeline and cicd_task will be created
120+
// @Tags plugins/webhook
121+
// @Param body body WebhookDeploymentReq true "json body"
122+
// @Success 200
123+
// @Failure 400 {string} errcode.Error "Bad Request"
124+
// @Failure 403 {string} errcode.Error "Forbidden"
125+
// @Failure 500 {string} errcode.Error "Internal Error"
126+
// @Router /projects/:projectName/deployments [POST]
127+
func PostDeploymentsByProjectName(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) {
128+
// find or create the connection for this project
129+
connection, err, shouldReturn := getOrCreateConnection(input)
130+
if shouldReturn {
131+
return nil, err
132+
}
133+
134+
return postDeployments(input, connection, err)
135+
}
136+
137+
func getOrCreateConnection(input *plugin.ApiResourceInput) (*models.WebhookConnection, errors.Error, bool) {
138+
connection := &models.WebhookConnection{}
139+
projectName := input.Params["projectName"]
140+
webhookName := fmt.Sprintf("%s_deployments", projectName)
141+
err := findByProjectName(connection, input.Params, pluginName, webhookName)
142+
dal := basicRes.GetDal()
143+
if err != nil {
144+
// if not found, we will attempt to create a new connection
145+
// Use direct comparison against the package sentinel; only treat other errors as fatal.
146+
if !dal.IsErrorNotFound(err) {
147+
logger.Error(err, "failed to find webhook connection for project", "projectName", projectName)
148+
return nil, err, true
149+
}
150+
151+
// create the connection
152+
logger.Debug("creating webhook connection for project %s", projectName)
153+
connection.Name = webhookName
154+
155+
// find the project and blueprint with which we will associate this connection
156+
projectOutput, err := services.GetProject(projectName)
157+
if err != nil {
158+
logger.Error(err, "failed to find project for webhook connection", "projectName", projectName)
159+
return nil, err, true
160+
}
161+
162+
if projectOutput == nil {
163+
logger.Error(err, "project not found for webhook connection", "projectName", projectName)
164+
return nil, errors.NotFound.New("project not found: " + projectName), true
165+
}
166+
167+
if projectOutput.Blueprint == nil {
168+
logger.Error(err, "unable to create webhook as the project has no blueprint", "projectName", projectName)
169+
return nil, errors.BadInput.New("project has no blueprint: " + projectName), true
170+
}
171+
172+
connectionInput := &plugin.ApiResourceInput{
173+
Params: map[string]string{
174+
"plugin": "webhook",
175+
},
176+
Body: map[string]interface{}{
177+
"name": webhookName,
178+
},
179+
}
180+
181+
err = connectionHelper.Create(connection, connectionInput)
182+
if err != nil {
183+
logger.Error(err, "failed to create webhook connection for project", "projectName", projectName)
184+
return nil, err, true
185+
}
186+
187+
// get the blueprint
188+
blueprintId := projectOutput.Blueprint.ID
189+
blueprint, err := services.GetBlueprint(blueprintId, true)
190+
191+
if err != nil {
192+
logger.Error(err, "failed to find blueprint for webhook connection", "blueprintId", blueprintId)
193+
return nil, err, true
194+
}
195+
196+
// we need to associate this connection with the blueprint
197+
blueprintConnection := &coremodels.BlueprintConnection{
198+
BlueprintId: blueprint.ID,
199+
PluginName: pluginName,
200+
ConnectionId: connection.ID,
201+
}
202+
203+
logger.Info("adding blueprint connection for blueprint %d and connection %d", blueprint.ID, connection.ID)
204+
err = dal.Create(blueprintConnection)
205+
if err != nil {
206+
logger.Error(err, "failed to create blueprint connection for project", "projectName", projectName)
207+
return nil, err, true
208+
}
209+
}
210+
return connection, err, false
211+
}
212+
112213
func postDeployments(input *plugin.ApiResourceInput, connection *models.WebhookConnection, err errors.Error) (*plugin.ApiResourceOutput, errors.Error) {
113214
if err != nil {
114215
return nil, err
@@ -251,3 +352,38 @@ func GenerateDeploymentCommitId(connectionId uint64, deploymentId string, repoUr
251352
urlHash16 := fmt.Sprintf("%x", md5.Sum([]byte(repoUrl)))[:16]
252353
return fmt.Sprintf("%s:%d:%s:%s:%s", "webhook", connectionId, deploymentId, urlHash16, commitSha)
253354
}
355+
356+
// findByProjectName finds the connection by project name and plugin name
357+
func findByProjectName(connection interface{}, params map[string]string, pluginName string, webhookName string) errors.Error {
358+
projectName := params["projectName"]
359+
if projectName == "" {
360+
return errors.BadInput.New("missing projectName")
361+
}
362+
if len(projectName) > 100 {
363+
return errors.BadInput.New("invalid projectName")
364+
}
365+
if pluginName == "" {
366+
return errors.BadInput.New("missing pluginName")
367+
}
368+
// We need to join three tables: _tool_webhook_connections, _devlake_blueprint_connections, and _devlake_blueprints
369+
// to find the connection associated with the given project name and plugin name.
370+
// The SQL query would look something like this:
371+
// SELECT wc.*
372+
// FROM _tool_webhook_connections AS wc
373+
// JOIN _devlake_blueprint_connections AS bc ON wc.id = bc.connection_id AND bc.plugin_name = ?
374+
// JOIN _devlake_blueprints AS bp ON bc.blueprint_id = bp.id
375+
// WHERE bp.project_name = ? and _tool_webhook_connections.name = ?
376+
// LIMIT 1;
377+
378+
basicRes.GetLogger().Debug("finding project webhook connection for project %s and plugin %s", projectName, pluginName)
379+
// Using DAL to construct the query
380+
clauses := []dal.Clause{dal.From(connection)}
381+
clauses = append(clauses,
382+
dal.Join("left join _devlake_blueprint_connections bc ON _tool_webhook_connections.id = bc.connection_id and bc.plugin_name = ?", pluginName),
383+
dal.Join("left join _devlake_blueprints bp ON bc.blueprint_id = bp.id"),
384+
dal.Where("bp.project_name = ? and _tool_webhook_connections.name = ?", projectName, webhookName),
385+
)
386+
387+
dal := basicRes.GetDal()
388+
return dal.First(connection, clauses...)
389+
}

backend/plugins/webhook/impl/impl.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,5 +128,8 @@ func (p Webhook) ApiResources() map[string]map[string]plugin.ApiResourceHandler
128128
"connections/by-name/:connectionName/issue/:issueKey/close": {
129129
"POST": api.CloseIssueByName,
130130
},
131+
"projects/:projectName/deployments": {
132+
"POST": api.PostDeploymentsByProjectName,
133+
},
131134
}
132135
}

config-ui/src/features/connections/utils.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,6 @@ export const transformWebhook = (connection: IWebhookAPI): IWebhook => {
5151
closeIssuesEndpoint: connection.closeIssuesEndpoint,
5252
postPipelineDeployTaskEndpoint: connection.postPipelineDeployTaskEndpoint,
5353
postPullRequestsEndpoint: connection.postPullRequestsEndpoint,
54-
apiKeyId: connection.apiKey.id,
54+
apiKeyId: connection.apiKey?.id,
5555
};
5656
};

0 commit comments

Comments
 (0)