debian-forge-composer/internal/jobqueue/api.go
Lars Karlitski 1b7cb6c11b store/jobqueue: remove distro field from jobs
A job's purpose is to build an osbuild manifest and upload the results
somewhere. It should not know about which distro was used to generate
the pipeline.

Workers depended on the distro package in two ways:

1. To set an osbuild `--build-env`. This is not necessary anymore in new
   versions of osbuild. More importantly, it was wrong: it passed the
   runner from the distro that is being built, instead of one that
   matches the host.

   This patch simply removes that logic.

2. To fetch the output filename with `Distro.FilenameFromType()`. While
   that is useful, I don't think it warrants the dependency.

   This patch uses the fact that all current pipelines output exactly
   one file and uploads that. This should probably be extended in the
   future to upload all output files, or to name them explicitly in the
   upload target.

The worker should now compile to a smaller binary and do less
unnecessary work on startup (like reading repository files).
2020-03-18 12:24:20 +01:00

189 lines
5.1 KiB
Go

package jobqueue
import (
"encoding/json"
"log"
"net"
"net/http"
"strconv"
"github.com/osbuild/osbuild-composer/internal/store"
"github.com/google/uuid"
"github.com/julienschmidt/httprouter"
)
type API struct {
logger *log.Logger
store *store.Store
router *httprouter.Router
}
func New(logger *log.Logger, store *store.Store) *API {
api := &API{
logger: logger,
store: store,
}
api.router = httprouter.New()
api.router.RedirectTrailingSlash = false
api.router.RedirectFixedPath = false
api.router.MethodNotAllowed = http.HandlerFunc(methodNotAllowedHandler)
api.router.NotFound = http.HandlerFunc(notFoundHandler)
api.router.POST("/job-queue/v1/jobs", api.addJobHandler)
api.router.PATCH("/job-queue/v1/jobs/:job_id/builds/:build_id", api.updateJobHandler)
api.router.POST("/job-queue/v1/jobs/:job_id/builds/:build_id/image", api.addJobImageHandler)
return api
}
func (api *API) Serve(listener net.Listener) error {
server := http.Server{Handler: api}
err := server.Serve(listener)
if err != nil && err != http.ErrServerClosed {
return err
}
return nil
}
func (api *API) ServeHTTP(writer http.ResponseWriter, request *http.Request) {
if api.logger != nil {
log.Println(request.Method, request.URL.Path)
}
writer.Header().Set("Content-Type", "application/json; charset=utf-8")
api.router.ServeHTTP(writer, request)
}
func methodNotAllowedHandler(writer http.ResponseWriter, request *http.Request) {
writer.WriteHeader(http.StatusMethodNotAllowed)
}
func notFoundHandler(writer http.ResponseWriter, request *http.Request) {
writer.WriteHeader(http.StatusNotFound)
}
func statusResponseOK(writer http.ResponseWriter) {
writer.WriteHeader(http.StatusOK)
}
func statusResponseError(writer http.ResponseWriter, code int, errors ...string) {
writer.WriteHeader(code)
for _, err := range errors {
_, e := writer.Write([]byte(err))
if e != nil {
panic(e)
}
}
}
func (api *API) addJobHandler(writer http.ResponseWriter, request *http.Request, _ httprouter.Params) {
type requestBody struct {
}
contentType := request.Header["Content-Type"]
if len(contentType) != 1 || contentType[0] != "application/json" {
statusResponseError(writer, http.StatusUnsupportedMediaType)
return
}
var body requestBody
err := json.NewDecoder(request.Body).Decode(&body)
if err != nil {
statusResponseError(writer, http.StatusBadRequest, "invalid request: "+err.Error())
return
}
nextJob := api.store.PopJob()
writer.WriteHeader(http.StatusCreated)
// FIXME: handle or comment this possible error
_ = json.NewEncoder(writer).Encode(Job{
ID: nextJob.ComposeID,
ImageBuildID: nextJob.ImageBuildID,
Manifest: nextJob.Manifest,
Targets: nextJob.Targets,
OutputType: nextJob.ImageType,
})
}
func (api *API) updateJobHandler(writer http.ResponseWriter, request *http.Request, params httprouter.Params) {
contentType := request.Header["Content-Type"]
if len(contentType) != 1 || contentType[0] != "application/json" {
statusResponseError(writer, http.StatusUnsupportedMediaType)
return
}
id, err := uuid.Parse(params.ByName("job_id"))
if err != nil {
statusResponseError(writer, http.StatusBadRequest, "invalid compose id: "+err.Error())
return
}
imageBuildId, err := strconv.Atoi(params.ByName("build_id"))
if err != nil {
statusResponseError(writer, http.StatusBadRequest, "invalid image build id: "+err.Error())
return
}
var body JobStatus
err = json.NewDecoder(request.Body).Decode(&body)
if err != nil {
statusResponseError(writer, http.StatusBadRequest, "invalid status: "+err.Error())
return
}
err = api.store.UpdateImageBuildInCompose(id, imageBuildId, body.Status, body.Result)
if err != nil {
switch err.(type) {
case *store.NotFoundError:
statusResponseError(writer, http.StatusNotFound, err.Error())
case *store.NotPendingError:
statusResponseError(writer, http.StatusNotFound, err.Error())
case *store.NotRunningError:
statusResponseError(writer, http.StatusBadRequest, err.Error())
case *store.InvalidRequestError:
statusResponseError(writer, http.StatusBadRequest, err.Error())
default:
statusResponseError(writer, http.StatusInternalServerError, err.Error())
}
return
}
statusResponseOK(writer)
}
func (api *API) addJobImageHandler(writer http.ResponseWriter, request *http.Request, params httprouter.Params) {
id, err := uuid.Parse(params.ByName("job_id"))
if err != nil {
statusResponseError(writer, http.StatusBadRequest, "invalid compose id: "+err.Error())
return
}
imageBuildId, err := strconv.Atoi(params.ByName("build_id"))
if err != nil {
statusResponseError(writer, http.StatusBadRequest, "invalid build id: "+err.Error())
return
}
err = api.store.AddImageToImageUpload(id, imageBuildId, request.Body)
if err != nil {
switch err.(type) {
case *store.NotFoundError:
statusResponseError(writer, http.StatusNotFound, err.Error())
case *store.NoLocalTargetError:
statusResponseError(writer, http.StatusBadRequest, err.Error())
default:
statusResponseError(writer, http.StatusInternalServerError, err.Error())
}
return
}
statusResponseOK(writer)
}