3724 lines
106 KiB
Go
3724 lines
106 KiB
Go
package weldr
|
|
|
|
import (
|
|
"archive/tar"
|
|
"bytes"
|
|
"context"
|
|
"crypto/rand"
|
|
"crypto/sha256"
|
|
"encoding/json"
|
|
"errors"
|
|
errors_package "errors"
|
|
"fmt"
|
|
"io"
|
|
"log"
|
|
"math"
|
|
"math/big"
|
|
"net"
|
|
"net/http"
|
|
"net/url"
|
|
"os"
|
|
"path"
|
|
"regexp"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/BurntSushi/toml"
|
|
"github.com/gobwas/glob"
|
|
"github.com/google/uuid"
|
|
"github.com/julienschmidt/httprouter"
|
|
"github.com/osbuild/osbuild-composer/pkg/jobqueue"
|
|
|
|
"github.com/osbuild/blueprint/pkg/blueprint"
|
|
"github.com/osbuild/images/pkg/arch"
|
|
"github.com/osbuild/images/pkg/container"
|
|
"github.com/osbuild/images/pkg/distro"
|
|
"github.com/osbuild/images/pkg/distrofactory"
|
|
"github.com/osbuild/images/pkg/distroidparser"
|
|
"github.com/osbuild/images/pkg/dnfjson"
|
|
"github.com/osbuild/images/pkg/osbuild"
|
|
"github.com/osbuild/images/pkg/ostree"
|
|
"github.com/osbuild/images/pkg/reporegistry"
|
|
"github.com/osbuild/images/pkg/rhsm/facts"
|
|
"github.com/osbuild/images/pkg/rpmmd"
|
|
"github.com/osbuild/images/pkg/sbom"
|
|
"github.com/osbuild/osbuild-composer/internal/common"
|
|
"github.com/osbuild/osbuild-composer/internal/store"
|
|
"github.com/osbuild/osbuild-composer/internal/target"
|
|
"github.com/osbuild/osbuild-composer/internal/worker"
|
|
)
|
|
|
|
type API struct {
|
|
store *store.Store
|
|
workers *worker.Server
|
|
|
|
solver *dnfjson.BaseSolver
|
|
hostArch string
|
|
repoRegistry *reporegistry.RepoRegistry
|
|
|
|
logger *log.Logger
|
|
router *httprouter.Router
|
|
server http.Server
|
|
|
|
compatOutputDir string
|
|
|
|
hostDistroName string // Name of the host distro
|
|
distroFactory *distrofactory.Factory // Available distros
|
|
|
|
// List of ImageType names, which should not be exposed by the API
|
|
distrosImageTypeDenylist map[string][]string
|
|
}
|
|
|
|
type ComposeState int
|
|
|
|
const (
|
|
ComposeWaiting ComposeState = iota
|
|
ComposeRunning
|
|
ComposeFinished
|
|
ComposeFailed
|
|
)
|
|
|
|
// ToString converts ImageBuildState into a human readable string
|
|
func (cs ComposeState) ToString() string {
|
|
switch cs {
|
|
case ComposeWaiting:
|
|
return "WAITING"
|
|
case ComposeRunning:
|
|
return "RUNNING"
|
|
case ComposeFinished:
|
|
return "FINISHED"
|
|
case ComposeFailed:
|
|
return "FAILED"
|
|
default:
|
|
panic("invalid ComposeState value")
|
|
}
|
|
}
|
|
|
|
// systemRepoNames returns a list of the system repos
|
|
// NOTE: The system repos have no concept of id vs. name so the id is returned
|
|
func (api *API) systemRepoNames() (names []string) {
|
|
repos, err := api.repoRegistry.ReposByArchName(api.hostDistroName, api.hostArch, false)
|
|
if err == nil {
|
|
for _, repo := range repos {
|
|
names = append(names, repo.Name)
|
|
}
|
|
}
|
|
return names
|
|
}
|
|
|
|
// validDistros returns a sorted list of distributions that also have
|
|
// repositories defined for the given architecture
|
|
func (api *API) validDistros(arch string) []string {
|
|
distros := []string{}
|
|
for _, distroName := range api.repoRegistry.ListDistros() {
|
|
distro := api.distroFactory.GetDistro(distroName)
|
|
if distro == nil {
|
|
if api.logger != nil {
|
|
api.logger.Printf("Distro %s has repositories defined, but it's not supported. Skipping.", distroName)
|
|
}
|
|
continue
|
|
}
|
|
|
|
_, err := api.repoRegistry.DistroHasRepos(distroName, arch)
|
|
if err == nil {
|
|
distros = append(distros, distroName)
|
|
} else {
|
|
if api.logger != nil {
|
|
api.logger.Printf("Distro %s has no repositories defined for %s architecture, skipping.", distroName, arch)
|
|
}
|
|
}
|
|
}
|
|
|
|
sort.Strings(distros)
|
|
return distros
|
|
}
|
|
|
|
var ValidBlueprintName = regexp.MustCompile(`^[a-zA-Z0-9._-]+$`)
|
|
|
|
// NewTestAPI is used for the test framework, sets up a single distro
|
|
func NewTestAPI(solver *dnfjson.BaseSolver, rr *reporegistry.RepoRegistry,
|
|
logger *log.Logger, storeFixture *store.Fixture, workers *worker.Server,
|
|
compatOutputDir string, distrosImageTypeDenylist map[string][]string) *API {
|
|
|
|
api := &API{
|
|
store: storeFixture.Store,
|
|
workers: workers,
|
|
solver: solver,
|
|
hostArch: storeFixture.HostArchName,
|
|
repoRegistry: rr,
|
|
logger: logger,
|
|
compatOutputDir: compatOutputDir,
|
|
hostDistroName: storeFixture.HostDistroName,
|
|
distroFactory: storeFixture.Factory,
|
|
distrosImageTypeDenylist: distrosImageTypeDenylist,
|
|
}
|
|
return setupRouter(api)
|
|
}
|
|
|
|
func New(rr *reporegistry.RepoRegistry, stateDir string, solver *dnfjson.BaseSolver, df *distrofactory.Factory,
|
|
logger *log.Logger, workers *worker.Server, distrosImageTypeDenylist map[string][]string) (*API, error) {
|
|
if logger == nil {
|
|
logger = log.New(os.Stdout, "", 0)
|
|
}
|
|
|
|
hostDistroName, err := distro.GetHostDistroName()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to read host distro information")
|
|
}
|
|
hostArch := arch.Current().String()
|
|
|
|
hostDistro := df.GetDistro(hostDistroName)
|
|
if hostDistro != nil {
|
|
// get canonical distro name if the host distro is supported
|
|
hostDistroName = hostDistro.Name()
|
|
|
|
_, err = hostDistro.GetArch(hostArch)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("Host distro does not support host architecture: %v", err)
|
|
}
|
|
|
|
// Check if repositories for the host distro and arch were loaded
|
|
_, err = rr.ReposByArchName(hostDistroName, hostArch, false)
|
|
if err != nil {
|
|
log.Printf("loaded repository definitions don't contain any for the host distro/arch: %v", err)
|
|
}
|
|
|
|
} else {
|
|
log.Printf("host distro %q is not supported: only cross-distro builds are available", hostDistroName)
|
|
}
|
|
|
|
store := store.New(&stateDir, df, logger)
|
|
compatOutputDir := path.Join(stateDir, "outputs")
|
|
|
|
api := &API{
|
|
store: store,
|
|
workers: workers,
|
|
solver: solver,
|
|
hostArch: hostArch,
|
|
repoRegistry: rr,
|
|
logger: logger,
|
|
compatOutputDir: compatOutputDir,
|
|
hostDistroName: hostDistroName,
|
|
distroFactory: df,
|
|
distrosImageTypeDenylist: distrosImageTypeDenylist,
|
|
}
|
|
return setupRouter(api), nil
|
|
}
|
|
|
|
func setupRouter(api *API) *API {
|
|
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.GET("/api/status", api.statusHandler)
|
|
api.router.GET("/api/v:version/projects/source/list", api.sourceListHandler)
|
|
api.router.GET("/api/v:version/projects/source/info/", api.sourceEmptyInfoHandler)
|
|
api.router.GET("/api/v:version/projects/source/info/:sources", api.sourceInfoHandler)
|
|
api.router.POST("/api/v:version/projects/source/new", api.sourceNewHandler)
|
|
api.router.DELETE("/api/v:version/projects/source/delete/*source", api.sourceDeleteHandler)
|
|
|
|
api.router.GET("/api/v:version/projects/depsolve", api.projectsDepsolveHandler)
|
|
api.router.GET("/api/v:version/projects/depsolve/*projects", api.projectsDepsolveHandler)
|
|
|
|
api.router.GET("/api/v:version/modules/list", api.modulesListHandler)
|
|
api.router.GET("/api/v:version/modules/list/*modules", api.modulesListHandler)
|
|
api.router.GET("/api/v:version/projects/list", api.projectsListHandler)
|
|
api.router.GET("/api/v:version/projects/list/", api.projectsListHandler)
|
|
|
|
// these are the same, except that modules/info also includes dependencies
|
|
api.router.GET("/api/v:version/modules/info", api.modulesInfoHandler)
|
|
api.router.GET("/api/v:version/modules/info/*modules", api.modulesInfoHandler)
|
|
api.router.GET("/api/v:version/projects/info", api.modulesInfoHandler)
|
|
api.router.GET("/api/v:version/projects/info/*modules", api.modulesInfoHandler)
|
|
|
|
api.router.GET("/api/v:version/blueprints/list", api.blueprintsListHandler)
|
|
api.router.GET("/api/v:version/blueprints/info/*blueprints", api.blueprintsInfoHandler)
|
|
api.router.GET("/api/v:version/blueprints/depsolve/*blueprints", api.blueprintsDepsolveHandler)
|
|
api.router.GET("/api/v:version/blueprints/freeze/*blueprints", api.blueprintsFreezeHandler)
|
|
api.router.GET("/api/v:version/blueprints/diff/:blueprint/:from/:to", api.blueprintsDiffHandler)
|
|
api.router.GET("/api/v:version/blueprints/change/:blueprint/:commit", api.blueprintsChangeHandler)
|
|
api.router.GET("/api/v:version/blueprints/changes/*blueprints", api.blueprintsChangesHandler)
|
|
api.router.POST("/api/v:version/blueprints/new", api.blueprintsNewHandler)
|
|
api.router.POST("/api/v:version/blueprints/workspace", api.blueprintsWorkspaceHandler)
|
|
api.router.POST("/api/v:version/blueprints/undo/:blueprint/:commit", api.blueprintUndoHandler)
|
|
api.router.POST("/api/v:version/blueprints/tag/:blueprint", api.blueprintsTagHandler)
|
|
api.router.DELETE("/api/v:version/blueprints/delete/:blueprint", api.blueprintDeleteHandler)
|
|
api.router.DELETE("/api/v:version/blueprints/workspace/:blueprint", api.blueprintDeleteWorkspaceHandler)
|
|
|
|
api.router.POST("/api/v:version/compose", api.composeHandler)
|
|
api.router.DELETE("/api/v:version/compose/delete/:uuids", api.composeDeleteHandler)
|
|
api.router.GET("/api/v:version/compose/types", api.composeTypesHandler)
|
|
api.router.GET("/api/v:version/compose/queue", api.composeQueueHandler)
|
|
api.router.GET("/api/v:version/compose/status/:uuids", api.composeStatusHandler)
|
|
api.router.GET("/api/v:version/compose/info/:uuid", api.composeInfoHandler)
|
|
api.router.GET("/api/v:version/compose/finished", api.composeFinishedHandler)
|
|
api.router.GET("/api/v:version/compose/failed", api.composeFailedHandler)
|
|
api.router.GET("/api/v:version/compose/image/:uuid", api.composeImageHandler)
|
|
api.router.GET("/api/v:version/compose/metadata/:uuid", api.composeMetadataHandler)
|
|
api.router.GET("/api/v:version/compose/results/:uuid", api.composeResultsHandler)
|
|
api.router.GET("/api/v:version/compose/logs/:uuid", api.composeLogsHandler)
|
|
api.router.GET("/api/v:version/compose/log/:uuid", api.composeLogHandler)
|
|
api.router.POST("/api/v:version/compose/uploads/schedule/:uuid", api.uploadsScheduleHandler)
|
|
api.router.DELETE("/api/v:version/compose/cancel/:uuid", api.composeCancelHandler)
|
|
|
|
api.router.DELETE("/api/v:version/upload/delete/:uuid", api.uploadsDeleteHandler)
|
|
api.router.GET("/api/v:version/upload/info/:uuid", api.uploadsInfoHandler)
|
|
api.router.GET("/api/v:version/upload/log/:uuid", api.uploadsLogHandler)
|
|
api.router.POST("/api/v:version/upload/reset/:uuid", api.uploadsResetHandler)
|
|
api.router.DELETE("/api/v:version/upload/cancel/:uuid", api.uploadsCancelHandler)
|
|
|
|
api.router.GET("/api/v:version/upload/providers", api.providersHandler)
|
|
api.router.POST("/api/v:version/upload/providers/save", api.providersSaveHandler)
|
|
api.router.DELETE("/api/v:version/upload/providers/delete/:provider/:profile", api.providersDeleteHandler)
|
|
|
|
api.router.GET("/api/v:version/distros/list", api.distrosListHandler)
|
|
return api
|
|
}
|
|
|
|
func (api *API) Serve(listener net.Listener) error {
|
|
api.server = http.Server{
|
|
Handler: api,
|
|
ReadHeaderTimeout: 5 * time.Second,
|
|
}
|
|
|
|
err := api.server.Serve(listener)
|
|
if err != nil && err != http.ErrServerClosed {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (api *API) Shutdown(ctx context.Context) error {
|
|
return api.server.Shutdown(ctx)
|
|
}
|
|
|
|
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)
|
|
}
|
|
|
|
// PreloadMetadata loads the metadata for all supported distros
|
|
// This starts a background depsolve for all known distros in order to preload the
|
|
// metadata.
|
|
func (api *API) PreloadMetadata() {
|
|
log.Printf("Starting metadata preload goroutines")
|
|
for _, distro := range api.validDistros(api.hostArch) {
|
|
go func(distro string) {
|
|
startTime := time.Now()
|
|
d := api.getDistro(distro, api.hostArch)
|
|
if d == nil {
|
|
log.Printf("GetDistro - unknown distribution: %s", distro)
|
|
return
|
|
}
|
|
|
|
repos, err := api.allRepositories(distro, api.hostArch)
|
|
if err != nil {
|
|
log.Printf("Error getting repositories for distro %s: %s", distro, err)
|
|
return
|
|
}
|
|
|
|
solver := api.solver.NewWithConfig(d.ModulePlatformID(), d.Releasever(), api.hostArch, d.Name())
|
|
_, err = solver.Depsolve([]rpmmd.PackageSet{{Include: []string{"filesystem"}, Repositories: repos}}, sbom.StandardTypeNone)
|
|
if err != nil {
|
|
log.Printf("Problem preloading distro metadata for %s: %s", distro, err)
|
|
}
|
|
log.Printf("Finished preload of %s in %v", distro, time.Since(startTime))
|
|
}(distro)
|
|
}
|
|
}
|
|
|
|
type composeStatus struct {
|
|
State ComposeState
|
|
Queued time.Time
|
|
Started time.Time
|
|
Finished time.Time
|
|
Result *osbuild.Result
|
|
}
|
|
|
|
func composeStateFromJobStatus(js *worker.JobStatus, result *worker.OSBuildJobResult) ComposeState {
|
|
if js.Canceled {
|
|
return ComposeFailed
|
|
}
|
|
|
|
if js.Started.IsZero() {
|
|
return ComposeWaiting
|
|
}
|
|
|
|
if js.Finished.IsZero() {
|
|
return ComposeRunning
|
|
}
|
|
|
|
if result.Success {
|
|
return ComposeFinished
|
|
}
|
|
|
|
return ComposeFailed
|
|
}
|
|
|
|
// Returns the state of the image in `compose` and the times the job was
|
|
// queued, started, and finished. Assumes that there's only one image in the
|
|
// compose.
|
|
func (api *API) getComposeStatus(compose store.Compose) (*composeStatus, error) {
|
|
jobId := compose.ImageBuild.JobID
|
|
|
|
// backwards compatibility: composes that were around before splitting
|
|
// the job queue from the store still contain their valid status and
|
|
// times. Return those here as a fallback.
|
|
if jobId == uuid.Nil {
|
|
var state ComposeState
|
|
switch compose.ImageBuild.QueueStatus {
|
|
case common.IBWaiting:
|
|
state = ComposeWaiting
|
|
case common.IBRunning:
|
|
state = ComposeRunning
|
|
case common.IBFinished:
|
|
state = ComposeFinished
|
|
case common.IBFailed:
|
|
state = ComposeFailed
|
|
}
|
|
return &composeStatus{
|
|
State: state,
|
|
Queued: compose.ImageBuild.JobCreated,
|
|
Started: compose.ImageBuild.JobStarted,
|
|
Finished: compose.ImageBuild.JobFinished,
|
|
Result: &osbuild.Result{},
|
|
}, nil
|
|
}
|
|
|
|
// All jobs are "osbuild" jobs.
|
|
var result worker.OSBuildJobResult
|
|
jobInfo, err := api.workers.OSBuildJobInfo(jobId, &result)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &composeStatus{
|
|
State: composeStateFromJobStatus(jobInfo.JobStatus, &result),
|
|
Queued: jobInfo.JobStatus.Queued,
|
|
Started: jobInfo.JobStatus.Started,
|
|
Finished: jobInfo.JobStatus.Finished,
|
|
Result: result.OSBuildOutput,
|
|
}, nil
|
|
}
|
|
|
|
// Opens the image file for `compose`. This asks the worker server for the
|
|
// artifact first, and then falls back to looking in
|
|
// `{outputs}/{composeId}/{imageBuildId}` for backwards compatibility.
|
|
func (api *API) openImageFile(composeId uuid.UUID, compose store.Compose) (io.Reader, int64, error) {
|
|
name := compose.ImageBuild.ImageType.Filename()
|
|
|
|
reader, size, err := api.workers.JobArtifact(compose.ImageBuild.JobID, name)
|
|
if err != nil {
|
|
if api.compatOutputDir == "" || err != jobqueue.ErrNotExist {
|
|
return nil, 0, err
|
|
}
|
|
|
|
p := path.Join(api.compatOutputDir, composeId.String(), "0", name)
|
|
f, err := os.Open(p)
|
|
if err != nil {
|
|
return nil, 0, err
|
|
}
|
|
|
|
info, err := f.Stat()
|
|
if err != nil {
|
|
return nil, 0, err
|
|
}
|
|
|
|
reader = f
|
|
size = info.Size()
|
|
}
|
|
|
|
return reader, size, nil
|
|
}
|
|
|
|
// isImageTypeAllowed checks the given ImageType and Distro names against
|
|
// the distro-specific ImageType Denylist provided to the API from configuration.
|
|
// If the given ImageType is not allowed the method returns an `false`.
|
|
// Otherwise `true` is returned.
|
|
func (api *API) isImageTypeAllowed(distroName, imageType string) (bool, error) {
|
|
for deniedDistro, deniedImgTypes := range api.distrosImageTypeDenylist {
|
|
deniedDistroPattern, err := glob.Compile(deniedDistro)
|
|
if err != nil {
|
|
// the bool return value here does not have any real meaning
|
|
return true, err
|
|
}
|
|
if deniedDistroPattern.Match(distroName) {
|
|
for _, deniedImgType := range deniedImgTypes {
|
|
deniedImageTypePattern, err := glob.Compile(deniedImgType)
|
|
if err != nil {
|
|
// the bool return value here does not have any real meaning
|
|
return true, err
|
|
}
|
|
|
|
if deniedImageTypePattern.Match(imageType) {
|
|
return false, nil
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return true, nil
|
|
}
|
|
|
|
// getImageType returns the ImageType for the selected distro
|
|
// This is necessary because different distros support different image types, and the image
|
|
// type may have a different package set than other distros.
|
|
func (api *API) getImageType(distroName, imageType, archName string) (distro.ImageType, error) {
|
|
imgAllowed, err := api.isImageTypeAllowed(distroName, imageType)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error while checking if image type is allowed: %v", err)
|
|
}
|
|
if !imgAllowed {
|
|
return nil, fmt.Errorf("image type %q for distro %q is denied by configuration", imageType, distroName)
|
|
}
|
|
|
|
distro := api.getDistro(distroName, archName)
|
|
if distro == nil {
|
|
return nil, fmt.Errorf("GetDistro - unknown distribution: %s", distroName)
|
|
}
|
|
|
|
arch, err := distro.GetArch(archName)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return arch.GetImageType(imageType)
|
|
}
|
|
|
|
func (api *API) parseDistro(query url.Values, arch string) (string, error) {
|
|
if distro := query.Get("distro"); distro != "" {
|
|
if common.IsStringInSortedSlice(api.validDistros(arch), distro) {
|
|
return distro, nil
|
|
}
|
|
return "", errors_package.New("Invalid distro: " + distro)
|
|
}
|
|
return api.hostDistroName, nil
|
|
}
|
|
|
|
// getDistro returns the named distro or nil
|
|
// It excludes unsupported distros by first checking the api.distros list
|
|
func (api *API) getDistro(name, arch string) distro.Distro {
|
|
if !common.IsStringInSortedSlice(api.validDistros(arch), name) {
|
|
return nil
|
|
}
|
|
return api.distroFactory.GetDistro(name)
|
|
}
|
|
|
|
func verifyRequestVersion(writer http.ResponseWriter, params httprouter.Params, minVersion uint) bool {
|
|
versionString := params.ByName("version")
|
|
|
|
version, err := strconv.ParseUint(versionString, 10, 0)
|
|
|
|
var MaxApiVersion uint = 1
|
|
|
|
if err != nil || uint(version) < minVersion || uint(version) > MaxApiVersion {
|
|
notFoundHandler(writer, nil)
|
|
return false
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
func isRequestVersionAtLeast(params httprouter.Params, minVersion uint) bool {
|
|
versionString := params.ByName("version")
|
|
|
|
version, err := strconv.ParseUint(versionString, 10, 0)
|
|
|
|
common.PanicOnError(err)
|
|
|
|
return uint(version) >= minVersion
|
|
}
|
|
|
|
func methodNotAllowedHandler(writer http.ResponseWriter, request *http.Request) {
|
|
writer.WriteHeader(http.StatusMethodNotAllowed)
|
|
}
|
|
|
|
func notFoundHandler(writer http.ResponseWriter, request *http.Request) {
|
|
errors := responseError{
|
|
Code: http.StatusNotFound,
|
|
ID: "HTTPError",
|
|
Msg: "Not Found",
|
|
}
|
|
statusResponseError(writer, http.StatusNotFound, errors)
|
|
}
|
|
|
|
func notImplementedHandler(writer http.ResponseWriter, httpRequest *http.Request, _ httprouter.Params) {
|
|
writer.WriteHeader(http.StatusNotImplemented)
|
|
}
|
|
|
|
func statusResponseOK(writer http.ResponseWriter) {
|
|
type reply struct {
|
|
Status bool `json:"status"`
|
|
}
|
|
|
|
writer.WriteHeader(http.StatusOK)
|
|
err := json.NewEncoder(writer).Encode(reply{true})
|
|
common.PanicOnError(err)
|
|
}
|
|
|
|
type responseError struct {
|
|
Code int `json:"code,omitempty"`
|
|
ID string `json:"id"`
|
|
Msg string `json:"msg"`
|
|
}
|
|
|
|
// verifyStringsWithRegex checks a slice of strings against a regex of allowed characters
|
|
// it writes the InvalidChars error to the writer and returns false if any of them fail the check
|
|
// It will also return an error if the string is empty
|
|
func verifyStringsWithRegex(writer http.ResponseWriter, strings []string, re *regexp.Regexp) bool {
|
|
for _, s := range strings {
|
|
if len(s) > 0 && re.MatchString(s) {
|
|
continue
|
|
}
|
|
errors := responseError{
|
|
ID: "InvalidChars",
|
|
Msg: "Invalid characters in API path",
|
|
}
|
|
statusResponseError(writer, http.StatusBadRequest, errors)
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
func statusResponseError(writer http.ResponseWriter, code int, errors ...responseError) {
|
|
type reply struct {
|
|
Status bool `json:"status"`
|
|
Errors []responseError `json:"errors,omitempty"`
|
|
}
|
|
|
|
writer.WriteHeader(code)
|
|
err := json.NewEncoder(writer).Encode(reply{false, errors})
|
|
common.PanicOnError(err)
|
|
}
|
|
|
|
func (api *API) statusHandler(writer http.ResponseWriter, request *http.Request, _ httprouter.Params) {
|
|
type reply struct {
|
|
API string `json:"api"`
|
|
DBSupported bool `json:"db_supported"`
|
|
DBVersion string `json:"db_version"`
|
|
SchemaVersion string `json:"schema_version"`
|
|
Backend string `json:"backend"`
|
|
Build string `json:"build"`
|
|
Messages []string `json:"msgs"`
|
|
}
|
|
|
|
err := json.NewEncoder(writer).Encode(reply{
|
|
API: "1",
|
|
DBSupported: true,
|
|
DBVersion: "0",
|
|
SchemaVersion: "0",
|
|
Backend: "osbuild-composer",
|
|
Build: common.BuildVersion(),
|
|
Messages: make([]string, 0),
|
|
})
|
|
common.PanicOnError(err)
|
|
}
|
|
|
|
func (api *API) sourceListHandler(writer http.ResponseWriter, request *http.Request, params httprouter.Params) {
|
|
if !verifyRequestVersion(writer, params, 0) {
|
|
return
|
|
}
|
|
|
|
type reply struct {
|
|
Sources []string `json:"sources"`
|
|
}
|
|
|
|
// The v0 API used the repo Name, a descriptive string, as the key
|
|
// In the v1 API this was changed to separate the Name and the Id (a short identifier)
|
|
var names []string
|
|
if isRequestVersionAtLeast(params, 1) {
|
|
names = api.store.ListSourcesById()
|
|
} else {
|
|
names = api.store.ListSourcesByName()
|
|
}
|
|
names = append(names, api.systemRepoNames()...)
|
|
|
|
err := json.NewEncoder(writer).Encode(reply{
|
|
Sources: names,
|
|
})
|
|
common.PanicOnError(err)
|
|
}
|
|
|
|
func (api *API) sourceEmptyInfoHandler(writer http.ResponseWriter, request *http.Request, params httprouter.Params) {
|
|
if !verifyRequestVersion(writer, params, 0) {
|
|
return
|
|
}
|
|
|
|
errors := responseError{
|
|
Code: http.StatusNotFound,
|
|
ID: "HTTPError",
|
|
Msg: "Not Found",
|
|
}
|
|
statusResponseError(writer, http.StatusNotFound, errors)
|
|
}
|
|
|
|
// getSourceConfigs retrieves the list of sources from the system repos an store
|
|
// Returning a list of store.SourceConfig entries indexed by the id of the source
|
|
func (api *API) getSourceConfigs(params httprouter.Params) (map[string]store.SourceConfig, []responseError) {
|
|
names := params.ByName("sources")
|
|
|
|
sources := map[string]store.SourceConfig{}
|
|
errors := []responseError{}
|
|
|
|
repos, err := api.repoRegistry.ReposByArchName(api.hostDistroName, api.hostArch, false)
|
|
if err != nil {
|
|
error := responseError{
|
|
ID: "InternalError",
|
|
Msg: fmt.Sprintf("error while getting system repos: %v", err),
|
|
}
|
|
errors = append(errors, error)
|
|
}
|
|
|
|
// if names is "*" we want all sources
|
|
if names == "*" {
|
|
sources = api.store.GetAllSourcesByID()
|
|
for _, repo := range repos {
|
|
sources[repo.Name] = store.NewSourceConfig(repo, true)
|
|
}
|
|
} else {
|
|
for _, name := range strings.Split(names, ",") {
|
|
// check if the source is one of the base repos
|
|
found := false
|
|
for _, repo := range repos {
|
|
if name == repo.Name {
|
|
sources[repo.Name] = store.NewSourceConfig(repo, true)
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
if found {
|
|
continue
|
|
}
|
|
// check if the source is in the store
|
|
if source := api.store.GetSource(name); source != nil {
|
|
sources[name] = *source
|
|
} else {
|
|
error := responseError{
|
|
ID: "UnknownSource",
|
|
Msg: fmt.Sprintf("%s is not a valid source", name),
|
|
}
|
|
errors = append(errors, error)
|
|
}
|
|
}
|
|
}
|
|
|
|
return sources, errors
|
|
}
|
|
|
|
// sourceInfoHandler routes the call to the correct API version handler
|
|
func (api *API) sourceInfoHandler(writer http.ResponseWriter, request *http.Request, params httprouter.Params) {
|
|
versionString := params.ByName("version")
|
|
version, err := strconv.ParseUint(versionString, 10, 0)
|
|
if err != nil {
|
|
notFoundHandler(writer, nil)
|
|
return
|
|
}
|
|
switch version {
|
|
case 0:
|
|
api.sourceInfoHandlerV0(writer, request, params)
|
|
case 1:
|
|
api.sourceInfoHandlerV1(writer, request, params)
|
|
default:
|
|
notFoundHandler(writer, nil)
|
|
}
|
|
}
|
|
|
|
// sourceInfoHandlerV0 handles the API v0 response
|
|
func (api *API) sourceInfoHandlerV0(writer http.ResponseWriter, request *http.Request, params httprouter.Params) {
|
|
sources, errors := api.getSourceConfigs(params)
|
|
|
|
// V0 responses use the source name as the key
|
|
v0Sources := make(map[string]SourceConfigV0, len(sources))
|
|
for _, s := range sources {
|
|
v0Sources[s.Name] = NewSourceConfigV0(s)
|
|
}
|
|
|
|
q, err := url.ParseQuery(request.URL.RawQuery)
|
|
if err != nil {
|
|
errors := responseError{
|
|
ID: "InvalidChars",
|
|
Msg: fmt.Sprintf("invalid query string: %v", err),
|
|
}
|
|
statusResponseError(writer, http.StatusBadRequest, errors)
|
|
return
|
|
}
|
|
|
|
format := q.Get("format")
|
|
if format == "json" || format == "" {
|
|
err := json.NewEncoder(writer).Encode(SourceInfoResponseV0{
|
|
Sources: v0Sources,
|
|
Errors: errors,
|
|
})
|
|
common.PanicOnError(err)
|
|
} else if format == "toml" {
|
|
encoder := toml.NewEncoder(writer)
|
|
encoder.Indent = ""
|
|
err := encoder.Encode(sources)
|
|
common.PanicOnError(err)
|
|
} else {
|
|
errors := responseError{
|
|
ID: "InvalidChars",
|
|
Msg: fmt.Sprintf("invalid format parameter: %s", format),
|
|
}
|
|
statusResponseError(writer, http.StatusBadRequest, errors)
|
|
}
|
|
}
|
|
|
|
// sourceInfoHandlerV1 handles the API v0 response
|
|
func (api *API) sourceInfoHandlerV1(writer http.ResponseWriter, request *http.Request, params httprouter.Params) {
|
|
sources, errors := api.getSourceConfigs(params)
|
|
|
|
// V1 responses use the source id as the key
|
|
v1Sources := make(map[string]SourceConfigV1, len(sources))
|
|
for id, s := range sources {
|
|
v1Sources[id] = NewSourceConfigV1(id, s)
|
|
}
|
|
|
|
q, err := url.ParseQuery(request.URL.RawQuery)
|
|
if err != nil {
|
|
errors := responseError{
|
|
ID: "InvalidChars",
|
|
Msg: fmt.Sprintf("invalid query string: %v", err),
|
|
}
|
|
statusResponseError(writer, http.StatusBadRequest, errors)
|
|
return
|
|
}
|
|
|
|
format := q.Get("format")
|
|
if format == "json" || format == "" {
|
|
err := json.NewEncoder(writer).Encode(SourceInfoResponseV1{
|
|
Sources: v1Sources,
|
|
Errors: errors,
|
|
})
|
|
common.PanicOnError(err)
|
|
} else if format == "toml" {
|
|
encoder := toml.NewEncoder(writer)
|
|
encoder.Indent = ""
|
|
err := encoder.Encode(sources)
|
|
common.PanicOnError(err)
|
|
} else {
|
|
errors := responseError{
|
|
ID: "InvalidChars",
|
|
Msg: fmt.Sprintf("invalid format parameter: %s", format),
|
|
}
|
|
statusResponseError(writer, http.StatusBadRequest, errors)
|
|
}
|
|
}
|
|
|
|
// DecodeSourceConfigV0 parses a request.Body into a SourceConfigV0
|
|
func DecodeSourceConfigV0(body io.Reader, contentType string) (source SourceConfigV0, err error) {
|
|
if contentType == "application/json" {
|
|
err = json.NewDecoder(body).Decode(&source)
|
|
} else if contentType == "text/x-toml" {
|
|
// Read all of body in case it needs to be parsed twice
|
|
var data []byte
|
|
data, err = io.ReadAll(body)
|
|
if err != nil {
|
|
return source, err
|
|
}
|
|
|
|
// First try to parse [ID]\n...SOURCE... form
|
|
var srcMap map[string]SourceConfigV0
|
|
err = toml.Unmarshal(data, &srcMap)
|
|
if err != nil {
|
|
// Failed, parse it again without [ID] mapping
|
|
err = toml.Unmarshal(data, &source)
|
|
} else {
|
|
// It is possible more than 1 source could be posted. We only use the first, after sorting
|
|
keys := make([]string, 0, len(srcMap))
|
|
for k := range srcMap {
|
|
keys = append(keys, k)
|
|
}
|
|
sort.Strings(keys)
|
|
source = srcMap[keys[0]]
|
|
}
|
|
} else {
|
|
err = errors_package.New("blueprint must be in json or toml format")
|
|
}
|
|
return source, err
|
|
}
|
|
|
|
// DecodeSourceConfigV1 parses a request.Body into a SourceConfigV1
|
|
func DecodeSourceConfigV1(body io.Reader, contentType string) (source SourceConfigV1, err error) {
|
|
if contentType == "application/json" {
|
|
err = json.NewDecoder(body).Decode(&source)
|
|
} else if contentType == "text/x-toml" {
|
|
// Read all of body in case it needs to be parsed twice
|
|
var data []byte
|
|
data, err = io.ReadAll(body)
|
|
if err != nil {
|
|
return source, err
|
|
}
|
|
|
|
// First try to parse [ID]\n...SOURCE... form
|
|
var srcMap map[string]SourceConfigV1
|
|
err = toml.Unmarshal(data, &srcMap)
|
|
if err != nil {
|
|
// Failed, parse it without [ID]
|
|
err = toml.Unmarshal(data, &source)
|
|
} else {
|
|
// It is possible more than 1 source could be posted. We only use the first, after sorting
|
|
keys := make([]string, 0, len(srcMap))
|
|
for k := range srcMap {
|
|
keys = append(keys, k)
|
|
}
|
|
sort.Strings(keys)
|
|
source = srcMap[keys[0]]
|
|
|
|
// If no id was set, use the ID from the map
|
|
if len(source.ID) == 0 {
|
|
source.ID = keys[0]
|
|
}
|
|
}
|
|
} else {
|
|
err = errors_package.New("blueprint must be in json or toml format")
|
|
}
|
|
|
|
if err == nil && len(source.GetKey()) == 0 {
|
|
err = errors_package.New("'id' field is missing from request")
|
|
}
|
|
|
|
return source, err
|
|
}
|
|
|
|
func (api *API) sourceNewHandler(writer http.ResponseWriter, request *http.Request, params httprouter.Params) {
|
|
if !verifyRequestVersion(writer, params, 0) { // TODO: version 1 API
|
|
return
|
|
}
|
|
|
|
contentType := request.Header["Content-Type"]
|
|
if len(contentType) == 0 {
|
|
errors := responseError{
|
|
ID: "HTTPError",
|
|
Msg: "malformed Content-Type header",
|
|
}
|
|
statusResponseError(writer, http.StatusBadRequest, errors)
|
|
return
|
|
}
|
|
|
|
if request.ContentLength == 0 {
|
|
errors := responseError{
|
|
ID: "ProjectsError",
|
|
Msg: "Missing source",
|
|
}
|
|
statusResponseError(writer, http.StatusBadRequest, errors)
|
|
return
|
|
}
|
|
|
|
var source SourceConfig
|
|
var err error
|
|
if isRequestVersionAtLeast(params, 1) {
|
|
source, err = DecodeSourceConfigV1(request.Body, contentType[0])
|
|
} else {
|
|
source, err = DecodeSourceConfigV0(request.Body, contentType[0])
|
|
}
|
|
|
|
// Basic check of the source, should at least have a name and type
|
|
if err == nil {
|
|
if len(source.GetName()) == 0 {
|
|
err = errors_package.New("'name' field is missing from request")
|
|
} else if len(source.GetType()) == 0 {
|
|
err = errors_package.New("'type' field is missing from request")
|
|
} else if len(source.SourceConfig().URL) == 0 {
|
|
err = errors_package.New("'url' field is missing from request")
|
|
}
|
|
}
|
|
if err != nil {
|
|
errors := responseError{
|
|
ID: "ProjectsError",
|
|
Msg: "Problem parsing POST body: " + err.Error(),
|
|
}
|
|
statusResponseError(writer, http.StatusBadRequest, errors)
|
|
return
|
|
}
|
|
|
|
// If there is a list of distros, check to make sure they are valid
|
|
invalid := []string{}
|
|
for _, d := range source.SourceConfig().Distros {
|
|
if !common.IsStringInSortedSlice(api.validDistros(api.hostArch), d) {
|
|
invalid = append(invalid, d)
|
|
}
|
|
}
|
|
if len(invalid) > 0 {
|
|
errors := responseError{
|
|
ID: "ProjectsError",
|
|
Msg: "Invalid distributions: " + strings.Join(invalid, ","),
|
|
}
|
|
statusResponseError(writer, http.StatusBadRequest, errors)
|
|
return
|
|
}
|
|
|
|
// Is there an existing System Repo using this id?
|
|
for _, n := range api.systemRepoNames() {
|
|
if n == source.GetKey() {
|
|
// Users cannot replace system repos
|
|
errors := responseError{
|
|
ID: "SystemSource",
|
|
Msg: fmt.Sprintf("%s is a system source, it cannot be changed.", source.GetKey()),
|
|
}
|
|
statusResponseError(writer, http.StatusBadRequest, errors)
|
|
return
|
|
}
|
|
}
|
|
|
|
api.store.PushSource(source.GetKey(), source.SourceConfig())
|
|
|
|
statusResponseOK(writer)
|
|
}
|
|
|
|
func (api *API) sourceDeleteHandler(writer http.ResponseWriter, request *http.Request, params httprouter.Params) {
|
|
if !verifyRequestVersion(writer, params, 0) {
|
|
return
|
|
}
|
|
|
|
name := strings.Split(params.ByName("source"), ",")
|
|
|
|
if name[0] == "/" {
|
|
errors := responseError{
|
|
Code: http.StatusNotFound,
|
|
ID: "HTTPError",
|
|
Msg: "Not Found",
|
|
}
|
|
statusResponseError(writer, http.StatusNotFound, errors)
|
|
return
|
|
}
|
|
|
|
// Check for system repos and return an error
|
|
for _, id := range api.systemRepoNames() {
|
|
if id == name[0][1:] {
|
|
errors := responseError{
|
|
ID: "SystemSource",
|
|
Msg: id + " is a system source, it cannot be deleted.",
|
|
}
|
|
statusResponseError(writer, http.StatusBadRequest, errors)
|
|
return
|
|
}
|
|
}
|
|
|
|
// Return an error for unknown sources
|
|
s := api.store.GetSource(name[0][1:])
|
|
if s == nil {
|
|
errors := responseError{
|
|
ID: "UnknownSource",
|
|
Msg: name[0][1:] + " is not a valid source.",
|
|
}
|
|
statusResponseError(writer, http.StatusBadRequest, errors)
|
|
return
|
|
}
|
|
|
|
// Only delete the first name, which will have a / at the start because of the /*source route
|
|
if isRequestVersionAtLeast(params, 1) {
|
|
api.store.DeleteSourceByID(name[0][1:])
|
|
} else {
|
|
api.store.DeleteSourceByName(name[0][1:])
|
|
}
|
|
|
|
statusResponseOK(writer)
|
|
}
|
|
|
|
func (api *API) modulesListHandler(writer http.ResponseWriter, request *http.Request, params httprouter.Params) {
|
|
if !verifyRequestVersion(writer, params, 0) {
|
|
return
|
|
}
|
|
|
|
type module struct {
|
|
Name string `json:"name"`
|
|
GroupType string `json:"group_type"`
|
|
}
|
|
|
|
type reply struct {
|
|
Total uint `json:"total"`
|
|
Offset uint `json:"offset"`
|
|
Limit uint `json:"limit"`
|
|
Modules []module `json:"modules"`
|
|
}
|
|
|
|
offset, limit, err := parseOffsetAndLimit(request.URL.Query())
|
|
if err != nil {
|
|
errors := responseError{
|
|
ID: "BadLimitOrOffset",
|
|
Msg: fmt.Sprintf("BadRequest: %s", err.Error()),
|
|
}
|
|
|
|
statusResponseError(writer, http.StatusBadRequest, errors)
|
|
return
|
|
}
|
|
|
|
modulesParam := params.ByName("modules")
|
|
|
|
archName := api.hostArch
|
|
if queryArch := request.URL.Query().Get("arch"); queryArch != "" {
|
|
archName = queryArch
|
|
}
|
|
|
|
// Optional distro parameter
|
|
// If it is empty it will return api.hostDistroName
|
|
distroName, err := api.parseDistro(request.URL.Query(), archName)
|
|
if err != nil {
|
|
errors := responseError{
|
|
ID: "DistroError",
|
|
Msg: err.Error(),
|
|
}
|
|
statusResponseError(writer, http.StatusBadRequest, errors)
|
|
return
|
|
}
|
|
|
|
var names []string
|
|
if modulesParam != "" && modulesParam != "/" {
|
|
// we have modules for search
|
|
|
|
// remove leading /
|
|
modulesParam = modulesParam[1:]
|
|
|
|
names = strings.Split(modulesParam, ",")
|
|
}
|
|
packages, err := api.fetchPackageList(distroName, archName, names)
|
|
|
|
if err != nil {
|
|
errors := responseError{
|
|
ID: "ModulesError",
|
|
Msg: fmt.Sprintf("msg: %s", err.Error()),
|
|
}
|
|
statusResponseError(writer, http.StatusBadRequest, errors)
|
|
return
|
|
}
|
|
|
|
if len(packages) == 0 {
|
|
errors := responseError{
|
|
ID: "UnknownModule",
|
|
Msg: "No packages have been found.",
|
|
}
|
|
statusResponseError(writer, http.StatusBadRequest, errors)
|
|
return
|
|
}
|
|
|
|
packageInfos := packages.ToPackageInfos()
|
|
|
|
total := uint(len(packageInfos))
|
|
start := min(offset, total)
|
|
n := min(limit, total-start)
|
|
|
|
modules := make([]module, n)
|
|
for i := uint(0); i < n; i++ {
|
|
modules[i] = module{packageInfos[start+i].Name, "rpm"}
|
|
}
|
|
|
|
err = json.NewEncoder(writer).Encode(reply{
|
|
Total: total,
|
|
Offset: offset,
|
|
Limit: limit,
|
|
Modules: modules,
|
|
})
|
|
common.PanicOnError(err)
|
|
}
|
|
|
|
func (api *API) projectsListHandler(writer http.ResponseWriter, request *http.Request, params httprouter.Params) {
|
|
if !verifyRequestVersion(writer, params, 0) {
|
|
return
|
|
}
|
|
|
|
type reply struct {
|
|
Total uint `json:"total"`
|
|
Offset uint `json:"offset"`
|
|
Limit uint `json:"limit"`
|
|
Projects []rpmmd.PackageInfo `json:"projects"`
|
|
}
|
|
|
|
offset, limit, err := parseOffsetAndLimit(request.URL.Query())
|
|
if err != nil {
|
|
errors := responseError{
|
|
ID: "BadLimitOrOffset",
|
|
Msg: fmt.Sprintf("BadRequest: %s", err.Error()),
|
|
}
|
|
statusResponseError(writer, http.StatusBadRequest, errors)
|
|
return
|
|
}
|
|
|
|
archName := api.hostArch
|
|
if queryArch := request.URL.Query().Get("arch"); queryArch != "" {
|
|
archName = queryArch
|
|
}
|
|
|
|
// Optional distro parameter
|
|
// If it is empty it will return api.hostDistroName
|
|
distroName, err := api.parseDistro(request.URL.Query(), archName)
|
|
if err != nil {
|
|
errors := responseError{
|
|
ID: "DistroError",
|
|
Msg: err.Error(),
|
|
}
|
|
statusResponseError(writer, http.StatusBadRequest, errors)
|
|
return
|
|
}
|
|
availablePackages, err := api.fetchPackageList(distroName, archName, []string{})
|
|
|
|
if err != nil {
|
|
errors := responseError{
|
|
ID: "ProjectsError",
|
|
Msg: fmt.Sprintf("msg: %s", err.Error()),
|
|
}
|
|
statusResponseError(writer, http.StatusBadRequest, errors)
|
|
return
|
|
}
|
|
|
|
packageInfos := availablePackages.ToPackageInfos()
|
|
|
|
total := uint(len(packageInfos))
|
|
start := min(offset, total)
|
|
n := min(limit, total-start)
|
|
|
|
packages := make([]rpmmd.PackageInfo, n)
|
|
for i := uint(0); i < n; i++ {
|
|
packages[i] = packageInfos[start+i]
|
|
}
|
|
|
|
err = json.NewEncoder(writer).Encode(reply{
|
|
Total: total,
|
|
Offset: offset,
|
|
Limit: limit,
|
|
Projects: packages,
|
|
})
|
|
common.PanicOnError(err)
|
|
}
|
|
|
|
func (api *API) modulesInfoHandler(writer http.ResponseWriter, request *http.Request, params httprouter.Params) {
|
|
if !verifyRequestVersion(writer, params, 0) {
|
|
return
|
|
}
|
|
|
|
type projectsReply struct {
|
|
Projects []rpmmd.PackageInfo `json:"projects"`
|
|
}
|
|
type modulesReply struct {
|
|
Modules []rpmmd.PackageInfo `json:"modules"`
|
|
}
|
|
|
|
// handle both projects/info and modules/info, the latter includes dependencies
|
|
modulesRequested := strings.HasPrefix(request.URL.Path, "/api/v0/modules") ||
|
|
strings.HasPrefix(request.URL.Path, "/api/v1/modules")
|
|
|
|
var errorId, unknownErrorId string
|
|
if modulesRequested {
|
|
errorId = "ModulesError"
|
|
unknownErrorId = "UnknownModule"
|
|
} else {
|
|
errorId = "ProjectsError"
|
|
unknownErrorId = "UnknownProject"
|
|
}
|
|
|
|
modules := params.ByName("modules")
|
|
|
|
if modules == "" || modules == "/" {
|
|
errors := responseError{
|
|
ID: unknownErrorId,
|
|
Msg: "No packages specified.",
|
|
}
|
|
statusResponseError(writer, http.StatusBadRequest, errors)
|
|
return
|
|
}
|
|
|
|
// remove leading /
|
|
modules = modules[1:]
|
|
|
|
names := strings.Split(modules, ",")
|
|
|
|
archName := api.hostArch
|
|
if queryArch := request.URL.Query().Get("arch"); queryArch != "" {
|
|
archName = queryArch
|
|
}
|
|
|
|
// Optional distro parameter
|
|
// If it is empty it will return api.hostDistroName
|
|
distroName, err := api.parseDistro(request.URL.Query(), archName)
|
|
if err != nil {
|
|
errors := responseError{
|
|
ID: "DistroError",
|
|
Msg: err.Error(),
|
|
}
|
|
statusResponseError(writer, http.StatusBadRequest, errors)
|
|
return
|
|
}
|
|
foundPackages, err := api.fetchPackageList(distroName, archName, names)
|
|
|
|
if err != nil {
|
|
errors := responseError{
|
|
ID: errorId,
|
|
Msg: fmt.Sprintf("msg: %s", err.Error()),
|
|
}
|
|
statusResponseError(writer, http.StatusBadRequest, errors)
|
|
return
|
|
}
|
|
|
|
if len(foundPackages) == 0 {
|
|
errors := responseError{
|
|
ID: unknownErrorId,
|
|
Msg: "No packages have been found.",
|
|
}
|
|
statusResponseError(writer, http.StatusBadRequest, errors)
|
|
return
|
|
}
|
|
|
|
packageInfos := foundPackages.ToPackageInfos()
|
|
|
|
if modulesRequested {
|
|
repos, err := api.allRepositories(distroName, archName)
|
|
if err != nil {
|
|
errors := responseError{
|
|
ID: "InternalError",
|
|
Msg: fmt.Sprintf("error while getting system repos: %v", err),
|
|
}
|
|
statusResponseError(writer, http.StatusBadRequest, errors)
|
|
return
|
|
}
|
|
d := api.getDistro(distroName, archName)
|
|
if d == nil {
|
|
errors := responseError{
|
|
ID: "DistroError",
|
|
Msg: fmt.Sprintf("Unknown distribution: %s", distroName),
|
|
}
|
|
statusResponseError(writer, http.StatusBadRequest, errors)
|
|
return
|
|
}
|
|
|
|
solver := api.solver.NewWithConfig(d.ModulePlatformID(), d.Releasever(), archName, d.Name())
|
|
for i := range packageInfos {
|
|
pkgName := packageInfos[i].Name
|
|
res, err := solver.Depsolve([]rpmmd.PackageSet{{Include: []string{pkgName}, Repositories: repos}}, sbom.StandardTypeNone)
|
|
if err != nil {
|
|
errors := responseError{
|
|
ID: errorId,
|
|
Msg: fmt.Sprintf("Cannot depsolve package %s: %s", packageInfos[i].Name, err.Error()),
|
|
}
|
|
statusResponseError(writer, http.StatusBadRequest, errors)
|
|
return
|
|
}
|
|
packageInfos[i].Dependencies = res.Packages
|
|
}
|
|
if err := solver.CleanCache(); err != nil {
|
|
// log and ignore
|
|
log.Printf("Error during rpm repo cache cleanup: %s", err.Error())
|
|
}
|
|
}
|
|
|
|
if modulesRequested {
|
|
err = json.NewEncoder(writer).Encode(modulesReply{packageInfos})
|
|
} else {
|
|
err = json.NewEncoder(writer).Encode(projectsReply{packageInfos})
|
|
}
|
|
common.PanicOnError(err)
|
|
}
|
|
|
|
func (api *API) projectsDepsolveHandler(writer http.ResponseWriter, request *http.Request, params httprouter.Params) {
|
|
if !verifyRequestVersion(writer, params, 0) {
|
|
return
|
|
}
|
|
|
|
type reply struct {
|
|
Projects []rpmmd.PackageSpec `json:"projects"`
|
|
}
|
|
|
|
projects := params.ByName("projects")
|
|
|
|
if projects == "" || projects == "/" {
|
|
errors := responseError{
|
|
ID: "UnknownProject",
|
|
Msg: "No packages specified.",
|
|
}
|
|
statusResponseError(writer, http.StatusBadRequest, errors)
|
|
return
|
|
}
|
|
|
|
// remove leading /
|
|
projects = projects[1:]
|
|
names := strings.Split(projects, ",")
|
|
|
|
archName := api.hostArch
|
|
if queryArch := request.URL.Query().Get("arch"); queryArch != "" {
|
|
archName = queryArch
|
|
}
|
|
|
|
// Optional distro parameter
|
|
// If it is empty it will return api.hostDistroName
|
|
distroName, err := api.parseDistro(request.URL.Query(), archName)
|
|
if err != nil {
|
|
errors := responseError{
|
|
ID: "DistroError",
|
|
Msg: err.Error(),
|
|
}
|
|
statusResponseError(writer, http.StatusBadRequest, errors)
|
|
return
|
|
}
|
|
|
|
d := api.getDistro(distroName, archName)
|
|
if d == nil {
|
|
errors := responseError{
|
|
ID: "DistroError",
|
|
Msg: fmt.Sprintf("Unknown distribution: %s", distroName),
|
|
}
|
|
statusResponseError(writer, http.StatusBadRequest, errors)
|
|
return
|
|
}
|
|
|
|
repos, err := api.allRepositories(distroName, archName)
|
|
if err != nil {
|
|
errors := responseError{
|
|
ID: "ProjectsError",
|
|
Msg: err.Error(),
|
|
}
|
|
statusResponseError(writer, http.StatusBadRequest, errors)
|
|
return
|
|
}
|
|
|
|
solver := api.solver.NewWithConfig(d.ModulePlatformID(), d.Releasever(), archName, d.Name())
|
|
res, err := solver.Depsolve([]rpmmd.PackageSet{{Include: names, Repositories: repos}}, sbom.StandardTypeNone)
|
|
if err != nil {
|
|
errors := responseError{
|
|
ID: "ProjectsError",
|
|
Msg: fmt.Sprintf("BadRequest: %s", err.Error()),
|
|
}
|
|
statusResponseError(writer, http.StatusBadRequest, errors)
|
|
return
|
|
}
|
|
if err := solver.CleanCache(); err != nil {
|
|
// log and ignore
|
|
log.Printf("Error during rpm repo cache cleanup: %s", err.Error())
|
|
}
|
|
err = json.NewEncoder(writer).Encode(reply{Projects: res.Packages})
|
|
common.PanicOnError(err)
|
|
}
|
|
|
|
func (api *API) blueprintsListHandler(writer http.ResponseWriter, request *http.Request, params httprouter.Params) {
|
|
if !verifyRequestVersion(writer, params, 0) {
|
|
return
|
|
}
|
|
|
|
type reply struct {
|
|
Total uint `json:"total"`
|
|
Offset uint `json:"offset"`
|
|
Limit uint `json:"limit"`
|
|
Blueprints []string `json:"blueprints"`
|
|
}
|
|
|
|
offset, limit, err := parseOffsetAndLimit(request.URL.Query())
|
|
if err != nil {
|
|
errors := responseError{
|
|
ID: "BadLimitOrOffset",
|
|
Msg: fmt.Sprintf("BadRequest: %s", err.Error()),
|
|
}
|
|
statusResponseError(writer, http.StatusBadRequest, errors)
|
|
return
|
|
}
|
|
|
|
names := api.store.ListBlueprints()
|
|
total := uint(len(names))
|
|
offset = min(offset, total)
|
|
limit = min(limit, total-offset)
|
|
|
|
err = json.NewEncoder(writer).Encode(reply{
|
|
Total: total,
|
|
Offset: offset,
|
|
Limit: limit,
|
|
Blueprints: names[offset : offset+limit],
|
|
})
|
|
common.PanicOnError(err)
|
|
}
|
|
|
|
func (api *API) blueprintsInfoHandler(writer http.ResponseWriter, request *http.Request, params httprouter.Params) {
|
|
if !verifyRequestVersion(writer, params, 0) {
|
|
return
|
|
}
|
|
|
|
type change struct {
|
|
Changed bool `json:"changed"`
|
|
Name string `json:"name"`
|
|
}
|
|
type reply struct {
|
|
Blueprints []blueprint.Blueprint `json:"blueprints"`
|
|
Changes []change `json:"changes"`
|
|
Errors []responseError `json:"errors"`
|
|
}
|
|
|
|
names := strings.Split(params.ByName("blueprints"), ",")
|
|
if names[0] == "/" {
|
|
errors := responseError{
|
|
Code: http.StatusNotFound,
|
|
ID: "HTTPError",
|
|
Msg: "Not Found",
|
|
}
|
|
statusResponseError(writer, http.StatusNotFound, errors)
|
|
return
|
|
}
|
|
|
|
// Remove the leading / from the first entry (check above ensures it is not just a /
|
|
names[0] = names[0][1:]
|
|
|
|
if !verifyStringsWithRegex(writer, names, ValidBlueprintName) {
|
|
return
|
|
}
|
|
|
|
query, err := url.ParseQuery(request.URL.RawQuery)
|
|
if err != nil {
|
|
errors := responseError{
|
|
ID: "InvalidChars",
|
|
Msg: fmt.Sprintf("invalid query string: %v", err),
|
|
}
|
|
statusResponseError(writer, http.StatusBadRequest, errors)
|
|
return
|
|
}
|
|
|
|
blueprints := []blueprint.Blueprint{}
|
|
changes := []change{}
|
|
blueprintErrors := []responseError{}
|
|
|
|
for _, name := range names {
|
|
blueprint, changed := api.store.GetBlueprint(name)
|
|
if blueprint == nil {
|
|
blueprintErrors = append(blueprintErrors, responseError{
|
|
ID: "UnknownBlueprint",
|
|
Msg: fmt.Sprintf("%s: ", name),
|
|
})
|
|
continue
|
|
}
|
|
blueprints = append(blueprints, *blueprint)
|
|
changes = append(changes, change{changed, blueprint.Name})
|
|
}
|
|
|
|
format := query.Get("format")
|
|
if format == "json" || format == "" {
|
|
err := json.NewEncoder(writer).Encode(reply{
|
|
Blueprints: blueprints,
|
|
Changes: changes,
|
|
Errors: blueprintErrors,
|
|
})
|
|
common.PanicOnError(err)
|
|
} else if format == "toml" {
|
|
// lorax concatenates multiple blueprints with `\n\n` here,
|
|
// which is never useful. Deviate by only returning the first
|
|
// blueprint.
|
|
if len(blueprints) > 1 {
|
|
errors := responseError{
|
|
ID: "HTTPError",
|
|
Msg: "toml format only supported when requesting one blueprint",
|
|
}
|
|
statusResponseError(writer, http.StatusBadRequest, errors)
|
|
return
|
|
}
|
|
if len(blueprintErrors) > 0 {
|
|
statusResponseError(writer, http.StatusBadRequest, blueprintErrors...)
|
|
return
|
|
}
|
|
encoder := toml.NewEncoder(writer)
|
|
encoder.Indent = ""
|
|
err := encoder.Encode(blueprints[0])
|
|
common.PanicOnError(err)
|
|
} else {
|
|
errors := responseError{
|
|
ID: "InvalidChars",
|
|
Msg: fmt.Sprintf("invalid format parameter: %s", format),
|
|
}
|
|
statusResponseError(writer, http.StatusBadRequest, errors)
|
|
return
|
|
}
|
|
}
|
|
|
|
func (api *API) blueprintsDepsolveHandler(writer http.ResponseWriter, request *http.Request, params httprouter.Params) {
|
|
if !verifyRequestVersion(writer, params, 0) {
|
|
return
|
|
}
|
|
|
|
type entry struct {
|
|
Blueprint blueprint.Blueprint `json:"blueprint"`
|
|
Dependencies []rpmmd.PackageSpec `json:"dependencies"`
|
|
}
|
|
type reply struct {
|
|
Blueprints []entry `json:"blueprints"`
|
|
Errors []responseError `json:"errors"`
|
|
}
|
|
|
|
names := strings.Split(params.ByName("blueprints"), ",")
|
|
if names[0] == "/" {
|
|
errors := responseError{
|
|
Code: http.StatusNotFound,
|
|
ID: "HTTPError",
|
|
Msg: "Not Found",
|
|
}
|
|
statusResponseError(writer, http.StatusNotFound, errors)
|
|
return
|
|
}
|
|
|
|
// Remove the leading / from the first entry (check above ensures it is not just a /
|
|
names[0] = names[0][1:]
|
|
|
|
if !verifyStringsWithRegex(writer, names, ValidBlueprintName) {
|
|
return
|
|
}
|
|
|
|
blueprints := []entry{}
|
|
blueprintsErrors := []responseError{}
|
|
for _, name := range names {
|
|
blueprint, _ := api.store.GetBlueprint(name)
|
|
if blueprint == nil {
|
|
blueprintsErrors = append(blueprintsErrors, responseError{
|
|
ID: "UnknownBlueprint",
|
|
Msg: fmt.Sprintf("%s: blueprint not found", name),
|
|
})
|
|
continue
|
|
}
|
|
|
|
dependencies, err := api.depsolveBlueprint(*blueprint)
|
|
|
|
if err != nil {
|
|
blueprintsErrors = append(blueprintsErrors, responseError{
|
|
ID: "BlueprintsError",
|
|
Msg: fmt.Sprintf("%s: %s", name, err.Error()),
|
|
})
|
|
dependencies = []rpmmd.PackageSpec{}
|
|
}
|
|
|
|
blueprints = append(blueprints, entry{*blueprint, dependencies})
|
|
}
|
|
|
|
err := json.NewEncoder(writer).Encode(reply{
|
|
Blueprints: blueprints,
|
|
Errors: blueprintsErrors,
|
|
})
|
|
common.PanicOnError(err)
|
|
}
|
|
|
|
// expandBlueprintGlobs will expand package name globs and versions using the depsolve results
|
|
// The result is a sorted list of Package structs with the full package name and version
|
|
// It will return an error if it cannot find a non-glob package name in the dependency list
|
|
func expandBlueprintGlobs(dependencies []rpmmd.PackageSpec, packages []blueprint.Package) ([]blueprint.Package, error) {
|
|
newPackages := make(map[string]blueprint.Package)
|
|
|
|
for _, pkg := range packages {
|
|
if !strings.ContainsAny(pkg.Name, "*?") {
|
|
// No glob characters, find an exact match in dependencies
|
|
i := sort.Search(len(dependencies), func(i int) bool {
|
|
return dependencies[i].Name >= pkg.Name
|
|
})
|
|
if i >= len(dependencies) || dependencies[i].Name != pkg.Name {
|
|
// Packages should not be missing from the depsolve results
|
|
return nil, fmt.Errorf("%s missing from depsolve results", pkg.Name)
|
|
}
|
|
newPackages[dependencies[i].GetNEVRA()] = blueprint.Package{
|
|
Name: dependencies[i].Name,
|
|
Version: dependencies[i].GetEVRA(),
|
|
}
|
|
} else {
|
|
// Add all the packages matching the glob
|
|
g, err := glob.Compile(pkg.Name)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
for _, d := range dependencies {
|
|
if g.Match(d.Name) {
|
|
newPackages[d.GetNEVRA()] = blueprint.Package{
|
|
Name: d.Name,
|
|
Version: d.GetEVRA(),
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Return a sorted slice of the Packages
|
|
np := make([]blueprint.Package, 0, len(newPackages))
|
|
for _, pkg := range newPackages {
|
|
np = append(np, pkg)
|
|
}
|
|
sort.Slice(np, func(i, j int) bool {
|
|
return np[i].Name < np[j].Name
|
|
})
|
|
|
|
return np, nil
|
|
}
|
|
|
|
func (api *API) blueprintsFreezeHandler(writer http.ResponseWriter, request *http.Request, params httprouter.Params) {
|
|
if !verifyRequestVersion(writer, params, 0) {
|
|
return
|
|
}
|
|
|
|
type blueprintFrozen struct {
|
|
Blueprint blueprint.Blueprint `json:"blueprint"`
|
|
}
|
|
|
|
type reply struct {
|
|
Blueprints []blueprintFrozen `json:"blueprints"`
|
|
Errors []responseError `json:"errors"`
|
|
}
|
|
|
|
names := strings.Split(params.ByName("blueprints"), ",")
|
|
if names[0] == "/" {
|
|
errors := responseError{
|
|
Code: http.StatusNotFound,
|
|
ID: "HTTPError",
|
|
Msg: "Not Found",
|
|
}
|
|
statusResponseError(writer, http.StatusNotFound, errors)
|
|
return
|
|
}
|
|
|
|
// Remove the leading / from the first entry (check above ensures it is not just a /
|
|
names[0] = names[0][1:]
|
|
|
|
if !verifyStringsWithRegex(writer, names, ValidBlueprintName) {
|
|
return
|
|
}
|
|
|
|
blueprints := []blueprintFrozen{}
|
|
errors := []responseError{}
|
|
for _, name := range names {
|
|
bp, _ := api.store.GetBlueprint(name)
|
|
if bp == nil {
|
|
rerr := responseError{
|
|
ID: "UnknownBlueprint",
|
|
Msg: fmt.Sprintf("%s: blueprint not found", name),
|
|
}
|
|
errors = append(errors, rerr)
|
|
break
|
|
}
|
|
// Make a copy of the blueprint since we will be replacing the version globs
|
|
blueprint := bp.DeepCopy()
|
|
dependencies, err := api.depsolveBlueprint(blueprint)
|
|
if err != nil {
|
|
rerr := responseError{
|
|
ID: "BlueprintsError",
|
|
Msg: fmt.Sprintf("%s: %s", name, err.Error()),
|
|
}
|
|
errors = append(errors, rerr)
|
|
break
|
|
}
|
|
// Sort dependencies by Name (names should be unique so no need to sort by EVRA)
|
|
sort.Slice(dependencies, func(i, j int) bool {
|
|
return dependencies[i].Name < dependencies[j].Name
|
|
})
|
|
|
|
// Expand any package name globs and set the package version
|
|
newPackages, err := expandBlueprintGlobs(dependencies, blueprint.Packages)
|
|
if err != nil {
|
|
rerr := responseError{
|
|
ID: "BlueprintsError",
|
|
Msg: fmt.Sprintf("%s: %s", name, err.Error()),
|
|
}
|
|
errors = append(errors, rerr)
|
|
break
|
|
}
|
|
blueprint.Packages = newPackages
|
|
|
|
// Expand any module name globs and set the module version
|
|
newModules, err := expandBlueprintGlobs(dependencies, blueprint.Modules)
|
|
if err != nil {
|
|
rerr := responseError{
|
|
ID: "BlueprintsError",
|
|
Msg: fmt.Sprintf("%s: %s", name, err.Error()),
|
|
}
|
|
errors = append(errors, rerr)
|
|
break
|
|
}
|
|
blueprint.Modules = newModules
|
|
blueprints = append(blueprints, blueprintFrozen{blueprint})
|
|
}
|
|
|
|
format := request.URL.Query().Get("format")
|
|
|
|
if format == "toml" {
|
|
// lorax concatenates multiple blueprints with `\n\n` here,
|
|
// which is never useful. Deviate by only returning the first
|
|
// blueprint.
|
|
if len(blueprints) == 0 {
|
|
// lorax-composer just outputs an empty string if there were no blueprints
|
|
writer.WriteHeader(http.StatusOK)
|
|
fmt.Fprintf(writer, "")
|
|
return
|
|
} else if len(blueprints) > 1 {
|
|
errors := responseError{
|
|
ID: "HTTPError",
|
|
Msg: "toml format only supported when requesting one blueprint",
|
|
}
|
|
statusResponseError(writer, http.StatusBadRequest, errors)
|
|
return
|
|
}
|
|
|
|
encoder := toml.NewEncoder(writer)
|
|
encoder.Indent = ""
|
|
err := encoder.Encode(blueprints[0].Blueprint)
|
|
common.PanicOnError(err)
|
|
} else {
|
|
err := json.NewEncoder(writer).Encode(reply{
|
|
Blueprints: blueprints,
|
|
Errors: errors,
|
|
})
|
|
common.PanicOnError(err)
|
|
}
|
|
}
|
|
|
|
func (api *API) blueprintsDiffHandler(writer http.ResponseWriter, request *http.Request, params httprouter.Params) {
|
|
if !verifyRequestVersion(writer, params, 0) {
|
|
return
|
|
}
|
|
|
|
type pack struct {
|
|
Package blueprint.Package `json:"Package"`
|
|
}
|
|
|
|
type diff struct {
|
|
New *pack `json:"new"`
|
|
Old *pack `json:"old"`
|
|
}
|
|
|
|
type reply struct {
|
|
Diffs []diff `json:"diff"`
|
|
}
|
|
|
|
name := params.ByName("blueprint")
|
|
if !verifyStringsWithRegex(writer, []string{name}, ValidBlueprintName) {
|
|
return
|
|
}
|
|
|
|
fromCommit := params.ByName("from")
|
|
if !verifyStringsWithRegex(writer, []string{fromCommit}, ValidBlueprintName) {
|
|
return
|
|
}
|
|
|
|
toCommit := params.ByName("to")
|
|
if !verifyStringsWithRegex(writer, []string{toCommit}, ValidBlueprintName) {
|
|
return
|
|
}
|
|
|
|
if len(name) == 0 || len(fromCommit) == 0 || len(toCommit) == 0 {
|
|
errors := responseError{
|
|
Code: http.StatusNotFound,
|
|
ID: "HTTPError",
|
|
Msg: "Not Found",
|
|
}
|
|
statusResponseError(writer, http.StatusNotFound, errors)
|
|
return
|
|
}
|
|
if fromCommit != "NEWEST" {
|
|
errors := responseError{
|
|
ID: "UnknownCommit",
|
|
Msg: fmt.Sprintf("ggit-error: revspec '%s' not found (-3)", fromCommit),
|
|
}
|
|
statusResponseError(writer, http.StatusBadRequest, errors)
|
|
return
|
|
}
|
|
if toCommit != "WORKSPACE" {
|
|
errors := responseError{
|
|
ID: "UnknownCommit",
|
|
Msg: fmt.Sprintf("ggit-error: revspec '%s' not found (-3)", toCommit),
|
|
}
|
|
statusResponseError(writer, http.StatusBadRequest, errors)
|
|
return
|
|
}
|
|
|
|
// Fetch old and new blueprint details from store and return error if not found
|
|
oldBlueprint := api.store.GetBlueprintCommitted(name)
|
|
newBlueprint, _ := api.store.GetBlueprint(name)
|
|
if oldBlueprint == nil || newBlueprint == nil {
|
|
errors := responseError{
|
|
ID: "UnknownBlueprint",
|
|
Msg: fmt.Sprintf("Unknown blueprint name: %s", name),
|
|
}
|
|
statusResponseError(writer, http.StatusNotFound, errors)
|
|
return
|
|
}
|
|
|
|
newSlice := newBlueprint.Packages
|
|
oldMap := make(map[string]blueprint.Package)
|
|
diffs := []diff{}
|
|
|
|
for _, oldPackage := range oldBlueprint.Packages {
|
|
oldMap[oldPackage.Name] = oldPackage
|
|
}
|
|
|
|
// For each package in new blueprint check if the old one contains it
|
|
for _, newPackage := range newSlice {
|
|
oldPackage, found := oldMap[newPackage.Name]
|
|
// If found remove from old packages map but otherwise create a diff with the added package
|
|
if found {
|
|
delete(oldMap, oldPackage.Name)
|
|
// Create a diff if the versions changed
|
|
if oldPackage.Version != newPackage.Version {
|
|
diffs = append(diffs, diff{Old: &pack{oldPackage}, New: &pack{newPackage}})
|
|
}
|
|
} else {
|
|
diffs = append(diffs, diff{Old: nil, New: &pack{newPackage}})
|
|
}
|
|
}
|
|
|
|
// All packages remaining in the old packages map have been removed in the new blueprint so create a diff
|
|
for _, oldPackage := range oldMap {
|
|
diffs = append(diffs, diff{Old: &pack{oldPackage}, New: nil})
|
|
}
|
|
|
|
err := json.NewEncoder(writer).Encode(reply{diffs})
|
|
common.PanicOnError(err)
|
|
}
|
|
|
|
// blueprintsChangeHandler returns a specific change to a blueprint
|
|
func (api *API) blueprintsChangeHandler(writer http.ResponseWriter, request *http.Request, params httprouter.Params) {
|
|
if !verifyRequestVersion(writer, params, 1) {
|
|
return
|
|
}
|
|
name := params.ByName("blueprint")
|
|
if !verifyStringsWithRegex(writer, []string{name}, ValidBlueprintName) {
|
|
return
|
|
}
|
|
|
|
commit := params.ByName("commit")
|
|
if !verifyStringsWithRegex(writer, []string{commit}, ValidBlueprintName) {
|
|
return
|
|
}
|
|
|
|
bpChange, err := api.store.GetBlueprintChange(name, commit)
|
|
if err != nil {
|
|
errors := responseError{
|
|
ID: "UnknownCommit",
|
|
Msg: err.Error(),
|
|
}
|
|
statusResponseError(writer, http.StatusBadRequest, errors)
|
|
return
|
|
}
|
|
|
|
bp := bpChange.Blueprint
|
|
if len(bpChange.Blueprint.Name) == 0 {
|
|
errors := responseError{
|
|
ID: "BlueprintsError",
|
|
Msg: fmt.Sprintf("no blueprint found for commit %s", commit),
|
|
}
|
|
statusResponseError(writer, http.StatusBadRequest, errors)
|
|
return
|
|
}
|
|
|
|
query, err := url.ParseQuery(request.URL.RawQuery)
|
|
if err != nil {
|
|
errors := responseError{
|
|
ID: "InvalidChars",
|
|
Msg: fmt.Sprintf("invalid query string: %v", err),
|
|
}
|
|
statusResponseError(writer, http.StatusBadRequest, errors)
|
|
return
|
|
}
|
|
|
|
format := query.Get("format")
|
|
if format == "json" || format == "" {
|
|
err := json.NewEncoder(writer).Encode(bp)
|
|
common.PanicOnError(err)
|
|
} else if format == "toml" {
|
|
encoder := toml.NewEncoder(writer)
|
|
encoder.Indent = ""
|
|
err := encoder.Encode(bp)
|
|
common.PanicOnError(err)
|
|
} else {
|
|
errors := responseError{
|
|
ID: "InvalidChars",
|
|
Msg: fmt.Sprintf("invalid format parameter: %s", format),
|
|
}
|
|
statusResponseError(writer, http.StatusBadRequest, errors)
|
|
return
|
|
}
|
|
}
|
|
|
|
func (api *API) blueprintsChangesHandler(writer http.ResponseWriter, request *http.Request, params httprouter.Params) {
|
|
if !verifyRequestVersion(writer, params, 0) {
|
|
return
|
|
}
|
|
|
|
type change struct {
|
|
Changes []blueprint.Change `json:"changes"`
|
|
Name string `json:"name"`
|
|
Total int `json:"total"`
|
|
}
|
|
|
|
type reply struct {
|
|
BlueprintsChanges []change `json:"blueprints"`
|
|
Errors []responseError `json:"errors"`
|
|
Limit uint `json:"limit"`
|
|
Offset uint `json:"offset"`
|
|
}
|
|
|
|
names := strings.Split(params.ByName("blueprints"), ",")
|
|
if names[0] == "/" {
|
|
errors := responseError{
|
|
Code: http.StatusNotFound,
|
|
ID: "HTTPError",
|
|
Msg: "Not Found",
|
|
}
|
|
statusResponseError(writer, http.StatusNotFound, errors)
|
|
return
|
|
}
|
|
|
|
// Remove the leading / from the first entry (check above ensures it is not just a /
|
|
names[0] = names[0][1:]
|
|
|
|
if !verifyStringsWithRegex(writer, names, ValidBlueprintName) {
|
|
return
|
|
}
|
|
|
|
offset, limit, err := parseOffsetAndLimit(request.URL.Query())
|
|
if err != nil {
|
|
errors := responseError{
|
|
ID: "BadLimitOrOffset",
|
|
Msg: fmt.Sprintf("BadRequest: %s", err.Error()),
|
|
}
|
|
statusResponseError(writer, http.StatusBadRequest, errors)
|
|
return
|
|
}
|
|
|
|
allChanges := []change{}
|
|
errors := []responseError{}
|
|
for _, name := range names {
|
|
bpChanges := api.store.GetBlueprintChanges(name)
|
|
// Reverse the changes, newest first
|
|
reversed := make([]blueprint.Change, 0, len(bpChanges))
|
|
for i := len(bpChanges) - 1; i >= 0; i-- {
|
|
reversed = append(reversed, bpChanges[i])
|
|
}
|
|
if bpChanges != nil {
|
|
change := change{
|
|
Changes: reversed,
|
|
Name: name,
|
|
Total: len(bpChanges),
|
|
}
|
|
allChanges = append(allChanges, change)
|
|
} else {
|
|
error := responseError{
|
|
ID: "UnknownBlueprint",
|
|
Msg: name,
|
|
}
|
|
errors = append(errors, error)
|
|
}
|
|
}
|
|
|
|
err = json.NewEncoder(writer).Encode(reply{
|
|
BlueprintsChanges: allChanges,
|
|
Errors: errors,
|
|
Offset: offset,
|
|
Limit: limit,
|
|
})
|
|
common.PanicOnError(err)
|
|
}
|
|
|
|
func (api *API) blueprintsNewHandler(writer http.ResponseWriter, request *http.Request, params httprouter.Params) {
|
|
if !verifyRequestVersion(writer, params, 0) {
|
|
return
|
|
}
|
|
|
|
contentType := request.Header["Content-Type"]
|
|
if len(contentType) == 0 {
|
|
errors := responseError{
|
|
ID: "BlueprintsError",
|
|
Msg: "missing Content-Type header",
|
|
}
|
|
statusResponseError(writer, http.StatusBadRequest, errors)
|
|
return
|
|
}
|
|
|
|
if request.ContentLength == 0 {
|
|
errors := responseError{
|
|
ID: "BlueprintsError",
|
|
Msg: "Missing blueprint",
|
|
}
|
|
statusResponseError(writer, http.StatusBadRequest, errors)
|
|
return
|
|
}
|
|
|
|
var blueprint blueprint.Blueprint
|
|
var err error
|
|
if contentType[0] == "application/json" {
|
|
err = json.NewDecoder(request.Body).Decode(&blueprint)
|
|
} else if contentType[0] == "text/x-toml" {
|
|
_, err = toml.NewDecoder(request.Body).Decode(&blueprint)
|
|
} else {
|
|
err = errors_package.New("blueprint must be in json or toml format")
|
|
}
|
|
|
|
if err != nil {
|
|
errors := responseError{
|
|
ID: "BlueprintsError",
|
|
Msg: "400 Bad Request: The browser (or proxy) sent a request that this server could not understand: " + err.Error(),
|
|
}
|
|
statusResponseError(writer, http.StatusBadRequest, errors)
|
|
return
|
|
}
|
|
|
|
if !verifyStringsWithRegex(writer, []string{blueprint.Name}, ValidBlueprintName) {
|
|
return
|
|
}
|
|
|
|
// Check the blueprint's distro to make sure it is valid
|
|
if len(blueprint.Distro) > 0 {
|
|
// NB: For backward compatibility, try to to standardize the distro name,
|
|
// because it may be missing a dot to separate major and minor verion.
|
|
// If it fails, just use the original name.
|
|
if distroStandardized, err := distroidparser.DefaultParser.Standardize(blueprint.Distro); err == nil {
|
|
blueprint.Distro = distroStandardized
|
|
}
|
|
|
|
arch := blueprint.Arch
|
|
if arch == "" {
|
|
arch = api.hostArch
|
|
}
|
|
if !common.IsStringInSortedSlice(api.validDistros(arch), blueprint.Distro) {
|
|
errors := responseError{
|
|
ID: "BlueprintsError",
|
|
Msg: fmt.Sprintf("'%s' is not a valid distribution (architecture '%s')", blueprint.Distro, arch),
|
|
}
|
|
statusResponseError(writer, http.StatusBadRequest, errors)
|
|
return
|
|
}
|
|
}
|
|
|
|
// Make sure the blueprint has default values and that the version is valid
|
|
err = blueprint.Initialize()
|
|
if err != nil {
|
|
errors := responseError{
|
|
ID: "BlueprintsError",
|
|
Msg: err.Error(),
|
|
}
|
|
statusResponseError(writer, http.StatusBadRequest, errors)
|
|
return
|
|
}
|
|
|
|
commitMsg := "Recipe " + blueprint.Name + ", version " + blueprint.Version + " saved."
|
|
err = api.store.PushBlueprint(blueprint, commitMsg)
|
|
if err != nil {
|
|
errors := responseError{
|
|
ID: "BlueprintsError",
|
|
Msg: err.Error(),
|
|
}
|
|
statusResponseError(writer, http.StatusBadRequest, errors)
|
|
return
|
|
}
|
|
|
|
statusResponseOK(writer)
|
|
}
|
|
|
|
func (api *API) blueprintsWorkspaceHandler(writer http.ResponseWriter, request *http.Request, params httprouter.Params) {
|
|
if !verifyRequestVersion(writer, params, 0) {
|
|
return
|
|
}
|
|
|
|
contentType := request.Header["Content-Type"]
|
|
if len(contentType) == 0 {
|
|
errors := responseError{
|
|
ID: "BlueprintsError",
|
|
Msg: "missing Content-Type header",
|
|
}
|
|
statusResponseError(writer, http.StatusBadRequest, errors)
|
|
return
|
|
}
|
|
|
|
if request.ContentLength == 0 {
|
|
errors := responseError{
|
|
ID: "BlueprintsError",
|
|
Msg: "Missing blueprint",
|
|
}
|
|
statusResponseError(writer, http.StatusBadRequest, errors)
|
|
return
|
|
}
|
|
|
|
var blueprint blueprint.Blueprint
|
|
var err error
|
|
if contentType[0] == "application/json" {
|
|
err = json.NewDecoder(request.Body).Decode(&blueprint)
|
|
} else if contentType[0] == "text/x-toml" {
|
|
_, err = toml.NewDecoder(request.Body).Decode(&blueprint)
|
|
} else {
|
|
err = errors_package.New("blueprint must be in json or toml format")
|
|
}
|
|
|
|
if err != nil {
|
|
errors := responseError{
|
|
ID: "BlueprintsError",
|
|
Msg: "400 Bad Request: The browser (or proxy) sent a request that this server could not understand: " + err.Error(),
|
|
}
|
|
statusResponseError(writer, http.StatusBadRequest, errors)
|
|
return
|
|
}
|
|
|
|
if !verifyStringsWithRegex(writer, []string{blueprint.Name}, ValidBlueprintName) {
|
|
return
|
|
}
|
|
|
|
err = api.store.PushBlueprintToWorkspace(blueprint)
|
|
if err != nil {
|
|
errors := responseError{
|
|
ID: "BlueprintsError",
|
|
Msg: err.Error(),
|
|
}
|
|
statusResponseError(writer, http.StatusBadRequest, errors)
|
|
return
|
|
}
|
|
|
|
statusResponseOK(writer)
|
|
}
|
|
|
|
func (api *API) blueprintUndoHandler(writer http.ResponseWriter, request *http.Request, params httprouter.Params) {
|
|
if !verifyRequestVersion(writer, params, 0) {
|
|
return
|
|
}
|
|
|
|
name := params.ByName("blueprint")
|
|
if !verifyStringsWithRegex(writer, []string{name}, ValidBlueprintName) {
|
|
return
|
|
}
|
|
|
|
commit := params.ByName("commit")
|
|
if !verifyStringsWithRegex(writer, []string{commit}, ValidBlueprintName) {
|
|
return
|
|
}
|
|
|
|
bpChange, err := api.store.GetBlueprintChange(name, commit)
|
|
if err != nil {
|
|
errors := responseError{
|
|
ID: "UnknownCommit",
|
|
Msg: err.Error(),
|
|
}
|
|
statusResponseError(writer, http.StatusBadRequest, errors)
|
|
return
|
|
}
|
|
|
|
bp := bpChange.Blueprint
|
|
if len(bpChange.Blueprint.Name) == 0 {
|
|
errors := responseError{
|
|
ID: "BlueprintsError",
|
|
Msg: fmt.Sprintf("no blueprint found for commit %s", commit),
|
|
}
|
|
statusResponseError(writer, http.StatusBadRequest, errors)
|
|
return
|
|
}
|
|
|
|
commitMsg := name + ".toml reverted to commit " + commit
|
|
err = api.store.PushBlueprint(bp, commitMsg)
|
|
if err != nil {
|
|
errors := responseError{
|
|
ID: "BlueprintsError",
|
|
Msg: err.Error(),
|
|
}
|
|
statusResponseError(writer, http.StatusBadRequest, errors)
|
|
return
|
|
}
|
|
statusResponseOK(writer)
|
|
}
|
|
|
|
func (api *API) blueprintDeleteHandler(writer http.ResponseWriter, request *http.Request, params httprouter.Params) {
|
|
if !verifyRequestVersion(writer, params, 0) {
|
|
return
|
|
}
|
|
|
|
name := params.ByName("blueprint")
|
|
if !verifyStringsWithRegex(writer, []string{name}, ValidBlueprintName) {
|
|
return
|
|
}
|
|
|
|
if err := api.store.DeleteBlueprint(name); err != nil {
|
|
errors := responseError{
|
|
ID: "BlueprintsError",
|
|
Msg: err.Error(),
|
|
}
|
|
statusResponseError(writer, http.StatusBadRequest, errors)
|
|
return
|
|
}
|
|
statusResponseOK(writer)
|
|
}
|
|
|
|
func (api *API) blueprintDeleteWorkspaceHandler(writer http.ResponseWriter, request *http.Request, params httprouter.Params) {
|
|
if !verifyRequestVersion(writer, params, 0) {
|
|
return
|
|
}
|
|
|
|
name := params.ByName("blueprint")
|
|
if !verifyStringsWithRegex(writer, []string{name}, ValidBlueprintName) {
|
|
return
|
|
}
|
|
|
|
if err := api.store.DeleteBlueprintFromWorkspace(name); err != nil {
|
|
errors := responseError{
|
|
ID: "BlueprintsError",
|
|
Msg: err.Error(),
|
|
}
|
|
statusResponseError(writer, http.StatusBadRequest, errors)
|
|
return
|
|
}
|
|
statusResponseOK(writer)
|
|
}
|
|
|
|
// blueprintsTagHandler tags the current blueprint commit as a new revision
|
|
func (api *API) blueprintsTagHandler(writer http.ResponseWriter, request *http.Request, params httprouter.Params) {
|
|
if !verifyRequestVersion(writer, params, 0) {
|
|
return
|
|
}
|
|
|
|
name := params.ByName("blueprint")
|
|
if !verifyStringsWithRegex(writer, []string{name}, ValidBlueprintName) {
|
|
return
|
|
}
|
|
|
|
err := api.store.TagBlueprint(name)
|
|
if err != nil {
|
|
errors := responseError{
|
|
ID: "BlueprintsError",
|
|
Msg: err.Error(),
|
|
}
|
|
statusResponseError(writer, http.StatusBadRequest, errors)
|
|
return
|
|
}
|
|
statusResponseOK(writer)
|
|
}
|
|
|
|
// depsolve handles depsolving package sets required for serializing a manifest for a given distribution.
|
|
//
|
|
// Distro name is not determined from the provided architecture, but has to be provided explicitly.
|
|
// The reason is that a distro name alias may have been used to get the distro object as well as the
|
|
// repositories for the depsolving. The actual distro object name may not correspond to the alias.
|
|
// Since the solver uses the distro name to namespace cache, it is important to use the same distro
|
|
// name as the one used to get the repositories.
|
|
func (api *API) depsolve(packageSets map[string][]rpmmd.PackageSet, distroName string, arch distro.Arch) (map[string]dnfjson.DepsolveResult, error) {
|
|
|
|
distro := arch.Distro()
|
|
platformID := distro.ModulePlatformID()
|
|
releasever := distro.Releasever()
|
|
solver := api.solver.NewWithConfig(platformID, releasever, arch.Name(), distroName)
|
|
|
|
depsolvedSets := make(map[string]dnfjson.DepsolveResult, len(packageSets))
|
|
|
|
for name, pkgSet := range packageSets {
|
|
res, err := solver.Depsolve(pkgSet, sbom.StandardTypeNone)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
depsolvedSets[name] = *res
|
|
}
|
|
if err := solver.CleanCache(); err != nil {
|
|
// log and ignore
|
|
log.Printf("Error during rpm repo cache cleanup: %s", err.Error())
|
|
}
|
|
return depsolvedSets, nil
|
|
}
|
|
|
|
func (api *API) resolveContainers(sourceSpecs map[string][]container.SourceSpec, archName string) (map[string][]container.Spec, error) {
|
|
|
|
specs := make(map[string][]container.Spec, len(sourceSpecs))
|
|
|
|
// shortcut
|
|
if len(sourceSpecs) == 0 {
|
|
return specs, nil
|
|
}
|
|
|
|
// Run one job for each value in the sourceSpecs in order.
|
|
// Currently this should still only be one job, but if containers are added
|
|
// to multiple pipelines at any point, this should work.
|
|
for name, sources := range sourceSpecs {
|
|
job := worker.ContainerResolveJob{
|
|
Arch: archName,
|
|
Specs: make([]worker.ContainerSpec, len(sources)),
|
|
}
|
|
|
|
for i, c := range sources {
|
|
job.Specs[i] = worker.ContainerSpec{
|
|
Source: c.Source,
|
|
Name: c.Name,
|
|
TLSVerify: c.TLSVerify,
|
|
}
|
|
}
|
|
|
|
jobId, err := api.workers.EnqueueContainerResolveJob(&job, "")
|
|
|
|
if err != nil {
|
|
return specs, err
|
|
}
|
|
|
|
var result worker.ContainerResolveJobResult
|
|
|
|
for {
|
|
jobInfo, err := api.workers.ContainerResolveJobInfo(jobId, &result)
|
|
|
|
if err != nil {
|
|
return specs, err
|
|
}
|
|
|
|
if result.JobError != nil {
|
|
return specs, errors.New(result.JobError.Reason)
|
|
} else if jobInfo.JobStatus.Canceled {
|
|
return specs, fmt.Errorf("Failed to resolve containers: job cancelled")
|
|
} else if !jobInfo.JobStatus.Finished.IsZero() {
|
|
break
|
|
}
|
|
|
|
time.Sleep(time.Millisecond * 250)
|
|
}
|
|
|
|
if len(result.Specs) != len(sources) {
|
|
panic("programming error: input / output length don't match")
|
|
}
|
|
|
|
specs[name] = make([]container.Spec, len(sources))
|
|
for i, s := range result.Specs {
|
|
specs[name][i].Source = s.Source
|
|
specs[name][i].Digest = s.Digest
|
|
specs[name][i].LocalName = s.Name
|
|
specs[name][i].TLSVerify = s.TLSVerify
|
|
specs[name][i].ImageID = s.ImageID
|
|
specs[name][i].ListDigest = s.ListDigest
|
|
}
|
|
}
|
|
|
|
return specs, nil
|
|
}
|
|
|
|
func (api *API) resolveOSTreeCommits(sourceSpecs map[string][]ostree.SourceSpec, test bool) (map[string][]ostree.CommitSpec, error) {
|
|
commitSpecs := make(map[string][]ostree.CommitSpec, len(sourceSpecs))
|
|
for name, sources := range sourceSpecs {
|
|
commits := make([]ostree.CommitSpec, len(sources))
|
|
for idx, source := range sources {
|
|
if test {
|
|
checksum := fmt.Sprintf("%x", sha256.Sum256([]byte(source.URL+source.Ref)))
|
|
commits[idx] = ostree.CommitSpec{
|
|
Ref: source.Ref,
|
|
URL: source.URL,
|
|
Checksum: checksum,
|
|
}
|
|
} else {
|
|
// MTLS not supported on-prem
|
|
commit, err := ostree.Resolve(source)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
commits[idx] = commit
|
|
}
|
|
}
|
|
commitSpecs[name] = commits
|
|
}
|
|
return commitSpecs, nil
|
|
}
|
|
|
|
// Schedule new compose by first translating the appropriate blueprint into a pipeline and then
|
|
// pushing it into the channel for waiting builds.
|
|
func (api *API) composeHandler(writer http.ResponseWriter, request *http.Request, params httprouter.Params) {
|
|
if !verifyRequestVersion(writer, params, 0) {
|
|
return
|
|
}
|
|
|
|
// https://weldr.io/lorax/pylorax.api.html#pylorax.api.v0.v0_compose_start
|
|
type ComposeRequest struct {
|
|
BlueprintName string `json:"blueprint_name"`
|
|
ComposeType string `json:"compose_type"`
|
|
Size uint64 `json:"size"`
|
|
OSTree *ostree.ImageOptions `json:"ostree,omitempty"`
|
|
Branch string `json:"branch"`
|
|
Upload *uploadRequest `json:"upload"`
|
|
}
|
|
type ComposeReply struct {
|
|
BuildID uuid.UUID `json:"build_id"`
|
|
Status bool `json:"status"`
|
|
Warnings []string `json:"warnings"`
|
|
}
|
|
|
|
contentType := request.Header["Content-Type"]
|
|
if len(contentType) != 1 || contentType[0] != "application/json" {
|
|
errors := responseError{
|
|
ID: "MissingPost",
|
|
Msg: "blueprint must be json",
|
|
}
|
|
statusResponseError(writer, http.StatusBadRequest, errors)
|
|
return
|
|
}
|
|
|
|
var cr ComposeRequest
|
|
err := json.NewDecoder(request.Body).Decode(&cr)
|
|
if err != nil {
|
|
errors := responseError{
|
|
Code: http.StatusNotFound,
|
|
ID: "HTTPError",
|
|
Msg: "Not Found",
|
|
}
|
|
statusResponseError(writer, http.StatusNotFound, errors)
|
|
return
|
|
}
|
|
|
|
if !verifyStringsWithRegex(writer, []string{cr.BlueprintName}, ValidBlueprintName) {
|
|
return
|
|
}
|
|
|
|
bp := api.store.GetBlueprintCommitted(cr.BlueprintName)
|
|
if bp == nil {
|
|
errors := responseError{
|
|
ID: "UnknownBlueprint",
|
|
Msg: fmt.Sprintf("Unknown blueprint name: %s", cr.BlueprintName),
|
|
}
|
|
statusResponseError(writer, http.StatusBadRequest, errors)
|
|
return
|
|
}
|
|
|
|
distroName := bp.Distro
|
|
if distroName == "" {
|
|
distroName = api.hostDistroName
|
|
}
|
|
|
|
archName := bp.Arch
|
|
if archName == "" {
|
|
archName = api.hostArch
|
|
}
|
|
|
|
if api.getDistro(distroName, archName) == nil {
|
|
errors := responseError{
|
|
ID: "DistroError",
|
|
Msg: fmt.Sprintf("Unknown distribution: %s for arch %s", distroName, archName),
|
|
}
|
|
statusResponseError(writer, http.StatusBadRequest, errors)
|
|
return
|
|
}
|
|
|
|
// Get the imageType that corresponds to the distribution selected by the blueprint
|
|
imageType, err := api.getImageType(distroName, cr.ComposeType, archName)
|
|
if err != nil {
|
|
errors := responseError{
|
|
ID: "ComposeError",
|
|
Msg: fmt.Sprintf("Failed to get compose type %q: %v", cr.ComposeType, err),
|
|
}
|
|
statusResponseError(writer, http.StatusBadRequest, errors)
|
|
return
|
|
}
|
|
|
|
composeID := uuid.New()
|
|
|
|
var targets []*target.Target
|
|
// Always instruct the worker to upload the artifact back to the server
|
|
workerServerTarget := target.NewWorkerServerTarget()
|
|
workerServerTarget.ImageName = imageType.Filename()
|
|
workerServerTarget.OsbuildArtifact.ExportFilename = imageType.Filename()
|
|
workerServerTarget.OsbuildArtifact.ExportName = imageType.Exports()[0]
|
|
targets = append(targets, workerServerTarget)
|
|
if isRequestVersionAtLeast(params, 1) && cr.Upload != nil {
|
|
t := uploadRequestToTarget(*cr.Upload, imageType)
|
|
targets = append(targets, t)
|
|
}
|
|
|
|
// Check for test parameter
|
|
q, err := url.ParseQuery(request.URL.RawQuery)
|
|
if err != nil {
|
|
errors := responseError{
|
|
ID: "InvalidChars",
|
|
Msg: fmt.Sprintf("invalid query string: %v", err),
|
|
}
|
|
statusResponseError(writer, http.StatusBadRequest, errors)
|
|
return
|
|
}
|
|
|
|
var size uint64
|
|
|
|
// check if filesytem customizations have been set.
|
|
// if compose size parameter is set, take the larger of
|
|
// the two values
|
|
if minSize := bp.Customizations.GetFilesystemsMinSize(); bp.Customizations != nil && minSize > 0 && minSize > cr.Size {
|
|
size = imageType.Size(minSize)
|
|
} else {
|
|
size = imageType.Size(cr.Size)
|
|
}
|
|
|
|
bigSeed, err := rand.Int(rand.Reader, big.NewInt(math.MaxInt64))
|
|
if err != nil {
|
|
panic("cannot generate a manifest seed: " + err.Error())
|
|
}
|
|
seed := bigSeed.Int64()
|
|
|
|
// Get the partitioning mode
|
|
pm, err := bp.Customizations.GetPartitioningMode()
|
|
if err != nil {
|
|
errors := responseError{
|
|
ID: "BlueprintsError",
|
|
Msg: err.Error(),
|
|
}
|
|
statusResponseError(writer, http.StatusBadRequest, errors)
|
|
return
|
|
}
|
|
|
|
options := distro.ImageOptions{
|
|
Size: size,
|
|
OSTree: cr.OSTree,
|
|
PartitioningMode: pm,
|
|
}
|
|
options.Facts = &facts.ImageOptions{
|
|
APIType: facts.WELDR_APITYPE,
|
|
}
|
|
|
|
imageRepos, err := api.allRepositoriesByImageType(distroName, imageType)
|
|
if err != nil {
|
|
errors := responseError{
|
|
ID: "InternalError",
|
|
Msg: err.Error(),
|
|
}
|
|
statusResponseError(writer, http.StatusInternalServerError, errors)
|
|
return
|
|
}
|
|
|
|
ibp := blueprint.Convert(*bp)
|
|
manifest, warnings, err := imageType.Manifest(&ibp, options, imageRepos, &seed)
|
|
if err != nil {
|
|
errors := responseError{
|
|
ID: "ManifestCreationFailed",
|
|
Msg: fmt.Sprintf("failed to initialize osbuild manifest: %v", err),
|
|
}
|
|
statusResponseError(writer, http.StatusBadRequest, errors)
|
|
return
|
|
}
|
|
|
|
depsolved, err := api.depsolve(manifest.GetPackageSetChains(), distroName, imageType.Arch())
|
|
if err != nil {
|
|
errors := responseError{
|
|
ID: "DepsolveError",
|
|
Msg: err.Error(),
|
|
}
|
|
statusResponseError(writer, http.StatusInternalServerError, errors)
|
|
return
|
|
}
|
|
|
|
containerSpecs, err := api.resolveContainers(manifest.GetContainerSourceSpecs(), archName)
|
|
if err != nil {
|
|
errors := responseError{
|
|
ID: "ContainerResolveError",
|
|
Msg: err.Error(),
|
|
}
|
|
statusResponseError(writer, http.StatusInternalServerError, errors)
|
|
return
|
|
}
|
|
|
|
testMode := q.Get("test")
|
|
|
|
ostreeCommitSpecs, err := api.resolveOSTreeCommits(manifest.GetOSTreeSourceSpecs(), testMode == "1" || testMode == "2")
|
|
if err != nil {
|
|
errors := responseError{
|
|
ID: "OSTreeOptionsError",
|
|
Msg: err.Error(),
|
|
}
|
|
statusResponseError(writer, http.StatusBadRequest, errors)
|
|
return
|
|
}
|
|
|
|
mf, err := manifest.Serialize(depsolved, containerSpecs, ostreeCommitSpecs, nil)
|
|
if err != nil {
|
|
errors := responseError{
|
|
ID: "ManifestCreationFailed",
|
|
Msg: fmt.Sprintf("failed to serialize osbuild manifest: %v", err),
|
|
}
|
|
statusResponseError(writer, http.StatusBadRequest, errors)
|
|
return
|
|
}
|
|
|
|
var packages []rpmmd.PackageSpec
|
|
// TODO: introduce a way to query these from the manifest / image type
|
|
// BUG: installer/container image types will have empty package sets
|
|
if packages = depsolved["packages"].Packages; len(packages) == 0 {
|
|
if packages = depsolved["os"].Packages; len(packages) == 0 {
|
|
packages = depsolved["ostree-tree"].Packages
|
|
}
|
|
}
|
|
|
|
workerAvailable, err := api.workers.WorkerAvailableForArch(archName)
|
|
if err != nil {
|
|
log.Println("error when pushing new compose: ", err.Error())
|
|
errors := responseError{
|
|
ID: "ComposePushErrored",
|
|
Msg: err.Error(),
|
|
}
|
|
statusResponseError(writer, http.StatusInternalServerError, errors)
|
|
return
|
|
}
|
|
if !workerAvailable {
|
|
errors := responseError{
|
|
ID: "ComposePushErrored",
|
|
Msg: fmt.Sprintf("No worker for arch '%s' available", archName),
|
|
}
|
|
statusResponseError(writer, http.StatusBadRequest, errors)
|
|
return
|
|
}
|
|
|
|
if testMode == "1" {
|
|
// Create a failed compose
|
|
err = api.store.PushTestCompose(composeID, mf, imageType, bp, size, targets, false, packages)
|
|
} else if testMode == "2" {
|
|
// Create a successful compose
|
|
err = api.store.PushTestCompose(composeID, mf, imageType, bp, size, targets, true, packages)
|
|
} else {
|
|
var jobId uuid.UUID
|
|
jobId, err = api.workers.EnqueueOSBuild(archName, &worker.OSBuildJob{
|
|
Manifest: mf,
|
|
Targets: targets,
|
|
PipelineNames: &worker.PipelineNames{
|
|
Build: imageType.BuildPipelines(),
|
|
Payload: imageType.PayloadPipelines(),
|
|
},
|
|
ImageBootMode: imageType.BootMode().String(),
|
|
}, "")
|
|
if err == nil {
|
|
err = api.store.PushCompose(composeID, mf, imageType, bp, size, targets, jobId, packages)
|
|
}
|
|
}
|
|
|
|
// TODO: we should probably do some kind of blueprint validation in future
|
|
// for now, let's just 500 and bail out
|
|
if err != nil {
|
|
log.Println("error when pushing new compose: ", err.Error())
|
|
errors := responseError{
|
|
ID: "ComposePushErrored",
|
|
Msg: err.Error(),
|
|
}
|
|
statusResponseError(writer, http.StatusInternalServerError, errors)
|
|
return
|
|
}
|
|
|
|
err = json.NewEncoder(writer).Encode(ComposeReply{
|
|
BuildID: composeID,
|
|
Status: true,
|
|
Warnings: warnings,
|
|
})
|
|
common.PanicOnError(err)
|
|
}
|
|
|
|
func (api *API) composeDeleteHandler(writer http.ResponseWriter, request *http.Request, params httprouter.Params) {
|
|
if !verifyRequestVersion(writer, params, 0) {
|
|
return
|
|
}
|
|
|
|
type composeDeleteStatus struct {
|
|
UUID uuid.UUID `json:"uuid"`
|
|
Status bool `json:"status"`
|
|
}
|
|
|
|
type composeDeleteError struct {
|
|
ID string `json:"id"`
|
|
Msg string `json:"msg"`
|
|
}
|
|
|
|
uuidsParam := params.ByName("uuids")
|
|
|
|
results := []composeDeleteStatus{}
|
|
errors := []composeDeleteError{}
|
|
uuidStrings := strings.Split(uuidsParam, ",")
|
|
for _, uuidString := range uuidStrings {
|
|
id, err := uuid.Parse(uuidString)
|
|
if err != nil {
|
|
errors = append(errors, composeDeleteError{
|
|
"UnknownUUID",
|
|
fmt.Sprintf("%s is not a valid uuid", uuidString),
|
|
})
|
|
continue
|
|
}
|
|
|
|
compose, exists := api.store.GetCompose(id)
|
|
if !exists {
|
|
errors = append(errors, composeDeleteError{
|
|
"UnknownUUID",
|
|
fmt.Sprintf("compose %s doesn't exist", id),
|
|
})
|
|
continue
|
|
}
|
|
|
|
composeStatus, err := api.getComposeStatus(compose)
|
|
if err != nil {
|
|
errors = append(errors, composeDeleteError{
|
|
"ComposeStatusError",
|
|
fmt.Sprintf("Error getting status of compose %s: %s", id, err),
|
|
})
|
|
continue
|
|
}
|
|
if composeStatus.State != ComposeFinished && composeStatus.State != ComposeFailed {
|
|
errors = append(errors, composeDeleteError{
|
|
"BuildInWrongState",
|
|
fmt.Sprintf("Compose %s is not in FINISHED or FAILED.", id),
|
|
})
|
|
continue
|
|
}
|
|
|
|
err = api.store.DeleteCompose(id)
|
|
if err != nil {
|
|
errors = append(errors, composeDeleteError{
|
|
"ComposeError",
|
|
fmt.Sprintf("%s: %s", id, err.Error()),
|
|
})
|
|
continue
|
|
}
|
|
|
|
// Delete artifacts from the worker server or — if that doesn't
|
|
// have this job — the compat output dir. Ignore errors,
|
|
// because there's no point of reporting them to the client
|
|
// after the compose itself has already been deleted.
|
|
err = api.workers.DeleteArtifacts(compose.ImageBuild.JobID)
|
|
if err == jobqueue.ErrNotExist && api.compatOutputDir != "" {
|
|
_ = os.RemoveAll(path.Join(api.compatOutputDir, id.String()))
|
|
}
|
|
|
|
results = append(results, composeDeleteStatus{id, true})
|
|
}
|
|
|
|
reply := struct {
|
|
UUIDs []composeDeleteStatus `json:"uuids"`
|
|
Errors []composeDeleteError `json:"errors"`
|
|
}{results, errors}
|
|
|
|
err := json.NewEncoder(writer).Encode(reply)
|
|
common.PanicOnError(err)
|
|
}
|
|
|
|
func (api *API) composeCancelHandler(writer http.ResponseWriter, request *http.Request, params httprouter.Params) {
|
|
if !verifyRequestVersion(writer, params, 0) {
|
|
return
|
|
}
|
|
|
|
uuidString := params.ByName("uuid")
|
|
id, err := uuid.Parse(uuidString)
|
|
if err != nil {
|
|
errors := responseError{
|
|
ID: "UnknownUUID",
|
|
Msg: fmt.Sprintf("%s is not a valid build uuid", uuidString),
|
|
}
|
|
statusResponseError(writer, http.StatusBadRequest, errors)
|
|
return
|
|
}
|
|
|
|
compose, exists := api.store.GetCompose(id)
|
|
if !exists {
|
|
errors := responseError{
|
|
ID: "UnknownUUID",
|
|
Msg: fmt.Sprintf("Compose %s doesn't exist", uuidString),
|
|
}
|
|
statusResponseError(writer, http.StatusBadRequest, errors)
|
|
return
|
|
}
|
|
|
|
composeStatus, err := api.getComposeStatus(compose)
|
|
if err != nil {
|
|
errors := responseError{
|
|
ID: "ComposeStatusError",
|
|
Msg: fmt.Sprintf("Error getting status of compose %s: %s", id, err),
|
|
}
|
|
statusResponseError(writer, http.StatusInternalServerError, errors)
|
|
return
|
|
}
|
|
if composeStatus.State != ComposeWaiting && composeStatus.State != ComposeRunning {
|
|
errors := responseError{
|
|
ID: "BuildInWrongState",
|
|
Msg: fmt.Sprintf("Build %s is not in WAITING or RUNNING.", uuidString),
|
|
}
|
|
statusResponseError(writer, http.StatusBadRequest, errors)
|
|
return
|
|
}
|
|
|
|
err = api.workers.Cancel(compose.ImageBuild.JobID)
|
|
if err != nil {
|
|
errors := responseError{
|
|
ID: "InternalServerError",
|
|
Msg: fmt.Sprintf("Internal server error: %v", err),
|
|
}
|
|
statusResponseError(writer, http.StatusBadRequest, errors)
|
|
return
|
|
}
|
|
|
|
reply := CancelComposeStatusV0{id, true}
|
|
_ = json.NewEncoder(writer).Encode(reply)
|
|
}
|
|
|
|
func (api *API) composeTypesHandler(writer http.ResponseWriter, request *http.Request, params httprouter.Params) {
|
|
if !verifyRequestVersion(writer, params, 0) {
|
|
return
|
|
}
|
|
|
|
archName := api.hostArch
|
|
if queryArch := request.URL.Query().Get("arch"); queryArch != "" {
|
|
archName = queryArch
|
|
}
|
|
|
|
// Optional distro parameter
|
|
// If it is empty it will return api.hostDistroName
|
|
distroName, err := api.parseDistro(request.URL.Query(), archName)
|
|
if err != nil {
|
|
errors := responseError{
|
|
ID: "DistroError",
|
|
Msg: err.Error(),
|
|
}
|
|
statusResponseError(writer, http.StatusBadRequest, errors)
|
|
return
|
|
}
|
|
|
|
d := api.getDistro(distroName, archName)
|
|
if d == nil {
|
|
errors := responseError{
|
|
ID: "DistroError",
|
|
Msg: fmt.Sprintf("Unknown distribution: %s", distroName),
|
|
}
|
|
statusResponseError(writer, http.StatusBadRequest, errors)
|
|
return
|
|
}
|
|
|
|
// Get the distro specific arch so that we can return the correct list of image types
|
|
arch, err := d.GetArch(archName)
|
|
if err != nil {
|
|
errors := responseError{
|
|
ID: "DistroError",
|
|
Msg: fmt.Sprintf("Unknown arch: %s", arch),
|
|
}
|
|
statusResponseError(writer, http.StatusBadRequest, errors)
|
|
return
|
|
}
|
|
|
|
type composeType struct {
|
|
Name string `json:"name"`
|
|
Enabled bool `json:"enabled"`
|
|
}
|
|
|
|
var reply struct {
|
|
Types []composeType `json:"types"`
|
|
}
|
|
|
|
for _, format := range arch.ListImageTypes() {
|
|
imgAllowed, err := api.isImageTypeAllowed(distroName, format)
|
|
if err != nil {
|
|
errors := responseError{
|
|
ID: "InternalError",
|
|
Msg: fmt.Sprintf("Error while checking if image type is allowed: %v", err),
|
|
}
|
|
statusResponseError(writer, http.StatusInternalServerError, errors)
|
|
return
|
|
}
|
|
if !imgAllowed {
|
|
continue
|
|
}
|
|
reply.Types = append(reply.Types, composeType{format, true})
|
|
}
|
|
|
|
err = json.NewEncoder(writer).Encode(reply)
|
|
common.PanicOnError(err)
|
|
}
|
|
|
|
func (api *API) composeQueueHandler(writer http.ResponseWriter, request *http.Request, params httprouter.Params) {
|
|
if !verifyRequestVersion(writer, params, 0) {
|
|
return
|
|
}
|
|
|
|
reply := struct {
|
|
New []*ComposeEntry `json:"new"`
|
|
Run []*ComposeEntry `json:"run"`
|
|
}{[]*ComposeEntry{}, []*ComposeEntry{}}
|
|
|
|
includeUploads := isRequestVersionAtLeast(params, 1)
|
|
|
|
composes := api.store.GetAllComposes()
|
|
for id, compose := range composes {
|
|
composeStatus, err := api.getComposeStatus(compose)
|
|
if err != nil {
|
|
log.Printf("Error getting status of compose %s: %s", id, err)
|
|
continue
|
|
}
|
|
switch composeStatus.State {
|
|
case ComposeWaiting:
|
|
reply.New = append(reply.New, composeToComposeEntry(id, compose, composeStatus, includeUploads))
|
|
case ComposeRunning:
|
|
reply.Run = append(reply.Run, composeToComposeEntry(id, compose, composeStatus, includeUploads))
|
|
}
|
|
}
|
|
|
|
err := json.NewEncoder(writer).Encode(reply)
|
|
common.PanicOnError(err)
|
|
}
|
|
|
|
func (api *API) composeStatusHandler(writer http.ResponseWriter, request *http.Request, params httprouter.Params) {
|
|
// TODO: lorax has some params: /api/v0/compose/status/<uuids>[?blueprint=<blueprint_name>&status=<compose_status>&type=<compose_type>]
|
|
if !verifyRequestVersion(writer, params, 0) {
|
|
return
|
|
}
|
|
|
|
var reply struct {
|
|
UUIDs []*ComposeEntry `json:"uuids"`
|
|
}
|
|
|
|
uuidsParam := params.ByName("uuids")
|
|
|
|
composes := api.store.GetAllComposes()
|
|
uuids := []uuid.UUID{}
|
|
|
|
if uuidsParam != "*" {
|
|
for _, uuidString := range strings.Split(uuidsParam, ",") {
|
|
id, err := uuid.Parse(uuidString)
|
|
if err != nil {
|
|
errors := responseError{
|
|
ID: "UnknownUUID",
|
|
Msg: fmt.Sprintf("%s is not a valid build uuid", uuidString),
|
|
}
|
|
statusResponseError(writer, http.StatusBadRequest, errors)
|
|
return
|
|
}
|
|
uuids = append(uuids, id)
|
|
}
|
|
} else {
|
|
for id := range composes {
|
|
uuids = append(uuids, id)
|
|
}
|
|
}
|
|
|
|
q, err := url.ParseQuery(request.URL.RawQuery)
|
|
if err != nil {
|
|
errors := responseError{
|
|
ID: "InvalidChars",
|
|
Msg: fmt.Sprintf("invalid query string: %v", err),
|
|
}
|
|
statusResponseError(writer, http.StatusBadRequest, errors)
|
|
return
|
|
}
|
|
|
|
filterBlueprint := q.Get("blueprint")
|
|
if len(filterBlueprint) > 0 && !verifyStringsWithRegex(writer, []string{filterBlueprint}, ValidBlueprintName) {
|
|
return
|
|
}
|
|
|
|
filterStatus := q.Get("status")
|
|
filterImageType := q.Get("type")
|
|
|
|
filteredUUIDs := []uuid.UUID{}
|
|
for _, id := range uuids {
|
|
compose, exists := composes[id]
|
|
if !exists {
|
|
continue
|
|
}
|
|
composeStatus, err := api.getComposeStatus(compose)
|
|
if err != nil {
|
|
log.Printf("Error getting status of compose %s: %s", id, err)
|
|
continue
|
|
}
|
|
|
|
if filterBlueprint != "" && compose.Blueprint.Name != filterBlueprint {
|
|
continue
|
|
} else if filterStatus != "" && composeStatus.State.ToString() != filterStatus {
|
|
continue
|
|
} else if filterImageType != "" && compose.ImageBuild.ImageType.Name() != filterImageType {
|
|
continue
|
|
}
|
|
filteredUUIDs = append(filteredUUIDs, id)
|
|
}
|
|
|
|
reply.UUIDs = []*ComposeEntry{}
|
|
includeUploads := isRequestVersionAtLeast(params, 1)
|
|
for _, id := range filteredUUIDs {
|
|
if compose, exists := composes[id]; exists {
|
|
composeStatus, err := api.getComposeStatus(compose)
|
|
if err != nil {
|
|
log.Printf("Error getting status of compose %s: %s", id, err)
|
|
continue
|
|
}
|
|
|
|
reply.UUIDs = append(reply.UUIDs, composeToComposeEntry(id, compose, composeStatus, includeUploads))
|
|
}
|
|
}
|
|
sortComposeEntries(reply.UUIDs)
|
|
|
|
err = json.NewEncoder(writer).Encode(reply)
|
|
common.PanicOnError(err)
|
|
}
|
|
|
|
func (api *API) composeInfoHandler(writer http.ResponseWriter, request *http.Request, params httprouter.Params) {
|
|
if !verifyRequestVersion(writer, params, 0) {
|
|
return
|
|
}
|
|
|
|
uuidString := params.ByName("uuid")
|
|
id, err := uuid.Parse(uuidString)
|
|
if err != nil {
|
|
errors := responseError{
|
|
ID: "UnknownUUID",
|
|
Msg: fmt.Sprintf("%s is not a valid build uuid", uuidString),
|
|
}
|
|
statusResponseError(writer, http.StatusBadRequest, errors)
|
|
return
|
|
}
|
|
|
|
compose, exists := api.store.GetCompose(id)
|
|
|
|
if !exists {
|
|
errors := responseError{
|
|
ID: "UnknownUUID",
|
|
Msg: fmt.Sprintf("%s is not a valid build uuid", uuidString),
|
|
}
|
|
statusResponseError(writer, http.StatusBadRequest, errors)
|
|
return
|
|
}
|
|
|
|
var reply struct {
|
|
ID uuid.UUID `json:"id"`
|
|
Config string `json:"config"` // anaconda config, let's ignore this field
|
|
Blueprint *blueprint.Blueprint `json:"blueprint"` // blueprint not frozen!
|
|
Commit string `json:"commit"` // empty for now
|
|
Deps struct {
|
|
Packages []rpmmd.PackageSpec `json:"packages"`
|
|
} `json:"deps"`
|
|
ComposeType string `json:"compose_type"`
|
|
QueueStatus string `json:"queue_status"`
|
|
ImageSize uint64 `json:"image_size"`
|
|
Uploads []uploadResponse `json:"uploads,omitempty"`
|
|
}
|
|
|
|
reply.ID = id
|
|
reply.Blueprint = compose.Blueprint
|
|
// Weldr API assumes only one image build per compose, that's why only the
|
|
// 1st build is considered
|
|
composeStatus, err := api.getComposeStatus(compose)
|
|
if err != nil {
|
|
errors := responseError{
|
|
ID: "ComposeStatusError",
|
|
Msg: fmt.Sprintf("Error getting status of compose %s: %s", id, err),
|
|
}
|
|
statusResponseError(writer, http.StatusInternalServerError, errors)
|
|
return
|
|
}
|
|
|
|
reply.ComposeType = compose.ImageBuild.ImageType.Name()
|
|
reply.QueueStatus = composeStatus.State.ToString()
|
|
reply.ImageSize = compose.ImageBuild.Size
|
|
|
|
if isRequestVersionAtLeast(params, 1) {
|
|
reply.Uploads = targetsToUploadResponses(compose.ImageBuild.Targets, composeStatus.State)
|
|
}
|
|
|
|
// Add package dependencies from the compose
|
|
// This information may not be included for the compose
|
|
dependencies := compose.Packages
|
|
// Sort dependencies by Name (names should be unique so no need to sort by EVRA)
|
|
sort.Slice(dependencies, func(i, j int) bool {
|
|
return dependencies[i].Name < dependencies[j].Name
|
|
})
|
|
reply.Deps.Packages = dependencies
|
|
err = json.NewEncoder(writer).Encode(reply)
|
|
common.PanicOnError(err)
|
|
}
|
|
|
|
func (api *API) composeImageHandler(writer http.ResponseWriter, request *http.Request, params httprouter.Params) {
|
|
if !verifyRequestVersion(writer, params, 0) {
|
|
return
|
|
}
|
|
|
|
uuidString := params.ByName("uuid")
|
|
uuid, err := uuid.Parse(uuidString)
|
|
if err != nil {
|
|
errors := responseError{
|
|
ID: "UnknownUUID",
|
|
Msg: fmt.Sprintf("%s is not a valid build uuid", uuidString),
|
|
}
|
|
statusResponseError(writer, http.StatusBadRequest, errors)
|
|
return
|
|
}
|
|
|
|
compose, exists := api.store.GetCompose(uuid)
|
|
if !exists {
|
|
errors := responseError{
|
|
ID: "UnknownUUID",
|
|
Msg: fmt.Sprintf("Compose %s doesn't exist", uuidString),
|
|
}
|
|
statusResponseError(writer, http.StatusBadRequest, errors)
|
|
return
|
|
}
|
|
|
|
composeStatus, err := api.getComposeStatus(compose)
|
|
if err != nil {
|
|
errors := responseError{
|
|
ID: "ComposeStatusError",
|
|
Msg: fmt.Sprintf("Error getting status of compose %s: %s", uuid, err),
|
|
}
|
|
statusResponseError(writer, http.StatusInternalServerError, errors)
|
|
return
|
|
}
|
|
if composeStatus.State != ComposeFinished {
|
|
errors := responseError{
|
|
ID: "BuildInWrongState",
|
|
Msg: fmt.Sprintf("Build %s is in wrong state: %s", uuidString, composeStatus.State.ToString()),
|
|
}
|
|
statusResponseError(writer, http.StatusBadRequest, errors)
|
|
return
|
|
}
|
|
|
|
imageName := compose.ImageBuild.ImageType.Filename()
|
|
imageMime := compose.ImageBuild.ImageType.MIMEType()
|
|
|
|
reader, fileSize, err := api.openImageFile(uuid, compose)
|
|
if err != nil {
|
|
errors := responseError{
|
|
ID: "InternalServerError",
|
|
Msg: fmt.Sprintf("Error accessing image file for compose %s: %v", uuid, err),
|
|
}
|
|
statusResponseError(writer, http.StatusBadRequest, errors)
|
|
return
|
|
}
|
|
|
|
writer.Header().Set("Content-Disposition", "attachment; filename="+uuid.String()+"-"+imageName)
|
|
writer.Header().Set("Content-Type", imageMime)
|
|
writer.Header().Set("Content-Length", fmt.Sprintf("%d", fileSize))
|
|
|
|
_, err = io.Copy(writer, reader)
|
|
common.PanicOnError(err)
|
|
}
|
|
|
|
// composeMetadataHandler returns a tar of the metadata used to compose the requested UUID
|
|
func (api *API) composeMetadataHandler(writer http.ResponseWriter, request *http.Request, params httprouter.Params) {
|
|
if !verifyRequestVersion(writer, params, 0) {
|
|
return
|
|
}
|
|
|
|
uuidString := params.ByName("uuid")
|
|
uuid, err := uuid.Parse(uuidString)
|
|
if err != nil {
|
|
errors := responseError{
|
|
ID: "UnknownUUID",
|
|
Msg: fmt.Sprintf("%s is not a valid build uuid", uuidString),
|
|
}
|
|
statusResponseError(writer, http.StatusBadRequest, errors)
|
|
return
|
|
}
|
|
|
|
compose, exists := api.store.GetCompose(uuid)
|
|
if !exists {
|
|
errors := responseError{
|
|
ID: "UnknownUUID",
|
|
Msg: fmt.Sprintf("Compose %s doesn't exist", uuidString),
|
|
}
|
|
statusResponseError(writer, http.StatusBadRequest, errors)
|
|
return
|
|
}
|
|
|
|
composeStatus, err := api.getComposeStatus(compose)
|
|
if err != nil {
|
|
errors := responseError{
|
|
ID: "ComposeStatusError",
|
|
Msg: fmt.Sprintf("Error getting status of compose %s: %s", uuid, err),
|
|
}
|
|
statusResponseError(writer, http.StatusInternalServerError, errors)
|
|
return
|
|
}
|
|
if composeStatus.State != ComposeFinished && composeStatus.State != ComposeFailed {
|
|
errors := responseError{
|
|
ID: "BuildInWrongState",
|
|
Msg: fmt.Sprintf("Build %s is in wrong state: %s", uuidString, composeStatus.State.ToString()),
|
|
}
|
|
statusResponseError(writer, http.StatusBadRequest, errors)
|
|
return
|
|
}
|
|
|
|
metadata, err := json.Marshal(&compose.ImageBuild.Manifest)
|
|
common.PanicOnError(err)
|
|
|
|
writer.Header().Set("Content-Disposition", "attachment; filename="+uuid.String()+"-metadata.tar")
|
|
writer.Header().Set("Content-Type", "application/x-tar")
|
|
// NOTE: Do not set Content-Length, it will use chunked transfer encoding automatically
|
|
|
|
tw := tar.NewWriter(writer)
|
|
hdr := &tar.Header{
|
|
Name: uuid.String() + ".json",
|
|
Mode: 0600,
|
|
Size: int64(len(metadata)),
|
|
ModTime: time.Now().Truncate(time.Second),
|
|
}
|
|
err = tw.WriteHeader(hdr)
|
|
common.PanicOnError(err)
|
|
|
|
_, err = tw.Write(metadata)
|
|
common.PanicOnError(err)
|
|
|
|
err = tw.Close()
|
|
common.PanicOnError(err)
|
|
}
|
|
|
|
// composeResultsHandler returns a tar of the metadata, logs, and image from a compose
|
|
func (api *API) composeResultsHandler(writer http.ResponseWriter, request *http.Request, params httprouter.Params) {
|
|
if !verifyRequestVersion(writer, params, 0) {
|
|
return
|
|
}
|
|
|
|
uuidString := params.ByName("uuid")
|
|
uuid, err := uuid.Parse(uuidString)
|
|
if err != nil {
|
|
errors := responseError{
|
|
ID: "UnknownUUID",
|
|
Msg: fmt.Sprintf("%s is not a valid build uuid", uuidString),
|
|
}
|
|
statusResponseError(writer, http.StatusBadRequest, errors)
|
|
return
|
|
}
|
|
|
|
compose, exists := api.store.GetCompose(uuid)
|
|
if !exists {
|
|
errors := responseError{
|
|
ID: "UnknownUUID",
|
|
Msg: fmt.Sprintf("Compose %s doesn't exist", uuidString),
|
|
}
|
|
statusResponseError(writer, http.StatusBadRequest, errors)
|
|
return
|
|
}
|
|
|
|
composeStatus, err := api.getComposeStatus(compose)
|
|
if err != nil {
|
|
errors := responseError{
|
|
ID: "ComposeStatusError",
|
|
Msg: fmt.Sprintf("Error getting status of compose %s: %s", uuid, err),
|
|
}
|
|
statusResponseError(writer, http.StatusInternalServerError, errors)
|
|
return
|
|
}
|
|
if composeStatus.State != ComposeFinished && composeStatus.State != ComposeFailed {
|
|
errors := responseError{
|
|
ID: "BuildInWrongState",
|
|
Msg: fmt.Sprintf("Build %s is in wrong state: %s", uuidString, composeStatus.State.ToString()),
|
|
}
|
|
statusResponseError(writer, http.StatusBadRequest, errors)
|
|
return
|
|
}
|
|
|
|
metadata, err := json.Marshal(&compose.ImageBuild.Manifest)
|
|
common.PanicOnError(err)
|
|
|
|
writer.Header().Set("Content-Disposition", "attachment; filename="+uuid.String()+".tar")
|
|
writer.Header().Set("Content-Type", "application/x-tar")
|
|
// NOTE: Do not set Content-Length, it should use chunked transfer encoding automatically
|
|
|
|
tw := tar.NewWriter(writer)
|
|
hdr := &tar.Header{
|
|
Name: uuid.String() + ".json",
|
|
Mode: 0644,
|
|
Size: int64(len(metadata)),
|
|
ModTime: time.Now().Truncate(time.Second),
|
|
}
|
|
err = tw.WriteHeader(hdr)
|
|
common.PanicOnError(err)
|
|
_, err = tw.Write(metadata)
|
|
common.PanicOnError(err)
|
|
|
|
// Add the logs
|
|
var fileContents bytes.Buffer
|
|
if composeStatus.Result != nil {
|
|
err = composeStatus.Result.Write(&fileContents)
|
|
common.PanicOnError(err)
|
|
|
|
hdr = &tar.Header{
|
|
Name: "logs/osbuild.log",
|
|
Mode: 0644,
|
|
Size: int64(fileContents.Len()),
|
|
ModTime: time.Now().Truncate(time.Second),
|
|
}
|
|
err = tw.WriteHeader(hdr)
|
|
common.PanicOnError(err)
|
|
_, err = tw.Write(fileContents.Bytes())
|
|
common.PanicOnError(err)
|
|
}
|
|
|
|
reader, fileSize, err := api.openImageFile(uuid, compose)
|
|
if err == nil {
|
|
hdr = &tar.Header{
|
|
Name: uuid.String() + "-" + compose.ImageBuild.ImageType.Filename(),
|
|
Mode: 0644,
|
|
Size: int64(fileSize),
|
|
ModTime: time.Now().Truncate(time.Second),
|
|
}
|
|
err = tw.WriteHeader(hdr)
|
|
common.PanicOnError(err)
|
|
_, err = io.Copy(tw, reader)
|
|
common.PanicOnError(err)
|
|
}
|
|
|
|
err = tw.Close()
|
|
common.PanicOnError(err)
|
|
}
|
|
|
|
func (api *API) composeLogsHandler(writer http.ResponseWriter, request *http.Request, params httprouter.Params) {
|
|
if !verifyRequestVersion(writer, params, 0) {
|
|
return
|
|
}
|
|
|
|
uuidString := params.ByName("uuid")
|
|
id, err := uuid.Parse(uuidString)
|
|
if err != nil {
|
|
errors := responseError{
|
|
ID: "UnknownUUID",
|
|
Msg: fmt.Sprintf("%s is not a valid build uuid", uuidString),
|
|
}
|
|
statusResponseError(writer, http.StatusBadRequest, errors)
|
|
return
|
|
}
|
|
|
|
compose, exists := api.store.GetCompose(id)
|
|
if !exists {
|
|
errors := responseError{
|
|
ID: "UnknownUUID",
|
|
Msg: fmt.Sprintf("Compose %s doesn't exist", uuidString),
|
|
}
|
|
statusResponseError(writer, http.StatusBadRequest, errors)
|
|
return
|
|
}
|
|
|
|
composeStatus, err := api.getComposeStatus(compose)
|
|
if err != nil {
|
|
errors := responseError{
|
|
ID: "ComposeStatusError",
|
|
Msg: fmt.Sprintf("Error getting status of compose %s: %s", id, err),
|
|
}
|
|
statusResponseError(writer, http.StatusInternalServerError, errors)
|
|
return
|
|
}
|
|
if composeStatus.State != ComposeFinished && composeStatus.State != ComposeFailed {
|
|
errors := responseError{
|
|
ID: "BuildInWrongState",
|
|
Msg: fmt.Sprintf("Build %s not in FINISHED or FAILED state.", uuidString),
|
|
}
|
|
statusResponseError(writer, http.StatusBadRequest, errors)
|
|
return
|
|
}
|
|
|
|
writer.Header().Set("Content-Disposition", "attachment; filename="+id.String()+"-logs.tar")
|
|
writer.Header().Set("Content-Type", "application/x-tar")
|
|
|
|
tw := tar.NewWriter(writer)
|
|
|
|
// tar format needs to contain file size before the actual file content, therefore the intermediate buffer
|
|
var fileContents bytes.Buffer
|
|
err = composeStatus.Result.Write(&fileContents)
|
|
common.PanicOnError(err)
|
|
|
|
header := &tar.Header{
|
|
Name: "logs/osbuild.log",
|
|
Mode: 0644,
|
|
Size: int64(fileContents.Len()),
|
|
ModTime: time.Now().Truncate(time.Second),
|
|
}
|
|
|
|
err = tw.WriteHeader(header)
|
|
common.PanicOnError(err)
|
|
|
|
_, err = io.Copy(tw, &fileContents)
|
|
common.PanicOnError(err)
|
|
|
|
err = tw.Close()
|
|
common.PanicOnError(err)
|
|
}
|
|
|
|
func (api *API) composeLogHandler(writer http.ResponseWriter, request *http.Request, params httprouter.Params) {
|
|
// TODO: implement size param
|
|
if !verifyRequestVersion(writer, params, 0) {
|
|
return
|
|
}
|
|
|
|
uuidString := params.ByName("uuid")
|
|
id, err := uuid.Parse(uuidString)
|
|
if err != nil {
|
|
errors := responseError{
|
|
ID: "UnknownUUID",
|
|
Msg: fmt.Sprintf("%s is not a valid build uuid", uuidString),
|
|
}
|
|
statusResponseError(writer, http.StatusBadRequest, errors)
|
|
return
|
|
}
|
|
|
|
compose, exists := api.store.GetCompose(id)
|
|
if !exists {
|
|
errors := responseError{
|
|
ID: "UnknownUUID",
|
|
Msg: fmt.Sprintf("Compose %s doesn't exist", uuidString),
|
|
}
|
|
statusResponseError(writer, http.StatusBadRequest, errors)
|
|
return
|
|
}
|
|
|
|
composeStatus, err := api.getComposeStatus(compose)
|
|
if err != nil {
|
|
errors := responseError{
|
|
ID: "ComposeStatusError",
|
|
Msg: fmt.Sprintf("Error getting status of compose %s: %s", id, err),
|
|
}
|
|
statusResponseError(writer, http.StatusInternalServerError, errors)
|
|
return
|
|
}
|
|
if composeStatus.State == ComposeWaiting {
|
|
errors := responseError{
|
|
ID: "BuildInWrongState",
|
|
Msg: fmt.Sprintf("Build %s has not started yet. No logs to view.", uuidString),
|
|
}
|
|
statusResponseError(writer, http.StatusOK, errors) // weirdly, Lorax returns 200 in this case
|
|
return
|
|
}
|
|
|
|
if composeStatus.State == ComposeRunning {
|
|
fmt.Fprintf(writer, "Build %s is still running.\n", uuidString)
|
|
return
|
|
}
|
|
|
|
err = composeStatus.Result.Write(writer)
|
|
common.PanicOnError(err)
|
|
}
|
|
|
|
func (api *API) composeFinishedHandler(writer http.ResponseWriter, request *http.Request, params httprouter.Params) {
|
|
if !verifyRequestVersion(writer, params, 0) {
|
|
return
|
|
}
|
|
|
|
reply := struct {
|
|
Finished []*ComposeEntry `json:"finished"`
|
|
}{[]*ComposeEntry{}}
|
|
|
|
includeUploads := isRequestVersionAtLeast(params, 1)
|
|
for id, compose := range api.store.GetAllComposes() {
|
|
composeStatus, err := api.getComposeStatus(compose)
|
|
if err != nil {
|
|
log.Printf("Error getting status of compose %s: %s", id, err)
|
|
continue
|
|
}
|
|
if composeStatus.State != ComposeFinished {
|
|
continue
|
|
}
|
|
reply.Finished = append(reply.Finished, composeToComposeEntry(id, compose, composeStatus, includeUploads))
|
|
}
|
|
sortComposeEntries(reply.Finished)
|
|
|
|
err := json.NewEncoder(writer).Encode(reply)
|
|
common.PanicOnError(err)
|
|
}
|
|
|
|
func (api *API) composeFailedHandler(writer http.ResponseWriter, request *http.Request, params httprouter.Params) {
|
|
if !verifyRequestVersion(writer, params, 0) {
|
|
return
|
|
}
|
|
|
|
reply := struct {
|
|
Failed []*ComposeEntry `json:"failed"`
|
|
}{[]*ComposeEntry{}}
|
|
|
|
includeUploads := isRequestVersionAtLeast(params, 1)
|
|
for id, compose := range api.store.GetAllComposes() {
|
|
composeStatus, err := api.getComposeStatus(compose)
|
|
if err != nil {
|
|
log.Printf("Error getting status of compose %s: %s", id, err)
|
|
continue
|
|
}
|
|
if composeStatus.State != ComposeFailed {
|
|
continue
|
|
}
|
|
reply.Failed = append(reply.Failed, composeToComposeEntry(id, compose, composeStatus, includeUploads))
|
|
}
|
|
sortComposeEntries(reply.Failed)
|
|
|
|
err := json.NewEncoder(writer).Encode(reply)
|
|
common.PanicOnError(err)
|
|
}
|
|
|
|
// fetchPackageList returns the package list or the selected distribution
|
|
func (api *API) fetchPackageList(distroName, arch string, names []string) (packages rpmmd.PackageList, err error) {
|
|
d := api.getDistro(distroName, arch)
|
|
if d == nil {
|
|
return nil, fmt.Errorf("GetDistro - unknown distribution: %s", distroName)
|
|
}
|
|
repos, err := api.allRepositories(distroName, arch)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
solver := api.solver.NewWithConfig(d.ModulePlatformID(), d.Releasever(), arch, d.Name())
|
|
if len(names) == 0 {
|
|
packages, err = solver.FetchMetadata(repos)
|
|
} else {
|
|
packages, err = solver.SearchMetadata(repos, names)
|
|
}
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if err := solver.CleanCache(); err != nil {
|
|
// log and ignore
|
|
log.Printf("Error during rpm repo cache cleanup: %s", err.Error())
|
|
}
|
|
return packages, nil
|
|
}
|
|
|
|
// Returns only user-defined repositories, which should be used only for
|
|
// payload package sets.
|
|
func (api *API) payloadRepositories(distroName string) []rpmmd.RepoConfig {
|
|
distroSourceConfigs := api.store.GetAllDistroSources(distroName)
|
|
payloadRepos := make([]rpmmd.RepoConfig, 0, len(distroSourceConfigs))
|
|
for id, source := range distroSourceConfigs {
|
|
payloadRepos = append(payloadRepos, source.RepoConfig(id))
|
|
}
|
|
return payloadRepos
|
|
}
|
|
|
|
// Returns all configured repositories as rpmmd.RepoConfig.
|
|
// Payload repositories (defined by the user) are assigned the payload package
|
|
// set names from the image type.
|
|
//
|
|
// The difference from allRepositories() is that this method may return additional repositories
|
|
// which are needed to build the specific image type. The allRepositories() can't do this, because
|
|
// it is used in places where image types are not considered.
|
|
//
|
|
// Note that the distro name is not determined from the image type, but must be provided as an argument.
|
|
// This enables using distro name aliases to get repositories.
|
|
func (api *API) allRepositoriesByImageType(distroName string, imageType distro.ImageType) ([]rpmmd.RepoConfig, error) {
|
|
repos, err := api.repoRegistry.ReposByImageTypeName(distroName, imageType.Arch().Name(), imageType.Name())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
payloadRepos := api.payloadRepositories(distroName)
|
|
// tag payload repositories with the payload package set names and add them to list of repos
|
|
for _, pr := range payloadRepos {
|
|
pr.PackageSets = imageType.PayloadPackageSets()
|
|
repos = append(repos, pr)
|
|
}
|
|
|
|
return repos, nil
|
|
}
|
|
|
|
// Returns all configured repositories (base + sources) as rpmmd.RepoConfig
|
|
func (api *API) allRepositories(distroName, arch string) ([]rpmmd.RepoConfig, error) {
|
|
repos, err := api.repoRegistry.ReposByArchName(distroName, arch, false)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
payloadRepos := api.payloadRepositories(distroName)
|
|
repos = append(repos, payloadRepos...)
|
|
|
|
return repos, nil
|
|
}
|
|
|
|
func (api *API) depsolveBlueprint(bp blueprint.Blueprint) ([]rpmmd.PackageSpec, error) {
|
|
// Depsolve using the host distro if none has been specified
|
|
if bp.Distro == "" {
|
|
bp.Distro = api.hostDistroName
|
|
}
|
|
|
|
arch := bp.Arch
|
|
if arch == "" {
|
|
arch = api.hostArch
|
|
}
|
|
|
|
d := api.getDistro(bp.Distro, arch)
|
|
if d == nil {
|
|
return nil, fmt.Errorf("GetDistro - unknown distribution: %s", bp.Distro)
|
|
}
|
|
repos, err := api.allRepositories(bp.Distro, arch)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
solver := api.solver.NewWithConfig(d.ModulePlatformID(), d.Releasever(), arch, d.Name())
|
|
res, err := solver.Depsolve([]rpmmd.PackageSet{{Include: bp.GetPackages(), EnabledModules: bp.GetEnabledModules(), Repositories: repos}}, sbom.StandardTypeNone)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if err := solver.CleanCache(); err != nil {
|
|
// log and ignore
|
|
log.Printf("Error during rpm repo cache cleanup: %s", err.Error())
|
|
}
|
|
return res.Packages, nil
|
|
}
|
|
|
|
func (api *API) uploadsScheduleHandler(writer http.ResponseWriter, request *http.Request, params httprouter.Params) {
|
|
if !verifyRequestVersion(writer, params, 1) {
|
|
return
|
|
}
|
|
|
|
// TODO: implement this route (it is v1 only)
|
|
notImplementedHandler(writer, request, params)
|
|
}
|
|
|
|
func (api *API) uploadsDeleteHandler(writer http.ResponseWriter, request *http.Request, params httprouter.Params) {
|
|
if !verifyRequestVersion(writer, params, 1) {
|
|
return
|
|
}
|
|
|
|
// TODO: implement this route (it is v1 only)
|
|
notImplementedHandler(writer, request, params)
|
|
}
|
|
|
|
func (api *API) uploadsInfoHandler(writer http.ResponseWriter, request *http.Request, params httprouter.Params) {
|
|
if !verifyRequestVersion(writer, params, 1) {
|
|
return
|
|
}
|
|
|
|
// TODO: implement this route (it is v1 only)
|
|
notImplementedHandler(writer, request, params)
|
|
}
|
|
|
|
func (api *API) uploadsLogHandler(writer http.ResponseWriter, request *http.Request, params httprouter.Params) {
|
|
if !verifyRequestVersion(writer, params, 1) {
|
|
return
|
|
}
|
|
|
|
// TODO: implement this route (it is v1 only)
|
|
notImplementedHandler(writer, request, params)
|
|
}
|
|
|
|
func (api *API) uploadsResetHandler(writer http.ResponseWriter, request *http.Request, params httprouter.Params) {
|
|
if !verifyRequestVersion(writer, params, 1) {
|
|
return
|
|
}
|
|
|
|
// TODO: implement this route (it is v1 only)
|
|
notImplementedHandler(writer, request, params)
|
|
}
|
|
|
|
func (api *API) uploadsCancelHandler(writer http.ResponseWriter, request *http.Request, params httprouter.Params) {
|
|
if !verifyRequestVersion(writer, params, 1) {
|
|
return
|
|
}
|
|
|
|
// TODO: implement this route (it is v1 only)
|
|
notImplementedHandler(writer, request, params)
|
|
}
|
|
|
|
func (api *API) providersHandler(writer http.ResponseWriter, request *http.Request, params httprouter.Params) {
|
|
if !verifyRequestVersion(writer, params, 1) {
|
|
return
|
|
}
|
|
|
|
// TODO: implement this route (it is v1 only)
|
|
notImplementedHandler(writer, request, params)
|
|
}
|
|
|
|
func (api *API) providersSaveHandler(writer http.ResponseWriter, request *http.Request, params httprouter.Params) {
|
|
if !verifyRequestVersion(writer, params, 1) {
|
|
return
|
|
}
|
|
|
|
// TODO: implement this route (it is v1 only)
|
|
notImplementedHandler(writer, request, params)
|
|
}
|
|
|
|
func (api *API) providersDeleteHandler(writer http.ResponseWriter, request *http.Request, params httprouter.Params) {
|
|
if !verifyRequestVersion(writer, params, 1) {
|
|
return
|
|
}
|
|
|
|
// TODO: implement this route (it is v1 only)
|
|
notImplementedHandler(writer, request, params)
|
|
}
|
|
|
|
func (api *API) distrosListHandler(writer http.ResponseWriter, request *http.Request, params httprouter.Params) {
|
|
if !verifyRequestVersion(writer, params, 1) {
|
|
return
|
|
}
|
|
|
|
archName := api.hostArch
|
|
if arch := request.URL.Query().Get("arch"); arch != "" {
|
|
archName = arch
|
|
}
|
|
|
|
var reply struct {
|
|
Distros []string `json:"distros"`
|
|
}
|
|
reply.Distros = api.validDistros(archName)
|
|
|
|
err := json.NewEncoder(writer).Encode(reply)
|
|
common.PanicOnError(err)
|
|
}
|