|
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 |
}
|