From a75d489bbf87712371d67dce0753577bdacce0c3 Mon Sep 17 00:00:00 2001 From: Ritu Sood Date: Fri, 31 Jan 2020 23:29:51 -0800 Subject: Restructure code and create module library Restructures and moves code to make it aligned with the current design. https://wiki.onap.org/display/DW/Multi+Cluster+Application+Scheduler examples/example_module.go shows how to import and use modules from this package. Patch#2 Updated example Issue-ID: MULTICLOUD-871 Signed-off-by: Ritu Sood Change-Id: Ia1e9802a946a07dcca8f79f0e2250933ab3efa66 --- src/orchestrator/api/api.go | 19 +- src/orchestrator/api/projecthandler.go | 12 +- src/orchestrator/api/projecthandler_test.go | 38 +- src/orchestrator/cmd/main.go | 6 +- src/orchestrator/examples/example_module.go | 48 ++ src/orchestrator/go.sum | 1 + src/orchestrator/internal/auth/auth.go | 107 ---- src/orchestrator/internal/auth/auth_test.go | 47 -- src/orchestrator/internal/config/config.go | 130 ----- src/orchestrator/internal/config/config_test.go | 40 -- src/orchestrator/internal/db/README.md | 123 ----- src/orchestrator/internal/db/mock.go | 94 ---- src/orchestrator/internal/db/mongo.go | 396 -------------- src/orchestrator/internal/db/mongo_test.go | 597 ---------------------- src/orchestrator/internal/db/store.go | 106 ---- src/orchestrator/internal/db/store_test.go | 121 ----- src/orchestrator/internal/logutils/logger.go | 28 - src/orchestrator/internal/project/project.go | 133 ----- src/orchestrator/internal/project/project_test.go | 177 ------- src/orchestrator/pkg/infra/auth/auth.go | 107 ++++ src/orchestrator/pkg/infra/auth/auth_test.go | 47 ++ src/orchestrator/pkg/infra/config/config.go | 130 +++++ src/orchestrator/pkg/infra/config/config_test.go | 40 ++ src/orchestrator/pkg/infra/db/README.md | 123 +++++ src/orchestrator/pkg/infra/db/mock.go | 94 ++++ src/orchestrator/pkg/infra/db/mongo.go | 396 ++++++++++++++ src/orchestrator/pkg/infra/db/mongo_test.go | 597 ++++++++++++++++++++++ src/orchestrator/pkg/infra/db/store.go | 106 ++++ src/orchestrator/pkg/infra/db/store_test.go | 121 +++++ src/orchestrator/pkg/infra/logutils/logger.go | 28 + src/orchestrator/pkg/module/module.go | 34 ++ src/orchestrator/pkg/module/project.go | 133 +++++ src/orchestrator/pkg/module/project_test.go | 177 +++++++ 33 files changed, 2219 insertions(+), 2137 deletions(-) create mode 100644 src/orchestrator/examples/example_module.go delete mode 100644 src/orchestrator/internal/auth/auth.go delete mode 100644 src/orchestrator/internal/auth/auth_test.go delete mode 100644 src/orchestrator/internal/config/config.go delete mode 100644 src/orchestrator/internal/config/config_test.go delete mode 100644 src/orchestrator/internal/db/README.md delete mode 100644 src/orchestrator/internal/db/mock.go delete mode 100644 src/orchestrator/internal/db/mongo.go delete mode 100644 src/orchestrator/internal/db/mongo_test.go delete mode 100644 src/orchestrator/internal/db/store.go delete mode 100644 src/orchestrator/internal/db/store_test.go delete mode 100644 src/orchestrator/internal/logutils/logger.go delete mode 100644 src/orchestrator/internal/project/project.go delete mode 100644 src/orchestrator/internal/project/project_test.go create mode 100644 src/orchestrator/pkg/infra/auth/auth.go create mode 100644 src/orchestrator/pkg/infra/auth/auth_test.go create mode 100644 src/orchestrator/pkg/infra/config/config.go create mode 100644 src/orchestrator/pkg/infra/config/config_test.go create mode 100644 src/orchestrator/pkg/infra/db/README.md create mode 100644 src/orchestrator/pkg/infra/db/mock.go create mode 100644 src/orchestrator/pkg/infra/db/mongo.go create mode 100644 src/orchestrator/pkg/infra/db/mongo_test.go create mode 100644 src/orchestrator/pkg/infra/db/store.go create mode 100644 src/orchestrator/pkg/infra/db/store_test.go create mode 100644 src/orchestrator/pkg/infra/logutils/logger.go create mode 100644 src/orchestrator/pkg/module/module.go create mode 100644 src/orchestrator/pkg/module/project.go create mode 100644 src/orchestrator/pkg/module/project_test.go diff --git a/src/orchestrator/api/api.go b/src/orchestrator/api/api.go index 83f17bbe..e37b158a 100644 --- a/src/orchestrator/api/api.go +++ b/src/orchestrator/api/api.go @@ -10,29 +10,28 @@ 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 ( - "github.com/onap/multicloud-k8s/src/orchestrator/internal/project" + moduleLib "github.com/onap/multicloud-k8s/src/orchestrator/pkg/module" "github.com/gorilla/mux" ) - +var moduleClient *moduleLib.Client // NewRouter creates a router that registers the various urls that are supported -func NewRouter(projectClient project.ProjectManager) *mux.Router { +func NewRouter(projectClient moduleLib.ProjectManager) *mux.Router { router := mux.NewRouter().PathPrefix("/v2").Subrouter() - + moduleClient = moduleLib.NewClient() if projectClient == nil { - projectClient = project.NewProjectClient() + projectClient = moduleClient.Project } projHandler := projectHandler{ client: projectClient, } - router.HandleFunc("/project", projHandler.createHandler).Methods("POST") - router.HandleFunc("/project/{project-name}", projHandler.getHandler).Methods("GET") - router.HandleFunc("/project/{project-name}", projHandler.deleteHandler).Methods("DELETE") + 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") return router -} +} \ No newline at end of file diff --git a/src/orchestrator/api/projecthandler.go b/src/orchestrator/api/projecthandler.go index 30f21de3..1830b91d 100644 --- a/src/orchestrator/api/projecthandler.go +++ b/src/orchestrator/api/projecthandler.go @@ -21,7 +21,7 @@ import ( "io" "net/http" - "github.com/onap/multicloud-k8s/src/orchestrator/internal/project" + moduleLib "github.com/onap/multicloud-k8s/src/orchestrator/pkg/module" "github.com/gorilla/mux" ) @@ -31,12 +31,12 @@ import ( type projectHandler struct { // Interface that implements Project operations // We will set this variable with a mock interface for testing - client project.ProjectManager + client moduleLib.ProjectManager } // Create handles creation of the Project entry in the database func (h projectHandler) createHandler(w http.ResponseWriter, r *http.Request) { - var p project.Project + var p moduleLib.Project err := json.NewDecoder(r.Body).Decode(&p) switch { @@ -54,7 +54,7 @@ func (h projectHandler) createHandler(w http.ResponseWriter, r *http.Request) { return } - ret, err := h.client.Create(p) + ret, err := h.client.CreateProject(p) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return @@ -75,7 +75,7 @@ func (h projectHandler) getHandler(w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) name := vars["project-name"] - ret, err := h.client.Get(name) + ret, err := h.client.GetProject(name) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return @@ -95,7 +95,7 @@ func (h projectHandler) deleteHandler(w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) name := vars["project-name"] - err := h.client.Delete(name) + err := h.client.DeleteProject(name) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return diff --git a/src/orchestrator/api/projecthandler_test.go b/src/orchestrator/api/projecthandler_test.go index 2699f2e3..41f515d0 100644 --- a/src/orchestrator/api/projecthandler_test.go +++ b/src/orchestrator/api/projecthandler_test.go @@ -25,7 +25,7 @@ import ( "reflect" "testing" - "github.com/onap/multicloud-k8s/src/orchestrator/internal/project" + moduleLib "github.com/onap/multicloud-k8s/src/orchestrator/pkg/module" pkgerrors "github.com/pkg/errors" ) @@ -36,27 +36,27 @@ import ( type mockProjectManager struct { // Items and err will be used to customize each test // via a localized instantiation of mockProjectManager - Items []project.Project + Items []moduleLib.Project Err error } -func (m *mockProjectManager) Create(inp project.Project) (project.Project, error) { +func (m *mockProjectManager) CreateProject(inp moduleLib.Project) (moduleLib.Project, error) { if m.Err != nil { - return project.Project{}, m.Err + return moduleLib.Project{}, m.Err } return m.Items[0], nil } -func (m *mockProjectManager) Get(name string) (project.Project, error) { +func (m *mockProjectManager) GetProject(name string) (moduleLib.Project, error) { if m.Err != nil { - return project.Project{}, m.Err + return moduleLib.Project{}, m.Err } return m.Items[0], nil } -func (m *mockProjectManager) Delete(name string) error { +func (m *mockProjectManager) DeleteProject(name string) error { return m.Err } @@ -64,7 +64,7 @@ func TestProjectCreateHandler(t *testing.T) { testCases := []struct { label string reader io.Reader - expected project.Project + expected moduleLib.Project expectedCode int projectClient *mockProjectManager }{ @@ -80,13 +80,13 @@ func TestProjectCreateHandler(t *testing.T) { "project-name":"testProject", "description":"Test Project used for unit testing" }`)), - expected: project.Project{ + expected: moduleLib.Project{ ProjectName: "testProject", Description: "Test Project used for unit testing", }, projectClient: &mockProjectManager{ //Items that will be returned by the mocked Client - Items: []project.Project{ + Items: []moduleLib.Project{ { ProjectName: "testProject", Description: "Test Project used for unit testing", @@ -106,7 +106,7 @@ func TestProjectCreateHandler(t *testing.T) { for _, testCase := range testCases { t.Run(testCase.label, func(t *testing.T) { - request := httptest.NewRequest("POST", "/v2/project", testCase.reader) + request := httptest.NewRequest("POST", "/v2/projects", testCase.reader) resp := executeRequest(request, NewRouter(testCase.projectClient)) //Check returned code @@ -116,7 +116,7 @@ func TestProjectCreateHandler(t *testing.T) { //Check returned body only if statusCreated if resp.StatusCode == http.StatusCreated { - got := project.Project{} + got := moduleLib.Project{} json.NewDecoder(resp.Body).Decode(&got) if reflect.DeepEqual(testCase.expected, got) == false { @@ -132,7 +132,7 @@ func TestProjectGetHandler(t *testing.T) { testCases := []struct { label string - expected project.Project + expected moduleLib.Project name, version string expectedCode int projectClient *mockProjectManager @@ -140,13 +140,13 @@ func TestProjectGetHandler(t *testing.T) { { label: "Get Project", expectedCode: http.StatusOK, - expected: project.Project{ + expected: moduleLib.Project{ ProjectName: "testProject", Description: "A Test project for unit testing", }, name: "testProject", projectClient: &mockProjectManager{ - Items: []project.Project{ + Items: []moduleLib.Project{ { ProjectName: "testProject", Description: "A Test project for unit testing", @@ -159,7 +159,7 @@ func TestProjectGetHandler(t *testing.T) { expectedCode: http.StatusInternalServerError, name: "nonexistingproject", projectClient: &mockProjectManager{ - Items: []project.Project{}, + Items: []moduleLib.Project{}, Err: pkgerrors.New("Internal Error"), }, }, @@ -167,7 +167,7 @@ func TestProjectGetHandler(t *testing.T) { for _, testCase := range testCases { t.Run(testCase.label, func(t *testing.T) { - request := httptest.NewRequest("GET", "/v2/project/"+testCase.name, nil) + request := httptest.NewRequest("GET", "/v2/projects/"+testCase.name, nil) resp := executeRequest(request, NewRouter(testCase.projectClient)) //Check returned code @@ -177,7 +177,7 @@ func TestProjectGetHandler(t *testing.T) { //Check returned body only if statusOK if resp.StatusCode == http.StatusOK { - got := project.Project{} + got := moduleLib.Project{} json.NewDecoder(resp.Body).Decode(&got) if reflect.DeepEqual(testCase.expected, got) == false { @@ -216,7 +216,7 @@ func TestProjectDeleteHandler(t *testing.T) { for _, testCase := range testCases { t.Run(testCase.label, func(t *testing.T) { - request := httptest.NewRequest("DELETE", "/v2/project/"+testCase.name, nil) + request := httptest.NewRequest("DELETE", "/v2/projects/"+testCase.name, nil) resp := executeRequest(request, NewRouter(testCase.projectClient)) //Check returned code diff --git a/src/orchestrator/cmd/main.go b/src/orchestrator/cmd/main.go index 657d5bf5..fb8f26d6 100644 --- a/src/orchestrator/cmd/main.go +++ b/src/orchestrator/cmd/main.go @@ -23,9 +23,9 @@ import ( "time" "github.com/onap/multicloud-k8s/src/orchestrator/api" - "github.com/onap/multicloud-k8s/src/orchestrator/internal/auth" - "github.com/onap/multicloud-k8s/src/orchestrator/internal/config" - "github.com/onap/multicloud-k8s/src/orchestrator/internal/db" + "github.com/onap/multicloud-k8s/src/orchestrator/pkg/infra/auth" + "github.com/onap/multicloud-k8s/src/orchestrator/pkg/infra/config" + "github.com/onap/multicloud-k8s/src/orchestrator/pkg/infra/db" "github.com/gorilla/handlers" ) diff --git a/src/orchestrator/examples/example_module.go b/src/orchestrator/examples/example_module.go new file mode 100644 index 00000000..29ecdc23 --- /dev/null +++ b/src/orchestrator/examples/example_module.go @@ -0,0 +1,48 @@ +/* + * Copyright 2018 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 test + +import ( + moduleLib "github.com/onap/multicloud-k8s/src/orchestrator/pkg/module" + "log" +) + +// ExampleClient_Project to test Project +func ExampleClient_Project() { + // Get handle to the client + c := moduleLib.NewClient() + // Check if project is initialized + if c.Project == nil { + log.Println("Project is Uninitialized") + return + } + // Perform operations on Project Module + _, err := c.Project.CreateProject(moduleLib.Project{ProjectName: "test"}) + if err != nil { + log.Println(err) + return + } + _, err = c.Project.GetProject("test") + if err != nil { + log.Println(err) + return + } + err = c.Project.DeleteProject("test") + if err != nil { + log.Println(err) + } +} diff --git a/src/orchestrator/go.sum b/src/orchestrator/go.sum index 732bc280..d2015406 100644 --- a/src/orchestrator/go.sum +++ b/src/orchestrator/go.sum @@ -171,6 +171,7 @@ github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9 github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/munnerz/goautoneg v0.0.0-20120707110453-a547fc61f48d/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/onap/multicloud-k8s v0.0.0-20191115005109-f168ebb73d8d h1:3uFucXVv6gqa3H1u85CjoLOvGraREfD8/NL7m/9W9tc= +github.com/onap/multicloud-k8s v0.0.0-20200131010833-90e13d101cf0 h1:2qDo6s4pdg/g7Vj6QGrCK02EP4jjwVehgEObnAfipSM= github.com/onap/multicloud-k8s/src/k8splugin v0.0.0-20191115005109-f168ebb73d8d h1:ucIEjqzNVeFPnQofeuBfUqro0OnilX//fajEFxuLsgA= github.com/onap/multicloud-k8s/src/k8splugin v0.0.0-20191115005109-f168ebb73d8d/go.mod h1:EnQd/vQGZR1/55IihaHxiux4ZUig/zfXZux7bfmU0S8= github.com/onsi/ginkgo v0.0.0-20170829012221-11459a886d9c/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= diff --git a/src/orchestrator/internal/auth/auth.go b/src/orchestrator/internal/auth/auth.go deleted file mode 100644 index 3da8f2af..00000000 --- a/src/orchestrator/internal/auth/auth.go +++ /dev/null @@ -1,107 +0,0 @@ -/* - * Copyright 2018 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 auth - -import ( - "crypto/tls" - "crypto/x509" - "encoding/base64" - "encoding/pem" - "io/ioutil" - "log" - - pkgerrors "github.com/pkg/errors" -) - -// GetTLSConfig initializes a tlsConfig using the CA's certificate -// This config is then used to enable the server for mutual TLS -func GetTLSConfig(caCertFile string, certFile string, keyFile string) (*tls.Config, error) { - - // Initialize tlsConfig once - caCert, err := ioutil.ReadFile(caCertFile) - - if err != nil { - return nil, pkgerrors.Wrap(err, "Read CA Cert file") - } - - caCertPool := x509.NewCertPool() - caCertPool.AppendCertsFromPEM(caCert) - - tlsConfig := &tls.Config{ - // Change to RequireAndVerify once we have mandatory certs - ClientAuth: tls.VerifyClientCertIfGiven, - ClientCAs: caCertPool, - MinVersion: tls.VersionTLS12, - } - - certPEMBlk, err := readPEMBlock(certFile) - if err != nil { - return nil, pkgerrors.Wrap(err, "Read Cert File") - } - - keyPEMBlk, err := readPEMBlock(keyFile) - if err != nil { - return nil, pkgerrors.Wrap(err, "Read Key File") - } - - tlsConfig.Certificates = make([]tls.Certificate, 1) - tlsConfig.Certificates[0], err = tls.X509KeyPair(certPEMBlk, keyPEMBlk) - if err != nil { - return nil, pkgerrors.Wrap(err, "Load x509 cert and key") - } - - tlsConfig.BuildNameToCertificate() - return tlsConfig, nil -} - -func readPEMBlock(filename string) ([]byte, error) { - - pemData, err := ioutil.ReadFile(filename) - if err != nil { - return nil, pkgerrors.Wrap(err, "Read PEM File") - } - - pemBlock, rest := pem.Decode(pemData) - if len(rest) > 0 { - log.Println("Pemfile has extra data") - } - - if x509.IsEncryptedPEMBlock(pemBlock) { - password, err := ioutil.ReadFile(filename + ".pass") - if err != nil { - return nil, pkgerrors.Wrap(err, "Read Password File") - } - - pByte, err := base64.StdEncoding.DecodeString(string(password)) - if err != nil { - return nil, pkgerrors.Wrap(err, "Decode PEM Password") - } - - pemData, err = x509.DecryptPEMBlock(pemBlock, pByte) - if err != nil { - return nil, pkgerrors.Wrap(err, "Decrypt PEM Data") - } - var newPEMBlock pem.Block - newPEMBlock.Type = pemBlock.Type - newPEMBlock.Bytes = pemData - // Converting back to PEM from DER data you get from - // DecryptPEMBlock - pemData = pem.EncodeToMemory(&newPEMBlock) - } - - return pemData, nil -} diff --git a/src/orchestrator/internal/auth/auth_test.go b/src/orchestrator/internal/auth/auth_test.go deleted file mode 100644 index e41cb1ac..00000000 --- a/src/orchestrator/internal/auth/auth_test.go +++ /dev/null @@ -1,47 +0,0 @@ -/* -* Copyright 2018 TechMahindra -* -* 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 auth - -import ( - "crypto/tls" - "testing" -) - -//Unit test to varify GetTLSconfig func and varify the tls config min version to be 771 -//Assuming cert file name as auth_test.cert -func TestGetTLSConfig(t *testing.T) { - _, err := GetTLSConfig("filedoesnotexist.cert", "filedoesnotexist.cert", "filedoesnotexist.cert") - if err == nil { - t.Errorf("Test failed, expected error but got none") - } - tlsConfig, err := GetTLSConfig("../../tests/certs/auth_test_certificate.pem", - "../../tests/certs/auth_test_certificate.pem", - "../../tests/certs/auth_test_key.pem") - if err != nil { - t.Fatal("Test Failed as GetTLSConfig returned error: " + err.Error()) - } - expected := tls.VersionTLS12 - actual := tlsConfig.MinVersion - if tlsConfig != nil { - if int(actual) != expected { - t.Errorf("Test Failed due to version mismatch") - } - if tlsConfig == nil { - t.Errorf("Test Failed due to GetTLSConfig returned nil") - } - } -} diff --git a/src/orchestrator/internal/config/config.go b/src/orchestrator/internal/config/config.go deleted file mode 100644 index cb4656f0..00000000 --- a/src/orchestrator/internal/config/config.go +++ /dev/null @@ -1,130 +0,0 @@ -/* - * Copyright 2019 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 config - -import ( - "encoding/json" - "log" - "os" - "reflect" -) - -// Configuration loads up all the values that are used to configure -// backend implementations -type Configuration struct { - CAFile string `json:"ca-file"` - ServerCert string `json:"server-cert"` - ServerKey string `json:"server-key"` - Password string `json:"password"` - DatabaseIP string `json:"database-ip"` - DatabaseType string `json:"database-type"` - PluginDir string `json:"plugin-dir"` - EtcdIP string `json:"etcd-ip"` - EtcdCert string `json:"etcd-cert"` - EtcdKey string `json:"etcd-key"` - EtcdCAFile string `json:"etcd-ca-file"` - ServicePort string `json:"service-port"` - KubernetesLabelName string `json:"kubernetes-label-name"` -} - -// Config is the structure that stores the configuration -var gConfig *Configuration - -// readConfigFile reads the specified smsConfig file to setup some env variables -func readConfigFile(file string) (*Configuration, error) { - f, err := os.Open(file) - if err != nil { - return defaultConfiguration(), err - } - defer f.Close() - - // Setup some defaults here - // If the json file has values in it, the defaults will be overwritten - conf := defaultConfiguration() - - // Read the configuration from json file - decoder := json.NewDecoder(f) - decoder.DisallowUnknownFields() - err = decoder.Decode(conf) - if err != nil { - return conf, err - } - - return conf, nil -} - -func defaultConfiguration() *Configuration { - cwd, err := os.Getwd() - if err != nil { - log.Println("Error getting cwd. Using .") - cwd = "." - } - - return &Configuration{ - CAFile: "ca.cert", - ServerCert: "server.cert", - ServerKey: "server.key", - Password: "", - DatabaseIP: "127.0.0.1", - DatabaseType: "mongo", - PluginDir: cwd, - EtcdIP: "127.0.0.1", - EtcdCert: "etcd.cert", - EtcdKey: "etcd.key", - EtcdCAFile: "etcd-ca.cert", - ServicePort: "9015", - KubernetesLabelName: "orchestrator.io/rb-instance-id", - } -} - -// GetConfiguration returns the configuration for the app. -// It will try to load it if it is not already loaded. -func GetConfiguration() *Configuration { - if gConfig == nil { - conf, err := readConfigFile("config.json") - if err != nil { - log.Println("Error loading config file: ", err) - log.Println("Using defaults...") - } - gConfig = conf - } - - return gConfig -} - -// SetConfigValue sets a value in the configuration -// This is mostly used to customize the application and -// should be used carefully. -func SetConfigValue(key string, value string) *Configuration { - c := GetConfiguration() - if value == "" || key == "" { - return c - } - - v := reflect.ValueOf(c).Elem() - if v.Kind() == reflect.Struct { - f := v.FieldByName(key) - if f.IsValid() { - if f.CanSet() { - if f.Kind() == reflect.String { - f.SetString(value) - } - } - } - } - return c -} diff --git a/src/orchestrator/internal/config/config_test.go b/src/orchestrator/internal/config/config_test.go deleted file mode 100644 index ce7641ae..00000000 --- a/src/orchestrator/internal/config/config_test.go +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Copyright 2019 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 config - -import ( - "testing" -) - -func TestReadConfigurationFile(t *testing.T) { - t.Run("Non Existent Configuration File", func(t *testing.T) { - _, err := readConfigFile("filedoesnotexist.json") - if err == nil { - t.Fatal("ReadConfiguationFile: Expected Error, got nil") - } - }) - - t.Run("Read Configuration File", func(t *testing.T) { - conf, err := readConfigFile("../../tests/configs/mock_config.json") - if err != nil { - t.Fatal("ReadConfigurationFile: Error reading file: ", err) - } - if conf.DatabaseType != "mock_db_test" { - t.Fatal("ReadConfigurationFile: Incorrect entry read from file") - } - }) -} diff --git a/src/orchestrator/internal/db/README.md b/src/orchestrator/internal/db/README.md deleted file mode 100644 index cba1b7ea..00000000 --- a/src/orchestrator/internal/db/README.md +++ /dev/null @@ -1,123 +0,0 @@ -# Database Abstraction Layer - -This package contains implementations of the Database interface defined in `store.go` -Any database can be used as the backend as long as the following interface is implemented; - -```go -type Store interface { - // Returns nil if db health is good - HealthCheck() error - - // Unmarshal implements any unmarshaling needed for the database - Unmarshal(inp []byte, out interface{}) error - - // Creates a new master table with key and links data with tag and - // creates a pointer to the newly added data in the master table - Create(table string, key Key, tag string, data interface{}) error - - // Reads data for a particular key with specific tag. - Read(table string, key Key, tag string) ([]byte, error) - - // Update data for particular key with specific tag - Update(table string, key Key, tag string, data interface{}) error - - // Deletes a specific tag data for key. - // TODO: If tag is empty, it will delete all tags under key. - Delete(table string, key Key, tag string) error - - // Reads all master tables and data from the specified tag in table - ReadAll(table string, tag string) (map[string][]byte, error) -} -``` - -Therefore, `mongo.go`, `consul.go` implement the above interface and can be used as the backend as needed based on initial configuration. - -## Details on Mongo Implementation - -`mongo.go` implements the above interface using the `go.mongodb.org/mongo-driver` package. -The code converts incoming binary data and creates a new document in the database. - -### Create - -Arguments: -```go -collection string -key interface -tag string -data []byte -``` - -Create inserts the provided `data` into the `collection` which returns an auto-generated (by `mongodb`) ID which we then associate with the `key` that is provided as one of the arguments. - -We use the `FindOneAndUpdate` mongo API to achieve this with the `upsert` option set to `true`. -We create the following documents in mongodb for each new definition added to the database: - -There is a Master Key document that contains references to other documents which are related to this `key`. - -#### Master Key Entry -```json -{ - "_id" : ObjectId("5e0a8554b78a15f71d2dce7e"), - "key" : { "rbname" : "edgex", "rbversion" : "v1"}, - "defmetadata" : ObjectId("5e0a8554be261ecb57f067eb"), - "defcontent" : ObjectId("5e0a8377bcfcdd0f01dc7b0d") -} -``` -#### Metadata Key Entry -```json -{ - "_id" : ObjectId("5e0a8554be261ecb57f067eb"), - "defmetadata" : { "rbname" : "edgex", "rbversion" : "v1", "chartname" : "", "description" : "", "labels" : null } -} -``` -#### Definition Content -```json -{ - "_id" : ObjectId("5e0a8377bcfcdd0f01dc7b0d"), - "defcontent" : "H4sICCVd3FwAA3Byb2ZpbGUxLnRhcgDt1NEKgjAUxvFd7ylG98aWOsGXiYELxLRwJvj2rbyoIPDGiuD/uzmwM9iB7Vvruvrgw7CdXHsUn6Ejm2W3aopcP9eZLYRJM1voPN+ZndAm16kVSn9onheXMLheKeGqfdM0rq07/3bfUv9PJUkiR9+H+tSVajRymM6+lEqN7njxoVSbU+z2deX388r9nWzkr8fGSt5d79pnLOZfm0f+dRrzb7P4DZD/LyDJAAAAAAAAAAAAAAAA/+0Ksq1N5QAoAAA=" -} -``` - -### Unmarshal - -Data in mongo is stored as `bson` which is a compressed form of `json`. We need mongo to convert the stored `bson` data to regular `json` -that we can use in our code when returned. - -We just use the `bson.Unmarshal` API to achieve this. - -### Read - -Arguments: -```go -collection string -key interface -tag string -``` - -Read is straight forward and it uses the `FindOne` API to find our Mongo document based on the provided `key` and then gets the corresponding data for the given `tag`. It will return []byte which can then be passed to the `Unmarshal` function to get the desired GO object. - -### Delete - -Delete is similar to Read and deletes all the objectIDs being stored for a given `key` in the collection. - -## Testing Interfaces - -The following interface exists to allow for the development of unit tests which don't require mongo to be running. -It is mentioned so in the code as well. - -```go -// MongoCollection defines the a subset of MongoDB operations -// Note: This interface is defined mainly for mock testing -type MongoCollection interface { - InsertOne(ctx context.Context, document interface{}, - opts ...*options.InsertOneOptions) (*mongo.InsertOneResult, error) - FindOne(ctx context.Context, filter interface{}, - opts ...*options.FindOneOptions) *mongo.SingleResult - FindOneAndUpdate(ctx context.Context, filter interface{}, - update interface{}, opts ...*options.FindOneAndUpdateOptions) *mongo.SingleResult - DeleteOne(ctx context.Context, filter interface{}, - opts ...*options.DeleteOptions) (*mongo.DeleteResult, error) - Find(ctx context.Context, filter interface{}, - opts ...*options.FindOptions) (*mongo.Cursor, error) -} -``` \ No newline at end of file diff --git a/src/orchestrator/internal/db/mock.go b/src/orchestrator/internal/db/mock.go deleted file mode 100644 index 1dbca4b4..00000000 --- a/src/orchestrator/internal/db/mock.go +++ /dev/null @@ -1,94 +0,0 @@ -/* -Copyright 2018 Intel Corporation. -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 db - -import ( - "encoding/json" - - pkgerrors "github.com/pkg/errors" -) - -type MockKey struct { - Key string -} - -func (m MockKey) String() string { - return m.Key -} - -//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 MockDB struct { - Store - Items map[string]map[string][]byte - Err error -} - -func (m *MockDB) HealthCheck() error { - return m.Err -} - -func (m *MockDB) Create(table string, key Key, tag string, data interface{}) error { - return m.Err -} - -func (m *MockDB) Update(table string, key Key, tag string, data interface{}) error { - return m.Err -} - -// MockDB uses simple JSON and not BSON -func (m *MockDB) Unmarshal(inp []byte, out interface{}) error { - err := json.Unmarshal(inp, out) - if err != nil { - return pkgerrors.Wrap(err, "Unmarshaling json") - } - return nil -} - -func (m *MockDB) Read(table string, key Key, tag string) ([]byte, error) { - if m.Err != nil { - return nil, m.Err - } - - for k, v := range m.Items { - if k == key.String() { - return v[tag], nil - } - } - - return nil, m.Err -} - -func (m *MockDB) Delete(table string, key Key, tag string) error { - return m.Err -} - -func (m *MockDB) ReadAll(table string, tag string) (map[string][]byte, error) { - if m.Err != nil { - return nil, m.Err - } - - ret := make(map[string][]byte) - - for k, v := range m.Items { - for k1, v1 := range v { - if k1 == tag { - ret[k] = v1 - } - } - } - - return ret, nil -} diff --git a/src/orchestrator/internal/db/mongo.go b/src/orchestrator/internal/db/mongo.go deleted file mode 100644 index 3720a4f2..00000000 --- a/src/orchestrator/internal/db/mongo.go +++ /dev/null @@ -1,396 +0,0 @@ -/* - * Copyright 2018 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 db - -import ( - "log" - - "golang.org/x/net/context" - - "github.com/onap/multicloud-k8s/src/orchestrator/internal/config" - - pkgerrors "github.com/pkg/errors" - "go.mongodb.org/mongo-driver/bson" - "go.mongodb.org/mongo-driver/bson/primitive" - "go.mongodb.org/mongo-driver/mongo" - "go.mongodb.org/mongo-driver/mongo/options" -) - -// MongoCollection defines the a subset of MongoDB operations -// Note: This interface is defined mainly for mock testing -type MongoCollection interface { - InsertOne(ctx context.Context, document interface{}, - opts ...*options.InsertOneOptions) (*mongo.InsertOneResult, error) - FindOne(ctx context.Context, filter interface{}, - opts ...*options.FindOneOptions) *mongo.SingleResult - FindOneAndUpdate(ctx context.Context, filter interface{}, - update interface{}, opts ...*options.FindOneAndUpdateOptions) *mongo.SingleResult - DeleteOne(ctx context.Context, filter interface{}, - opts ...*options.DeleteOptions) (*mongo.DeleteResult, error) - Find(ctx context.Context, filter interface{}, - opts ...*options.FindOptions) (*mongo.Cursor, error) -} - -// MongoStore is an implementation of the db.Store interface -type MongoStore struct { - db *mongo.Database -} - -// This exists only for allowing us to mock the collection object -// for testing purposes -var getCollection = func(coll string, m *MongoStore) MongoCollection { - return m.db.Collection(coll) -} - -// This exists only for allowing us to mock the DecodeBytes function -// Mainly because we cannot construct a SingleResult struct from our -// tests. All fields in that struct are private. -var decodeBytes = func(sr *mongo.SingleResult) (bson.Raw, error) { - return sr.DecodeBytes() -} - -// These exists only for allowing us to mock the cursor.Next function -// Mainly because we cannot construct a mongo.Cursor struct from our -// tests. All fields in that struct are private and there is no public -// constructor method. -var cursorNext = func(ctx context.Context, cursor *mongo.Cursor) bool { - return cursor.Next(ctx) -} -var cursorClose = func(ctx context.Context, cursor *mongo.Cursor) error { - return cursor.Close(ctx) -} - -// NewMongoStore initializes a Mongo Database with the name provided -// If a database with that name exists, it will be returned -func NewMongoStore(name string, store *mongo.Database) (Store, error) { - if store == nil { - ip := "mongodb://" + config.GetConfiguration().DatabaseIP + ":27017" - clientOptions := options.Client() - clientOptions.ApplyURI(ip) - mongoClient, err := mongo.NewClient(clientOptions) - if err != nil { - return nil, err - } - - err = mongoClient.Connect(context.Background()) - if err != nil { - return nil, err - } - store = mongoClient.Database(name) - } - - return &MongoStore{ - db: store, - }, nil -} - -// HealthCheck verifies if the database is up and running -func (m *MongoStore) HealthCheck() error { - - _, err := decodeBytes(m.db.RunCommand(context.Background(), bson.D{{"serverStatus", 1}})) - if err != nil { - return pkgerrors.Wrap(err, "Error getting server status") - } - - return nil -} - -// validateParams checks to see if any parameters are empty -func (m *MongoStore) validateParams(args ...interface{}) bool { - for _, v := range args { - val, ok := v.(string) - if ok { - if val == "" { - return false - } - } else { - if v == nil { - return false - } - } - } - - return true -} - -// Create is used to create a DB entry -func (m *MongoStore) Create(coll string, key Key, tag string, data interface{}) error { - if data == nil || !m.validateParams(coll, key, tag) { - return pkgerrors.New("No Data to store") - } - - c := getCollection(coll, m) - ctx := context.Background() - - //Insert the data and then add the objectID to the masterTable - res, err := c.InsertOne(ctx, bson.D{ - {tag, data}, - }) - if err != nil { - return pkgerrors.Errorf("Error inserting into database: %s", err.Error()) - } - - //Add objectID of created data to masterKey document - //Create masterkey document if it does not exist - filter := bson.D{{"key", key}} - - _, err = decodeBytes( - c.FindOneAndUpdate( - ctx, - filter, - bson.D{ - {"$set", bson.D{ - {tag, res.InsertedID}, - }}, - }, - options.FindOneAndUpdate().SetUpsert(true).SetReturnDocument(options.After))) - - if err != nil { - return pkgerrors.Errorf("Error updating master table: %s", err.Error()) - } - - return nil -} - -// Update is used to update a DB entry -func (m *MongoStore) Update(coll string, key Key, tag string, data interface{}) error { - if data == nil || !m.validateParams(coll, key, tag) { - return pkgerrors.New("No Data to update") - } - - c := getCollection(coll, m) - ctx := context.Background() - - //Get the masterkey document based on given key - filter := bson.D{{"key", key}} - keydata, err := decodeBytes(c.FindOne(context.Background(), filter)) - if err != nil { - return pkgerrors.Errorf("Error finding master table: %s", err.Error()) - } - - //Read the tag objectID from document - tagoid, ok := keydata.Lookup(tag).ObjectIDOK() - if !ok { - return pkgerrors.Errorf("Error finding objectID for tag %s", tag) - } - - //Update the document with new data - filter = bson.D{{"_id", tagoid}} - - _, err = decodeBytes( - c.FindOneAndUpdate( - ctx, - filter, - bson.D{ - {"$set", bson.D{ - {tag, data}, - }}, - }, - options.FindOneAndUpdate().SetReturnDocument(options.After))) - - if err != nil { - return pkgerrors.Errorf("Error updating record: %s", err.Error()) - } - - return nil -} - -// Unmarshal implements an unmarshaler for bson data that -// is produced from the mongo database -func (m *MongoStore) Unmarshal(inp []byte, out interface{}) error { - err := bson.Unmarshal(inp, out) - if err != nil { - return pkgerrors.Wrap(err, "Unmarshaling bson") - } - return nil -} - -// Read method returns the data stored for this key and for this particular tag -func (m *MongoStore) Read(coll string, key Key, tag string) ([]byte, error) { - if !m.validateParams(coll, key, tag) { - return nil, pkgerrors.New("Mandatory fields are missing") - } - - c := getCollection(coll, m) - ctx := context.Background() - - //Get the masterkey document based on given key - filter := bson.D{{"key", key}} - keydata, err := decodeBytes(c.FindOne(context.Background(), filter)) - if err != nil { - return nil, pkgerrors.Errorf("Error finding master table: %s", err.Error()) - } - - //Read the tag objectID from document - tagoid, ok := keydata.Lookup(tag).ObjectIDOK() - if !ok { - return nil, pkgerrors.Errorf("Error finding objectID for tag %s", tag) - } - - //Use tag objectID to read the data from store - filter = bson.D{{"_id", tagoid}} - tagdata, err := decodeBytes(c.FindOne(ctx, filter)) - if err != nil { - return nil, pkgerrors.Errorf("Error reading found object: %s", err.Error()) - } - - //Return the data as a byte array - //Convert string data to byte array using the built-in functions - switch tagdata.Lookup(tag).Type { - case bson.TypeString: - return []byte(tagdata.Lookup(tag).StringValue()), nil - default: - return tagdata.Lookup(tag).Value, nil - } -} - -// Helper function that deletes an object by its ID -func (m *MongoStore) deleteObjectByID(coll string, objID primitive.ObjectID) error { - - c := getCollection(coll, m) - ctx := context.Background() - - _, err := c.DeleteOne(ctx, bson.D{{"_id", objID}}) - if err != nil { - return pkgerrors.Errorf("Error Deleting from database: %s", err.Error()) - } - - log.Printf("Deleted Obj with ID %s", objID.String()) - return nil -} - -// Delete method removes a document from the Database that matches key -// TODO: delete all referenced docs if tag is empty string -func (m *MongoStore) Delete(coll string, key Key, tag string) error { - if !m.validateParams(coll, key, tag) { - return pkgerrors.New("Mandatory fields are missing") - } - - c := getCollection(coll, m) - ctx := context.Background() - - //Get the masterkey document based on given key - filter := bson.D{{"key", key}} - //Remove the tag ID entry from masterkey table - update := bson.D{ - { - "$unset", bson.D{ - {tag, ""}, - }, - }, - } - keydata, err := decodeBytes(c.FindOneAndUpdate(ctx, filter, update, - options.FindOneAndUpdate().SetReturnDocument(options.Before))) - if err != nil { - //No document was found. Return nil. - if err == mongo.ErrNoDocuments { - return nil - } - //Return any other error that was found. - return pkgerrors.Errorf("Error decoding master table after update: %s", - err.Error()) - } - - //Read the tag objectID from document - elems, err := keydata.Elements() - if err != nil { - return pkgerrors.Errorf("Error reading elements from database: %s", err.Error()) - } - - tagoid, ok := keydata.Lookup(tag).ObjectIDOK() - if !ok { - return pkgerrors.Errorf("Error finding objectID for tag %s", tag) - } - - //Use tag objectID to read the data from store - err = m.deleteObjectByID(coll, tagoid) - if err != nil { - return pkgerrors.Errorf("Error deleting from database: %s", err.Error()) - } - - //Delete master table if no more tags left - //_id, key and tag should be elements in before doc - //if master table needs to be removed too - if len(elems) == 3 { - keyid, ok := keydata.Lookup("_id").ObjectIDOK() - if !ok { - return pkgerrors.Errorf("Error finding objectID for key %s", key) - } - err = m.deleteObjectByID(coll, keyid) - if err != nil { - return pkgerrors.Errorf("Error deleting master table from database: %s", err.Error()) - } - } - - return nil -} - -// ReadAll is used to get all documents in db of a particular tag -func (m *MongoStore) ReadAll(coll, tag string) (map[string][]byte, error) { - if !m.validateParams(coll, tag) { - return nil, pkgerrors.New("Missing collection or tag name") - } - - c := getCollection(coll, m) - ctx := context.Background() - - //Get all master tables in this collection - filter := bson.D{ - {"key", bson.D{ - {"$exists", true}, - }}, - } - cursor, err := c.Find(ctx, filter) - if err != nil { - return nil, pkgerrors.Errorf("Error reading from database: %s", err.Error()) - } - defer cursorClose(ctx, cursor) - - //Iterate over all the master tables - result := make(map[string][]byte) - for cursorNext(ctx, cursor) { - d := cursor.Current - - //Read key of each master table - key, ok := d.Lookup("key").DocumentOK() - if !ok { - //Throw error if key is not found - pkgerrors.New("Unable to read key from mastertable") - } - - //Get objectID of tag document - tid, ok := d.Lookup(tag).ObjectIDOK() - if !ok { - log.Printf("Did not find tag: %s", tag) - continue - } - - //Find tag document and unmarshal it into []byte - tagData, err := decodeBytes(c.FindOne(ctx, bson.D{{"_id", tid}})) - if err != nil { - log.Printf("Unable to decode tag data %s", err.Error()) - continue - } - result[key.String()] = tagData.Lookup(tag).Value - } - - if len(result) == 0 { - return result, pkgerrors.Errorf("Did not find any objects with tag: %s", tag) - } - - return result, nil -} diff --git a/src/orchestrator/internal/db/mongo_test.go b/src/orchestrator/internal/db/mongo_test.go deleted file mode 100644 index 171c908f..00000000 --- a/src/orchestrator/internal/db/mongo_test.go +++ /dev/null @@ -1,597 +0,0 @@ -/* - * Copyright 2018 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 db - -import ( - "bytes" - "context" - "reflect" - "strings" - "testing" - - pkgerrors "github.com/pkg/errors" - "go.mongodb.org/mongo-driver/bson" - "go.mongodb.org/mongo-driver/mongo" - "go.mongodb.org/mongo-driver/mongo/options" -) - -//Implements the functions used currently in mongo.go -type mockCollection struct { - Err error - mCursor *mongo.Cursor - mCursorCount int -} - -func (c *mockCollection) InsertOne(ctx context.Context, document interface{}, - opts ...*options.InsertOneOptions) (*mongo.InsertOneResult, error) { - - if c.Err != nil { - return nil, c.Err - } - - return &mongo.InsertOneResult{InsertedID: "_id1234"}, nil -} - -func (c *mockCollection) FindOne(ctx context.Context, filter interface{}, - opts ...*options.FindOneOptions) *mongo.SingleResult { - - return &mongo.SingleResult{} -} - -func (c *mockCollection) FindOneAndUpdate(ctx context.Context, filter interface{}, - update interface{}, opts ...*options.FindOneAndUpdateOptions) *mongo.SingleResult { - - return &mongo.SingleResult{} -} - -func (c *mockCollection) DeleteOne(ctx context.Context, filter interface{}, - opts ...*options.DeleteOptions) (*mongo.DeleteResult, error) { - - return nil, c.Err -} - -func (c *mockCollection) Find(ctx context.Context, filter interface{}, - opts ...*options.FindOptions) (*mongo.Cursor, error) { - - return c.mCursor, c.Err -} - -func TestCreate(t *testing.T) { - testCases := []struct { - label string - input map[string]interface{} - mockColl *mockCollection - bson bson.Raw - expectedError string - }{ - { - label: "Successfull creation of entry", - input: map[string]interface{}{ - "coll": "collname", - "key": MockKey{Key: "keyvalue"}, - "tag": "tagName", - "data": "Data In String Format", - }, - bson: bson.Raw{'\x08', '\x00', '\x00', '\x00', '\x0A', 'x', '\x00', '\x00'}, - mockColl: &mockCollection{}, - }, - { - label: "UnSuccessfull creation of entry", - input: map[string]interface{}{ - "coll": "collname", - "key": MockKey{Key: "keyvalue"}, - "tag": "tagName", - "data": "Data In String Format", - }, - mockColl: &mockCollection{ - Err: pkgerrors.New("DB Error"), - }, - expectedError: "DB Error", - }, - { - label: "Missing input fields", - input: map[string]interface{}{ - "coll": "", - "key": MockKey{Key: ""}, - "tag": "", - "data": "", - }, - expectedError: "No Data to store", - mockColl: &mockCollection{}, - }, - } - - for _, testCase := range testCases { - t.Run(testCase.label, func(t *testing.T) { - m, _ := NewMongoStore("name", &mongo.Database{}) - // Override the getCollection function with our mocked version - getCollection = func(coll string, m *MongoStore) MongoCollection { - return testCase.mockColl - } - - decodeBytes = func(sr *mongo.SingleResult) (bson.Raw, error) { - return testCase.bson, testCase.mockColl.Err - } - - err := m.Create(testCase.input["coll"].(string), testCase.input["key"].(Key), - testCase.input["tag"].(string), testCase.input["data"]) - if err != nil { - if testCase.expectedError == "" { - t.Fatalf("Create method returned an un-expected (%s)", err) - } - if !strings.Contains(string(err.Error()), testCase.expectedError) { - t.Fatalf("Create method returned an error (%s)", err) - } - } - }) - } -} - -func TestUpdate(t *testing.T) { - testCases := []struct { - label string - input map[string]interface{} - mockColl *mockCollection - bson bson.Raw - expectedError string - }{ - { - label: "Successfull update of entry", - input: map[string]interface{}{ - "coll": "collname", - "key": MockKey{Key: "keyvalue"}, - "tag": "metadata", - "data": "Data In String Format", - }, - // Binary form of - // { - // "_id" : ObjectId("5c115156777ff85654248ae1"), - // "key" : bson.D{{"name","testdef"},{"version","v1"}}, - // "metadata" : ObjectId("5c115156c9755047e318bbfd") - // } - bson: bson.Raw{ - '\x58', '\x00', '\x00', '\x00', '\x03', '\x6b', '\x65', '\x79', - '\x00', '\x27', '\x00', '\x00', '\x00', '\x02', '\x6e', '\x61', - '\x6d', '\x65', '\x00', '\x08', '\x00', '\x00', '\x00', '\x74', - '\x65', '\x73', '\x74', '\x64', '\x65', '\x66', '\x00', '\x02', - '\x76', '\x65', '\x72', '\x73', '\x69', '\x6f', '\x6e', '\x00', - '\x03', '\x00', '\x00', '\x00', '\x76', '\x31', '\x00', '\x00', - '\x07', '\x6d', '\x65', '\x74', '\x61', '\x64', '\x61', '\x74', - '\x61', '\x00', '\x5c', '\x11', '\x51', '\x56', '\x77', '\x7f', - '\xf8', '\x56', '\x54', '\x24', '\x8a', '\xe1', '\x07', '\x5f', - '\x69', '\x64', '\x00', '\x5c', '\x11', '\x51', '\x56', '\x77', - '\x7f', '\xf8', '\x56', '\x54', '\x24', '\x8a', '\xe1', '\x00', - }, - mockColl: &mockCollection{}, - }, - { - label: "Entry does not exist", - input: map[string]interface{}{ - "coll": "collname", - "key": MockKey{Key: "keyvalue"}, - "tag": "tagName", - "data": "Data In String Format", - }, - mockColl: &mockCollection{ - Err: pkgerrors.New("DB Error"), - }, - expectedError: "DB Error", - }, - } - - for _, testCase := range testCases { - t.Run(testCase.label, func(t *testing.T) { - m, _ := NewMongoStore("name", &mongo.Database{}) - // Override the getCollection function with our mocked version - getCollection = func(coll string, m *MongoStore) MongoCollection { - return testCase.mockColl - } - - decodeBytes = func(sr *mongo.SingleResult) (bson.Raw, error) { - return testCase.bson, testCase.mockColl.Err - } - - err := m.Update(testCase.input["coll"].(string), testCase.input["key"].(Key), - testCase.input["tag"].(string), testCase.input["data"]) - if err != nil { - if testCase.expectedError == "" { - t.Fatalf("Create method returned an un-expected (%s)", err) - } - if !strings.Contains(string(err.Error()), testCase.expectedError) { - t.Fatalf("Create method returned an error (%s)", err) - } - } - }) - } -} - -func TestRead(t *testing.T) { - testCases := []struct { - label string - input map[string]interface{} - mockColl *mockCollection - bson bson.Raw - expectedError string - expected []byte - }{ - { - label: "Successfull Read of entry", - input: map[string]interface{}{ - "coll": "collname", - "key": MockKey{Key: "keyvalue"}, - "tag": "metadata", - }, - // Binary form of - // { - // "_id" : ObjectId("5c115156777ff85654248ae1"), - // "key" : bson.D{{"name","testdef"},{"version","v1"}}, - // "metadata" : ObjectId("5c115156c9755047e318bbfd") - // } - bson: bson.Raw{ - '\x58', '\x00', '\x00', '\x00', '\x03', '\x6b', '\x65', '\x79', - '\x00', '\x27', '\x00', '\x00', '\x00', '\x02', '\x6e', '\x61', - '\x6d', '\x65', '\x00', '\x08', '\x00', '\x00', '\x00', '\x74', - '\x65', '\x73', '\x74', '\x64', '\x65', '\x66', '\x00', '\x02', - '\x76', '\x65', '\x72', '\x73', '\x69', '\x6f', '\x6e', '\x00', - '\x03', '\x00', '\x00', '\x00', '\x76', '\x31', '\x00', '\x00', - '\x07', '\x6d', '\x65', '\x74', '\x61', '\x64', '\x61', '\x74', - '\x61', '\x00', '\x5c', '\x11', '\x51', '\x56', '\x77', '\x7f', - '\xf8', '\x56', '\x54', '\x24', '\x8a', '\xe1', '\x07', '\x5f', - '\x69', '\x64', '\x00', '\x5c', '\x11', '\x51', '\x56', '\x77', - '\x7f', '\xf8', '\x56', '\x54', '\x24', '\x8a', '\xe1', '\x00', - }, - mockColl: &mockCollection{}, - // This is not the document because we are mocking decodeBytes - expected: []byte{92, 17, 81, 86, 119, 127, 248, 86, 84, 36, 138, 225}, - }, - { - label: "UnSuccessfull Read of entry: object not found", - input: map[string]interface{}{ - "coll": "collname", - "key": MockKey{Key: "keyvalue"}, - "tag": "badtag", - }, - // Binary form of - // { - // "_id" : ObjectId("5c115156777ff85654248ae1"), - // "key" : bson.D{{"name","testdef"},{"version","v1"}}, - // "metadata" : ObjectId("5c115156c9755047e318bbfd") - // } - bson: bson.Raw{ - '\x58', '\x00', '\x00', '\x00', '\x03', '\x6b', '\x65', '\x79', - '\x00', '\x27', '\x00', '\x00', '\x00', '\x02', '\x6e', '\x61', - '\x6d', '\x65', '\x00', '\x08', '\x00', '\x00', '\x00', '\x74', - '\x65', '\x73', '\x74', '\x64', '\x65', '\x66', '\x00', '\x02', - '\x76', '\x65', '\x72', '\x73', '\x69', '\x6f', '\x6e', '\x00', - '\x03', '\x00', '\x00', '\x00', '\x76', '\x31', '\x00', '\x00', - '\x07', '\x6d', '\x65', '\x74', '\x61', '\x64', '\x61', '\x74', - '\x61', '\x00', '\x5c', '\x11', '\x51', '\x56', '\x77', '\x7f', - '\xf8', '\x56', '\x54', '\x24', '\x8a', '\xe1', '\x07', '\x5f', - '\x69', '\x64', '\x00', '\x5c', '\x11', '\x51', '\x56', '\x77', - '\x7f', '\xf8', '\x56', '\x54', '\x24', '\x8a', '\xe1', '\x00', - }, - mockColl: &mockCollection{}, - expectedError: "Error finding objectID", - }, - { - label: "UnSuccessfull Read of entry", - input: map[string]interface{}{ - "coll": "collname", - "key": MockKey{Key: "keyvalue"}, - "tag": "tagName", - }, - mockColl: &mockCollection{ - Err: pkgerrors.New("DB Error"), - }, - expectedError: "DB Error", - }, - { - label: "Missing input fields", - input: map[string]interface{}{ - "coll": "", - "key": MockKey{Key: ""}, - "tag": "", - }, - expectedError: "Mandatory fields are missing", - mockColl: &mockCollection{}, - }, - } - - for _, testCase := range testCases { - t.Run(testCase.label, func(t *testing.T) { - m, _ := NewMongoStore("name", &mongo.Database{}) - // Override the getCollection function with our mocked version - getCollection = func(coll string, m *MongoStore) MongoCollection { - return testCase.mockColl - } - - decodeBytes = func(sr *mongo.SingleResult) (bson.Raw, error) { - return testCase.bson, testCase.mockColl.Err - } - got, err := m.Read(testCase.input["coll"].(string), testCase.input["key"].(Key), - testCase.input["tag"].(string)) - if err != nil { - if testCase.expectedError == "" { - t.Fatalf("Read method returned an un-expected (%s)", err) - } - if !strings.Contains(string(err.Error()), testCase.expectedError) { - t.Fatalf("Read method returned an error (%s)", err) - } - } else { - if bytes.Compare(got, testCase.expected) != 0 { - t.Fatalf("Read returned unexpected data: %v, expected: %v", - string(got), testCase.expected) - } - } - }) - } -} - -func TestDelete(t *testing.T) { - testCases := []struct { - label string - input map[string]interface{} - mockColl *mockCollection - bson bson.Raw - expectedError string - }{ - { - label: "Successfull Delete of entry", - input: map[string]interface{}{ - "coll": "collname", - "key": MockKey{Key: "keyvalue"}, - "tag": "metadata", - }, - // Binary form of - // { - // "_id" : ObjectId("5c115156777ff85654248ae1"), - // "key" : bson.D{{"name","testdef"},{"version","v1"}}, - // "metadata" : ObjectId("5c115156c9755047e318bbfd") - // } - bson: bson.Raw{ - '\x58', '\x00', '\x00', '\x00', '\x03', '\x6b', '\x65', '\x79', - '\x00', '\x27', '\x00', '\x00', '\x00', '\x02', '\x6e', '\x61', - '\x6d', '\x65', '\x00', '\x08', '\x00', '\x00', '\x00', '\x74', - '\x65', '\x73', '\x74', '\x64', '\x65', '\x66', '\x00', '\x02', - '\x76', '\x65', '\x72', '\x73', '\x69', '\x6f', '\x6e', '\x00', - '\x03', '\x00', '\x00', '\x00', '\x76', '\x31', '\x00', '\x00', - '\x07', '\x6d', '\x65', '\x74', '\x61', '\x64', '\x61', '\x74', - '\x61', '\x00', '\x5c', '\x11', '\x51', '\x56', '\x77', '\x7f', - '\xf8', '\x56', '\x54', '\x24', '\x8a', '\xe1', '\x07', '\x5f', - '\x69', '\x64', '\x00', '\x5c', '\x11', '\x51', '\x56', '\x77', - '\x7f', '\xf8', '\x56', '\x54', '\x24', '\x8a', '\xe1', '\x00', - }, - mockColl: &mockCollection{}, - }, - { - label: "UnSuccessfull Delete of entry", - input: map[string]interface{}{ - "coll": "collname", - "key": MockKey{Key: "keyvalue"}, - "tag": "tagName", - }, - mockColl: &mockCollection{ - Err: pkgerrors.New("DB Error"), - }, - expectedError: "DB Error", - }, - { - label: "UnSuccessfull Delete, key not found", - input: map[string]interface{}{ - "coll": "collname", - "key": MockKey{Key: "keyvalue"}, - "tag": "tagName", - }, - // Binary form of - // { - // "_id" : ObjectId("5c115156777ff85654248ae1"), - // "key" : bson.D{{"name","testdef"},{"version","v1"}}, - // "metadata" : ObjectId("5c115156c9755047e318bbfd") - // } - bson: bson.Raw{ - '\x58', '\x00', '\x00', '\x00', '\x03', '\x6b', '\x65', '\x79', - '\x00', '\x27', '\x00', '\x00', '\x00', '\x02', '\x6e', '\x61', - '\x6d', '\x65', '\x00', '\x08', '\x00', '\x00', '\x00', '\x74', - '\x65', '\x73', '\x74', '\x64', '\x65', '\x66', '\x00', '\x02', - '\x76', '\x65', '\x72', '\x73', '\x69', '\x6f', '\x6e', '\x00', - '\x03', '\x00', '\x00', '\x00', '\x76', '\x31', '\x00', '\x00', - '\x07', '\x6d', '\x65', '\x74', '\x61', '\x64', '\x61', '\x74', - '\x61', '\x00', '\x5c', '\x11', '\x51', '\x56', '\x77', '\x7f', - '\xf8', '\x56', '\x54', '\x24', '\x8a', '\xe1', '\x07', '\x5f', - '\x69', '\x64', '\x00', '\x5c', '\x11', '\x51', '\x56', '\x77', - '\x7f', '\xf8', '\x56', '\x54', '\x24', '\x8a', '\xe1', '\x00', - }, - mockColl: &mockCollection{}, - expectedError: "Error finding objectID", - }, - { - label: "Missing input fields", - input: map[string]interface{}{ - "coll": "", - "key": MockKey{Key: ""}, - "tag": "", - }, - expectedError: "Mandatory fields are missing", - mockColl: &mockCollection{}, - }, - } - - for _, testCase := range testCases { - t.Run(testCase.label, func(t *testing.T) { - m, _ := NewMongoStore("name", &mongo.Database{}) - // Override the getCollection function with our mocked version - getCollection = func(coll string, m *MongoStore) MongoCollection { - return testCase.mockColl - } - - decodeBytes = func(sr *mongo.SingleResult) (bson.Raw, error) { - return testCase.bson, testCase.mockColl.Err - } - err := m.Delete(testCase.input["coll"].(string), testCase.input["key"].(Key), - testCase.input["tag"].(string)) - if err != nil { - if testCase.expectedError == "" { - t.Fatalf("Delete method returned an un-expected (%s)", err) - } - if !strings.Contains(string(err.Error()), testCase.expectedError) { - t.Fatalf("Delete method returned an error (%s)", err) - } - } - }) - } -} - -func TestReadAll(t *testing.T) { - testCases := []struct { - label string - input map[string]interface{} - mockColl *mockCollection - bson bson.Raw - expectedError string - expected map[string][]byte - }{ - { - label: "Successfully Read all entries", - input: map[string]interface{}{ - "coll": "collname", - "tag": "metadata", - }, - mockColl: &mockCollection{ - mCursor: &mongo.Cursor{ - // Binary form of - // { - // "_id" : ObjectId("5c115156777ff85654248ae1"), - // "key" : bson.D{{"name","testdef"},{"version","v1"}}, - // "metadata" : ObjectId("5c115156c9755047e318bbfd") - // } - - Current: bson.Raw{ - '\x58', '\x00', '\x00', '\x00', '\x03', '\x6b', '\x65', '\x79', - '\x00', '\x27', '\x00', '\x00', '\x00', '\x02', '\x6e', '\x61', - '\x6d', '\x65', '\x00', '\x08', '\x00', '\x00', '\x00', '\x74', - '\x65', '\x73', '\x74', '\x64', '\x65', '\x66', '\x00', '\x02', - '\x76', '\x65', '\x72', '\x73', '\x69', '\x6f', '\x6e', '\x00', - '\x03', '\x00', '\x00', '\x00', '\x76', '\x31', '\x00', '\x00', - '\x07', '\x6d', '\x65', '\x74', '\x61', '\x64', '\x61', '\x74', - '\x61', '\x00', '\x5c', '\x11', '\x51', '\x56', '\x77', '\x7f', - '\xf8', '\x56', '\x54', '\x24', '\x8a', '\xe1', '\x07', '\x5f', - '\x69', '\x64', '\x00', '\x5c', '\x11', '\x51', '\x56', '\x77', - '\x7f', '\xf8', '\x56', '\x54', '\x24', '\x8a', '\xe1', '\x00', - }, - }, - mCursorCount: 1, - }, - expected: map[string][]byte{ - `{"name": "testdef","version": "v1"}`: []byte{ - 92, 17, 81, 86, 119, 127, 248, 86, 84, 36, 138, 225}, - }, - }, - { - label: "UnSuccessfully Read of all entries", - input: map[string]interface{}{ - "coll": "collname", - "tag": "tagName", - }, - mockColl: &mockCollection{ - Err: pkgerrors.New("DB Error"), - }, - expectedError: "DB Error", - }, - { - label: "UnSuccessfull Readall, tag not found", - input: map[string]interface{}{ - "coll": "collname", - "tag": "tagName", - }, - mockColl: &mockCollection{ - mCursor: &mongo.Cursor{ - // Binary form of - // { - // "_id" : ObjectId("5c115156777ff85654248ae1"), - // "key" : bson.D{{"name","testdef"},{"version","v1"}}, - // "metadata" : ObjectId("5c115156c9755047e318bbfd") - // } - Current: bson.Raw{ - '\x58', '\x00', '\x00', '\x00', '\x03', '\x6b', '\x65', '\x79', - '\x00', '\x27', '\x00', '\x00', '\x00', '\x02', '\x6e', '\x61', - '\x6d', '\x65', '\x00', '\x08', '\x00', '\x00', '\x00', '\x74', - '\x65', '\x73', '\x74', '\x64', '\x65', '\x66', '\x00', '\x02', - '\x76', '\x65', '\x72', '\x73', '\x69', '\x6f', '\x6e', '\x00', - '\x03', '\x00', '\x00', '\x00', '\x76', '\x31', '\x00', '\x00', - '\x07', '\x6d', '\x65', '\x74', '\x61', '\x64', '\x61', '\x74', - '\x61', '\x00', '\x5c', '\x11', '\x51', '\x56', '\x77', '\x7f', - '\xf8', '\x56', '\x54', '\x24', '\x8a', '\xe1', '\x07', '\x5f', - '\x69', '\x64', '\x00', '\x5c', '\x11', '\x51', '\x56', '\x77', - '\x7f', '\xf8', '\x56', '\x54', '\x24', '\x8a', '\xe1', '\x00', - }, - }, - mCursorCount: 1, - }, - expectedError: "Did not find any objects with tag", - }, - { - label: "Missing input fields", - input: map[string]interface{}{ - "coll": "", - "tag": "", - }, - expectedError: "Missing collection or tag name", - mockColl: &mockCollection{}, - }, - } - - for _, testCase := range testCases { - t.Run(testCase.label, func(t *testing.T) { - m, _ := NewMongoStore("name", &mongo.Database{}) - // Override the getCollection function with our mocked version - getCollection = func(coll string, m *MongoStore) MongoCollection { - return testCase.mockColl - } - - decodeBytes = func(sr *mongo.SingleResult) (bson.Raw, error) { - return testCase.mockColl.mCursor.Current, testCase.mockColl.Err - } - - cursorNext = func(ctx context.Context, cursor *mongo.Cursor) bool { - if testCase.mockColl.mCursorCount > 0 { - testCase.mockColl.mCursorCount -= 1 - return true - } - return false - } - - cursorClose = func(ctx context.Context, cursor *mongo.Cursor) error { - return nil - } - - got, err := m.ReadAll(testCase.input["coll"].(string), testCase.input["tag"].(string)) - if err != nil { - if testCase.expectedError == "" { - t.Fatalf("Readall method returned an un-expected (%s)", err) - } - if !strings.Contains(string(err.Error()), testCase.expectedError) { - t.Fatalf("Readall method returned an error (%s)", err) - } - } else { - if reflect.DeepEqual(got, testCase.expected) == false { - t.Fatalf("Readall returned unexpected data: %v, expected: %v", - got, testCase.expected) - } - } - }) - } -} diff --git a/src/orchestrator/internal/db/store.go b/src/orchestrator/internal/db/store.go deleted file mode 100644 index ed394205..00000000 --- a/src/orchestrator/internal/db/store.go +++ /dev/null @@ -1,106 +0,0 @@ -/* -Copyright 2018 Intel Corporation. -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 db - -import ( - "encoding/json" - "reflect" - - "github.com/onap/multicloud-k8s/src/orchestrator/internal/config" - - pkgerrors "github.com/pkg/errors" -) - -// DBconn interface used to talk a concrete Database connection -var DBconn Store - -// Key is an interface that will be implemented by anypackage -// that wants to use the Store interface. This allows various -// db backends and key types. -type Key interface { - String() string -} - -// Store is an interface for accessing the database -type Store interface { - // Returns nil if db health is good - HealthCheck() error - - // Unmarshal implements any unmarshaling needed for the database - Unmarshal(inp []byte, out interface{}) error - - // Creates a new master document with key and links data with tag and - // creates a pointer(row) to the newly added data in the master table - Create(table string, key Key, tag string, data interface{}) error - - // Reads data for a particular key with specific tag. - Read(table string, key Key, tag string) ([]byte, error) - - // Update data for particular key with specific tag - Update(table string, key Key, tag string, data interface{}) error - - // Deletes a specific tag data for key. - // TODO: If tag is empty, it will delete all tags under key. - Delete(table string, key Key, tag string) error - - // Reads all master tables and data from the specified tag in table - ReadAll(table string, tag string) (map[string][]byte, error) -} - -// CreateDBClient creates the DB client -func createDBClient(dbType string) error { - var err error - switch dbType { - case "mongo": - // create a mongodb database with orchestrator as the name - DBconn, err = NewMongoStore("orchestrator", nil) - default: - return pkgerrors.New(dbType + "DB not supported") - } - return err -} - -// Serialize converts given data into a JSON string -func Serialize(v interface{}) (string, error) { - out, err := json.Marshal(v) - if err != nil { - return "", pkgerrors.Wrap(err, "Error serializing "+reflect.TypeOf(v).String()) - } - return string(out), nil -} - -// DeSerialize converts string to a json object specified by type -func DeSerialize(str string, v interface{}) error { - err := json.Unmarshal([]byte(str), &v) - if err != nil { - return pkgerrors.Wrap(err, "Error deSerializing "+str) - } - return nil -} - -// InitializeDatabaseConnection sets up the connection to the -// configured database to allow the application to talk to it. -func InitializeDatabaseConnection() error { - err := createDBClient(config.GetConfiguration().DatabaseType) - if err != nil { - return pkgerrors.Cause(err) - } - - err = DBconn.HealthCheck() - if err != nil { - return pkgerrors.Cause(err) - } - - return nil -} diff --git a/src/orchestrator/internal/db/store_test.go b/src/orchestrator/internal/db/store_test.go deleted file mode 100644 index 42a41787..00000000 --- a/src/orchestrator/internal/db/store_test.go +++ /dev/null @@ -1,121 +0,0 @@ -/* -Copyright 2018 Intel Corporation. -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 db - -import ( - "reflect" - "strings" - "testing" -) - -func TestCreateDBClient(t *testing.T) { - t.Run("Successfully create DB client", func(t *testing.T) { - expected := &MongoStore{} - - err := createDBClient("mongo") - if err != nil { - t.Fatalf("CreateDBClient returned an error (%s)", err) - } - if reflect.TypeOf(DBconn) != reflect.TypeOf(expected) { - t.Fatalf("CreateDBClient set DBconn as:\n result=%T\n expected=%T", DBconn, expected) - } - }) - t.Run("Fail to create client for unsupported DB", func(t *testing.T) { - err := createDBClient("fakeDB") - if err == nil { - t.Fatal("CreateDBClient didn't return an error") - } - if !strings.Contains(string(err.Error()), "DB not supported") { - t.Fatalf("CreateDBClient method returned an error (%s)", err) - } - }) -} - -func TestSerialize(t *testing.T) { - - inp := map[string]interface{}{ - "UUID": "123e4567-e89b-12d3-a456-426655440000", - "Data": "sdaijsdiodalkfjsdlagf", - "Number": 23, - "Float": 34.4, - "Map": map[string]interface{}{ - "m1": "m1", - "m2": 2, - "m3": 3.0, - }, - } - - got, err := Serialize(inp) - if err != nil { - t.Fatal(err) - } - - expected := "{\"Data\":\"sdaijsdiodalkfjsdlagf\"," + - "\"Float\":34.4,\"Map\":{\"m1\":\"m1\",\"m2\":2,\"m3\":3}," + - "\"Number\":23,\"UUID\":\"123e4567-e89b-12d3-a456-426655440000\"}" - - if expected != got { - t.Errorf("Serialize returned unexpected string: %s;"+ - " expected %sv", got, expected) - } -} - -func TestDeSerialize(t *testing.T) { - testCases := []struct { - label string - input string - expected map[string]interface{} - errMsg string - }{ - { - label: "Sucessful deserialize entry", - input: "{\"Data\":\"sdaijsdiodalkfjsdlagf\"," + - "\"Float\":34.4,\"Map\":{\"m1\":\"m1\",\"m3\":3}," + - "\"UUID\":\"123e4567-e89b-12d3-a456-426655440000\"}", - expected: map[string]interface{}{ - "UUID": "123e4567-e89b-12d3-a456-426655440000", - "Data": "sdaijsdiodalkfjsdlagf", - "Float": 34.4, - "Map": map[string]interface{}{ - "m1": "m1", - "m3": 3.0, - }, - }, - }, - { - label: "Fail to deserialize invalid entry", - input: "{invalid}", - errMsg: "Error deSerializing {invalid}: invalid character 'i' looking for beginning of object key string", - }, - } - for _, testCase := range testCases { - t.Run(testCase.label, func(t *testing.T) { - got := make(map[string]interface{}) - err := DeSerialize(testCase.input, &got) - if err != nil { - if testCase.errMsg == "" { - t.Fatalf("DeSerialize method return an un-expected (%s)", err) - } - if !strings.Contains(string(err.Error()), testCase.errMsg) { - t.Fatalf("DeSerialize method returned an error (%s)", err) - } - } else { - if !reflect.DeepEqual(testCase.expected, got) { - t.Errorf("Serialize returned unexpected : %v;"+ - " expected %v", got, testCase.expected) - } - } - }) - } -} diff --git a/src/orchestrator/internal/logutils/logger.go b/src/orchestrator/internal/logutils/logger.go deleted file mode 100644 index 2e8f9969..00000000 --- a/src/orchestrator/internal/logutils/logger.go +++ /dev/null @@ -1,28 +0,0 @@ -package logutils - -import ( - log "github.com/sirupsen/logrus" -) - -//Fields is type that will be used by the calling function -type Fields map[string]interface{} - -func init() { - // Log as JSON instead of the default ASCII formatter. - log.SetFormatter(&log.JSONFormatter{}) -} - -// Error uses the fields provided and logs -func Error(msg string, fields Fields) { - log.WithFields(log.Fields(fields)).Error(msg) -} - -// Warn uses the fields provided and logs -func Warn(msg string, fields Fields) { - log.WithFields(log.Fields(fields)).Warn(msg) -} - -// Info uses the fields provided and logs -func Info(msg string, fields Fields) { - log.WithFields(log.Fields(fields)).Info(msg) -} diff --git a/src/orchestrator/internal/project/project.go b/src/orchestrator/internal/project/project.go deleted file mode 100644 index f0c50065..00000000 --- a/src/orchestrator/internal/project/project.go +++ /dev/null @@ -1,133 +0,0 @@ -/* - * Copyright 2019 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 project - -import ( - "encoding/json" - - "github.com/onap/multicloud-k8s/src/orchestrator/internal/db" - - pkgerrors "github.com/pkg/errors" -) - -// Project contains the parameters needed for Projects -// It implements the interface for managing the Projects -type Project struct { - ProjectName string `json:"project-name"` - Description string `json:"description"` -} - -// ProjectKey is the key structure that is used in the database -type ProjectKey struct { - ProjectName string `json:"rb-name"` -} - -// We will use json marshalling to convert to string to -// preserve the underlying structure. -func (pk ProjectKey) String() string { - out, err := json.Marshal(pk) - if err != nil { - return "" - } - - return string(out) -} - -// ProjectManager is an interface exposes the Project functionality -type ProjectManager interface { - Create(pr Project) (Project, error) - Get(name string) (Project, error) - Delete(name string) error -} - -// ProjectClient implements the ProjectManager -// It will also be used to maintain some localized state -type ProjectClient struct { - storeName string - tagMeta, tagContent string -} - -// NewProjectClient returns an instance of the ProjectClient -// which implements the ProjectManager -func NewProjectClient() *ProjectClient { - return &ProjectClient{ - tagMeta: "projectmetadata", - } -} - -// Create a new collection based on the project -func (v *ProjectClient) Create(p Project) (Project, error) { - - //Construct the composite key to select the entry - key := ProjectKey{ - ProjectName: p.ProjectName, - } - - //Check if this Project already exists - _, err := v.Get(p.ProjectName) - if err == nil { - return Project{}, pkgerrors.New("Project already exists") - } - - err = db.DBconn.Create(p.ProjectName, key, v.tagMeta, p) - if err != nil { - return Project{}, pkgerrors.Wrap(err, "Creating DB Entry") - } - - return p, nil -} - -// Get returns the Project for corresponding name -func (v *ProjectClient) Get(name string) (Project, error) { - - //Construct the composite key to select the entry - key := ProjectKey{ - ProjectName: name, - } - value, err := db.DBconn.Read(name, key, v.tagMeta) - if err != nil { - return Project{}, pkgerrors.Wrap(err, "Get Project") - } - - //value is a byte array - if value != nil { - proj := Project{} - err = db.DBconn.Unmarshal(value, &proj) - if err != nil { - return Project{}, pkgerrors.Wrap(err, "Unmarshaling Value") - } - return proj, nil - } - - return Project{}, pkgerrors.New("Error getting Project") -} - -// Delete the Project from database -func (v *ProjectClient) Delete(name string) error { - - //Construct the composite key to select the entry - key := ProjectKey{ - ProjectName: name, - } - err := db.DBconn.Delete(name, key, v.tagMeta) - if err != nil { - return pkgerrors.Wrap(err, "Delete Project Entry;") - } - - //TODO: Delete the collection when the project is deleted - return nil -} diff --git a/src/orchestrator/internal/project/project_test.go b/src/orchestrator/internal/project/project_test.go deleted file mode 100644 index cc691e33..00000000 --- a/src/orchestrator/internal/project/project_test.go +++ /dev/null @@ -1,177 +0,0 @@ -/* - * Copyright 2018 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 project - -import ( - "reflect" - "strings" - "testing" - - "github.com/onap/multicloud-k8s/src/orchestrator/internal/db" - - pkgerrors "github.com/pkg/errors" -) - -func TestCreateProject(t *testing.T) { - testCases := []struct { - label string - inp Project - expectedError string - mockdb *db.MockDB - expected Project - }{ - { - label: "Create Project", - inp: Project{ - ProjectName: "testProject", - Description: "A sample Project used for unit testing", - }, - expected: Project{ - ProjectName: "testProject", - Description: "A sample Project used for unit testing", - }, - expectedError: "", - mockdb: &db.MockDB{}, - }, - { - label: "Failed Create Project", - expectedError: "Error Creating Project", - mockdb: &db.MockDB{ - Err: pkgerrors.New("Error Creating Project"), - }, - }, - } - - for _, testCase := range testCases { - t.Run(testCase.label, func(t *testing.T) { - db.DBconn = testCase.mockdb - impl := NewProjectClient() - got, err := impl.Create(testCase.inp) - 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 TestGetProject(t *testing.T) { - - testCases := []struct { - label string - name string - expectedError string - mockdb *db.MockDB - inp string - expected Project - }{ - { - label: "Get Project", - name: "testProject", - expected: Project{ - ProjectName: "testProject", - Description: "Test project for unit testing", - }, - expectedError: "", - mockdb: &db.MockDB{ - Items: map[string]map[string][]byte{ - ProjectKey{ProjectName: "testProject"}.String(): { - "projectmetadata": []byte( - "{\"project-name\":\"testProject\"," + - "\"description\":\"Test project for unit testing\"}"), - }, - }, - }, - }, - { - 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 := NewProjectClient() - got, err := impl.Get(testCase.name) - 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 TestDeleteProject(t *testing.T) { - - testCases := []struct { - label string - name string - expectedError string - mockdb *db.MockDB - }{ - { - label: "Delete Project", - name: "testProject", - mockdb: &db.MockDB{}, - }, - { - 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 := NewProjectClient() - err := impl.Delete(testCase.name) - 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/infra/auth/auth.go b/src/orchestrator/pkg/infra/auth/auth.go new file mode 100644 index 00000000..3da8f2af --- /dev/null +++ b/src/orchestrator/pkg/infra/auth/auth.go @@ -0,0 +1,107 @@ +/* + * Copyright 2018 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 auth + +import ( + "crypto/tls" + "crypto/x509" + "encoding/base64" + "encoding/pem" + "io/ioutil" + "log" + + pkgerrors "github.com/pkg/errors" +) + +// GetTLSConfig initializes a tlsConfig using the CA's certificate +// This config is then used to enable the server for mutual TLS +func GetTLSConfig(caCertFile string, certFile string, keyFile string) (*tls.Config, error) { + + // Initialize tlsConfig once + caCert, err := ioutil.ReadFile(caCertFile) + + if err != nil { + return nil, pkgerrors.Wrap(err, "Read CA Cert file") + } + + caCertPool := x509.NewCertPool() + caCertPool.AppendCertsFromPEM(caCert) + + tlsConfig := &tls.Config{ + // Change to RequireAndVerify once we have mandatory certs + ClientAuth: tls.VerifyClientCertIfGiven, + ClientCAs: caCertPool, + MinVersion: tls.VersionTLS12, + } + + certPEMBlk, err := readPEMBlock(certFile) + if err != nil { + return nil, pkgerrors.Wrap(err, "Read Cert File") + } + + keyPEMBlk, err := readPEMBlock(keyFile) + if err != nil { + return nil, pkgerrors.Wrap(err, "Read Key File") + } + + tlsConfig.Certificates = make([]tls.Certificate, 1) + tlsConfig.Certificates[0], err = tls.X509KeyPair(certPEMBlk, keyPEMBlk) + if err != nil { + return nil, pkgerrors.Wrap(err, "Load x509 cert and key") + } + + tlsConfig.BuildNameToCertificate() + return tlsConfig, nil +} + +func readPEMBlock(filename string) ([]byte, error) { + + pemData, err := ioutil.ReadFile(filename) + if err != nil { + return nil, pkgerrors.Wrap(err, "Read PEM File") + } + + pemBlock, rest := pem.Decode(pemData) + if len(rest) > 0 { + log.Println("Pemfile has extra data") + } + + if x509.IsEncryptedPEMBlock(pemBlock) { + password, err := ioutil.ReadFile(filename + ".pass") + if err != nil { + return nil, pkgerrors.Wrap(err, "Read Password File") + } + + pByte, err := base64.StdEncoding.DecodeString(string(password)) + if err != nil { + return nil, pkgerrors.Wrap(err, "Decode PEM Password") + } + + pemData, err = x509.DecryptPEMBlock(pemBlock, pByte) + if err != nil { + return nil, pkgerrors.Wrap(err, "Decrypt PEM Data") + } + var newPEMBlock pem.Block + newPEMBlock.Type = pemBlock.Type + newPEMBlock.Bytes = pemData + // Converting back to PEM from DER data you get from + // DecryptPEMBlock + pemData = pem.EncodeToMemory(&newPEMBlock) + } + + return pemData, nil +} diff --git a/src/orchestrator/pkg/infra/auth/auth_test.go b/src/orchestrator/pkg/infra/auth/auth_test.go new file mode 100644 index 00000000..fdf81e6d --- /dev/null +++ b/src/orchestrator/pkg/infra/auth/auth_test.go @@ -0,0 +1,47 @@ +/* +* Copyright 2018 TechMahindra +* +* 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 auth + +import ( + "crypto/tls" + "testing" +) + +//Unit test to varify GetTLSconfig func and varify the tls config min version to be 771 +//Assuming cert file name as auth_test.cert +func TestGetTLSConfig(t *testing.T) { + _, err := GetTLSConfig("filedoesnotexist.cert", "filedoesnotexist.cert", "filedoesnotexist.cert") + if err == nil { + t.Errorf("Test failed, expected error but got none") + } + tlsConfig, err := GetTLSConfig("../../../tests/certs/auth_test_certificate.pem", + "../../../tests/certs/auth_test_certificate.pem", + "../../../tests/certs/auth_test_key.pem") + if err != nil { + t.Fatal("Test Failed as GetTLSConfig returned error: " + err.Error()) + } + expected := tls.VersionTLS12 + actual := tlsConfig.MinVersion + if tlsConfig != nil { + if int(actual) != expected { + t.Errorf("Test Failed due to version mismatch") + } + if tlsConfig == nil { + t.Errorf("Test Failed due to GetTLSConfig returned nil") + } + } +} diff --git a/src/orchestrator/pkg/infra/config/config.go b/src/orchestrator/pkg/infra/config/config.go new file mode 100644 index 00000000..df9cec92 --- /dev/null +++ b/src/orchestrator/pkg/infra/config/config.go @@ -0,0 +1,130 @@ +/* + * Copyright 2019 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 config + +import ( + "encoding/json" + "log" + "os" + "reflect" +) + +// Configuration loads up all the values that are used to configure +// backend implementations +type Configuration struct { + CAFile string `json:"ca-file"` + ServerCert string `json:"server-cert"` + ServerKey string `json:"server-key"` + Password string `json:"password"` + DatabaseIP string `json:"database-ip"` + DatabaseType string `json:"database-type"` + PluginDir string `json:"plugin-dir"` + EtcdIP string `json:"etcd-ip"` + EtcdCert string `json:"etcd-cert"` + EtcdKey string `json:"etcd-key"` + EtcdCAFile string `json:"etcd-ca-file"` + ServicePort string `json:"service-port"` + KubernetesLabelName string `json:"kubernetes-label-name"` +} + +// Config is the structure that stores the configuration +var gConfig *Configuration + +// readConfigFile reads the specified smsConfig file to setup some env variables +func readConfigFile(file string) (*Configuration, error) { + f, err := os.Open(file) + if err != nil { + return defaultConfiguration(), err + } + defer f.Close() + + // Setup some defaults here + // If the json file has values in it, the defaults will be overwritten + conf := defaultConfiguration() + + // Read the configuration from json file + decoder := json.NewDecoder(f) + decoder.DisallowUnknownFields() + err = decoder.Decode(conf) + if err != nil { + return conf, err + } + + return conf, nil +} + +func defaultConfiguration() *Configuration { + cwd, err := os.Getwd() + if err != nil { + log.Println("Error getting cwd. Using .") + cwd = "." + } + + return &Configuration{ + CAFile: "ca.cert", + ServerCert: "server.cert", + ServerKey: "server.key", + Password: "", + DatabaseIP: "127.0.0.1", + DatabaseType: "mongo", + PluginDir: cwd, + EtcdIP: "127.0.0.1", + EtcdCert: "", + EtcdKey: "", + EtcdCAFile: "", + ServicePort: "9015", + KubernetesLabelName: "orchestrator.io/rb-instance-id", + } +} + +// GetConfiguration returns the configuration for the app. +// It will try to load it if it is not already loaded. +func GetConfiguration() *Configuration { + if gConfig == nil { + conf, err := readConfigFile("config.json") + if err != nil { + log.Println("Error loading config file: ", err) + log.Println("Using defaults...") + } + gConfig = conf + } + + return gConfig +} + +// SetConfigValue sets a value in the configuration +// This is mostly used to customize the application and +// should be used carefully. +func SetConfigValue(key string, value string) *Configuration { + c := GetConfiguration() + if value == "" || key == "" { + return c + } + + v := reflect.ValueOf(c).Elem() + if v.Kind() == reflect.Struct { + f := v.FieldByName(key) + if f.IsValid() { + if f.CanSet() { + if f.Kind() == reflect.String { + f.SetString(value) + } + } + } + } + return c +} diff --git a/src/orchestrator/pkg/infra/config/config_test.go b/src/orchestrator/pkg/infra/config/config_test.go new file mode 100644 index 00000000..dce37e76 --- /dev/null +++ b/src/orchestrator/pkg/infra/config/config_test.go @@ -0,0 +1,40 @@ +/* + * Copyright 2019 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 config + +import ( + "testing" +) + +func TestReadConfigurationFile(t *testing.T) { + t.Run("Non Existent Configuration File", func(t *testing.T) { + _, err := readConfigFile("filedoesnotexist.json") + if err == nil { + t.Fatal("ReadConfiguationFile: Expected Error, got nil") + } + }) + + t.Run("Read Configuration File", func(t *testing.T) { + conf, err := readConfigFile("../../../tests/configs/mock_config.json") + if err != nil { + t.Fatal("ReadConfigurationFile: Error reading file: ", err) + } + if conf.DatabaseType != "mock_db_test" { + t.Fatal("ReadConfigurationFile: Incorrect entry read from file") + } + }) +} diff --git a/src/orchestrator/pkg/infra/db/README.md b/src/orchestrator/pkg/infra/db/README.md new file mode 100644 index 00000000..cba1b7ea --- /dev/null +++ b/src/orchestrator/pkg/infra/db/README.md @@ -0,0 +1,123 @@ +# Database Abstraction Layer + +This package contains implementations of the Database interface defined in `store.go` +Any database can be used as the backend as long as the following interface is implemented; + +```go +type Store interface { + // Returns nil if db health is good + HealthCheck() error + + // Unmarshal implements any unmarshaling needed for the database + Unmarshal(inp []byte, out interface{}) error + + // Creates a new master table with key and links data with tag and + // creates a pointer to the newly added data in the master table + Create(table string, key Key, tag string, data interface{}) error + + // Reads data for a particular key with specific tag. + Read(table string, key Key, tag string) ([]byte, error) + + // Update data for particular key with specific tag + Update(table string, key Key, tag string, data interface{}) error + + // Deletes a specific tag data for key. + // TODO: If tag is empty, it will delete all tags under key. + Delete(table string, key Key, tag string) error + + // Reads all master tables and data from the specified tag in table + ReadAll(table string, tag string) (map[string][]byte, error) +} +``` + +Therefore, `mongo.go`, `consul.go` implement the above interface and can be used as the backend as needed based on initial configuration. + +## Details on Mongo Implementation + +`mongo.go` implements the above interface using the `go.mongodb.org/mongo-driver` package. +The code converts incoming binary data and creates a new document in the database. + +### Create + +Arguments: +```go +collection string +key interface +tag string +data []byte +``` + +Create inserts the provided `data` into the `collection` which returns an auto-generated (by `mongodb`) ID which we then associate with the `key` that is provided as one of the arguments. + +We use the `FindOneAndUpdate` mongo API to achieve this with the `upsert` option set to `true`. +We create the following documents in mongodb for each new definition added to the database: + +There is a Master Key document that contains references to other documents which are related to this `key`. + +#### Master Key Entry +```json +{ + "_id" : ObjectId("5e0a8554b78a15f71d2dce7e"), + "key" : { "rbname" : "edgex", "rbversion" : "v1"}, + "defmetadata" : ObjectId("5e0a8554be261ecb57f067eb"), + "defcontent" : ObjectId("5e0a8377bcfcdd0f01dc7b0d") +} +``` +#### Metadata Key Entry +```json +{ + "_id" : ObjectId("5e0a8554be261ecb57f067eb"), + "defmetadata" : { "rbname" : "edgex", "rbversion" : "v1", "chartname" : "", "description" : "", "labels" : null } +} +``` +#### Definition Content +```json +{ + "_id" : ObjectId("5e0a8377bcfcdd0f01dc7b0d"), + "defcontent" : "H4sICCVd3FwAA3Byb2ZpbGUxLnRhcgDt1NEKgjAUxvFd7ylG98aWOsGXiYELxLRwJvj2rbyoIPDGiuD/uzmwM9iB7Vvruvrgw7CdXHsUn6Ejm2W3aopcP9eZLYRJM1voPN+ZndAm16kVSn9onheXMLheKeGqfdM0rq07/3bfUv9PJUkiR9+H+tSVajRymM6+lEqN7njxoVSbU+z2deX388r9nWzkr8fGSt5d79pnLOZfm0f+dRrzb7P4DZD/LyDJAAAAAAAAAAAAAAAA/+0Ksq1N5QAoAAA=" +} +``` + +### Unmarshal + +Data in mongo is stored as `bson` which is a compressed form of `json`. We need mongo to convert the stored `bson` data to regular `json` +that we can use in our code when returned. + +We just use the `bson.Unmarshal` API to achieve this. + +### Read + +Arguments: +```go +collection string +key interface +tag string +``` + +Read is straight forward and it uses the `FindOne` API to find our Mongo document based on the provided `key` and then gets the corresponding data for the given `tag`. It will return []byte which can then be passed to the `Unmarshal` function to get the desired GO object. + +### Delete + +Delete is similar to Read and deletes all the objectIDs being stored for a given `key` in the collection. + +## Testing Interfaces + +The following interface exists to allow for the development of unit tests which don't require mongo to be running. +It is mentioned so in the code as well. + +```go +// MongoCollection defines the a subset of MongoDB operations +// Note: This interface is defined mainly for mock testing +type MongoCollection interface { + InsertOne(ctx context.Context, document interface{}, + opts ...*options.InsertOneOptions) (*mongo.InsertOneResult, error) + FindOne(ctx context.Context, filter interface{}, + opts ...*options.FindOneOptions) *mongo.SingleResult + FindOneAndUpdate(ctx context.Context, filter interface{}, + update interface{}, opts ...*options.FindOneAndUpdateOptions) *mongo.SingleResult + DeleteOne(ctx context.Context, filter interface{}, + opts ...*options.DeleteOptions) (*mongo.DeleteResult, error) + Find(ctx context.Context, filter interface{}, + opts ...*options.FindOptions) (*mongo.Cursor, error) +} +``` \ No newline at end of file diff --git a/src/orchestrator/pkg/infra/db/mock.go b/src/orchestrator/pkg/infra/db/mock.go new file mode 100644 index 00000000..1dbca4b4 --- /dev/null +++ b/src/orchestrator/pkg/infra/db/mock.go @@ -0,0 +1,94 @@ +/* +Copyright 2018 Intel Corporation. +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 db + +import ( + "encoding/json" + + pkgerrors "github.com/pkg/errors" +) + +type MockKey struct { + Key string +} + +func (m MockKey) String() string { + return m.Key +} + +//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 MockDB struct { + Store + Items map[string]map[string][]byte + Err error +} + +func (m *MockDB) HealthCheck() error { + return m.Err +} + +func (m *MockDB) Create(table string, key Key, tag string, data interface{}) error { + return m.Err +} + +func (m *MockDB) Update(table string, key Key, tag string, data interface{}) error { + return m.Err +} + +// MockDB uses simple JSON and not BSON +func (m *MockDB) Unmarshal(inp []byte, out interface{}) error { + err := json.Unmarshal(inp, out) + if err != nil { + return pkgerrors.Wrap(err, "Unmarshaling json") + } + return nil +} + +func (m *MockDB) Read(table string, key Key, tag string) ([]byte, error) { + if m.Err != nil { + return nil, m.Err + } + + for k, v := range m.Items { + if k == key.String() { + return v[tag], nil + } + } + + return nil, m.Err +} + +func (m *MockDB) Delete(table string, key Key, tag string) error { + return m.Err +} + +func (m *MockDB) ReadAll(table string, tag string) (map[string][]byte, error) { + if m.Err != nil { + return nil, m.Err + } + + ret := make(map[string][]byte) + + for k, v := range m.Items { + for k1, v1 := range v { + if k1 == tag { + ret[k] = v1 + } + } + } + + return ret, nil +} diff --git a/src/orchestrator/pkg/infra/db/mongo.go b/src/orchestrator/pkg/infra/db/mongo.go new file mode 100644 index 00000000..32d0b549 --- /dev/null +++ b/src/orchestrator/pkg/infra/db/mongo.go @@ -0,0 +1,396 @@ +/* + * Copyright 2018 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 db + +import ( + "log" + + "golang.org/x/net/context" + + "github.com/onap/multicloud-k8s/src/orchestrator/pkg/infra/config" + + pkgerrors "github.com/pkg/errors" + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" +) + +// MongoCollection defines the a subset of MongoDB operations +// Note: This interface is defined mainly for mock testing +type MongoCollection interface { + InsertOne(ctx context.Context, document interface{}, + opts ...*options.InsertOneOptions) (*mongo.InsertOneResult, error) + FindOne(ctx context.Context, filter interface{}, + opts ...*options.FindOneOptions) *mongo.SingleResult + FindOneAndUpdate(ctx context.Context, filter interface{}, + update interface{}, opts ...*options.FindOneAndUpdateOptions) *mongo.SingleResult + DeleteOne(ctx context.Context, filter interface{}, + opts ...*options.DeleteOptions) (*mongo.DeleteResult, error) + Find(ctx context.Context, filter interface{}, + opts ...*options.FindOptions) (*mongo.Cursor, error) +} + +// MongoStore is an implementation of the db.Store interface +type MongoStore struct { + db *mongo.Database +} + +// This exists only for allowing us to mock the collection object +// for testing purposes +var getCollection = func(coll string, m *MongoStore) MongoCollection { + return m.db.Collection(coll) +} + +// This exists only for allowing us to mock the DecodeBytes function +// Mainly because we cannot construct a SingleResult struct from our +// tests. All fields in that struct are private. +var decodeBytes = func(sr *mongo.SingleResult) (bson.Raw, error) { + return sr.DecodeBytes() +} + +// These exists only for allowing us to mock the cursor.Next function +// Mainly because we cannot construct a mongo.Cursor struct from our +// tests. All fields in that struct are private and there is no public +// constructor method. +var cursorNext = func(ctx context.Context, cursor *mongo.Cursor) bool { + return cursor.Next(ctx) +} +var cursorClose = func(ctx context.Context, cursor *mongo.Cursor) error { + return cursor.Close(ctx) +} + +// NewMongoStore initializes a Mongo Database with the name provided +// If a database with that name exists, it will be returned +func NewMongoStore(name string, store *mongo.Database) (Store, error) { + if store == nil { + ip := "mongodb://" + config.GetConfiguration().DatabaseIP + ":27017" + clientOptions := options.Client() + clientOptions.ApplyURI(ip) + mongoClient, err := mongo.NewClient(clientOptions) + if err != nil { + return nil, err + } + + err = mongoClient.Connect(context.Background()) + if err != nil { + return nil, err + } + store = mongoClient.Database(name) + } + + return &MongoStore{ + db: store, + }, nil +} + +// HealthCheck verifies if the database is up and running +func (m *MongoStore) HealthCheck() error { + + _, err := decodeBytes(m.db.RunCommand(context.Background(), bson.D{{"serverStatus", 1}})) + if err != nil { + return pkgerrors.Wrap(err, "Error getting server status") + } + + return nil +} + +// validateParams checks to see if any parameters are empty +func (m *MongoStore) validateParams(args ...interface{}) bool { + for _, v := range args { + val, ok := v.(string) + if ok { + if val == "" { + return false + } + } else { + if v == nil { + return false + } + } + } + + return true +} + +// Create is used to create a DB entry +func (m *MongoStore) Create(coll string, key Key, tag string, data interface{}) error { + if data == nil || !m.validateParams(coll, key, tag) { + return pkgerrors.New("No Data to store") + } + + c := getCollection(coll, m) + ctx := context.Background() + + //Insert the data and then add the objectID to the masterTable + res, err := c.InsertOne(ctx, bson.D{ + {tag, data}, + }) + if err != nil { + return pkgerrors.Errorf("Error inserting into database: %s", err.Error()) + } + + //Add objectID of created data to masterKey document + //Create masterkey document if it does not exist + filter := bson.D{{"key", key}} + + _, err = decodeBytes( + c.FindOneAndUpdate( + ctx, + filter, + bson.D{ + {"$set", bson.D{ + {tag, res.InsertedID}, + }}, + }, + options.FindOneAndUpdate().SetUpsert(true).SetReturnDocument(options.After))) + + if err != nil { + return pkgerrors.Errorf("Error updating master table: %s", err.Error()) + } + + return nil +} + +// Update is used to update a DB entry +func (m *MongoStore) Update(coll string, key Key, tag string, data interface{}) error { + if data == nil || !m.validateParams(coll, key, tag) { + return pkgerrors.New("No Data to update") + } + + c := getCollection(coll, m) + ctx := context.Background() + + //Get the masterkey document based on given key + filter := bson.D{{"key", key}} + keydata, err := decodeBytes(c.FindOne(context.Background(), filter)) + if err != nil { + return pkgerrors.Errorf("Error finding master table: %s", err.Error()) + } + + //Read the tag objectID from document + tagoid, ok := keydata.Lookup(tag).ObjectIDOK() + if !ok { + return pkgerrors.Errorf("Error finding objectID for tag %s", tag) + } + + //Update the document with new data + filter = bson.D{{"_id", tagoid}} + + _, err = decodeBytes( + c.FindOneAndUpdate( + ctx, + filter, + bson.D{ + {"$set", bson.D{ + {tag, data}, + }}, + }, + options.FindOneAndUpdate().SetReturnDocument(options.After))) + + if err != nil { + return pkgerrors.Errorf("Error updating record: %s", err.Error()) + } + + return nil +} + +// Unmarshal implements an unmarshaler for bson data that +// is produced from the mongo database +func (m *MongoStore) Unmarshal(inp []byte, out interface{}) error { + err := bson.Unmarshal(inp, out) + if err != nil { + return pkgerrors.Wrap(err, "Unmarshaling bson") + } + return nil +} + +// Read method returns the data stored for this key and for this particular tag +func (m *MongoStore) Read(coll string, key Key, tag string) ([]byte, error) { + if !m.validateParams(coll, key, tag) { + return nil, pkgerrors.New("Mandatory fields are missing") + } + + c := getCollection(coll, m) + ctx := context.Background() + + //Get the masterkey document based on given key + filter := bson.D{{"key", key}} + keydata, err := decodeBytes(c.FindOne(context.Background(), filter)) + if err != nil { + return nil, pkgerrors.Errorf("Error finding master table: %s", err.Error()) + } + + //Read the tag objectID from document + tagoid, ok := keydata.Lookup(tag).ObjectIDOK() + if !ok { + return nil, pkgerrors.Errorf("Error finding objectID for tag %s", tag) + } + + //Use tag objectID to read the data from store + filter = bson.D{{"_id", tagoid}} + tagdata, err := decodeBytes(c.FindOne(ctx, filter)) + if err != nil { + return nil, pkgerrors.Errorf("Error reading found object: %s", err.Error()) + } + + //Return the data as a byte array + //Convert string data to byte array using the built-in functions + switch tagdata.Lookup(tag).Type { + case bson.TypeString: + return []byte(tagdata.Lookup(tag).StringValue()), nil + default: + return tagdata.Lookup(tag).Value, nil + } +} + +// Helper function that deletes an object by its ID +func (m *MongoStore) deleteObjectByID(coll string, objID primitive.ObjectID) error { + + c := getCollection(coll, m) + ctx := context.Background() + + _, err := c.DeleteOne(ctx, bson.D{{"_id", objID}}) + if err != nil { + return pkgerrors.Errorf("Error Deleting from database: %s", err.Error()) + } + + log.Printf("Deleted Obj with ID %s", objID.String()) + return nil +} + +// Delete method removes a document from the Database that matches key +// TODO: delete all referenced docs if tag is empty string +func (m *MongoStore) Delete(coll string, key Key, tag string) error { + if !m.validateParams(coll, key, tag) { + return pkgerrors.New("Mandatory fields are missing") + } + + c := getCollection(coll, m) + ctx := context.Background() + + //Get the masterkey document based on given key + filter := bson.D{{"key", key}} + //Remove the tag ID entry from masterkey table + update := bson.D{ + { + "$unset", bson.D{ + {tag, ""}, + }, + }, + } + keydata, err := decodeBytes(c.FindOneAndUpdate(ctx, filter, update, + options.FindOneAndUpdate().SetReturnDocument(options.Before))) + if err != nil { + //No document was found. Return nil. + if err == mongo.ErrNoDocuments { + return nil + } + //Return any other error that was found. + return pkgerrors.Errorf("Error decoding master table after update: %s", + err.Error()) + } + + //Read the tag objectID from document + elems, err := keydata.Elements() + if err != nil { + return pkgerrors.Errorf("Error reading elements from database: %s", err.Error()) + } + + tagoid, ok := keydata.Lookup(tag).ObjectIDOK() + if !ok { + return pkgerrors.Errorf("Error finding objectID for tag %s", tag) + } + + //Use tag objectID to read the data from store + err = m.deleteObjectByID(coll, tagoid) + if err != nil { + return pkgerrors.Errorf("Error deleting from database: %s", err.Error()) + } + + //Delete master table if no more tags left + //_id, key and tag should be elements in before doc + //if master table needs to be removed too + if len(elems) == 3 { + keyid, ok := keydata.Lookup("_id").ObjectIDOK() + if !ok { + return pkgerrors.Errorf("Error finding objectID for key %s", key) + } + err = m.deleteObjectByID(coll, keyid) + if err != nil { + return pkgerrors.Errorf("Error deleting master table from database: %s", err.Error()) + } + } + + return nil +} + +// ReadAll is used to get all documents in db of a particular tag +func (m *MongoStore) ReadAll(coll, tag string) (map[string][]byte, error) { + if !m.validateParams(coll, tag) { + return nil, pkgerrors.New("Missing collection or tag name") + } + + c := getCollection(coll, m) + ctx := context.Background() + + //Get all master tables in this collection + filter := bson.D{ + {"key", bson.D{ + {"$exists", true}, + }}, + } + cursor, err := c.Find(ctx, filter) + if err != nil { + return nil, pkgerrors.Errorf("Error reading from database: %s", err.Error()) + } + defer cursorClose(ctx, cursor) + + //Iterate over all the master tables + result := make(map[string][]byte) + for cursorNext(ctx, cursor) { + d := cursor.Current + + //Read key of each master table + key, ok := d.Lookup("key").DocumentOK() + if !ok { + //Throw error if key is not found + pkgerrors.New("Unable to read key from mastertable") + } + + //Get objectID of tag document + tid, ok := d.Lookup(tag).ObjectIDOK() + if !ok { + log.Printf("Did not find tag: %s", tag) + continue + } + + //Find tag document and unmarshal it into []byte + tagData, err := decodeBytes(c.FindOne(ctx, bson.D{{"_id", tid}})) + if err != nil { + log.Printf("Unable to decode tag data %s", err.Error()) + continue + } + result[key.String()] = tagData.Lookup(tag).Value + } + + if len(result) == 0 { + return result, pkgerrors.Errorf("Did not find any objects with tag: %s", tag) + } + + return result, nil +} diff --git a/src/orchestrator/pkg/infra/db/mongo_test.go b/src/orchestrator/pkg/infra/db/mongo_test.go new file mode 100644 index 00000000..171c908f --- /dev/null +++ b/src/orchestrator/pkg/infra/db/mongo_test.go @@ -0,0 +1,597 @@ +/* + * Copyright 2018 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 db + +import ( + "bytes" + "context" + "reflect" + "strings" + "testing" + + pkgerrors "github.com/pkg/errors" + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" +) + +//Implements the functions used currently in mongo.go +type mockCollection struct { + Err error + mCursor *mongo.Cursor + mCursorCount int +} + +func (c *mockCollection) InsertOne(ctx context.Context, document interface{}, + opts ...*options.InsertOneOptions) (*mongo.InsertOneResult, error) { + + if c.Err != nil { + return nil, c.Err + } + + return &mongo.InsertOneResult{InsertedID: "_id1234"}, nil +} + +func (c *mockCollection) FindOne(ctx context.Context, filter interface{}, + opts ...*options.FindOneOptions) *mongo.SingleResult { + + return &mongo.SingleResult{} +} + +func (c *mockCollection) FindOneAndUpdate(ctx context.Context, filter interface{}, + update interface{}, opts ...*options.FindOneAndUpdateOptions) *mongo.SingleResult { + + return &mongo.SingleResult{} +} + +func (c *mockCollection) DeleteOne(ctx context.Context, filter interface{}, + opts ...*options.DeleteOptions) (*mongo.DeleteResult, error) { + + return nil, c.Err +} + +func (c *mockCollection) Find(ctx context.Context, filter interface{}, + opts ...*options.FindOptions) (*mongo.Cursor, error) { + + return c.mCursor, c.Err +} + +func TestCreate(t *testing.T) { + testCases := []struct { + label string + input map[string]interface{} + mockColl *mockCollection + bson bson.Raw + expectedError string + }{ + { + label: "Successfull creation of entry", + input: map[string]interface{}{ + "coll": "collname", + "key": MockKey{Key: "keyvalue"}, + "tag": "tagName", + "data": "Data In String Format", + }, + bson: bson.Raw{'\x08', '\x00', '\x00', '\x00', '\x0A', 'x', '\x00', '\x00'}, + mockColl: &mockCollection{}, + }, + { + label: "UnSuccessfull creation of entry", + input: map[string]interface{}{ + "coll": "collname", + "key": MockKey{Key: "keyvalue"}, + "tag": "tagName", + "data": "Data In String Format", + }, + mockColl: &mockCollection{ + Err: pkgerrors.New("DB Error"), + }, + expectedError: "DB Error", + }, + { + label: "Missing input fields", + input: map[string]interface{}{ + "coll": "", + "key": MockKey{Key: ""}, + "tag": "", + "data": "", + }, + expectedError: "No Data to store", + mockColl: &mockCollection{}, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.label, func(t *testing.T) { + m, _ := NewMongoStore("name", &mongo.Database{}) + // Override the getCollection function with our mocked version + getCollection = func(coll string, m *MongoStore) MongoCollection { + return testCase.mockColl + } + + decodeBytes = func(sr *mongo.SingleResult) (bson.Raw, error) { + return testCase.bson, testCase.mockColl.Err + } + + err := m.Create(testCase.input["coll"].(string), testCase.input["key"].(Key), + testCase.input["tag"].(string), testCase.input["data"]) + if err != nil { + if testCase.expectedError == "" { + t.Fatalf("Create method returned an un-expected (%s)", err) + } + if !strings.Contains(string(err.Error()), testCase.expectedError) { + t.Fatalf("Create method returned an error (%s)", err) + } + } + }) + } +} + +func TestUpdate(t *testing.T) { + testCases := []struct { + label string + input map[string]interface{} + mockColl *mockCollection + bson bson.Raw + expectedError string + }{ + { + label: "Successfull update of entry", + input: map[string]interface{}{ + "coll": "collname", + "key": MockKey{Key: "keyvalue"}, + "tag": "metadata", + "data": "Data In String Format", + }, + // Binary form of + // { + // "_id" : ObjectId("5c115156777ff85654248ae1"), + // "key" : bson.D{{"name","testdef"},{"version","v1"}}, + // "metadata" : ObjectId("5c115156c9755047e318bbfd") + // } + bson: bson.Raw{ + '\x58', '\x00', '\x00', '\x00', '\x03', '\x6b', '\x65', '\x79', + '\x00', '\x27', '\x00', '\x00', '\x00', '\x02', '\x6e', '\x61', + '\x6d', '\x65', '\x00', '\x08', '\x00', '\x00', '\x00', '\x74', + '\x65', '\x73', '\x74', '\x64', '\x65', '\x66', '\x00', '\x02', + '\x76', '\x65', '\x72', '\x73', '\x69', '\x6f', '\x6e', '\x00', + '\x03', '\x00', '\x00', '\x00', '\x76', '\x31', '\x00', '\x00', + '\x07', '\x6d', '\x65', '\x74', '\x61', '\x64', '\x61', '\x74', + '\x61', '\x00', '\x5c', '\x11', '\x51', '\x56', '\x77', '\x7f', + '\xf8', '\x56', '\x54', '\x24', '\x8a', '\xe1', '\x07', '\x5f', + '\x69', '\x64', '\x00', '\x5c', '\x11', '\x51', '\x56', '\x77', + '\x7f', '\xf8', '\x56', '\x54', '\x24', '\x8a', '\xe1', '\x00', + }, + mockColl: &mockCollection{}, + }, + { + label: "Entry does not exist", + input: map[string]interface{}{ + "coll": "collname", + "key": MockKey{Key: "keyvalue"}, + "tag": "tagName", + "data": "Data In String Format", + }, + mockColl: &mockCollection{ + Err: pkgerrors.New("DB Error"), + }, + expectedError: "DB Error", + }, + } + + for _, testCase := range testCases { + t.Run(testCase.label, func(t *testing.T) { + m, _ := NewMongoStore("name", &mongo.Database{}) + // Override the getCollection function with our mocked version + getCollection = func(coll string, m *MongoStore) MongoCollection { + return testCase.mockColl + } + + decodeBytes = func(sr *mongo.SingleResult) (bson.Raw, error) { + return testCase.bson, testCase.mockColl.Err + } + + err := m.Update(testCase.input["coll"].(string), testCase.input["key"].(Key), + testCase.input["tag"].(string), testCase.input["data"]) + if err != nil { + if testCase.expectedError == "" { + t.Fatalf("Create method returned an un-expected (%s)", err) + } + if !strings.Contains(string(err.Error()), testCase.expectedError) { + t.Fatalf("Create method returned an error (%s)", err) + } + } + }) + } +} + +func TestRead(t *testing.T) { + testCases := []struct { + label string + input map[string]interface{} + mockColl *mockCollection + bson bson.Raw + expectedError string + expected []byte + }{ + { + label: "Successfull Read of entry", + input: map[string]interface{}{ + "coll": "collname", + "key": MockKey{Key: "keyvalue"}, + "tag": "metadata", + }, + // Binary form of + // { + // "_id" : ObjectId("5c115156777ff85654248ae1"), + // "key" : bson.D{{"name","testdef"},{"version","v1"}}, + // "metadata" : ObjectId("5c115156c9755047e318bbfd") + // } + bson: bson.Raw{ + '\x58', '\x00', '\x00', '\x00', '\x03', '\x6b', '\x65', '\x79', + '\x00', '\x27', '\x00', '\x00', '\x00', '\x02', '\x6e', '\x61', + '\x6d', '\x65', '\x00', '\x08', '\x00', '\x00', '\x00', '\x74', + '\x65', '\x73', '\x74', '\x64', '\x65', '\x66', '\x00', '\x02', + '\x76', '\x65', '\x72', '\x73', '\x69', '\x6f', '\x6e', '\x00', + '\x03', '\x00', '\x00', '\x00', '\x76', '\x31', '\x00', '\x00', + '\x07', '\x6d', '\x65', '\x74', '\x61', '\x64', '\x61', '\x74', + '\x61', '\x00', '\x5c', '\x11', '\x51', '\x56', '\x77', '\x7f', + '\xf8', '\x56', '\x54', '\x24', '\x8a', '\xe1', '\x07', '\x5f', + '\x69', '\x64', '\x00', '\x5c', '\x11', '\x51', '\x56', '\x77', + '\x7f', '\xf8', '\x56', '\x54', '\x24', '\x8a', '\xe1', '\x00', + }, + mockColl: &mockCollection{}, + // This is not the document because we are mocking decodeBytes + expected: []byte{92, 17, 81, 86, 119, 127, 248, 86, 84, 36, 138, 225}, + }, + { + label: "UnSuccessfull Read of entry: object not found", + input: map[string]interface{}{ + "coll": "collname", + "key": MockKey{Key: "keyvalue"}, + "tag": "badtag", + }, + // Binary form of + // { + // "_id" : ObjectId("5c115156777ff85654248ae1"), + // "key" : bson.D{{"name","testdef"},{"version","v1"}}, + // "metadata" : ObjectId("5c115156c9755047e318bbfd") + // } + bson: bson.Raw{ + '\x58', '\x00', '\x00', '\x00', '\x03', '\x6b', '\x65', '\x79', + '\x00', '\x27', '\x00', '\x00', '\x00', '\x02', '\x6e', '\x61', + '\x6d', '\x65', '\x00', '\x08', '\x00', '\x00', '\x00', '\x74', + '\x65', '\x73', '\x74', '\x64', '\x65', '\x66', '\x00', '\x02', + '\x76', '\x65', '\x72', '\x73', '\x69', '\x6f', '\x6e', '\x00', + '\x03', '\x00', '\x00', '\x00', '\x76', '\x31', '\x00', '\x00', + '\x07', '\x6d', '\x65', '\x74', '\x61', '\x64', '\x61', '\x74', + '\x61', '\x00', '\x5c', '\x11', '\x51', '\x56', '\x77', '\x7f', + '\xf8', '\x56', '\x54', '\x24', '\x8a', '\xe1', '\x07', '\x5f', + '\x69', '\x64', '\x00', '\x5c', '\x11', '\x51', '\x56', '\x77', + '\x7f', '\xf8', '\x56', '\x54', '\x24', '\x8a', '\xe1', '\x00', + }, + mockColl: &mockCollection{}, + expectedError: "Error finding objectID", + }, + { + label: "UnSuccessfull Read of entry", + input: map[string]interface{}{ + "coll": "collname", + "key": MockKey{Key: "keyvalue"}, + "tag": "tagName", + }, + mockColl: &mockCollection{ + Err: pkgerrors.New("DB Error"), + }, + expectedError: "DB Error", + }, + { + label: "Missing input fields", + input: map[string]interface{}{ + "coll": "", + "key": MockKey{Key: ""}, + "tag": "", + }, + expectedError: "Mandatory fields are missing", + mockColl: &mockCollection{}, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.label, func(t *testing.T) { + m, _ := NewMongoStore("name", &mongo.Database{}) + // Override the getCollection function with our mocked version + getCollection = func(coll string, m *MongoStore) MongoCollection { + return testCase.mockColl + } + + decodeBytes = func(sr *mongo.SingleResult) (bson.Raw, error) { + return testCase.bson, testCase.mockColl.Err + } + got, err := m.Read(testCase.input["coll"].(string), testCase.input["key"].(Key), + testCase.input["tag"].(string)) + if err != nil { + if testCase.expectedError == "" { + t.Fatalf("Read method returned an un-expected (%s)", err) + } + if !strings.Contains(string(err.Error()), testCase.expectedError) { + t.Fatalf("Read method returned an error (%s)", err) + } + } else { + if bytes.Compare(got, testCase.expected) != 0 { + t.Fatalf("Read returned unexpected data: %v, expected: %v", + string(got), testCase.expected) + } + } + }) + } +} + +func TestDelete(t *testing.T) { + testCases := []struct { + label string + input map[string]interface{} + mockColl *mockCollection + bson bson.Raw + expectedError string + }{ + { + label: "Successfull Delete of entry", + input: map[string]interface{}{ + "coll": "collname", + "key": MockKey{Key: "keyvalue"}, + "tag": "metadata", + }, + // Binary form of + // { + // "_id" : ObjectId("5c115156777ff85654248ae1"), + // "key" : bson.D{{"name","testdef"},{"version","v1"}}, + // "metadata" : ObjectId("5c115156c9755047e318bbfd") + // } + bson: bson.Raw{ + '\x58', '\x00', '\x00', '\x00', '\x03', '\x6b', '\x65', '\x79', + '\x00', '\x27', '\x00', '\x00', '\x00', '\x02', '\x6e', '\x61', + '\x6d', '\x65', '\x00', '\x08', '\x00', '\x00', '\x00', '\x74', + '\x65', '\x73', '\x74', '\x64', '\x65', '\x66', '\x00', '\x02', + '\x76', '\x65', '\x72', '\x73', '\x69', '\x6f', '\x6e', '\x00', + '\x03', '\x00', '\x00', '\x00', '\x76', '\x31', '\x00', '\x00', + '\x07', '\x6d', '\x65', '\x74', '\x61', '\x64', '\x61', '\x74', + '\x61', '\x00', '\x5c', '\x11', '\x51', '\x56', '\x77', '\x7f', + '\xf8', '\x56', '\x54', '\x24', '\x8a', '\xe1', '\x07', '\x5f', + '\x69', '\x64', '\x00', '\x5c', '\x11', '\x51', '\x56', '\x77', + '\x7f', '\xf8', '\x56', '\x54', '\x24', '\x8a', '\xe1', '\x00', + }, + mockColl: &mockCollection{}, + }, + { + label: "UnSuccessfull Delete of entry", + input: map[string]interface{}{ + "coll": "collname", + "key": MockKey{Key: "keyvalue"}, + "tag": "tagName", + }, + mockColl: &mockCollection{ + Err: pkgerrors.New("DB Error"), + }, + expectedError: "DB Error", + }, + { + label: "UnSuccessfull Delete, key not found", + input: map[string]interface{}{ + "coll": "collname", + "key": MockKey{Key: "keyvalue"}, + "tag": "tagName", + }, + // Binary form of + // { + // "_id" : ObjectId("5c115156777ff85654248ae1"), + // "key" : bson.D{{"name","testdef"},{"version","v1"}}, + // "metadata" : ObjectId("5c115156c9755047e318bbfd") + // } + bson: bson.Raw{ + '\x58', '\x00', '\x00', '\x00', '\x03', '\x6b', '\x65', '\x79', + '\x00', '\x27', '\x00', '\x00', '\x00', '\x02', '\x6e', '\x61', + '\x6d', '\x65', '\x00', '\x08', '\x00', '\x00', '\x00', '\x74', + '\x65', '\x73', '\x74', '\x64', '\x65', '\x66', '\x00', '\x02', + '\x76', '\x65', '\x72', '\x73', '\x69', '\x6f', '\x6e', '\x00', + '\x03', '\x00', '\x00', '\x00', '\x76', '\x31', '\x00', '\x00', + '\x07', '\x6d', '\x65', '\x74', '\x61', '\x64', '\x61', '\x74', + '\x61', '\x00', '\x5c', '\x11', '\x51', '\x56', '\x77', '\x7f', + '\xf8', '\x56', '\x54', '\x24', '\x8a', '\xe1', '\x07', '\x5f', + '\x69', '\x64', '\x00', '\x5c', '\x11', '\x51', '\x56', '\x77', + '\x7f', '\xf8', '\x56', '\x54', '\x24', '\x8a', '\xe1', '\x00', + }, + mockColl: &mockCollection{}, + expectedError: "Error finding objectID", + }, + { + label: "Missing input fields", + input: map[string]interface{}{ + "coll": "", + "key": MockKey{Key: ""}, + "tag": "", + }, + expectedError: "Mandatory fields are missing", + mockColl: &mockCollection{}, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.label, func(t *testing.T) { + m, _ := NewMongoStore("name", &mongo.Database{}) + // Override the getCollection function with our mocked version + getCollection = func(coll string, m *MongoStore) MongoCollection { + return testCase.mockColl + } + + decodeBytes = func(sr *mongo.SingleResult) (bson.Raw, error) { + return testCase.bson, testCase.mockColl.Err + } + err := m.Delete(testCase.input["coll"].(string), testCase.input["key"].(Key), + testCase.input["tag"].(string)) + if err != nil { + if testCase.expectedError == "" { + t.Fatalf("Delete method returned an un-expected (%s)", err) + } + if !strings.Contains(string(err.Error()), testCase.expectedError) { + t.Fatalf("Delete method returned an error (%s)", err) + } + } + }) + } +} + +func TestReadAll(t *testing.T) { + testCases := []struct { + label string + input map[string]interface{} + mockColl *mockCollection + bson bson.Raw + expectedError string + expected map[string][]byte + }{ + { + label: "Successfully Read all entries", + input: map[string]interface{}{ + "coll": "collname", + "tag": "metadata", + }, + mockColl: &mockCollection{ + mCursor: &mongo.Cursor{ + // Binary form of + // { + // "_id" : ObjectId("5c115156777ff85654248ae1"), + // "key" : bson.D{{"name","testdef"},{"version","v1"}}, + // "metadata" : ObjectId("5c115156c9755047e318bbfd") + // } + + Current: bson.Raw{ + '\x58', '\x00', '\x00', '\x00', '\x03', '\x6b', '\x65', '\x79', + '\x00', '\x27', '\x00', '\x00', '\x00', '\x02', '\x6e', '\x61', + '\x6d', '\x65', '\x00', '\x08', '\x00', '\x00', '\x00', '\x74', + '\x65', '\x73', '\x74', '\x64', '\x65', '\x66', '\x00', '\x02', + '\x76', '\x65', '\x72', '\x73', '\x69', '\x6f', '\x6e', '\x00', + '\x03', '\x00', '\x00', '\x00', '\x76', '\x31', '\x00', '\x00', + '\x07', '\x6d', '\x65', '\x74', '\x61', '\x64', '\x61', '\x74', + '\x61', '\x00', '\x5c', '\x11', '\x51', '\x56', '\x77', '\x7f', + '\xf8', '\x56', '\x54', '\x24', '\x8a', '\xe1', '\x07', '\x5f', + '\x69', '\x64', '\x00', '\x5c', '\x11', '\x51', '\x56', '\x77', + '\x7f', '\xf8', '\x56', '\x54', '\x24', '\x8a', '\xe1', '\x00', + }, + }, + mCursorCount: 1, + }, + expected: map[string][]byte{ + `{"name": "testdef","version": "v1"}`: []byte{ + 92, 17, 81, 86, 119, 127, 248, 86, 84, 36, 138, 225}, + }, + }, + { + label: "UnSuccessfully Read of all entries", + input: map[string]interface{}{ + "coll": "collname", + "tag": "tagName", + }, + mockColl: &mockCollection{ + Err: pkgerrors.New("DB Error"), + }, + expectedError: "DB Error", + }, + { + label: "UnSuccessfull Readall, tag not found", + input: map[string]interface{}{ + "coll": "collname", + "tag": "tagName", + }, + mockColl: &mockCollection{ + mCursor: &mongo.Cursor{ + // Binary form of + // { + // "_id" : ObjectId("5c115156777ff85654248ae1"), + // "key" : bson.D{{"name","testdef"},{"version","v1"}}, + // "metadata" : ObjectId("5c115156c9755047e318bbfd") + // } + Current: bson.Raw{ + '\x58', '\x00', '\x00', '\x00', '\x03', '\x6b', '\x65', '\x79', + '\x00', '\x27', '\x00', '\x00', '\x00', '\x02', '\x6e', '\x61', + '\x6d', '\x65', '\x00', '\x08', '\x00', '\x00', '\x00', '\x74', + '\x65', '\x73', '\x74', '\x64', '\x65', '\x66', '\x00', '\x02', + '\x76', '\x65', '\x72', '\x73', '\x69', '\x6f', '\x6e', '\x00', + '\x03', '\x00', '\x00', '\x00', '\x76', '\x31', '\x00', '\x00', + '\x07', '\x6d', '\x65', '\x74', '\x61', '\x64', '\x61', '\x74', + '\x61', '\x00', '\x5c', '\x11', '\x51', '\x56', '\x77', '\x7f', + '\xf8', '\x56', '\x54', '\x24', '\x8a', '\xe1', '\x07', '\x5f', + '\x69', '\x64', '\x00', '\x5c', '\x11', '\x51', '\x56', '\x77', + '\x7f', '\xf8', '\x56', '\x54', '\x24', '\x8a', '\xe1', '\x00', + }, + }, + mCursorCount: 1, + }, + expectedError: "Did not find any objects with tag", + }, + { + label: "Missing input fields", + input: map[string]interface{}{ + "coll": "", + "tag": "", + }, + expectedError: "Missing collection or tag name", + mockColl: &mockCollection{}, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.label, func(t *testing.T) { + m, _ := NewMongoStore("name", &mongo.Database{}) + // Override the getCollection function with our mocked version + getCollection = func(coll string, m *MongoStore) MongoCollection { + return testCase.mockColl + } + + decodeBytes = func(sr *mongo.SingleResult) (bson.Raw, error) { + return testCase.mockColl.mCursor.Current, testCase.mockColl.Err + } + + cursorNext = func(ctx context.Context, cursor *mongo.Cursor) bool { + if testCase.mockColl.mCursorCount > 0 { + testCase.mockColl.mCursorCount -= 1 + return true + } + return false + } + + cursorClose = func(ctx context.Context, cursor *mongo.Cursor) error { + return nil + } + + got, err := m.ReadAll(testCase.input["coll"].(string), testCase.input["tag"].(string)) + if err != nil { + if testCase.expectedError == "" { + t.Fatalf("Readall method returned an un-expected (%s)", err) + } + if !strings.Contains(string(err.Error()), testCase.expectedError) { + t.Fatalf("Readall method returned an error (%s)", err) + } + } else { + if reflect.DeepEqual(got, testCase.expected) == false { + t.Fatalf("Readall returned unexpected data: %v, expected: %v", + got, testCase.expected) + } + } + }) + } +} diff --git a/src/orchestrator/pkg/infra/db/store.go b/src/orchestrator/pkg/infra/db/store.go new file mode 100644 index 00000000..1a9632e7 --- /dev/null +++ b/src/orchestrator/pkg/infra/db/store.go @@ -0,0 +1,106 @@ +/* +Copyright 2018 Intel Corporation. +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 db + +import ( + "encoding/json" + "reflect" + + "github.com/onap/multicloud-k8s/src/orchestrator/pkg/infra/config" + + pkgerrors "github.com/pkg/errors" +) + +// DBconn interface used to talk a concrete Database connection +var DBconn Store + +// Key is an interface that will be implemented by anypackage +// that wants to use the Store interface. This allows various +// db backends and key types. +type Key interface { + String() string +} + +// Store is an interface for accessing the database +type Store interface { + // Returns nil if db health is good + HealthCheck() error + + // Unmarshal implements any unmarshaling needed for the database + Unmarshal(inp []byte, out interface{}) error + + // Creates a new master document with key and links data with tag and + // creates a pointer(row) to the newly added data in the master table + Create(table string, key Key, tag string, data interface{}) error + + // Reads data for a particular key with specific tag. + Read(table string, key Key, tag string) ([]byte, error) + + // Update data for particular key with specific tag + Update(table string, key Key, tag string, data interface{}) error + + // Deletes a specific tag data for key. + // TODO: If tag is empty, it will delete all tags under key. + Delete(table string, key Key, tag string) error + + // Reads all master tables and data from the specified tag in table + ReadAll(table string, tag string) (map[string][]byte, error) +} + +// CreateDBClient creates the DB client +func createDBClient(dbType string) error { + var err error + switch dbType { + case "mongo": + // create a mongodb database with orchestrator as the name + DBconn, err = NewMongoStore("orchestrator", nil) + default: + return pkgerrors.New(dbType + "DB not supported") + } + return err +} + +// Serialize converts given data into a JSON string +func Serialize(v interface{}) (string, error) { + out, err := json.Marshal(v) + if err != nil { + return "", pkgerrors.Wrap(err, "Error serializing "+reflect.TypeOf(v).String()) + } + return string(out), nil +} + +// DeSerialize converts string to a json object specified by type +func DeSerialize(str string, v interface{}) error { + err := json.Unmarshal([]byte(str), &v) + if err != nil { + return pkgerrors.Wrap(err, "Error deSerializing "+str) + } + return nil +} + +// InitializeDatabaseConnection sets up the connection to the +// configured database to allow the application to talk to it. +func InitializeDatabaseConnection() error { + err := createDBClient(config.GetConfiguration().DatabaseType) + if err != nil { + return pkgerrors.Cause(err) + } + + err = DBconn.HealthCheck() + if err != nil { + return pkgerrors.Cause(err) + } + + return nil +} diff --git a/src/orchestrator/pkg/infra/db/store_test.go b/src/orchestrator/pkg/infra/db/store_test.go new file mode 100644 index 00000000..42a41787 --- /dev/null +++ b/src/orchestrator/pkg/infra/db/store_test.go @@ -0,0 +1,121 @@ +/* +Copyright 2018 Intel Corporation. +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 db + +import ( + "reflect" + "strings" + "testing" +) + +func TestCreateDBClient(t *testing.T) { + t.Run("Successfully create DB client", func(t *testing.T) { + expected := &MongoStore{} + + err := createDBClient("mongo") + if err != nil { + t.Fatalf("CreateDBClient returned an error (%s)", err) + } + if reflect.TypeOf(DBconn) != reflect.TypeOf(expected) { + t.Fatalf("CreateDBClient set DBconn as:\n result=%T\n expected=%T", DBconn, expected) + } + }) + t.Run("Fail to create client for unsupported DB", func(t *testing.T) { + err := createDBClient("fakeDB") + if err == nil { + t.Fatal("CreateDBClient didn't return an error") + } + if !strings.Contains(string(err.Error()), "DB not supported") { + t.Fatalf("CreateDBClient method returned an error (%s)", err) + } + }) +} + +func TestSerialize(t *testing.T) { + + inp := map[string]interface{}{ + "UUID": "123e4567-e89b-12d3-a456-426655440000", + "Data": "sdaijsdiodalkfjsdlagf", + "Number": 23, + "Float": 34.4, + "Map": map[string]interface{}{ + "m1": "m1", + "m2": 2, + "m3": 3.0, + }, + } + + got, err := Serialize(inp) + if err != nil { + t.Fatal(err) + } + + expected := "{\"Data\":\"sdaijsdiodalkfjsdlagf\"," + + "\"Float\":34.4,\"Map\":{\"m1\":\"m1\",\"m2\":2,\"m3\":3}," + + "\"Number\":23,\"UUID\":\"123e4567-e89b-12d3-a456-426655440000\"}" + + if expected != got { + t.Errorf("Serialize returned unexpected string: %s;"+ + " expected %sv", got, expected) + } +} + +func TestDeSerialize(t *testing.T) { + testCases := []struct { + label string + input string + expected map[string]interface{} + errMsg string + }{ + { + label: "Sucessful deserialize entry", + input: "{\"Data\":\"sdaijsdiodalkfjsdlagf\"," + + "\"Float\":34.4,\"Map\":{\"m1\":\"m1\",\"m3\":3}," + + "\"UUID\":\"123e4567-e89b-12d3-a456-426655440000\"}", + expected: map[string]interface{}{ + "UUID": "123e4567-e89b-12d3-a456-426655440000", + "Data": "sdaijsdiodalkfjsdlagf", + "Float": 34.4, + "Map": map[string]interface{}{ + "m1": "m1", + "m3": 3.0, + }, + }, + }, + { + label: "Fail to deserialize invalid entry", + input: "{invalid}", + errMsg: "Error deSerializing {invalid}: invalid character 'i' looking for beginning of object key string", + }, + } + for _, testCase := range testCases { + t.Run(testCase.label, func(t *testing.T) { + got := make(map[string]interface{}) + err := DeSerialize(testCase.input, &got) + if err != nil { + if testCase.errMsg == "" { + t.Fatalf("DeSerialize method return an un-expected (%s)", err) + } + if !strings.Contains(string(err.Error()), testCase.errMsg) { + t.Fatalf("DeSerialize method returned an error (%s)", err) + } + } else { + if !reflect.DeepEqual(testCase.expected, got) { + t.Errorf("Serialize returned unexpected : %v;"+ + " expected %v", got, testCase.expected) + } + } + }) + } +} diff --git a/src/orchestrator/pkg/infra/logutils/logger.go b/src/orchestrator/pkg/infra/logutils/logger.go new file mode 100644 index 00000000..2e8f9969 --- /dev/null +++ b/src/orchestrator/pkg/infra/logutils/logger.go @@ -0,0 +1,28 @@ +package logutils + +import ( + log "github.com/sirupsen/logrus" +) + +//Fields is type that will be used by the calling function +type Fields map[string]interface{} + +func init() { + // Log as JSON instead of the default ASCII formatter. + log.SetFormatter(&log.JSONFormatter{}) +} + +// Error uses the fields provided and logs +func Error(msg string, fields Fields) { + log.WithFields(log.Fields(fields)).Error(msg) +} + +// Warn uses the fields provided and logs +func Warn(msg string, fields Fields) { + log.WithFields(log.Fields(fields)).Warn(msg) +} + +// Info uses the fields provided and logs +func Info(msg string, fields Fields) { + log.WithFields(log.Fields(fields)).Info(msg) +} diff --git a/src/orchestrator/pkg/module/module.go b/src/orchestrator/pkg/module/module.go new file mode 100644 index 00000000..e4482098 --- /dev/null +++ b/src/orchestrator/pkg/module/module.go @@ -0,0 +1,34 @@ +/* + * Copyright 2018 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 ( + ) + +// Client for using the services in the orchestrator +type Client struct { + Project *ProjectClient + // Add Clients for API's here +} + +// NewClient creates a new client for using the services +func NewClient() *Client { + c:= &Client{} + c.Project = NewProjectClient() + // Add Client API handlers here + return c +} \ No newline at end of file diff --git a/src/orchestrator/pkg/module/project.go b/src/orchestrator/pkg/module/project.go new file mode 100644 index 00000000..e44164f9 --- /dev/null +++ b/src/orchestrator/pkg/module/project.go @@ -0,0 +1,133 @@ +/* + * Copyright 2019 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 ( + "encoding/json" + + "github.com/onap/multicloud-k8s/src/orchestrator/pkg/infra/db" + + pkgerrors "github.com/pkg/errors" +) + +// Project contains the parameters needed for Projects +// It implements the interface for managing the Projects +type Project struct { + ProjectName string `json:"project-name"` + Description string `json:"description"` +} + +// ProjectKey is the key structure that is used in the database +type ProjectKey struct { + ProjectName string `json:"rb-name"` +} + +// We will use json marshalling to convert to string to +// preserve the underlying structure. +func (pk ProjectKey) String() string { + out, err := json.Marshal(pk) + if err != nil { + return "" + } + + return string(out) +} + +// Manager is an interface exposes the Project functionality +type ProjectManager interface { + CreateProject(pr Project) (Project, error) + GetProject(name string) (Project, error) + DeleteProject(name string) error +} + +// ProjectClient implements the Manager +// It will also be used to maintain some localized state +type ProjectClient struct { + storeName string + tagMeta, tagContent string +} + +// NewProjectClient returns an instance of the ProjectClient +// which implements the Manager +func NewProjectClient() *ProjectClient { + return &ProjectClient{ + tagMeta: "projectmetadata", + } +} + +// CreateProject a new collection based on the project +func (v *ProjectClient) CreateProject(p Project) (Project, error) { + + //Construct the composite key to select the entry + key := ProjectKey{ + ProjectName: p.ProjectName, + } + + //Check if this Project already exists + _, err := v.GetProject(p.ProjectName) + if err == nil { + return Project{}, pkgerrors.New("Project already exists") + } + + err = db.DBconn.Create(p.ProjectName, key, v.tagMeta, p) + if err != nil { + return Project{}, pkgerrors.Wrap(err, "Creating DB Entry") + } + + return p, nil +} + +// GetProject returns the Project for corresponding name +func (v *ProjectClient) GetProject(name string) (Project, error) { + + //Construct the composite key to select the entry + key := ProjectKey{ + ProjectName: name, + } + value, err := db.DBconn.Read(name, key, v.tagMeta) + if err != nil { + return Project{}, pkgerrors.Wrap(err, "Get Project") + } + + //value is a byte array + if value != nil { + proj := Project{} + err = db.DBconn.Unmarshal(value, &proj) + if err != nil { + return Project{}, pkgerrors.Wrap(err, "Unmarshaling Value") + } + return proj, nil + } + + return Project{}, pkgerrors.New("Error getting Project") +} + +// DeleteProject the Project from database +func (v *ProjectClient) DeleteProject(name string) error { + + //Construct the composite key to select the entry + key := ProjectKey{ + ProjectName: name, + } + err := db.DBconn.Delete(name, key, v.tagMeta) + if err != nil { + return pkgerrors.Wrap(err, "Delete Project Entry;") + } + + //TODO: Delete the collection when the project is deleted + return nil +} diff --git a/src/orchestrator/pkg/module/project_test.go b/src/orchestrator/pkg/module/project_test.go new file mode 100644 index 00000000..7f4d9b3e --- /dev/null +++ b/src/orchestrator/pkg/module/project_test.go @@ -0,0 +1,177 @@ +/* + * Copyright 2018 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" +) + +func TestCreateProject(t *testing.T) { + testCases := []struct { + label string + inp Project + expectedError string + mockdb *db.MockDB + expected Project + }{ + { + label: "Create Project", + inp: Project{ + ProjectName: "testProject", + Description: "A sample Project used for unit testing", + }, + expected: Project{ + ProjectName: "testProject", + Description: "A sample Project used for unit testing", + }, + expectedError: "", + mockdb: &db.MockDB{}, + }, + { + label: "Failed Create Project", + expectedError: "Error Creating Project", + mockdb: &db.MockDB{ + Err: pkgerrors.New("Error Creating Project"), + }, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.label, func(t *testing.T) { + db.DBconn = testCase.mockdb + impl := NewProjectClient() + got, err := impl.CreateProject(testCase.inp) + 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 TestGetProject(t *testing.T) { + + testCases := []struct { + label string + name string + expectedError string + mockdb *db.MockDB + inp string + expected Project + }{ + { + label: "Get Project", + name: "testProject", + expected: Project{ + ProjectName: "testProject", + Description: "Test project for unit testing", + }, + expectedError: "", + mockdb: &db.MockDB{ + Items: map[string]map[string][]byte{ + ProjectKey{ProjectName: "testProject"}.String(): { + "projectmetadata": []byte( + "{\"project-name\":\"testProject\"," + + "\"description\":\"Test project for unit testing\"}"), + }, + }, + }, + }, + { + 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 := NewProjectClient() + got, err := impl.GetProject(testCase.name) + 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 TestDeleteProject(t *testing.T) { + + testCases := []struct { + label string + name string + expectedError string + mockdb *db.MockDB + }{ + { + label: "Delete Project", + name: "testProject", + mockdb: &db.MockDB{}, + }, + { + 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 := NewProjectClient() + err := impl.DeleteProject(testCase.name) + 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) + } + } + }) + } +} -- cgit 1.2.3-korg