No need to have an interface we don't really use yet. This moves fetching the list of packages out of weldr, which will ease testing.
503 lines
14 KiB
Go
503 lines
14 KiB
Go
package weldr
|
|
|
|
import (
|
|
"encoding/json"
|
|
"log"
|
|
"net/http"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/julienschmidt/httprouter"
|
|
|
|
"osbuild-composer/rpmmd"
|
|
)
|
|
|
|
type API struct {
|
|
store *store
|
|
|
|
repo rpmmd.RepoConfig
|
|
packages rpmmd.PackageList
|
|
|
|
router *httprouter.Router
|
|
}
|
|
|
|
func New(repo rpmmd.RepoConfig, packages rpmmd.PackageList) *API {
|
|
api := &API{
|
|
store: newStore(),
|
|
repo: repo,
|
|
packages: packages,
|
|
}
|
|
|
|
// sample blueprint
|
|
api.store.pushBlueprint(blueprint{
|
|
Name: "example",
|
|
Description: "An Example",
|
|
Version: "1",
|
|
Packages: []blueprintPackage{{"httpd", "2.*"}},
|
|
Modules: []blueprintPackage{},
|
|
})
|
|
|
|
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/v0/projects/source/list", api.sourceListHandler)
|
|
api.router.GET("/api/v0/projects/source/info/:sources", api.sourceInfoHandler)
|
|
|
|
api.router.GET("/api/v0/modules/list", api.modulesListAllHandler)
|
|
api.router.GET("/api/v0/modules/list/:modules", api.modulesListHandler)
|
|
|
|
// these are the same, except that modules/info also includes dependencies
|
|
api.router.GET("/api/v0/modules/info/:modules", api.modulesInfoHandler)
|
|
api.router.GET("/api/v0/projects/info/:modules", api.modulesInfoHandler)
|
|
|
|
api.router.GET("/api/v0/blueprints/list", api.blueprintsListHandler)
|
|
api.router.GET("/api/v0/blueprints/info/:blueprints", api.blueprintsInfoHandler)
|
|
api.router.GET("/api/v0/blueprints/depsolve/:blueprints", api.blueprintsDepsolveHandler)
|
|
api.router.GET("/api/v0/blueprints/diff/:blueprint/:from/:to", api.blueprintsDiffHandler)
|
|
api.router.POST("/api/v0/blueprints/new", api.blueprintsNewHandler)
|
|
|
|
api.router.GET("/api/v0/compose/queue", api.composeQueueHandler)
|
|
api.router.GET("/api/v0/compose/finished", api.composeFinishedHandler)
|
|
api.router.GET("/api/v0/compose/failed", api.composeFailedHandler)
|
|
|
|
return api
|
|
}
|
|
|
|
func (api *API) ServeHTTP(writer http.ResponseWriter, request *http.Request) {
|
|
log.Println(request.URL.Path)
|
|
writer.Header().Set("Content-Type", "application/json; charset=utf-8")
|
|
api.router.ServeHTTP(writer, request)
|
|
}
|
|
|
|
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 statusResponseOK(writer http.ResponseWriter) {
|
|
type reply struct {
|
|
Status bool `json:"status"`
|
|
}
|
|
|
|
writer.WriteHeader(http.StatusOK)
|
|
json.NewEncoder(writer).Encode(reply{true})
|
|
}
|
|
|
|
func statusResponseError(writer http.ResponseWriter, code int, errors ...string) {
|
|
type reply struct {
|
|
Status bool `json:"status"`
|
|
Errors []string `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 uint `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, _ httprouter.Params) {
|
|
type reply struct {
|
|
Sources []string `json:"sources"`
|
|
}
|
|
|
|
json.NewEncoder(writer).Encode(reply{
|
|
Sources: []string{api.repo.Name},
|
|
})
|
|
}
|
|
|
|
func (api *API) sourceInfoHandler(writer http.ResponseWriter, request *http.Request, params httprouter.Params) {
|
|
// weldr uses a slightly different format than dnf to store repository
|
|
// configuration
|
|
type sourceConfig struct {
|
|
Id string `json:"id"`
|
|
Name string `json:"name"`
|
|
Type string `json:"type"`
|
|
URL string `json:"url"`
|
|
CheckGPG bool `json:"check_gpg"`
|
|
CheckSSL bool `json:"check_ssl"`
|
|
System bool `json:"system"`
|
|
}
|
|
type reply struct {
|
|
Sources map[string]sourceConfig `json:"sources"`
|
|
}
|
|
|
|
// we only have one repository
|
|
names := strings.Split(params.ByName("sources"), ",")
|
|
if names[0] != api.repo.Name && names[0] != "*" {
|
|
statusResponseError(writer, http.StatusBadRequest, "repository not found: "+names[0])
|
|
return
|
|
}
|
|
|
|
cfg := sourceConfig{
|
|
Id: api.repo.Id,
|
|
Name: api.repo.Name,
|
|
CheckGPG: true,
|
|
CheckSSL: true,
|
|
System: true,
|
|
}
|
|
|
|
if api.repo.BaseURL != "" {
|
|
cfg.URL = api.repo.BaseURL
|
|
cfg.Type = "yum-baseurl"
|
|
} else if api.repo.Metalink != "" {
|
|
cfg.URL = api.repo.Metalink
|
|
cfg.Type = "yum-metalink"
|
|
} else if api.repo.MirrorList != "" {
|
|
cfg.URL = api.repo.MirrorList
|
|
cfg.Type = "yum-mirrorlist"
|
|
}
|
|
|
|
json.NewEncoder(writer).Encode(reply{
|
|
Sources: map[string]sourceConfig{cfg.Name: cfg},
|
|
})
|
|
}
|
|
|
|
type modulesListModule struct {
|
|
Name string `json:"name"`
|
|
GroupType string `json:"group_type"`
|
|
}
|
|
|
|
type modulesListReply struct {
|
|
Total uint `json:"total"`
|
|
Offset uint `json:"offset"`
|
|
Limit uint `json:"limit"`
|
|
Modules []modulesListModule `json:"modules"`
|
|
}
|
|
|
|
func (api *API) modulesListAllHandler(writer http.ResponseWriter, request *http.Request, _ httprouter.Params) {
|
|
offset, limit, err := parseOffsetAndLimit(request.URL.Query())
|
|
if err != nil {
|
|
statusResponseError(writer, http.StatusBadRequest, "BadRequest: "+err.Error())
|
|
return
|
|
}
|
|
|
|
total := uint(len(api.packages))
|
|
offset = min(offset, total)
|
|
limit = min(limit, total-offset)
|
|
|
|
modules := make([]modulesListModule, limit)
|
|
for i := uint(0); i < limit; i++ {
|
|
modules[i] = modulesListModule{api.packages[offset+i].Name, "rpm"}
|
|
}
|
|
|
|
json.NewEncoder(writer).Encode(modulesListReply{
|
|
Total: total,
|
|
Offset: offset,
|
|
Limit: limit,
|
|
Modules: modules,
|
|
})
|
|
}
|
|
|
|
func (api *API) modulesListHandler(writer http.ResponseWriter, request *http.Request, params httprouter.Params) {
|
|
names := strings.Split(params.ByName("modules"), ",")
|
|
if names[0] == "" || names[0] == "*" {
|
|
api.modulesListAllHandler(writer, request, params)
|
|
return
|
|
}
|
|
|
|
offset, limit, err := parseOffsetAndLimit(request.URL.Query())
|
|
if err != nil {
|
|
statusResponseError(writer, http.StatusBadRequest, "BadRequest: "+err.Error())
|
|
return
|
|
}
|
|
|
|
// we don't support glob-matching, but cockpit-composer surrounds some
|
|
// queries with asterisks; this is crude, but solves that case
|
|
for i := range names {
|
|
names[i] = strings.ReplaceAll(names[i], "*", "")
|
|
}
|
|
|
|
modules := make([]modulesListModule, 0)
|
|
total := uint(0)
|
|
end := offset + limit
|
|
|
|
for _, pkg := range api.packages {
|
|
for _, name := range names {
|
|
if strings.Contains(pkg.Name, name) {
|
|
total += 1
|
|
if total > offset && total < end {
|
|
modules = append(modules, modulesListModule{pkg.Name, "rpm"})
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
json.NewEncoder(writer).Encode(modulesListReply{
|
|
Total: total,
|
|
Offset: min(offset, total),
|
|
Limit: min(limit, total-offset),
|
|
Modules: modules,
|
|
})
|
|
}
|
|
|
|
func (api *API) modulesInfoHandler(writer http.ResponseWriter, request *http.Request, params httprouter.Params) {
|
|
type source struct {
|
|
License string `json:"license"`
|
|
Version string `json:"version"`
|
|
}
|
|
type build struct {
|
|
Arch string `json:"arch"`
|
|
BuildTime time.Time `json:"build_time"`
|
|
Epoch uint `json:"epoch"`
|
|
Release string `json:"release"`
|
|
Source source `json:"source"`
|
|
}
|
|
type project struct {
|
|
Name string `json:"name"`
|
|
Summary string `json:"summary"`
|
|
Description string `json:"description"`
|
|
Homepage string `json:"homepage"`
|
|
Builds []build `json:"builds"`
|
|
Dependencies []rpmmd.PackageSpec `json:"dependencies,omitempty"`
|
|
}
|
|
type projectsReply struct {
|
|
Projects []project `json:"projects"`
|
|
}
|
|
type modulesReply struct {
|
|
Modules []project `json:"modules"`
|
|
}
|
|
|
|
// handle both projects/info and modules/info, the latter includes dependencies
|
|
modulesRequested := strings.HasPrefix(request.URL.Path, "/api/v0/modules")
|
|
|
|
names := strings.Split(params.ByName("modules"), ",")
|
|
if names[0] == "" {
|
|
statusResponseError(writer, http.StatusNotFound)
|
|
return
|
|
}
|
|
|
|
projects := make([]project, 0)
|
|
for _, name := range names {
|
|
first, n := api.packages.Search(name)
|
|
if n == 0 {
|
|
statusResponseError(writer, http.StatusNotFound)
|
|
return
|
|
}
|
|
|
|
// get basic info from the first package, but collect build
|
|
// information from all that have the same name
|
|
pkg := api.packages[first]
|
|
project := project{
|
|
Name: pkg.Name,
|
|
Summary: pkg.Summary,
|
|
Description: pkg.Description,
|
|
Homepage: pkg.URL,
|
|
}
|
|
|
|
project.Builds = make([]build, n)
|
|
for i, pkg := range api.packages[first : first+n] {
|
|
project.Builds[i] = build{
|
|
Arch: pkg.Arch,
|
|
BuildTime: pkg.BuildTime,
|
|
Epoch: pkg.Epoch,
|
|
Release: pkg.Release,
|
|
Source: source{pkg.License, pkg.Version},
|
|
}
|
|
}
|
|
|
|
if modulesRequested {
|
|
project.Dependencies, _ = rpmmd.Depsolve(pkg.Name)
|
|
}
|
|
|
|
projects = append(projects, project)
|
|
}
|
|
|
|
if modulesRequested {
|
|
json.NewEncoder(writer).Encode(modulesReply{projects})
|
|
} else {
|
|
json.NewEncoder(writer).Encode(projectsReply{projects})
|
|
}
|
|
}
|
|
|
|
func (api *API) blueprintsListHandler(writer http.ResponseWriter, request *http.Request, _ httprouter.Params) {
|
|
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 {
|
|
statusResponseError(writer, http.StatusBadRequest, "BadRequest: "+err.Error())
|
|
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) {
|
|
type change struct {
|
|
Changed bool `json:"changed"`
|
|
Name string `json:"name"`
|
|
}
|
|
type reply struct {
|
|
Blueprints []blueprint `json:"blueprints"`
|
|
Changes []change `json:"changes"`
|
|
Errors []string `json:"errors"`
|
|
}
|
|
|
|
names := strings.Split(params.ByName("blueprints"), ",")
|
|
if names[0] == "" {
|
|
statusResponseError(writer, http.StatusNotFound)
|
|
return
|
|
}
|
|
|
|
blueprints := []blueprint{}
|
|
changes := []change{}
|
|
for _, name := range names {
|
|
blueprint, ok := api.store.getBlueprint(name)
|
|
if !ok {
|
|
statusResponseError(writer, http.StatusNotFound)
|
|
return
|
|
}
|
|
blueprints = append(blueprints, blueprint)
|
|
changes = append(changes, change{false, blueprint.Name})
|
|
}
|
|
|
|
json.NewEncoder(writer).Encode(reply{
|
|
Blueprints: blueprints,
|
|
Changes: changes,
|
|
Errors: []string{},
|
|
})
|
|
}
|
|
|
|
func (api *API) blueprintsDepsolveHandler(writer http.ResponseWriter, request *http.Request, params httprouter.Params) {
|
|
type entry struct {
|
|
Blueprint blueprint `json:"blueprint"`
|
|
Dependencies []rpmmd.PackageSpec `json:"dependencies"`
|
|
}
|
|
type reply struct {
|
|
Blueprints []entry `json:"blueprints"`
|
|
Errors []string `json:"errors"`
|
|
}
|
|
|
|
names := strings.Split(params.ByName("blueprints"), ",")
|
|
if names[0] == "" {
|
|
statusResponseError(writer, http.StatusNotFound)
|
|
return
|
|
}
|
|
|
|
blueprints := []entry{}
|
|
for _, name := range names {
|
|
blueprint, ok := api.store.getBlueprint(name)
|
|
if !ok {
|
|
statusResponseError(writer, http.StatusNotFound)
|
|
return
|
|
}
|
|
|
|
specs := make([]string, len(blueprint.Packages))
|
|
for i, pkg := range blueprint.Packages {
|
|
specs[i] = pkg.Name
|
|
if pkg.Version != "" {
|
|
specs[i] += "-" + pkg.Version
|
|
}
|
|
}
|
|
|
|
dependencies, _ := rpmmd.Depsolve(specs...)
|
|
|
|
blueprints = append(blueprints, entry{blueprint, dependencies})
|
|
}
|
|
|
|
json.NewEncoder(writer).Encode(reply{
|
|
Blueprints: blueprints,
|
|
Errors: []string{},
|
|
})
|
|
}
|
|
|
|
func (api *API) blueprintsDiffHandler(writer http.ResponseWriter, request *http.Request, _ httprouter.Params) {
|
|
var reply struct {
|
|
Diff []interface{} `json:"diff"`
|
|
}
|
|
|
|
reply.Diff = make([]interface{}, 0)
|
|
json.NewEncoder(writer).Encode(reply)
|
|
}
|
|
|
|
func (api *API) blueprintsNewHandler(writer http.ResponseWriter, request *http.Request, _ httprouter.Params) {
|
|
contentType := request.Header["Content-Type"]
|
|
if len(contentType) != 1 || contentType[0] != "application/json" {
|
|
statusResponseError(writer, http.StatusUnsupportedMediaType, "blueprint must be json")
|
|
return
|
|
}
|
|
|
|
var blueprint blueprint
|
|
err := json.NewDecoder(request.Body).Decode(&blueprint)
|
|
if err != nil {
|
|
statusResponseError(writer, http.StatusBadRequest, "invalid blueprint: "+err.Error())
|
|
return
|
|
}
|
|
|
|
api.store.pushBlueprint(blueprint)
|
|
|
|
statusResponseOK(writer)
|
|
}
|
|
|
|
func (api *API) composeQueueHandler(writer http.ResponseWriter, request *http.Request, _ httprouter.Params) {
|
|
var reply struct {
|
|
New []interface{} `json:"new"`
|
|
Run []interface{} `json:"run"`
|
|
}
|
|
|
|
reply.New = make([]interface{}, 0)
|
|
reply.Run = make([]interface{}, 0)
|
|
|
|
json.NewEncoder(writer).Encode(reply)
|
|
}
|
|
|
|
func (api *API) composeFinishedHandler(writer http.ResponseWriter, request *http.Request, _ httprouter.Params) {
|
|
var reply struct {
|
|
Finished []interface{} `json:"finished"`
|
|
}
|
|
|
|
reply.Finished = make([]interface{}, 0)
|
|
|
|
json.NewEncoder(writer).Encode(reply)
|
|
}
|
|
|
|
func (api *API) composeFailedHandler(writer http.ResponseWriter, request *http.Request, _ httprouter.Params) {
|
|
var reply struct {
|
|
Failed []interface{} `json:"failed"`
|
|
}
|
|
|
|
reply.Failed = make([]interface{}, 0)
|
|
|
|
json.NewEncoder(writer).Encode(reply)
|
|
}
|