From 277f322e53bb123783c0945a78a263ca79c5dd2a Mon Sep 17 00:00:00 2001 From: Timo Tietavainen Date: Tue, 23 Nov 2021 08:32:04 +0200 Subject: [PATCH] Implement SDL CLI 'get namespaces' -command Implement a new 'get namespaces' -command for 'sdlcli' -tool. With this command user can list all the namespaces found in database. Command syntax is: sdlcli get namespaces [flags] Supported flags are -g, --group, -h and --help flags. With -g,--group flag user can get a list of namespace per SDL cluster group address and -h, --help flags shows the command help. Issue-Id: RIC-113 Signed-off-by: Timo Tietavainen Change-Id: If4d66497eeec94d479748c81cf0898c2fbc78364 --- internal/cli/cli_private_fn_test.go | 13 +- internal/cli/get.go | 17 +- internal/cli/get_test.go | 2 +- internal/cli/namespaces.go | 140 +++++++++++++++ internal/cli/namespaces_test.go | 270 +++++++++++++++++++++++++++++ internal/cli/types.go | 1 + internal/mocks/db_mocks_private_testing.go | 5 + 7 files changed, 434 insertions(+), 14 deletions(-) create mode 100644 internal/cli/namespaces.go create mode 100644 internal/cli/namespaces_test.go diff --git a/internal/cli/cli_private_fn_test.go b/internal/cli/cli_private_fn_test.go index 2dacf4d..d37cb9e 100644 --- a/internal/cli/cli_private_fn_test.go +++ b/internal/cli/cli_private_fn_test.go @@ -23,10 +23,11 @@ package cli var ( - NewRootCmd = newRootCmd - NewHealthCheckCmd = newHealthCheckCmd - NewKeysCmdForTest = newKeysCmd - NewGetCmdForTest = newGetCmd - NewSetCmdForTest = newSetCmd - NewRemoveCmdForTest = newRemoveCmd + NewRootCmd = newRootCmd + NewHealthCheckCmd = newHealthCheckCmd + NewKeysCmdForTest = newKeysCmd + NewGetCmdForTest = newGetCmd + NewSetCmdForTest = newSetCmd + NewRemoveCmdForTest = newRemoveCmd + NewNamespacesCmdForTest = newNamespacesCmd ) diff --git a/internal/cli/get.go b/internal/cli/get.go index de4f80c..f22ae73 100644 --- a/internal/cli/get.go +++ b/internal/cli/get.go @@ -43,16 +43,19 @@ func init() { var ( getLong = `Display one or many resources. -Prints keys and keys data in the given namespace.` +Prints namespaces, keys or keys data in the given namespace.` - getExample = ` # Get reads keys data in the given namespace. - sdlcli get sdlns key1 - - # Get reads multiple keys data in the given namespace. - sdlcli get sdlns key1 key2 key3 + getExample = ` # List all the namespaces in database. + sdlcli get namespaces # List keys in the given namespace. - sdlcli get keys sdlns` + sdlcli get keys sdlns + + # Reads key data in the given namespace. + sdlcli get sdlns key1 + + # Read multiple keys data in the given namespace. + sdlcli get sdlns key1 key2 key3` ) func newGetCmd(sdlCb SyncStorageCreateCb) *cobra.Command { diff --git a/internal/cli/get_test.go b/internal/cli/get_test.go index 22301b9..66bc115 100644 --- a/internal/cli/get_test.go +++ b/internal/cli/get_test.go @@ -72,7 +72,7 @@ func runGetCmdCli() (string, string, error) { func TestGetCmdShowHelp(t *testing.T) { var expOkErr error - expHelp := "Display one or many resources.\n\nPrints keys and keys data in the given namespace." + expHelp := "Display one or many resources.\n\nPrints namespaces, keys or keys data in the given namespace." expHelpUsage := "Usage:\n get [ ... ] [flags]" expArgsErr := errors.New("accepts command or arguments, received 0") expNokErr := errors.New("unknown flag: --ff") diff --git a/internal/cli/namespaces.go b/internal/cli/namespaces.go new file mode 100644 index 0000000..9cf5ee8 --- /dev/null +++ b/internal/cli/namespaces.go @@ -0,0 +1,140 @@ +/* + Copyright (c) 2021 AT&T Intellectual Property. + Copyright (c) 2018-2021 Nokia. + + 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. +*/ + +/* + * This source code is part of the near-RT RIC (RAN Intelligent Controller) + * platform project (RICP). + */ + +package cli + +import ( + "fmt" + "github.com/spf13/cobra" + "os" + "sort" + "strings" +) + +func init() { + getCmd.AddCommand(newNamespacesCmd(newDatabase)) +} + +func newNamespacesCmd(dbCreateCb DbCreateCb) *cobra.Command { + cmd := &cobra.Command{ + Use: "namespaces", + Short: "List all the namespaces in database", + Long: "List all the namespaces in database", + Args: cobra.ExactArgs(0), + RunE: func(cmd *cobra.Command, args []string) error { + showPerDb, _ := cmd.Flags().GetBool("group") + nsMap, err := runNamespaces(dbCreateCb) + if err != nil { + cmd.PrintErrf("%s\n", buf.String()) + return err + } + if showPerDb { + printNamespacesPerDb(cmd, nsMap) + } else { + printNamespaces(cmd, nsMap) + } + return err + }, + } + cmd.SetOut(os.Stdout) + cmd.Flags().BoolP("group", "g", false, "Show namespaces per SDL DB cluster group") + return cmd +} + +func runNamespaces(dbCreateCb DbCreateCb) (map[string][]string, error) { + nsMap := make(map[string][]string) + for _, dbInst := range dbCreateCb().Instances { + keys, err := dbInst.Keys("*") + if err != nil { + return nil, err + } + id, err := getServiceAddress(dbInst) + if err != nil { + return nil, err + } + for _, key := range keys { + namespace, err := parseKeyNamespace(key) + if err != nil { + return nil, err + } + if isUniqueNamespace(nsMap[id], namespace) { + nsMap[id] = append(nsMap[id], namespace) + } + } + } + return nsMap, nil +} + +func getServiceAddress(db iDatabase) (string, error) { + state, err := db.State() + if err != nil { + return "", err + } + return state.MasterDbState.GetAddress(), nil +} + +func parseKeyNamespace(key string) (string, error) { + sIndex := strings.Index(key, "{") + if sIndex == -1 { + return "", fmt.Errorf("Namespace parsing error, no '{' in key string '%s'", key) + } + str := key[sIndex+len("{"):] + eIndex := strings.Index(str, "}") + if eIndex == -1 { + return "", fmt.Errorf("Namespace parsing error, no '}' in key string '%s'", key) + } + return str[:eIndex], nil +} + +func isUniqueNamespace(namespaces []string, newNs string) bool { + for _, n := range namespaces { + if n == newNs { + return false + } + } + return true +} + +func printNamespaces(cmd *cobra.Command, nsMap map[string][]string) { + var namespaces []string + for _, nsList := range nsMap { + namespaces = append(namespaces, nsList...) + } + + sort.Strings(namespaces) + for _, ns := range namespaces { + cmd.Println(ns) + } +} + +func printNamespacesPerDb(cmd *cobra.Command, nsMap map[string][]string) { + for addr, nsList := range nsMap { + sort.Strings(nsList) + for _, ns := range nsList { + if addr == "" { + cmd.Printf("%s\n", ns) + } else { + cmd.Printf("%s: %s\n", addr, ns) + } + } + } +} diff --git a/internal/cli/namespaces_test.go b/internal/cli/namespaces_test.go new file mode 100644 index 0000000..28b6343 --- /dev/null +++ b/internal/cli/namespaces_test.go @@ -0,0 +1,270 @@ +/* + Copyright (c) 2021 AT&T Intellectual Property. + Copyright (c) 2018-2021 Nokia. + + 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. +*/ + +/* + * This source code is part of the near-RT RIC (RAN Intelligent Controller) + * platform project (RICP). + */ + +package cli_test + +import ( + "bytes" + "errors" + "gerrit.o-ran-sc.org/r/ric-plt/sdlgo/internal/cli" + "gerrit.o-ran-sc.org/r/ric-plt/sdlgo/internal/mocks" + "gerrit.o-ran-sc.org/r/ric-plt/sdlgo/internal/sdlgoredis" + "github.com/stretchr/testify/assert" + "testing" +) + +var mNs *nsMock + +type nsMock struct { + dbIface *mocks.MockDB + dbKeys []string + dbState sdlgoredis.DbState + dbKeysErr error + dbStateErr error +} + +func setupNamespacesCliMock(keys []string, addr string, keysErr, stateErr error) { + mNs = new(nsMock) + mNs.dbKeys = keys + mNs.dbState.MasterDbState.Fields.Role = "Master" + mNs.dbState.MasterDbState.Fields.Ip = addr + if addr != "" { + mNs.dbState.MasterDbState.Fields.Port = "6379" + } + mNs.dbKeysErr = keysErr + mNs.dbStateErr = stateErr +} + +func newNsMockDatabase() *cli.Database { + db := &cli.Database{} + mNs.dbIface = new(mocks.MockDB) + mNs.dbIface.On("Keys", "*").Return(mNs.dbKeys, mNs.dbKeysErr) + mNs.dbIface.On("State").Return(&mNs.dbState, mNs.dbStateErr).Maybe() + db.Instances = append(db.Instances, mNs.dbIface) + return db +} + +func TestNamespacesCmdShowHelp(t *testing.T) { + var expOkErr error + expNokErrTooManyArgs := errors.New("accepts 0 arg(s), received 1") + expHelp := "Usage:\n namespaces [flags]" + tests := []struct { + args []string + expOut string + expErr error + }{ + {args: []string{"-h"}, expErr: expOkErr, expOut: expHelp}, + {args: []string{"--help"}, expErr: expOkErr, expOut: expHelp}, + {args: []string{"extra-arg"}, expErr: expNokErrTooManyArgs, expOut: expHelp}, + } + + for _, test := range tests { + buf := new(bytes.Buffer) + cmd := cli.NewNamespacesCmdForTest(newNsMockDatabase) + cmd.SetOut(buf) + cmd.SetErr(buf) + cmd.SetArgs(test.args) + err := cmd.Execute() + result := buf.String() + + assert.Equal(t, test.expErr, err) + assert.Contains(t, result, test.expOut) + } +} + +func TestNamespacesCmdSuccess(t *testing.T) { + expOut := "ns1\n" + "ns2\n" + "ns3\n" + buf := new(bytes.Buffer) + setupNamespacesCliMock([]string{ + "{ns1},key1", + "{ns3},key1", + "{ns2},key1", + "{ns1},key2", + }, "1.2.3.4", nil, nil) + cmd := cli.NewNamespacesCmdForTest(newNsMockDatabase) + cmd.SetOut(buf) + cmd.SetErr(buf) + + err := cmd.Execute() + result := buf.String() + + assert.Nil(t, err) + assert.Equal(t, expOut, result) + mNs.dbIface.AssertExpectations(t) +} + +func TestNamespacesCmdWithPerDbFlagSuccess(t *testing.T) { + expOut := "1.2.3.4:6379: ns1\n" + "1.2.3.4:6379: ns2\n" + "1.2.3.4:6379: ns3\n" + buf := new(bytes.Buffer) + setupNamespacesCliMock([]string{ + "{ns1},key1", + "{ns3},key1", + "{ns2},key1", + "{ns1},key2", + }, "1.2.3.4", nil, nil) + cmd := cli.NewNamespacesCmdForTest(newNsMockDatabase) + cmd.SetOut(buf) + cmd.SetErr(buf) + cmd.SetArgs([]string{"--group"}) + + err := cmd.Execute() + result := buf.String() + + assert.Nil(t, err) + assert.Equal(t, expOut, result) + mNs.dbIface.AssertExpectations(t) +} + +func TestNamespacesCmdNoKeysInDbSuccess(t *testing.T) { + expOut := "" + buf := new(bytes.Buffer) + setupNamespacesCliMock([]string{}, "1.2.3.4", nil, nil) + cmd := cli.NewNamespacesCmdForTest(newNsMockDatabase) + cmd.SetOut(buf) + cmd.SetErr(buf) + + err := cmd.Execute() + result := buf.String() + + assert.Nil(t, err) + assert.Equal(t, expOut, result) + mNs.dbIface.AssertExpectations(t) +} + +func TestNamespacesCmdWithPerDbFlagNoKeysInDbSuccess(t *testing.T) { + expOut := "" + buf := new(bytes.Buffer) + setupNamespacesCliMock([]string{}, "1.2.3.4", nil, nil) + cmd := cli.NewNamespacesCmdForTest(newNsMockDatabase) + cmd.SetOut(buf) + cmd.SetErr(buf) + cmd.SetArgs([]string{"--group"}) + + err := cmd.Execute() + result := buf.String() + + assert.Nil(t, err) + assert.Equal(t, expOut, result) + mNs.dbIface.AssertExpectations(t) +} + +func TestNamespacesCmdWithPerDbFlagStandaloneRedisAddressMissingSuccess(t *testing.T) { + expOut := "ns1\n" + "ns2\n" + "ns3\n" + buf := new(bytes.Buffer) + setupNamespacesCliMock([]string{ + "{ns1},key1", + "{ns3},key1", + "{ns2},key1", + "{ns1},key2", + }, "", nil, nil) + cmd := cli.NewNamespacesCmdForTest(newNsMockDatabase) + cmd.SetOut(buf) + cmd.SetErr(buf) + cmd.SetArgs([]string{"--group"}) + + err := cmd.Execute() + result := buf.String() + + assert.Nil(t, err) + assert.Equal(t, expOut, result) + mNs.dbIface.AssertExpectations(t) +} + +func TestNamespacesCmdDbKeysFailure(t *testing.T) { + expNokErr := errors.New("Some error") + expOut := "Error: Some error" + + buf := new(bytes.Buffer) + setupNamespacesCliMock(nil, "1.2.3.4", expNokErr, nil) + cmd := cli.NewNamespacesCmdForTest(newNsMockDatabase) + cmd.SetOut(buf) + cmd.SetErr(buf) + + err := cmd.Execute() + result := buf.String() + + assert.Equal(t, expNokErr, err) + assert.Contains(t, result, expOut) + mNs.dbIface.AssertExpectations(t) +} + +func TestNamespacesCmdDbStateFailure(t *testing.T) { + expNokErr := errors.New("Some error") + expOut := "Error: Some error" + + buf := new(bytes.Buffer) + setupNamespacesCliMock(nil, "1.2.3.4", nil, expNokErr) + cmd := cli.NewNamespacesCmdForTest(newNsMockDatabase) + cmd.SetOut(buf) + cmd.SetErr(buf) + + err := cmd.Execute() + result := buf.String() + + assert.Equal(t, expNokErr, err) + assert.Contains(t, result, expOut) + mNs.dbIface.AssertExpectations(t) +} + +func TestNamespacesCmdNsStartMarkerFailure(t *testing.T) { + expNokErr := errors.New("Namespace parsing error, no '{' in key string 'ns2},key1'") + expOut := "Error: Namespace parsing error, no '{' in key string 'ns2},key1'" + + buf := new(bytes.Buffer) + setupNamespacesCliMock([]string{ + "{ns1},key1", + "ns2},key1", + "{ns1},key2", + }, "1.2.3.4", nil, nil) + cmd := cli.NewNamespacesCmdForTest(newNsMockDatabase) + cmd.SetOut(buf) + cmd.SetErr(buf) + + err := cmd.Execute() + result := buf.String() + + assert.Equal(t, expNokErr, err) + assert.Contains(t, result, expOut) + mNs.dbIface.AssertExpectations(t) +} + +func TestNamespacesCmdNsEndMarkerFailure(t *testing.T) { + expNokErr := errors.New("Namespace parsing error, no '}' in key string '{ns2,key1'") + expOut := "Error: Namespace parsing error, no '}' in key string '{ns2,key1'" + + buf := new(bytes.Buffer) + setupNamespacesCliMock([]string{ + "{ns1},key1", + "{ns2,key1", + "{ns1},key2", + }, "1.2.3.4", nil, nil) + cmd := cli.NewNamespacesCmdForTest(newNsMockDatabase) + cmd.SetOut(buf) + cmd.SetErr(buf) + + err := cmd.Execute() + result := buf.String() + + assert.Equal(t, expNokErr, err) + assert.Contains(t, result, expOut) + mNs.dbIface.AssertExpectations(t) +} diff --git a/internal/cli/types.go b/internal/cli/types.go index 53953a8..f5c4439 100644 --- a/internal/cli/types.go +++ b/internal/cli/types.go @@ -29,6 +29,7 @@ import "gerrit.o-ran-sc.org/r/ric-plt/sdlgo/internal/sdlgoredis" type iDatabase interface { Info() (*sdlgoredis.DbInfo, error) State() (*sdlgoredis.DbState, error) + Keys(pattern string) ([]string, error) } //Database struct is a holder for the internal database instances. diff --git a/internal/mocks/db_mocks_private_testing.go b/internal/mocks/db_mocks_private_testing.go index a17f7d3..42bb061 100644 --- a/internal/mocks/db_mocks_private_testing.go +++ b/internal/mocks/db_mocks_private_testing.go @@ -41,6 +41,11 @@ func (m *MockDB) State() (*sdlgoredis.DbState, error) { return a.Get(0).(*sdlgoredis.DbState), a.Error(1) } +func (m *MockDB) Keys(pattern string) ([]string, error) { + a := m.Called(pattern) + return a.Get(0).([]string), a.Error(1) +} + type MockSdlApi struct { mock.Mock } -- 2.16.6