Blame internal/cloudapi/server.go

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