From 2b9389d7fc654433edb82db08200b731227556b0 Mon Sep 17 00:00:00 2001 From: May Rosenbaum Date: Wed, 29 Nov 2023 01:07:40 +0200 Subject: [PATCH] cluster management test - add node Signed-off-by: May Rosenbaum --- cli/README.md | 90 +++++++- cli/commands/config.go | 109 ++++++++- cli/commands/config_test.go | 355 ++++++++++++++++++++++++++++- pkg/bcdb/config_tx_context.go | 22 ++ pkg/bcdb/config_tx_context_test.go | 29 +++ pkg/bcdb/tx_context.go | 3 +- 6 files changed, 596 insertions(+), 12 deletions(-) diff --git a/cli/README.md b/cli/README.md index 1bf6204..e8f16b8 100644 --- a/cli/README.md +++ b/cli/README.md @@ -6,6 +6,7 @@ This command-line tool provides a simple way to config an orion database server. 1. Run from `orion-sdk` root folder 2. Run `make binary` to create an executable file named bcdbadmin under `bin` directory. + ## Commands Here we list and describe the available commands. @@ -13,6 +14,7 @@ We give a short explanation of their usage and describe the flags for each comma We provide real-world examples demonstrating how to use the CLI tool for various tasks. + ### Version Command This command prints the version of the CLI tool. 1. Run from `orion-sdk` root folder. @@ -23,6 +25,8 @@ This command prints the version of the CLI tool. ### Config Command This command enables to config an orion server or ask for the configuration of an orion server. +### + #### Get Config Command 1. Run from 'orion-sdk' root folder. 2. For Get Config Run `bin/bcdbadmin config get [args]`. @@ -45,11 +49,65 @@ Running `bin/bcdbadmin config get -d "connection-session-config.yaml" -c "local/config"` reads the connection and session details needed for connecting to a server from `connection-session-config.yaml` and sends a config TX. -It creates directories in `local/config` with the respective certificates, a yaml file, named shared_cluster_config.yml, that includes the cluster configuration -and a yaml file, named version.yml, that includes the version. +It creates directories in `local/config` with the respective certificates, a yaml file, named `shared_cluster_config.yml`, that includes the cluster configuration +and a yaml file, named `version.yml`, that includes the version. + + +### + +#### Get Last Config Block Command +1. Run from 'orion-sdk' root folder. +2. For Get last config block Run `bin/bcdbadmin config getLastConfigBlock [args]`. + + Replace `[args]` with flags. + +### +##### Flags +| Flags | Description | +|-----------------------------------|----------------------------------------------------------------------------| +| `-d, --db-connection-config-path` | the absolute or relative path of CLI connection configuration file | +| `-c, --cluster-config-path` | the absolute or relative path to which the last config block will be saved | + +Both flags are necessary flags. If any flag is missing, the cli will raise an error. + +### +##### Example: +Running +`bin/bcdbadmin config getLastconfigBlock -d "connection-session-config.yaml" -c "local/config"` +reads the connection and session details needed for connecting to a server from `connection-session-config.yaml` and +sends a config TX. +It creates a yaml file, named `last_config_block.yml`, under the `local/config` directory. +### + +#### Get Cluster Status Command +1. Run from 'orion-sdk' root folder. +2. For Get last config block Run `bin/bcdbadmin config getClusterStatus [args]`. + + Replace `[args]` with flags. + +### +##### Flags +| Flags | Description | +|-----------------------------------|----------------------------------------------------------------------------| +| `-d, --db-connection-config-path` | the absolute or relative path of CLI connection configuration file | + +The flag above is a necessary flag. If the flag is missing, the cli will raise an error. + +### +##### Example: + +Running +`bin/bcdbadmin config getClusterStatus -d "connection-session-config.yaml"` +reads the connection and session details needed for connecting to a server from `connection-session-config.yaml` and +sends a config TX. +It prints the output (the cluster status) to the screen. + + +### + #### Set Config Command 1. Run from 'orion-sdk' root folder. 2. For Set Config Run: @@ -77,4 +135,30 @@ Running `bin/bcdbadmin config set -d "connection-session-config.yaml" -c "local/new_cluster_config.yml"` reads the connection and session details needed for connecting to a server from `connection-session-config.yaml` and sends a config TX. -It reads the `local/new_cluster_config.yml` to fetch the new cluster configuration and set it. \ No newline at end of file +It reads the `local/new_cluster_config.yml` to fetch the new cluster configuration and set it. + + +### +#### Using the set config command to manage the cluster configuration +In addition to reconfiguring parameters, the above commands can be used to add or remove a node. + +The following steps describe how to add a node to the cluster: +1. Run from 'orion-sdk' root folder. +2. Run `bin/bcdbadmin config get [args]` to get the cluster configuration. Replace `[args]` with corresponding flags as detailed above, see [Get Config Command](#get_config_command). +3. Create a new shared configuration file, named `new_cluster_config.yml`, and add the 4th node to the configuration. Make sure to add the node to both the Members list and the Nodes list. + + Note: it is possible to create a new file or to edit the `shared_cluster_config.yml` obtained in the previous step. +4. Run `bin/bcdbadmin config set [args]` to set the new configuration. Replace `[args]` with corresponding flags as detailed above, see [Set Config Command](#set_config_command). + + After this step the cluster configuration should contain 4 nodes. +5. Run `bin/bcdbadmin config getLastConfigBlock [args]` to get the last config block. Replace `[args]` with corresponding flags as detailed above, see [Get Last Config Block Command](#get_last_config_block_command). +6. Edit the `config.yml` file of 4th node and change the boostrap method and file: + ```yaml + - bootstrap: + method: join + file: [the path for last_config_block.yml file] + ``` +8. Start the 4th node. +9. Run `bin/bcdbadmin config getClusterStatus [args]` to get the cluster status. Replace `[args]` with corresponding flags as detailed above, see [Get Cluster Status Command](#get_cluster_status_command). + + Make sure there are 4 nodes in the resulting configuration. \ No newline at end of file diff --git a/cli/commands/config.go b/cli/commands/config.go index 32f1948..482092b 100644 --- a/cli/commands/config.go +++ b/cli/commands/config.go @@ -28,7 +28,7 @@ func configCmd() *cobra.Command { panic(err.Error()) } - configCmd.AddCommand(getConfigCmd(), setConfigCmd()) + configCmd.AddCommand(getConfigCmd(), setConfigCmd(), getLastConfigBlockCmd(), getClusterStatusCmd()) return configCmd } @@ -65,6 +65,33 @@ func setConfigCmd() *cobra.Command { return setConfigCmd } +func getLastConfigBlockCmd() *cobra.Command { + getLastConfigBlockCmd := &cobra.Command{ + Use: "getLastConfigBlock", + Short: "Get last configuration block", + Example: "cli config getLastConfigBlock -d -c ", + RunE: getLastConfigBlock, + } + + getLastConfigBlockCmd.PersistentFlags().StringP("last-config-block-path", "c", "", "set the absolute or relative path of the last configuration block file") + if err := getLastConfigBlockCmd.MarkPersistentFlagRequired("last-config-block-path"); err != nil { + panic(err.Error()) + } + + return getLastConfigBlockCmd +} + +func getClusterStatusCmd() *cobra.Command { + getClusterStatusCmd := &cobra.Command{ + Use: "getClusterStatus", + Short: "Get cluster status", + Example: "cli config getClusterStatus -d ", + RunE: getClusterStatus, + } + + return getClusterStatusCmd +} + func getConfig(cmd *cobra.Command, args []string) error { cliConfigPath, err := cmd.Flags().GetString("db-connection-config-path") if err != nil { @@ -181,6 +208,86 @@ func setConfig(cmd *cobra.Command, args []string) error { return nil } +func getLastConfigBlock(cmd *cobra.Command, args []string) error { + cliConfigPath, err := cmd.Flags().GetString("db-connection-config-path") + if err != nil { + return errors.Wrapf(err, "failed to fetch the path of CLI connection configuration file") + } + + getLastConfigBlockPath, err := cmd.Flags().GetString("last-config-block-path") + if err != nil { + return errors.Wrapf(err, "failed to fetch the path to which the last configuration block will be saved") + } + + params := cliConfigParams{ + cliConfigPath: cliConfigPath, + cliConfig: cliConnectionConfig{}, + db: nil, + session: nil, + } + + err = params.CreateDbAndOpenSession() + if err != nil { + return err + } + + tx, err := params.session.ConfigTx() + if err != nil { + return errors.Wrapf(err, "failed to instanciate a config TX") + } + defer abort(tx) + + blk, err := tx.GetLastConfigBlock() + if err != nil { + return errors.Wrapf(err, "failed to fetch the last config block") + } + + err = os.MkdirAll(getLastConfigBlockPath, 0755) + if err != nil { + errors.Wrapf(err, "failed to create output directory") + } + + err = os.WriteFile(path.Join(getLastConfigBlockPath, "last_config_block.yml"), blk, 0644) + if err != nil { + return errors.Wrapf(err, "failed to create last config block yaml file") + } + + return nil +} + +func getClusterStatus(cmd *cobra.Command, args []string) error { + cliConfigPath, err := cmd.Flags().GetString("db-connection-config-path") + if err != nil { + return errors.Wrapf(err, "failed to fetch the path of CLI connection configuration file") + } + + params := cliConfigParams{ + cliConfigPath: cliConfigPath, + cliConfig: cliConnectionConfig{}, + db: nil, + session: nil, + } + + err = params.CreateDbAndOpenSession() + if err != nil { + return err + } + + tx, err := params.session.ConfigTx() + if err != nil { + return errors.Wrapf(err, "failed to instanciate a config TX") + } + defer abort(tx) + + status, err := tx.GetClusterStatus() + if err != nil { + return errors.Wrapf(err, "failed to fetch the cluster status") + } + + cmd.Printf("Cluster status is: %s\n", status) + return nil +} + func abort(tx bcdb.TxContext) { _ = tx.Abort() } diff --git a/cli/commands/config_test.go b/cli/commands/config_test.go index c550a4d..3f8e402 100644 --- a/cli/commands/config_test.go +++ b/cli/commands/config_test.go @@ -1,12 +1,24 @@ package commands import ( + "bytes" + "encoding/pem" + "fmt" + "math" "net/url" "os" "path" "path/filepath" "strconv" + "strings" "testing" + "time" + + "github.com/golang/protobuf/proto" + sdkconfig "github.com/hyperledger-labs/orion-sdk-go/pkg/config" + "github.com/hyperledger-labs/orion-server/config" + "github.com/hyperledger-labs/orion-server/pkg/server/testutils" + "github.com/hyperledger-labs/orion-server/test/setup" "github.com/hyperledger-labs/orion-sdk-go/examples/util" "github.com/hyperledger-labs/orion-sdk-go/pkg/bcdb" @@ -22,7 +34,7 @@ func TestCheckCertsEncoderIsValid(t *testing.T) { tempDir, err := os.MkdirTemp(os.TempDir(), "Cli-Check-Certs-Test") require.NoError(t, err) - testServer, _, _, err := util.SetupTestEnv(t, tempDir, uint32(6003)) + testServer, _, _, err := util.SetupTestEnv(t, tempDir, uint32(6004)) require.NoError(t, err) defer testServer.Stop() util.StartTestServer(t, testServer) @@ -37,7 +49,7 @@ func TestCheckCertsEncoderIsValid(t *testing.T) { session, err := bcdb.Session(&c.SessionConfig) require.NoError(t, err) - // 3. get cluster configration + // 3. get cluster configuration tx, err := session.ConfigTx() require.NoError(t, err) @@ -54,7 +66,7 @@ func TestCheckCertsEncoderIsValid(t *testing.T) { err = parseAndSaveCerts(clusterConfig, parsedCertsDir) require.NoError(t, err) - // 5. compare the generated certs with the certs recieved from the tx + // 5. compare the generated certs with the certs received from the tx err = compareFiles(path.Join(tempDir, "crypto", "admin", "admin.pem"), path.Join(parsedCertsDir, "admins", "admin.pem")) require.NoError(t, err) err = compareFiles(path.Join(tempDir, "crypto", "node", "node.pem"), path.Join(parsedCertsDir, "nodes", "server1.pem")) @@ -68,7 +80,7 @@ func TestCheckCertsDecoderIsValid(t *testing.T) { tempDir, err := os.MkdirTemp(os.TempDir(), "Cli-Check-Certs-Test") require.NoError(t, err) - testServer, _, _, err := util.SetupTestEnv(t, tempDir, uint32(6003)) + testServer, _, _, err := util.SetupTestEnv(t, tempDir, uint32(6004)) require.NoError(t, err) defer testServer.Stop() util.StartTestServer(t, testServer) @@ -83,7 +95,7 @@ func TestCheckCertsDecoderIsValid(t *testing.T) { session, err := bcdb.Session(&c.SessionConfig) require.NoError(t, err) - // 3. get cluster configration + // 3. get cluster configuration tx, err := session.ConfigTx() require.NoError(t, err) @@ -165,7 +177,7 @@ func TestGetConfigCommand(t *testing.T) { tempDir, err := os.MkdirTemp(os.TempDir(), "Cli-Get-Config-Test") require.NoError(t, err) - testServer, _, _, err := util.SetupTestEnv(t, tempDir, uint32(6003)) + testServer, _, _, err := util.SetupTestEnv(t, tempDir, uint32(6004)) require.NoError(t, err) defer testServer.Stop() util.StartTestServer(t, testServer) @@ -258,7 +270,7 @@ func TestSetConfigCommand_UpdateParam(t *testing.T) { tempDir, err := os.MkdirTemp(os.TempDir(), "Cli-Set-Config-Test-Update-Param") require.NoError(t, err) - testServer, _, _, err := util.SetupTestEnv(t, tempDir, uint32(6003)) + testServer, _, _, err := util.SetupTestEnv(t, tempDir, uint32(6004)) require.NoError(t, err) defer testServer.Stop() util.StartTestServer(t, testServer) @@ -326,6 +338,335 @@ func TestSetConfigCommand_UpdateParam(t *testing.T) { require.Equal(t, newClusterConfig.ConsensusConfig.RaftConfig.SnapshotIntervalSize, lastSnapshotIntervalSize) } +func TestInvalidFlagsGetLastConfigBlockCommand(t *testing.T) { + tests := []struct { + name string + args []string + expectedErrMsg string + }{ + { + name: "No Flags", + args: []string{"config", "getLastConfigBlock"}, + expectedErrMsg: "required flag(s) \"db-connection-config-path\", \"last-config-block-path\" not set", + }, + { + name: "Missing Cli DB Connection Config Flag", + args: []string{"config", "getLastConfigBlock", "-c", "/path/to/cluster-config.yaml"}, + expectedErrMsg: "required flag(s) \"db-connection-config-path\" not set", + }, + { + name: "Missing Last Config Block Flag", + args: []string{"config", "getLastConfigBlock", "-d", "/path/to/cli-db-connection-config.yaml"}, + expectedErrMsg: "required flag(s) \"last-config-block-path\" not set", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + rootCmd := InitializeOrionCli() + rootCmd.SetArgs(tt.args) + err := rootCmd.Execute() + require.Error(t, err) + require.Contains(t, err.Error(), tt.expectedErrMsg) + }) + } +} + +func TestGetLastConfigBlockCommand(t *testing.T) { + // 1. Create crypto material and start server + tempDir, err := os.MkdirTemp(os.TempDir(), "Cli-Get-Last-Config-Block-Test") + require.NoError(t, err) + + testServer, _, _, err := util.SetupTestEnv(t, tempDir, uint32(6004)) + require.NoError(t, err) + defer testServer.Stop() + util.StartTestServer(t, testServer) + + // 2. Get last config block from the server by the CLI GetLastConfigBlock command + rootCmd := InitializeOrionCli() + + pwd, err := os.Getwd() + require.NoError(t, err) + testDbConnectionConfigFilePath := path.Join(tempDir, "config.yml") + pathForLastConfigBlockOutput, err := os.MkdirTemp(os.TempDir(), "TestOutput") + defer os.RemoveAll(pathForLastConfigBlockOutput) + require.NoError(t, err) + relativePathForLastConfigBlockOutput := path.Join("..", "..", pathForLastConfigBlockOutput) + + rootCmd.SetArgs([]string{"config", "getLastConfigBlock", "-d", testDbConnectionConfigFilePath, "-c", filepath.Join(pwd, relativePathForLastConfigBlockOutput)}) + err = rootCmd.Execute() + require.NoError(t, err) + + // 3. Check the server response + blk, err := os.ReadFile(path.Join(relativePathForLastConfigBlockOutput, "last_config_block.yml")) + if err != nil { + errors.Wrapf(err, "failed to read last config block file") + } + require.NotNil(t, blk) +} + +func TestInvalidFlagsGetClusterStatusCommand(t *testing.T) { + rootCmd := InitializeOrionCli() + rootCmd.SetArgs([]string{"config", "getClusterStatus"}) + err := rootCmd.Execute() + require.Error(t, err) + require.EqualError(t, err, "required flag(s) \"db-connection-config-path\" not set") +} + +func TestGetClusterStatusCommand(t *testing.T) { + // 1. Create crypto material and start server + tempDir, err := os.MkdirTemp(os.TempDir(), "Cli-Get-Cluster-Status-Test") + require.NoError(t, err) + + testServer, _, _, err := util.SetupTestEnv(t, tempDir, uint32(6004)) + require.NoError(t, err) + defer testServer.Stop() + util.StartTestServer(t, testServer) + + // 2. Get cluster status by the CLI GetClusterStatus command + rootCmd := InitializeOrionCli() + output := new(bytes.Buffer) + rootCmd.SetOut(output) + + testDbConnectionConfigFilePath := path.Join(tempDir, "config.yml") + rootCmd.SetArgs([]string{"config", "getClusterStatus", "-d", testDbConnectionConfigFilePath}) + err = rootCmd.Execute() + require.NoError(t, err) + require.NotNil(t, output) + + idx := strings.Index(output.String(), "header:") + extractedOutput := output.String()[idx : len(output.Bytes())-1] + status := &types.GetClusterStatusResponse{} + err = proto.UnmarshalText(extractedOutput, status) + require.NoError(t, err) + require.Equal(t, 1, len(status.GetNodes())) + require.Equal(t, 1, len(status.GetActive())) +} + +// Start a 3-node cluster. +// Add the 4th node using the CLI +func TestSetConfigCommand_ClusterAddNode(t *testing.T) { + // create crypto material and start 3-node cluster + dir, err := os.MkdirTemp("", "cluster-test") + require.NoError(t, err) + fmt.Printf("the path for cluster material is: %s", dir) + nPort := uint32(6581) + pPort := uint32(6681) + setupConfig := &setup.Config{ + NumberOfServers: 3, + TestDirAbsolutePath: dir, + BDBBinaryPath: "../../bin/bdb", + CmdTimeout: 10 * time.Second, + BaseNodePort: nPort, + BasePeerPort: pPort, + } + c, err := setup.NewCluster(setupConfig) + require.NoError(t, err) + defer c.ShutdownAndCleanup() + + require.NoError(t, c.Start()) + + require.Eventually(t, func() bool { return c.AgreedLeader(t, 0, 1, 2) >= 0 }, 30*time.Second, time.Second) + + // create connection configuration file + connConfig := &sdkconfig.ConnectionConfig{ + RootCAs: []string{path.Join(setupConfig.TestDirAbsolutePath, "ca", testutils.RootCAFileName+".pem")}, + ReplicaSet: []*sdkconfig.Replica{ + { + ID: "node-1", + Endpoint: c.Servers[0].URL(), + }, + { + ID: "node-2", + Endpoint: c.Servers[1].URL(), + }, + { + ID: "node-3", + Endpoint: c.Servers[2].URL(), + }, + }, + } + + sessionConfig := &sdkconfig.SessionConfig{ + UserConfig: &sdkconfig.UserConfig{ + UserID: "admin", + CertPath: path.Join(path.Join(setupConfig.TestDirAbsolutePath, "users"), "admin.pem"), + PrivateKeyPath: path.Join(path.Join(setupConfig.TestDirAbsolutePath, "users"), "admin.key"), + }, + TxTimeout: 10 * time.Second, + QueryTimeout: 20 * time.Second, + } + + cliConnectionConfig := &util.Config{ + ConnectionConfig: *connConfig, + SessionConfig: *sessionConfig, + } + + marshaledCliConnConfig, err := yaml.Marshal(cliConnectionConfig) + require.NoError(t, err) + + err = os.WriteFile(path.Join(dir, "connection_config.yml"), marshaledCliConnConfig, 0644) + require.NoError(t, err) + + // get current cluster configuration + rootCmd := InitializeOrionCli() + + pwd, err := os.Getwd() + require.NoError(t, err) + testConnConfigFilePath := path.Join(dir, "connection_config.yml") + getConfigDirPath, err := os.MkdirTemp(os.TempDir(), "GetConfig_ClusterTest#1") + defer os.RemoveAll(getConfigDirPath) + require.NoError(t, err) + getConfigDirRelativePath := path.Join("..", "..", getConfigDirPath) + + rootCmd.SetArgs([]string{"config", "get", "-d", testConnConfigFilePath, "-c", path.Join(pwd, getConfigDirRelativePath)}) + err = rootCmd.Execute() + require.NoError(t, err) + + sharedConfigYaml, err := readSharedConfigYaml(path.Join(getConfigDirRelativePath, "shared_cluster_config.yml")) + require.NoError(t, err) + require.Equal(t, 3, len(sharedConfigYaml.Nodes)) + + // create the 4th node + newServer, newPeer, newNode, err := createNewServer(c, setupConfig, 3) + require.NoError(t, err) + require.NotNil(t, newServer) + require.NotNil(t, newPeer) + require.NotNil(t, newNode) + require.NoError(t, newServer.CreateConfigFile(&config.LocalConfiguration{})) + + newNodeConf := &NodeConf{ + NodeID: newNode.GetId(), + Host: newNode.GetAddress(), + Port: newNode.GetPort(), + CertificatePath: path.Join(dir, "node-4", "crypto", "server.pem"), + } + + newPeerConf := &PeerConf{ + NodeId: newPeer.GetNodeId(), + RaftId: newPeer.GetRaftId(), + PeerHost: newPeer.GetPeerHost(), + PeerPort: newPeer.GetPeerPort(), + } + + // add the 4th node to the shared configuration, create a new shared configuration file + newSharedConfiguration := &SharedConfiguration{ + Nodes: append(sharedConfigYaml.Nodes, newNodeConf), + Consensus: &ConsensusConf{ + Algorithm: sharedConfigYaml.Consensus.Algorithm, + Members: append(sharedConfigYaml.Consensus.Members, newPeerConf), + Observers: sharedConfigYaml.Consensus.Observers, + RaftConfig: sharedConfigYaml.Consensus.RaftConfig, + }, + CAConfig: sharedConfigYaml.CAConfig, + Admin: sharedConfigYaml.Admin, + Ledger: sharedConfigYaml.Ledger, + } + marshaledNewSharedConfiguration, err := yaml.Marshal(newSharedConfiguration) + require.NoError(t, err) + err = os.WriteFile(filepath.Join(pwd, getConfigDirRelativePath, "new_cluster_config.yml"), marshaledNewSharedConfiguration, 0644) + require.NoError(t, err) + + // set the new cluster config + rootCmd.SetArgs([]string{"config", "set", "-d", testConnConfigFilePath, "-c", filepath.Join(pwd, getConfigDirRelativePath)}) + err = rootCmd.Execute() + require.NoError(t, err) + + // Get cluster configuration again + getConfigDirPath, err = os.MkdirTemp(os.TempDir(), "GetConfig_ClusterTest#2") + defer os.RemoveAll(getConfigDirPath) + require.NoError(t, err) + getConfigDirRelativePath = path.Join("..", "..", getConfigDirPath) + + rootCmd.SetArgs([]string{"config", "get", "-d", testConnConfigFilePath, "-c", path.Join(pwd, getConfigDirRelativePath)}) + err = rootCmd.Execute() + require.NoError(t, err) + + // check nodes and members have 4 elements + newSharedConfigYaml, err := readSharedConfigYaml(path.Join(getConfigDirRelativePath, "shared_cluster_config.yml")) + require.NoError(t, err) + require.Equal(t, 4, len(newSharedConfigYaml.Nodes)) + require.Equal(t, 4, len(newSharedConfigYaml.Consensus.Members)) + + // get last config block via the cli getLastConfigBlock command + rootCmd.SetArgs([]string{"config", "getLastConfigBlock", "-d", testConnConfigFilePath, "-c", filepath.Join(pwd, getConfigDirRelativePath)}) + err = rootCmd.Execute() + require.NoError(t, err) + + block, err := os.ReadFile(path.Join(getConfigDirRelativePath, "last_config_block.yml")) + require.NoError(t, err) + require.NotNil(t, block) + + // write the last config block to the boostrap file path of the 4th node and check the config.yml has the right properties + err = os.WriteFile(newServer.BootstrapFilePath(), block, 0644) + require.NoError(t, err) + + config, err := config.Read(newServer.ConfigFilePath()) + require.NoError(t, err) + require.NotNil(t, config.LocalConfig) + require.Nil(t, config.SharedConfig) + require.NotNil(t, config.JoinBlock) + + c.AddNewServerToCluster(newServer) + + // start the 4th node + c.StartServer(newServer) + + leaderIndex := -1 + require.Eventually(t, func() bool { + leaderIndex = c.AgreedLeader(t, 0, 1, 2, 3) + return leaderIndex >= 0 + }, 30*time.Second, 100*time.Millisecond) + + // get cluster status + output := new(bytes.Buffer) + rootCmd.SetOut(output) + + rootCmd.SetArgs([]string{"config", "getClusterStatus", "-d", testConnConfigFilePath}) + err = rootCmd.Execute() + require.NoError(t, err) + require.NotNil(t, output) + + idx := strings.Index(output.String(), "header:") + extractedOutput := output.String()[idx : len(output.Bytes())-1] + status := &types.GetClusterStatusResponse{} + err = proto.UnmarshalText(extractedOutput, status) + require.NoError(t, err) + require.Equal(t, 4, len(status.GetNodes())) + require.Equal(t, 4, len(status.GetActive())) +} + +func createNewServer(c *setup.Cluster, conf *setup.Config, serverNum int) (*setup.Server, *types.PeerConfig, *types.NodeConfig, error) { + newServer, err := setup.NewServer(uint64(serverNum), conf.TestDirAbsolutePath, conf.BaseNodePort, conf.BasePeerPort, conf.CheckRedirectFunc, c.GetLogger(), "join", math.MaxUint64) + if err != nil { + return nil, nil, nil, err + } + clusterCaPrivKey, clusterRootCAPemCert := c.GetKeyAndCA() + decca, _ := pem.Decode(clusterRootCAPemCert) + newServer.CreateCryptoMaterials(clusterRootCAPemCert, clusterCaPrivKey) + if err != nil { + return nil, nil, nil, err + } + server0 := c.Servers[0] + newServer.SetAdmin(server0.AdminID(), server0.AdminCertPath(), server0.AdminKeyPath(), server0.AdminSigner()) + + newPeer := &types.PeerConfig{ + NodeId: "node-" + strconv.Itoa(serverNum+1), + RaftId: uint64(serverNum + 1), + PeerHost: "127.0.0.1", + PeerPort: conf.BasePeerPort + uint32(serverNum), + } + + newNode := &types.NodeConfig{ + Id: "node-" + strconv.Itoa(serverNum+1), + Address: "127.0.0.1", + Port: conf.BaseNodePort + uint32(serverNum), + Certificate: decca.Bytes, + } + + return newServer, newPeer, newNode, nil +} + func readConnConfig(localConfigFile string) (*cliConnectionConfig, error) { if localConfigFile == "" { return nil, errors.New("path to the local configuration file is empty") diff --git a/pkg/bcdb/config_tx_context.go b/pkg/bcdb/config_tx_context.go index 7454c46..9f53ac1 100644 --- a/pkg/bcdb/config_tx_context.go +++ b/pkg/bcdb/config_tx_context.go @@ -83,6 +83,8 @@ type ConfigTxContext interface { // GetLastConfigBlock returns the last config block. GetLastConfigBlock() ([]byte, error) + + GetClusterStatus() (*types.GetClusterStatusResponse, error) } type configTxContext struct { @@ -370,6 +372,26 @@ func (c *configTxContext) GetLastConfigBlock() ([]byte, error) { return confResp.GetBlock(), nil } +func (c *configTxContext) GetClusterStatus() (*types.GetClusterStatusResponse, error) { + clusterStatusResponseEnv := &types.GetClusterStatusResponseEnvelope{} + path := constants.GetClusterStatus + err := c.handleRequest( + path, + &types.GetConfigBlockQuery{ + UserId: c.userID, + }, + clusterStatusResponseEnv, + ) + if err != nil { + c.logger.Errorf("failed to execute cluster config query path %s, due to %s", path, err) + return nil, err + } + + resp := clusterStatusResponseEnv.GetResponse() + + return resp, nil +} + func (c *configTxContext) queryClusterConfig() error { if c.oldConfig != nil { return nil diff --git a/pkg/bcdb/config_tx_context_test.go b/pkg/bcdb/config_tx_context_test.go index c39e3c6..ea14571 100644 --- a/pkg/bcdb/config_tx_context_test.go +++ b/pkg/bcdb/config_tx_context_test.go @@ -856,3 +856,32 @@ func TestGetLastConfigBlock(t *testing.T) { require.Equal(t, uint64(3), block.GetHeader().GetBaseHeader().GetNumber()) }) } + +func TestGetClusterStatus(t *testing.T) { + cryptoDir := testutils.GenerateTestCrypto(t, []string{"admin", "server"}) + testServer, _, _, err := SetupTestServer(t, cryptoDir) + defer func() { + if testServer != nil { + _ = testServer.Stop() + } + }() + require.NoError(t, err) + StartTestServer(t, testServer) + + serverPort, err := testServer.Port() + require.NoError(t, err) + + bcdb := createDBInstance(t, cryptoDir, serverPort) + session := openUserSession(t, bcdb, "admin", cryptoDir) + + t.Logf("Get cluster status") + tx, err := session.ConfigTx() + require.NoError(t, err) + + status, err := tx.GetClusterStatus() + require.NoError(t, err) + require.NotNil(t, status) + + require.Equal(t, 1, len(status.GetNodes())) + require.Equal(t, 1, len(status.GetActive())) +} diff --git a/pkg/bcdb/tx_context.go b/pkg/bcdb/tx_context.go index 3ff69dd..aa44570 100644 --- a/pkg/bcdb/tx_context.go +++ b/pkg/bcdb/tx_context.go @@ -392,7 +392,8 @@ func ResponseSelector(envelop ResponseEnvelop) (ResponseWithHeader, error) { return envelop.(*types.GetTxResponseEnvelope).GetResponse(), nil case *types.GetConfigBlockResponseEnvelope: return envelop.(*types.GetConfigBlockResponseEnvelope).GetResponse(), nil - + case *types.GetClusterStatusResponseEnvelope: + return envelop.(*types.GetClusterStatusResponseEnvelope).GetResponse(), nil default: return nil, errors.Errorf("unknown response type %T", t) }