summaryrefslogtreecommitdiffstats
path: root/src/k8splugin/internal
diff options
context:
space:
mode:
Diffstat (limited to 'src/k8splugin/internal')
-rw-r--r--src/k8splugin/internal/app/client.go44
-rw-r--r--src/k8splugin/internal/app/client_test.go36
-rw-r--r--src/k8splugin/internal/app/vnfhelper.go196
-rw-r--r--src/k8splugin/internal/app/vnfhelper_test.go133
-rw-r--r--src/k8splugin/internal/db/consul.go118
-rw-r--r--src/k8splugin/internal/db/consul_test.go313
-rw-r--r--src/k8splugin/internal/db/mongo.go334
-rw-r--r--src/k8splugin/internal/db/mongo_test.go530
-rw-r--r--src/k8splugin/internal/db/store.go83
-rw-r--r--src/k8splugin/internal/db/store_test.go123
-rw-r--r--src/k8splugin/internal/db/testing.go79
-rw-r--r--src/k8splugin/internal/rb/archive.go65
-rw-r--r--src/k8splugin/internal/rb/archive_test.go66
-rw-r--r--src/k8splugin/internal/rb/definition.go164
-rw-r--r--src/k8splugin/internal/rb/definition_test.go420
-rw-r--r--src/k8splugin/internal/rb/profile.go185
-rw-r--r--src/k8splugin/internal/rb/profile_test.go426
-rw-r--r--src/k8splugin/internal/utils.go135
-rw-r--r--src/k8splugin/internal/utils_test.go95
19 files changed, 3545 insertions, 0 deletions
diff --git a/src/k8splugin/internal/app/client.go b/src/k8splugin/internal/app/client.go
new file mode 100644
index 00000000..3555afdd
--- /dev/null
+++ b/src/k8splugin/internal/app/client.go
@@ -0,0 +1,44 @@
+/*
+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 app
+
+import (
+ "errors"
+
+ pkgerrors "github.com/pkg/errors"
+
+ "k8s.io/client-go/kubernetes"
+ "k8s.io/client-go/tools/clientcmd"
+)
+
+// GetKubeClient loads the Kubernetes configuation values stored into the local configuration file
+var GetKubeClient = func(configPath string) (kubernetes.Clientset, error) {
+ var clientset *kubernetes.Clientset
+
+ if configPath == "" {
+ return *clientset, errors.New("config not passed and is not found in ~/.kube. ")
+ }
+
+ config, err := clientcmd.BuildConfigFromFlags("", configPath)
+ if err != nil {
+ return kubernetes.Clientset{}, pkgerrors.Wrap(err, "setConfig: Build config from flags raised an error")
+ }
+
+ clientset, err = kubernetes.NewForConfig(config)
+ if err != nil {
+ return *clientset, err
+ }
+
+ return *clientset, nil
+}
diff --git a/src/k8splugin/internal/app/client_test.go b/src/k8splugin/internal/app/client_test.go
new file mode 100644
index 00000000..9d892633
--- /dev/null
+++ b/src/k8splugin/internal/app/client_test.go
@@ -0,0 +1,36 @@
+// +build unit
+
+/*
+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 app
+
+import (
+ "reflect"
+ "testing"
+)
+
+func TestGetKubeClient(t *testing.T) {
+ t.Run("Successfully create Kube Client", func(t *testing.T) {
+
+ clientset, err := GetKubeClient("../../mock_files/mock_configs/mock_config")
+ if err != nil {
+ t.Fatalf("TestGetKubeClient returned an error (%s)", err)
+ }
+
+ if reflect.TypeOf(clientset).Name() != "Clientset" {
+ t.Fatalf("TestGetKubeClient returned :\n result=%v\n expected=%v", clientset, "Clientset")
+ }
+
+ })
+}
diff --git a/src/k8splugin/internal/app/vnfhelper.go b/src/k8splugin/internal/app/vnfhelper.go
new file mode 100644
index 00000000..3deb9f21
--- /dev/null
+++ b/src/k8splugin/internal/app/vnfhelper.go
@@ -0,0 +1,196 @@
+/*
+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 app
+
+import (
+ "encoding/hex"
+ "io/ioutil"
+ "log"
+ "math/rand"
+ "os"
+
+ "k8s.io/client-go/kubernetes"
+
+ pkgerrors "github.com/pkg/errors"
+ yaml "gopkg.in/yaml.v2"
+
+ utils "k8splugin/internal"
+)
+
+func generateExternalVNFID() string {
+ b := make([]byte, 2)
+ rand.Read(b)
+ return hex.EncodeToString(b)
+}
+
+func ensuresNamespace(namespace string, kubeclient kubernetes.Interface) error {
+ namespacePlugin, ok := utils.LoadedPlugins["namespace"]
+ if !ok {
+ return pkgerrors.New("No plugin for namespace resource found")
+ }
+
+ symGetNamespaceFunc, err := namespacePlugin.Lookup("Get")
+ if err != nil {
+ return pkgerrors.Wrap(err, "Error fetching get namespace function")
+ }
+
+ ns, err := symGetNamespaceFunc.(func(string, string, kubernetes.Interface) (string, error))(
+ namespace, namespace, kubeclient)
+ if err != nil {
+ return pkgerrors.Wrap(err, "An error ocurred during the get namespace execution")
+ }
+
+ if ns == "" {
+ log.Println("Creating " + namespace + " namespace")
+ symGetNamespaceFunc, err := namespacePlugin.Lookup("Create")
+ if err != nil {
+ return pkgerrors.Wrap(err, "Error fetching create namespace plugin")
+ }
+ namespaceResource := &utils.ResourceData{
+ Namespace: namespace,
+ }
+
+ _, err = symGetNamespaceFunc.(func(*utils.ResourceData, kubernetes.Interface) (string, error))(
+ namespaceResource, kubeclient)
+ if err != nil {
+ return pkgerrors.Wrap(err, "Error creating "+namespace+" namespace")
+ }
+ }
+ return nil
+}
+
+// CreateVNF reads the CSAR files from the files system and creates them one by one
+var CreateVNF = func(csarID string, cloudRegionID string, namespace string, kubeclient *kubernetes.Clientset) (string, map[string][]string, error) {
+ if err := ensuresNamespace(namespace, kubeclient); err != nil {
+ return "", nil, pkgerrors.Wrap(err, "Error while ensuring namespace: "+namespace)
+ }
+ externalVNFID := generateExternalVNFID()
+ internalVNFID := cloudRegionID + "-" + namespace + "-" + externalVNFID
+
+ csarDirPath := os.Getenv("CSAR_DIR") + "/" + csarID
+ metadataYAMLPath := csarDirPath + "/metadata.yaml"
+
+ log.Println("Reading " + metadataYAMLPath + " file")
+ metadataFile, err := ReadMetadataFile(metadataYAMLPath)
+ if err != nil {
+ return "", nil, pkgerrors.Wrap(err, "Error while reading Metadata File: "+metadataYAMLPath)
+ }
+
+ var path string
+ resourceYAMLNameMap := make(map[string][]string)
+ // Iterates over the resources defined in the metadata file to create kubernetes resources
+ log.Println(string(len(metadataFile.ResourceTypePathMap)) + " resource(s) type(s) to be processed")
+ for resource, fileNames := range metadataFile.ResourceTypePathMap {
+ log.Println("Processing items of " + string(resource) + " resource")
+ var resourcesCreated []string
+ for _, filename := range fileNames {
+ path = csarDirPath + "/" + filename
+
+ if _, err := os.Stat(path); os.IsNotExist(err) {
+ return "", nil, pkgerrors.New("File " + path + "does not exists")
+ }
+ log.Println("Processing file: " + path)
+
+ genericKubeData := &utils.ResourceData{
+ YamlFilePath: path,
+ Namespace: namespace,
+ VnfId: internalVNFID,
+ }
+
+ typePlugin, ok := utils.LoadedPlugins[resource]
+ if !ok {
+ return "", nil, pkgerrors.New("No plugin for resource " + resource + " found")
+ }
+
+ symCreateResourceFunc, err := typePlugin.Lookup("Create")
+ if err != nil {
+ return "", nil, pkgerrors.Wrap(err, "Error fetching "+resource+" plugin")
+ }
+
+ internalResourceName, err := symCreateResourceFunc.(func(*utils.ResourceData, kubernetes.Interface) (string, error))(
+ genericKubeData, kubeclient)
+ if err != nil {
+ return "", nil, pkgerrors.Wrap(err, "Error in plugin "+resource+" plugin")
+ }
+ log.Print(internalResourceName + " succesful resource created")
+ resourcesCreated = append(resourcesCreated, internalResourceName)
+ }
+ resourceYAMLNameMap[resource] = resourcesCreated
+ }
+
+ return externalVNFID, resourceYAMLNameMap, nil
+}
+
+// DestroyVNF deletes VNFs based on data passed
+var DestroyVNF = func(data map[string][]string, namespace string, kubeclient *kubernetes.Clientset) error {
+ /* data:
+ {
+ "deployment": ["cloud1-default-uuid-sisedeploy1", "cloud1-default-uuid-sisedeploy2", ... ]
+ "service": ["cloud1-default-uuid-sisesvc1", "cloud1-default-uuid-sisesvc2", ... ]
+ },
+ */
+
+ for resourceName, resourceList := range data {
+ typePlugin, ok := utils.LoadedPlugins[resourceName]
+ if !ok {
+ return pkgerrors.New("No plugin for resource " + resourceName + " found")
+ }
+
+ symDeleteResourceFunc, err := typePlugin.Lookup("Delete")
+ if err != nil {
+ return pkgerrors.Wrap(err, "Error fetching "+resourceName+" plugin")
+ }
+
+ for _, resourceName := range resourceList {
+
+ log.Println("Deleting resource: " + resourceName)
+
+ err = symDeleteResourceFunc.(func(string, string, kubernetes.Interface) error)(
+ resourceName, namespace, kubeclient)
+ if err != nil {
+ return pkgerrors.Wrap(err, "Error destroying "+resourceName)
+ }
+ }
+ }
+
+ return nil
+}
+
+// MetadataFile stores the metadata of execution
+type MetadataFile struct {
+ ResourceTypePathMap map[string][]string `yaml:"resources"`
+}
+
+// ReadMetadataFile reads the metadata yaml to return the order or reads
+var ReadMetadataFile = func(path string) (MetadataFile, error) {
+ var metadataFile MetadataFile
+
+ if _, err := os.Stat(path); os.IsNotExist(err) {
+ return metadataFile, pkgerrors.Wrap(err, "Metadata YAML file does not exist")
+ }
+
+ log.Println("Reading metadata YAML: " + path)
+ yamlFile, err := ioutil.ReadFile(path)
+ if err != nil {
+ return metadataFile, pkgerrors.Wrap(err, "Metadata YAML file read error")
+ }
+
+ err = yaml.Unmarshal(yamlFile, &metadataFile)
+ if err != nil {
+ return metadataFile, pkgerrors.Wrap(err, "Metadata YAML file unmarshal error")
+ }
+ log.Printf("metadata:\n%v", metadataFile)
+
+ return metadataFile, nil
+}
diff --git a/src/k8splugin/internal/app/vnfhelper_test.go b/src/k8splugin/internal/app/vnfhelper_test.go
new file mode 100644
index 00000000..81bea627
--- /dev/null
+++ b/src/k8splugin/internal/app/vnfhelper_test.go
@@ -0,0 +1,133 @@
+// +build integration
+
+/*
+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 app
+
+import (
+ "io/ioutil"
+ "log"
+ "os"
+ "plugin"
+ "testing"
+
+ yaml "gopkg.in/yaml.v2"
+ "k8s.io/client-go/kubernetes"
+
+ pkgerrors "github.com/pkg/errors"
+
+ utils "k8splugin/internal"
+)
+
+func LoadMockPlugins(krdLoadedPlugins *map[string]*plugin.Plugin) error {
+ if _, err := os.Stat("../../mock_files/mock_plugins/mockplugin.so"); os.IsNotExist(err) {
+ return pkgerrors.New("mockplugin.so does not exist. Please compile mockplugin.go to generate")
+ }
+
+ mockPlugin, err := plugin.Open("../../mock_files/mock_plugins/mockplugin.so")
+ if err != nil {
+ return pkgerrors.Cause(err)
+ }
+
+ (*krdLoadedPlugins)["namespace"] = mockPlugin
+ (*krdLoadedPlugins)["deployment"] = mockPlugin
+ (*krdLoadedPlugins)["service"] = mockPlugin
+
+ return nil
+}
+
+func TestCreateVNF(t *testing.T) {
+ oldkrdPluginData := utils.LoadedPlugins
+ oldReadMetadataFile := ReadMetadataFile
+
+ defer func() {
+ utils.LoadedPlugins = oldkrdPluginData
+ ReadMetadataFile = oldReadMetadataFile
+ }()
+
+ err := LoadMockPlugins(&utils.LoadedPlugins)
+ if err != nil {
+ t.Fatalf("TestCreateVNF returned an error (%s)", err)
+ }
+
+ ReadMetadataFile = func(yamlFilePath string) (MetadataFile, error) {
+ var seqFile MetadataFile
+
+ if _, err := os.Stat(yamlFilePath); err == nil {
+ rawBytes, err := ioutil.ReadFile("../../mock_files/mock_yamls/metadata.yaml")
+ if err != nil {
+ return seqFile, pkgerrors.Wrap(err, "Metadata YAML file read error")
+ }
+
+ err = yaml.Unmarshal(rawBytes, &seqFile)
+ if err != nil {
+ return seqFile, pkgerrors.Wrap(err, "Metadata YAML file unmarshall error")
+ }
+ }
+
+ return seqFile, nil
+ }
+
+ kubeclient := kubernetes.Clientset{}
+
+ t.Run("Successfully create VNF", func(t *testing.T) {
+ externaluuid, data, err := CreateVNF("uuid", "cloudregion1", "test", &kubeclient)
+ if err != nil {
+ t.Fatalf("TestCreateVNF returned an error (%s)", err)
+ }
+
+ log.Println(externaluuid)
+
+ if data == nil {
+ t.Fatalf("TestCreateVNF returned empty data (%s)", data)
+ }
+ })
+
+}
+
+func TestDeleteVNF(t *testing.T) {
+ oldkrdPluginData := utils.LoadedPlugins
+
+ defer func() {
+ utils.LoadedPlugins = oldkrdPluginData
+ }()
+
+ err := LoadMockPlugins(&utils.LoadedPlugins)
+ if err != nil {
+ t.Fatalf("TestCreateVNF returned an error (%s)", err)
+ }
+
+ kubeclient := kubernetes.Clientset{}
+
+ t.Run("Successfully delete VNF", func(t *testing.T) {
+ data := map[string][]string{
+ "deployment": []string{"cloud1-default-uuid-sisedeploy"},
+ "service": []string{"cloud1-default-uuid-sisesvc"},
+ }
+
+ err := DestroyVNF(data, "test", &kubeclient)
+ if err != nil {
+ t.Fatalf("TestCreateVNF returned an error (%s)", err)
+ }
+ })
+}
+
+func TestReadMetadataFile(t *testing.T) {
+ t.Run("Successfully read Metadata YAML file", func(t *testing.T) {
+ _, err := ReadMetadataFile("../../mock_files//mock_yamls/metadata.yaml")
+ if err != nil {
+ t.Fatalf("TestReadMetadataFile returned an error (%s)", err)
+ }
+ })
+}
diff --git a/src/k8splugin/internal/db/consul.go b/src/k8splugin/internal/db/consul.go
new file mode 100644
index 00000000..a61a4c10
--- /dev/null
+++ b/src/k8splugin/internal/db/consul.go
@@ -0,0 +1,118 @@
+/*
+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 (
+ "os"
+
+ "github.com/hashicorp/consul/api"
+ pkgerrors "github.com/pkg/errors"
+)
+
+// ConsulKVStore defines the a subset of Consul DB operations
+// Note: This interface is defined mainly for allowing mock testing
+type ConsulKVStore interface {
+ List(prefix string, q *api.QueryOptions) (api.KVPairs, *api.QueryMeta, error)
+ Get(key string, q *api.QueryOptions) (*api.KVPair, *api.QueryMeta, error)
+ Put(p *api.KVPair, q *api.WriteOptions) (*api.WriteMeta, error)
+ Delete(key string, w *api.WriteOptions) (*api.WriteMeta, error)
+}
+
+// ConsulStore is an implementation of the ConsulKVStore interface
+type ConsulStore struct {
+ client ConsulKVStore
+}
+
+// NewConsulStore initializes a Consul Store instance using the default values
+func NewConsulStore(store ConsulKVStore) (Store, error) {
+ if store == nil {
+ config := api.DefaultConfig()
+ config.Address = os.Getenv("DATABASE_IP") + ":8500"
+
+ consulClient, err := api.NewClient(config)
+ if err != nil {
+ return nil, err
+ }
+ store = consulClient.KV()
+ }
+
+ return &ConsulStore{
+ client: store,
+ }, nil
+}
+
+// HealthCheck verifies if the database is up and running
+func (c *ConsulStore) HealthCheck() error {
+ _, err := c.Read("test", "test", "test")
+ if err != nil {
+ return pkgerrors.New("[ERROR] Cannot talk to Datastore. Check if it is running/reachable.")
+ }
+ return nil
+}
+
+// Unmarshal implements any unmarshaling that is needed when using consul
+func (c *ConsulStore) Unmarshal(inp []byte, out interface{}) error {
+ return nil
+}
+
+// Create is used to create a DB entry
+func (c *ConsulStore) Create(root, key, tag string, data interface{}) error {
+
+ value, err := Serialize(data)
+ if err != nil {
+ return pkgerrors.Wrap(err, "Serializing input data")
+ }
+
+ p := &api.KVPair{
+ Key: key,
+ Value: []byte(value),
+ }
+ _, err = c.client.Put(p, nil)
+ return err
+}
+
+// Read method returns the internalID for a particular externalID
+func (c *ConsulStore) Read(root, key, tag string) ([]byte, error) {
+ key = root + "/" + key + "/" + tag
+ pair, _, err := c.client.Get(key, nil)
+ if err != nil {
+ return nil, err
+ }
+ if pair == nil {
+ return nil, nil
+ }
+ return pair.Value, nil
+}
+
+// Delete method removes an internalID from the Database
+func (c *ConsulStore) Delete(root, key, tag string) error {
+ _, err := c.client.Delete(key, nil)
+ return err
+}
+
+// ReadAll is used to get all ExternalIDs in a namespace
+func (c *ConsulStore) ReadAll(root, tag string) (map[string][]byte, error) {
+ pairs, _, err := c.client.List(root, nil)
+ if err != nil {
+ return nil, err
+ }
+
+ //TODO: Filter results by tag and return it
+ result := make(map[string][]byte)
+ for _, keypair := range pairs {
+ result[keypair.Key] = keypair.Value
+ }
+
+ return result, nil
+}
diff --git a/src/k8splugin/internal/db/consul_test.go b/src/k8splugin/internal/db/consul_test.go
new file mode 100644
index 00000000..754112ad
--- /dev/null
+++ b/src/k8splugin/internal/db/consul_test.go
@@ -0,0 +1,313 @@
+// +build unit
+
+/*
+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"
+
+ "github.com/hashicorp/consul/api"
+ pkgerrors "github.com/pkg/errors"
+)
+
+type mockConsulKVStore struct {
+ Items api.KVPairs
+ Err error
+}
+
+func (c *mockConsulKVStore) Put(p *api.KVPair, q *api.WriteOptions) (*api.WriteMeta, error) {
+ return nil, c.Err
+}
+
+func (c *mockConsulKVStore) Get(key string, q *api.QueryOptions) (*api.KVPair, *api.QueryMeta, error) {
+ if c.Err != nil {
+ return nil, nil, c.Err
+ }
+ for _, kvpair := range c.Items {
+ if kvpair.Key == key {
+ return kvpair, nil, nil
+ }
+ }
+ return nil, nil, nil
+}
+
+func (c *mockConsulKVStore) Delete(key string, w *api.WriteOptions) (*api.WriteMeta, error) {
+ return nil, c.Err
+}
+
+func (c *mockConsulKVStore) List(prefix string, q *api.QueryOptions) (api.KVPairs, *api.QueryMeta, error) {
+ if c.Err != nil {
+ return nil, nil, c.Err
+ }
+ return c.Items, nil, nil
+}
+
+func TestConsulHealthCheck(t *testing.T) {
+ testCases := []struct {
+ label string
+ mock *mockConsulKVStore
+ expectedError string
+ }{
+ {
+ label: "Sucessful health check Consul Database",
+ mock: &mockConsulKVStore{
+ Items: api.KVPairs{
+ &api.KVPair{
+ Key: "test-key",
+ Value: nil,
+ },
+ },
+ },
+ },
+ {
+ label: "Fail connectivity to Consul Database",
+ mock: &mockConsulKVStore{
+ Err: pkgerrors.New("Timeout"),
+ },
+ expectedError: "Cannot talk to Datastore. Check if it is running/reachable.",
+ },
+ }
+
+ for _, testCase := range testCases {
+ t.Run(testCase.label, func(t *testing.T) {
+ client, _ := NewConsulStore(testCase.mock)
+ err := client.HealthCheck()
+ if err != nil {
+ if testCase.expectedError == "" {
+ t.Fatalf("HealthCheck method return an un-expected (%s)", err)
+ }
+ if !strings.Contains(string(err.Error()), testCase.expectedError) {
+ t.Fatalf("HealthCheck method returned an error (%s)", err)
+ }
+ }
+ })
+ }
+}
+
+func TestConsulCreate(t *testing.T) {
+ testCases := []struct {
+ label string
+ input map[string]string
+ mock *mockConsulKVStore
+ expectedError string
+ }{
+ {
+ label: "Sucessful register a record to Consul Database",
+ input: map[string]string{"root": "rbinst", "key": "test-key",
+ "tag": "data", "value": "test-value"},
+ mock: &mockConsulKVStore{},
+ },
+ {
+ label: "Fail to create a new record in Consul Database",
+ input: map[string]string{"root": "rbinst", "key": "test-key",
+ "tag": "data", "value": "test-value"},
+ mock: &mockConsulKVStore{
+ Err: pkgerrors.New("DB error"),
+ },
+ expectedError: "DB error",
+ },
+ }
+
+ for _, testCase := range testCases {
+ t.Run(testCase.label, func(t *testing.T) {
+ client, _ := NewConsulStore(testCase.mock)
+ err := client.Create(testCase.input["root"], testCase.input["key"],
+ testCase.input["tag"], testCase.input["value"])
+ if err != nil {
+ if testCase.expectedError == "" {
+ t.Fatalf("Create method return an un-expected (%s)", err)
+ }
+ if !strings.Contains(string(err.Error()), testCase.expectedError) {
+ t.Fatalf("Create method returned an error (%s)", err)
+ }
+ }
+ })
+ }
+}
+
+func TestConsulRead(t *testing.T) {
+ testCases := []struct {
+ label string
+ input map[string]string
+ mock *mockConsulKVStore
+ expectedError string
+ expectedResult string
+ }{
+ {
+ label: "Sucessful retrieve a record from Consul Database",
+ input: map[string]string{"root": "rbinst", "key": "test",
+ "tag": "data"},
+ mock: &mockConsulKVStore{
+ Items: api.KVPairs{
+ &api.KVPair{
+ Key: "rbinst/test/data",
+ Value: []byte("test-value"),
+ },
+ },
+ },
+ expectedResult: "test-value",
+ },
+ {
+ label: "Fail retrieve a non-existing record from Consul Database",
+ input: map[string]string{"root": "rbinst", "key": "test-key",
+ "tag": "data"},
+ mock: &mockConsulKVStore{},
+ },
+ {
+ label: "Fail retrieve a record from Consul Database",
+ input: map[string]string{"root": "rbinst", "key": "test-key",
+ "tag": "data"},
+ mock: &mockConsulKVStore{
+ Err: pkgerrors.New("DB error"),
+ },
+ expectedError: "DB error",
+ },
+ }
+
+ for _, testCase := range testCases {
+ t.Run(testCase.label, func(t *testing.T) {
+ client, _ := NewConsulStore(testCase.mock)
+ result, err := client.Read(testCase.input["root"], testCase.input["key"],
+ testCase.input["tag"])
+ if err != nil {
+ if testCase.expectedError == "" {
+ t.Fatalf("Read method return an un-expected (%s)", err)
+ }
+ if !strings.Contains(string(err.Error()), testCase.expectedError) {
+ t.Fatalf("Read method returned an error (%s)", err)
+ }
+ } else {
+ if testCase.expectedError != "" && testCase.expectedResult == "" {
+ t.Fatalf("Read method was expecting \"%s\" error message", testCase.expectedError)
+ }
+ if !reflect.DeepEqual(testCase.expectedResult, string(result)) {
+
+ t.Fatalf("Read method returned: \n%v\n and it was expected: \n%v", result, testCase.expectedResult)
+ }
+ }
+ })
+ }
+}
+
+func TestConsulDelete(t *testing.T) {
+ testCases := []struct {
+ label string
+ input map[string]string
+ mock *mockConsulKVStore
+ expectedError string
+ }{
+ {
+ label: "Sucessful delete a record to Consul Database",
+ input: map[string]string{"root": "rbinst", "key": "test-key",
+ "tag": "data"},
+ mock: &mockConsulKVStore{},
+ },
+ {
+ label: "Fail to delete a record in Consul Database",
+ mock: &mockConsulKVStore{
+ Err: pkgerrors.New("DB error"),
+ },
+ expectedError: "DB error",
+ },
+ }
+
+ for _, testCase := range testCases {
+ t.Run(testCase.label, func(t *testing.T) {
+ client, _ := NewConsulStore(testCase.mock)
+ err := client.Delete(testCase.input["root"], testCase.input["key"],
+ testCase.input["tag"])
+ if err != nil {
+ if testCase.expectedError == "" {
+ t.Fatalf("Delete method return an un-expected (%s)", err)
+ }
+ if !strings.Contains(string(err.Error()), testCase.expectedError) {
+ t.Fatalf("Delete method returned an error (%s)", err)
+ }
+ }
+ })
+ }
+}
+
+func TestConsulReadAll(t *testing.T) {
+ testCases := []struct {
+ label string
+ input map[string]string
+ mock *mockConsulKVStore
+ expectedError string
+ expectedResult map[string][]byte
+ }{
+ {
+ label: "Sucessful retrieve a list from Consul Database",
+ input: map[string]string{"root": "rbinst", "key": "test-key",
+ "tag": "data"},
+ mock: &mockConsulKVStore{
+ Items: api.KVPairs{
+ &api.KVPair{
+ Key: "test",
+ Value: []byte("test-value"),
+ },
+ &api.KVPair{
+ Key: "test2",
+ Value: []byte("test-value2"),
+ },
+ },
+ },
+ expectedResult: map[string][]byte{"test": []byte("test-value"),
+ "test2": []byte("test-value2")},
+ },
+ {
+ label: "Sucessful retrieve an empty list from Consul Database",
+ input: map[string]string{"root": "rbinst", "key": "test-key",
+ "tag": "data"},
+ mock: &mockConsulKVStore{},
+ expectedResult: map[string][]byte{},
+ },
+ {
+ label: "Fail retrieve a record from Consul Database",
+ input: map[string]string{"root": "rbinst", "key": "test-key",
+ "tag": "data"},
+ mock: &mockConsulKVStore{
+ Err: pkgerrors.New("DB error"),
+ },
+ expectedError: "DB error",
+ },
+ }
+
+ for _, testCase := range testCases {
+ t.Run(testCase.label, func(t *testing.T) {
+ client, _ := NewConsulStore(testCase.mock)
+ result, err := client.ReadAll(testCase.input["root"],
+ testCase.input["tag"])
+ if err != nil {
+ if testCase.expectedError == "" {
+ t.Fatalf("ReadAll method return an un-expected (%s)", err)
+ }
+ if !strings.Contains(string(err.Error()), testCase.expectedError) {
+ t.Fatalf("ReadAll method returned an error (%s)", err)
+ }
+ } else {
+ if testCase.expectedError != "" && testCase.expectedResult == nil {
+ t.Fatalf("ReadAll method was expecting \"%s\" error message", testCase.expectedError)
+ }
+ if !reflect.DeepEqual(testCase.expectedResult, result) {
+
+ t.Fatalf("ReadAll method returned: \n%v\n and it was expected: \n%v", result, testCase.expectedResult)
+ }
+ }
+ })
+ }
+}
diff --git a/src/k8splugin/internal/db/mongo.go b/src/k8splugin/internal/db/mongo.go
new file mode 100644
index 00000000..05976b12
--- /dev/null
+++ b/src/k8splugin/internal/db/mongo.go
@@ -0,0 +1,334 @@
+/*
+ * 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 (
+ "github.com/mongodb/mongo-go-driver/bson"
+ "github.com/mongodb/mongo-go-driver/bson/primitive"
+ "github.com/mongodb/mongo-go-driver/mongo"
+ "github.com/mongodb/mongo-go-driver/mongo/options"
+ pkgerrors "github.com/pkg/errors"
+ "golang.org/x/net/context"
+ "log"
+ "os"
+)
+
+// 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()
+}
+
+// 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://" + os.Getenv("DATABASE_IP") + ":27017"
+ mongoClient, err := mongo.NewClient(ip)
+ 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 ...string) bool {
+ for _, v := range args {
+ if v == "" {
+ return false
+ }
+ }
+
+ return true
+}
+
+// Create is used to create a DB entry
+func (m *MongoStore) Create(coll, 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
+}
+
+// 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, 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, 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 cursor.Close(ctx)
+
+ //Iterate over all the master tables
+ result := make(map[string][]byte)
+ for cursor.Next(ctx) {
+ d, err := cursor.DecodeBytes()
+ if err != nil {
+ log.Printf("Unable to decode data in Readall: %s", err.Error())
+ continue
+ }
+
+ //Read key of each master table
+ key, ok := d.Lookup("key").StringValueOK()
+ if !ok {
+ log.Printf("Unable to read key string from mastertable %s", err.Error())
+ continue
+ }
+
+ //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] = 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/k8splugin/internal/db/mongo_test.go b/src/k8splugin/internal/db/mongo_test.go
new file mode 100644
index 00000000..1663e774
--- /dev/null
+++ b/src/k8splugin/internal/db/mongo_test.go
@@ -0,0 +1,530 @@
+// +build unit
+
+/*
+ * 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"
+ "github.com/mongodb/mongo-go-driver/bson"
+ "github.com/mongodb/mongo-go-driver/mongo"
+ "github.com/mongodb/mongo-go-driver/mongo/options"
+ pkgerrors "github.com/pkg/errors"
+ "reflect"
+ "strings"
+ "testing"
+)
+
+// Implements the mongo.Cursor interface
+type mockCursor struct {
+ mongo.Cursor
+ err error
+ bson bson.Raw
+ count int
+}
+
+func (mc *mockCursor) Next(ctx context.Context) bool {
+ if mc.count > 0 {
+ mc.count = mc.count - 1
+ return true
+ }
+ return false
+}
+
+func (mc *mockCursor) DecodeBytes() (bson.Raw, error) {
+ return mc.bson, mc.err
+}
+
+func (mc *mockCursor) Close(ctx context.Context) error {
+ return nil
+}
+
+//Implements the functions used currently in mongo.go
+type mockCollection struct {
+ Err error
+ mCursor mongo.Cursor
+}
+
+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": "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": "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": "",
+ "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"].(string),
+ 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": "keyvalue",
+ "tag": "metadata",
+ },
+ // Binary form of
+ // {
+ // "_id" : ObjectId("5c115156777ff85654248ae1"),
+ // "key" : "b82c4bb1-09ff-6093-4d58-8327b94e1e20",
+ // "metadata" : ObjectId("5c115156c9755047e318bbfd")
+ // }
+ bson: bson.Raw{
+ '\x5a', '\x00', '\x00', '\x00', '\x07', '\x5f', '\x69', '\x64',
+ '\x00', '\x5c', '\x11', '\x51', '\x56', '\x77', '\x7f', '\xf8',
+ '\x56', '\x54', '\x24', '\x8a', '\xe1', '\x02', '\x6b', '\x65',
+ '\x79', '\x00', '\x25', '\x00', '\x00', '\x00', '\x62', '\x38',
+ '\x32', '\x63', '\x34', '\x62', '\x62', '\x31', '\x2d', '\x30',
+ '\x39', '\x66', '\x66', '\x2d', '\x36', '\x30', '\x39', '\x33',
+ '\x2d', '\x34', '\x64', '\x35', '\x38', '\x2d', '\x38', '\x33',
+ '\x32', '\x37', '\x62', '\x39', '\x34', '\x65', '\x31', '\x65',
+ '\x32', '\x30', '\x00', '\x07', '\x6d', '\x65', '\x74', '\x61',
+ '\x64', '\x61', '\x74', '\x61', '\x00', '\x5c', '\x11', '\x51',
+ '\x56', '\xc9', '\x75', '\x50', '\x47', '\xe3', '\x18', '\xbb',
+ '\xfd', '\x00',
+ },
+ mockColl: &mockCollection{},
+ // This is not the document because we are mocking decodeBytes
+ expected: []byte{92, 17, 81, 86, 201, 117, 80, 71, 227, 24, 187, 253},
+ },
+ {
+ label: "UnSuccessfull Read of entry: object not found",
+ input: map[string]interface{}{
+ "coll": "collname",
+ "key": "keyvalue",
+ "tag": "badtag",
+ },
+ // Binary form of
+ // {
+ // "_id" : ObjectId("5c115156777ff85654248ae1"),
+ // "key" : "b82c4bb1-09ff-6093-4d58-8327b94e1e20",
+ // "metadata" : ObjectId("5c115156c9755047e318bbfd")
+ // }
+ bson: bson.Raw{
+ '\x5a', '\x00', '\x00', '\x00', '\x07', '\x5f', '\x69', '\x64',
+ '\x00', '\x5c', '\x11', '\x51', '\x56', '\x77', '\x7f', '\xf8',
+ '\x56', '\x54', '\x24', '\x8a', '\xe1', '\x02', '\x6b', '\x65',
+ '\x79', '\x00', '\x25', '\x00', '\x00', '\x00', '\x62', '\x38',
+ '\x32', '\x63', '\x34', '\x62', '\x62', '\x31', '\x2d', '\x30',
+ '\x39', '\x66', '\x66', '\x2d', '\x36', '\x30', '\x39', '\x33',
+ '\x2d', '\x34', '\x64', '\x35', '\x38', '\x2d', '\x38', '\x33',
+ '\x32', '\x37', '\x62', '\x39', '\x34', '\x65', '\x31', '\x65',
+ '\x32', '\x30', '\x00', '\x07', '\x6d', '\x65', '\x74', '\x61',
+ '\x64', '\x61', '\x74', '\x61', '\x00', '\x5c', '\x11', '\x51',
+ '\x56', '\xc9', '\x75', '\x50', '\x47', '\xe3', '\x18', '\xbb',
+ '\xfd', '\x00',
+ },
+ mockColl: &mockCollection{},
+ expectedError: "Error finding objectID",
+ },
+ {
+ label: "UnSuccessfull Read of entry",
+ input: map[string]interface{}{
+ "coll": "collname",
+ "key": "keyvalue",
+ "tag": "tagName",
+ },
+ mockColl: &mockCollection{
+ Err: pkgerrors.New("DB Error"),
+ },
+ expectedError: "DB Error",
+ },
+ {
+ label: "Missing input fields",
+ input: map[string]interface{}{
+ "coll": "",
+ "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"].(string),
+ 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: %s, expected: %s",
+ 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": "keyvalue",
+ "tag": "metadata",
+ },
+ // Binary form of
+ // {
+ // "_id" : ObjectId("5c115156777ff85654248ae1"),
+ // "key" : "b82c4bb1-09ff-6093-4d58-8327b94e1e20",
+ // "metadata" : ObjectId("5c115156c9755047e318bbfd")
+ // }
+ bson: bson.Raw{
+ '\x5a', '\x00', '\x00', '\x00', '\x07', '\x5f', '\x69', '\x64',
+ '\x00', '\x5c', '\x11', '\x51', '\x56', '\x77', '\x7f', '\xf8',
+ '\x56', '\x54', '\x24', '\x8a', '\xe1', '\x02', '\x6b', '\x65',
+ '\x79', '\x00', '\x25', '\x00', '\x00', '\x00', '\x62', '\x38',
+ '\x32', '\x63', '\x34', '\x62', '\x62', '\x31', '\x2d', '\x30',
+ '\x39', '\x66', '\x66', '\x2d', '\x36', '\x30', '\x39', '\x33',
+ '\x2d', '\x34', '\x64', '\x35', '\x38', '\x2d', '\x38', '\x33',
+ '\x32', '\x37', '\x62', '\x39', '\x34', '\x65', '\x31', '\x65',
+ '\x32', '\x30', '\x00', '\x07', '\x6d', '\x65', '\x74', '\x61',
+ '\x64', '\x61', '\x74', '\x61', '\x00', '\x5c', '\x11', '\x51',
+ '\x56', '\xc9', '\x75', '\x50', '\x47', '\xe3', '\x18', '\xbb',
+ '\xfd', '\x00',
+ },
+ mockColl: &mockCollection{},
+ },
+ {
+ label: "UnSuccessfull Delete of entry",
+ input: map[string]interface{}{
+ "coll": "collname",
+ "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": "keyvalue",
+ "tag": "tagName",
+ },
+ bson: bson.Raw{
+ '\x5a', '\x00', '\x00', '\x00', '\x07', '\x5f', '\x69', '\x64',
+ '\x00', '\x5c', '\x11', '\x51', '\x56', '\x77', '\x7f', '\xf8',
+ '\x56', '\x54', '\x24', '\x8a', '\xe1', '\x02', '\x6b', '\x65',
+ '\x79', '\x00', '\x25', '\x00', '\x00', '\x00', '\x62', '\x38',
+ '\x32', '\x63', '\x34', '\x62', '\x62', '\x31', '\x2d', '\x30',
+ '\x39', '\x66', '\x66', '\x2d', '\x36', '\x30', '\x39', '\x33',
+ '\x2d', '\x34', '\x64', '\x35', '\x38', '\x2d', '\x38', '\x33',
+ '\x32', '\x37', '\x62', '\x39', '\x34', '\x65', '\x31', '\x65',
+ '\x32', '\x30', '\x00', '\x07', '\x6d', '\x65', '\x74', '\x61',
+ '\x64', '\x61', '\x74', '\x61', '\x00', '\x5c', '\x11', '\x51',
+ '\x56', '\xc9', '\x75', '\x50', '\x47', '\xe3', '\x18', '\xbb',
+ '\xfd', '\x00',
+ },
+ mockColl: &mockCollection{},
+ expectedError: "Error finding objectID",
+ },
+ {
+ label: "Missing input fields",
+ input: map[string]interface{}{
+ "coll": "",
+ "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"].(string),
+ 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: &mockCursor{
+ // Binary form of
+ // {
+ // "_id" : ObjectId("5c115156777ff85654248ae1"),
+ // "key" : "b82c4bb1-09ff-6093-4d58-8327b94e1e20",
+ // "metadata" : ObjectId("5c115156c9755047e318bbfd")
+ // }
+ bson: bson.Raw{
+ '\x5a', '\x00', '\x00', '\x00', '\x07', '\x5f', '\x69', '\x64',
+ '\x00', '\x5c', '\x11', '\x51', '\x56', '\x77', '\x7f', '\xf8',
+ '\x56', '\x54', '\x24', '\x8a', '\xe1', '\x02', '\x6b', '\x65',
+ '\x79', '\x00', '\x25', '\x00', '\x00', '\x00', '\x62', '\x38',
+ '\x32', '\x63', '\x34', '\x62', '\x62', '\x31', '\x2d', '\x30',
+ '\x39', '\x66', '\x66', '\x2d', '\x36', '\x30', '\x39', '\x33',
+ '\x2d', '\x34', '\x64', '\x35', '\x38', '\x2d', '\x38', '\x33',
+ '\x32', '\x37', '\x62', '\x39', '\x34', '\x65', '\x31', '\x65',
+ '\x32', '\x30', '\x00', '\x07', '\x6d', '\x65', '\x74', '\x61',
+ '\x64', '\x61', '\x74', '\x61', '\x00', '\x5c', '\x11', '\x51',
+ '\x56', '\xc9', '\x75', '\x50', '\x47', '\xe3', '\x18', '\xbb',
+ '\xfd', '\x00',
+ },
+ count: 1,
+ },
+ },
+ expected: map[string][]byte{
+ "b82c4bb1-09ff-6093-4d58-8327b94e1e20": []byte{
+ 92, 17, 81, 86, 201, 117, 80, 71, 227, 24, 187, 253},
+ },
+ },
+ {
+ 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: &mockCursor{
+ // Binary form of
+ // {
+ // "_id" : ObjectId("5c115156777ff85654248ae1"),
+ // "key" : "b82c4bb1-09ff-6093-4d58-8327b94e1e20",
+ // "metadata" : ObjectId("5c115156c9755047e318bbfd")
+ // }
+ bson: bson.Raw{
+ '\x5a', '\x00', '\x00', '\x00', '\x07', '\x5f', '\x69', '\x64',
+ '\x00', '\x5c', '\x11', '\x51', '\x56', '\x77', '\x7f', '\xf8',
+ '\x56', '\x54', '\x24', '\x8a', '\xe1', '\x02', '\x6b', '\x65',
+ '\x79', '\x00', '\x25', '\x00', '\x00', '\x00', '\x62', '\x38',
+ '\x32', '\x63', '\x34', '\x62', '\x62', '\x31', '\x2d', '\x30',
+ '\x39', '\x66', '\x66', '\x2d', '\x36', '\x30', '\x39', '\x33',
+ '\x2d', '\x34', '\x64', '\x35', '\x38', '\x2d', '\x38', '\x33',
+ '\x32', '\x37', '\x62', '\x39', '\x34', '\x65', '\x31', '\x65',
+ '\x32', '\x30', '\x00', '\x07', '\x6d', '\x65', '\x74', '\x61',
+ '\x64', '\x61', '\x74', '\x61', '\x00', '\x5c', '\x11', '\x51',
+ '\x56', '\xc9', '\x75', '\x50', '\x47', '\xe3', '\x18', '\xbb',
+ '\xfd', '\x00',
+ },
+ count: 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.DecodeBytes()
+ }
+
+ 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/k8splugin/internal/db/store.go b/src/k8splugin/internal/db/store.go
new file mode 100644
index 00000000..a235597a
--- /dev/null
+++ b/src/k8splugin/internal/db/store.go
@@ -0,0 +1,83 @@
+/*
+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"
+
+ pkgerrors "github.com/pkg/errors"
+)
+
+// DBconn interface used to talk a concrete Database connection
+var DBconn Store
+
+// Store is an interface for accessing a 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 table with key and links data with tag and
+ // creates a pointer to the newly added data in the master table
+ Create(table, key, tag string, data interface{}) error
+
+ // Reads data for a particular key with specific tag.
+ Read(table, key, tag string) ([]byte, error)
+
+ //TODO: Update(context.Context, string, interface{}) error
+
+ // Deletes a specific tag data for key.
+ // TODO: If tag is empty, it will delete all tags under key.
+ Delete(table, key, tag string) error
+
+ // Reads all master tables and data from the specified tag in table
+ ReadAll(table, 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 k8splugin as the name
+ DBconn, err = NewMongoStore("k8splugin", nil)
+ case "consul":
+ // create a consul kv store
+ DBconn, err = NewConsulStore(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
+}
diff --git a/src/k8splugin/internal/db/store_test.go b/src/k8splugin/internal/db/store_test.go
new file mode 100644
index 00000000..eed7065f
--- /dev/null
+++ b/src/k8splugin/internal/db/store_test.go
@@ -0,0 +1,123 @@
+// +build unit
+
+/*
+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/k8splugin/internal/db/testing.go b/src/k8splugin/internal/db/testing.go
new file mode 100644
index 00000000..003399af
--- /dev/null
+++ b/src/k8splugin/internal/db/testing.go
@@ -0,0 +1,79 @@
+// +build unit
+
+/*
+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"
+)
+
+//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) Create(table, 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, key, tag string) ([]byte, error) {
+ if m.Err != nil {
+ return nil, m.Err
+ }
+
+ for k, v := range m.Items {
+ if k == key {
+ return v[tag], nil
+ }
+ }
+
+ return nil, m.Err
+}
+
+func (m *MockDB) Delete(table, key, tag string) error {
+ return m.Err
+}
+
+func (m *MockDB) ReadAll(table, 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/k8splugin/internal/rb/archive.go b/src/k8splugin/internal/rb/archive.go
new file mode 100644
index 00000000..8eb0fbed
--- /dev/null
+++ b/src/k8splugin/internal/rb/archive.go
@@ -0,0 +1,65 @@
+/*
+ * 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 rb
+
+import (
+ "archive/tar"
+ "compress/gzip"
+ pkgerrors "github.com/pkg/errors"
+ "io"
+)
+
+func isTarGz(r io.Reader) error {
+ //Check if it is a valid gz
+ gzf, err := gzip.NewReader(r)
+ if err != nil {
+ return pkgerrors.Errorf("Invalid gz format %s", err.Error())
+ }
+
+ //Check if it is a valid tar file
+ //Unfortunately this can only be done by inspecting all the tar contents
+ tarR := tar.NewReader(gzf)
+ first := true
+
+ for true {
+ header, err := tarR.Next()
+
+ if err == io.EOF {
+ //Check if we have just a gzip file without a tar archive inside
+ if first {
+ return pkgerrors.New("Empty or non-existant Tar file found")
+ }
+ //End of archive
+ break
+ }
+
+ if err != nil {
+ return pkgerrors.Errorf("Error reading tar file %s", err.Error())
+ }
+
+ //Check if files are of type directory and regular file
+ if header.Typeflag != tar.TypeDir &&
+ header.Typeflag != tar.TypeReg {
+ return pkgerrors.Errorf("Unknown header in tar %s, %s",
+ header.Name, string(header.Typeflag))
+ }
+
+ first = false
+ }
+
+ return nil
+}
diff --git a/src/k8splugin/internal/rb/archive_test.go b/src/k8splugin/internal/rb/archive_test.go
new file mode 100644
index 00000000..a327dfd4
--- /dev/null
+++ b/src/k8splugin/internal/rb/archive_test.go
@@ -0,0 +1,66 @@
+/*
+ * 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 rb
+
+import (
+ "bytes"
+ "testing"
+)
+
+func TestIsTarGz(t *testing.T) {
+
+ t.Run("Valid tar.gz", func(t *testing.T) {
+ content := []byte{
+ 0x1f, 0x8b, 0x08, 0x08, 0xb0, 0x6b, 0xf4, 0x5b,
+ 0x00, 0x03, 0x74, 0x65, 0x73, 0x74, 0x2e, 0x74,
+ 0x61, 0x72, 0x00, 0xed, 0xce, 0x41, 0x0a, 0xc2,
+ 0x30, 0x10, 0x85, 0xe1, 0xac, 0x3d, 0x45, 0x4e,
+ 0x50, 0x12, 0xd2, 0xc4, 0xe3, 0x48, 0xa0, 0x01,
+ 0x4b, 0x52, 0x0b, 0xed, 0x88, 0x1e, 0xdf, 0x48,
+ 0x11, 0x5c, 0x08, 0xa5, 0x8b, 0x52, 0x84, 0xff,
+ 0xdb, 0xbc, 0x61, 0x66, 0x16, 0x4f, 0xd2, 0x2c,
+ 0x8d, 0x3c, 0x45, 0xed, 0xc8, 0x54, 0x21, 0xb4,
+ 0xef, 0xb4, 0x67, 0x6f, 0xbe, 0x73, 0x61, 0x9d,
+ 0xb2, 0xce, 0xd5, 0x55, 0xf0, 0xde, 0xd7, 0x3f,
+ 0xdb, 0xd6, 0x49, 0x69, 0xb3, 0x67, 0xa9, 0x8f,
+ 0xfb, 0x2c, 0x71, 0xd2, 0x5a, 0xc5, 0xee, 0x92,
+ 0x73, 0x8e, 0x43, 0x7f, 0x4b, 0x3f, 0xff, 0xd6,
+ 0xee, 0x7f, 0xea, 0x9a, 0x4a, 0x19, 0x1f, 0xe3,
+ 0x54, 0xba, 0xd3, 0xd1, 0x55, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x1b, 0xbc, 0x00, 0xb5, 0xe8,
+ 0x4a, 0xf9, 0x00, 0x28, 0x00, 0x00,
+ }
+
+ err := isTarGz(bytes.NewBuffer(content))
+ if err != nil {
+ t.Errorf("Error reading valid Zip file %s", err.Error())
+ }
+ })
+
+ t.Run("Invalid tar.gz", func(t *testing.T) {
+ content := []byte{
+ 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0xff, 0xf2, 0x48, 0xcd,
+ }
+
+ err := isTarGz(bytes.NewBuffer(content))
+ if err == nil {
+ t.Errorf("Error should NOT be nil")
+ }
+ })
+}
diff --git a/src/k8splugin/internal/rb/definition.go b/src/k8splugin/internal/rb/definition.go
new file mode 100644
index 00000000..8a26332b
--- /dev/null
+++ b/src/k8splugin/internal/rb/definition.go
@@ -0,0 +1,164 @@
+/*
+ * 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 rb
+
+import (
+ "bytes"
+ "encoding/base64"
+ "k8splugin/internal/db"
+ "log"
+
+ uuid "github.com/hashicorp/go-uuid"
+ pkgerrors "github.com/pkg/errors"
+)
+
+// Definition contains the parameters needed for resource bundle (rb) definitions
+// It implements the interface for managing the definitions
+type Definition struct {
+ UUID string `json:"uuid,omitempty"`
+ Name string `json:"name"`
+ ChartName string `json:"chart-name"`
+ Description string `json:"description"`
+ ServiceType string `json:"service-type"`
+}
+
+// DefinitionManager is an interface exposes the resource bundle definition functionality
+type DefinitionManager interface {
+ Create(def Definition) (Definition, error)
+ List() ([]Definition, error)
+ Get(resID string) (Definition, error)
+ Delete(resID string) error
+ Upload(resID string, inp []byte) error
+}
+
+// DefinitionClient implements the DefinitionManager
+// It will also be used to maintain some localized state
+type DefinitionClient struct {
+ storeName string
+ tagMeta, tagContent string
+}
+
+// NewDefinitionClient returns an instance of the DefinitionClient
+// which implements the DefinitionManager
+// Uses rbdef collection in underlying db
+func NewDefinitionClient() *DefinitionClient {
+ return &DefinitionClient{
+ storeName: "rbdef",
+ tagMeta: "metadata",
+ tagContent: "content",
+ }
+}
+
+// Create an entry for the resource in the database
+func (v *DefinitionClient) Create(def Definition) (Definition, error) {
+ // If UUID is empty, we will generate one
+ if def.UUID == "" {
+ def.UUID, _ = uuid.GenerateUUID()
+ }
+ key := def.UUID
+
+ err := db.DBconn.Create(v.storeName, key, v.tagMeta, def)
+ if err != nil {
+ return Definition{}, pkgerrors.Wrap(err, "Creating DB Entry")
+ }
+
+ return def, nil
+}
+
+// List all resource entries in the database
+func (v *DefinitionClient) List() ([]Definition, error) {
+ res, err := db.DBconn.ReadAll(v.storeName, v.tagMeta)
+ if err != nil || len(res) == 0 {
+ return []Definition{}, pkgerrors.Wrap(err, "Listing Resource Bundle Definitions")
+ }
+
+ var results []Definition
+ for key, value := range res {
+ //value is a byte array
+ if len(value) > 0 {
+ def := Definition{}
+ err = db.DBconn.Unmarshal(value, &def)
+ if err != nil {
+ log.Printf("[Definition] Error Unmarshaling value for: %s", key)
+ continue
+ }
+ results = append(results, def)
+ }
+ }
+
+ return results, nil
+}
+
+// Get returns the Resource Bundle Definition for corresponding ID
+func (v *DefinitionClient) Get(id string) (Definition, error) {
+ value, err := db.DBconn.Read(v.storeName, id, v.tagMeta)
+ if err != nil {
+ return Definition{}, pkgerrors.Wrap(err, "Get Resource Bundle definition")
+ }
+
+ //value is a byte array
+ if value != nil {
+ def := Definition{}
+ err = db.DBconn.Unmarshal(value, &def)
+ if err != nil {
+ return Definition{}, pkgerrors.Wrap(err, "Unmarshaling Value")
+ }
+ return def, nil
+ }
+
+ return Definition{}, pkgerrors.New("Error getting Resource Bundle Definition")
+}
+
+// Delete the Resource Bundle definition from database
+func (v *DefinitionClient) Delete(id string) error {
+ err := db.DBconn.Delete(v.storeName, id, v.tagMeta)
+ if err != nil {
+ return pkgerrors.Wrap(err, "Delete Resource Bundle Definition")
+ }
+
+ //Delete the content when the delete operation happens
+ err = db.DBconn.Delete(v.storeName, id, v.tagContent)
+ if err != nil {
+ return pkgerrors.Wrap(err, "Delete Resource Bundle Definition Content")
+ }
+
+ return nil
+}
+
+// Upload the contents of resource bundle into database
+func (v *DefinitionClient) Upload(id string, inp []byte) error {
+
+ //ignore the returned data here
+ _, err := v.Get(id)
+ if err != nil {
+ return pkgerrors.Errorf("Invalid Definition ID provided: %s", err.Error())
+ }
+
+ err = isTarGz(bytes.NewBuffer(inp))
+ if err != nil {
+ return pkgerrors.Errorf("Error in file format: %s", err.Error())
+ }
+
+ //Encode given byte stream to text for storage
+ encodedStr := base64.StdEncoding.EncodeToString(inp)
+ err = db.DBconn.Create(v.storeName, id, v.tagContent, encodedStr)
+ if err != nil {
+ return pkgerrors.Errorf("Error uploading data to db: %s", err.Error())
+ }
+
+ return nil
+}
diff --git a/src/k8splugin/internal/rb/definition_test.go b/src/k8splugin/internal/rb/definition_test.go
new file mode 100644
index 00000000..46ab3c07
--- /dev/null
+++ b/src/k8splugin/internal/rb/definition_test.go
@@ -0,0 +1,420 @@
+// +build unit
+
+/*
+ * 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 rb
+
+import (
+ "k8splugin/internal/db"
+ "reflect"
+ "sort"
+ "strings"
+ "testing"
+
+ pkgerrors "github.com/pkg/errors"
+)
+
+func TestCreateDefinition(t *testing.T) {
+ testCases := []struct {
+ label string
+ inp Definition
+ expectedError string
+ mockdb *db.MockDB
+ expected Definition
+ }{
+ {
+ label: "Create Resource Bundle Definition",
+ inp: Definition{
+ UUID: "123e4567-e89b-12d3-a456-426655440000",
+ Name: "testresourcebundle",
+ Description: "testresourcebundle",
+ ServiceType: "firewall",
+ },
+ expected: Definition{
+ UUID: "123e4567-e89b-12d3-a456-426655440000",
+ Name: "testresourcebundle",
+ Description: "testresourcebundle",
+ ServiceType: "firewall",
+ },
+ expectedError: "",
+ mockdb: &db.MockDB{},
+ },
+ {
+ label: "Failed Create Resource Bundle Definition",
+ expectedError: "Error Creating Definition",
+ mockdb: &db.MockDB{
+ Err: pkgerrors.New("Error Creating Definition"),
+ },
+ },
+ }
+
+ for _, testCase := range testCases {
+ t.Run(testCase.label, func(t *testing.T) {
+ db.DBconn = testCase.mockdb
+ impl := NewDefinitionClient()
+ 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 Resource Bundle returned unexpected body: got %v;"+
+ " expected %v", got, testCase.expected)
+ }
+ }
+ })
+ }
+}
+
+func TestListDefinition(t *testing.T) {
+
+ testCases := []struct {
+ label string
+ expectedError string
+ mockdb *db.MockDB
+ expected []Definition
+ }{
+ {
+ label: "List Resource Bundle Definition",
+ expected: []Definition{
+ {
+ UUID: "123e4567-e89b-12d3-a456-426655440000",
+ Name: "testresourcebundle",
+ Description: "testresourcebundle",
+ ServiceType: "firewall",
+ },
+ {
+ UUID: "123e4567-e89b-12d3-a456-426655441111",
+ Name: "testresourcebundle2",
+ Description: "testresourcebundle2",
+ ServiceType: "dns",
+ },
+ },
+ expectedError: "",
+ mockdb: &db.MockDB{
+ Items: map[string]map[string][]byte{
+ "123e4567-e89b-12d3-a456-426655440000": {
+ "metadata": []byte(
+ "{\"name\":\"testresourcebundle\"," +
+ "\"description\":\"testresourcebundle\"," +
+ "\"uuid\":\"123e4567-e89b-12d3-a456-426655440000\"," +
+ "\"service-type\":\"firewall\"}"),
+ },
+ "123e4567-e89b-12d3-a456-426655441111": {
+ "metadata": []byte(
+ "{\"name\":\"testresourcebundle2\"," +
+ "\"description\":\"testresourcebundle2\"," +
+ "\"uuid\":\"123e4567-e89b-12d3-a456-426655441111\"," +
+ "\"service-type\":\"dns\"}"),
+ },
+ },
+ },
+ },
+ {
+ label: "List 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 := NewDefinitionClient()
+ got, err := impl.List()
+ if err != nil {
+ if testCase.expectedError == "" {
+ t.Fatalf("List returned an unexpected error %s", err)
+ }
+ if strings.Contains(err.Error(), testCase.expectedError) == false {
+ t.Fatalf("List returned an unexpected error %s", err)
+ }
+ } else {
+ // Since the order of returned slice is not guaranteed
+ // Check both and return error if both don't match
+ sort.Slice(got, func(i, j int) bool {
+ return got[i].UUID < got[j].UUID
+ })
+ // Sort both as it is not expected that testCase.expected
+ // is sorted
+ sort.Slice(testCase.expected, func(i, j int) bool {
+ return testCase.expected[i].UUID < testCase.expected[j].UUID
+ })
+
+ if reflect.DeepEqual(testCase.expected, got) == false {
+ t.Errorf("List Resource Bundle returned unexpected body: got %v;"+
+ " expected %v", got, testCase.expected)
+ }
+ }
+ })
+ }
+}
+
+func TestGetDefinition(t *testing.T) {
+
+ testCases := []struct {
+ label string
+ expectedError string
+ mockdb *db.MockDB
+ inp string
+ expected Definition
+ }{
+ {
+ label: "Get Resource Bundle Definition",
+ inp: "123e4567-e89b-12d3-a456-426655440000",
+ expected: Definition{
+ UUID: "123e4567-e89b-12d3-a456-426655440000",
+ Name: "testresourcebundle",
+ Description: "testresourcebundle",
+ ServiceType: "firewall",
+ },
+ expectedError: "",
+ mockdb: &db.MockDB{
+ Items: map[string]map[string][]byte{
+ "123e4567-e89b-12d3-a456-426655440000": {
+ "metadata": []byte(
+ "{\"name\":\"testresourcebundle\"," +
+ "\"description\":\"testresourcebundle\"," +
+ "\"uuid\":\"123e4567-e89b-12d3-a456-426655440000\"," +
+ "\"service-type\":\"firewall\"}"),
+ },
+ },
+ },
+ },
+ {
+ 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 := NewDefinitionClient()
+ got, err := impl.Get(testCase.inp)
+ 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 Resource Bundle returned unexpected body: got %v;"+
+ " expected %v", got, testCase.expected)
+ }
+ }
+ })
+ }
+}
+
+func TestDeleteDefinition(t *testing.T) {
+
+ testCases := []struct {
+ label string
+ inp string
+ expectedError string
+ mockdb *db.MockDB
+ }{
+ {
+ label: "Delete Resource Bundle Definition",
+ inp: "123e4567-e89b-12d3-a456-426655440000",
+ 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 := NewDefinitionClient()
+ err := impl.Delete(testCase.inp)
+ 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)
+ }
+ }
+ })
+ }
+}
+
+func TestUploadDefinition(t *testing.T) {
+ testCases := []struct {
+ label string
+ inp string
+ content []byte
+ expectedError string
+ mockdb *db.MockDB
+ }{
+ {
+ label: "Upload Resource Bundle Definition",
+ inp: "123e4567-e89b-12d3-a456-426655440000",
+ content: []byte{
+ 0x1f, 0x8b, 0x08, 0x08, 0xb0, 0x6b, 0xf4, 0x5b,
+ 0x00, 0x03, 0x74, 0x65, 0x73, 0x74, 0x2e, 0x74,
+ 0x61, 0x72, 0x00, 0xed, 0xce, 0x41, 0x0a, 0xc2,
+ 0x30, 0x10, 0x85, 0xe1, 0xac, 0x3d, 0x45, 0x4e,
+ 0x50, 0x12, 0xd2, 0xc4, 0xe3, 0x48, 0xa0, 0x01,
+ 0x4b, 0x52, 0x0b, 0xed, 0x88, 0x1e, 0xdf, 0x48,
+ 0x11, 0x5c, 0x08, 0xa5, 0x8b, 0x52, 0x84, 0xff,
+ 0xdb, 0xbc, 0x61, 0x66, 0x16, 0x4f, 0xd2, 0x2c,
+ 0x8d, 0x3c, 0x45, 0xed, 0xc8, 0x54, 0x21, 0xb4,
+ 0xef, 0xb4, 0x67, 0x6f, 0xbe, 0x73, 0x61, 0x9d,
+ 0xb2, 0xce, 0xd5, 0x55, 0xf0, 0xde, 0xd7, 0x3f,
+ 0xdb, 0xd6, 0x49, 0x69, 0xb3, 0x67, 0xa9, 0x8f,
+ 0xfb, 0x2c, 0x71, 0xd2, 0x5a, 0xc5, 0xee, 0x92,
+ 0x73, 0x8e, 0x43, 0x7f, 0x4b, 0x3f, 0xff, 0xd6,
+ 0xee, 0x7f, 0xea, 0x9a, 0x4a, 0x19, 0x1f, 0xe3,
+ 0x54, 0xba, 0xd3, 0xd1, 0x55, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x1b, 0xbc, 0x00, 0xb5, 0xe8,
+ 0x4a, 0xf9, 0x00, 0x28, 0x00, 0x00,
+ },
+ mockdb: &db.MockDB{
+ Items: map[string]map[string][]byte{
+ "123e4567-e89b-12d3-a456-426655440000": {
+ "metadata": []byte(
+ "{\"name\":\"testresourcebundle\"," +
+ "\"description\":\"testresourcebundle\"," +
+ "\"uuid\":\"123e4567-e89b-12d3-a456-426655440000\"," +
+ "\"service-type\":\"firewall\"}"),
+ },
+ },
+ },
+ },
+ {
+ label: "Upload with an Invalid Resource Bundle Definition",
+ inp: "123e4567-e89b-12d3-a456-426655440000",
+ expectedError: "Invalid Definition ID provided",
+ content: []byte{
+ 0x1f, 0x8b, 0x08, 0x08, 0xb0, 0x6b, 0xf4, 0x5b,
+ 0x00, 0x03, 0x74, 0x65, 0x73, 0x74, 0x2e, 0x74,
+ 0x61, 0x72, 0x00, 0xed, 0xce, 0x41, 0x0a, 0xc2,
+ 0x30, 0x10, 0x85, 0xe1, 0xac, 0x3d, 0x45, 0x4e,
+ 0x50, 0x12, 0xd2, 0xc4, 0xe3, 0x48, 0xa0, 0x01,
+ 0x4b, 0x52, 0x0b, 0xed, 0x88, 0x1e, 0xdf, 0x48,
+ 0x11, 0x5c, 0x08, 0xa5, 0x8b, 0x52, 0x84, 0xff,
+ 0xdb, 0xbc, 0x61, 0x66, 0x16, 0x4f, 0xd2, 0x2c,
+ 0x8d, 0x3c, 0x45, 0xed, 0xc8, 0x54, 0x21, 0xb4,
+ 0xef, 0xb4, 0x67, 0x6f, 0xbe, 0x73, 0x61, 0x9d,
+ 0xb2, 0xce, 0xd5, 0x55, 0xf0, 0xde, 0xd7, 0x3f,
+ 0xdb, 0xd6, 0x49, 0x69, 0xb3, 0x67, 0xa9, 0x8f,
+ 0xfb, 0x2c, 0x71, 0xd2, 0x5a, 0xc5, 0xee, 0x92,
+ 0x73, 0x8e, 0x43, 0x7f, 0x4b, 0x3f, 0xff, 0xd6,
+ 0xee, 0x7f, 0xea, 0x9a, 0x4a, 0x19, 0x1f, 0xe3,
+ 0x54, 0xba, 0xd3, 0xd1, 0x55, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x1b, 0xbc, 0x00, 0xb5, 0xe8,
+ 0x4a, 0xf9, 0x00, 0x28, 0x00, 0x00,
+ },
+ mockdb: &db.MockDB{
+ Items: map[string]map[string][]byte{
+ "123e4567-e89b-12d3-a456-426655441111": {
+ "metadata": []byte(
+ "{\"name\":\"testresourcebundle\"," +
+ "\"description\":\"testresourcebundle\"," +
+ "\"uuid\":\"123e4567-e89b-12d3-a456-426655441111\"," +
+ "\"service-type\":\"firewall\"}"),
+ },
+ },
+ },
+ },
+ {
+ label: "Invalid File Format Error",
+ inp: "123e4567-e89b-12d3-a456-426655440000",
+ expectedError: "Error in file format",
+ content: []byte{
+ 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0xff, 0xf2, 0x48, 0xcd,
+ },
+ mockdb: &db.MockDB{
+ Items: map[string]map[string][]byte{
+ "123e4567-e89b-12d3-a456-426655440000": {
+ "metadata": []byte(
+ "{\"name\":\"testresourcebundle\"," +
+ "\"description\":\"testresourcebundle\"," +
+ "\"uuid\":\"123e4567-e89b-12d3-a456-426655440000\"," +
+ "\"service-type\":\"firewall\"}"),
+ },
+ },
+ },
+ },
+ {
+ label: "Upload Error",
+ expectedError: "DB Error",
+ content: []byte{
+ 0x1f, 0x8b, 0x08, 0x08, 0xb0, 0x6b, 0xf4, 0x5b,
+ 0x00, 0x03, 0x74, 0x65, 0x73, 0x74, 0x2e, 0x74,
+ 0x61, 0x72, 0x00, 0xed, 0xce, 0x41, 0x0a, 0xc2,
+ 0x30, 0x10, 0x85, 0xe1, 0xac, 0x3d, 0x45, 0x4e,
+ 0x50, 0x12, 0xd2, 0xc4, 0xe3, 0x48, 0xa0, 0x01,
+ 0x4b, 0x52, 0x0b, 0xed, 0x88, 0x1e, 0xdf, 0x48,
+ 0x11, 0x5c, 0x08, 0xa5, 0x8b, 0x52, 0x84, 0xff,
+ 0xdb, 0xbc, 0x61, 0x66, 0x16, 0x4f, 0xd2, 0x2c,
+ 0x8d, 0x3c, 0x45, 0xed, 0xc8, 0x54, 0x21, 0xb4,
+ 0xef, 0xb4, 0x67, 0x6f, 0xbe, 0x73, 0x61, 0x9d,
+ 0xb2, 0xce, 0xd5, 0x55, 0xf0, 0xde, 0xd7, 0x3f,
+ 0xdb, 0xd6, 0x49, 0x69, 0xb3, 0x67, 0xa9, 0x8f,
+ 0xfb, 0x2c, 0x71, 0xd2, 0x5a, 0xc5, 0xee, 0x92,
+ 0x73, 0x8e, 0x43, 0x7f, 0x4b, 0x3f, 0xff, 0xd6,
+ 0xee, 0x7f, 0xea, 0x9a, 0x4a, 0x19, 0x1f, 0xe3,
+ 0x54, 0xba, 0xd3, 0xd1, 0x55, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x1b, 0xbc, 0x00, 0xb5, 0xe8,
+ 0x4a, 0xf9, 0x00, 0x28, 0x00, 0x00,
+ },
+ 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 := NewDefinitionClient()
+ err := impl.Upload(testCase.inp, testCase.content)
+ if err != nil {
+ if testCase.expectedError == "" {
+ t.Errorf("Upload returned an unexpected error %s", err)
+ }
+ if strings.Contains(err.Error(), testCase.expectedError) == false {
+ t.Errorf("Upload returned an unexpected error %s", err)
+ }
+ }
+ })
+ }
+}
diff --git a/src/k8splugin/internal/rb/profile.go b/src/k8splugin/internal/rb/profile.go
new file mode 100644
index 00000000..a0245af1
--- /dev/null
+++ b/src/k8splugin/internal/rb/profile.go
@@ -0,0 +1,185 @@
+/*
+ * 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 rb
+
+import (
+ "bytes"
+ "encoding/base64"
+ "k8splugin/internal/db"
+ "log"
+
+ uuid "github.com/hashicorp/go-uuid"
+ pkgerrors "github.com/pkg/errors"
+)
+
+// Profile contains the parameters needed for resource bundle (rb) profiles
+// It implements the interface for managing the profiles
+type Profile struct {
+ UUID string `json:"uuid,omitempty"`
+ RBDID string `json:"rbdid"`
+ Name string `json:"name"`
+ Namespace string `json:"namespace"`
+ KubernetesVersion string `json:"kubernetesversion"`
+}
+
+// ProfileManager is an interface exposes the resource bundle profile functionality
+type ProfileManager interface {
+ Create(def Profile) (Profile, error)
+ List() ([]Profile, error)
+ Get(resID string) (Profile, error)
+ Help() map[string]string
+ Delete(resID string) error
+ Upload(resID string, inp []byte) error
+}
+
+// ProfileClient implements the ProfileManager
+// It will also be used to maintain some localized state
+type ProfileClient struct {
+ storeName string
+ tagMeta, tagContent string
+}
+
+// NewProfileClient returns an instance of the ProfileClient
+// which implements the ProfileManager
+// Uses rb/def prefix
+func NewProfileClient() *ProfileClient {
+ return &ProfileClient{
+ storeName: "rbprofile",
+ tagMeta: "metadata",
+ tagContent: "content",
+ }
+}
+
+// Help returns some information on how to create the content
+// for the profile in the form of html formatted page
+func (v *ProfileClient) Help() map[string]string {
+ ret := make(map[string]string)
+
+ return ret
+}
+
+// Create an entry for the resource bundle profile in the database
+func (v *ProfileClient) Create(p Profile) (Profile, error) {
+
+ //Check if provided RBID is a valid resource bundle
+ _, err := NewDefinitionClient().Get(p.RBDID)
+ if err != nil {
+ return Profile{}, pkgerrors.Errorf("Invalid Resource Bundle ID provided: %s", err.Error())
+ }
+
+ // Name is required
+ if p.Name == "" {
+ return Profile{}, pkgerrors.New("Name is required for Resource Bundle Profile")
+ }
+
+ // If UUID is empty, we will generate one
+ if p.UUID == "" {
+ p.UUID, _ = uuid.GenerateUUID()
+ }
+ key := p.UUID
+
+ err = db.DBconn.Create(v.storeName, key, v.tagMeta, p)
+ if err != nil {
+ return Profile{}, pkgerrors.Wrap(err, "Creating Profile DB Entry")
+ }
+
+ return p, nil
+}
+
+// List all resource entries in the database
+func (v *ProfileClient) List() ([]Profile, error) {
+ res, err := db.DBconn.ReadAll(v.storeName, v.tagMeta)
+ if err != nil || len(res) == 0 {
+ return []Profile{}, pkgerrors.Wrap(err, "Listing Resource Bundle Profiles")
+ }
+
+ var retData []Profile
+
+ for key, value := range res {
+ //value is a byte array
+ if len(value) > 0 {
+ pr := Profile{}
+ err = db.DBconn.Unmarshal(value, &pr)
+ if err != nil {
+ log.Printf("[Profile] Error Unmarshaling value for: %s", key)
+ continue
+ }
+ retData = append(retData, pr)
+ }
+ }
+
+ return retData, nil
+}
+
+// Get returns the Resource Bundle Profile for corresponding ID
+func (v *ProfileClient) Get(id string) (Profile, error) {
+ value, err := db.DBconn.Read(v.storeName, id, v.tagMeta)
+ if err != nil {
+ return Profile{}, pkgerrors.Wrap(err, "Get Resource Bundle Profile")
+ }
+
+ //value is a byte array
+ if value != nil {
+ pr := Profile{}
+ err = db.DBconn.Unmarshal(value, &pr)
+ if err != nil {
+ return Profile{}, pkgerrors.Wrap(err, "Unmarshaling Profile Value")
+ }
+ return pr, nil
+ }
+
+ return Profile{}, pkgerrors.New("Error getting Resource Bundle Profile")
+}
+
+// Delete the Resource Bundle Profile from database
+func (v *ProfileClient) Delete(id string) error {
+ err := db.DBconn.Delete(v.storeName, id, v.tagMeta)
+ if err != nil {
+ return pkgerrors.Wrap(err, "Delete Resource Bundle Profile")
+ }
+
+ err = db.DBconn.Delete(v.storeName, id, v.tagContent)
+ if err != nil {
+ return pkgerrors.Wrap(err, "Delete Resource Bundle Profile Content")
+ }
+
+ return nil
+}
+
+// Upload the contents of resource bundle into database
+func (v *ProfileClient) Upload(id string, inp []byte) error {
+
+ //ignore the returned data here.
+ _, err := v.Get(id)
+ if err != nil {
+ return pkgerrors.Errorf("Invalid Profile ID provided %s", err.Error())
+ }
+
+ err = isTarGz(bytes.NewBuffer(inp))
+ if err != nil {
+ return pkgerrors.Errorf("Error in file format %s", err.Error())
+ }
+
+ //Encode given byte stream to text for storage
+ encodedStr := base64.StdEncoding.EncodeToString(inp)
+ err = db.DBconn.Create(v.storeName, id, v.tagContent, encodedStr)
+ if err != nil {
+ return pkgerrors.Errorf("Error uploading data to db %s", err.Error())
+ }
+
+ return nil
+}
diff --git a/src/k8splugin/internal/rb/profile_test.go b/src/k8splugin/internal/rb/profile_test.go
new file mode 100644
index 00000000..15ff8951
--- /dev/null
+++ b/src/k8splugin/internal/rb/profile_test.go
@@ -0,0 +1,426 @@
+// +build unit
+
+/*
+ * 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 rb
+
+import (
+ "k8splugin/internal/db"
+ "reflect"
+ "sort"
+ "strings"
+ "testing"
+
+ pkgerrors "github.com/pkg/errors"
+)
+
+func TestCreateProfile(t *testing.T) {
+ testCases := []struct {
+ label string
+ inp Profile
+ expectedError string
+ mockdb *db.MockDB
+ expected Profile
+ }{
+ {
+ label: "Create Resource Bundle Profile",
+ inp: Profile{
+ UUID: "123e4567-e89b-12d3-a456-426655440000",
+ RBDID: "abcde123-e89b-8888-a456-986655447236",
+ Name: "testresourcebundle",
+ Namespace: "default",
+ KubernetesVersion: "1.12.3",
+ },
+ expected: Profile{
+ UUID: "123e4567-e89b-12d3-a456-426655440000",
+ RBDID: "abcde123-e89b-8888-a456-986655447236",
+ Name: "testresourcebundle",
+ Namespace: "default",
+ KubernetesVersion: "1.12.3",
+ },
+ expectedError: "",
+ mockdb: &db.MockDB{
+ Items: map[string]map[string][]byte{
+ "abcde123-e89b-8888-a456-986655447236": {
+ "metadata": []byte(
+ "{\"name\":\"testresourcebundle\"," +
+ "\"namespace\":\"default\"," +
+ "\"uuid\":\"123e4567-e89b-12d3-a456-426655440000\"," +
+ "\"kubernetesversion\":\"1.12.3\"}"),
+ },
+ },
+ },
+ },
+ {
+ label: "Failed Create Resource Bundle Profile",
+ expectedError: "Error Creating Profile",
+ mockdb: &db.MockDB{
+ Err: pkgerrors.New("Error Creating Profile"),
+ },
+ },
+ }
+
+ for _, testCase := range testCases {
+ t.Run(testCase.label, func(t *testing.T) {
+ db.DBconn = testCase.mockdb
+ impl := NewProfileClient()
+ 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 Resource Bundle returned unexpected body: got %v;"+
+ " expected %v", got, testCase.expected)
+ }
+ }
+ })
+ }
+}
+
+func TestListProfiles(t *testing.T) {
+
+ testCases := []struct {
+ label string
+ expectedError string
+ mockdb *db.MockDB
+ expected []Profile
+ }{
+ {
+ label: "List Resource Bundle Profile",
+ expected: []Profile{
+ {
+ UUID: "123e4567-e89b-12d3-a456-426655440000",
+ RBDID: "abcde123-e89b-8888-a456-986655447236",
+ Name: "testresourcebundle",
+ Namespace: "default",
+ KubernetesVersion: "1.12.3",
+ },
+ },
+ expectedError: "",
+ mockdb: &db.MockDB{
+ Items: map[string]map[string][]byte{
+ "123e4567-e89b-12d3-a456-426655440000": {
+ "metadata": []byte(
+ "{\"name\":\"testresourcebundle\"," +
+ "\"namespace\":\"default\"," +
+ "\"uuid\":\"123e4567-e89b-12d3-a456-426655440000\"," +
+ "\"rbdid\":\"abcde123-e89b-8888-a456-986655447236\"," +
+ "\"kubernetesversion\":\"1.12.3\"}"),
+ },
+ },
+ },
+ },
+ {
+ label: "List 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 := NewProfileClient()
+ got, err := impl.List()
+ if err != nil {
+ if testCase.expectedError == "" {
+ t.Fatalf("List returned an unexpected error %s", err)
+ }
+ if strings.Contains(err.Error(), testCase.expectedError) == false {
+ t.Fatalf("List returned an unexpected error %s", err)
+ }
+ } else {
+ // Since the order of returned slice is not guaranteed
+ // Check both and return error if both don't match
+ sort.Slice(got, func(i, j int) bool {
+ return got[i].UUID < got[j].UUID
+ })
+ // Sort both as it is not expected that testCase.expected
+ // is sorted
+ sort.Slice(testCase.expected, func(i, j int) bool {
+ return testCase.expected[i].UUID < testCase.expected[j].UUID
+ })
+
+ if reflect.DeepEqual(testCase.expected, got) == false {
+ t.Errorf("List Resource Bundle returned unexpected body: got %v;"+
+ " expected %v", got, testCase.expected)
+ }
+ }
+ })
+ }
+}
+
+func TestGetProfile(t *testing.T) {
+
+ testCases := []struct {
+ label string
+ expectedError string
+ mockdb *db.MockDB
+ inp string
+ expected Profile
+ }{
+ {
+ label: "Get Resource Bundle Profile",
+ inp: "123e4567-e89b-12d3-a456-426655440000",
+ expected: Profile{
+ UUID: "123e4567-e89b-12d3-a456-426655440000",
+ RBDID: "abcde123-e89b-8888-a456-986655447236",
+ Name: "testresourcebundle",
+ Namespace: "default",
+ KubernetesVersion: "1.12.3",
+ },
+ expectedError: "",
+ mockdb: &db.MockDB{
+ Items: map[string]map[string][]byte{
+ "123e4567-e89b-12d3-a456-426655440000": {
+ "metadata": []byte(
+ "{\"name\":\"testresourcebundle\"," +
+ "\"namespace\":\"default\"," +
+ "\"uuid\":\"123e4567-e89b-12d3-a456-426655440000\"," +
+ "\"rbdid\":\"abcde123-e89b-8888-a456-986655447236\"," +
+ "\"kubernetesversion\":\"1.12.3\"}"),
+ },
+ },
+ },
+ },
+ {
+ 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 := NewProfileClient()
+ got, err := impl.Get(testCase.inp)
+ 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 Resource Bundle returned unexpected body: got %v;"+
+ " expected %v", got, testCase.expected)
+ }
+ }
+ })
+ }
+}
+
+func TestDeleteProfile(t *testing.T) {
+
+ testCases := []struct {
+ label string
+ inp string
+ expectedError string
+ mockdb *db.MockDB
+ }{
+ {
+ label: "Delete Resource Bundle Profile",
+ inp: "123e4567-e89b-12d3-a456-426655440000",
+ 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 := NewProfileClient()
+ err := impl.Delete(testCase.inp)
+ 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)
+ }
+ }
+ })
+ }
+}
+
+func TestUploadProfile(t *testing.T) {
+ testCases := []struct {
+ label string
+ inp string
+ content []byte
+ expectedError string
+ mockdb *db.MockDB
+ }{
+ {
+ label: "Upload Resource Bundle Profile",
+ inp: "123e4567-e89b-12d3-a456-426655440000",
+ content: []byte{
+ 0x1f, 0x8b, 0x08, 0x08, 0xb0, 0x6b, 0xf4, 0x5b,
+ 0x00, 0x03, 0x74, 0x65, 0x73, 0x74, 0x2e, 0x74,
+ 0x61, 0x72, 0x00, 0xed, 0xce, 0x41, 0x0a, 0xc2,
+ 0x30, 0x10, 0x85, 0xe1, 0xac, 0x3d, 0x45, 0x4e,
+ 0x50, 0x12, 0xd2, 0xc4, 0xe3, 0x48, 0xa0, 0x01,
+ 0x4b, 0x52, 0x0b, 0xed, 0x88, 0x1e, 0xdf, 0x48,
+ 0x11, 0x5c, 0x08, 0xa5, 0x8b, 0x52, 0x84, 0xff,
+ 0xdb, 0xbc, 0x61, 0x66, 0x16, 0x4f, 0xd2, 0x2c,
+ 0x8d, 0x3c, 0x45, 0xed, 0xc8, 0x54, 0x21, 0xb4,
+ 0xef, 0xb4, 0x67, 0x6f, 0xbe, 0x73, 0x61, 0x9d,
+ 0xb2, 0xce, 0xd5, 0x55, 0xf0, 0xde, 0xd7, 0x3f,
+ 0xdb, 0xd6, 0x49, 0x69, 0xb3, 0x67, 0xa9, 0x8f,
+ 0xfb, 0x2c, 0x71, 0xd2, 0x5a, 0xc5, 0xee, 0x92,
+ 0x73, 0x8e, 0x43, 0x7f, 0x4b, 0x3f, 0xff, 0xd6,
+ 0xee, 0x7f, 0xea, 0x9a, 0x4a, 0x19, 0x1f, 0xe3,
+ 0x54, 0xba, 0xd3, 0xd1, 0x55, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x1b, 0xbc, 0x00, 0xb5, 0xe8,
+ 0x4a, 0xf9, 0x00, 0x28, 0x00, 0x00,
+ },
+ mockdb: &db.MockDB{
+ Items: map[string]map[string][]byte{
+ "123e4567-e89b-12d3-a456-426655440000": {
+ "metadata": []byte(
+ "{\"name\":\"testresourcebundle\"," +
+ "\"namespace\":\"default\"," +
+ "\"uuid\":\"123e4567-e89b-12d3-a456-426655440000\"," +
+ "\"rbdid\":\"abcde123-e89b-8888-a456-986655447236\"," +
+ "\"kubernetesversion\":\"1.12.3\"}"),
+ },
+ },
+ },
+ },
+ {
+ label: "Upload with an Invalid Resource Bundle Profile",
+ inp: "123e4567-e89b-12d3-a456-426655440000",
+ expectedError: "Invalid Profile ID provided",
+ content: []byte{
+ 0x1f, 0x8b, 0x08, 0x08, 0xb0, 0x6b, 0xf4, 0x5b,
+ 0x00, 0x03, 0x74, 0x65, 0x73, 0x74, 0x2e, 0x74,
+ 0x61, 0x72, 0x00, 0xed, 0xce, 0x41, 0x0a, 0xc2,
+ 0x30, 0x10, 0x85, 0xe1, 0xac, 0x3d, 0x45, 0x4e,
+ 0x50, 0x12, 0xd2, 0xc4, 0xe3, 0x48, 0xa0, 0x01,
+ 0x4b, 0x52, 0x0b, 0xed, 0x88, 0x1e, 0xdf, 0x48,
+ 0x11, 0x5c, 0x08, 0xa5, 0x8b, 0x52, 0x84, 0xff,
+ 0xdb, 0xbc, 0x61, 0x66, 0x16, 0x4f, 0xd2, 0x2c,
+ 0x8d, 0x3c, 0x45, 0xed, 0xc8, 0x54, 0x21, 0xb4,
+ 0xef, 0xb4, 0x67, 0x6f, 0xbe, 0x73, 0x61, 0x9d,
+ 0xb2, 0xce, 0xd5, 0x55, 0xf0, 0xde, 0xd7, 0x3f,
+ 0xdb, 0xd6, 0x49, 0x69, 0xb3, 0x67, 0xa9, 0x8f,
+ 0xfb, 0x2c, 0x71, 0xd2, 0x5a, 0xc5, 0xee, 0x92,
+ 0x73, 0x8e, 0x43, 0x7f, 0x4b, 0x3f, 0xff, 0xd6,
+ 0xee, 0x7f, 0xea, 0x9a, 0x4a, 0x19, 0x1f, 0xe3,
+ 0x54, 0xba, 0xd3, 0xd1, 0x55, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x1b, 0xbc, 0x00, 0xb5, 0xe8,
+ 0x4a, 0xf9, 0x00, 0x28, 0x00, 0x00,
+ },
+ mockdb: &db.MockDB{
+ Items: map[string]map[string][]byte{
+ "123e4567-e89b-12d3-a456-426655441111": {
+ "metadata": []byte(
+ "{\"name\":\"testresourcebundle\"," +
+ "\"namespace\":\"default\"," +
+ "\"uuid\":\"123e4567-e89b-12d3-a456-426655441111\"," +
+ "\"rbdid\":\"abcde123-e89b-8888-a456-986655447236\"," +
+ "\"kubernetesversion\":\"1.12.3\"}"),
+ },
+ },
+ },
+ },
+ {
+ label: "Invalid File Format Error",
+ inp: "123e4567-e89b-12d3-a456-426655440000",
+ expectedError: "Error in file format",
+ content: []byte{
+ 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0xff, 0xf2, 0x48, 0xcd,
+ },
+ mockdb: &db.MockDB{
+ Items: map[string]map[string][]byte{
+ "123e4567-e89b-12d3-a456-426655440000": {
+ "metadata": []byte(
+ "{\"name\":\"testresourcebundle\"," +
+ "\"namespace\":\"default\"," +
+ "\"uuid\":\"123e4567-e89b-12d3-a456-426655440000\"," +
+ "\"rbdid\":\"abcde123-e89b-8888-a456-986655447236\"," +
+ "\"kubernetesversion\":\"1.12.3\"}"),
+ },
+ },
+ },
+ },
+ {
+ label: "Upload Error",
+ expectedError: "DB Error",
+ content: []byte{
+ 0x1f, 0x8b, 0x08, 0x08, 0xb0, 0x6b, 0xf4, 0x5b,
+ 0x00, 0x03, 0x74, 0x65, 0x73, 0x74, 0x2e, 0x74,
+ 0x61, 0x72, 0x00, 0xed, 0xce, 0x41, 0x0a, 0xc2,
+ 0x30, 0x10, 0x85, 0xe1, 0xac, 0x3d, 0x45, 0x4e,
+ 0x50, 0x12, 0xd2, 0xc4, 0xe3, 0x48, 0xa0, 0x01,
+ 0x4b, 0x52, 0x0b, 0xed, 0x88, 0x1e, 0xdf, 0x48,
+ 0x11, 0x5c, 0x08, 0xa5, 0x8b, 0x52, 0x84, 0xff,
+ 0xdb, 0xbc, 0x61, 0x66, 0x16, 0x4f, 0xd2, 0x2c,
+ 0x8d, 0x3c, 0x45, 0xed, 0xc8, 0x54, 0x21, 0xb4,
+ 0xef, 0xb4, 0x67, 0x6f, 0xbe, 0x73, 0x61, 0x9d,
+ 0xb2, 0xce, 0xd5, 0x55, 0xf0, 0xde, 0xd7, 0x3f,
+ 0xdb, 0xd6, 0x49, 0x69, 0xb3, 0x67, 0xa9, 0x8f,
+ 0xfb, 0x2c, 0x71, 0xd2, 0x5a, 0xc5, 0xee, 0x92,
+ 0x73, 0x8e, 0x43, 0x7f, 0x4b, 0x3f, 0xff, 0xd6,
+ 0xee, 0x7f, 0xea, 0x9a, 0x4a, 0x19, 0x1f, 0xe3,
+ 0x54, 0xba, 0xd3, 0xd1, 0x55, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x1b, 0xbc, 0x00, 0xb5, 0xe8,
+ 0x4a, 0xf9, 0x00, 0x28, 0x00, 0x00,
+ },
+ 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 := NewProfileClient()
+ err := impl.Upload(testCase.inp, testCase.content)
+ if err != nil {
+ if testCase.expectedError == "" {
+ t.Errorf("Upload returned an unexpected error %s", err)
+ }
+ if strings.Contains(err.Error(), testCase.expectedError) == false {
+ t.Errorf("Upload returned an unexpected error %s", err)
+ }
+ }
+ })
+ }
+}
diff --git a/src/k8splugin/internal/utils.go b/src/k8splugin/internal/utils.go
new file mode 100644
index 00000000..4b28b688
--- /dev/null
+++ b/src/k8splugin/internal/utils.go
@@ -0,0 +1,135 @@
+/*
+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 utils
+
+import (
+ "io/ioutil"
+ "k8splugin/internal/db"
+ "log"
+ "os"
+ "path/filepath"
+ "plugin"
+ "strings"
+
+ pkgerrors "github.com/pkg/errors"
+ "k8s.io/apimachinery/pkg/runtime"
+ "k8s.io/client-go/kubernetes/scheme"
+)
+
+// LoadedPlugins stores references to the stored plugins
+var LoadedPlugins = map[string]*plugin.Plugin{}
+
+const ResourcesListLimit = 10
+
+// ResourceData stores all supported Kubernetes plugin types
+type ResourceData struct {
+ YamlFilePath string
+ Namespace string
+ VnfId string
+}
+
+// DecodeYAML reads a YAMl file to extract the Kubernetes object definition
+var DecodeYAML = func(path string, into runtime.Object) (runtime.Object, error) {
+ if _, err := os.Stat(path); err != nil {
+ if os.IsNotExist(err) {
+ return nil, pkgerrors.New("File " + path + " not found")
+ } else {
+ return nil, pkgerrors.Wrap(err, "Stat file error")
+ }
+ }
+
+ log.Println("Reading YAML file")
+ rawBytes, err := ioutil.ReadFile(path)
+ if err != nil {
+ return nil, pkgerrors.Wrap(err, "Read YAML file error")
+ }
+
+ log.Println("Decoding deployment YAML")
+ decode := scheme.Codecs.UniversalDeserializer().Decode
+ obj, _, err := decode(rawBytes, nil, into)
+ if err != nil {
+ return nil, pkgerrors.Wrap(err, "Deserialize YAML error")
+ }
+
+ return obj, nil
+}
+
+// CheckEnvVariables checks for required Environment variables
+func CheckEnvVariables() error {
+ envList := []string{"CSAR_DIR", "KUBE_CONFIG_DIR", "PLUGINS_DIR",
+ "DATABASE_TYPE", "DATABASE_IP", "OVN_CENTRAL_ADDRESS"}
+ for _, env := range envList {
+ if _, ok := os.LookupEnv(env); !ok {
+ return pkgerrors.New("environment variable " + env + " not set")
+ }
+ }
+
+ return nil
+}
+
+// CheckDatabaseConnection checks if the database is up and running and
+// plugin can talk to it
+func CheckDatabaseConnection() error {
+ err := db.CreateDBClient(os.Getenv("DATABASE_TYPE"))
+ if err != nil {
+ return pkgerrors.Cause(err)
+ }
+
+ err = db.DBconn.HealthCheck()
+ if err != nil {
+ return pkgerrors.Cause(err)
+ }
+ return nil
+}
+
+// LoadPlugins loads all the compiled .so plugins
+func LoadPlugins() error {
+ pluginsDir := os.Getenv("PLUGINS_DIR")
+ err := filepath.Walk(pluginsDir,
+ func(path string, info os.FileInfo, err error) error {
+ if strings.Contains(path, ".so") {
+ p, err := plugin.Open(path)
+ if err != nil {
+ return pkgerrors.Cause(err)
+ }
+ LoadedPlugins[info.Name()[:len(info.Name())-3]] = p
+ }
+ return err
+ })
+ if err != nil {
+ return err
+ }
+
+ return nil
+}
+
+// CheckInitialSettings is used to check initial settings required to start api
+func CheckInitialSettings() error {
+ err := CheckEnvVariables()
+ if err != nil {
+ return pkgerrors.Cause(err)
+ }
+
+ err = CheckDatabaseConnection()
+ if err != nil {
+ return pkgerrors.Cause(err)
+ }
+
+ err = LoadPlugins()
+ if err != nil {
+ return pkgerrors.Cause(err)
+ }
+
+ return nil
+}
diff --git a/src/k8splugin/internal/utils_test.go b/src/k8splugin/internal/utils_test.go
new file mode 100644
index 00000000..6b49d427
--- /dev/null
+++ b/src/k8splugin/internal/utils_test.go
@@ -0,0 +1,95 @@
+// +build unit
+
+/*
+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 utils
+
+import (
+ "strings"
+ "testing"
+
+ appsV1 "k8s.io/api/apps/v1"
+ coreV1 "k8s.io/api/core/v1"
+ metaV1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+ "k8s.io/apimachinery/pkg/runtime"
+)
+
+func TestDecodeYAML(t *testing.T) {
+ testCases := []struct {
+ label string
+ input string
+ expectedResult runtime.Object
+ expectedError string
+ }{
+ {
+ label: "Fail to read non-existing YAML file",
+ input: "unexisting-file.yaml",
+ expectedError: "not found",
+ },
+ {
+ label: "Fail to read invalid YAML format",
+ input: "./utils_test.go",
+ expectedError: "mapping values are not allowed in this contex",
+ },
+ {
+ label: "Successfully read YAML file",
+ input: "../mock_files/mock_yamls/deployment.yaml",
+ expectedResult: &appsV1.Deployment{
+ ObjectMeta: metaV1.ObjectMeta{
+ Name: "mock-deployment",
+ },
+ Spec: appsV1.DeploymentSpec{
+ Template: coreV1.PodTemplateSpec{
+ ObjectMeta: metaV1.ObjectMeta{
+ Labels: map[string]string{"app": "sise"},
+ },
+ Spec: coreV1.PodSpec{
+ Containers: []coreV1.Container{
+ coreV1.Container{
+ Name: "sise",
+ Image: "mhausenblas/simpleservice:0.5.0",
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ }
+
+ for _, testCase := range testCases {
+ t.Run(testCase.label, func(t *testing.T) {
+ result, err := DecodeYAML(testCase.input, nil)
+ if err != nil {
+ if testCase.expectedError == "" {
+ t.Fatalf("Decode YAML method return an un-expected (%s)", err)
+ }
+ if !strings.Contains(string(err.Error()), testCase.expectedError) {
+ t.Fatalf("Decode YAML method returned an error (%s)", err)
+ }
+ } else {
+ if testCase.expectedError != "" && testCase.expectedResult == nil {
+ t.Fatalf("Decode YAML method was expecting \"%s\" error message", testCase.expectedError)
+ }
+ if result == nil {
+ t.Fatal("Decode YAML method returned nil result")
+ }
+ // if !reflect.DeepEqual(testCase.expectedResult, result) {
+
+ // t.Fatalf("Decode YAML method returned: \n%v\n and it was expected: \n%v", result, testCase.expectedResult)
+ // }
+ }
+ })
+ }
+}