debian-forge-composer/internal/worker/client.go
Lars Karlitski b03e1254e9 worker/api: remove token in favor of callback URLs
Instead of sending a `token` to workers, send back to URLs:

 1. "location": URL at which the job can be inspected (GET) and updated
    (PATCH).
 2. "artifact_location": URL at which artifacts should be uploaded to.

The actual URLs remain the same, but a client does not need to stitch
them together manually (except appending the artifact's name).

Unfortunately, the client code generated by `deepmap` does not lend
itself to this style of APIs. Use standard http.Client again, which is a
partial revert of 0962fbd30.
2020-09-11 14:23:24 +01:00

206 lines
4.7 KiB
Go

package worker
import (
"bytes"
"context"
"crypto/tls"
"encoding/json"
"errors"
"fmt"
"io"
"net"
"net/http"
"net/url"
"github.com/osbuild/osbuild-composer/internal/common"
"github.com/osbuild/osbuild-composer/internal/distro"
"github.com/osbuild/osbuild-composer/internal/osbuild"
"github.com/osbuild/osbuild-composer/internal/target"
"github.com/osbuild/osbuild-composer/internal/worker/api"
)
type Client struct {
server *url.URL
requester *http.Client
}
type Job interface {
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 {
requester *http.Client
manifest distro.Manifest
targets []*target.Target
location string
artifactLocation string
}
func NewClient(baseURL string, conf *tls.Config) (*Client, error) {
server, err := url.Parse(baseURL)
if err != nil {
return nil, err
}
requester := &http.Client{
Transport: &http.Transport{
TLSClientConfig: conf,
},
}
return &Client{server, requester}, nil
}
func NewClientUnix(path string) *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)
},
},
}
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)
}
var buf bytes.Buffer
err = json.NewEncoder(&buf).Encode(api.RequestJobJSONRequestBody{})
if err != nil {
panic(err)
}
response, err := c.requester.Post(url.String(), "application/json", &buf)
if err != nil {
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)
}
var jr requestJobResponse
err = json.NewDecoder(response.Body).Decode(&jr)
if err != nil {
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{
requester: c.requester,
manifest: jr.Manifest,
targets: jr.Targets,
location: location.String(),
artifactLocation: artifactLocation.String(),
}, nil
}
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 {
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 errors.New("error setting job status")
}
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, fmt.Errorf("unexpected return value: %v", response.StatusCode)
}
var jr getJobResponse
err = json.NewDecoder(response.Body).Decode(&jr)
if err != nil {
return false, fmt.Errorf("error parsing reponse: %v", err)
}
return jr.Canceled, nil
}
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 fmt.Errorf("error parsing job location: %v", err)
}
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")
_, err = j.requester.Do(req)
if err != nil {
return fmt.Errorf("error uploading artifcat: %v", err)
}
return nil
}