From 95e9ddd082cb2ccd28d9d33ea2f8607bd5c793f5 Mon Sep 17 00:00:00 2001 From: Marcus G K Williams Date: Fri, 14 Feb 2020 13:29:37 -0800 Subject: Add Controller Register API to Orchestrator Add API allows Controllers to register themselves as gRPC servers consumed by the orchestrator. Issue-ID: MULTICLOUD-995 Signed-off-by: Marcus G K Williams Change-Id: I75946a4af711bf2e9d65f354923db494da667e70 --- src/orchestrator/api/api.go | 13 +- src/orchestrator/api/controllerhandler.go | 104 +++++++++++ src/orchestrator/api/controllerhandler_test.go | 233 +++++++++++++++++++++++++ src/orchestrator/api/projecthandler_test.go | 6 +- 4 files changed, 352 insertions(+), 4 deletions(-) create mode 100644 src/orchestrator/api/controllerhandler.go create mode 100644 src/orchestrator/api/controllerhandler_test.go (limited to 'src/orchestrator/api') diff --git a/src/orchestrator/api/api.go b/src/orchestrator/api/api.go index 1cb4299e..f0342433 100644 --- a/src/orchestrator/api/api.go +++ b/src/orchestrator/api/api.go @@ -22,7 +22,8 @@ import ( var moduleClient *moduleLib.Client // NewRouter creates a router that registers the various urls that are supported -func NewRouter(projectClient moduleLib.ProjectManager, compositeAppClient moduleLib.CompositeAppManager) *mux.Router { + +func NewRouter(projectClient moduleLib.ProjectManager, compositeAppClient moduleLib.CompositeAppManager, ControllerClient moduleLib.ControllerManager) *mux.Router { router := mux.NewRouter().PathPrefix("/v2").Subrouter() moduleClient = moduleLib.NewClient() @@ -32,6 +33,12 @@ func NewRouter(projectClient moduleLib.ProjectManager, compositeAppClient module projHandler := projectHandler{ client: projectClient, } + if ControllerClient == nil { + ControllerClient = moduleClient.Controller + } + controlHandler := controllerHandler{ + client: ControllerClient, + } router.HandleFunc("/projects", projHandler.createHandler).Methods("POST") router.HandleFunc("/projects/{project-name}", projHandler.getHandler).Methods("GET") router.HandleFunc("/projects/{project-name}", projHandler.deleteHandler).Methods("DELETE") @@ -47,5 +54,9 @@ func NewRouter(projectClient moduleLib.ProjectManager, compositeAppClient module router.HandleFunc("/projects/{project-name}/composite-apps/{composite-app-name}/{version}", compAppHandler.getHandler).Methods("GET") router.HandleFunc("/projects/{project-name}/composite-apps/{composite-app-name}/{version}", compAppHandler.deleteHandler).Methods("DELETE") + router.HandleFunc("/controllers", controlHandler.createHandler).Methods("POST") + router.HandleFunc("/controllers", controlHandler.createHandler).Methods("PUT") + router.HandleFunc("/controllers/{controller-name}", controlHandler.getHandler).Methods("GET") + router.HandleFunc("/controllers/{controller-name}", controlHandler.deleteHandler).Methods("DELETE") return router } diff --git a/src/orchestrator/api/controllerhandler.go b/src/orchestrator/api/controllerhandler.go new file mode 100644 index 00000000..4f98c023 --- /dev/null +++ b/src/orchestrator/api/controllerhandler.go @@ -0,0 +1,104 @@ +/* + * Copyright 2020 Intel Corporation, Inc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package api + +import ( + "encoding/json" + "io" + "net/http" + + "github.com/gorilla/mux" + moduleLib "github.com/onap/multicloud-k8s/src/orchestrator/pkg/module" +) + +// Used to store backend implementations objects +// Also simplifies mocking for unit testing purposes +type controllerHandler struct { + // Interface that implements controller operations + // We will set this variable with a mock interface for testing + client moduleLib.ControllerManager +} + +// Create handles creation of the controller entry in the database +func (h controllerHandler) createHandler(w http.ResponseWriter, r *http.Request) { + var m moduleLib.Controller + + err := json.NewDecoder(r.Body).Decode(&m) + switch { + case err == io.EOF: + http.Error(w, "Empty body", http.StatusBadRequest) + return + case err != nil: + http.Error(w, err.Error(), http.StatusUnprocessableEntity) + return + } + + // Name is required. + if m.Name == "" { + http.Error(w, "Missing name in POST request", http.StatusBadRequest) + return + } + + ret, err := h.client.CreateController(m) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusCreated) + err = json.NewEncoder(w).Encode(ret) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } +} + +// Get handles GET operations on a particular controller Name +// Returns a controller +func (h controllerHandler) getHandler(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + name := vars["controller-name"] + + ret, err := h.client.GetController(name) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + err = json.NewEncoder(w).Encode(ret) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } +} + +// Delete handles DELETE operations on a particular controller Name +func (h controllerHandler) deleteHandler(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + name := vars["controller-name"] + + err := h.client.DeleteController(name) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + w.WriteHeader(http.StatusNoContent) +} diff --git a/src/orchestrator/api/controllerhandler_test.go b/src/orchestrator/api/controllerhandler_test.go new file mode 100644 index 00000000..f0804107 --- /dev/null +++ b/src/orchestrator/api/controllerhandler_test.go @@ -0,0 +1,233 @@ +/* + * Copyright 2020 Intel Corporation, Inc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package api + +import ( + "bytes" + "encoding/json" + "io" + "net/http" + "net/http/httptest" + "reflect" + "testing" + + moduleLib "github.com/onap/multicloud-k8s/src/orchestrator/pkg/module" + + pkgerrors "github.com/pkg/errors" +) + +//Creating an embedded interface via anonymous variable +//This allows us to make mockDB satisfy the DatabaseConnection +//interface even if we are not implementing all the methods in it +type mockControllerManager struct { + // Items and err will be used to customize each test + // via a localized instantiation of mockControllerManager + Items []moduleLib.Controller + Err error +} + +func (m *mockControllerManager) CreateController(inp moduleLib.Controller) (moduleLib.Controller, error) { + if m.Err != nil { + return moduleLib.Controller{}, m.Err + } + + return m.Items[0], nil +} + +func (m *mockControllerManager) GetController(name string) (moduleLib.Controller, error) { + if m.Err != nil { + return moduleLib.Controller{}, m.Err + } + + return m.Items[0], nil +} + +func (m *mockControllerManager) DeleteController(name string) error { + return m.Err +} + +func TestControllerCreateHandler(t *testing.T) { + testCases := []struct { + label string + reader io.Reader + expected moduleLib.Controller + expectedCode int + controllerClient *mockControllerManager + }{ + { + label: "Missing Body Failure", + expectedCode: http.StatusBadRequest, + controllerClient: &mockControllerManager{}, + }, + { + label: "Create Controller", + expectedCode: http.StatusCreated, + reader: bytes.NewBuffer([]byte(`{ + "name":"testController", + "ip-address":"10.188.234.1", + "port":8080 + }`)), + expected: moduleLib.Controller{ + Name: "testController", + Host: "10.188.234.1", + Port: 8080, + }, + controllerClient: &mockControllerManager{ + //Items that will be returned by the mocked Client + Items: []moduleLib.Controller{ + { + Name: "testController", + Host: "10.188.234.1", + Port: 8080, + }, + }, + }, + }, + { + label: "Missing Controller Name in Request Body", + reader: bytes.NewBuffer([]byte(`{ + "description":"test description" + }`)), + expectedCode: http.StatusBadRequest, + controllerClient: &mockControllerManager{}, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.label, func(t *testing.T) { + request := httptest.NewRequest("POST", "/v2/controllers", testCase.reader) + resp := executeRequest(request, NewRouter(nil, nil, testCase.controllerClient)) + + //Check returned code + if resp.StatusCode != testCase.expectedCode { + t.Fatalf("Expected %d; Got: %d", testCase.expectedCode, resp.StatusCode) + } + + //Check returned body only if statusCreated + if resp.StatusCode == http.StatusCreated { + got := moduleLib.Controller{} + json.NewDecoder(resp.Body).Decode(&got) + + if reflect.DeepEqual(testCase.expected, got) == false { + t.Errorf("createHandler returned unexpected body: got %v;"+ + " expected %v", got, testCase.expected) + } + } + }) + } +} + +func TestControllerGetHandler(t *testing.T) { + + testCases := []struct { + label string + expected moduleLib.Controller + name, version string + expectedCode int + controllerClient *mockControllerManager + }{ + { + label: "Get Controller", + expectedCode: http.StatusOK, + expected: moduleLib.Controller{ + Name: "testController", + Host: "10.188.234.1", + Port: 8080, + }, + name: "testController", + controllerClient: &mockControllerManager{ + Items: []moduleLib.Controller{ + { + Name: "testController", + Host: "10.188.234.1", + Port: 8080, + }, + }, + }, + }, + { + label: "Get Non-Existing Controller", + expectedCode: http.StatusInternalServerError, + name: "nonexistingController", + controllerClient: &mockControllerManager{ + Items: []moduleLib.Controller{}, + Err: pkgerrors.New("Internal Error"), + }, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.label, func(t *testing.T) { + request := httptest.NewRequest("GET", "/v2/controllers/"+testCase.name, nil) + resp := executeRequest(request, NewRouter(nil, nil, testCase.controllerClient)) + + //Check returned code + if resp.StatusCode != testCase.expectedCode { + t.Fatalf("Expected %d; Got: %d", testCase.expectedCode, resp.StatusCode) + } + + //Check returned body only if statusOK + if resp.StatusCode == http.StatusOK { + got := moduleLib.Controller{} + json.NewDecoder(resp.Body).Decode(&got) + + if reflect.DeepEqual(testCase.expected, got) == false { + t.Errorf("listHandler returned unexpected body: got %v;"+ + " expected %v", got, testCase.expected) + } + } + }) + } +} + +func TestControllerDeleteHandler(t *testing.T) { + + testCases := []struct { + label string + name string + version string + expectedCode int + controllerClient *mockControllerManager + }{ + { + label: "Delete Controller", + expectedCode: http.StatusNoContent, + name: "testController", + controllerClient: &mockControllerManager{}, + }, + { + label: "Delete Non-Existing Controller", + expectedCode: http.StatusInternalServerError, + name: "testController", + controllerClient: &mockControllerManager{ + Err: pkgerrors.New("Internal Error"), + }, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.label, func(t *testing.T) { + request := httptest.NewRequest("DELETE", "/v2/controllers/"+testCase.name, nil) + resp := executeRequest(request, NewRouter(nil, nil, testCase.controllerClient)) + + //Check returned code + if resp.StatusCode != testCase.expectedCode { + t.Fatalf("Expected %d; Got: %d", testCase.expectedCode, resp.StatusCode) + } + }) + } +} diff --git a/src/orchestrator/api/projecthandler_test.go b/src/orchestrator/api/projecthandler_test.go index c76764b3..1e273349 100644 --- a/src/orchestrator/api/projecthandler_test.go +++ b/src/orchestrator/api/projecthandler_test.go @@ -119,7 +119,7 @@ func TestProjectCreateHandler(t *testing.T) { for _, testCase := range testCases { t.Run(testCase.label, func(t *testing.T) { request := httptest.NewRequest("POST", "/v2/projects", testCase.reader) - resp := executeRequest(request, NewRouter(testCase.projectClient, nil)) + resp := executeRequest(request, NewRouter(testCase.projectClient, nil, nil)) //Check returned code if resp.StatusCode != testCase.expectedCode { @@ -188,7 +188,7 @@ func TestProjectGetHandler(t *testing.T) { for _, testCase := range testCases { t.Run(testCase.label, func(t *testing.T) { request := httptest.NewRequest("GET", "/v2/projects/"+testCase.name, nil) - resp := executeRequest(request, NewRouter(testCase.projectClient, nil)) + resp := executeRequest(request, NewRouter(testCase.projectClient, nil, nil)) //Check returned code if resp.StatusCode != testCase.expectedCode { @@ -237,7 +237,7 @@ func TestProjectDeleteHandler(t *testing.T) { for _, testCase := range testCases { t.Run(testCase.label, func(t *testing.T) { request := httptest.NewRequest("DELETE", "/v2/projects/"+testCase.name, nil) - resp := executeRequest(request, NewRouter(testCase.projectClient, nil)) + resp := executeRequest(request, NewRouter(testCase.projectClient, nil, nil)) //Check returned code if resp.StatusCode != testCase.expectedCode { -- cgit 1.2.3-korg