diff --git a/cmd/server/docs/docs.go b/cmd/server/docs/docs.go index dbe9d53c6..ffd410851 100644 --- a/cmd/server/docs/docs.go +++ b/cmd/server/docs/docs.go @@ -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": { diff --git a/server/api/helper.go b/server/api/helper.go index 8b621487b..dbe6ce6e2 100644 --- a/server/api/helper.go +++ b/server/api/helper.go @@ -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 +} diff --git a/server/api/pipeline.go b/server/api/pipeline.go index 1bb8725d7..b7af59956 100644 --- a/server/api/pipeline.go +++ b/server/api/pipeline.go @@ -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 ) +// @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 } diff --git a/server/api/pipeline_test.go b/server/api/pipeline_test.go index 9f0608dc9..2d88059c2 100644 --- a/server/api/pipeline_test.go +++ b/server/api/pipeline_test.go @@ -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()) + }) + }) +} diff --git a/server/router/api.go b/server/router/api.go index 29926987a..80453b320 100644 --- a/server/router/api.go +++ b/server/router/api.go @@ -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)