weldr: add support for the blueprint workspace
This commit is contained in:
parent
6089ebe077
commit
73655f600e
3 changed files with 159 additions and 53 deletions
43
weldr/api.go
43
weldr/api.go
|
|
@ -61,6 +61,9 @@ func New(repo rpmmd.RepoConfig, packages rpmmd.PackageList, logger *log.Logger)
|
||||||
api.router.GET("/api/v0/blueprints/depsolve/:blueprints", api.blueprintsDepsolveHandler)
|
api.router.GET("/api/v0/blueprints/depsolve/:blueprints", api.blueprintsDepsolveHandler)
|
||||||
api.router.GET("/api/v0/blueprints/diff/:blueprint/:from/:to", api.blueprintsDiffHandler)
|
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/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/queue", api.composeQueueHandler)
|
||||||
api.router.GET("/api/v0/compose/finished", api.composeFinishedHandler)
|
api.router.GET("/api/v0/compose/finished", api.composeFinishedHandler)
|
||||||
|
|
@ -388,13 +391,14 @@ func (api *API) blueprintsInfoHandler(writer http.ResponseWriter, request *http.
|
||||||
blueprints := []blueprint{}
|
blueprints := []blueprint{}
|
||||||
changes := []change{}
|
changes := []change{}
|
||||||
for _, name := range names {
|
for _, name := range names {
|
||||||
blueprint, ok := api.store.getBlueprint(name)
|
var blueprint blueprint
|
||||||
if !ok {
|
var changed bool
|
||||||
|
if !api.store.getBlueprint(name, &blueprint, &changed) {
|
||||||
statusResponseError(writer, http.StatusNotFound)
|
statusResponseError(writer, http.StatusNotFound)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
blueprints = append(blueprints, blueprint)
|
blueprints = append(blueprints, blueprint)
|
||||||
changes = append(changes, change{false, blueprint.Name})
|
changes = append(changes, change{changed, blueprint.Name})
|
||||||
}
|
}
|
||||||
|
|
||||||
json.NewEncoder(writer).Encode(reply{
|
json.NewEncoder(writer).Encode(reply{
|
||||||
|
|
@ -422,8 +426,8 @@ func (api *API) blueprintsDepsolveHandler(writer http.ResponseWriter, request *h
|
||||||
|
|
||||||
blueprints := []entry{}
|
blueprints := []entry{}
|
||||||
for _, name := range names {
|
for _, name := range names {
|
||||||
blueprint, ok := api.store.getBlueprint(name)
|
var blueprint blueprint
|
||||||
if !ok {
|
if !api.store.getBlueprint(name, &blueprint, nil) {
|
||||||
statusResponseError(writer, http.StatusNotFound)
|
statusResponseError(writer, http.StatusNotFound)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
@ -475,6 +479,35 @@ func (api *API) blueprintsNewHandler(writer http.ResponseWriter, request *http.R
|
||||||
statusResponseOK(writer)
|
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) {
|
func (api *API) composeQueueHandler(writer http.ResponseWriter, request *http.Request, _ httprouter.Params) {
|
||||||
var reply struct {
|
var reply struct {
|
||||||
New []interface{} `json:"new"`
|
New []interface{} `json:"new"`
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
package weldr_test
|
package weldr_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
@ -24,7 +25,56 @@ var packages = rpmmd.PackageList {
|
||||||
{ Name: "package2" },
|
{ Name: "package2" },
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestAPI(t *testing.T) {
|
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 {
|
var cases = []struct {
|
||||||
Path string
|
Path string
|
||||||
ExpectedStatus int
|
ExpectedStatus int
|
||||||
|
|
@ -61,50 +111,27 @@ func TestAPI(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, c:= range cases {
|
for _, c:= range cases {
|
||||||
req := httptest.NewRequest("GET", c.Path, nil)
|
|
||||||
resp := httptest.NewRecorder()
|
|
||||||
|
|
||||||
api := weldr.New(repo, packages, nil)
|
api := weldr.New(repo, packages, nil)
|
||||||
api.ServeHTTP(resp, req)
|
testRoute(t, api, "GET", c.Path, ``, c.ExpectedStatus, c.ExpectedJSON)
|
||||||
|
|
||||||
if resp.Code != c.ExpectedStatus {
|
|
||||||
t.Errorf("%s: unexpected status code: %v", c.Path, resp.Code)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
body, err := ioutil.ReadAll(resp.Body)
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("%s: could not read reponse body: %v", c.Path, err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if c.ExpectedJSON == "" {
|
|
||||||
if len(body) != 0 {
|
|
||||||
t.Errorf("%s: expected no response body, but got:\n%s", c.Path, body)
|
|
||||||
}
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
var reply, expected interface{}
|
|
||||||
err = json.Unmarshal(body, &reply)
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("%s: %v\n%s", c.Path, err, string(body))
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if c.ExpectedJSON == "*" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
err = json.Unmarshal([]byte(c.ExpectedJSON), &expected)
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("%s: expected JSON is invalid: %v", c.Path, err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if !reflect.DeepEqual(reply, expected) {
|
|
||||||
t.Errorf("%s: reply != expected:\n reply: %s\nexpected: %s", c.Path, strings.TrimSpace(string(body)), c.ExpectedJSON)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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":[]}`)
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ import (
|
||||||
|
|
||||||
type store struct {
|
type store struct {
|
||||||
Blueprints map[string]blueprint `json:"blueprints"`
|
Blueprints map[string]blueprint `json:"blueprints"`
|
||||||
|
Workspace map[string]blueprint `json:"workspace"`
|
||||||
|
|
||||||
mu sync.RWMutex // protects all fields
|
mu sync.RWMutex // protects all fields
|
||||||
}
|
}
|
||||||
|
|
@ -27,6 +28,7 @@ type blueprintPackage struct {
|
||||||
func newStore() *store {
|
func newStore() *store {
|
||||||
return &store{
|
return &store{
|
||||||
Blueprints: make(map[string]blueprint),
|
Blueprints: make(map[string]blueprint),
|
||||||
|
Workspace: make(map[string]blueprint),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -43,17 +45,61 @@ func (s *store) listBlueprints() []string {
|
||||||
return names
|
return names
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *store) getBlueprint(name string) (blueprint, bool) {
|
func (s *store) getBlueprint(name string, bp *blueprint, changed *bool) bool {
|
||||||
s.mu.RLock()
|
s.mu.RLock()
|
||||||
defer s.mu.RUnlock()
|
defer s.mu.RUnlock()
|
||||||
|
|
||||||
bp, ok := s.Blueprints[name]
|
var inWorkspace bool
|
||||||
return bp, ok
|
*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) {
|
func (s *store) pushBlueprint(bp blueprint) {
|
||||||
s.mu.Lock()
|
s.mu.Lock()
|
||||||
defer s.mu.Unlock()
|
defer s.mu.Unlock()
|
||||||
|
|
||||||
|
delete(s.Workspace, bp.Name)
|
||||||
s.Blueprints[bp.Name] = bp
|
s.Blueprints[bp.Name] = bp
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *store) pushBlueprintToWorkspace(bp blueprint) {
|
||||||
|
s.mu.Lock()
|
||||||
|
defer s.mu.Unlock()
|
||||||
|
|
||||||
|
s.Workspace[bp.Name] = bp
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *store) deleteBlueprint(name string) {
|
||||||
|
s.mu.Lock()
|
||||||
|
defer s.mu.Unlock()
|
||||||
|
|
||||||
|
delete(s.Workspace, name)
|
||||||
|
delete(s.Blueprints, name)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *store) deleteBlueprintFromWorkspace(name string) {
|
||||||
|
s.mu.Lock()
|
||||||
|
defer s.mu.Unlock()
|
||||||
|
|
||||||
|
delete(s.Workspace, name)
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue