In our base distro definitions we exclude packages in addition to including them. Extend dnf-json to support this, so we can depsolve the base package set as well as the packages added in blueprints. Signed-off-by: Tom Gundersen <teg@jklm.no>
1902 lines
52 KiB
Go
1902 lines
52 KiB
Go
package weldr
|
|
|
|
import (
|
|
"archive/tar"
|
|
"bytes"
|
|
"encoding/json"
|
|
errors_package "errors"
|
|
"fmt"
|
|
"io"
|
|
"log"
|
|
"net"
|
|
"net/http"
|
|
"net/url"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/BurntSushi/toml"
|
|
"github.com/google/uuid"
|
|
"github.com/julienschmidt/httprouter"
|
|
|
|
"github.com/osbuild/osbuild-composer/internal/blueprint"
|
|
"github.com/osbuild/osbuild-composer/internal/common"
|
|
"github.com/osbuild/osbuild-composer/internal/distro"
|
|
"github.com/osbuild/osbuild-composer/internal/rpmmd"
|
|
"github.com/osbuild/osbuild-composer/internal/store"
|
|
"github.com/osbuild/osbuild-composer/internal/target"
|
|
)
|
|
|
|
type API struct {
|
|
store *store.Store
|
|
|
|
rpmmd rpmmd.RPMMD
|
|
arch string
|
|
distro distro.Distro
|
|
|
|
logger *log.Logger
|
|
router *httprouter.Router
|
|
}
|
|
|
|
func New(rpmmd rpmmd.RPMMD, arch string, distro distro.Distro, logger *log.Logger, store *store.Store) *API {
|
|
// This needs to be shared with the worker API so that they can communicate with each other
|
|
// builds := make(chan queue.Build, 200)
|
|
api := &API{
|
|
store: store,
|
|
rpmmd: rpmmd,
|
|
arch: arch,
|
|
distro: distro,
|
|
logger: logger,
|
|
}
|
|
|
|
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/: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/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.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/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/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)
|
|
|
|
return api
|
|
}
|
|
|
|
func (api *API) Serve(listener net.Listener) error {
|
|
server := http.Server{Handler: api}
|
|
|
|
err := server.Serve(listener)
|
|
if err != nil && err != http.ErrServerClosed {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (api *API) ServeHTTP(writer http.ResponseWriter, request *http.Request) {
|
|
if api.logger != nil {
|
|
log.Println(request.Method, request.URL.Path)
|
|
}
|
|
|
|
writer.Header().Set("Content-Type", "application/json; charset=utf-8")
|
|
api.router.ServeHTTP(writer, request)
|
|
}
|
|
|
|
func 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)
|
|
|
|
if err != nil {
|
|
panic(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) {
|
|
writer.WriteHeader(http.StatusNotFound)
|
|
}
|
|
|
|
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)
|
|
json.NewEncoder(writer).Encode(reply{true})
|
|
}
|
|
|
|
type responseError struct {
|
|
Code int `json:"code,omitempty"`
|
|
ID string `json:"id"`
|
|
Msg string `json:"msg"`
|
|
}
|
|
|
|
func statusResponseError(writer http.ResponseWriter, code int, errors ...responseError) {
|
|
type reply struct {
|
|
Status bool `json:"status"`
|
|
Errors []responseError `json:"errors,omitempty"`
|
|
}
|
|
|
|
writer.WriteHeader(code)
|
|
json.NewEncoder(writer).Encode(reply{false, errors})
|
|
}
|
|
|
|
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:"messages"`
|
|
}
|
|
|
|
json.NewEncoder(writer).Encode(reply{
|
|
API: "1",
|
|
DBSupported: true,
|
|
DBVersion: "0",
|
|
SchemaVersion: "0",
|
|
Backend: "osbuild-composer",
|
|
Build: "devel",
|
|
Messages: make([]string, 0),
|
|
})
|
|
}
|
|
|
|
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"`
|
|
}
|
|
|
|
names := api.store.ListSources()
|
|
|
|
for _, repo := range api.distro.Repositories(api.arch) {
|
|
names = append(names, repo.Id)
|
|
}
|
|
|
|
json.NewEncoder(writer).Encode(reply{
|
|
Sources: names,
|
|
})
|
|
}
|
|
|
|
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)
|
|
}
|
|
|
|
func (api *API) sourceInfoHandler(writer http.ResponseWriter, request *http.Request, params httprouter.Params) {
|
|
if !verifyRequestVersion(writer, params, 0) { // TODO: version 1 API
|
|
return
|
|
}
|
|
|
|
// weldr uses a slightly different format than dnf to store repository
|
|
// configuration
|
|
type reply struct {
|
|
Sources map[string]store.SourceConfig `json:"sources"`
|
|
Errors []responseError `json:"errors"`
|
|
}
|
|
|
|
names := params.ByName("sources")
|
|
|
|
sources := map[string]store.SourceConfig{}
|
|
errors := []responseError{}
|
|
|
|
// if names is "*" we want all sources
|
|
if names == "*" {
|
|
sources = api.store.GetAllSources()
|
|
for _, repo := range api.distro.Repositories(api.arch) {
|
|
sources[repo.Id] = 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 api.distro.Repositories(api.arch) {
|
|
if name == repo.Id {
|
|
sources[repo.Id] = 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[source.Name] = *source
|
|
} else {
|
|
error := responseError{
|
|
ID: "UnknownSource",
|
|
Msg: fmt.Sprintf("%s is not a valid source", name),
|
|
}
|
|
errors = append(errors, error)
|
|
}
|
|
}
|
|
}
|
|
|
|
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 == "" {
|
|
json.NewEncoder(writer).Encode(reply{
|
|
Sources: sources,
|
|
Errors: errors,
|
|
})
|
|
} else if format == "toml" {
|
|
encoder := toml.NewEncoder(writer)
|
|
encoder.Indent = ""
|
|
encoder.Encode(sources)
|
|
} else {
|
|
errors := responseError{
|
|
ID: "InvalidChars",
|
|
Msg: fmt.Sprintf("invalid format parameter: %s", format),
|
|
}
|
|
statusResponseError(writer, http.StatusBadRequest, errors)
|
|
return
|
|
}
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
var source store.SourceConfig
|
|
var err error
|
|
if contentType[0] == "application/json" {
|
|
err = json.NewDecoder(request.Body).Decode(&source)
|
|
} else if contentType[0] == "text/x-toml" {
|
|
_, err = toml.DecodeReader(request.Body, &source)
|
|
} else {
|
|
err = errors_package.New("blueprint must be in json or toml format")
|
|
}
|
|
|
|
if err != nil {
|
|
errors := responseError{
|
|
Code: http.StatusBadRequest,
|
|
ID: "HTTPError",
|
|
Msg: "Bad Request",
|
|
}
|
|
statusResponseError(writer, http.StatusBadRequest, errors)
|
|
return
|
|
}
|
|
|
|
api.store.PushSource(source)
|
|
|
|
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
|
|
}
|
|
|
|
// remove leading / from first name
|
|
api.store.DeleteSource(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")
|
|
|
|
availablePackages, err := api.fetchPackageList()
|
|
|
|
if err != nil {
|
|
errors := responseError{
|
|
ID: "ModulesError",
|
|
Msg: fmt.Sprintf("msg: %s", err.Error()),
|
|
}
|
|
statusResponseError(writer, http.StatusBadRequest, errors)
|
|
return
|
|
}
|
|
|
|
var packages rpmmd.PackageList
|
|
if modulesParam != "" && modulesParam != "/" {
|
|
// we have modules for search
|
|
|
|
// remove leading /
|
|
modulesParam = modulesParam[1:]
|
|
|
|
names := strings.Split(modulesParam, ",")
|
|
|
|
packages, err = availablePackages.Search(names...)
|
|
|
|
if err != nil {
|
|
errors := responseError{
|
|
ID: "ModulesError",
|
|
Msg: fmt.Sprintf("Wrong glob pattern: %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
|
|
}
|
|
} else {
|
|
// just return all available packages
|
|
packages = availablePackages
|
|
}
|
|
|
|
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"}
|
|
}
|
|
|
|
json.NewEncoder(writer).Encode(reply{
|
|
Total: total,
|
|
Offset: offset,
|
|
Limit: limit,
|
|
Modules: modules,
|
|
})
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
availablePackages, err := api.fetchPackageList()
|
|
|
|
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]
|
|
}
|
|
|
|
json.NewEncoder(writer).Encode(reply{
|
|
Total: total,
|
|
Offset: offset,
|
|
Limit: limit,
|
|
Projects: packages,
|
|
})
|
|
}
|
|
|
|
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")
|
|
|
|
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, ",")
|
|
|
|
availablePackages, err := api.fetchPackageList()
|
|
|
|
if err != nil {
|
|
errors := responseError{
|
|
ID: "ModulesError",
|
|
Msg: fmt.Sprintf("msg: %s", err.Error()),
|
|
}
|
|
statusResponseError(writer, http.StatusBadRequest, errors)
|
|
return
|
|
}
|
|
|
|
foundPackages, err := availablePackages.Search(names...)
|
|
|
|
if err != nil {
|
|
errors := responseError{
|
|
ID: errorId,
|
|
Msg: fmt.Sprintf("Wrong glob pattern: %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 {
|
|
for i, _ := range packageInfos {
|
|
err := packageInfos[i].FillDependencies(api.rpmmd, api.distro.Repositories(api.arch))
|
|
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
|
|
}
|
|
}
|
|
}
|
|
|
|
if modulesRequested {
|
|
json.NewEncoder(writer).Encode(modulesReply{packageInfos})
|
|
} else {
|
|
json.NewEncoder(writer).Encode(projectsReply{packageInfos})
|
|
}
|
|
}
|
|
|
|
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"`
|
|
}
|
|
|
|
names := strings.Split(params.ByName("projects"), ",")
|
|
|
|
packages, _, err := api.rpmmd.Depsolve(names, nil, api.distro.Repositories(api.arch), false)
|
|
|
|
if err != nil {
|
|
errors := responseError{
|
|
ID: "PROJECTS_ERROR",
|
|
Msg: fmt.Sprintf("BadRequest: %s", err.Error()),
|
|
}
|
|
statusResponseError(writer, http.StatusBadRequest, errors)
|
|
return
|
|
}
|
|
|
|
json.NewEncoder(writer).Encode(reply{
|
|
Projects: packages,
|
|
})
|
|
}
|
|
|
|
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)
|
|
|
|
json.NewEncoder(writer).Encode(reply{
|
|
Total: total,
|
|
Offset: offset,
|
|
Limit: limit,
|
|
Blueprints: names[offset : offset+limit],
|
|
})
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
blueprints := []blueprint.Blueprint{}
|
|
changes := []change{}
|
|
|
|
for i, name := range names {
|
|
// remove leading / from first name
|
|
if i == 0 {
|
|
name = name[1:]
|
|
}
|
|
|
|
blueprint, changed := api.store.GetBlueprint(name)
|
|
if blueprint == nil {
|
|
errors := responseError{
|
|
ID: "UnknownBlueprint",
|
|
Msg: fmt.Sprintf("%s: ", name),
|
|
}
|
|
statusResponseError(writer, http.StatusBadRequest, errors)
|
|
return
|
|
}
|
|
blueprints = append(blueprints, *blueprint)
|
|
changes = append(changes, change{changed, blueprint.Name})
|
|
}
|
|
|
|
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 == "" {
|
|
json.NewEncoder(writer).Encode(reply{
|
|
Blueprints: blueprints,
|
|
Changes: changes,
|
|
Errors: []responseError{},
|
|
})
|
|
} 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
|
|
}
|
|
encoder := toml.NewEncoder(writer)
|
|
encoder.Indent = ""
|
|
encoder.Encode(blueprints[0])
|
|
} 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
|
|
}
|
|
|
|
blueprints := []entry{}
|
|
for i, name := range names {
|
|
// remove leading / from first name
|
|
if i == 0 {
|
|
name = name[1:]
|
|
}
|
|
blueprint, _ := api.store.GetBlueprint(name)
|
|
if blueprint == nil {
|
|
errors := responseError{
|
|
ID: "UnknownBlueprint",
|
|
Msg: fmt.Sprintf("%s: blueprint not found", name),
|
|
}
|
|
statusResponseError(writer, http.StatusBadRequest, errors)
|
|
return
|
|
}
|
|
|
|
dependencies, _, err := api.depsolveBlueprint(blueprint, false)
|
|
|
|
if err != nil {
|
|
errors := responseError{
|
|
ID: "BlueprintsError",
|
|
Msg: fmt.Sprintf("%s: %s", name, err.Error()),
|
|
}
|
|
statusResponseError(writer, http.StatusBadRequest, errors)
|
|
return
|
|
}
|
|
|
|
blueprints = append(blueprints, entry{*blueprint, dependencies})
|
|
}
|
|
|
|
json.NewEncoder(writer).Encode(reply{
|
|
Blueprints: blueprints,
|
|
Errors: []responseError{},
|
|
})
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
blueprints := []blueprintFrozen{}
|
|
errors := []responseError{}
|
|
for i, name := range names {
|
|
// remove leading / from first name
|
|
if i == 0 {
|
|
name = name[1:]
|
|
}
|
|
blueprint, _ := api.store.GetBlueprint(name)
|
|
if blueprint == nil {
|
|
rerr := responseError{
|
|
ID: "UnknownBlueprint",
|
|
Msg: fmt.Sprintf("%s: blueprint_not_found", name),
|
|
}
|
|
errors = append(errors, rerr)
|
|
break
|
|
}
|
|
|
|
dependencies, _, err := api.depsolveBlueprint(blueprint, false)
|
|
if err != nil {
|
|
rerr := responseError{
|
|
ID: "BlueprintsError",
|
|
Msg: fmt.Sprintf("%s: %s", name, err.Error()),
|
|
}
|
|
errors = append(errors, rerr)
|
|
break
|
|
}
|
|
|
|
for pkgIndex, pkg := range blueprint.Packages {
|
|
i := sort.Search(len(dependencies), func(i int) bool {
|
|
return dependencies[i].Name >= pkg.Name
|
|
})
|
|
if i < len(dependencies) && dependencies[i].Name == pkg.Name {
|
|
blueprint.Packages[pkgIndex].Version = dependencies[i].Version + "-" + dependencies[i].Release + "." + dependencies[i].Arch
|
|
}
|
|
}
|
|
|
|
blueprints = append(blueprints, blueprintFrozen{*blueprint})
|
|
}
|
|
|
|
json.NewEncoder(writer).Encode(reply{
|
|
Blueprints: blueprints,
|
|
Errors: errors,
|
|
})
|
|
}
|
|
|
|
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")
|
|
fromCommit := params.ByName("from")
|
|
toCommit := params.ByName("to")
|
|
|
|
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})
|
|
}
|
|
|
|
json.NewEncoder(writer).Encode(reply{diffs})
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
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 i, name := range names {
|
|
// remove leading / from first name
|
|
if i == 0 {
|
|
name = name[1:]
|
|
}
|
|
bpChanges := api.store.GetBlueprintChanges(name)
|
|
if bpChanges != nil {
|
|
change := change{
|
|
Changes: bpChanges,
|
|
Name: name,
|
|
Total: len(bpChanges),
|
|
}
|
|
allChanges = append(allChanges, change)
|
|
} else {
|
|
error := responseError{
|
|
ID: "UnknownBlueprint",
|
|
Msg: name,
|
|
}
|
|
errors = append(errors, error)
|
|
}
|
|
}
|
|
|
|
json.NewEncoder(writer).Encode(reply{
|
|
BlueprintsChanges: allChanges,
|
|
Errors: errors,
|
|
Offset: offset,
|
|
Limit: limit,
|
|
})
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
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.DecodeReader(request.Body, &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
|
|
}
|
|
|
|
commitMsg := "Recipe " + blueprint.Name + ", version " + blueprint.Version + " saved."
|
|
api.store.PushBlueprint(blueprint, commitMsg)
|
|
|
|
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
|
|
}
|
|
|
|
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.DecodeReader(request.Body, &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
|
|
}
|
|
|
|
api.store.PushBlueprintToWorkspace(blueprint)
|
|
|
|
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")
|
|
commit := params.ByName("commit")
|
|
bpChange := api.store.GetBlueprintChange(name, commit)
|
|
bp := bpChange.Blueprint
|
|
commitMsg := name + ".toml reverted to commit " + commit
|
|
api.store.PushBlueprint(bp, commitMsg)
|
|
statusResponseOK(writer)
|
|
}
|
|
|
|
func (api *API) blueprintDeleteHandler(writer http.ResponseWriter, request *http.Request, params httprouter.Params) {
|
|
if !verifyRequestVersion(writer, params, 0) {
|
|
return
|
|
}
|
|
|
|
api.store.DeleteBlueprint(params.ByName("blueprint"))
|
|
statusResponseOK(writer)
|
|
}
|
|
|
|
func (api *API) blueprintDeleteWorkspaceHandler(writer http.ResponseWriter, request *http.Request, params httprouter.Params) {
|
|
if !verifyRequestVersion(writer, params, 0) {
|
|
return
|
|
}
|
|
|
|
api.store.DeleteBlueprintFromWorkspace(params.ByName("blueprint"))
|
|
statusResponseOK(writer)
|
|
}
|
|
|
|
// 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"`
|
|
Branch string `json:"branch"`
|
|
Upload *UploadRequest `json:"upload"`
|
|
}
|
|
type ComposeReply struct {
|
|
BuildID uuid.UUID `json:"build_id"`
|
|
Status bool `json:"status"`
|
|
}
|
|
const MegaByte = 1024 * 1024
|
|
|
|
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
|
|
}
|
|
|
|
reply := ComposeReply{
|
|
BuildID: uuid.New(),
|
|
Status: true,
|
|
}
|
|
|
|
var uploadTarget *target.Target
|
|
if isRequestVersionAtLeast(params, 1) && cr.Upload != nil {
|
|
uploadTarget, err = UploadRequestToTarget(*cr.Upload)
|
|
|
|
if err != nil {
|
|
errors := responseError{
|
|
ID: "UploadError",
|
|
Msg: fmt.Sprintf("bad input format: %s", err.Error()),
|
|
}
|
|
|
|
statusResponseError(writer, http.StatusBadRequest, errors)
|
|
return
|
|
}
|
|
}
|
|
|
|
bp := api.store.GetBlueprintCommitted(cr.BlueprintName)
|
|
|
|
size := cr.Size
|
|
|
|
// Microsoft Azure requires vhd images to be rounded up to the nearest MB
|
|
if cr.ComposeType == "vhd" && size%MegaByte != 0 {
|
|
size = (size/MegaByte + 1) * MegaByte
|
|
}
|
|
|
|
if bp != nil {
|
|
_, checksums, err := api.depsolveBlueprint(bp, true)
|
|
if err != nil {
|
|
errors := responseError{
|
|
ID: "DepsolveError",
|
|
Msg: err.Error(),
|
|
}
|
|
statusResponseError(writer, http.StatusInternalServerError, errors)
|
|
return
|
|
}
|
|
|
|
err = api.store.PushCompose(reply.BuildID, bp, checksums, api.arch, cr.ComposeType, size, uploadTarget)
|
|
|
|
// 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
|
|
}
|
|
} else {
|
|
errors := responseError{
|
|
ID: "UnknownBlueprint",
|
|
Msg: fmt.Sprintf("Unknown blueprint name: %s", cr.BlueprintName),
|
|
}
|
|
statusResponseError(writer, http.StatusBadRequest, errors)
|
|
return
|
|
}
|
|
|
|
json.NewEncoder(writer).Encode(reply)
|
|
}
|
|
|
|
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),
|
|
})
|
|
}
|
|
|
|
err = api.store.DeleteCompose(id)
|
|
|
|
if err != nil {
|
|
switch err.(type) {
|
|
case *store.NotFoundError:
|
|
errors = append(errors, composeDeleteError{
|
|
"UnknownUUID",
|
|
fmt.Sprintf("compose %s doesn't exist", id),
|
|
})
|
|
case *store.InvalidRequestError:
|
|
errors = append(errors, composeDeleteError{
|
|
"BuildInWrongState",
|
|
err.Error(),
|
|
})
|
|
default:
|
|
errors = append(errors, composeDeleteError{
|
|
"ComposeError",
|
|
fmt.Sprintf("%s: %s", id, err.Error()),
|
|
})
|
|
}
|
|
}
|
|
|
|
results = append(results, composeDeleteStatus{id, true})
|
|
}
|
|
|
|
reply := struct {
|
|
UUIDs []composeDeleteStatus `json:"uuids"`
|
|
Errors []composeDeleteError `json:"errors"`
|
|
}{results, errors}
|
|
|
|
json.NewEncoder(writer).Encode(reply)
|
|
}
|
|
|
|
func (api *API) composeTypesHandler(writer http.ResponseWriter, request *http.Request, params httprouter.Params) {
|
|
if !verifyRequestVersion(writer, params, 0) {
|
|
return
|
|
}
|
|
type composeType struct {
|
|
Name string `json:"name"`
|
|
Enabled bool `json:"enabled"`
|
|
}
|
|
|
|
var reply struct {
|
|
Types []composeType `json:"types"`
|
|
}
|
|
|
|
for _, format := range api.distro.ListOutputFormats() {
|
|
reply.Types = append(reply.Types, composeType{format, true})
|
|
}
|
|
|
|
json.NewEncoder(writer).Encode(reply)
|
|
}
|
|
|
|
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{}}
|
|
|
|
composes := api.store.GetAllComposes()
|
|
for id, compose := range composes {
|
|
switch compose.GetState() {
|
|
case common.CWaiting:
|
|
reply.New = append(reply.New, composeToComposeEntry(id, compose, isRequestVersionAtLeast(params, 1)))
|
|
case common.CRunning:
|
|
reply.Run = append(reply.Run, composeToComposeEntry(id, compose, isRequestVersionAtLeast(params, 1)))
|
|
}
|
|
}
|
|
|
|
json.NewEncoder(writer).Encode(reply)
|
|
}
|
|
|
|
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")
|
|
|
|
var uuids []uuid.UUID
|
|
|
|
if uuidsParam != "*" {
|
|
uuidStrings := strings.Split(uuidsParam, ",")
|
|
uuids = make([]uuid.UUID, len(uuidStrings))
|
|
for _, uuidString := range uuidStrings {
|
|
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)
|
|
}
|
|
}
|
|
composes := api.store.GetAllComposes()
|
|
|
|
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")
|
|
filterStatus := q.Get("status")
|
|
filterImageType := q.Get("type")
|
|
|
|
if filterBlueprint != "" || filterStatus != "" || filterImageType != "" {
|
|
for uuid, compose := range composes {
|
|
filterImageType, found := common.ImageTypeFromCompatString(filterImageType)
|
|
|
|
if filterBlueprint != "" && compose.Blueprint.Name != filterBlueprint {
|
|
delete(composes, uuid)
|
|
} else if filterStatus != "" && compose.ImageBuilds[0].QueueStatus.ToString() != filterStatus {
|
|
delete(composes, uuid)
|
|
} else if found && compose.ImageBuilds[0].ImageType != filterImageType {
|
|
delete(composes, uuid)
|
|
}
|
|
}
|
|
}
|
|
|
|
reply.UUIDs = composesToComposeEntries(composes, uuids, isRequestVersionAtLeast(params, 1))
|
|
|
|
json.NewEncoder(writer).Encode(reply)
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
type Dependencies struct {
|
|
Packages []map[string]interface{} `json:"packages"`
|
|
}
|
|
|
|
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 Dependencies `json:"deps"` // empty for now
|
|
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
|
|
reply.Deps = Dependencies{
|
|
Packages: make([]map[string]interface{}, 0),
|
|
}
|
|
// Weldr API assumes only one image build per compose, that's why only the
|
|
// 1st build is considered
|
|
reply.ComposeType, _ = compose.ImageBuilds[0].ImageType.ToCompatString()
|
|
reply.QueueStatus = compose.GetState().ToString()
|
|
reply.ImageSize = compose.ImageBuilds[0].Size
|
|
|
|
if isRequestVersionAtLeast(params, 1) {
|
|
reply.Uploads = TargetsToUploadResponses(compose.ImageBuilds[0].Targets)
|
|
}
|
|
|
|
json.NewEncoder(writer).Encode(reply)
|
|
}
|
|
|
|
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: "BuildMissingFile",
|
|
Msg: fmt.Sprintf("Compose %s doesn't exist", uuidString),
|
|
}
|
|
statusResponseError(writer, http.StatusBadRequest, errors)
|
|
return
|
|
}
|
|
|
|
if compose.GetState() != common.CFinished {
|
|
errors := responseError{
|
|
ID: "BuildInWrongState",
|
|
Msg: fmt.Sprintf("Build %s is in wrong state: %s", uuidString, compose.GetState().ToString()),
|
|
}
|
|
statusResponseError(writer, http.StatusBadRequest, errors)
|
|
return
|
|
}
|
|
|
|
imageBuild := compose.ImageBuilds[0]
|
|
imageType, _ := imageBuild.ImageType.ToCompatString()
|
|
imageName, imageMime, err := api.distro.FilenameFromType(imageType)
|
|
|
|
if err != nil {
|
|
errors := responseError{
|
|
ID: "BadCompose",
|
|
Msg: fmt.Sprintf("Compose %s is ill-formed: output type %v is invalid for distro %s", uuidString, imageBuild.ImageType, api.distro.Name()),
|
|
}
|
|
statusResponseError(writer, http.StatusInternalServerError, errors)
|
|
return
|
|
}
|
|
|
|
reader, fileSize, err := api.store.GetImageBuildImage(uuid, 0)
|
|
|
|
// TODO: this might return misleading error
|
|
if err != nil {
|
|
errors := responseError{
|
|
ID: "BuildMissingFile",
|
|
Msg: fmt.Sprintf("Build %s is missing file %s!", uuidString, imageName),
|
|
}
|
|
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))
|
|
|
|
io.Copy(writer, reader)
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
if compose.GetState() != common.CFinished && compose.GetState() != common.CFailed {
|
|
errors := responseError{
|
|
ID: "BuildInWrongState",
|
|
Msg: fmt.Sprintf("Build %s not in FINISHED or FAILED state.", uuidString),
|
|
}
|
|
statusResponseError(writer, http.StatusBadRequest, errors)
|
|
return
|
|
}
|
|
|
|
resultReader, err := api.store.GetImageBuildResult(id, 0)
|
|
|
|
if err != nil {
|
|
errors := responseError{
|
|
ID: "ComposeError",
|
|
Msg: fmt.Sprintf("Opening log for compose %s failed", uuidString),
|
|
}
|
|
statusResponseError(writer, http.StatusBadRequest, errors)
|
|
return
|
|
}
|
|
|
|
var result common.ComposeResult
|
|
err = json.NewDecoder(resultReader).Decode(&result)
|
|
if err != nil {
|
|
errors := responseError{
|
|
ID: "ComposeError",
|
|
Msg: fmt.Sprintf("Parsing log for compose %s failed", uuidString),
|
|
}
|
|
statusResponseError(writer, http.StatusBadRequest, errors)
|
|
return
|
|
}
|
|
|
|
resultReader.Close()
|
|
|
|
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
|
|
result.Write(&fileContents)
|
|
|
|
header := &tar.Header{
|
|
Name: "logs/osbuild.log",
|
|
Mode: 0644,
|
|
Size: int64(fileContents.Len()),
|
|
}
|
|
|
|
tw.WriteHeader(header)
|
|
io.Copy(tw, &fileContents)
|
|
tw.Close()
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
if compose.GetState() == common.CWaiting {
|
|
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 compose.GetState() == common.CRunning {
|
|
fmt.Fprintf(writer, "Running...\n")
|
|
return
|
|
}
|
|
|
|
resultReader, err := api.store.GetImageBuildResult(id, 0)
|
|
|
|
if err != nil {
|
|
errors := responseError{
|
|
ID: "ComposeError",
|
|
Msg: fmt.Sprintf("Opening log for compose %s failed", uuidString),
|
|
}
|
|
statusResponseError(writer, http.StatusBadRequest, errors)
|
|
return
|
|
}
|
|
|
|
var result common.ComposeResult
|
|
err = json.NewDecoder(resultReader).Decode(&result)
|
|
if err != nil {
|
|
errors := responseError{
|
|
ID: "ComposeError",
|
|
Msg: fmt.Sprintf("Parsing log for compose %s failed", uuidString),
|
|
}
|
|
statusResponseError(writer, http.StatusBadRequest, errors)
|
|
return
|
|
}
|
|
|
|
resultReader.Close()
|
|
|
|
result.Write(writer)
|
|
}
|
|
|
|
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{}}
|
|
|
|
composes := api.store.GetAllComposes()
|
|
for _, entry := range composesToComposeEntries(composes, nil, isRequestVersionAtLeast(params, 1)) {
|
|
switch entry.QueueStatus {
|
|
case common.IBFinished:
|
|
reply.Finished = append(reply.Finished, entry)
|
|
}
|
|
}
|
|
|
|
json.NewEncoder(writer).Encode(reply)
|
|
}
|
|
|
|
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{}}
|
|
|
|
composes := api.store.GetAllComposes()
|
|
for _, entry := range composesToComposeEntries(composes, nil, isRequestVersionAtLeast(params, 1)) {
|
|
switch entry.QueueStatus {
|
|
case common.IBFailed:
|
|
reply.Failed = append(reply.Failed, entry)
|
|
}
|
|
}
|
|
|
|
json.NewEncoder(writer).Encode(reply)
|
|
}
|
|
|
|
func (api *API) fetchPackageList() (rpmmd.PackageList, error) {
|
|
var repos []rpmmd.RepoConfig
|
|
for _, repo := range api.distro.Repositories(api.arch) {
|
|
repos = append(repos, repo)
|
|
}
|
|
for _, source := range api.store.GetAllSources() {
|
|
repos = append(repos, source.RepoConfig())
|
|
}
|
|
|
|
packages, _, err := api.rpmmd.FetchPackageList(repos)
|
|
return packages, err
|
|
}
|
|
|
|
func (api *API) depsolveBlueprint(bp *blueprint.Blueprint, clean bool) ([]rpmmd.PackageSpec, map[string]string, error) {
|
|
specs := make([]string, len(bp.Packages))
|
|
for i, pkg := range bp.Packages {
|
|
specs[i] = pkg.Name
|
|
// If a package has version "*" the package name suffix must be equal to "-*-*.*"
|
|
// Using just "-*" would find any other package containing the package name
|
|
if pkg.Version != "" && pkg.Version != "*" {
|
|
specs[i] += "-" + pkg.Version
|
|
} else if pkg.Version == "*" {
|
|
specs[i] += "-*-*.*"
|
|
}
|
|
}
|
|
|
|
var repos []rpmmd.RepoConfig
|
|
for _, repo := range api.distro.Repositories(api.arch) {
|
|
repos = append(repos, repo)
|
|
}
|
|
for _, source := range api.store.GetAllSources() {
|
|
repos = append(repos, source.RepoConfig())
|
|
}
|
|
|
|
return api.rpmmd.Depsolve(specs, nil, repos, clean)
|
|
}
|
|
|
|
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)
|
|
}
|