diff options
-rw-r--r-- | src/orchestrator/api/api.go | 13 | ||||
-rw-r--r-- | src/orchestrator/api/apphandler.go | 224 | ||||
-rw-r--r-- | src/orchestrator/api/clusterhandler_test.go | 34 | ||||
-rw-r--r-- | src/orchestrator/api/composite_app_handler.go (renamed from src/orchestrator/api/compositeapphandler.go) | 1 | ||||
-rw-r--r-- | src/orchestrator/api/composite_profilehandler_test.go | 2 | ||||
-rw-r--r-- | src/orchestrator/api/controllerhandler_test.go | 6 | ||||
-rw-r--r-- | src/orchestrator/api/projecthandler_test.go | 6 | ||||
-rw-r--r-- | src/orchestrator/cmd/main.go | 2 | ||||
-rw-r--r-- | src/orchestrator/pkg/infra/db/mock.go | 4 | ||||
-rw-r--r-- | src/orchestrator/pkg/module/app.go | 202 | ||||
-rw-r--r-- | src/orchestrator/pkg/module/app_test.go | 327 | ||||
-rw-r--r-- | src/orchestrator/pkg/module/compositeapp.go | 8 | ||||
-rw-r--r-- | src/orchestrator/pkg/module/module.go | 2 |
13 files changed, 801 insertions, 30 deletions
diff --git a/src/orchestrator/api/api.go b/src/orchestrator/api/api.go index 9b33daf2..70b40d96 100644 --- a/src/orchestrator/api/api.go +++ b/src/orchestrator/api/api.go @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package api import ( @@ -25,6 +26,7 @@ var moduleClient *moduleLib.Client // NewRouter creates a router that registers the various urls that are supported func NewRouter(projectClient moduleLib.ProjectManager, compositeAppClient moduleLib.CompositeAppManager, + appClient moduleLib.AppManager, ControllerClient moduleLib.ControllerManager, clusterClient moduleLib.ClusterManager, genericPlacementIntentClient moduleLib.GenericPlacementIntentManager, @@ -73,6 +75,17 @@ func NewRouter(projectClient moduleLib.ProjectManager, 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") + if appClient == nil { + appClient = moduleClient.App + } + appHandler := appHandler{ + client: appClient, + } + + router.HandleFunc("/projects/{project-name}/composite-apps/{composite-app-name}/{version}/apps", appHandler.createAppHandler).Methods("POST") + router.HandleFunc("/projects/{project-name}/composite-apps/{composite-app-name}/{version}/apps/{app-name}", appHandler.getAppHandler).Methods("GET") + router.HandleFunc("/projects/{project-name}/composite-apps/{composite-app-name}/{version}/apps/{app-name}", appHandler.deleteAppHandler).Methods("DELETE") + if compositeProfileClient == nil { compositeProfileClient = moduleClient.CompositeProfile } diff --git a/src/orchestrator/api/apphandler.go b/src/orchestrator/api/apphandler.go new file mode 100644 index 00000000..3cd2dbc8 --- /dev/null +++ b/src/orchestrator/api/apphandler.go @@ -0,0 +1,224 @@ +/* + * 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/base64" + "encoding/json" + "io" + "io/ioutil" + "mime" + "mime/multipart" + "net/http" + "net/textproto" + + moduleLib "github.com/onap/multicloud-k8s/src/orchestrator/pkg/module" + "github.com/onap/multicloud-k8s/src/orchestrator/utils" + + "github.com/gorilla/mux" +) + +// appHandler to store backend implementations objects +// Also simplifies mocking for unit testing purposes +type appHandler struct { + // Interface that implements App operations + // We will set this variable with a mock interface for testing + client moduleLib.AppManager +} + +// createAppHandler handles creation of the App entry in the database +// This is a multipart handler. See following example curl request +// curl -X POST http://localhost:9015/v2/projects/sampleProject/composite-apps/sampleCompositeApp/v1/apps \ +// -F "metadata={\"metadata\":{\"name\":\"app\",\"description\":\"sample app\",\"UserData1\":\"data1\",\"UserData2\":\"data2\"}};type=application/json" \ +// -F file=@/pathToFile + +func (h appHandler) createAppHandler(w http.ResponseWriter, r *http.Request) { + var a moduleLib.App + var ac moduleLib.AppContent + + // Implemenation using multipart form + // Set Max size to 16mb here + err := r.ParseMultipartForm(16777216) + if err != nil { + http.Error(w, err.Error(), http.StatusUnprocessableEntity) + return + } + + jsn := bytes.NewBuffer([]byte(r.FormValue("metadata"))) + err = json.NewDecoder(jsn).Decode(&a) + 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 a.Metadata.Name == "" { + http.Error(w, "Missing name in POST request", http.StatusBadRequest) + return + } + + //Read the file section and ignore the header + file, _, err := r.FormFile("file") + if err != nil { + http.Error(w, "Unable to process file", http.StatusUnprocessableEntity) + return + } + + defer file.Close() + + //Convert the file content to base64 for storage + content, err := ioutil.ReadAll(file) + if err != nil { + http.Error(w, "Unable to read file", http.StatusUnprocessableEntity) + return + } + + err = utils.IsTarGz(bytes.NewBuffer(content)) + if err != nil { + http.Error(w, "Error in file format", http.StatusUnprocessableEntity) + return + } + + ac.FileContent = base64.StdEncoding.EncodeToString(content) + + vars := mux.Vars(r) + projectName := vars["project-name"] + compositeAppName := vars["composite-app-name"] + compositeAppVersion := vars["version"] + + ret, err := h.client.CreateApp(a, ac, projectName, compositeAppName, compositeAppVersion) + 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 + } +} + +// getAppHandler handles GET operations on a particular App Name +// Returns an app +func (h appHandler) getAppHandler(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + projectName := vars["project-name"] + compositeAppName := vars["composite-app-name"] + compositeAppVersion := vars["version"] + name := vars["app-name"] + + accepted, _, err := mime.ParseMediaType(r.Header.Get("Accept")) + if err != nil { + http.Error(w, err.Error(), http.StatusNotAcceptable) + return + } + + var retApp moduleLib.App + var retAppContent moduleLib.AppContent + + retApp, err = h.client.GetApp(name, projectName, compositeAppName, compositeAppVersion) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + retAppContent, err = h.client.GetAppContent(name, projectName, compositeAppName, compositeAppVersion) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + switch accepted { + case "multipart/form-data": + mpw := multipart.NewWriter(w) + w.Header().Set("Content-Type", mpw.FormDataContentType()) + w.WriteHeader(http.StatusOK) + pw, err := mpw.CreatePart(textproto.MIMEHeader{"Content-Type": {"application/json"}, "Content-Disposition": {"form-data; name=metadata"}}) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + if err := json.NewEncoder(pw).Encode(retApp); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + pw, err = mpw.CreatePart(textproto.MIMEHeader{"Content-Type": {"application/octet-stream"}, "Content-Disposition": {"form-data; name=file; filename=fileContent"}}) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + fcBytes, err := base64.StdEncoding.DecodeString(retAppContent.FileContent) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + _, err = pw.Write(fcBytes) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + case "application/json": + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + err = json.NewEncoder(w).Encode(retApp) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + case "application/octet-stream": + w.Header().Set("Content-Type", "application/octet-stream") + w.WriteHeader(http.StatusOK) + fcBytes, err := base64.StdEncoding.DecodeString(retAppContent.FileContent) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + _, err = w.Write(fcBytes) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + default: + http.Error(w, "set Accept: multipart/form-data, application/json or application/octet-stream", http.StatusMultipleChoices) + return + } +} + +// deleteAppHandler handles DELETE operations on a particular App Name +func (h appHandler) deleteAppHandler(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + projectName := vars["project-name"] + compositeAppName := vars["composite-app-name"] + compositeAppVersion := vars["version"] + name := vars["app-name"] + + err := h.client.DeleteApp(name, projectName, compositeAppName, compositeAppVersion) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + w.WriteHeader(http.StatusNoContent) +} diff --git a/src/orchestrator/api/clusterhandler_test.go b/src/orchestrator/api/clusterhandler_test.go index 71afdd1b..e5161a42 100644 --- a/src/orchestrator/api/clusterhandler_test.go +++ b/src/orchestrator/api/clusterhandler_test.go @@ -229,7 +229,7 @@ func TestClusterProviderCreateHandler(t *testing.T) { for _, testCase := range testCases { t.Run(testCase.label, func(t *testing.T) { request := httptest.NewRequest("POST", "/v2/cluster-providers", testCase.reader) - resp := executeRequest(request, NewRouter(nil, nil, nil, testCase.clusterClient, nil, nil, nil, nil, nil, nil)) + resp := executeRequest(request, NewRouter(nil, nil, nil, nil, testCase.clusterClient, nil, nil, nil, nil, nil, nil)) //Check returned code if resp.StatusCode != testCase.expectedCode { @@ -307,7 +307,7 @@ func TestClusterProviderGetAllHandler(t *testing.T) { for _, testCase := range testCases { t.Run(testCase.label, func(t *testing.T) { request := httptest.NewRequest("GET", "/v2/cluster-providers", nil) - resp := executeRequest(request, NewRouter(nil, nil, nil, testCase.clusterClient, nil, nil, nil, nil, nil, nil)) + resp := executeRequest(request, NewRouter(nil, nil, nil, nil, testCase.clusterClient, nil, nil, nil, nil, nil, nil)) //Check returned code if resp.StatusCode != testCase.expectedCode { @@ -377,7 +377,7 @@ func TestClusterProviderGetHandler(t *testing.T) { for _, testCase := range testCases { t.Run(testCase.label, func(t *testing.T) { request := httptest.NewRequest("GET", "/v2/cluster-providers/"+testCase.name, nil) - resp := executeRequest(request, NewRouter(nil, nil, nil, testCase.clusterClient, nil, nil, nil, nil, nil, nil)) + resp := executeRequest(request, NewRouter(nil, nil, nil, nil, testCase.clusterClient, nil, nil, nil, nil, nil, nil)) //Check returned code if resp.StatusCode != testCase.expectedCode { @@ -426,7 +426,7 @@ func TestClusterProviderDeleteHandler(t *testing.T) { for _, testCase := range testCases { t.Run(testCase.label, func(t *testing.T) { request := httptest.NewRequest("DELETE", "/v2/cluster-providers/"+testCase.name, nil) - resp := executeRequest(request, NewRouter(nil, nil, nil, testCase.clusterClient, nil, nil, nil, nil, nil, nil)) + resp := executeRequest(request, NewRouter(nil, nil, nil, nil, testCase.clusterClient, nil, nil, nil, nil, nil, nil)) //Check returned code if resp.StatusCode != testCase.expectedCode { @@ -538,7 +538,7 @@ of clusterTest request := httptest.NewRequest("POST", "/v2/cluster-providers/clusterProvider1/clusters", bytes.NewBuffer(body.Bytes())) request.Header.Set("Content-Type", multiwr.FormDataContentType()) - resp := executeRequest(request, NewRouter(nil, nil, nil, testCase.clusterClient, nil, nil, nil, nil, nil, nil)) + resp := executeRequest(request, NewRouter(nil, nil, nil, nil, testCase.clusterClient, nil, nil, nil, nil, nil, nil)) //Check returned code if resp.StatusCode != testCase.expectedCode { @@ -625,7 +625,7 @@ func TestClusterGetAllHandler(t *testing.T) { for _, testCase := range testCases { t.Run(testCase.label, func(t *testing.T) { request := httptest.NewRequest("GET", "/v2/cluster-providers/clusterProvder1/clusters", nil) - resp := executeRequest(request, NewRouter(nil, nil, nil, testCase.clusterClient, nil, nil, nil, nil, nil, nil)) + resp := executeRequest(request, NewRouter(nil, nil, nil, nil, testCase.clusterClient, nil, nil, nil, nil, nil, nil)) //Check returned code if resp.StatusCode != testCase.expectedCode { @@ -706,7 +706,7 @@ func TestClusterGetHandler(t *testing.T) { if len(testCase.accept) > 0 { request.Header.Set("Accept", testCase.accept) } - resp := executeRequest(request, NewRouter(nil, nil, nil, testCase.clusterClient, nil, nil, nil, nil, nil, nil)) + resp := executeRequest(request, NewRouter(nil, nil, nil, nil, testCase.clusterClient, nil, nil, nil, nil, nil, nil)) //Check returned code if resp.StatusCode != testCase.expectedCode { @@ -784,7 +784,7 @@ of clusterTest if len(testCase.accept) > 0 { request.Header.Set("Accept", testCase.accept) } - resp := executeRequest(request, NewRouter(nil, nil, nil, testCase.clusterClient, nil, nil, nil, nil, nil, nil)) + resp := executeRequest(request, NewRouter(nil, nil, nil, nil, testCase.clusterClient, nil, nil, nil, nil, nil, nil)) //Check returned code if resp.StatusCode != testCase.expectedCode { @@ -834,7 +834,7 @@ func TestClusterDeleteHandler(t *testing.T) { for _, testCase := range testCases { t.Run(testCase.label, func(t *testing.T) { request := httptest.NewRequest("DELETE", "/v2/cluster-providers/clusterProvider1/clusters/"+testCase.name, nil) - resp := executeRequest(request, NewRouter(nil, nil, nil, testCase.clusterClient, nil, nil, nil, nil, nil, nil)) + resp := executeRequest(request, NewRouter(nil, nil, nil, nil, testCase.clusterClient, nil, nil, nil, nil, nil, nil)) //Check returned code if resp.StatusCode != testCase.expectedCode { @@ -880,7 +880,7 @@ func TestClusterLabelCreateHandler(t *testing.T) { for _, testCase := range testCases { t.Run(testCase.label, func(t *testing.T) { request := httptest.NewRequest("POST", "/v2/cluster-providers/cp1/clusters/cl1/labels", testCase.reader) - resp := executeRequest(request, NewRouter(nil, nil, nil, testCase.clusterClient, nil, nil, nil, nil, nil, nil)) + resp := executeRequest(request, NewRouter(nil, nil, nil, nil, testCase.clusterClient, nil, nil, nil, nil, nil, nil)) //Check returned code if resp.StatusCode != testCase.expectedCode { @@ -944,7 +944,7 @@ func TestClusterLabelsGetHandler(t *testing.T) { for _, testCase := range testCases { t.Run(testCase.label, func(t *testing.T) { request := httptest.NewRequest("GET", "/v2/cluster-providers/cp1/clusters/cl1/labels", nil) - resp := executeRequest(request, NewRouter(nil, nil, nil, testCase.clusterClient, nil, nil, nil, nil, nil, nil)) + resp := executeRequest(request, NewRouter(nil, nil, nil, nil, testCase.clusterClient, nil, nil, nil, nil, nil, nil)) //Check returned code if resp.StatusCode != testCase.expectedCode { @@ -1004,7 +1004,7 @@ func TestClusterLabelGetHandler(t *testing.T) { for _, testCase := range testCases { t.Run(testCase.label, func(t *testing.T) { request := httptest.NewRequest("GET", "/v2/cluster-providers/clusterProvider1/clusters/cl1/labels/"+testCase.name, nil) - resp := executeRequest(request, NewRouter(nil, nil, nil, testCase.clusterClient, nil, nil, nil, nil, nil, nil)) + resp := executeRequest(request, NewRouter(nil, nil, nil, nil, testCase.clusterClient, nil, nil, nil, nil, nil, nil)) //Check returned code if resp.StatusCode != testCase.expectedCode { @@ -1053,7 +1053,7 @@ func TestClusterLabelDeleteHandler(t *testing.T) { for _, testCase := range testCases { t.Run(testCase.label, func(t *testing.T) { request := httptest.NewRequest("DELETE", "/v2/cluster-providers/cp1/clusters/cl1/labels/"+testCase.name, nil) - resp := executeRequest(request, NewRouter(nil, nil, nil, testCase.clusterClient, nil, nil, nil, nil, nil, nil)) + resp := executeRequest(request, NewRouter(nil, nil, nil, nil, testCase.clusterClient, nil, nil, nil, nil, nil, nil)) //Check returned code if resp.StatusCode != testCase.expectedCode { @@ -1144,7 +1144,7 @@ func TestClusterKvPairsCreateHandler(t *testing.T) { for _, testCase := range testCases { t.Run(testCase.label, func(t *testing.T) { request := httptest.NewRequest("POST", "/v2/cluster-providers/cp1/clusters/cl1/kv-pairs", testCase.reader) - resp := executeRequest(request, NewRouter(nil, nil, nil, testCase.clusterClient, nil, nil, nil, nil, nil, nil)) + resp := executeRequest(request, NewRouter(nil, nil, nil, nil, testCase.clusterClient, nil, nil, nil, nil, nil, nil)) //Check returned code if resp.StatusCode != testCase.expectedCode { @@ -1262,7 +1262,7 @@ func TestClusterKvPairsGetAllHandler(t *testing.T) { for _, testCase := range testCases { t.Run(testCase.label, func(t *testing.T) { request := httptest.NewRequest("GET", "/v2/cluster-providers/cp1/clusters/cl1/kv-pairs", nil) - resp := executeRequest(request, NewRouter(nil, nil, nil, testCase.clusterClient, nil, nil, nil, nil, nil, nil)) + resp := executeRequest(request, NewRouter(nil, nil, nil, nil, testCase.clusterClient, nil, nil, nil, nil, nil, nil)) //Check returned code if resp.StatusCode != testCase.expectedCode { @@ -1352,7 +1352,7 @@ func TestClusterKvPairsGetHandler(t *testing.T) { for _, testCase := range testCases { t.Run(testCase.label, func(t *testing.T) { request := httptest.NewRequest("GET", "/v2/cluster-providers/clusterProvider1/clusters/cl1/kv-pairs/"+testCase.name, nil) - resp := executeRequest(request, NewRouter(nil, nil, nil, testCase.clusterClient, nil, nil, nil, nil, nil, nil)) + resp := executeRequest(request, NewRouter(nil, nil, nil, nil, testCase.clusterClient, nil, nil, nil, nil, nil, nil)) //Check returned code if resp.StatusCode != testCase.expectedCode { @@ -1401,7 +1401,7 @@ func TestClusterKvPairsDeleteHandler(t *testing.T) { for _, testCase := range testCases { t.Run(testCase.label, func(t *testing.T) { request := httptest.NewRequest("DELETE", "/v2/cluster-providers/cp1/clusters/cl1/kv-pairs/"+testCase.name, nil) - resp := executeRequest(request, NewRouter(nil, nil, nil, testCase.clusterClient, nil, nil, nil, nil, nil, nil)) + resp := executeRequest(request, NewRouter(nil, nil, nil, nil, testCase.clusterClient, nil, nil, nil, nil, nil, nil)) //Check returned code if resp.StatusCode != testCase.expectedCode { diff --git a/src/orchestrator/api/compositeapphandler.go b/src/orchestrator/api/composite_app_handler.go index 42c72cdb..b54c488e 100644 --- a/src/orchestrator/api/compositeapphandler.go +++ b/src/orchestrator/api/composite_app_handler.go @@ -35,7 +35,6 @@ type compositeAppHandler struct { } // createHandler handles creation of the CompositeApp entry in the database -// This is a multipart handler func (h compositeAppHandler) createHandler(w http.ResponseWriter, r *http.Request) { var c moduleLib.CompositeApp diff --git a/src/orchestrator/api/composite_profilehandler_test.go b/src/orchestrator/api/composite_profilehandler_test.go index 360653c7..7c84f12a 100644 --- a/src/orchestrator/api/composite_profilehandler_test.go +++ b/src/orchestrator/api/composite_profilehandler_test.go @@ -128,7 +128,7 @@ func Test_compositeProfileHandler_createHandler(t *testing.T) { for _, testCase := range testCases { t.Run(testCase.label, func(t *testing.T) { request := httptest.NewRequest("POST", "/v2/projects/{project-name}/composite-apps/{composite-app-name}/{version}/composite-profiles", testCase.reader) - resp := executeRequest(request, NewRouter(nil, nil, nil, nil, nil, nil, nil, nil, testCase.cProfClient, nil)) + resp := executeRequest(request, NewRouter(nil, nil, nil, nil, nil, nil, nil, nil, nil, testCase.cProfClient, nil)) //Check returned code if resp.StatusCode != testCase.expectedCode { diff --git a/src/orchestrator/api/controllerhandler_test.go b/src/orchestrator/api/controllerhandler_test.go index ab0aeed8..3c543cb8 100644 --- a/src/orchestrator/api/controllerhandler_test.go +++ b/src/orchestrator/api/controllerhandler_test.go @@ -110,7 +110,7 @@ func TestControllerCreateHandler(t *testing.T) { 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, nil, nil, nil, nil, nil, nil, nil)) + resp := executeRequest(request, NewRouter(nil, nil, nil, testCase.controllerClient, nil, nil, nil, nil, nil, nil, nil)) //Check returned code if resp.StatusCode != testCase.expectedCode { @@ -173,7 +173,7 @@ func TestControllerGetHandler(t *testing.T) { 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, nil, nil, nil, nil, nil, nil, nil)) + resp := executeRequest(request, NewRouter(nil, nil, nil, testCase.controllerClient, nil, nil, nil, nil, nil, nil, nil)) //Check returned code if resp.StatusCode != testCase.expectedCode { @@ -222,7 +222,7 @@ func TestControllerDeleteHandler(t *testing.T) { 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, nil, nil, nil, nil, nil, nil, nil)) + resp := executeRequest(request, NewRouter(nil, nil, nil, testCase.controllerClient, nil, nil, nil, nil, nil, nil, nil)) //Check returned code if resp.StatusCode != testCase.expectedCode { diff --git a/src/orchestrator/api/projecthandler_test.go b/src/orchestrator/api/projecthandler_test.go index af40f961..5e820aa2 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, nil, nil, nil, nil, nil, nil, nil, nil)) + resp := executeRequest(request, NewRouter(testCase.projectClient, nil, nil, nil, nil, nil, nil, nil, nil, 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, nil, nil, nil, nil, nil, nil, nil, nil)) + resp := executeRequest(request, NewRouter(testCase.projectClient, nil, nil, nil, nil, nil, nil, nil, nil, 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, nil, nil, nil, nil, nil, nil, nil, nil)) + resp := executeRequest(request, NewRouter(testCase.projectClient, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil)) //Check returned code if resp.StatusCode != testCase.expectedCode { diff --git a/src/orchestrator/cmd/main.go b/src/orchestrator/cmd/main.go index f95c057e..001903a7 100644 --- a/src/orchestrator/cmd/main.go +++ b/src/orchestrator/cmd/main.go @@ -47,7 +47,7 @@ func main() { log.Fatalln("Exiting...") } - httpRouter := api.NewRouter(nil, nil, nil, nil, nil, nil, nil, nil, nil, nil) + httpRouter := api.NewRouter(nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil) loggedRouter := handlers.LoggingHandler(os.Stdout, httpRouter) log.Println("Starting Kubernetes Multicloud API") diff --git a/src/orchestrator/pkg/infra/db/mock.go b/src/orchestrator/pkg/infra/db/mock.go index 79366d10..e43be8fb 100644 --- a/src/orchestrator/pkg/infra/db/mock.go +++ b/src/orchestrator/pkg/infra/db/mock.go @@ -96,3 +96,7 @@ func (m *MockDB) Find(table string, key Key, tag string) ([][]byte, error) { func (m *MockDB) Delete(table string, key Key, tag string) error { return m.Err } + +func (m *MockDB) Remove(table string, key Key) error { + return m.Err +} diff --git a/src/orchestrator/pkg/module/app.go b/src/orchestrator/pkg/module/app.go new file mode 100644 index 00000000..c25a1b51 --- /dev/null +++ b/src/orchestrator/pkg/module/app.go @@ -0,0 +1,202 @@ +/* + * 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 governinog permissions and + * limitations under the License. + */ + +package module + +import ( + "encoding/json" + + "github.com/onap/multicloud-k8s/src/orchestrator/pkg/infra/db" + + pkgerrors "github.com/pkg/errors" +) + +// App contains metadata for Apps +type App struct { + Metadata AppMetaData `json:"metadata"` +} + +//AppMetaData contains the parameters needed for Apps +type AppMetaData struct { + Name string `json:"name"` + Description string `json:"description"` + UserData1 string `json:"userData1"` + UserData2 string `json:"userData2"` +} + +//AppContent contains fileContent +type AppContent struct { + FileContent string +} + +// AppKey is the key structure that is used in the database +type AppKey struct { + App string `json:"app"` + Project string `json:"project"` + CompositeApp string `json:"compositeapp"` + CompositeAppVersion string `json:"compositeappversion"` +} + +// We will use json marshalling to convert to string to +// preserve the underlying structure. +func (aK AppKey) String() string { + out, err := json.Marshal(aK) + if err != nil { + return "" + } + return string(out) +} + +// AppManager is an interface exposes the App functionality +type AppManager interface { + CreateApp(a App, ac AppContent, p string, cN string, cV string) (App, error) + GetApp(name string, p string, cN string, cV string) (App, error) + GetAppContent(name string, p string, cN string, cV string) (AppContent, error) + DeleteApp(name string, p string, cN string, cV string) error +} + +// AppClient implements the AppManager +// It will also be used to maintain some localized state +type AppClient struct { + storeName string + tagMeta, tagContent string +} + +// NewAppClient returns an instance of the AppClient +// which implements the AppManager +func NewAppClient() *AppClient { + return &AppClient{ + storeName: "orchestrator", + tagMeta: "appmetadata", + tagContent: "appcontent", + } +} + +// CreateApp creates a new collection based on the App +func (v *AppClient) CreateApp(a App, ac AppContent, p string, cN string, cV string) (App, error) { + + //Construct the composite key to select the entry + key := AppKey{ + App: a.Metadata.Name, + Project: p, + CompositeApp: cN, + CompositeAppVersion: cV, + } + + //Check if this App already exists + _, err := v.GetApp(a.Metadata.Name, p, cN, cV) + if err == nil { + return App{}, pkgerrors.New("App already exists") + } + + //Check if Project exists + _, err = NewProjectClient().GetProject(p) + if err != nil { + return App{}, pkgerrors.New("Unable to find the project") + } + + //check if CompositeApp with version exists + _, err = NewCompositeAppClient().GetCompositeApp(cN, cV, p) + if err != nil { + return App{}, pkgerrors.New("Unable to find the composite app with version") + } + + err = db.DBconn.Insert(v.storeName, key, nil, v.tagMeta, a) + if err != nil { + return App{}, pkgerrors.Wrap(err, "Creating DB Entry") + } + + err = db.DBconn.Insert(v.storeName, key, nil, v.tagContent, ac) + if err != nil { + return App{}, pkgerrors.Wrap(err, "Creating DB Entry") + } + + return a, nil +} + +// GetApp returns the App for corresponding name +func (v *AppClient) GetApp(name string, p string, cN string, cV string) (App, error) { + + //Construct the composite key to select the entry + key := AppKey{ + App: name, + Project: p, + CompositeApp: cN, + CompositeAppVersion: cV, + } + value, err := db.DBconn.Find(v.storeName, key, v.tagMeta) + if err != nil { + return App{}, pkgerrors.Wrap(err, "Get app") + } + + //value is a byte array + if value != nil { + app := App{} + err = db.DBconn.Unmarshal(value[0], &app) + if err != nil { + return App{}, pkgerrors.Wrap(err, "Unmarshaling Value") + } + return app, nil + } + + return App{}, pkgerrors.New("Error getting app") +} + +// GetAppContent returns content for corresponding app +func (v *AppClient) GetAppContent(name string, p string, cN string, cV string) (AppContent, error) { + + //Construct the composite key to select the entry + key := AppKey{ + App: name, + Project: p, + CompositeApp: cN, + CompositeAppVersion: cV, + } + value, err := db.DBconn.Find(v.storeName, key, v.tagContent) + if err != nil { + return AppContent{}, pkgerrors.Wrap(err, "Get app content") + } + + //value is a byte array + if value != nil { + ac := AppContent{} + err = db.DBconn.Unmarshal(value[0], &ac) + if err != nil { + return AppContent{}, pkgerrors.Wrap(err, "Unmarshaling Value") + } + return ac, nil + } + + return AppContent{}, pkgerrors.New("Error getting app content") +} + +// DeleteApp deletes the App from database +func (v *AppClient) DeleteApp(name string, p string, cN string, cV string) error { + + //Construct the composite key to select the entry + key := AppKey{ + App: name, + Project: p, + CompositeApp: cN, + CompositeAppVersion: cV, + } + err := db.DBconn.Remove(v.storeName, key) + if err != nil { + return pkgerrors.Wrap(err, "Delete App Entry;") + } + + return nil +} diff --git a/src/orchestrator/pkg/module/app_test.go b/src/orchestrator/pkg/module/app_test.go new file mode 100644 index 00000000..42c08ef6 --- /dev/null +++ b/src/orchestrator/pkg/module/app_test.go @@ -0,0 +1,327 @@ +/* + * 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 module + +import ( + "reflect" + "strings" + "testing" + + "github.com/onap/multicloud-k8s/src/orchestrator/pkg/infra/db" + pkgerrors "github.com/pkg/errors" + // pkgerrors "github.com/pkg/errors" +) + +func TestCreateApp(t *testing.T) { + testCases := []struct { + label string + inpApp App + inpAppContent AppContent + inpProject string + inpCompositeAppName string + inpCompositeAppVersion string + expectedError string + mockdb *db.MockDB + expected App + }{ + { + label: "Create App", + inpApp: App{ + Metadata: AppMetaData{ + Name: "testApp", + Description: "A sample app used for unit testing", + UserData1: "userData1", + UserData2: "userData2", + }, + }, + + inpAppContent: AppContent{ + FileContent: "Sample file content", + }, + inpProject: "testProject", + inpCompositeAppName: "testCompositeApp", + inpCompositeAppVersion: "v1", + expected: App{ + Metadata: AppMetaData{ + Name: "testApp", + Description: "A sample app used for unit testing", + UserData1: "userData1", + UserData2: "userData2", + }, + }, + expectedError: "", + mockdb: &db.MockDB{ + Items: map[string]map[string][]byte{ + ProjectKey{ProjectName: "testProject"}.String(): { + "projectmetadata": []byte( + "{" + + "\"metadata\": {" + + "\"Name\": \"testProject\"," + + "\"Description\": \"Test project for unit testing\"," + + "\"UserData1\": \"userData1\"," + + "\"UserData2\": \"userData2\"}" + + "}"), + }, + CompositeAppKey{CompositeAppName: "testCompositeApp", Version: "v1", Project: "testProject"}.String(): { + "compositeapp": []byte( + "{" + + "\"metadata\":{" + + "\"Name\":\"testCompositeApp\"," + + "\"Description\":\"Test CompositeApp for unit testing\"," + + "\"UserData1\":\"userData1\"," + + "\"UserData2\":\"userData2\"}," + + "\"spec\":{" + + "\"Version\":\"v1\"}" + + "}"), + }, + }, + }, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.label, func(t *testing.T) { + db.DBconn = testCase.mockdb + impl := NewAppClient() + got, err := impl.CreateApp(testCase.inpApp, testCase.inpAppContent, testCase.inpProject, testCase.inpCompositeAppName, testCase.inpCompositeAppVersion) + if err != nil { + if testCase.expectedError == "" { + t.Fatalf("Create returned an unexpected error %s", err) + } + if strings.Contains(err.Error(), testCase.expectedError) == false { + t.Fatalf("Create returned an unexpected error %s", err) + } + } else { + if reflect.DeepEqual(testCase.expected, got) == false { + t.Errorf("Create returned unexpected body: got %v;"+ + " expected %v", got, testCase.expected) + } + } + }) + } +} + +func TestGetApp(t *testing.T) { + + testCases := []struct { + label string + inpApp string + inpProject string + inpCompositeAppName string + inpCompositeAppVersion string + expectedError string + mockdb *db.MockDB + expected App + }{ + { + label: "Get Composite App", + inpApp: "testApp", + inpProject: "testProject", + inpCompositeAppName: "testCompositeApp", + inpCompositeAppVersion: "v1", + expected: App{ + Metadata: AppMetaData{ + Name: "testApp", + Description: "Test App for unit testing", + UserData1: "userData1", + UserData2: "userData2", + }, + }, + expectedError: "", + mockdb: &db.MockDB{ + Items: map[string]map[string][]byte{ + AppKey{App: "testApp", Project: "testProject", CompositeApp: "testCompositeApp", CompositeAppVersion: "v1"}.String(): { + "appmetadata": []byte( + "{" + + "\"metadata\": {" + + "\"Name\": \"testApp\"," + + "\"Description\": \"Test App for unit testing\"," + + "\"UserData1\": \"userData1\"," + + "\"UserData2\": \"userData2\"}" + + "}"), + "appcontent": []byte( + "{" + + "\"FileContent\": \"sample file content\"" + + "}"), + }, + }, + }, + }, + { + label: "Get Error", + expectedError: "DB Error", + mockdb: &db.MockDB{ + Err: pkgerrors.New("DB Error"), + }, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.label, func(t *testing.T) { + db.DBconn = testCase.mockdb + impl := NewAppClient() + got, err := impl.GetApp(testCase.inpApp, testCase.inpProject, testCase.inpCompositeAppName, testCase.inpCompositeAppVersion) + if err != nil { + if testCase.expectedError == "" { + t.Fatalf("Get returned an unexpected error: %s", err) + } + if strings.Contains(err.Error(), testCase.expectedError) == false { + t.Fatalf("Get returned an unexpected error: %s", err) + } + } else { + if reflect.DeepEqual(testCase.expected, got) == false { + t.Errorf("Get returned unexpected body: got %v;"+ + " expected %v", got, testCase.expected) + } + } + }) + } +} + +func TestGetAppContent(t *testing.T) { + + testCases := []struct { + label string + inpApp string + inpProject string + inpCompositeAppName string + inpCompositeAppVersion string + expectedError string + mockdb *db.MockDB + expected AppContent + }{ + { + label: "Get App content", + inpApp: "testApp", + inpProject: "testProject", + inpCompositeAppName: "testCompositeApp", + inpCompositeAppVersion: "v1", + expected: AppContent{ + FileContent: "Samplefilecontent", + }, + expectedError: "", + mockdb: &db.MockDB{ + Items: map[string]map[string][]byte{ + AppKey{App: "testApp", Project: "testProject", CompositeApp: "testCompositeApp", CompositeAppVersion: "v1"}.String(): { + "appmetadata": []byte( + "{" + + "\"metadata\": {" + + "\"Name\": \"testApp\"," + + "\"Description\": \"Test App for unit testing\"," + + "\"UserData1\": \"userData1\"," + + "\"UserData2\": \"userData2\"}" + + "}"), + "appcontent": []byte( + "{" + + "\"FileContent\": \"Samplefilecontent\"" + + "}"), + }, + }, + }, + }, + { + label: "Get Error", + expectedError: "DB Error", + mockdb: &db.MockDB{ + Err: pkgerrors.New("DB Error"), + }, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.label, func(t *testing.T) { + db.DBconn = testCase.mockdb + impl := NewAppClient() + got, err := impl.GetAppContent(testCase.inpApp, testCase.inpProject, testCase.inpCompositeAppName, testCase.inpCompositeAppVersion) + if err != nil { + if testCase.expectedError == "" { + t.Fatalf("Get returned an unexpected error: %s", err) + } + if strings.Contains(err.Error(), testCase.expectedError) == false { + t.Fatalf("Get returned an unexpected error: %s", err) + } + } else { + if reflect.DeepEqual(testCase.expected, got) == false { + t.Errorf("Get returned unexpected body: got %v;"+ + " expected %v", got, testCase.expected) + } + } + }) + } +} + +func TestDeleteApp(t *testing.T) { + + testCases := []struct { + label string + inpApp string + inpProject string + inpCompositeAppName string + inpCompositeAppVersion string + expectedError string + mockdb *db.MockDB + }{ + { + label: "Delete App", + inpApp: "testApp", + inpProject: "testProject", + inpCompositeAppName: "testCompositeApp", + inpCompositeAppVersion: "v1", + mockdb: &db.MockDB{ + Items: map[string]map[string][]byte{ + AppKey{App: "testApp", Project: "testProject", CompositeApp: "testCompositeApp", CompositeAppVersion: "v1"}.String(): { + "appmetadata": []byte( + "{" + + "\"metadata\": {" + + "\"Name\": \"testApp\"," + + "\"Description\": \"Test App for unit testing\"," + + "\"UserData1\": \"userData1\"," + + "\"UserData2\": \"userData2\"}" + + "}"), + "appcontent": []byte( + "{" + + "\"FileContent\": \"Samplefilecontent\"" + + "}"), + }, + }, + }, + }, + { + label: "Delete Error", + expectedError: "DB Error", + mockdb: &db.MockDB{ + Err: pkgerrors.New("DB Error"), + }, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.label, func(t *testing.T) { + db.DBconn = testCase.mockdb + impl := NewAppClient() + err := impl.DeleteApp(testCase.inpApp, testCase.inpProject, testCase.inpCompositeAppName, testCase.inpCompositeAppVersion) + if err != nil { + if testCase.expectedError == "" { + t.Fatalf("Delete returned an unexpected error %s", err) + } + if strings.Contains(err.Error(), testCase.expectedError) == false { + t.Fatalf("Delete returned an unexpected error %s", err) + } + } + }) + } +} diff --git a/src/orchestrator/pkg/module/compositeapp.go b/src/orchestrator/pkg/module/compositeapp.go index 74fbe0d5..59fbbab5 100644 --- a/src/orchestrator/pkg/module/compositeapp.go +++ b/src/orchestrator/pkg/module/compositeapp.go @@ -105,7 +105,7 @@ func (v *CompositeAppClient) CreateCompositeApp(c CompositeApp, p string) (Compo return CompositeApp{}, pkgerrors.New("Unable to find the project") } - err = db.DBconn.Create(v.storeName, key, v.tagMeta, c) + err = db.DBconn.Insert(v.storeName, key, nil, v.tagMeta, c) if err != nil { return CompositeApp{}, pkgerrors.Wrap(err, "Creating DB Entry") } @@ -122,7 +122,7 @@ func (v *CompositeAppClient) GetCompositeApp(name string, version string, p stri Version: version, Project: p, } - value, err := db.DBconn.Read(v.storeName, key, v.tagMeta) + value, err := db.DBconn.Find(v.storeName, key, v.tagMeta) if err != nil { return CompositeApp{}, pkgerrors.Wrap(err, "Get composite application") } @@ -130,7 +130,7 @@ func (v *CompositeAppClient) GetCompositeApp(name string, version string, p stri //value is a byte array if value != nil { compApp := CompositeApp{} - err = db.DBconn.Unmarshal(value, &compApp) + err = db.DBconn.Unmarshal(value[0], &compApp) if err != nil { return CompositeApp{}, pkgerrors.Wrap(err, "Unmarshaling Value") } @@ -149,7 +149,7 @@ func (v *CompositeAppClient) DeleteCompositeApp(name string, version string, p s Version: version, Project: p, } - err := db.DBconn.Delete(v.storeName, key, v.tagMeta) + err := db.DBconn.Remove(v.storeName, key) if err != nil { return pkgerrors.Wrap(err, "Delete CompositeApp Entry;") } diff --git a/src/orchestrator/pkg/module/module.go b/src/orchestrator/pkg/module/module.go index 8f2948dd..77a2f5bf 100644 --- a/src/orchestrator/pkg/module/module.go +++ b/src/orchestrator/pkg/module/module.go @@ -20,6 +20,7 @@ package module type Client struct { Project *ProjectClient CompositeApp *CompositeAppClient + App *AppClient Controller *ControllerClient Cluster *ClusterClient GenericPlacementIntent *GenericPlacementIntentClient @@ -36,6 +37,7 @@ func NewClient() *Client { c := &Client{} c.Project = NewProjectClient() c.CompositeApp = NewCompositeAppClient() + c.App = NewAppClient() c.Controller = NewControllerClient() c.Cluster = NewClusterClient() c.GenericPlacementIntent = NewGenericPlacementIntentClient() |