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
This commit is contained in:
Brian C. Lane 2024-04-15 11:21:49 -07:00 committed by Tomáš Hozza
parent a569ac0f7b
commit 56fc58cca3
4 changed files with 111 additions and 2 deletions

View file

@ -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"},

View file

@ -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)
}

View file

@ -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, `[]`)
}

View file

@ -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 {