debian-forge-composer/internal/cloudapi/server.go
Tom Gundersen c777a18df0 jobqueue: expose dependencies when querying status
The status of a job may depend on the status of its dependenices,
as we do not repeat for instance the failed state in each dependent
job.

Return also the list of dependencies so these can be queried too.
2020-11-11 18:16:42 +01:00

258 lines
8 KiB
Go

//go:generate go run github.com/deepmap/oapi-codegen/cmd/oapi-codegen --package=cloudapi --generate types,chi-server,client -o openapi.gen.go openapi.yml
package cloudapi
import (
"encoding/json"
"fmt"
"net/http"
"github.com/go-chi/chi"
"github.com/google/uuid"
"github.com/osbuild/osbuild-composer/internal/blueprint"
"github.com/osbuild/osbuild-composer/internal/distro"
"github.com/osbuild/osbuild-composer/internal/rpmmd"
"github.com/osbuild/osbuild-composer/internal/target"
"github.com/osbuild/osbuild-composer/internal/worker"
)
const (
StatusPending = "pending"
StatusRunning = "running"
StatusSuccess = "success"
StatusFailure = "failure"
)
// Server represents the state of the cloud Server
type Server struct {
workers *worker.Server
rpmMetadata rpmmd.RPMMD
distros *distro.Registry
}
// NewServer creates a new cloud server
func NewServer(workers *worker.Server, rpmMetadata rpmmd.RPMMD, distros *distro.Registry) *Server {
server := &Server{
workers: workers,
rpmMetadata: rpmMetadata,
distros: distros,
}
return server
}
// Create an http.Handler() for this server, that provides the composer API at
// the given path.
func (server *Server) Handler(path string) http.Handler {
r := chi.NewRouter()
r.Route(path, func(r chi.Router) {
HandlerFromMux(server, r)
})
return r
}
// Compose handles a new /compose POST request
func (server *Server) Compose(w http.ResponseWriter, r *http.Request) {
contentType := r.Header["Content-Type"]
if len(contentType) != 1 || contentType[0] != "application/json" {
http.Error(w, "Only 'application/json' content type is supported", http.StatusUnsupportedMediaType)
return
}
var request ComposeRequest
err := json.NewDecoder(r.Body).Decode(&request)
if err != nil {
http.Error(w, "Could not parse JSON body", http.StatusBadRequest)
return
}
distribution := server.distros.GetDistro(request.Distribution)
if distribution == nil {
http.Error(w, fmt.Sprintf("Unsupported distribution: %s", request.Distribution), http.StatusBadRequest)
return
}
type imageRequest struct {
manifest distro.Manifest
arch string
}
imageRequests := make([]imageRequest, len(request.ImageRequests))
var targets []*target.Target
for i, ir := range request.ImageRequests {
arch, err := distribution.GetArch(ir.Architecture)
if err != nil {
http.Error(w, fmt.Sprintf("Unsupported architecture '%s' for distribution '%s'", ir.Architecture, request.Distribution), http.StatusBadRequest)
return
}
imageType, err := arch.GetImageType(ir.ImageType)
if err != nil {
http.Error(w, fmt.Sprintf("Unsupported image type '%s' for %s/%s", ir.ImageType, ir.Architecture, request.Distribution), http.StatusBadRequest)
return
}
repositories := make([]rpmmd.RepoConfig, len(ir.Repositories))
for j, repo := range ir.Repositories {
repositories[j].BaseURL = repo.Baseurl
repositories[j].RHSM = repo.Rhsm
}
var bp = blueprint.Blueprint{}
err = bp.Initialize()
if err != nil {
http.Error(w, "Unable to initialize blueprint", http.StatusInternalServerError)
return
}
packageSpecs, _ := imageType.Packages(bp)
packages, _, err := server.rpmMetadata.Depsolve(packageSpecs, nil, repositories, distribution.ModulePlatformID(), arch.Name())
if err != nil {
http.Error(w, fmt.Sprintf("Failed to depsolve base packages for %s/%s/%s: %s", ir.ImageType, ir.Architecture, request.Distribution, err), http.StatusInternalServerError)
return
}
buildPackageSpecs := imageType.BuildPackages()
buildPackages, _, err := server.rpmMetadata.Depsolve(buildPackageSpecs, nil, repositories, distribution.ModulePlatformID(), arch.Name())
if err != nil {
http.Error(w, fmt.Sprintf("Failed to depsolve build packages for %s/%s/%s: %s", ir.ImageType, ir.Architecture, request.Distribution, err), http.StatusInternalServerError)
return
}
imageOptions := distro.ImageOptions{Size: imageType.Size(0)}
if request.Customizations != nil && request.Customizations.Subscription != nil {
imageOptions.Subscription = &distro.SubscriptionImageOptions{
Organization: request.Customizations.Subscription.Organization,
ActivationKey: request.Customizations.Subscription.ActivationKey,
ServerUrl: request.Customizations.Subscription.ServerUrl,
BaseUrl: request.Customizations.Subscription.BaseUrl,
Insights: request.Customizations.Subscription.Insights,
}
}
manifest, err := imageType.Manifest(nil, imageOptions, repositories, packages, buildPackages)
if err != nil {
http.Error(w, fmt.Sprintf("Failed to get manifest for for %s/%s/%s: %s", ir.ImageType, ir.Architecture, request.Distribution, err), http.StatusBadRequest)
return
}
imageRequests[i].manifest = manifest
imageRequests[i].arch = arch.Name()
if len(ir.UploadRequests) != 1 {
http.Error(w, "Only compose requests with a single upload target are currently supported", http.StatusBadRequest)
return
}
uploadRequest := (ir.UploadRequests)[0]
/* oneOf is not supported by the openapi generator so marshal and unmarshal the uploadrequest based on the type */
if uploadRequest.Type == "aws" {
var awsUploadOptions AWSUploadRequestOptions
jsonUploadOptions, err := json.Marshal(uploadRequest.Options)
if err != nil {
http.Error(w, "Unable to marshal aws upload request", http.StatusInternalServerError)
return
}
err = json.Unmarshal(jsonUploadOptions, &awsUploadOptions)
if err != nil {
http.Error(w, "Unable to unmarshal aws upload request", http.StatusInternalServerError)
return
}
key := fmt.Sprintf("composer-api-%s", uuid.New().String())
t := target.NewAWSTarget(&target.AWSTargetOptions{
Filename: imageType.Filename(),
Region: awsUploadOptions.Region,
AccessKeyID: awsUploadOptions.S3.AccessKeyId,
SecretAccessKey: awsUploadOptions.S3.SecretAccessKey,
Bucket: awsUploadOptions.S3.Bucket,
Key: key,
})
if awsUploadOptions.Ec2.SnapshotName != nil {
t.ImageName = *awsUploadOptions.Ec2.SnapshotName
} else {
t.ImageName = key
}
targets = append(targets, t)
} else {
http.Error(w, "Unknown upload request type, only aws is supported", http.StatusBadRequest)
return
}
}
var ir imageRequest
if len(imageRequests) == 1 {
// NOTE: the store currently does not support multi-image composes
ir = imageRequests[0]
} else {
http.Error(w, "Only single-image composes are currently supported", http.StatusBadRequest)
return
}
id, err := server.workers.EnqueueOSBuild(ir.arch, &worker.OSBuildJob{
Manifest: ir.manifest,
Targets: targets,
})
if err != nil {
http.Error(w, "Failed to enqueue manifest", http.StatusInternalServerError)
return
}
var response ComposeResult
response.Id = id.String()
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.WriteHeader(http.StatusCreated)
err = json.NewEncoder(w).Encode(response)
if err != nil {
panic("Failed to write response")
}
}
// ComposeStatus handles a /compose/{id} GET request
func (server *Server) ComposeStatus(w http.ResponseWriter, r *http.Request, id string) {
jobId, err := uuid.Parse(id)
if err != nil {
http.Error(w, fmt.Sprintf("Invalid format for parameter id: %s", err), http.StatusBadRequest)
return
}
var result worker.OSBuildJobResult
status, _, err := server.workers.JobStatus(jobId, &result)
if err != nil {
http.Error(w, fmt.Sprintf("Job %s not found: %s", id, err), http.StatusNotFound)
return
}
response := ComposeStatus{
Status: composeStatusFromJobStatus(status, &result),
ImageStatuses: &[]ImageStatus{
{
Status: composeStatusFromJobStatus(status, &result),
},
},
}
w.Header().Set("Content-Type", "application/json; charset=utf-8")
err = json.NewEncoder(w).Encode(response)
if err != nil {
panic("Failed to write response")
}
}
func composeStatusFromJobStatus(js *worker.JobStatus, result *worker.OSBuildJobResult) string {
if js.Canceled {
return StatusFailure
}
if js.Started.IsZero() {
return StatusPending
}
if js.Finished.IsZero() {
return StatusRunning
}
if result.Success {
return StatusSuccess
}
return StatusFailure
}