aboutsummaryrefslogtreecommitdiffstats
path: root/src/orchestrator/internal
diff options
context:
space:
mode:
authorKiran Kamineni <kiran.k.kamineni@intel.com>2019-11-19 11:51:46 -0800
committerKiran Kamineni <kiran.k.kamineni@intel.com>2019-11-19 16:10:31 -0800
commit432cee9b36a87f63e787fe4465ec385aa750e172 (patch)
tree7ebcaa63f7e629e25602bccb041f26079c2f6f0b /src/orchestrator/internal
parentc58a35989f731a0d1c8ec75e85480873da4b1c35 (diff)
Add v2 with project API
Definiton, Profile and other APIs will migrate to this area with support for Projects and v2. This patch adds the Project API only along with support for the Mongo database. Migration of other APIs will happen in future patches Issue-ID: MULTICLOUD-871 Change-Id: I2eb2d0db2384fd58d1ec874e24fa9125a1f5b288 Signed-off-by: Kiran Kamineni <kiran.k.kamineni@intel.com>
Diffstat (limited to 'src/orchestrator/internal')
-rw-r--r--src/orchestrator/internal/auth/auth.go107
-rw-r--r--src/orchestrator/internal/auth/auth_test.go47
-rw-r--r--src/orchestrator/internal/config/config.go130
-rw-r--r--src/orchestrator/internal/config/config_test.go40
-rw-r--r--src/orchestrator/internal/db/mock.go94
-rw-r--r--src/orchestrator/internal/db/mongo.go396
-rw-r--r--src/orchestrator/internal/db/mongo_test.go597
-rw-r--r--src/orchestrator/internal/db/store.go106
-rw-r--r--src/orchestrator/internal/db/store_test.go121
-rw-r--r--src/orchestrator/internal/project/project.go133
-rw-r--r--src/orchestrator/internal/project/project_test.go177
11 files changed, 1948 insertions, 0 deletions
diff --git a/src/orchestrator/internal/auth/auth.go b/src/orchestrator/internal/auth/auth.go
new file mode 100644
index 00000000..3da8f2af
--- /dev/null
+++ b/src/orchestrator/internal/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/internal/auth/auth_test.go b/src/orchestrator/internal/auth/auth_test.go
new file mode 100644
index 00000000..e41cb1ac
--- /dev/null
+++ b/src/orchestrator/internal/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/internal/config/config.go b/src/orchestrator/internal/config/config.go
new file mode 100644
index 00000000..cb4656f0
--- /dev/null
+++ b/src/orchestrator/internal/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: "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
new file mode 100644
index 00000000..ce7641ae
--- /dev/null
+++ b/src/orchestrator/internal/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/internal/db/mock.go b/src/orchestrator/internal/db/mock.go
new file mode 100644
index 00000000..1dbca4b4
--- /dev/null
+++ b/src/orchestrator/internal/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/internal/db/mongo.go b/src/orchestrator/internal/db/mongo.go
new file mode 100644
index 00000000..3720a4f2
--- /dev/null
+++ b/src/orchestrator/internal/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/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
new file mode 100644
index 00000000..171c908f
--- /dev/null
+++ b/src/orchestrator/internal/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/internal/db/store.go b/src/orchestrator/internal/db/store.go
new file mode 100644
index 00000000..ed394205
--- /dev/null
+++ b/src/orchestrator/internal/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/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
new file mode 100644
index 00000000..42a41787
--- /dev/null
+++ b/src/orchestrator/internal/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/internal/project/project.go b/src/orchestrator/internal/project/project.go
new file mode 100644
index 00000000..f0c50065
--- /dev/null
+++ b/src/orchestrator/internal/project/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 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
new file mode 100644
index 00000000..cc691e33
--- /dev/null
+++ b/src/orchestrator/internal/project/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 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)
+ }
+ }
+ })
+ }
+}