From c3e1c9a5fefde3fcb6aaf05c19b18f211c1a43ba Mon Sep 17 00:00:00 2001 From: Ritu Sood Date: Fri, 7 Feb 2020 18:36:54 -0800 Subject: Add etcd support in infrastructure layer Create a etcd client and provide minimal functionality needed by the orchestrator for using etcd Issue-ID: MULTICLOUD-871 Signed-off-by: Ritu Sood Change-Id: I56fb4643addf43cdc59366e7163b66bb1618876d --- src/orchestrator/cmd/main.go | 8 +- src/orchestrator/pkg/infra/contextdb/contextdb.go | 72 ++++++ src/orchestrator/pkg/infra/contextdb/etcd.go | 175 ++++++++++++++ src/orchestrator/pkg/infra/contextdb/etcd_test.go | 276 ++++++++++++++++++++++ src/orchestrator/pkg/infra/contextdb/mock.go | 58 +++++ 5 files changed, 588 insertions(+), 1 deletion(-) create mode 100644 src/orchestrator/pkg/infra/contextdb/contextdb.go create mode 100644 src/orchestrator/pkg/infra/contextdb/etcd.go create mode 100644 src/orchestrator/pkg/infra/contextdb/etcd_test.go create mode 100644 src/orchestrator/pkg/infra/contextdb/mock.go (limited to 'src/orchestrator') diff --git a/src/orchestrator/cmd/main.go b/src/orchestrator/cmd/main.go index fb8f26d6..f46fe910 100644 --- a/src/orchestrator/cmd/main.go +++ b/src/orchestrator/cmd/main.go @@ -26,7 +26,7 @@ import ( "github.com/onap/multicloud-k8s/src/orchestrator/pkg/infra/auth" "github.com/onap/multicloud-k8s/src/orchestrator/pkg/infra/config" "github.com/onap/multicloud-k8s/src/orchestrator/pkg/infra/db" - + contextDb "github.com/onap/multicloud-k8s/src/orchestrator/pkg/infra/contextdb" "github.com/gorilla/handlers" ) @@ -40,6 +40,12 @@ func main() { log.Println(err) log.Fatalln("Exiting...") } + err = contextDb.InitializeContextDatabase() + if err != nil { + log.Println("Unable to initialize database connection...") + log.Println(err) + log.Fatalln("Exiting...") + } httpRouter := api.NewRouter(nil) loggedRouter := handlers.LoggingHandler(os.Stdout, httpRouter) diff --git a/src/orchestrator/pkg/infra/contextdb/contextdb.go b/src/orchestrator/pkg/infra/contextdb/contextdb.go new file mode 100644 index 00000000..d18af227 --- /dev/null +++ b/src/orchestrator/pkg/infra/contextdb/contextdb.go @@ -0,0 +1,72 @@ +/* +Copyright 2020 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 contextdb + +import ( + "github.com/onap/multicloud-k8s/src/orchestrator/pkg/infra/config" + pkgerrors "github.com/pkg/errors" +) + +// Db interface used to talk a concrete Database connection +var Db ContextDb + +// ContextDb is an interface for accessing the context database +type ContextDb interface { + // Returns nil if db health is good + HealthCheck() error + // Puts Json Struct in db with key + Put(key string, value interface{}) error + // Delete k,v + Delete(key string) error + // Gets Json Struct from db + Get(key string, value interface{}) error + // Returns all keys with a prefix + GetAllKeys(path string) ([]string, error) +} + +// createContextDBClient creates the DB client +func createContextDBClient(dbType string) error { + var err error + switch dbType { + case "etcd": + c := EtcdConfig{ + Endpoint: config.GetConfiguration().EtcdIP, + CertFile: config.GetConfiguration().EtcdCert, + KeyFile: config.GetConfiguration().EtcdKey, + CAFile: config.GetConfiguration().EtcdCAFile, + } + Db, err = NewEtcdClient(nil, c) + if err != nil { + pkgerrors.Wrap(err, "Etcd Client Initialization failed with error") + } + default: + return pkgerrors.New(dbType + "DB not supported") + } + return err +} + +// InitializeContextDatabase sets up the connection to the +// configured database to allow the application to talk to it. +func InitializeContextDatabase() error { + // Only support Etcd for now + err := createContextDBClient("etcd") + if err != nil { + return pkgerrors.Cause(err) + } + err = Db.HealthCheck() + if err != nil { + return pkgerrors.Cause(err) + } + return nil +} diff --git a/src/orchestrator/pkg/infra/contextdb/etcd.go b/src/orchestrator/pkg/infra/contextdb/etcd.go new file mode 100644 index 00000000..a1922d3b --- /dev/null +++ b/src/orchestrator/pkg/infra/contextdb/etcd.go @@ -0,0 +1,175 @@ +/* + * Copyright 2020 Intel Corporation, Inc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package contextdb + +import ( + "context" + "encoding/json" + pkgerrors "github.com/pkg/errors" + "go.etcd.io/etcd/clientv3" + "go.etcd.io/etcd/pkg/transport" + "time" +) + +// EtcdConfig Configuration values needed for Etcd Client +type EtcdConfig struct { + Endpoint string + CertFile string + KeyFile string + CAFile string +} + +// EtcdClient for Etcd +type EtcdClient struct { + cli *clientv3.Client + endpoint string +} + +// Etcd For Mocking purposes +type Etcd interface { + Put(ctx context.Context, key, val string, opts ...clientv3.OpOption) (*clientv3.PutResponse, error) + Get(ctx context.Context, key string, opts ...clientv3.OpOption) (*clientv3.GetResponse, error) + Delete(ctx context.Context, key string, opts ...clientv3.OpOption) (*clientv3.DeleteResponse, error) +} + +var getEtcd = func(e *EtcdClient) Etcd { + return e.cli +} + +// NewEtcdClient function initializes Etcd client +func NewEtcdClient(store *clientv3.Client, c EtcdConfig) (ContextDb, error) { + var endpoint string + if store == nil { + tlsInfo := transport.TLSInfo{ + CertFile: c.CertFile, + KeyFile: c.KeyFile, + CAFile: c.CAFile, + } + tlsConfig, err := tlsInfo.ClientConfig() + if err != nil { + return nil, pkgerrors.Errorf("Error creating etcd TLSInfo: %s", err.Error()) + } + // NOTE: Client relies on nil tlsConfig + // for non-secure connections, update the implicit variable + if len(c.CertFile) == 0 && len(c.KeyFile) == 0 && len(c.CAFile) == 0 { + tlsConfig = nil + } + endpoint = "" + if tlsConfig == nil { + endpoint = "http://" + c.Endpoint + ":2379" + } else { + endpoint = "https://" + c.Endpoint + ":2379" + } + + store, err = clientv3.New(clientv3.Config{ + Endpoints: []string{endpoint}, + DialTimeout: 5 * time.Second, + TLS: tlsConfig, + }) + if err != nil { + return nil, pkgerrors.Errorf("Error creating etcd client: %s", err.Error()) + } + } + + return &EtcdClient{ + cli: store, + endpoint: endpoint, + }, nil +} + +// Put values in Etcd DB +func (e *EtcdClient) Put(key string, value interface{}) error { + cli := getEtcd(e) + if cli == nil { + return pkgerrors.Errorf("Etcd Client not initialized") + } + if key == "" { + return pkgerrors.Errorf("Key is null") + } + if value == nil { + return pkgerrors.Errorf("Value is nil") + } + v, err := json.Marshal(value) + if err != nil { + return pkgerrors.Errorf("Json Marshal error: %s", err.Error()) + } + _, err = cli.Put(context.Background(), key, string(v)) + if err != nil { + return pkgerrors.Errorf("Error creating etcd entry: %s", err.Error()) + } + return nil +} + +// Get values from Etcd DB and decodes from json +func (e *EtcdClient) Get(key string, value interface{}) error { + cli := getEtcd(e) + if cli == nil { + return pkgerrors.Errorf("Etcd Client not initialized") + } + if key == "" { + return pkgerrors.Errorf("Key is null") + } + if value == nil { + return pkgerrors.Errorf("Value is nil") + } + getResp, err := cli.Get(context.Background(), key) + if err != nil { + return pkgerrors.Errorf("Error getting etcd entry: %s", err.Error()) + } + if getResp.Count == 0 { + return pkgerrors.Errorf("Key doesn't exist") + } + return json.Unmarshal(getResp.Kvs[0].Value, value) +} + +// GetAllKeys values from Etcd DB +func (e *EtcdClient) GetAllKeys(key string) ([]string, error) { + cli := getEtcd(e) + if cli == nil { + return nil, pkgerrors.Errorf("Etcd Client not initialized") + } + getResp, err := cli.Get(context.Background(), key, clientv3.WithPrefix()) + if err != nil { + return nil, pkgerrors.Errorf("Error getting etcd entry: %s", err.Error()) + } + if getResp.Count == 0 { + return nil, pkgerrors.Errorf("Key doesn't exist") + } + var keys []string + for _, ev := range getResp.Kvs { + keys = append(keys, string(ev.Key)) + } + return keys, nil +} + +// Delete values from Etcd DB +func (e *EtcdClient) Delete(key string) error { + cli := getEtcd(e) + if cli == nil { + return pkgerrors.Errorf("Etcd Client not initialized") + } + _, err := cli.Delete(context.Background(), key, clientv3.WithPrefix()) + if err != nil { + return pkgerrors.Errorf("Delete failed etcd entry: %s", err.Error()) + } + return nil +} + +// HealthCheck for checking health of the etcd cluster +func (e *EtcdClient) HealthCheck() error { + return nil +} diff --git a/src/orchestrator/pkg/infra/contextdb/etcd_test.go b/src/orchestrator/pkg/infra/contextdb/etcd_test.go new file mode 100644 index 00000000..17b7a5d5 --- /dev/null +++ b/src/orchestrator/pkg/infra/contextdb/etcd_test.go @@ -0,0 +1,276 @@ +/* +Copyright 2020 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 contextdb + +import ( + "context" + mvccpb "github.com/coreos/etcd/mvcc/mvccpb" + pkgerrors "github.com/pkg/errors" + "go.etcd.io/etcd/clientv3" + "strings" + "testing" +) + +type kv struct { + Key []byte + Value []byte +} + +// MockEtcdClient for mocking etcd +type MockEtcdClient struct { + Kvs []*mvccpb.KeyValue + Count int64 + Err error +} + +// Mocking only Single Value +// Put function +func (e *MockEtcdClient) Put(ctx context.Context, key, val string, opts ...clientv3.OpOption) (*clientv3.PutResponse, error) { + var m mvccpb.KeyValue + m.Key = []byte(key) + m.Value = []byte(val) + e.Count = e.Count + 1 + e.Kvs = append(e.Kvs, &m) + return &clientv3.PutResponse{}, e.Err +} + +// Get function +func (e *MockEtcdClient) Get(ctx context.Context, key string, opts ...clientv3.OpOption) (*clientv3.GetResponse, error) { + var g clientv3.GetResponse + g.Kvs = e.Kvs + g.Count = e.Count + return &g, e.Err +} + +// Delete function +func (e *MockEtcdClient) Delete(ctx context.Context, key string, opts ...clientv3.OpOption) (*clientv3.DeleteResponse, error) { + return &clientv3.DeleteResponse{}, e.Err +} + +type testStruct struct { + Name string `json:"name"` + Num int `json:"num"` +} + +// TestPut test Put +func TestPut(t *testing.T) { + testCases := []struct { + label string + mockEtcd *MockEtcdClient + expectedError string + key string + value *testStruct + }{ + { + label: "Success Case", + mockEtcd: &MockEtcdClient{}, + key: "test1", + value: &testStruct{Name: "test", Num: 5}, + }, + { + label: "Key is null", + mockEtcd: &MockEtcdClient{}, + key: "", + expectedError: "Key is null", + }, + { + label: "Value is nil", + mockEtcd: &MockEtcdClient{}, + key: "test1", + value: nil, + expectedError: "Value is nil", + }, + { + label: "Error creating etcd entry", + mockEtcd: &MockEtcdClient{Err: pkgerrors.New("DB Error")}, + key: "test1", + value: &testStruct{Name: "test", Num: 5}, + expectedError: "Error creating etcd entry: DB Error", + }, + } + for _, testCase := range testCases { + t.Run(testCase.label, func(t *testing.T) { + cli, _ := NewEtcdClient(&clientv3.Client{}, EtcdConfig{}) + getEtcd = func(e *EtcdClient) Etcd { + return testCase.mockEtcd + } + err := cli.Put(testCase.key, testCase.value) + if err != nil { + if testCase.expectedError == "" { + t.Fatalf("Method returned an un-expected (%s)", err) + } + if !strings.Contains(string(err.Error()), testCase.expectedError) { + t.Fatalf("Method returned an error (%s)", err) + } + } + + }) + } +} + +func TestGet(t *testing.T) { + testCases := []struct { + label string + mockEtcd *MockEtcdClient + expectedError string + key string + value *testStruct + }{ + { + label: "Key is null", + mockEtcd: &MockEtcdClient{}, + key: "", + value: nil, + expectedError: "Key is null", + }, + { + label: "Key doesn't exist", + mockEtcd: &MockEtcdClient{}, + key: "test1", + value: &testStruct{}, + expectedError: "Key doesn't exist", + }, + { + label: "Error getting etcd entry", + mockEtcd: &MockEtcdClient{Err: pkgerrors.New("DB Error")}, + key: "test1", + value: &testStruct{}, + expectedError: "Error getting etcd entry: DB Error", + }, + } + for _, testCase := range testCases { + t.Run(testCase.label, func(t *testing.T) { + cli, _ := NewEtcdClient(&clientv3.Client{}, EtcdConfig{}) + getEtcd = func(e *EtcdClient) Etcd { + return testCase.mockEtcd + } + err := cli.Get(testCase.key, testCase.value) + if err != nil { + if testCase.expectedError == "" { + t.Fatalf("Method returned an un-expected (%s)", err) + } + if !strings.Contains(string(err.Error()), testCase.expectedError) { + t.Fatalf("Method returned an error (%s)", err) + } + } + + }) + } +} + +func TestGetString(t *testing.T) { + testCases := []struct { + label string + mockEtcd *MockEtcdClient + expectedError string + value string + }{ + { + label: "Success Case", + mockEtcd: &MockEtcdClient{}, + }, + } + for _, testCase := range testCases { + t.Run(testCase.label, func(t *testing.T) { + cli, _ := NewEtcdClient(&clientv3.Client{}, EtcdConfig{}) + getEtcd = func(e *EtcdClient) Etcd { + return testCase.mockEtcd + } + err := cli.Put("test", "test1") + if err != nil { + t.Error("Test failed", err) + } + var s string + err = cli.Get("test", &s) + if err != nil { + t.Error("Test failed", err) + } + if "test1" != s { + t.Error("Get Failed") + } + }) + } +} + +func TestDelete(t *testing.T) { + testCases := []struct { + label string + mockEtcd *MockEtcdClient + expectedError string + }{ + { + label: "Success Case", + mockEtcd: &MockEtcdClient{}, + }, + { + label: "Delete failed etcd entry", + mockEtcd: &MockEtcdClient{Err: pkgerrors.New("DB Error")}, + expectedError: "Delete failed etcd entry: DB Error", + }, + } + for _, testCase := range testCases { + t.Run(testCase.label, func(t *testing.T) { + cli, _ := NewEtcdClient(&clientv3.Client{}, EtcdConfig{}) + getEtcd = func(e *EtcdClient) Etcd { + return testCase.mockEtcd + } + err := cli.Delete("test") + if err != nil { + if testCase.expectedError == "" { + t.Fatalf("Method returned an un-expected (%s)", err) + } + if !strings.Contains(string(err.Error()), testCase.expectedError) { + t.Fatalf("Method returned an error (%s)", err) + } + } + + }) + } +} + +func TestGetAll(t *testing.T) { + testCases := []struct { + label string + mockEtcd *MockEtcdClient + expectedError string + }{ + { + label: "Key doesn't exist", + mockEtcd: &MockEtcdClient{}, + expectedError: "Key doesn't exist", + }, + { + label: "Error getting etcd entry", + mockEtcd: &MockEtcdClient{Err: pkgerrors.New("DB Error")}, + expectedError: "Error getting etcd entry: DB Error", + }, + } + for _, testCase := range testCases { + t.Run(testCase.label, func(t *testing.T) { + cli, _ := NewEtcdClient(&clientv3.Client{}, EtcdConfig{}) + getEtcd = func(e *EtcdClient) Etcd { + return testCase.mockEtcd + } + _, err := cli.GetAllKeys("test") + if err != nil { + if testCase.expectedError == "" { + t.Fatalf("Method returned an un-expected (%s)", err) + } + if !strings.Contains(string(err.Error()), testCase.expectedError) { + t.Fatalf("Method returned an error (%s)", err) + } + } + }) + } +} diff --git a/src/orchestrator/pkg/infra/contextdb/mock.go b/src/orchestrator/pkg/infra/contextdb/mock.go new file mode 100644 index 00000000..fc0f8ff7 --- /dev/null +++ b/src/orchestrator/pkg/infra/contextdb/mock.go @@ -0,0 +1,58 @@ +/* +Copyright 2020 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 contextdb + +import ( + pkgerrors "github.com/pkg/errors" +) + +type MockEtcd struct { + Items map[string]interface{} + Err error +} + +func (c *MockEtcd) Put(key string, value interface{}) error { + if c.Items == nil { + c.Items = make(map[string]interface{}) + } + c.Items[key] = value + return c.Err +} + +func (c *MockEtcd) Get(key string, value interface{}) error { + for kvKey, kvValue := range c.Items { + if kvKey == key { + value = kvValue + return nil + } + } + return pkgerrors.Errorf("Key doesn't exist") +} + +func (c *MockEtcd) Delete(key string) error { + delete(c.Items, key) + return c.Err +} + +func (c *MockEtcd) GetAllKeys(path string) ([]string, error) { + var keys []string + for k, _ := range c.Items { + keys = append(keys, string(k)) + } + return keys, nil +} + +func (e *MockEtcd) HealthCheck() error { + return nil +} -- cgit 1.2.3-korg