Add DeletePipeline API (#3506)

This is just a first step, the final goal is to have an API endpoint to
prune Repo Pipelines older than the given date.

@woodpecker-ci/maintainers Can I get some feedback if this is the right
direction?

---------

Co-authored-by: 6543 <m.huber@kithara.com>
This commit is contained in:
Robert Kaussow 2024-04-25 10:59:17 +02:00 committed by GitHub
parent 9972c24924
commit d0057736f1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 141 additions and 4 deletions

View file

@ -2339,6 +2339,44 @@ const docTemplate = `{
}
}
}
},
"delete": {
"produces": [
"text/plain"
],
"tags": [
"Pipelines"
],
"summary": "Delete pipeline",
"parameters": [
{
"type": "string",
"default": "Bearer \u003cpersonal access token\u003e",
"description": "Insert your personal access token",
"name": "Authorization",
"in": "header",
"required": true
},
{
"type": "integer",
"description": "the repository id",
"name": "repo_id",
"in": "path",
"required": true
},
{
"type": "integer",
"description": "the number of the pipeline",
"name": "number",
"in": "path",
"required": true
}
],
"responses": {
"204": {
"description": "No Content"
}
}
}
},
"/repos/{repo_id}/pipelines/{number}/approve": {

View file

@ -64,3 +64,14 @@ func refreshUserToken(c *gin.Context, user *model.User) {
}
forge.Refresh(c, _forge, _store, user)
}
// pipelineDeleteAllowed checks if the given pipeline can be deleted based on its status.
// It returns a bool indicating if delete is allowed, and the pipeline's status.
func pipelineDeleteAllowed(pl *model.Pipeline) bool {
switch pl.Status {
case model.StatusRunning, model.StatusPending, model.StatusBlocked:
return false
}
return true
}

View file

@ -144,6 +144,46 @@ func GetPipelines(c *gin.Context) {
c.JSON(http.StatusOK, pipelines)
}
// DeletePipeline
//
// @Summary Delete pipeline
// @Router /repos/{repo_id}/pipelines/{number} [delete]
// @Produce plain
// @Success 204
// @Tags Pipelines
// @Param Authorization header string true "Insert your personal access token" default(Bearer <personal access token>)
// @Param repo_id path int true "the repository id"
// @Param number path int true "the number of the pipeline"
func DeletePipeline(c *gin.Context) {
_store := store.FromContext(c)
repo := session.Repo(c)
num, err := strconv.ParseInt(c.Param("number"), 10, 64)
if err != nil {
_ = c.AbortWithError(http.StatusBadRequest, err)
return
}
pl, err := _store.GetPipelineNumber(repo, num)
if err != nil {
handleDBError(c, err)
return
}
if ok := pipelineDeleteAllowed(pl); !ok {
c.String(http.StatusUnprocessableEntity, "Cannot delete pipeline with status %s", pl.Status)
return
}
err = store.FromContext(c).DeletePipeline(pl)
if err != nil {
c.String(http.StatusInternalServerError, "Error deleting pipeline. %s", err)
return
}
c.Status(http.StatusNoContent)
}
// GetPipeline
//
// @Summary Pipeline information by number
@ -574,9 +614,8 @@ func DeletePipelineLogs(c *gin.Context) {
return
}
switch pl.Status {
case model.StatusRunning, model.StatusPending:
c.String(http.StatusUnprocessableEntity, "Cannot delete logs for a pending or running pipeline")
if ok := pipelineDeleteAllowed(pl); !ok {
c.String(http.StatusUnprocessableEntity, "Cannot delete logs for pipeline with status %s", pl.Status)
return
}
@ -586,7 +625,7 @@ func DeletePipelineLogs(c *gin.Context) {
}
}
if err != nil {
c.String(http.StatusInternalServerError, "There was a problem deleting your logs. %s", err)
c.String(http.StatusInternalServerError, "Error deleting pipeline logs. %s", err)
return
}

View file

@ -79,3 +79,51 @@ func TestGetPipelines(t *testing.T) {
})
})
}
func TestDeletePipeline(t *testing.T) {
gin.SetMode(gin.TestMode)
g := goblin.Goblin(t)
g.Describe("Pipeline", func() {
g.It("should delete pipeline", func() {
mockStore := mocks.NewStore(t)
mockStore.On("GetPipelineNumber", mock.Anything, mock.Anything).Return(fakePipeline, nil)
mockStore.On("DeletePipeline", mock.Anything).Return(nil)
c, _ := gin.CreateTestContext(httptest.NewRecorder())
c.Set("store", mockStore)
c.Params = gin.Params{{Key: "number", Value: "1"}}
DeletePipeline(c)
mockStore.AssertCalled(t, "GetPipelineNumber", mock.Anything, mock.Anything)
mockStore.AssertCalled(t, "DeletePipeline", mock.Anything)
assert.Equal(t, http.StatusNoContent, c.Writer.Status())
})
g.It("should not delete without pipeline number", func() {
c, _ := gin.CreateTestContext(httptest.NewRecorder())
DeletePipeline(c)
assert.Equal(t, http.StatusBadRequest, c.Writer.Status())
})
g.It("should not delete pending", func() {
fakePipeline.Status = model.StatusPending
mockStore := mocks.NewStore(t)
mockStore.On("GetPipelineNumber", mock.Anything, mock.Anything).Return(fakePipeline, nil)
c, _ := gin.CreateTestContext(httptest.NewRecorder())
c.Set("store", mockStore)
c.Params = gin.Params{{Key: "number", Value: "1"}}
DeletePipeline(c)
mockStore.AssertCalled(t, "GetPipelineNumber", mock.Anything, mock.Anything)
mockStore.AssertNotCalled(t, "DeletePipeline", mock.Anything)
assert.Equal(t, http.StatusUnprocessableEntity, c.Writer.Status())
})
})
}

View file

@ -94,6 +94,7 @@ func apiRoutes(e *gin.RouterGroup) {
repo.GET("/pipelines", api.GetPipelines)
repo.POST("/pipelines", session.MustPush, api.CreatePipeline)
repo.DELETE("/pipelines/:number", session.MustRepoAdmin(), api.DeletePipeline)
repo.GET("/pipelines/:number", api.GetPipeline)
repo.GET("/pipelines/:number/config", api.GetPipelineConfig)