diff options
Diffstat (limited to 'src/k8splugin/db')
-rw-r--r-- | src/k8splugin/db/consul.go | 118 | ||||
-rw-r--r-- | src/k8splugin/db/consul_test.go | 298 | ||||
-rw-r--r-- | src/k8splugin/db/db_test.go | 99 | ||||
-rw-r--r-- | src/k8splugin/db/store.go (renamed from src/k8splugin/db/DB.go) | 27 | ||||
-rw-r--r-- | src/k8splugin/db/store_test.go | 123 | ||||
-rw-r--r-- | src/k8splugin/db/testing.go | 65 |
6 files changed, 557 insertions, 173 deletions
diff --git a/src/k8splugin/db/consul.go b/src/k8splugin/db/consul.go index 950eea34..d7507242 100644 --- a/src/k8splugin/db/consul.go +++ b/src/k8splugin/db/consul.go @@ -16,95 +16,89 @@ package db import ( "os" - consulapi "github.com/hashicorp/consul/api" + "github.com/hashicorp/consul/api" pkgerrors "github.com/pkg/errors" ) -// ConsulDB is an implementation of the DatabaseConnection interface -type ConsulDB struct { - consulClient *consulapi.Client +// 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) } -// InitializeDatabase initialized the initial steps -func (c *ConsulDB) InitializeDatabase() error { - config := consulapi.DefaultConfig() - config.Address = os.Getenv("DATABASE_IP") + ":8500" +// ConsulStore is an implementation of the ConsulKVStore interface +type ConsulStore struct { + client ConsulKVStore +} - client, err := consulapi.NewClient(config) - if err != nil { - return err +// 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() } - c.consulClient = client - return nil + + return &ConsulStore{ + client: store, + }, nil } -// CheckDatabase checks if the database is running -func (c *ConsulDB) CheckDatabase() error { - kv := c.consulClient.KV() - _, _, err := kv.Get("test", nil) +// HealthCheck verifies if the database is up and running +func (c *ConsulStore) HealthCheck() error { + _, err := c.Read("test") if err != nil { return pkgerrors.New("[ERROR] Cannot talk to Datastore. Check if it is running/reachable.") } return nil } -// CreateEntry is used to create a DB entry -func (c *ConsulDB) CreateEntry(key string, value string) error { - kv := c.consulClient.KV() - - p := &consulapi.KVPair{Key: key, Value: []byte(value)} - - _, err := kv.Put(p, nil) - - if err != nil { - return err +// Create is used to create a DB entry +func (c *ConsulStore) Create(key, value string) error { + p := &api.KVPair{ + Key: key, + Value: []byte(value), } - - return nil + _, err := c.client.Put(p, nil) + return err } -// ReadEntry returns the internalID for a particular externalID is present in a namespace -func (c *ConsulDB) ReadEntry(key string) (string, bool, error) { - - kv := c.consulClient.KV() - - pair, _, err := kv.Get(key, nil) - +// Read method returns the internalID for a particular externalID +func (c *ConsulStore) Read(key string) (string, error) { + pair, _, err := c.client.Get(key, nil) + if err != nil { + return "", err + } if pair == nil { - return string("No value found for ID: " + key), false, err + return "", nil } - return string(pair.Value), true, err + return string(pair.Value), nil } -// DeleteEntry is used to delete an ID -func (c *ConsulDB) DeleteEntry(key string) error { - - kv := c.consulClient.KV() - - _, err := kv.Delete(key, nil) - - if err != nil { - return err - } - - return nil +// Delete method removes an internalID from the Database +func (c *ConsulStore) Delete(key string) error { + _, err := c.client.Delete(key, nil) + return err } // ReadAll is used to get all ExternalIDs in a namespace -func (c *ConsulDB) ReadAll(prefix string) ([]string, error) { - kv := c.consulClient.KV() - - pairs, _, err := kv.List(prefix, nil) - - if len(pairs) == 0 { - return []string{""}, err +func (c *ConsulStore) ReadAll(prefix string) ([]string, error) { + pairs, _, err := c.client.List(prefix, nil) + if err != nil { + return nil, err } - - var res []string - + var result []string for _, keypair := range pairs { - res = append(res, keypair.Key) + result = append(result, keypair.Key) } - return res, err + return result, nil } diff --git a/src/k8splugin/db/consul_test.go b/src/k8splugin/db/consul_test.go new file mode 100644 index 00000000..ede1a5e9 --- /dev/null +++ b/src/k8splugin/db/consul_test.go @@ -0,0 +1,298 @@ +// +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{"key": "test-key", "value": "test-value"}, + mock: &mockConsulKVStore{}, + }, + { + label: "Fail to create a new record in Consul Database", + input: map[string]string{"key": "test-key", "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["key"], 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 string + mock *mockConsulKVStore + expectedError string + expectedResult string + }{ + { + label: "Sucessful retrieve a record from Consul Database", + input: "test", + mock: &mockConsulKVStore{ + Items: api.KVPairs{ + &api.KVPair{ + Key: "test", + Value: []byte("test-value"), + }, + }, + }, + expectedResult: "test-value", + }, + { + label: "Fail retrieve a non-existing record from Consul Database", + input: "test", + mock: &mockConsulKVStore{}, + }, + { + label: "Fail retrieve a record from Consul Database", + input: "test", + 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) + 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, 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 string + mock *mockConsulKVStore + expectedError string + }{ + { + label: "Sucessful delete a record to Consul Database", + input: "test", + 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) + 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 string + mock *mockConsulKVStore + expectedError string + expectedResult []string + }{ + { + label: "Sucessful retrieve a list from Consul Database", + input: "test", + mock: &mockConsulKVStore{ + Items: api.KVPairs{ + &api.KVPair{ + Key: "test", + Value: []byte("test-value"), + }, + &api.KVPair{ + Key: "test2", + Value: []byte("test-value2"), + }, + }, + }, + expectedResult: []string{"test", "test2"}, + }, + { + label: "Sucessful retrieve an empty list from Consul Database", + input: "test", + mock: &mockConsulKVStore{}, + }, + { + label: "Fail retrieve a record from Consul Database", + input: "test", + 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) + 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/db/db_test.go b/src/k8splugin/db/db_test.go deleted file mode 100644 index d37cd7ae..00000000 --- a/src/k8splugin/db/db_test.go +++ /dev/null @@ -1,99 +0,0 @@ -// +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" - "testing" -) - -func TestCreateDBClient(t *testing.T) { - oldDBconn := DBconn - - defer func() { - DBconn = oldDBconn - }() - - t.Run("Successfully create DB client", func(t *testing.T) { - expectedDB := ConsulDB{} - - err := CreateDBClient("consul") - if err != nil { - t.Fatalf("TestCreateDBClient returned an error (%s)", err) - } - - if !reflect.DeepEqual(DBconn, &expectedDB) { - t.Fatalf("TestCreateDBClient set DBconn as:\n result=%v\n expected=%v", DBconn, expectedDB) - } - }) -} - -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) { - - inp := "{\"Data\":\"sdaijsdiodalkfjsdlagf\"," + - "\"Float\":34.4,\"Map\":{\"m1\":\"m1\",\"m3\":3}," + - "\"UUID\":\"123e4567-e89b-12d3-a456-426655440000\"}" - - got := make(map[string]interface{}) - err := DeSerialize(inp, &got) - if err != nil { - t.Fatal(err) - } - - expected := map[string]interface{}{ - "UUID": "123e4567-e89b-12d3-a456-426655440000", - "Data": "sdaijsdiodalkfjsdlagf", - "Float": 34.4, - "Map": map[string]interface{}{ - "m1": "m1", - "m3": 3.0, - }, - } - - if reflect.DeepEqual(expected, got) == false { - t.Errorf("Serialize returned unexpected : %s;"+ - " expected %s", got, expected) - } -} diff --git a/src/k8splugin/db/DB.go b/src/k8splugin/db/store.go index d92b5953..c1a8b31f 100644 --- a/src/k8splugin/db/DB.go +++ b/src/k8splugin/db/store.go @@ -21,27 +21,30 @@ import ( ) // DBconn interface used to talk a concrete Database connection -var DBconn DatabaseConnection - -// DatabaseConnection is an interface for accessing a database -type DatabaseConnection interface { - InitializeDatabase() error - CheckDatabase() error - CreateEntry(string, string) error - ReadEntry(string) (string, bool, error) - DeleteEntry(string) error +var DBconn Store + +// Store is an interface for accessing a database +type Store interface { + HealthCheck() error + + Create(string, string) error + Read(string) (string, error) + // Update(string) (string, error) + Delete(string) error + ReadAll(string) ([]string, error) } // CreateDBClient creates the DB client -var CreateDBClient = func(dbType string) error { +func CreateDBClient(dbType string) error { + var err error switch dbType { case "consul": - DBconn = &ConsulDB{} - return nil + DBconn, err = NewConsulStore(nil) default: return pkgerrors.New(dbType + "DB not supported") } + return err } // Serialize converts given data into a JSON string diff --git a/src/k8splugin/db/store_test.go b/src/k8splugin/db/store_test.go new file mode 100644 index 00000000..9bbe4a92 --- /dev/null +++ b/src/k8splugin/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 := &ConsulStore{} + + err := CreateDBClient("consul") + 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/db/testing.go b/src/k8splugin/db/testing.go new file mode 100644 index 00000000..672fcbfb --- /dev/null +++ b/src/k8splugin/db/testing.go @@ -0,0 +1,65 @@ +// +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 ( + "github.com/hashicorp/consul/api" +) + +//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 api.KVPairs + Err error +} + +func (m *MockDB) Create(key string, value string) error { + return m.Err +} + +func (m *MockDB) Read(key string) (string, error) { + if m.Err != nil { + return "", m.Err + } + + for _, kvpair := range m.Items { + if kvpair.Key == key { + return string(kvpair.Value), nil + } + } + + return "", nil +} + +func (m *MockDB) Delete(key string) error { + return m.Err +} + +func (m *MockDB) ReadAll(prefix string) ([]string, error) { + if m.Err != nil { + return []string{}, m.Err + } + + var res []string + + for _, keypair := range m.Items { + res = append(res, keypair.Key) + } + + return res, nil +} |