From 73655f600edbfdeafd00fb912fa9c50d7623eca4 Mon Sep 17 00:00:00 2001 From: Lars Karlitski Date: Sun, 15 Sep 2019 14:08:46 +0200 Subject: [PATCH] weldr: add support for the blueprint workspace --- weldr/api.go | 43 +++++++++++++++-- weldr/api_test.go | 117 ++++++++++++++++++++++++++++------------------ weldr/store.go | 52 +++++++++++++++++++-- 3 files changed, 159 insertions(+), 53 deletions(-) diff --git a/weldr/api.go b/weldr/api.go index 89f893237..1631a5763 100644 --- a/weldr/api.go +++ b/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/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) @@ -388,13 +391,14 @@ func (api *API) blueprintsInfoHandler(writer http.ResponseWriter, request *http. blueprints := []blueprint{} changes := []change{} for _, name := range names { - blueprint, ok := api.store.getBlueprint(name) - if !ok { + 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{false, blueprint.Name}) + changes = append(changes, change{changed, blueprint.Name}) } json.NewEncoder(writer).Encode(reply{ @@ -422,8 +426,8 @@ func (api *API) blueprintsDepsolveHandler(writer http.ResponseWriter, request *h blueprints := []entry{} for _, name := range names { - blueprint, ok := api.store.getBlueprint(name) - if !ok { + var blueprint blueprint + if !api.store.getBlueprint(name, &blueprint, nil) { statusResponseError(writer, http.StatusNotFound) return } @@ -475,6 +479,35 @@ func (api *API) blueprintsNewHandler(writer http.ResponseWriter, request *http.R 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"` diff --git a/weldr/api_test.go b/weldr/api_test.go index 496a8d8cf..b333340f8 100644 --- a/weldr/api_test.go +++ b/weldr/api_test.go @@ -1,6 +1,7 @@ package weldr_test import ( + "bytes" "encoding/json" "io/ioutil" "net/http" @@ -24,7 +25,56 @@ var packages = rpmmd.PackageList { { 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 { Path string ExpectedStatus int @@ -61,50 +111,27 @@ func TestAPI(t *testing.T) { } for _, c:= range cases { - req := httptest.NewRequest("GET", c.Path, nil) - resp := httptest.NewRecorder() - api := weldr.New(repo, packages, nil) - api.ServeHTTP(resp, req) - - 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 - } + 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":[]}`) +} diff --git a/weldr/store.go b/weldr/store.go index ea7fd729f..3b146ab12 100644 --- a/weldr/store.go +++ b/weldr/store.go @@ -7,6 +7,7 @@ import ( type store struct { Blueprints map[string]blueprint `json:"blueprints"` + Workspace map[string]blueprint `json:"workspace"` mu sync.RWMutex // protects all fields } @@ -27,6 +28,7 @@ type blueprintPackage struct { func newStore() *store { return &store{ Blueprints: make(map[string]blueprint), + Workspace: make(map[string]blueprint), } } @@ -43,17 +45,61 @@ func (s *store) listBlueprints() []string { return names } -func (s *store) getBlueprint(name string) (blueprint, bool) { +func (s *store) getBlueprint(name string, bp *blueprint, changed *bool) bool { s.mu.RLock() defer s.mu.RUnlock() - bp, ok := s.Blueprints[name] - return bp, ok + 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.mu.Lock() defer s.mu.Unlock() + delete(s.Workspace, bp.Name) 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) +}