tree-wide: use a standard project layout
Keep main.go files under cmd/ and internal libraries under internal/. This will allow us to add more exutables under cmd/ (whereas only one was possible when main.go was kept in the root). Signed-off-by: Tom Gundersen <teg@jklm.no>
This commit is contained in:
parent
11af3f0c0b
commit
b60f580d92
6 changed files with 5 additions and 5 deletions
90
internal/rpmmd/repository.go
Normal file
90
internal/rpmmd/repository.go
Normal file
|
|
@ -0,0 +1,90 @@
|
|||
package rpmmd
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os/exec"
|
||||
"sort"
|
||||
"time"
|
||||
)
|
||||
|
||||
type RepoConfig struct {
|
||||
Id string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
BaseURL string `json:"baseurl,omitempty"`
|
||||
Metalink string `json:"metalink,omitempty"`
|
||||
MirrorList string `json:"mirrorlist,omitempty"`
|
||||
}
|
||||
|
||||
type PackageList []Package
|
||||
|
||||
type Package struct {
|
||||
Name string
|
||||
Summary string
|
||||
Description string
|
||||
URL string
|
||||
Epoch uint
|
||||
Version string
|
||||
Release string
|
||||
Arch string
|
||||
BuildTime time.Time
|
||||
License string
|
||||
}
|
||||
|
||||
type PackageSpec struct {
|
||||
Name string `json:"name"`
|
||||
Epoch uint `json:"epoch,omitempty"`
|
||||
Version string `json:"version,omitempty"`
|
||||
Release string `json:"release,omitempty"`
|
||||
Arch string `json:"arch,omitempty"`
|
||||
}
|
||||
|
||||
func runDNF(command string, arguments []string, result interface{}) error {
|
||||
argv := append([]string{"dnf-json", command}, arguments...)
|
||||
|
||||
cmd := exec.Command("python3", argv...)
|
||||
stdout, err := cmd.StdoutPipe()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = cmd.Start()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = json.NewDecoder(stdout).Decode(result)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return cmd.Wait()
|
||||
}
|
||||
|
||||
func FetchPackageList(repo RepoConfig) (PackageList, error) {
|
||||
var packages PackageList
|
||||
err := runDNF("dump", nil, &packages)
|
||||
return packages, err
|
||||
}
|
||||
|
||||
func Depsolve(specs ...string) ([]PackageSpec, error) {
|
||||
var dependencies []PackageSpec
|
||||
err := runDNF("depsolve", specs, &dependencies)
|
||||
return dependencies, err
|
||||
}
|
||||
|
||||
func (packages PackageList) Search(name string) (int, int) {
|
||||
first := sort.Search(len(packages), func(i int) bool {
|
||||
return packages[i].Name >= name
|
||||
})
|
||||
|
||||
if first == len(packages) || packages[first].Name != name {
|
||||
return first, 0
|
||||
}
|
||||
|
||||
last := first + 1
|
||||
for last < len(packages) && packages[last].Name == name {
|
||||
last++
|
||||
}
|
||||
|
||||
return first, last - first
|
||||
}
|
||||
543
internal/weldr/api.go
Normal file
543
internal/weldr/api.go
Normal file
|
|
@ -0,0 +1,543 @@
|
|||
package weldr
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"log"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/julienschmidt/httprouter"
|
||||
|
||||
"osbuild-composer/internal/rpmmd"
|
||||
)
|
||||
|
||||
type API struct {
|
||||
store *store
|
||||
|
||||
repo rpmmd.RepoConfig
|
||||
packages rpmmd.PackageList
|
||||
|
||||
logger *log.Logger
|
||||
router *httprouter.Router
|
||||
}
|
||||
|
||||
func New(repo rpmmd.RepoConfig, packages rpmmd.PackageList, logger *log.Logger, initialState []byte, stateChannel chan<- []byte) *API {
|
||||
api := &API{
|
||||
store: newStore(initialState, stateChannel),
|
||||
repo: repo,
|
||||
packages: packages,
|
||||
logger: logger,
|
||||
}
|
||||
|
||||
// sample blueprint on first run
|
||||
if initialState == nil {
|
||||
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.POST("/api/v0/blueprints/workspace", api.blueprintsWorkspaceHandler)
|
||||
api.router.DELETE("/api/v0/blueprints/delete/:blueprint", api.blueprintDeleteHandler)
|
||||
api.router.DELETE("/api/v0/blueprints/workspace/:blueprint", api.blueprintDeleteWorkspaceHandler)
|
||||
|
||||
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) {
|
||||
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 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.Id},
|
||||
})
|
||||
}
|
||||
|
||||
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.Id && 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.Id: 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))
|
||||
start := min(offset, total)
|
||||
n := min(limit, total-start)
|
||||
|
||||
modules := make([]modulesListModule, n)
|
||||
for i := uint(0); i < n; i++ {
|
||||
modules[i] = modulesListModule{api.packages[start+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: offset,
|
||||
Limit: limit,
|
||||
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 {
|
||||
var blueprint blueprint
|
||||
var changed bool
|
||||
if !api.store.getBlueprint(name, &blueprint, &changed) {
|
||||
statusResponseError(writer, http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
blueprints = append(blueprints, blueprint)
|
||||
changes = append(changes, change{changed, 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 {
|
||||
var blueprint blueprint
|
||||
if !api.store.getBlueprint(name, &blueprint, nil) {
|
||||
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) blueprintsWorkspaceHandler(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.pushBlueprintToWorkspace(blueprint)
|
||||
|
||||
statusResponseOK(writer)
|
||||
}
|
||||
|
||||
func (api *API) blueprintDeleteHandler(writer http.ResponseWriter, request *http.Request, params httprouter.Params) {
|
||||
api.store.deleteBlueprint(params.ByName("blueprint"))
|
||||
statusResponseOK(writer)
|
||||
}
|
||||
|
||||
func (api *API) blueprintDeleteWorkspaceHandler(writer http.ResponseWriter, request *http.Request, params httprouter.Params) {
|
||||
api.store.deleteBlueprintFromWorkspace(params.ByName("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)
|
||||
}
|
||||
137
internal/weldr/api_test.go
Normal file
137
internal/weldr/api_test.go
Normal file
|
|
@ -0,0 +1,137 @@
|
|||
package weldr_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"osbuild-composer/internal/rpmmd"
|
||||
"osbuild-composer/internal/weldr"
|
||||
)
|
||||
|
||||
var repo = rpmmd.RepoConfig{
|
||||
Id: "test",
|
||||
Name: "Test",
|
||||
BaseURL: "http://example.com/test/os",
|
||||
}
|
||||
|
||||
var packages = rpmmd.PackageList {
|
||||
{ Name: "package1" },
|
||||
{ Name: "package2" },
|
||||
}
|
||||
|
||||
func testRoute(t *testing.T, api *weldr.API, method, path, body string, expectedStatus int, expectedJSON string) {
|
||||
req := httptest.NewRequest(method, path, bytes.NewReader([]byte(body)))
|
||||
if method == "POST" {
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
}
|
||||
resp := httptest.NewRecorder()
|
||||
api.ServeHTTP(resp, req)
|
||||
|
||||
if resp.Code != expectedStatus {
|
||||
t.Errorf("%s: expected status %v, but got %v", path, expectedStatus, resp.Code)
|
||||
return
|
||||
}
|
||||
|
||||
replyJSON, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
t.Errorf("%s: could not read reponse body: %v", path, err)
|
||||
return
|
||||
}
|
||||
|
||||
if expectedJSON == "" {
|
||||
if len(replyJSON) != 0 {
|
||||
t.Errorf("%s: expected no response body, but got:\n%s", path, replyJSON)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
var reply, expected interface{}
|
||||
err = json.Unmarshal(replyJSON, &reply)
|
||||
if err != nil {
|
||||
t.Errorf("%s: %v\n%s", path, err, string(replyJSON))
|
||||
return
|
||||
}
|
||||
|
||||
if expectedJSON == "*" {
|
||||
return
|
||||
}
|
||||
|
||||
err = json.Unmarshal([]byte(expectedJSON), &expected)
|
||||
if err != nil {
|
||||
t.Errorf("%s: expected JSON is invalid: %v", path, err)
|
||||
return
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(reply, expected) {
|
||||
t.Errorf("%s: reply != expected:\n reply: %s\nexpected: %s", path, strings.TrimSpace(string(replyJSON)), expectedJSON)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func TestBasic(t *testing.T) {
|
||||
var cases = []struct {
|
||||
Path string
|
||||
ExpectedStatus int
|
||||
ExpectedJSON string
|
||||
}{
|
||||
{"/api/status", http.StatusOK, `{"api":1,"db_supported":true,"db_version":"0","schema_version":"0","backend":"osbuild-composer","build":"devel","messages":[]}`},
|
||||
|
||||
{"/api/v0/projects/source/list", http.StatusOK, `{"sources":["test"]}`},
|
||||
|
||||
{"/api/v0/projects/source/info", http.StatusNotFound, ``},
|
||||
{"/api/v0/projects/source/info/", http.StatusNotFound, ``},
|
||||
{"/api/v0/projects/source/info/foo", http.StatusBadRequest, `{"status":false,"errors":["repository not found: foo"]}`},
|
||||
{"/api/v0/projects/source/info/test", http.StatusOK, `{"sources":{"test":{"id":"test","name":"Test","type":"yum-baseurl","url":"http://example.com/test/os","check_gpg":true,"check_ssl":true,"system":true}}}`},
|
||||
{"/api/v0/projects/source/info/*", http.StatusOK, `{"sources":{"test":{"id":"test","name":"Test","type":"yum-baseurl","url":"http://example.com/test/os","check_gpg":true,"check_ssl":true,"system":true}}}`},
|
||||
|
||||
{"/api/v0/modules/list", http.StatusOK, `{"total":2,"offset":0,"limit":20,"modules":[{"name":"package1","group_type":"rpm"},{"name":"package2","group_type":"rpm"}]}`},
|
||||
{"/api/v0/modules/list/*", http.StatusOK, `{"total":2,"offset":0,"limit":20,"modules":[{"name":"package1","group_type":"rpm"},{"name":"package2","group_type":"rpm"}]}`},
|
||||
{"/api/v0/modules/list?offset=1", http.StatusOK, `{"total":2,"offset":1,"limit":20,"modules":[{"name":"package2","group_type":"rpm"}]}`},
|
||||
{"/api/v0/modules/list?limit=1", http.StatusOK, `{"total":2,"offset":0,"limit":1,"modules":[{"name":"package1","group_type":"rpm"}]}`},
|
||||
{"/api/v0/modules/list?limit=0", http.StatusOK, `{"total":2,"offset":0,"limit":0,"modules":[]}`},
|
||||
{"/api/v0/modules/list?offset=10&limit=10", http.StatusOK, `{"total":2,"offset":10,"limit":10,"modules":[]}`},
|
||||
{"/api/v0/modules/list/foo", http.StatusOK, `{"total":0,"offset":0,"limit":20,"modules":[]}`}, // returns empty list instead of an error for unknown packages
|
||||
{"/api/v0/modules/list/package2", http.StatusOK, `{"total":1,"offset":0,"limit":20,"modules":[{"name":"package2","group_type":"rpm"}]}`},
|
||||
{"/api/v0/modules/list/*package2*", http.StatusOK, `{"total":1,"offset":0,"limit":20,"modules":[{"name":"package2","group_type":"rpm"}]}`},
|
||||
{"/api/v0/modules/list/*package*", http.StatusOK, `{"total":2,"offset":0,"limit":20,"modules":[{"name":"package1","group_type":"rpm"},{"name":"package2","group_type":"rpm"}]}`},
|
||||
|
||||
{"/api/v0/modules/info", http.StatusNotFound, ``},
|
||||
{"/api/v0/modules/info/", http.StatusNotFound, ``},
|
||||
|
||||
{"/api/v0/blueprints/list", http.StatusOK, `{"total":1,"offset":0,"limit":1,"blueprints":["example"]}`},
|
||||
{"/api/v0/blueprints/info/", http.StatusNotFound, ``},
|
||||
{"/api/v0/blueprints/info/foo", http.StatusNotFound, `{"status":false}`},
|
||||
{"/api/v0/blueprints/info/example", http.StatusOK, `*`},
|
||||
}
|
||||
|
||||
for _, c:= range cases {
|
||||
api := weldr.New(repo, packages, nil)
|
||||
testRoute(t, api, "GET", c.Path, ``, c.ExpectedStatus, c.ExpectedJSON)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBlueprints(t *testing.T) {
|
||||
api := weldr.New(repo, packages, nil)
|
||||
|
||||
testRoute(t, api, "POST", "/api/v0/blueprints/new",
|
||||
`{"name":"test","description":"Test","packages":[{"name":"httpd","version":"2.4.*"}],"version":"0"}`,
|
||||
http.StatusOK, `{"status":true}`)
|
||||
|
||||
testRoute(t, api, "GET", "/api/v0/blueprints/info/test", ``,
|
||||
http.StatusOK, `{"blueprints":[{"name":"test","description":"Test","modules":[],"packages":[{"name":"httpd","version":"2.4.*"}],"version":"0"}],
|
||||
"changes":[{"name":"test","changed":false}], "errors":[]}`)
|
||||
|
||||
testRoute(t, api, "POST", "/api/v0/blueprints/workspace",
|
||||
`{"name":"test","description":"Test","packages":[{"name":"systemd","version":"123"}],"version":"0"}`,
|
||||
http.StatusOK, `{"status":true}`)
|
||||
|
||||
testRoute(t, api, "GET", "/api/v0/blueprints/info/test", ``,
|
||||
http.StatusOK, `{"blueprints":[{"name":"test","description":"Test","modules":[],"packages":[{"name":"systemd","version":"123"}],"version":"0"}],
|
||||
"changes":[{"name":"test","changed":true}], "errors":[]}`)
|
||||
}
|
||||
135
internal/weldr/store.go
Normal file
135
internal/weldr/store.go
Normal file
|
|
@ -0,0 +1,135 @@
|
|||
package weldr
|
||||
|
||||
import (
|
||||
"log"
|
||||
"encoding/json"
|
||||
"sort"
|
||||
"sync"
|
||||
)
|
||||
|
||||
type store struct {
|
||||
Blueprints map[string]blueprint `json:"blueprints"`
|
||||
Workspace map[string]blueprint `json:"workspace"`
|
||||
|
||||
mu sync.RWMutex // protects all fields
|
||||
stateChannel chan<- []byte
|
||||
}
|
||||
|
||||
type blueprint struct {
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description,omitempty"`
|
||||
Version string `json:"version,omitempty"`
|
||||
Packages []blueprintPackage `json:"packages"`
|
||||
Modules []blueprintPackage `json:"modules"`
|
||||
}
|
||||
|
||||
type blueprintPackage struct {
|
||||
Name string `json:"name"`
|
||||
Version string `json:"version,omitempty"`
|
||||
}
|
||||
|
||||
func newStore(initialState []byte, stateChannel chan<- []byte) *store {
|
||||
var s store
|
||||
|
||||
if initialState != nil {
|
||||
err := json.Unmarshal(initialState, &s)
|
||||
if err != nil {
|
||||
log.Fatalf("invalid initial state: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
if s.Blueprints == nil {
|
||||
s.Blueprints = make(map[string]blueprint)
|
||||
}
|
||||
if s.Workspace == nil {
|
||||
s.Workspace = make(map[string]blueprint)
|
||||
}
|
||||
s.stateChannel = stateChannel
|
||||
|
||||
return &s
|
||||
}
|
||||
|
||||
func (s *store) change(f func()) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
f()
|
||||
|
||||
if s.stateChannel != nil {
|
||||
serialized, err := json.Marshal(s)
|
||||
if err != nil {
|
||||
// we ought to know all types that go into the store
|
||||
panic(err)
|
||||
}
|
||||
|
||||
s.stateChannel <- serialized
|
||||
}
|
||||
}
|
||||
|
||||
func (s *store) listBlueprints() []string {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
|
||||
names := make([]string, 0, len(s.Blueprints))
|
||||
for name := range s.Blueprints {
|
||||
names = append(names, name)
|
||||
}
|
||||
sort.Strings(names)
|
||||
|
||||
return names
|
||||
}
|
||||
|
||||
func (s *store) getBlueprint(name string, bp *blueprint, changed *bool) bool {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
|
||||
var inWorkspace bool
|
||||
*bp, inWorkspace = s.Workspace[name]
|
||||
if !inWorkspace {
|
||||
var ok bool
|
||||
*bp, ok = s.Blueprints[name]
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// cockpit-composer cannot deal with missing "packages" or "modules"
|
||||
if bp.Packages == nil {
|
||||
bp.Packages = []blueprintPackage{}
|
||||
}
|
||||
if bp.Modules == nil {
|
||||
bp.Modules = []blueprintPackage{}
|
||||
}
|
||||
|
||||
if changed != nil {
|
||||
*changed = inWorkspace
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func (s *store) pushBlueprint(bp blueprint) {
|
||||
s.change(func() {
|
||||
delete(s.Workspace, bp.Name)
|
||||
s.Blueprints[bp.Name] = bp
|
||||
})
|
||||
}
|
||||
|
||||
func (s *store) pushBlueprintToWorkspace(bp blueprint) {
|
||||
s.change(func() {
|
||||
s.Workspace[bp.Name] = bp
|
||||
})
|
||||
}
|
||||
|
||||
func (s *store) deleteBlueprint(name string) {
|
||||
s.change(func() {
|
||||
delete(s.Workspace, name)
|
||||
delete(s.Blueprints, name)
|
||||
})
|
||||
}
|
||||
|
||||
func (s *store) deleteBlueprintFromWorkspace(name string) {
|
||||
s.change(func() {
|
||||
delete(s.Workspace, name)
|
||||
})
|
||||
}
|
||||
36
internal/weldr/util.go
Normal file
36
internal/weldr/util.go
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
package weldr
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/url"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
func parseOffsetAndLimit(query url.Values) (uint, uint, error) {
|
||||
var offset, limit uint64 = 0, 20
|
||||
var err error
|
||||
|
||||
if v := query.Get("offset"); v != "" {
|
||||
offset, err = strconv.ParseUint(v, 10, 64)
|
||||
if err != nil {
|
||||
return 0, 0, errors.New("invalid value for 'offset': " + err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
if v := query.Get("limit"); v != "" {
|
||||
limit, err = strconv.ParseUint(v, 10, 64)
|
||||
if err != nil {
|
||||
return 0, 0, errors.New("invalid value for 'limit': " + err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
return uint(offset), uint(limit), nil
|
||||
}
|
||||
|
||||
func min(a, b uint) uint {
|
||||
if a < b {
|
||||
return a
|
||||
} else {
|
||||
return b
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue