From 56fc58cca37a2f966edccbc11e348dfb3ddc9335 Mon Sep 17 00:00:00 2001 From: "Brian C. Lane" Date: Mon, 15 Apr 2024 11:21:49 -0700 Subject: [PATCH] cloudapi: Add DeleteCompose to delete a job by UUID This adds the handler for DELETE /composes/{id} which will delete a job and all of its dependencies, and any artifacts. Related: RHEL-60120 --- internal/cloudapi/v2/errors.go | 2 + internal/cloudapi/v2/handler.go | 23 ++++++++++ internal/cloudapi/v2/v2_test.go | 75 ++++++++++++++++++++++++++++++++- internal/worker/server.go | 13 ++++++ 4 files changed, 111 insertions(+), 2 deletions(-) diff --git a/internal/cloudapi/v2/errors.go b/internal/cloudapi/v2/errors.go index 216c6d2e7..ea7ac0174 100644 --- a/internal/cloudapi/v2/errors.go +++ b/internal/cloudapi/v2/errors.go @@ -78,6 +78,7 @@ const ( ErrorTenantNotInContext ServiceErrorCode = 1020 ErrorGettingComposeList ServiceErrorCode = 1021 ErrorArtifactNotFound ServiceErrorCode = 1022 + ErrorDeletingJob ServiceErrorCode = 1023 // Errors contained within this file ErrorUnspecified ServiceErrorCode = 10000 @@ -163,6 +164,7 @@ func getServiceErrors() serviceErrors { serviceError{ErrorGettingJobType, http.StatusInternalServerError, "Unable to get job type of existing job"}, serviceError{ErrorTenantNotInContext, http.StatusInternalServerError, "Unable to retrieve tenant from request context"}, serviceError{ErrorGettingComposeList, http.StatusInternalServerError, "Unable to get list of composes"}, + serviceError{ErrorDeletingJob, http.StatusInternalServerError, "Unable to delete job"}, serviceError{ErrorUnspecified, http.StatusInternalServerError, "Unspecified internal error "}, serviceError{ErrorNotHTTPError, http.StatusInternalServerError, "Error is not an instance of HTTPError"}, diff --git a/internal/cloudapi/v2/handler.go b/internal/cloudapi/v2/handler.go index 3232c001e..76aa6dead 100644 --- a/internal/cloudapi/v2/handler.go +++ b/internal/cloudapi/v2/handler.go @@ -329,6 +329,29 @@ func (h *apiHandlers) GetComposeList(ctx echo.Context) error { return ctx.JSON(http.StatusOK, stats) } +// DeleteCompose deletes a compose by UUID +func (h *apiHandlers) DeleteCompose(ctx echo.Context, jobId uuid.UUID) error { + return h.server.EnsureJobChannel(h.deleteComposeImpl)(ctx, jobId) +} + +func (h *apiHandlers) deleteComposeImpl(ctx echo.Context, jobId uuid.UUID) error { + _, err := h.server.workers.JobType(jobId) + if err != nil { + return HTTPError(ErrorComposeNotFound) + } + + err = h.server.workers.DeleteJob(ctx.Request().Context(), jobId) + if err != nil { + return HTTPErrorWithInternal(ErrorDeletingJob, err) + } + + return ctx.JSON(http.StatusOK, ComposeDeleteStatus{ + Href: fmt.Sprintf("/api/image-builder-composer/v2/composes/delete/%v", jobId), + Id: jobId.String(), + Kind: "ComposeDeleteStatus", + }) +} + func (h *apiHandlers) GetComposeStatus(ctx echo.Context, jobId uuid.UUID) error { return h.server.EnsureJobChannel(h.getComposeStatusImpl)(ctx, jobId) } diff --git a/internal/cloudapi/v2/v2_test.go b/internal/cloudapi/v2/v2_test.go index b3a25fea6..f272f95b6 100644 --- a/internal/cloudapi/v2/v2_test.go +++ b/internal/cloudapi/v2/v2_test.go @@ -6,6 +6,7 @@ import ( "fmt" "net/http" "os" + "path/filepath" "sync" "testing" "time" @@ -241,11 +242,19 @@ func mockSearch(t *testing.T, workerServer *worker.Server, wg *sync.WaitGroup, f } func newV2Server(t *testing.T, dir string, enableJWT bool, fail bool) (*v2.Server, *worker.Server, jobqueue.JobQueue, context.CancelFunc) { - q, err := fsjobqueue.New(dir) + jobsDir := filepath.Join(dir, "jobs") + err := os.Mkdir(jobsDir, 0755) require.NoError(t, err) + q, err := fsjobqueue.New(jobsDir) + require.NoError(t, err) + + artifactsDir := filepath.Join(dir, "artifacts") + err = os.Mkdir(artifactsDir, 0755) + require.NoError(t, err) + workerServer := worker.NewServer(nil, q, worker.Config{ - ArtifactsDir: t.TempDir(), + ArtifactsDir: artifactsDir, BasePath: "/api/worker/v1", JWTEnabled: enableJWT, TenantProviderFields: []string{"rh-org-id", "account_id"}, @@ -2127,3 +2136,65 @@ func TestComposeRequestMetadata(t *testing.T) { "request": %s }`, jobId, request)) } + +func TestComposesDeleteRoute(t *testing.T) { + srv, wrksrv, _, cancel := newV2Server(t, t.TempDir(), false, false) + defer cancel() + + // Make a compose so it has something to list and delete + reply := test.TestRouteWithReply(t, srv.Handler("/api/image-builder-composer/v2"), false, "POST", "/api/image-builder-composer/v2/compose", fmt.Sprintf(` + { + "distribution": "%s", + "image_request":{ + "architecture": "%s", + "image_type": "%s", + "repositories": [{ + "baseurl": "somerepo.org", + "rhsm": false + }], + "upload_options": { + "region": "eu-central-1", + "snapshot_name": "name", + "share_with_accounts": ["123456789012","234567890123"] + } + } + }`, test_distro.TestDistro1Name, test_distro.TestArch3Name, string(v2.ImageTypesAws)), http.StatusCreated, ` + { + "href": "/api/image-builder-composer/v2/compose", + "kind": "ComposeId" + }`, "id") + + // Extract the compose ID to use to test the list response + var composeReply v2.ComposeId + err := json.Unmarshal(reply, &composeReply) + require.NoError(t, err) + jobID := composeReply.Id + + _, token, jobType, _, _, err := wrksrv.RequestJob(context.Background(), test_distro.TestArch3Name, []string{worker.JobTypeOSBuild}, []string{""}, uuid.Nil) + require.NoError(t, err) + require.Equal(t, worker.JobTypeOSBuild, jobType) + res, err := json.Marshal(&worker.OSBuildJobResult{ + Success: true, + OSBuildOutput: &osbuild.Result{Success: true}, + }) + require.NoError(t, err) + err = wrksrv.FinishJob(token, res) + require.NoError(t, err) + + // List root composes + test.TestRoute(t, srv.Handler("/api/image-builder-composer/v2"), false, "GET", "/api/image-builder-composer/v2/composes/", ``, + http.StatusOK, fmt.Sprintf(`[{"href":"/api/image-builder-composer/v2/composes/%[1]s", "id":"%[1]s", "image_status":{"status":"success"}, "kind":"ComposeStatus", "status":"success"}]`, + jobID.String())) + + // Delete the compose + test.TestRoute(t, srv.Handler("/api/image-builder-composer/v2"), false, "DELETE", fmt.Sprintf("/api/image-builder-composer/v2/composes/%s", jobID.String()), ``, + http.StatusOK, fmt.Sprintf(` + { + "id": "%s", + "kind": "ComposeDeleteStatus" + }`, jobID.String()), "href") + + // List root composes (should now be none) + test.TestRoute(t, srv.Handler("/api/image-builder-composer/v2"), false, "GET", "/api/image-builder-composer/v2/composes/", ``, + http.StatusOK, `[]`) +} diff --git a/internal/worker/server.go b/internal/worker/server.go index 4969b482d..ce86d6484 100644 --- a/internal/worker/server.go +++ b/internal/worker/server.go @@ -385,6 +385,19 @@ func (s *Server) CleanupArtifacts() error { return nil } +// DeleteJob deletes a job and all of its dependencies +func (s *Server) DeleteJob(ctx context.Context, id uuid.UUID) error { + jobInfo, err := s.jobInfo(id, nil) + if err != nil { + return err + } + if jobInfo.JobStatus.Finished.IsZero() { + return fmt.Errorf("Cannot delete job before job is finished: %s", id) + } + + return s.jobs.DeleteJob(ctx, id) +} + func (s *Server) OSBuildJobInfo(id uuid.UUID, result *OSBuildJobResult) (*JobInfo, error) { jobInfo, err := s.jobInfo(id, result) if err != nil {