Repository: osbuild/osbuild-composer
Pull request #971 (2020-09-07 18:45:20)

Further simplify worker API
See individual commits. I'm not yet done, but this is going towards a worker API that supports multiple job types.

larskarlitski: Update: add a commit which changes the API to use callback URLs instead of predefined routes.
larskarlitski: Update to set `Content-Type` correctly in the client and expose server errors in the worker's log.
larskarlitski: Update: fix typos, clarify an error message and print errors at all when uploading fails. Also, fix an issue the tests found: the upload URL was wrong.
teg: One test failed due to a mirror issue, unrelated to this PR.

Patch:
diff --git a/cmd/osbuild-worker/main.go b/cmd/osbuild-worker/main.go
index dafe2a1cf..909fdcdc6 100644
--- a/cmd/osbuild-worker/main.go
+++ b/cmd/osbuild-worker/main.go
@@ -7,7 +7,6 @@ import (
 	"errors"
 	"flag"
 	"fmt"
-	"io"
 	"io/ioutil"
 	"log"
 	"os"
@@ -67,7 +66,7 @@ func (e *TargetsError) Error() string {
 	return errString
 }
 
-func RunJob(job *worker.Job, store string, uploadFunc func(uuid.UUID, string, io.Reader) error) (*osbuild.Result, error) {
+func RunJob(job worker.Job, store string) (*osbuild.Result, error) {
 	outputDirectory, err := ioutil.TempDir("/var/tmp", "osbuild-worker-*")
 	if err != nil {
 		return nil, fmt.Errorf("error creating temporary output directory: %v", err)
@@ -79,14 +78,19 @@ func RunJob(job *worker.Job, store string, uploadFunc func(uuid.UUID, string, io
 		}
 	}()
 
-	result, err := RunOSBuild(job.Manifest, store, outputDirectory, os.Stderr)
+	manifest, targets, err := job.OSBuildArgs()
+	if err != nil {
+		return nil, err
+	}
+
+	result, err := RunOSBuild(manifest, store, outputDirectory, os.Stderr)
 	if err != nil {
 		return nil, err
 	}
 
 	var r []error
 
-	for _, t := range job.Targets {
+	for _, t := range targets {
 		switch options := t.Options.(type) {
 		case *target.LocalTargetOptions:
 			var f *os.File
@@ -105,7 +109,7 @@ func RunJob(job *worker.Job, store string, uploadFunc func(uuid.UUID, string, io
 				}
 			}
 
-			err = uploadFunc(job.Id, options.Filename, f)
+			err = job.UploadArtifact(options.Filename, f)
 			if err != nil {
 				r = append(r, err)
 				continue
@@ -119,18 +123,19 @@ func RunJob(job *worker.Job, store string, uploadFunc func(uuid.UUID, string, io
 				continue
 			}
 
-			if options.Key == "" {
-				options.Key = job.Id.String()
+			key := options.Key
+			if key == "" {
+				key = uuid.New().String()
 			}
 
-			_, err = a.Upload(path.Join(outputDirectory, options.Filename), options.Bucket, options.Key)
+			_, err = a.Upload(path.Join(outputDirectory, options.Filename), options.Bucket, key)
 			if err != nil {
 				r = append(r, err)
 				continue
 			}
 
 			/* TODO: communicate back the AMI */
-			_, err = a.Register(t.ImageName, options.Bucket, options.Key)
+			_, err = a.Register(t.ImageName, options.Bucket, key)
 			if err != nil {
 				r = append(r, err)
 				continue
@@ -180,11 +185,16 @@ func RunJob(job *worker.Job, store string, uploadFunc func(uuid.UUID, string, io
 // It would be cleaner to kill the osbuild process using (`exec.CommandContext`
 // or similar), but osbuild does not currently support this. Exiting here will
 // make systemd clean up the whole cgroup and restart this service.
-func WatchJob(ctx context.Context, client *worker.Client, job *worker.Job) {
+func WatchJob(ctx context.Context, job worker.Job) {
 	for {
 		select {
 		case <-time.After(15 * time.Second):
-			if client.JobCanceled(job) {
+			canceled, err := job.Canceled()
+			if err != nil {
+				log.Printf("Error fetching job status: %v", err)
+				os.Exit(0)
+			}
+			if canceled {
 				log.Println("Job was canceled. Exiting.")
 				os.Exit(0)
 			}
@@ -238,18 +248,18 @@ func main() {
 
 	for {
 		fmt.Println("Waiting for a new job...")
-		job, err := client.AddJob()
+		job, err := client.RequestJob()
 		if err != nil {
 			log.Fatal(err)
 		}
 
-		fmt.Printf("Running job %s\n", job.Id)
+		fmt.Printf("Running job %v\n", job.Id())
 
 		ctx, cancel := context.WithCancel(context.Background())
-		go WatchJob(ctx, client, job)
+		go WatchJob(ctx, job)
 
 		var status common.ImageBuildState
-		result, err := RunJob(job, store, client.UploadImage)
+		result, err := RunJob(job, store)
 		if err != nil {
 			log.Printf("  Job failed: %v", err)
 			status = common.IBFailed
@@ -275,14 +285,14 @@ func main() {
 			// flag to indicate all error kinds.
 			result.Success = false
 		} else {
-			log.Printf("  🎉 Job completed successfully: %s", job.Id)
+			log.Printf("  🎉 Job completed successfully: %v", job.Id())
 			status = common.IBFinished
 		}
 
 		// signal to WatchJob() that it can stop watching
 		cancel()
 
-		err = client.UpdateJob(job, status, result)
+		err = job.Update(status, result)
 		if err != nil {
 			log.Fatalf("Error reporting job result: %v", err)
 		}
diff --git a/internal/worker/api/api.gen.go b/internal/worker/api/api.gen.go
index 1c3c20db5..b2bebfcd0 100644
--- a/internal/worker/api/api.gen.go
+++ b/internal/worker/api/api.gen.go
@@ -4,784 +4,46 @@
 package api
 
 import (
-	"bytes"
-	"context"
-	"encoding/json"
 	"fmt"
 	"github.com/deepmap/oapi-codegen/pkg/runtime"
 	"github.com/labstack/echo/v4"
-	"io"
-	"io/ioutil"
 	"net/http"
-	"net/url"
-	"strings"
 )
 
-// PostJobQueueV1JobsJSONBody defines parameters for PostJobQueueV1Jobs.
-type PostJobQueueV1JobsJSONBody map[string]interface{}
-
-// PatchJobQueueV1JobsJobIdJSONBody defines parameters for PatchJobQueueV1JobsJobId.
-type PatchJobQueueV1JobsJobIdJSONBody struct {
-	Result interface{} `json:"result"`
-	Status string      `json:"status"`
-}
-
-// PostJobQueueV1JobsRequestBody defines body for PostJobQueueV1Jobs for application/json ContentType.
-type PostJobQueueV1JobsJSONRequestBody PostJobQueueV1JobsJSONBody
-
-// PatchJobQueueV1JobsJobIdRequestBody defines body for PatchJobQueueV1JobsJobId for application/json ContentType.
-type PatchJobQueueV1JobsJobIdJSONRequestBody PatchJobQueueV1JobsJobIdJSONBody
-
-// RequestEditorFn  is the function signature for the RequestEditor callback function
-type RequestEditorFn func(ctx context.Context, req *http.Request) error
-
-// Doer performs HTTP requests.
-//
-// The standard http.Client implements this interface.
-type HttpRequestDoer interface {
-	Do(req *http.Request) (*http.Response, error)
-}
-
-// Client which conforms to the OpenAPI3 specification for this service.
-type Client struct {
-	// The endpoint of the server conforming to this interface, with scheme,
-	// https://api.deepmap.com for example.
-	Server string
-
-	// Doer for performing requests, typically a *http.Client with any
-	// customized settings, such as certificate chains.
-	Client HttpRequestDoer
-
-	// A callback for modifying requests which are generated before sending over
-	// the network.
-	RequestEditor RequestEditorFn
-}
-
-// ClientOption allows setting custom parameters during construction
-type ClientOption func(*Client) error
-
-// Creates a new Client, with reasonable defaults
-func NewClient(server string, opts ...ClientOption) (*Client, error) {
-	// create a client with sane default values
-	client := Client{
-		Server: server,
-	}
-	// mutate client and add all optional params
-	for _, o := range opts {
-		if err := o(&client); err != nil {
-			return nil, err
-		}
-	}
-	// ensure the server URL always has a trailing slash
-	if !strings.HasSuffix(client.Server, "/") {
-		client.Server += "/"
-	}
-	// create httpClient, if not already present
-	if client.Client == nil {
-		client.Client = http.DefaultClient
-	}
-	return &client, nil
-}
-
-// WithHTTPClient allows overriding the default Doer, which is
-// automatically created using http.Client. This is useful for tests.
-func WithHTTPClient(doer HttpRequestDoer) ClientOption {
-	return func(c *Client) error {
-		c.Client = doer
-		return nil
-	}
-}
-
-// WithRequestEditorFn allows setting up a callback function, which will be
-// called right before sending the request. This can be used to mutate the request.
-func WithRequestEditorFn(fn RequestEditorFn) ClientOption {
-	return func(c *Client) error {
-		c.RequestEditor = fn
-		return nil
-	}
-}
-
-// The interface specification for the client above.
-type ClientInterface interface {
-	// PostJobQueueV1Jobs request  with any body
-	PostJobQueueV1JobsWithBody(ctx context.Context, contentType string, body io.Reader) (*http.Response, error)
-
-	PostJobQueueV1Jobs(ctx context.Context, body PostJobQueueV1JobsJSONRequestBody) (*http.Response, error)
-
-	// GetJobQueueV1JobsJobId request
-	GetJobQueueV1JobsJobId(ctx context.Context, jobId string) (*http.Response, error)
-
-	// PatchJobQueueV1JobsJobId request  with any body
-	PatchJobQueueV1JobsJobIdWithBody(ctx context.Context, jobId string, contentType string, body io.Reader) (*http.Response, error)
-
-	PatchJobQueueV1JobsJobId(ctx context.Context, jobId string, body PatchJobQueueV1JobsJobIdJSONRequestBody) (*http.Response, error)
-
-	// PostJobQueueV1JobsJobIdArtifactsName request  with any body
-	PostJobQueueV1JobsJobIdArtifactsNameWithBody(ctx context.Context, jobId string, name string, contentType string, body io.Reader) (*http.Response, error)
-
-	// GetStatus request
-	GetStatus(ctx context.Context) (*http.Response, error)
-}
-
-func (c *Client) PostJobQueueV1JobsWithBody(ctx context.Context, contentType string, body io.Reader) (*http.Response, error) {
-	req, err := NewPostJobQueueV1JobsRequestWithBody(c.Server, contentType, body)
-	if err != nil {
-		return nil, err
-	}
-	req = req.WithContext(ctx)
-	if c.RequestEditor != nil {
-		err = c.RequestEditor(ctx, req)
-		if err != nil {
-			return nil, err
-		}
-	}
-	return c.Client.Do(req)
-}
-
-func (c *Client) PostJobQueueV1Jobs(ctx context.Context, body PostJobQueueV1JobsJSONRequestBody) (*http.Response, error) {
-	req, err := NewPostJobQueueV1JobsRequest(c.Server, body)
-	if err != nil {
-		return nil, err
-	}
-	req = req.WithContext(ctx)
-	if c.RequestEditor != nil {
-		err = c.RequestEditor(ctx, req)
-		if err != nil {
-			return nil, err
-		}
-	}
-	return c.Client.Do(req)
-}
-
-func (c *Client) GetJobQueueV1JobsJobId(ctx context.Context, jobId string) (*http.Response, error) {
-	req, err := NewGetJobQueueV1JobsJobIdRequest(c.Server, jobId)
-	if err != nil {
-		return nil, err
-	}
-	req = req.WithContext(ctx)
-	if c.RequestEditor != nil {
-		err = c.RequestEditor(ctx, req)
-		if err != nil {
-			return nil, err
-		}
-	}
-	return c.Client.Do(req)
-}
-
-func (c *Client) PatchJobQueueV1JobsJobIdWithBody(ctx context.Context, jobId string, contentType string, body io.Reader) (*http.Response, error) {
-	req, err := NewPatchJobQueueV1JobsJobIdRequestWithBody(c.Server, jobId, contentType, body)
-	if err != nil {
-		return nil, err
-	}
-	req = req.WithContext(ctx)
-	if c.RequestEditor != nil {
-		err = c.RequestEditor(ctx, req)
-		if err != nil {
-			return nil, err
-		}
-	}
-	return c.Client.Do(req)
-}
-
-func (c *Client) PatchJobQueueV1JobsJobId(ctx context.Context, jobId string, body PatchJobQueueV1JobsJobIdJSONRequestBody) (*http.Response, error) {
-	req, err := NewPatchJobQueueV1JobsJobIdRequest(c.Server, jobId, body)
-	if err != nil {
-		return nil, err
-	}
-	req = req.WithContext(ctx)
-	if c.RequestEditor != nil {
-		err = c.RequestEditor(ctx, req)
-		if err != nil {
-			return nil, err
-		}
-	}
-	return c.Client.Do(req)
+// Error defines model for Error.
+type Error struct {
+	Message string `json:"message"`
 }
 
-func (c *Client) PostJobQueueV1JobsJobIdArtifactsNameWithBody(ctx context.Context, jobId string, name string, contentType string, body io.Reader) (*http.Response, error) {
-	req, err := NewPostJobQueueV1JobsJobIdArtifactsNameRequestWithBody(c.Server, jobId, name, contentType, body)
-	if err != nil {
-		return nil, err
-	}
-	req = req.WithContext(ctx)
-	if c.RequestEditor != nil {
-		err = c.RequestEditor(ctx, req)
-		if err != nil {
-			return nil, err
-		}
-	}
-	return c.Client.Do(req)
-}
+// RequestJobJSONBody defines parameters for RequestJob.
+type RequestJobJSONBody map[string]interface{}
 
-func (c *Client) GetStatus(ctx context.Context) (*http.Response, error) {
-	req, err := NewGetStatusRequest(c.Server)
-	if err != nil {
-		return nil, err
-	}
-	req = req.WithContext(ctx)
-	if c.RequestEditor != nil {
-		err = c.RequestEditor(ctx, req)
-		if err != nil {
-			return nil, err
-		}
-	}
-	return c.Client.Do(req)
-}
-
-// NewPostJobQueueV1JobsRequest calls the generic PostJobQueueV1Jobs builder with application/json body
-func NewPostJobQueueV1JobsRequest(server string, body PostJobQueueV1JobsJSONRequestBody) (*http.Request, error) {
-	var bodyReader io.Reader
-	buf, err := json.Marshal(body)
-	if err != nil {
-		return nil, err
-	}
-	bodyReader = bytes.NewReader(buf)
-	return NewPostJobQueueV1JobsRequestWithBody(server, "application/json", bodyReader)
-}
-
-// NewPostJobQueueV1JobsRequestWithBody generates requests for PostJobQueueV1Jobs with any type of body
-func NewPostJobQueueV1JobsRequestWithBody(server string, contentType string, body io.Reader) (*http.Request, error) {
-	var err error
-
-	queryUrl, err := url.Parse(server)
-	if err != nil {
-		return nil, err
-	}
-
-	basePath := fmt.Sprintf("/job-queue/v1/jobs")
-	if basePath[0] == '/' {
-		basePath = basePath[1:]
-	}
-
-	queryUrl, err = queryUrl.Parse(basePath)
-	if err != nil {
-		return nil, err
-	}
-
-	req, err := http.NewRequest("POST", queryUrl.String(), body)
-	if err != nil {
-		return nil, err
-	}
-
-	req.Header.Add("Content-Type", contentType)
-	return req, nil
-}
-
-// NewGetJobQueueV1JobsJobIdRequest generates requests for GetJobQueueV1JobsJobId
-func NewGetJobQueueV1JobsJobIdRequest(server string, jobId string) (*http.Request, error) {
-	var err error
-
-	var pathParam0 string
-
-	pathParam0, err = runtime.StyleParam("simple", false, "job_id", jobId)
-	if err != nil {
-		return nil, err
-	}
-
-	queryUrl, err := url.Parse(server)
-	if err != nil {
-		return nil, err
-	}
-
-	basePath := fmt.Sprintf("/job-queue/v1/jobs/%s", pathParam0)
-	if basePath[0] == '/' {
-		basePath = basePath[1:]
-	}
-
-	queryUrl, err = queryUrl.Parse(basePath)
-	if err != nil {
-		return nil, err
-	}
-
-	req, err := http.NewRequest("GET", queryUrl.String(), nil)
-	if err != nil {
-		return nil, err
-	}
-
-	return req, nil
-}
-
-// NewPatchJobQueueV1JobsJobIdRequest calls the generic PatchJobQueueV1JobsJobId builder with application/json body
-func NewPatchJobQueueV1JobsJobIdRequest(server string, jobId string, body PatchJobQueueV1JobsJobIdJSONRequestBody) (*http.Request, error) {
-	var bodyReader io.Reader
-	buf, err := json.Marshal(body)
-	if err != nil {
-		return nil, err
-	}
-	bodyReader = bytes.NewReader(buf)
-	return NewPatchJobQueueV1JobsJobIdRequestWithBody(server, jobId, "application/json", bodyReader)
-}
-
-// NewPatchJobQueueV1JobsJobIdRequestWithBody generates requests for PatchJobQueueV1JobsJobId with any type of body
-func NewPatchJobQueueV1JobsJobIdRequestWithBody(server string, jobId string, contentType string, body io.Reader) (*http.Request, error) {
-	var err error
-
-	var pathParam0 string
-
-	pathParam0, err = runtime.StyleParam("simple", false, "job_id", jobId)
-	if err != nil {
-		return nil, err
-	}
-
-	queryUrl, err := url.Parse(server)
-	if err != nil {
-		return nil, err
-	}
-
-	basePath := fmt.Sprintf("/job-queue/v1/jobs/%s", pathParam0)
-	if basePath[0] == '/' {
-		basePath = basePath[1:]
-	}
-
-	queryUrl, err = queryUrl.Parse(basePath)
-	if err != nil {
-		return nil, err
-	}
-
-	req, err := http.NewRequest("PATCH", queryUrl.String(), body)
-	if err != nil {
-		return nil, err
-	}
-
-	req.Header.Add("Content-Type", contentType)
-	return req, nil
-}
-
-// NewPostJobQueueV1JobsJobIdArtifactsNameRequestWithBody generates requests for PostJobQueueV1JobsJobIdArtifactsName with any type of body
-func NewPostJobQueueV1JobsJobIdArtifactsNameRequestWithBody(server string, jobId string, name string, contentType string, body io.Reader) (*http.Request, error) {
-	var err error
-
-	var pathParam0 string
-
-	pathParam0, err = runtime.StyleParam("simple", false, "job_id", jobId)
-	if err != nil {
-		return nil, err
-	}
-
-	var pathParam1 string
-
-	pathParam1, err = runtime.StyleParam("simple", false, "name", name)
-	if err != nil {
-		return nil, err
-	}
-
-	queryUrl, err := url.Parse(server)
-	if err != nil {
-		return nil, err
-	}
-
-	basePath := fmt.Sprintf("/job-queue/v1/jobs/%s/artifacts/%s", pathParam0, pathParam1)
-	if basePath[0] == '/' {
-		basePath = basePath[1:]
-	}
-
-	queryUrl, err = queryUrl.Parse(basePath)
-	if err != nil {
-		return nil, err
-	}
-
-	req, err := http.NewRequest("POST", queryUrl.String(), body)
-	if err != nil {
-		return nil, err
-	}
-
-	req.Header.Add("Content-Type", contentType)
-	return req, nil
-}
-
-// NewGetStatusRequest generates requests for GetStatus
-func NewGetStatusRequest(server string) (*http.Request, error) {
-	var err error
-
-	queryUrl, err := url.Parse(server)
-	if err != nil {
-		return nil, err
-	}
-
-	basePath := fmt.Sprintf("/status")
-	if basePath[0] == '/' {
-		basePath = basePath[1:]
-	}
-
-	queryUrl, err = queryUrl.Parse(basePath)
-	if err != nil {
-		return nil, err
-	}
-
-	req, err := http.NewRequest("GET", queryUrl.String(), nil)
-	if err != nil {
-		return nil, err
-	}
-
-	return req, nil
-}
-
-// ClientWithResponses builds on ClientInterface to offer response payloads
-type ClientWithResponses struct {
-	ClientInterface
-}
-
-// NewClientWithResponses creates a new ClientWithResponses, which wraps
-// Client with return type handling
-func NewClientWithResponses(server string, opts ...ClientOption) (*ClientWithResponses, error) {
-	client, err := NewClient(server, opts...)
-	if err != nil {
-		return nil, err
-	}
-	return &ClientWithResponses{client}, nil
-}
-
-// WithBaseURL overrides the baseURL.
-func WithBaseURL(baseURL string) ClientOption {
-	return func(c *Client) error {
-		newBaseURL, err := url.Parse(baseURL)
-		if err != nil {
-			return err
-		}
-		c.Server = newBaseURL.String()
-		return nil
-	}
-}
-
-// ClientWithResponsesInterface is the interface specification for the client with responses above.
-type ClientWithResponsesInterface interface {
-	// PostJobQueueV1Jobs request  with any body
-	PostJobQueueV1JobsWithBodyWithResponse(ctx context.Context, contentType string, body io.Reader) (*PostJobQueueV1JobsResponse, error)
-
-	PostJobQueueV1JobsWithResponse(ctx context.Context, body PostJobQueueV1JobsJSONRequestBody) (*PostJobQueueV1JobsResponse, error)
-
-	// GetJobQueueV1JobsJobId request
-	GetJobQueueV1JobsJobIdWithResponse(ctx context.Context, jobId string) (*GetJobQueueV1JobsJobIdResponse, error)
-
-	// PatchJobQueueV1JobsJobId request  with any body
-	PatchJobQueueV1JobsJobIdWithBodyWithResponse(ctx context.Context, jobId string, contentType string, body io.Reader) (*PatchJobQueueV1JobsJobIdResponse, error)
-
-	PatchJobQueueV1JobsJobIdWithResponse(ctx context.Context, jobId string, body PatchJobQueueV1JobsJobIdJSONRequestBody) (*PatchJobQueueV1JobsJobIdResponse, error)
-
-	// PostJobQueueV1JobsJobIdArtifactsName request  with any body
-	PostJobQueueV1JobsJobIdArtifactsNameWithBodyWithResponse(ctx context.Context, jobId string, name string, contentType string, body io.Reader) (*PostJobQueueV1JobsJobIdArtifactsNameResponse, error)
-
-	// GetStatus request
-	GetStatusWithResponse(ctx context.Context) (*GetStatusResponse, error)
-}
-
-type PostJobQueueV1JobsResponse struct {
-	Body         []byte
-	HTTPResponse *http.Response
-	JSON200      *struct {
-		Id       string        `json:"id"`
-		Manifest interface{}   `json:"manifest"`
-		Targets  []interface{} `json:"targets"`
-	}
-}
-
-// Status returns HTTPResponse.Status
-func (r PostJobQueueV1JobsResponse) Status() string {
-	if r.HTTPResponse != nil {
-		return r.HTTPResponse.Status
-	}
-	return http.StatusText(0)
-}
-
-// StatusCode returns HTTPResponse.StatusCode
-func (r PostJobQueueV1JobsResponse) StatusCode() int {
-	if r.HTTPResponse != nil {
-		return r.HTTPResponse.StatusCode
-	}
-	return 0
-}
-
-type GetJobQueueV1JobsJobIdResponse struct {
-	Body         []byte
-	HTTPResponse *http.Response
-	JSON200      *struct {
-		Canceled bool   `json:"canceled"`
-		Id       string `json:"id"`
-	}
-}
-
-// Status returns HTTPResponse.Status
-func (r GetJobQueueV1JobsJobIdResponse) Status() string {
-	if r.HTTPResponse != nil {
-		return r.HTTPResponse.Status
-	}
-	return http.StatusText(0)
-}
-
-// StatusCode returns HTTPResponse.StatusCode
-func (r GetJobQueueV1JobsJobIdResponse) StatusCode() int {
-	if r.HTTPResponse != nil {
-		return r.HTTPResponse.StatusCode
-	}
-	return 0
-}
-
-type PatchJobQueueV1JobsJobIdResponse struct {
-	Body         []byte
-	HTTPResponse *http.Response
-}
-
-// Status returns HTTPResponse.Status
-func (r PatchJobQueueV1JobsJobIdResponse) Status() string {
-	if r.HTTPResponse != nil {
-		return r.HTTPResponse.Status
-	}
-	return http.StatusText(0)
-}
-
-// StatusCode returns HTTPResponse.StatusCode
-func (r PatchJobQueueV1JobsJobIdResponse) StatusCode() int {
-	if r.HTTPResponse != nil {
-		return r.HTTPResponse.StatusCode
-	}
-	return 0
-}
-
-type PostJobQueueV1JobsJobIdArtifactsNameResponse struct {
-	Body         []byte
-	HTTPResponse *http.Response
-}
-
-// Status returns HTTPResponse.Status
-func (r PostJobQueueV1JobsJobIdArtifactsNameResponse) Status() string {
-	if r.HTTPResponse != nil {
-		return r.HTTPResponse.Status
-	}
-	return http.StatusText(0)
-}
-
-// StatusCode returns HTTPResponse.StatusCode
-func (r PostJobQueueV1JobsJobIdArtifactsNameResponse) StatusCode() int {
-	if r.HTTPResponse != nil {
-		return r.HTTPResponse.StatusCode
-	}
-	return 0
-}
-
-type GetStatusResponse struct {
-	Body         []byte
-	HTTPResponse *http.Response
-	JSON200      *struct {
-		Status string `json:"status"`
-	}
-}
-
-// Status returns HTTPResponse.Status
-func (r GetStatusResponse) Status() string {
-	if r.HTTPResponse != nil {
-		return r.HTTPResponse.Status
-	}
-	return http.StatusText(0)
-}
-
-// StatusCode returns HTTPResponse.StatusCode
-func (r GetStatusResponse) StatusCode() int {
-	if r.HTTPResponse != nil {
-		return r.HTTPResponse.StatusCode
-	}
-	return 0
-}
-
-// PostJobQueueV1JobsWithBodyWithResponse request with arbitrary body returning *PostJobQueueV1JobsResponse
-func (c *ClientWithResponses) PostJobQueueV1JobsWithBodyWithResponse(ctx context.Context, contentType string, body io.Reader) (*PostJobQueueV1JobsResponse, error) {
-	rsp, err := c.PostJobQueueV1JobsWithBody(ctx, contentType, body)
-	if err != nil {
-		return nil, err
-	}
-	return ParsePostJobQueueV1JobsResponse(rsp)
-}
-
-func (c *ClientWithResponses) PostJobQueueV1JobsWithResponse(ctx context.Context, body PostJobQueueV1JobsJSONRequestBody) (*PostJobQueueV1JobsResponse, error) {
-	rsp, err := c.PostJobQueueV1Jobs(ctx, body)
-	if err != nil {
-		return nil, err
-	}
-	return ParsePostJobQueueV1JobsResponse(rsp)
-}
-
-// GetJobQueueV1JobsJobIdWithResponse request returning *GetJobQueueV1JobsJobIdResponse
-func (c *ClientWithResponses) GetJobQueueV1JobsJobIdWithResponse(ctx context.Context, jobId string) (*GetJobQueueV1JobsJobIdResponse, error) {
-	rsp, err := c.GetJobQueueV1JobsJobId(ctx, jobId)
-	if err != nil {
-		return nil, err
-	}
-	return ParseGetJobQueueV1JobsJobIdResponse(rsp)
-}
-
-// PatchJobQueueV1JobsJobIdWithBodyWithResponse request with arbitrary body returning *PatchJobQueueV1JobsJobIdResponse
-func (c *ClientWithResponses) PatchJobQueueV1JobsJobIdWithBodyWithResponse(ctx context.Context, jobId string, contentType string, body io.Reader) (*PatchJobQueueV1JobsJobIdResponse, error) {
-	rsp, err := c.PatchJobQueueV1JobsJobIdWithBody(ctx, jobId, contentType, body)
-	if err != nil {
-		return nil, err
-	}
-	return ParsePatchJobQueueV1JobsJobIdResponse(rsp)
-}
-
-func (c *ClientWithResponses) PatchJobQueueV1JobsJobIdWithResponse(ctx context.Context, jobId string, body PatchJobQueueV1JobsJobIdJSONRequestBody) (*PatchJobQueueV1JobsJobIdResponse, error) {
-	rsp, err := c.PatchJobQueueV1JobsJobId(ctx, jobId, body)
-	if err != nil {
-		return nil, err
-	}
-	return ParsePatchJobQueueV1JobsJobIdResponse(rsp)
-}
-
-// PostJobQueueV1JobsJobIdArtifactsNameWithBodyWithResponse request with arbitrary body returning *PostJobQueueV1JobsJobIdArtifactsNameResponse
-func (c *ClientWithResponses) PostJobQueueV1JobsJobIdArtifactsNameWithBodyWithResponse(ctx context.Context, jobId string, name string, contentType string, body io.Reader) (*PostJobQueueV1JobsJobIdArtifactsNameResponse, error) {
-	rsp, err := c.PostJobQueueV1JobsJobIdArtifactsNameWithBody(ctx, jobId, name, contentType, body)
-	if err != nil {
-		return nil, err
-	}
-	return ParsePostJobQueueV1JobsJobIdArtifactsNameResponse(rsp)
-}
-
-// GetStatusWithResponse request returning *GetStatusResponse
-func (c *ClientWithResponses) GetStatusWithResponse(ctx context.Context) (*GetStatusResponse, error) {
-	rsp, err := c.GetStatus(ctx)
-	if err != nil {
-		return nil, err
-	}
-	return ParseGetStatusResponse(rsp)
-}
-
-// ParsePostJobQueueV1JobsResponse parses an HTTP response from a PostJobQueueV1JobsWithResponse call
-func ParsePostJobQueueV1JobsResponse(rsp *http.Response) (*PostJobQueueV1JobsResponse, error) {
-	bodyBytes, err := ioutil.ReadAll(rsp.Body)
-	defer rsp.Body.Close()
-	if err != nil {
-		return nil, err
-	}
-
-	response := &PostJobQueueV1JobsResponse{
-		Body:         bodyBytes,
-		HTTPResponse: rsp,
-	}
-
-	switch {
-	case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200:
-		var dest struct {
-			Id       string        `json:"id"`
-			Manifest interface{}   `json:"manifest"`
-			Targets  []interface{} `json:"targets"`
-		}
-		if err := json.Unmarshal(bodyBytes, &dest); err != nil {
-			return nil, err
-		}
-		response.JSON200 = &dest
-
-	}
-
-	return response, nil
-}
-
-// ParseGetJobQueueV1JobsJobIdResponse parses an HTTP response from a GetJobQueueV1JobsJobIdWithResponse call
-func ParseGetJobQueueV1JobsJobIdResponse(rsp *http.Response) (*GetJobQueueV1JobsJobIdResponse, error) {
-	bodyBytes, err := ioutil.ReadAll(rsp.Body)
-	defer rsp.Body.Close()
-	if err != nil {
-		return nil, err
-	}
-
-	response := &GetJobQueueV1JobsJobIdResponse{
-		Body:         bodyBytes,
-		HTTPResponse: rsp,
-	}
-
-	switch {
-	case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200:
-		var dest struct {
-			Canceled bool   `json:"canceled"`
-			Id       string `json:"id"`
-		}
-		if err := json.Unmarshal(bodyBytes, &dest); err != nil {
-			return nil, err
-		}
-		response.JSON200 = &dest
-
-	}
-
-	return response, nil
-}
-
-// ParsePatchJobQueueV1JobsJobIdResponse parses an HTTP response from a PatchJobQueueV1JobsJobIdWithResponse call
-func ParsePatchJobQueueV1JobsJobIdResponse(rsp *http.Response) (*PatchJobQueueV1JobsJobIdResponse, error) {
-	bodyBytes, err := ioutil.ReadAll(rsp.Body)
-	defer rsp.Body.Close()
-	if err != nil {
-		return nil, err
-	}
-
-	response := &PatchJobQueueV1JobsJobIdResponse{
-		Body:         bodyBytes,
-		HTTPResponse: rsp,
-	}
-
-	switch {
-	}
-
-	return response, nil
-}
-
-// ParsePostJobQueueV1JobsJobIdArtifactsNameResponse parses an HTTP response from a PostJobQueueV1JobsJobIdArtifactsNameWithResponse call
-func ParsePostJobQueueV1JobsJobIdArtifactsNameResponse(rsp *http.Response) (*PostJobQueueV1JobsJobIdArtifactsNameResponse, error) {
-	bodyBytes, err := ioutil.ReadAll(rsp.Body)
-	defer rsp.Body.Close()
-	if err != nil {
-		return nil, err
-	}
-
-	response := &PostJobQueueV1JobsJobIdArtifactsNameResponse{
-		Body:         bodyBytes,
-		HTTPResponse: rsp,
-	}
-
-	switch {
-	}
-
-	return response, nil
+// UpdateJobJSONBody defines parameters for UpdateJob.
+type UpdateJobJSONBody struct {
+	Result interface{} `json:"result"`
+	Status string      `json:"status"`
 }
 
-// ParseGetStatusResponse parses an HTTP response from a GetStatusWithResponse call
-func ParseGetStatusResponse(rsp *http.Response) (*GetStatusResponse, error) {
-	bodyBytes, err := ioutil.ReadAll(rsp.Body)
-	defer rsp.Body.Close()
-	if err != nil {
-		return nil, err
-	}
-
-	response := &GetStatusResponse{
-		Body:         bodyBytes,
-		HTTPResponse: rsp,
-	}
+// RequestJobRequestBody defines body for RequestJob for application/json ContentType.
+type RequestJobJSONRequestBody RequestJobJSONBody
 
-	switch {
-	case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200:
-		var dest struct {
-			Status string `json:"status"`
-		}
-		if err := json.Unmarshal(bodyBytes, &dest); err != nil {
-			return nil, err
-		}
-		response.JSON200 = &dest
-
-	}
-
-	return response, nil
-}
+// UpdateJobRequestBody defines body for UpdateJob for application/json ContentType.
+type UpdateJobJSONRequestBody UpdateJobJSONBody
 
 // ServerInterface represents all server handlers.
 type ServerInterface interface {
-	// create-job
-	// (POST /job-queue/v1/jobs)
-	PostJobQueueV1Jobs(ctx echo.Context) error
-	// get-job
-	// (GET /job-queue/v1/jobs/{job_id})
-	GetJobQueueV1JobsJobId(ctx echo.Context, jobId string) error
-	// update-job
-	// (PATCH /job-queue/v1/jobs/{job_id})
-	PatchJobQueueV1JobsJobId(ctx echo.Context, jobId string) error
-	// add-image
-	// (POST /job-queue/v1/jobs/{job_id}/artifacts/{name})
-	PostJobQueueV1JobsJobIdArtifactsName(ctx echo.Context, jobId string, name string) error
+	// Request a job
+	// (POST /jobs)
+	RequestJob(ctx echo.Context) error
+	// Get running job
+	// (GET /jobs/{token})
+	GetJob(ctx echo.Context, token string) error
+	// Update a running job
+	// (PATCH /jobs/{token})
+	UpdateJob(ctx echo.Context, token string) error
+	// Upload an artifact
+	// (PUT /jobs/{token}/artifacts/{name})
+	UploadJobArtifact(ctx echo.Context, token string, name string) error
 	// status
 	// (GET /status)
 	GetStatus(ctx echo.Context) error
@@ -792,56 +54,56 @@ type ServerInterfaceWrapper struct {
 	Handler ServerInterface
 }
 
-// PostJobQueueV1Jobs converts echo context to params.
-func (w *ServerInterfaceWrapper) PostJobQueueV1Jobs(ctx echo.Context) error {
+// RequestJob converts echo context to params.
+func (w *ServerInterfaceWrapper) RequestJob(ctx echo.Context) error {
 	var err error
 
 	// Invoke the callback with all the unmarshalled arguments
-	err = w.Handler.PostJobQueueV1Jobs(ctx)
+	err = w.Handler.RequestJob(ctx)
 	return err
 }
 
-// GetJobQueueV1JobsJobId converts echo context to params.
-func (w *ServerInterfaceWrapper) GetJobQueueV1JobsJobId(ctx echo.Context) error {
+// GetJob converts echo context to params.
+func (w *ServerInterfaceWrapper) GetJob(ctx echo.Context) error {
 	var err error
-	// ------------- Path parameter "job_id" -------------
-	var jobId string
+	// ------------- Path parameter "token" -------------
+	var token string
 
-	err = runtime.BindStyledParameter("simple", false, "job_id", ctx.Param("job_id"), &jobId)
+	err = runtime.BindStyledParameter("simple", false, "token", ctx.Param("token"), &token)
 	if err != nil {
-		return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter job_id: %s", err))
+		return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter token: %s", err))
 	}
 
 	// Invoke the callback with all the unmarshalled arguments
-	err = w.Handler.GetJobQueueV1JobsJobId(ctx, jobId)
+	err = w.Handler.GetJob(ctx, token)
 	return err
 }
 
-// PatchJobQueueV1JobsJobId converts echo context to params.
-func (w *ServerInterfaceWrapper) PatchJobQueueV1JobsJobId(ctx echo.Context) error {
+// UpdateJob converts echo context to params.
+func (w *ServerInterfaceWrapper) UpdateJob(ctx echo.Context) error {
 	var err error
-	// ------------- Path parameter "job_id" -------------
-	var jobId string
+	// ------------- Path parameter "token" -------------
+	var token string
 
-	err = runtime.BindStyledParameter("simple", false, "job_id", ctx.Param("job_id"), &jobId)
+	err = runtime.BindStyledParameter("simple", false, "token", ctx.Param("token"), &token)
 	if err != nil {
-		return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter job_id: %s", err))
+		return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter token: %s", err))
 	}
 
 	// Invoke the callback with all the unmarshalled arguments
-	err = w.Handler.PatchJobQueueV1JobsJobId(ctx, jobId)
+	err = w.Handler.UpdateJob(ctx, token)
 	return err
 }
 
-// PostJobQueueV1JobsJobIdArtifactsName converts echo context to params.
-func (w *ServerInterfaceWrapper) PostJobQueueV1JobsJobIdArtifactsName(ctx echo.Context) error {
+// UploadJobArtifact converts echo context to params.
+func (w *ServerInterfaceWrapper) UploadJobArtifact(ctx echo.Context) error {
 	var err error
-	// ------------- Path parameter "job_id" -------------
-	var jobId string
+	// ------------- Path parameter "token" -------------
+	var token string
 
-	err = runtime.BindStyledParameter("simple", false, "job_id", ctx.Param("job_id"), &jobId)
+	err = runtime.BindStyledParameter("simple", false, "token", ctx.Param("token"), &token)
 	if err != nil {
-		return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter job_id: %s", err))
+		return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter token: %s", err))
 	}
 
 	// ------------- Path parameter "name" -------------
@@ -853,7 +115,7 @@ func (w *ServerInterfaceWrapper) PostJobQueueV1JobsJobIdArtifactsName(ctx echo.C
 	}
 
 	// Invoke the callback with all the unmarshalled arguments
-	err = w.Handler.PostJobQueueV1JobsJobIdArtifactsName(ctx, jobId, name)
+	err = w.Handler.UploadJobArtifact(ctx, token, name)
 	return err
 }
 
@@ -888,10 +150,10 @@ func RegisterHandlers(router EchoRouter, si ServerInterface) {
 		Handler: si,
 	}
 
-	router.POST("/job-queue/v1/jobs", wrapper.PostJobQueueV1Jobs)
-	router.GET("/job-queue/v1/jobs/:job_id", wrapper.GetJobQueueV1JobsJobId)
-	router.PATCH("/job-queue/v1/jobs/:job_id", wrapper.PatchJobQueueV1JobsJobId)
-	router.POST("/job-queue/v1/jobs/:job_id/artifacts/:name", wrapper.PostJobQueueV1JobsJobIdArtifactsName)
+	router.POST("/jobs", wrapper.RequestJob)
+	router.GET("/jobs/:token", wrapper.GetJob)
+	router.PATCH("/jobs/:token", wrapper.UpdateJob)
+	router.PUT("/jobs/:token/artifacts/:name", wrapper.UploadJobArtifact)
 	router.GET("/status", wrapper.GetStatus)
 
 }
diff --git a/internal/worker/api/api.go b/internal/worker/api/api.go
index 7e9674759..b5f2978c4 100644
--- a/internal/worker/api/api.go
+++ b/internal/worker/api/api.go
@@ -1,3 +1,3 @@
-//go:generate go run github.com/deepmap/oapi-codegen/cmd/oapi-codegen -package=api -generate types,server,client -o api.gen.go openapi.yml
+//go:generate go run github.com/deepmap/oapi-codegen/cmd/oapi-codegen -package=api -generate types,server -o api.gen.go openapi.yml
 
 package api
diff --git a/internal/worker/api/openapi.yml b/internal/worker/api/openapi.yml
index 40356ed07..e1b132c1f 100644
--- a/internal/worker/api/openapi.yml
+++ b/internal/worker/api/openapi.yml
@@ -23,48 +23,77 @@ paths:
                       - OK
                 required:
                   - status
-      operationId: get-status
+        4XX:
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/Error'
+        5XX:
+          description: ''
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/Error'
+      operationId: GetStatus
       description: Simple status handler to check whether the service is up.
-  /job-queue/v1/jobs:
+  /jobs:
     post:
-      summary: create-job
+      summary: Request a job
       tags: []
       responses:
-        '200':
-          description: OK
+        '201':
+          description: Created
           content:
             application/json:
               schema:
                 type: object
                 additionalProperties: false
                 properties:
-                  id:
-                    type: string
-                    format: uuid
                   manifest: {}
                   targets:
                     type: array
                     items: {}
+                  location:
+                    type: string
+                  artifact_location:
+                    type: string
+                  id:
+                    type: string
+                    format: uuid
                 required:
-                  - id
                   - manifest
-                  - targets
-      operationId: post-job-queue-v1-jobs
+                  - location
+                  - id
+        4XX:
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/Error'
+        5XX:
+          description: ''
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/Error'
+      operationId: RequestJob
       requestBody:
         content:
           application/json:
             schema:
               type: object
-              properties: {}
-  '/job-queue/v1/jobs/{job_id}':
+              additionalProperties: false
+        description: ''
+      description: Requests a job. This operation blocks until a job is available.
+    parameters: []
+  '/jobs/{token}':
     parameters:
       - schema:
           type: string
-        name: job_id
+        name: token
         in: path
         required: true
     get:
-      summary: get-job
+      summary: Get running job
       tags: []
       responses:
         '200':
@@ -74,20 +103,29 @@ paths:
               schema:
                 type: object
                 properties:
-                  id:
-                    type: string
-                    format: uuid
                   canceled:
                     type: boolean
                 required:
-                  - id
                   - canceled
-      operationId: get-job-queue-v1-jobs-job_id
+        4XX:
+          description: ''
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/Error'
+        5XX:
+          description: ''
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/Error'
+      operationId: GetJob
+      description: ''
     patch:
-      summary: update-job
+      summary: Update a running job
       tags: []
       responses: {}
-      operationId: patch-job-queue-v1-jobs-job_id
+      operationId: UpdateJob
       requestBody:
         content:
           application/json:
@@ -105,29 +143,48 @@ paths:
               required:
                 - status
                 - result
-  '/job-queue/v1/jobs/{job_id}/artifacts/{name}':
+  '/jobs/{token}/artifacts/{name}':
     parameters:
       - schema:
           type: string
-        name: job_id
+        name: name
         in: path
         required: true
       - schema:
           type: string
-        name: name
+        name: token
         in: path
         required: true
-    post:
-      summary: add-image
+    put:
+      summary: Upload an artifact
       tags: []
       responses:
         '200':
           description: OK
-      operationId: post-job-queue-v1-jobs-job_id-artifacts-name
+        4XX:
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/Error'
+        5XX:
+          description: ''
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/Error'
+      operationId: UploadJobArtifact
       requestBody:
         content:
           application/octet-stream:
             schema:
               type: string
 components:
-  schemas: {}
+  schemas:
+    Error:
+      title: Error
+      type: object
+      properties:
+        message:
+          type: string
+      required:
+        - message
diff --git a/internal/worker/client.go b/internal/worker/client.go
index 331f59520..8b6db3aed 100644
--- a/internal/worker/client.go
+++ b/internal/worker/client.go
@@ -1,17 +1,17 @@
 package worker
 
 import (
+	"bytes"
 	"context"
 	"crypto/tls"
 	"encoding/json"
-	"errors"
 	"fmt"
 	"io"
 	"net"
 	"net/http"
+	"net/url"
 
 	"github.com/google/uuid"
-
 	"github.com/osbuild/osbuild-composer/internal/common"
 	"github.com/osbuild/osbuild-composer/internal/distro"
 	"github.com/osbuild/osbuild-composer/internal/osbuild"
@@ -20,32 +20,49 @@ import (
 )
 
 type Client struct {
-	api *api.Client
+	server    *url.URL
+	requester *http.Client
+}
+
+type Job interface {
+	Id() uuid.UUID
+	OSBuildArgs() (distro.Manifest, []*target.Target, error)
+	Update(status common.ImageBuildState, result *osbuild.Result) error
+	Canceled() (bool, error)
+	UploadArtifact(name string, reader io.Reader) error
 }
 
-type Job struct {
-	Id       uuid.UUID
-	Manifest distro.Manifest
-	Targets  []*target.Target
+type job struct {
+	requester        *http.Client
+	id               uuid.UUID
+	manifest         distro.Manifest
+	targets          []*target.Target
+	location         string
+	artifactLocation string
 }
 
 func NewClient(baseURL string, conf *tls.Config) (*Client, error) {
-	httpClient := http.Client{
+	server, err := url.Parse(baseURL)
+	if err != nil {
+		return nil, err
+	}
+
+	requester := &http.Client{
 		Transport: &http.Transport{
 			TLSClientConfig: conf,
 		},
 	}
 
-	c, err := api.NewClient(baseURL, api.WithHTTPClient(&httpClient))
-	if err != nil {
-		return nil, err
-	}
-
-	return &Client{c}, nil
+	return &Client{server, requester}, nil
 }
 
 func NewClientUnix(path string) *Client {
-	httpClient := http.Client{
+	server, err := url.Parse("http://localhost")
+	if err != nil {
+		panic(err)
+	}
+
+	requester := &http.Client{
 		Transport: &http.Transport{
 			DialContext: func(context context.Context, network, addr string) (net.Conn, error) {
 				return net.Dial("unix", path)
@@ -53,79 +70,158 @@ func NewClientUnix(path string) *Client {
 		},
 	}
 
-	c, err := api.NewClient("http://localhost", api.WithHTTPClient(&httpClient))
+	return &Client{server, requester}
+}
+
+func (c *Client) RequestJob() (Job, error) {
+	url, err := c.server.Parse("/jobs")
 	if err != nil {
+		// This only happens when "/jobs" cannot be parsed.
 		panic(err)
 	}
 
-	return &Client{c}
-}
+	var buf bytes.Buffer
+	err = json.NewEncoder(&buf).Encode(api.RequestJobJSONRequestBody{})
+	if err != nil {
+		panic(err)
+	}
 
-func (c *Client) AddJob() (*Job, error) {
-	response, err := c.api.PostJobQueueV1Jobs(context.Background(), api.PostJobQueueV1JobsJSONRequestBody{})
+	response, err := c.requester.Post(url.String(), "application/json", &buf)
 	if err != nil {
-		return nil, err
+		return nil, fmt.Errorf("error requesting job: %v", err)
 	}
 	defer response.Body.Close()
 
 	if response.StatusCode != http.StatusCreated {
-		var er errorResponse
-		_ = json.NewDecoder(response.Body).Decode(&er)
-		return nil, fmt.Errorf("couldn't create job, got %d: %s", response.StatusCode, er.Message)
+		return nil, errorFromResponse(response, "error requesting job")
 	}
 
-	var jr addJobResponse
+	var jr requestJobResponse
 	err = json.NewDecoder(response.Body).Decode(&jr)
 	if err != nil {
-		return nil, err
+		return nil, fmt.Errorf("error parsing response: %v", err)
+	}
+
+	location, err := c.server.Parse(jr.Location)
+	if err != nil {
+		return nil, fmt.Errorf("error parsing location url in response: %v", err)
+	}
+
+	artifactLocation, err := c.server.Parse(jr.ArtifactLocation)
+	if err != nil {
+		return nil, fmt.Errorf("error parsing artifact location url in response: %v", err)
 	}
 
-	return &Job{
-		jr.Id,
-		jr.Manifest,
-		jr.Targets,
+	return &job{
+		requester:        c.requester,
+		id:               jr.Id,
+		manifest:         jr.Manifest,
+		targets:          jr.Targets,
+		location:         location.String(),
+		artifactLocation: artifactLocation.String(),
 	}, nil
 }
 
-func (c *Client) JobCanceled(job *Job) bool {
-	response, err := c.api.GetJobQueueV1JobsJobId(context.Background(), job.Id.String())
+func (j *job) Id() uuid.UUID {
+	return j.id
+}
+
+func (j *job) OSBuildArgs() (distro.Manifest, []*target.Target, error) {
+	return j.manifest, j.targets, nil
+}
+
+func (j *job) Update(status common.ImageBuildState, result *osbuild.Result) error {
+	var buf bytes.Buffer
+	err := json.NewEncoder(&buf).Encode(api.UpdateJobJSONRequestBody{
+		Result: result,
+		Status: status.ToString(),
+	})
 	if err != nil {
-		return true
+		panic(err)
+	}
+
+	req, err := http.NewRequest("PATCH", j.location, &buf)
+	if err != nil {
+		panic(err)
+	}
+
+	req.Header.Add("Content-Type", "application/json")
+
+	response, err := j.requester.Do(req)
+	if err != nil {
+		return fmt.Errorf("error fetching job info: %v", err)
 	}
 	defer response.Body.Close()
 
 	if response.StatusCode != http.StatusOK {
-		return true
+		return errorFromResponse(response, "error setting job status")
 	}
 
-	var jr jobResponse
+	return nil
+}
+
+func (j *job) Canceled() (bool, error) {
+	response, err := j.requester.Get(j.location)
+	if err != nil {
+		return false, fmt.Errorf("error fetching job info: %v", err)
+	}
+	defer response.Body.Close()
+
+	if response.StatusCode != http.StatusOK {
+		return false, errorFromResponse(response, "error fetching job info")
+	}
+
+	var jr getJobResponse
 	err = json.NewDecoder(response.Body).Decode(&jr)
 	if err != nil {
-		return true
+		return false, fmt.Errorf("error parsing reponse: %v", err)
 	}
 
-	return jr.Canceled
+	return jr.Canceled, nil
 }
 
-func (c *Client) UpdateJob(job *Job, status common.ImageBuildState, result *osbuild.Result) error {
-	response, err := c.api.PatchJobQueueV1JobsJobId(context.Background(), job.Id.String(), api.PatchJobQueueV1JobsJobIdJSONRequestBody{
-		Result: result,
-		Status: status.ToString(),
-	})
+func (j *job) UploadArtifact(name string, reader io.Reader) error {
+	if j.artifactLocation == "" {
+		return fmt.Errorf("server does not accept artifacts for this job")
+	}
+
+	loc, err := url.Parse(j.artifactLocation)
 	if err != nil {
-		return err
+		return fmt.Errorf("error parsing job location: %v", err)
 	}
 
-	if response.StatusCode != http.StatusOK {
-		return errors.New("error setting job status")
+	loc, err = loc.Parse(url.PathEscape(name))
+	if err != nil {
+		panic(err)
+	}
+
+	req, err := http.NewRequest("PUT", loc.String(), reader)
+	if err != nil {
+		return fmt.Errorf("cannot create request: %v", err)
+	}
+
+	req.Header.Add("Content-Type", "application/octet-stream")
+
+	response, err := j.requester.Do(req)
+	if err != nil {
+		return fmt.Errorf("error uploading artifact: %v", err)
+	}
+
+	if response.StatusCode != 200 {
+		return errorFromResponse(response, "error uploading artifact")
 	}
 
 	return nil
 }
 
-func (c *Client) UploadImage(job uuid.UUID, name string, reader io.Reader) error {
-	_, err := c.api.PostJobQueueV1JobsJobIdArtifactsNameWithBody(context.Background(),
-		job.String(), name, "application/octet-stream", reader)
-
-	return err
+// Parses an api.Error from a response and returns it as a golang error. Other
+// errors, such failing to parse the response, are returned as golang error as
+// well. If client code expects an error, it gets one.
+func errorFromResponse(response *http.Response, message string) error {
+	var e api.Error
+	err := json.NewDecoder(response.Body).Decode(&e)
+	if err != nil {
+		return fmt.Errorf("failed to parse error response: %v", err)
+	}
+	return fmt.Errorf("%v: %v — %v", message, response.StatusCode, e.Message)
 }
diff --git a/internal/worker/json.go b/internal/worker/json.go
index 6f969721b..60f95fc5c 100644
--- a/internal/worker/json.go
+++ b/internal/worker/json.go
@@ -2,7 +2,6 @@ package worker
 
 import (
 	"github.com/google/uuid"
-
 	"github.com/osbuild/osbuild-composer/internal/common"
 	"github.com/osbuild/osbuild-composer/internal/distro"
 	"github.com/osbuild/osbuild-composer/internal/osbuild"
@@ -30,22 +29,16 @@ type statusResponse struct {
 	Status string `json:"status"`
 }
 
-type errorResponse struct {
-	Message string `json:"message"`
-}
-
-type addJobRequest struct {
-}
-
-type addJobResponse struct {
-	Id       uuid.UUID        `json:"id"`
-	Manifest distro.Manifest  `json:"manifest"`
-	Targets  []*target.Target `json:"targets,omitempty"`
+type requestJobResponse struct {
+	Id               uuid.UUID        `json:"id"`
+	Manifest         distro.Manifest  `json:"manifest"`
+	Targets          []*target.Target `json:"targets,omitempty"`
+	Location         string           `json:"location"`
+	ArtifactLocation string           `json:"artifact_location"`
 }
 
-type jobResponse struct {
-	Id       uuid.UUID `json:"id"`
-	Canceled bool      `json:"canceled"`
+type getJobResponse struct {
+	Canceled bool `json:"canceled"`
 }
 
 type updateJobRequest struct {
diff --git a/internal/worker/server.go b/internal/worker/server.go
index 17ac3e397..6409d11af 100644
--- a/internal/worker/server.go
+++ b/internal/worker/server.go
@@ -1,7 +1,9 @@
 package worker
 
 import (
+	"context"
 	"encoding/json"
+	"errors"
 	"fmt"
 	"io"
 	"io/ioutil"
@@ -10,6 +12,7 @@ import (
 	"net/http"
 	"os"
 	"path"
+	"sync"
 	"time"
 
 	"github.com/google/uuid"
@@ -26,6 +29,18 @@ type Server struct {
 	jobs         jobqueue.JobQueue
 	echo         *echo.Echo
 	artifactsDir string
+
+	// Currently running jobs. Workers are not handed job ids, but
+	// independent tokens which serve as an indirection. This enables
+	// race-free uploading of artifacts and makes restarting composer more
+	// robust (workers from an old run cannot report results for jobs
+	// composer thinks are not running).
+	// This map maps these tokens to job ids. Artifacts are stored in
+	// `$STATE_DIRECTORY/artifacts/tmp/$TOKEN` while the worker is running,
+	// and renamed to `$STATE_DIRECTORY/artifacts/$JOB_ID` once the job is
+	// reported as done.
+	running      map[uuid.UUID]uuid.UUID
+	runningMutex sync.Mutex
 }
 
 type JobStatus struct {
@@ -37,10 +52,13 @@ type JobStatus struct {
 	Result   OSBuildJobResult
 }
 
+var ErrTokenNotExist = errors.New("worker token does not exist")
+
 func NewServer(logger *log.Logger, jobs jobqueue.JobQueue, artifactsDir string) *Server {
 	s := &Server{
 		jobs:         jobs,
 		artifactsDir: artifactsDir,
+		running:      make(map[uuid.UUID]uuid.UUID),
 	}
 
 	s.echo = echo.New()
@@ -151,6 +169,72 @@ func (s *Server) DeleteArtifacts(id uuid.UUID) error {
 	return os.RemoveAll(path.Join(s.artifactsDir, id.String()))
 }
 
+func (s *Server) RequestJob(ctx context.Context) (uuid.UUID, uuid.UUID, *OSBuildJob, error) {
+	token := uuid.New()
+
+	var args OSBuildJob
+	jobId, err := s.jobs.Dequeue(ctx, []string{"osbuild"}, &args)
+	if err != nil {
+		return uuid.Nil, uuid.Nil, nil, err
+	}
+
+	if s.artifactsDir != "" {
+		err := os.MkdirAll(path.Join(s.artifactsDir, "tmp", token.String()), 0700)
+		if err != nil {
+			return uuid.Nil, uuid.Nil, nil, fmt.Errorf("cannot create artifact directory: %v", err)
+		}
+	}
+
+	s.runningMutex.Lock()
+	defer s.runningMutex.Unlock()
+	s.running[token] = jobId
+
+	return token, jobId, &args, nil
+}
+
+func (s *Server) RunningJob(token uuid.UUID) (uuid.UUID, error) {
+	s.runningMutex.Lock()
+	defer s.runningMutex.Unlock()
+
+	jobId, ok := s.running[token]
+	if !ok {
+		return uuid.Nil, ErrTokenNotExist
+	}
+
+	return jobId, nil
+}
+
+func (s *Server) FinishJob(token uuid.UUID, result *OSBuildJobResult) error {
+	s.runningMutex.Lock()
+	defer s.runningMutex.Unlock()
+
+	jobId, ok := s.running[token]
+	if !ok {
+		return ErrTokenNotExist
+	}
+
+	// Always delete the running job, even if there are errors finishing
+	// the job, because callers won't call this a second time on error.
+	delete(s.running, token)
+
+	err := s.jobs.FinishJob(jobId, result)
+	if err != nil {
+		return fmt.Errorf("error finishing job: %v", err)
+	}
+
+	// Move artifacts from the temporary location to the final job
+	// location. Log any errors, but do not treat them as fatal. The job is
+	// already finished.
+	if s.artifactsDir != "" {
+		err := os.Rename(path.Join(s.artifactsDir, "tmp", token.String()), path.Join(s.artifactsDir, jobId.String()))
+		if err != nil {
+			log.Printf("Error moving artifacts for job%s: %v", jobId, err)
+		}
+	}
+
+	return nil
+}
+
 // apiHandlers implements api.ServerInterface - the http api route handlers
 // generated from api/openapi.yml. This is a separate object, because these
 // handlers should not be exposed on the `Server` object.
@@ -164,52 +248,61 @@ func (h *apiHandlers) GetStatus(ctx echo.Context) error {
 	})
 }
 
-func (h *apiHandlers) GetJobQueueV1JobsJobId(ctx echo.Context, jobId string) error {
-	id, err := uuid.Parse(jobId)
+func (h *apiHandlers) RequestJob(ctx echo.Context) error {
+	var body struct{}
+	err := ctx.Bind(&body)
 	if err != nil {
-		return echo.NewHTTPError(http.StatusBadRequest, "cannot parse compose id: %v", err)
+		return err
 	}
 
-	status, err := h.server.JobStatus(id)
+	token, jobId, jobArgs, err := h.server.RequestJob(ctx.Request().Context())
 	if err != nil {
-		switch err {
-		case jobqueue.ErrNotExist:
-			return echo.NewHTTPError(http.StatusNotFound, "job does not exist: %s", id)
-		default:
-			return err
-		}
+		return err
 	}
 
-	return ctx.JSON(http.StatusOK, jobResponse{
-		Id:       id,
-		Canceled: status.Canceled,
+	return ctx.JSON(http.StatusCreated, requestJobResponse{
+		Id:               jobId,
+		Manifest:         jobArgs.Manifest,
+		Targets:          jobArgs.Targets,
+		Location:         fmt.Sprintf("/jobs/%v", token),
+		ArtifactLocation: fmt.Sprintf("/jobs/%v/artifacts/", token),
 	})
 }
 
-func (h *apiHandlers) PostJobQueueV1Jobs(ctx echo.Context) error {
-	var body addJobRequest
-	err := ctx.Bind(&body)
+func (h *apiHandlers) GetJob(ctx echo.Context, tokenstr string) error {
+	token, err := uuid.Parse(tokenstr)
 	if err != nil {
-		return err
+		return echo.NewHTTPError(http.StatusBadRequest, "cannot parse job token")
 	}
 
-	var job OSBuildJob
-	id, err := h.server.jobs.Dequeue(ctx.Request().Context(), []string{"osbuild"}, &job)
+	jobId, err := h.server.RunningJob(token)
 	if err != nil {
-		return echo.NewHTTPError(http.StatusInternalServerError, "%v", err)
+		switch err {
+		case ErrTokenNotExist:
+			return echo.NewHTTPError(http.StatusNotFound, "not found")
+		default:
+			return err
+		}
+	}
+
+	if jobId == uuid.Nil {
+		return ctx.JSON(http.StatusOK, getJobResponse{})
+	}
+
+	status, err := h.server.JobStatus(jobId)
+	if err != nil {
+		return err
 	}
 
-	return ctx.JSON(http.StatusCreated, addJobResponse{
-		Id:       id,
-		Manifest: job.Manifest,
-		Targets:  job.Targets,
+	return ctx.JSON(http.StatusOK, getJobResponse{
+		Canceled: status.Canceled,
 	})
 }
 
-func (h *apiHandlers) PatchJobQueueV1JobsJobId(ctx echo.Context, jobId string) error {
-	id, err := uuid.Parse(jobId)
+func (h *apiHandlers) UpdateJob(ctx echo.Context, idstr string) error {
+	token, err := uuid.Parse(idstr)
 	if err != nil {
-		return echo.NewHTTPError(http.StatusBadRequest, "cannot parse compose id: %v", err)
+		return echo.NewHTTPError(http.StatusBadRequest, "cannot parse job token")
 	}
 
 	var body updateJobRequest
@@ -225,13 +318,11 @@ func (h *apiHandlers) PatchJobQueueV1JobsJobId(ctx echo.Context, jobId string) e
 		return echo.NewHTTPError(http.StatusBadRequest, "setting status of a job to waiting or running is not supported")
 	}
 
-	err = h.server.jobs.FinishJob(id, OSBuildJobResult{OSBuildOutput: body.Result})
+	err = h.server.FinishJob(token, &OSBuildJobResult{OSBuildOutput: body.Result})
 	if err != nil {
 		switch err {
-		case jobqueue.ErrNotExist:
-			return echo.NewHTTPError(http.StatusNotFound, "job does not exist: %s", id)
-		case jobqueue.ErrNotRunning:
-			return echo.NewHTTPError(http.StatusBadRequest, "job is not running: %s", id)
+		case ErrTokenNotExist:
+			return echo.NewHTTPError(http.StatusNotFound, "not found")
 		default:
 			return err
 		}
@@ -240,10 +331,10 @@ func (h *apiHandlers) PatchJobQueueV1JobsJobId(ctx echo.Context, jobId string) e
 	return ctx.JSON(http.StatusOK, updateJobResponse{})
 }
 
-func (h *apiHandlers) PostJobQueueV1JobsJobIdArtifactsName(ctx echo.Context, jobId string, name string) error {
-	id, err := uuid.Parse(jobId)
+func (h *apiHandlers) UploadJobArtifact(ctx echo.Context, tokenstr string, name string) error {
+	token, err := uuid.Parse(tokenstr)
 	if err != nil {
-		return echo.NewHTTPError(http.StatusBadRequest, "cannot parse compose id: %v", err)
+		return echo.NewHTTPError(http.StatusBadRequest, "cannot parse job token")
 	}
 
 	request := ctx.Request()
@@ -251,24 +342,19 @@ func (h *apiHandlers) PostJobQueueV1JobsJobIdArtifactsName(ctx echo.Context, job
 	if h.server.artifactsDir == "" {
 		_, err := io.Copy(ioutil.Discard, request.Body)
 		if err != nil {
-			return echo.NewHTTPError(http.StatusInternalServerError, "error discarding artifact: %v", err)
+			return fmt.Errorf("error discarding artifact: %v", err)
 		}
 		return ctx.NoContent(http.StatusOK)
 	}
 
-	err = os.Mkdir(path.Join(h.server.artifactsDir, id.String()), 0700)
-	if err != nil {
-		return echo.NewHTTPError(http.StatusInternalServerError, "cannot create artifact directory: %v", err)
-	}
-
-	f, err := os.Create(path.Join(h.server.artifactsDir, id.String(), name))
+	f, err := os.Create(path.Join(h.server.artifactsDir, "tmp", token.String(), name))
 	if err != nil {
-		return echo.NewHTTPError(http.StatusInternalServerError, "cannot create artifact file: %v", err)
+		return fmt.Errorf("cannot create artifact file: %v", err)
 	}
 
 	_, err = io.Copy(f, request.Body)
 	if err != nil {
-		return echo.NewHTTPError(http.StatusInternalServerError, "error writing artifact file: %v", err)
+		return fmt.Errorf("error writing artifact file: %v", err)
 	}
 
 	return ctx.NoContent(http.StatusOK)
@@ -289,7 +375,7 @@ func (b binder) Bind(i interface{}, ctx echo.Context) error {
 
 	err := json.NewDecoder(request.Body).Decode(i)
 	if err != nil {
-		return echo.NewHTTPError(http.StatusBadRequest, "cannot parse request body: %v", err)
+		return echo.NewHTTPError(http.StatusBadRequest, "cannot parse request body: "+err.Error())
 	}
 
 	return nil


Test patch:
diff --git a/internal/worker/server_test.go b/internal/worker/server_test.go
index baa1db5f1..ee3200613 100644
--- a/internal/worker/server_test.go
+++ b/internal/worker/server_test.go
@@ -1,11 +1,11 @@
 package worker_test
 
 import (
+	"context"
 	"fmt"
 	"net/http"
 	"testing"
 
-	"github.com/google/uuid"
 	"github.com/stretchr/testify/require"
 
 	"github.com/osbuild/osbuild-composer/internal/distro"
@@ -31,15 +31,15 @@ func TestErrors(t *testing.T) {
 		// Bogus path
 		{"GET", "/foo", ``, http.StatusNotFound},
 		// Create job with invalid body
-		{"POST", "/job-queue/v1/jobs", ``, http.StatusBadRequest},
+		{"POST", "/jobs", ``, http.StatusBadRequest},
 		// Wrong method
-		{"GET", "/job-queue/v1/jobs", ``, http.StatusMethodNotAllowed},
+		{"GET", "/jobs", ``, http.StatusMethodNotAllowed},
 		// Update job with invalid ID
-		{"PATCH", "/job-queue/v1/jobs/foo", `{"status":"FINISHED"}`, http.StatusBadRequest},
+		{"PATCH", "/jobs/foo", `{"status":"FINISHED"}`, http.StatusBadRequest},
 		// Update job that does not exist, with invalid body
-		{"PATCH", "/job-queue/v1/jobs/aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa", ``, http.StatusBadRequest},
+		{"PATCH", "/jobs/aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa", ``, http.StatusBadRequest},
 		// Update job that does not exist
-		{"PATCH", "/job-queue/v1/jobs/aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa", `{"status":"FINISHED"}`, http.StatusNotFound},
+		{"PATCH", "/jobs/aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa", `{"status":"FINISHED"}`, http.StatusNotFound},
 	}
 
 	for _, c := range cases {
@@ -64,14 +64,11 @@ func TestCreate(t *testing.T) {
 	}
 	server := worker.NewServer(nil, testjobqueue.New(), "")
 
-	id, err := server.Enqueue(manifest, nil)
+	_, err = server.Enqueue(manifest, nil)
 	require.NoError(t, err)
 
-	test.TestRoute(t, server, false, "POST", "/job-queue/v1/jobs", `{}`, http.StatusCreated,
-		`{"id":"`+id.String()+`","manifest":{"sources":{},"pipeline":{}}}`, "created")
-
-	test.TestRoute(t, server, false, "GET", fmt.Sprintf("/job-queue/v1/jobs/%s", id), `{}`, http.StatusOK,
-		`{"id":"`+id.String()+`","canceled":false}`)
+	test.TestRoute(t, server, false, "POST", "/jobs", `{}`, http.StatusCreated,
+		`{"manifest":{"sources":{},"pipeline":{}}}`, "id", "location", "artifact_location")
 }
 
 func TestCancel(t *testing.T) {
@@ -90,82 +87,16 @@ func TestCancel(t *testing.T) {
 	}
 	server := worker.NewServer(nil, testjobqueue.New(), "")
 
-	id, err := server.Enqueue(manifest, nil)
+	jobId, err := server.Enqueue(manifest, nil)
 	require.NoError(t, err)
 
-	test.TestRoute(t, server, false, "POST", "/job-queue/v1/jobs", `{}`, http.StatusCreated,
-		`{"id":"`+id.String()+`","manifest":{"sources":{},"pipeline":{}}}`, "created")
-
-	err = server.Cancel(id)
+	token, j, _, err := server.RequestJob(context.Background())
 	require.NoError(t, err)
+	require.Equal(t, jobId, j)
 
-	test.TestRoute(t, server, false, "GET", fmt.Sprintf("/job-queue/v1/jobs/%s", id), `{}`, http.StatusOK,
-		`{"id":"`+id.String()+`","canceled":true}`)
-}
-
-func testUpdateTransition(t *testing.T, from, to string, expectedStatus int) {
-	distroStruct := fedoratest.New()
-	arch, err := distroStruct.GetArch("x86_64")
-	if err != nil {
-		t.Fatalf("error getting arch from distro")
-	}
-	imageType, err := arch.GetImageType("qcow2")
-	if err != nil {
-		t.Fatalf("error getting image type from arch")
-	}
-	server := worker.NewServer(nil, testjobqueue.New(), "")
-
-	id := uuid.Nil
-	if from != "VOID" {
-		manifest, err := imageType.Manifest(nil, distro.ImageOptions{Size: imageType.Size(0)}, nil, nil, nil)
-		if err != nil {
-			t.Fatalf("error creating osbuild manifest")
-		}
-
-		id, err = server.Enqueue(manifest, nil)
-		require.NoError(t, err)
-
-		if from != "WAITING" {
-			test.SendHTTP(server, false, "POST", "/job-queue/v1/jobs", `{}`)
-			if from != "RUNNING" {
-				test.SendHTTP(server, false, "PATCH", "/job-queue/v1/jobs/"+id.String(), `{"status":"`+from+`"}`)
-			}
-		}
-	}
-
-	test.TestRoute(t, server, false, "PATCH", "/job-queue/v1/jobs/"+id.String(), `{"status":"`+to+`"}`, expectedStatus, "{}", "message")
-}
-
-func TestUpdate(t *testing.T) {
-	var cases = []struct {
-		From           string
-		To             string
-		ExpectedStatus int
-	}{
-		{"VOID", "WAITING", http.StatusBadRequest},
-		{"VOID", "RUNNING", http.StatusBadRequest},
-		{"VOID", "FINISHED", http.StatusNotFound},
-		{"VOID", "FAILED", http.StatusNotFound},
-		{"WAITING", "WAITING", http.StatusBadRequest},
-		{"WAITING", "RUNNING", http.StatusBadRequest},
-		{"WAITING", "FINISHED", http.StatusBadRequest},
-		{"WAITING", "FAILED", http.StatusBadRequest},
-		{"RUNNING", "WAITING", http.StatusBadRequest},
-		{"RUNNING", "RUNNING", http.StatusBadRequest},
-		{"RUNNING", "FINISHED", http.StatusOK},
-		{"RUNNING", "FAILED", http.StatusOK},
-		{"FINISHED", "WAITING", http.StatusBadRequest},
-		{"FINISHED", "RUNNING", http.StatusBadRequest},
-		{"FINISHED", "FINISHED", http.StatusBadRequest},
-		{"FINISHED", "FAILED", http.StatusBadRequest},
-		{"FAILED", "WAITING", http.StatusBadRequest},
-		{"FAILED", "RUNNING", http.StatusBadRequest},
-		{"FAILED", "FINISHED", http.StatusBadRequest},
-		{"FAILED", "FAILED", http.StatusBadRequest},
-	}
+	err = server.Cancel(jobId)
+	require.NoError(t, err)
 
-	for _, c := range cases {
-		t.Log(c)
-		testUpdateTransition(t, c.From, c.To, c.ExpectedStatus)
-	}
+	test.TestRoute(t, server, false, "GET", fmt.Sprintf("/jobs/%s", token), `{}`, http.StatusOK,
+		`{"canceled":true}`)
 }