From 01845b7de8fa15aa0c829aea9254f3176df32f79 Mon Sep 17 00:00:00 2001 From: MayRosenbaum <113279625+MayRosenbaum@users.noreply.github.com> Date: Tue, 17 Oct 2023 12:20:36 +0300 Subject: [PATCH] CLI get config + version commands + tests (#176) Signed-off-by: May Rosenbaum --- Makefile | 1 + cli/README.md | 49 ++++++ cli/commands/admin.go | 43 +++++ cli/commands/admin_test.go | 27 ++++ cli/commands/cas.go | 35 ++++ cli/commands/cas_test.go | 27 ++++ cli/commands/cli_utils.go | 75 +++++++++ cli/commands/config.go | 294 ++++++++++++++++++++++++++++++++++ cli/commands/config_test.go | 205 ++++++++++++++++++++++++ cli/commands/node.go | 43 +++++ cli/commands/node_test.go | 27 ++++ cli/commands/root.go | 23 +++ cli/commands/shared_config.go | 115 +++++++++++++ cli/commands/version.go | 34 ++++ cli/commands/version_test.go | 30 ++++ cli/main.go | 17 ++ 16 files changed, 1045 insertions(+) create mode 100644 cli/README.md create mode 100644 cli/commands/admin.go create mode 100644 cli/commands/admin_test.go create mode 100644 cli/commands/cas.go create mode 100644 cli/commands/cas_test.go create mode 100644 cli/commands/cli_utils.go create mode 100644 cli/commands/config.go create mode 100644 cli/commands/config_test.go create mode 100644 cli/commands/node.go create mode 100644 cli/commands/node_test.go create mode 100644 cli/commands/root.go create mode 100644 cli/commands/shared_config.go create mode 100644 cli/commands/version.go create mode 100644 cli/commands/version_test.go create mode 100644 cli/main.go diff --git a/Makefile b/Makefile index 9fe663f..da16b74 100644 --- a/Makefile +++ b/Makefile @@ -48,6 +48,7 @@ goimports: .PHONY: binary binary: $(GO) build -o $(BIN)/bdb github.com/hyperledger-labs/orion-server/cmd/bdb + $(GO) build -o $(BIN)/bcdbadmin github.com/hyperledger-labs/orion-sdk-go/cli .PHONY: test test-script: diff --git a/cli/README.md b/cli/README.md new file mode 100644 index 0000000..99957de --- /dev/null +++ b/cli/README.md @@ -0,0 +1,49 @@ +# Config Orion via CLI + +This command-line tool provides a simple way to config an orion database server. + +## Building the tool +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. +We give a short explanation of their usage and describe the flags for each command. +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. +2. Run `./bin/bcdbadmin version`. This command has no flags. + + + +### 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]`. + + 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 server configuration will be saved | + +Both flags are necessary flags. If any flag is missing, the cli will raise an error. + +### +##### Example: + +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. \ No newline at end of file diff --git a/cli/commands/admin.go b/cli/commands/admin.go new file mode 100644 index 0000000..dbc6953 --- /dev/null +++ b/cli/commands/admin.go @@ -0,0 +1,43 @@ +package commands + +import ( + "github.com/pkg/errors" + "github.com/spf13/cobra" +) + +func adminCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "admin", + Short: "manage administrators", + Args: nil, + RunE: func(cmd *cobra.Command, args []string) error { + return errors.New("not implemented yet") + }, + } + + cmd.AddCommand(&cobra.Command{ + Use: "add", + Short: "Add an admin", + RunE: func(cmd *cobra.Command, args []string) error { + return errors.New("not implemented yet") + }, + }) + + cmd.AddCommand(&cobra.Command{ + Use: "remove", + Short: "Remove an admin", + RunE: func(cmd *cobra.Command, args []string) error { + return errors.New("not implemented yet") + }, + }) + + cmd.AddCommand(&cobra.Command{ + Use: "update", + Short: "Update an admin", + RunE: func(cmd *cobra.Command, args []string) error { + return errors.New("not implemented yet") + }, + }) + + return cmd +} diff --git a/cli/commands/admin_test.go b/cli/commands/admin_test.go new file mode 100644 index 0000000..8f55c98 --- /dev/null +++ b/cli/commands/admin_test.go @@ -0,0 +1,27 @@ +package commands + +import ( + "os" + "testing" + + "github.com/hyperledger-labs/orion-sdk-go/examples/util" + "github.com/stretchr/testify/require" +) + +func TestAdminCommand(t *testing.T) { + // 1. Create crypto material and start server + tempDir, err := os.MkdirTemp(os.TempDir(), "Cli-Admin-Test") + require.NoError(t, err) + + testServer, _, _, err := util.SetupTestEnv(t, tempDir, uint32(6003)) + require.NoError(t, err) + defer testServer.Stop() + util.StartTestServer(t, testServer) + + // 2. Check admin command response + rootCmd := InitializeOrionCli() + rootCmd.SetArgs([]string{"admin"}) + err = rootCmd.Execute() + require.Error(t, err) + require.Equal(t, err.Error(), "not implemented yet") +} diff --git a/cli/commands/cas.go b/cli/commands/cas.go new file mode 100644 index 0000000..f6f0162 --- /dev/null +++ b/cli/commands/cas.go @@ -0,0 +1,35 @@ +package commands + +import ( + "github.com/pkg/errors" + "github.com/spf13/cobra" +) + +func casCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "CAs", + Short: "manage CA's", + Args: nil, + RunE: func(cmd *cobra.Command, args []string) error { + return errors.New("not implemented yet") + }, + } + + cmd.AddCommand(&cobra.Command{ + Use: "add", + Short: "Add CA", + RunE: func(cmd *cobra.Command, args []string) error { + return errors.New("not implemented yet") + }, + }) + + cmd.AddCommand(&cobra.Command{ + Use: "remove", + Short: "Remove CA", + RunE: func(cmd *cobra.Command, args []string) error { + return errors.New("not implemented yet") + }, + }) + + return cmd +} diff --git a/cli/commands/cas_test.go b/cli/commands/cas_test.go new file mode 100644 index 0000000..b9b8b84 --- /dev/null +++ b/cli/commands/cas_test.go @@ -0,0 +1,27 @@ +package commands + +import ( + "os" + "testing" + + "github.com/hyperledger-labs/orion-sdk-go/examples/util" + "github.com/stretchr/testify/require" +) + +func TestCasCommand(t *testing.T) { + // 1. Create crypto material and start server + tempDir, err := os.MkdirTemp(os.TempDir(), "Cli-Cas-Test") + require.NoError(t, err) + + testServer, _, _, err := util.SetupTestEnv(t, tempDir, uint32(6003)) + require.NoError(t, err) + defer testServer.Stop() + util.StartTestServer(t, testServer) + + // 2. Check cas command response + rootCmd := InitializeOrionCli() + rootCmd.SetArgs([]string{"CAs"}) + err = rootCmd.Execute() + require.Error(t, err) + require.Equal(t, err.Error(), "not implemented yet") +} diff --git a/cli/commands/cli_utils.go b/cli/commands/cli_utils.go new file mode 100644 index 0000000..7bd98e6 --- /dev/null +++ b/cli/commands/cli_utils.go @@ -0,0 +1,75 @@ +package commands + +import ( + "github.com/hyperledger-labs/orion-sdk-go/pkg/bcdb" + "github.com/hyperledger-labs/orion-sdk-go/pkg/config" + "github.com/hyperledger-labs/orion-server/pkg/logger" + "github.com/pkg/errors" + "github.com/spf13/viper" +) + +type cliConnectionConfig struct { + ConnectionConfig config.ConnectionConfig `yaml:"connection"` + SessionConfig config.SessionConfig `yaml:"session"` +} + +type cliConfigParams struct { + cliConfigPath string + cliConfig cliConnectionConfig + db bcdb.BCDB + session bcdb.DBSession +} + +// CreateDbAndOpenSession read connection and session configurations to create a db instance and open a session with the server. +func (c *cliConfigParams) CreateDbAndOpenSession() error { + var err error + if err = c.cliConfig.ReadAndConstructCliConnConfig(c.cliConfigPath); err != nil { + return errors.Wrapf(err, "failed to read CLI configuration file") + } + + c.db, err = bcdb.Create(&c.cliConfig.ConnectionConfig) + if err != nil { + return errors.Wrapf(err, "failed to instanciate a databse connection") + } + + c.session, err = c.db.Session(&c.cliConfig.SessionConfig) + if err != nil { + return errors.Wrapf(err, "failed to instanciate a databse session") + } + + return nil +} + +// ReadAndConstructCliConnConfig read unmarshal the yaml config file into a cliConnectionConfig object +func (c *cliConnectionConfig) ReadAndConstructCliConnConfig(filePath string) error { + if filePath == "" { + return errors.New("path to the shared configuration file is empty") + } + + v := viper.New() + v.SetConfigFile(filePath) + + if err := v.ReadInConfig(); err != nil { + return errors.Wrapf(err, "error reading shared config file: %s", filePath) + } + + if err := v.UnmarshalExact(c); err != nil { + return errors.Wrapf(err, "unable to unmarshal shared config file: '%s' into struct", filePath) + } + + clientLogger, err := logger.New( + &logger.Config{ + Level: "debug", + OutputPath: []string{"stdout"}, + ErrOutputPath: []string{"stderr"}, + Encoding: "console", + Name: "bcdb-client", + }, + ) + if err != nil { + return err + } + c.ConnectionConfig.Logger = clientLogger + + return nil +} diff --git a/cli/commands/config.go b/cli/commands/config.go new file mode 100644 index 0000000..201b710 --- /dev/null +++ b/cli/commands/config.go @@ -0,0 +1,294 @@ +package commands + +import ( + "encoding/pem" + "os" + "path" + "strconv" + + "github.com/hyperledger-labs/orion-sdk-go/pkg/bcdb" + orionconfig "github.com/hyperledger-labs/orion-server/config" + "github.com/hyperledger-labs/orion-server/pkg/types" + "github.com/pkg/errors" + "github.com/spf13/cobra" + "gopkg.in/yaml.v2" +) + +func configCmd() *cobra.Command { + configCmd := &cobra.Command{ + Use: "config", + Short: "Manage cluster configuration", + Long: "The config command allows you to manage the cluster configuration. " + + "You can use 'get' to retrieve the current cluster configuration or 'set' to update the cluster configuration", + } + + configCmd.PersistentFlags().StringP("db-connection-config-path", "d", "", "set the absolute or relative path of CLI connection configuration file") + if err := configCmd.MarkPersistentFlagRequired("db-connection-config-path"); err != nil { + panic(err.Error()) + } + + configCmd.AddCommand(getConfigCmd(), setConfigCmd()) + + return configCmd +} + +func getConfigCmd() *cobra.Command { + getConfigCmd := &cobra.Command{ + Use: "get", + Short: "Get cluster configuration", + Example: "cli config get -d -c ", + RunE: getConfig, + } + + getConfigCmd.PersistentFlags().StringP("cluster-config-path", "c", "", "set the absolute or relative path to which the server configuration will be saved") + if err := getConfigCmd.MarkPersistentFlagRequired("cluster-config-path"); err != nil { + panic(err.Error()) + } + + return getConfigCmd +} + +func setConfigCmd() *cobra.Command { + setConfigCmd := &cobra.Command{ + Use: "set", + Short: "Set cluster configuration", + RunE: func(cmd *cobra.Command, args []string) error { + return errors.New("not implemented yet") + }, + } + return setConfigCmd +} + +func getConfig(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") + } + + getClusterConfigPath, err := cmd.Flags().GetString("cluster-config-path") + if err != nil { + return errors.Wrapf(err, "failed to fetch the path to which the server configuration 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) + + clusterConfig, version, err := tx.GetClusterConfig() + if err != nil { + return errors.Wrapf(err, "failed to fetch cluster config") + } + + err = parseAndSaveCerts(clusterConfig, getClusterConfigPath) + if err != nil { + return errors.Wrapf(err, "failed to fetch certificates from cluster config") + } + + err = WriteClusterConfigToYaml(clusterConfig, version, getClusterConfigPath) + if err != nil { + return errors.Wrapf(err, "failed to create cluster config yaml file") + } + + return nil +} + +func abort(tx bcdb.TxContext) { + _ = tx.Abort() +} + +// WriteClusterConfigToYaml builds the shared clusterConfig object and writes it to a YAML file. +func WriteClusterConfigToYaml(clusterConfig *types.ClusterConfig, version *types.Version, configYamlFilePath string) error { + sharedConfiguration := buildSharedClusterConfig(clusterConfig, configYamlFilePath) + c, err := yaml.Marshal(sharedConfiguration) + if err != nil { + return err + } + + v, err := yaml.Marshal(version) + if err != nil { + return err + } + + err = os.WriteFile(path.Join(configYamlFilePath, "shared_cluster_config.yml"), c, 0644) + err = os.WriteFile(path.Join(configYamlFilePath, "version.yml"), v, 0644) + if err != nil { + return err + } + return nil +} + +// parse certificates and save in a folder structure +func parseAndSaveCerts(clusterConfig *types.ClusterConfig, getClusterConfigPath string) error { + for _, node := range clusterConfig.Nodes { + nodeCert := node.Certificate + nodePemCert := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: nodeCert}) + nodeCertDirPath := path.Join(getClusterConfigPath, "nodes") + err := os.MkdirAll(nodeCertDirPath, 0755) + if err != nil { + return err + } + + fileName := node.GetId() + ".pem" + nodeCertFilePath := path.Join(nodeCertDirPath, fileName) + + err = os.WriteFile(nodeCertFilePath, nodePemCert, 0644) + if err != nil { + return err + } + } + + for _, adminNode := range clusterConfig.Admins { + adminNodeCert := adminNode.Certificate + adminNodePemCert := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: adminNodeCert}) + adminNodeCertDirPath := path.Join(getClusterConfigPath, "admins") + err := os.MkdirAll(adminNodeCertDirPath, 0755) + if err != nil { + return err + } + + fileName := adminNode.GetId() + ".pem" + nodeCertFilePath := path.Join(adminNodeCertDirPath, fileName) + + err = os.WriteFile(nodeCertFilePath, adminNodePemCert, 0644) + if err != nil { + return err + } + } + + for i, rootCACert := range clusterConfig.CertAuthConfig.Roots { + rootCAPemCert := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: rootCACert}) + rootCACertDirPath := path.Join(getClusterConfigPath, "rootCAs") + err := os.MkdirAll(rootCACertDirPath, 0755) + if err != nil { + return err + } + + fileName := "rootCA" + strconv.Itoa(i) + ".pem" + rootCACertFilePath := path.Join(rootCACertDirPath, fileName) + + err = os.WriteFile(rootCACertFilePath, rootCAPemCert, 0644) + if err != nil { + return err + } + } + + for i, intermediateCACert := range clusterConfig.CertAuthConfig.Intermediates { + intermediateCAPemCert := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: intermediateCACert}) + intermediateCACertDirPath := path.Join(getClusterConfigPath, "intermediateCAs") + err := os.MkdirAll(intermediateCACertDirPath, 0755) + if err != nil { + return err + } + + fileName := "intermediateCA" + strconv.Itoa(i) + ".pem" + intermediateCACertFilePath := path.Join(intermediateCACertDirPath, fileName) + + err = os.WriteFile(intermediateCACertFilePath, intermediateCAPemCert, 0644) + if err != nil { + return err + } + } + + return nil +} + +// buildSharedClusterConfig builds the shared configuration from a clusterConfig +func buildSharedClusterConfig(clusterConfig *types.ClusterConfig, configYamlFilePath string) *SharedConfiguration { + var nodesSharedConfiguration []*NodeConf + for _, node := range clusterConfig.Nodes { + nodeSharedConfiguration := &NodeConf{ + NodeID: node.GetId(), + Host: node.GetAddress(), + Port: node.GetPort(), + CertificatePath: path.Join(configYamlFilePath, "nodes", node.GetId()+".pem"), + } + nodesSharedConfiguration = append(nodesSharedConfiguration, nodeSharedConfiguration) + } + + var membersSharedConfiguration []*PeerConf + for _, member := range clusterConfig.ConsensusConfig.Members { + memberSharedConfiguration := &PeerConf{ + NodeId: member.GetNodeId(), + RaftId: member.GetRaftId(), + PeerHost: member.GetPeerHost(), + PeerPort: member.GetPeerPort(), + } + membersSharedConfiguration = append(membersSharedConfiguration, memberSharedConfiguration) + } + + var observersSharedConfiguration []*PeerConf + for _, observer := range clusterConfig.ConsensusConfig.Observers { + observerSharedConfiguration := &PeerConf{ + NodeId: observer.GetNodeId(), + RaftId: observer.GetRaftId(), + PeerHost: observer.GetPeerHost(), + PeerPort: observer.GetPeerPort(), + } + observersSharedConfiguration = append(observersSharedConfiguration, observerSharedConfiguration) + } + + var rootCACertsPathSharedConfiguration []string + for i, root := range clusterConfig.CertAuthConfig.Roots { + rootCACertPathSharedConfiguration := "[]" + if root != nil { + rootCACertPathSharedConfiguration = path.Join(configYamlFilePath, "rootCAs", "rootCA"+strconv.Itoa(i)+".pem") + } + rootCACertsPathSharedConfiguration = append(rootCACertsPathSharedConfiguration, rootCACertPathSharedConfiguration) + } + + var intermediateCACertsPathSharedConfiguration []string + for i, intermediateCA := range clusterConfig.CertAuthConfig.Intermediates { + intermediateCACertPathSharedConfiguration := "[]" + if intermediateCA != nil { + intermediateCACertPathSharedConfiguration = path.Join(configYamlFilePath, "intermediateCAs", "intermediateCA"+strconv.Itoa(i)+".pem") + } + intermediateCACertsPathSharedConfiguration = append(intermediateCACertsPathSharedConfiguration, intermediateCACertPathSharedConfiguration) + } + + var adminsSharedConfiguration []*AdminConf + for i, admin := range clusterConfig.Admins { + adminSharedConfiguration := &AdminConf{ + ID: admin.GetId(), + CertificatePath: path.Join(configYamlFilePath, "admins", clusterConfig.Admins[i].GetId()+".pem"), + } + adminsSharedConfiguration = append(adminsSharedConfiguration, adminSharedConfiguration) + } + + sharedConfiguration := &SharedConfiguration{ + Nodes: nodesSharedConfiguration, + Consensus: &ConsensusConf{ + Algorithm: clusterConfig.ConsensusConfig.Algorithm, + Members: membersSharedConfiguration, + Observers: observersSharedConfiguration, + RaftConfig: &RaftConf{ + TickInterval: clusterConfig.ConsensusConfig.RaftConfig.TickInterval, + ElectionTicks: clusterConfig.ConsensusConfig.RaftConfig.ElectionTicks, + HeartbeatTicks: clusterConfig.ConsensusConfig.RaftConfig.HeartbeatTicks, + MaxInflightBlocks: clusterConfig.ConsensusConfig.RaftConfig.MaxInflightBlocks, + SnapshotIntervalSize: clusterConfig.ConsensusConfig.RaftConfig.SnapshotIntervalSize, + }, + }, + CAConfig: orionconfig.CAConfiguration{ + RootCACertsPath: rootCACertsPathSharedConfiguration, + IntermediateCACertsPath: intermediateCACertsPathSharedConfiguration, + }, + Admin: adminsSharedConfiguration, + Ledger: LedgerConf{StateMerklePatriciaTrieDisabled: clusterConfig.LedgerConfig.StateMerklePatriciaTrieDisabled}, + } + + return sharedConfiguration +} diff --git a/cli/commands/config_test.go b/cli/commands/config_test.go new file mode 100644 index 0000000..908bc0b --- /dev/null +++ b/cli/commands/config_test.go @@ -0,0 +1,205 @@ +package commands + +import ( + "net/url" + "os" + "path" + "path/filepath" + "strconv" + "testing" + + "github.com/hyperledger-labs/orion-sdk-go/examples/util" + "github.com/hyperledger-labs/orion-server/pkg/types" + "github.com/pkg/errors" + "github.com/spf13/viper" + "github.com/stretchr/testify/require" +) + +func TestInvalidFlagsGetConfigCommand(t *testing.T) { + tests := []struct { + name string + args []string + expectedErrMsg string + }{ + { + name: "No Flags", + args: []string{"config", "get"}, + expectedErrMsg: "required flag(s) \"cluster-config-path\", \"db-connection-config-path\" not set", + }, + { + name: "Missing Cli DB Connection Config Flag", + args: []string{"config", "get", "-c", "/path/to/cluster-config.yaml"}, + expectedErrMsg: "required flag(s) \"db-connection-config-path\" not set", + }, + { + name: "Missing Cluster Config Flag", + args: []string{"config", "get", "-d", "/path/to/cli-db-connection-config.yaml"}, + expectedErrMsg: "required flag(s) \"cluster-config-path\" not set", + }, + { + name: "File path not found", + args: []string{"config", "get", "-d", "/path/to/cli-db-connection-config.yaml", "-c", "/path/to/cluster-config.yaml"}, + expectedErrMsg: "failed to read CLI configuration file", + }, + } + + 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 TestGetConfigCommand(t *testing.T) { + // 1. Create crypto material and start server + tempDir, err := os.MkdirTemp(os.TempDir(), "Cli-Get-Config-Test") + require.NoError(t, err) + + testServer, _, _, err := util.SetupTestEnv(t, tempDir, uint32(6003)) + require.NoError(t, err) + defer testServer.Stop() + util.StartTestServer(t, testServer) + + // 2. Get cluster config from the server by the CLI GetConfig command + rootCmd := InitializeOrionCli() + pwd, err := os.Getwd() + require.NoError(t, err) + testDbConnectionConfigFilePath := path.Join(tempDir, "config.yml") + createdDirName, err := os.MkdirTemp(os.TempDir(), "configTest") + defer os.RemoveAll(createdDirName) + relativePathForCreatedDirName := path.Join("..", "..", createdDirName) + require.NoError(t, err) + rootCmd.SetArgs([]string{"config", "get", "-d", testDbConnectionConfigFilePath, "-c", filepath.Join(pwd, relativePathForCreatedDirName)}) + err = rootCmd.Execute() + require.NoError(t, err) + + // 3. Check the server response + // check that certs are equal to the expected certs + err = compareFiles(path.Join(tempDir, "crypto", "admin", "admin.pem"), path.Join(relativePathForCreatedDirName, "admins", "admin.pem")) + require.NoError(t, err) + err = compareFiles(path.Join(tempDir, "crypto", "node", "node.pem"), path.Join(relativePathForCreatedDirName, "nodes", "server1.pem")) + require.NoError(t, err) + err = compareFiles(path.Join(tempDir, "crypto", "CA", "CA.pem"), path.Join(relativePathForCreatedDirName, "rootCAs", "rootCA0.pem")) + require.NoError(t, err) + + // extract server endpoint and compare to the endpoint received by Get Config command + expectedConfigRes, err := readConnConfig(testDbConnectionConfigFilePath) + if err != nil { + errors.Wrapf(err, "failed to read expected shared configuration") + } + + actualSharedConfigRes, err := readSharedConfigYaml(path.Join(relativePathForCreatedDirName, "shared_cluster_config.yml")) + if err != nil { + errors.Wrapf(err, "failed to read shared configuration") + } + + require.Equal(t, expectedConfigRes.ConnectionConfig.ReplicaSet[0].ID, actualSharedConfigRes.Nodes[0].NodeID) + url, err := url.Parse(expectedConfigRes.ConnectionConfig.ReplicaSet[0].Endpoint) + if err != nil { + errors.Wrapf(err, "failed to parse server endpoint") + } + require.Equal(t, url.Host, actualSharedConfigRes.Nodes[0].Host+":"+strconv.Itoa(int(actualSharedConfigRes.Nodes[0].Port))) + + // 4. check the version + version, err := readVersionYaml(path.Join(relativePathForCreatedDirName, "version.yml")) + if err != nil { + errors.Wrapf(err, "failed to read version file") + } + require.Equal(t, version.GetBlockNum(), uint64(1)) + require.Equal(t, version.GetTxNum(), uint64(0)) +} + +func TestSetConfigCommand(t *testing.T) { + // 1. Create crypto material and start server + tempDir, err := os.MkdirTemp(os.TempDir(), "Cli-Set-Config-Test") + require.NoError(t, err) + + testServer, _, _, err := util.SetupTestEnv(t, tempDir, uint32(6003)) + require.NoError(t, err) + defer testServer.Stop() + util.StartTestServer(t, testServer) + + // 2. Check cas command response + rootCmd := InitializeOrionCli() + testDbConnectionConfigFilePath := path.Join(tempDir, "config.yml") + rootCmd.SetArgs([]string{"config", "set", "-d", testDbConnectionConfigFilePath}) + err = rootCmd.Execute() + require.Error(t, err) + require.Equal(t, err.Error(), "not implemented yet") +} + +func readConnConfig(localConfigFile string) (*cliConnectionConfig, error) { + if localConfigFile == "" { + return nil, errors.New("path to the local configuration file is empty") + } + + v := viper.New() + v.SetConfigFile(localConfigFile) + + if err := v.ReadInConfig(); err != nil { + return nil, errors.Wrapf(err, "error reading local config file: %s", localConfigFile) + } + + localConf := &cliConnectionConfig{} + if err := v.UnmarshalExact(localConf); err != nil { + return nil, errors.Wrapf(err, "unable to unmarshal local config file: '%s' into struct", localConfigFile) + } + return localConf, nil +} + +func readSharedConfigYaml(sharedConfigFile string) (*SharedConfiguration, error) { + if sharedConfigFile == "" { + return nil, errors.New("path to the shared configuration file is empty") + } + + v := viper.New() + v.SetConfigFile(sharedConfigFile) + + if err := v.ReadInConfig(); err != nil { + return nil, errors.Wrapf(err, "error reading shared config file: %s", sharedConfigFile) + } + + sharedConf := &SharedConfiguration{} + if err := v.UnmarshalExact(sharedConf); err != nil { + return nil, errors.Wrapf(err, "unable to unmarshal shared config file: '%s' into struct", sharedConfigFile) + } + return sharedConf, nil +} + +func readVersionYaml(versionFile string) (*types.Version, error) { + if versionFile == "" { + return nil, errors.New("path to the shared configuration file is empty") + } + + v := viper.New() + v.SetConfigFile(versionFile) + + if err := v.ReadInConfig(); err != nil { + return nil, errors.Wrapf(err, "error reading version file: %s", versionFile) + } + + version := &types.Version{} + if err := v.UnmarshalExact(version); err != nil { + return nil, errors.Wrapf(err, "unable to unmarshal version file: '%s' into struct", version) + } + return version, nil +} + +func compareFiles(filepath1 string, filepath2 string) error { + data1, err := os.ReadFile(filepath1) + if err != nil { + return errors.Wrapf(err, "failed to read file: '%s'", filepath1) + } + data2, err := os.ReadFile(filepath2) + if err != nil { + return errors.Wrapf(err, "failed to read file: '%s'", filepath2) + } + if string(data1) != string(data2) { + return errors.Wrapf(err, "files are different") + } + return nil +} diff --git a/cli/commands/node.go b/cli/commands/node.go new file mode 100644 index 0000000..4418b65 --- /dev/null +++ b/cli/commands/node.go @@ -0,0 +1,43 @@ +package commands + +import ( + "github.com/pkg/errors" + "github.com/spf13/cobra" +) + +func nodeCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "node", + Short: "manage cluster", + Args: nil, + RunE: func(cmd *cobra.Command, args []string) error { + return errors.New("not implemented yet") + }, + } + + cmd.AddCommand(&cobra.Command{ + Use: "add", + Short: "Add a cluster node", + RunE: func(cmd *cobra.Command, args []string) error { + return errors.New("not implemented yet") + }, + }) + + cmd.AddCommand(&cobra.Command{ + Use: "remove", + Short: "Remove a cluster node", + RunE: func(cmd *cobra.Command, args []string) error { + return errors.New("not implemented yet") + }, + }) + + cmd.AddCommand(&cobra.Command{ + Use: "update", + Short: "Update a cluster node", + RunE: func(cmd *cobra.Command, args []string) error { + return errors.New("not implemented yet") + }, + }) + + return cmd +} diff --git a/cli/commands/node_test.go b/cli/commands/node_test.go new file mode 100644 index 0000000..0516f5b --- /dev/null +++ b/cli/commands/node_test.go @@ -0,0 +1,27 @@ +package commands + +import ( + "os" + "testing" + + "github.com/hyperledger-labs/orion-sdk-go/examples/util" + "github.com/stretchr/testify/require" +) + +func TestNodeCommand(t *testing.T) { + // 1. Create crypto material and start server + tempDir, err := os.MkdirTemp(os.TempDir(), "Cli-Node-Test") + require.NoError(t, err) + + testServer, _, _, err := util.SetupTestEnv(t, tempDir, uint32(6003)) + require.NoError(t, err) + defer testServer.Stop() + util.StartTestServer(t, testServer) + + // 2. Check node command response + rootCmd := InitializeOrionCli() + rootCmd.SetArgs([]string{"node"}) + err = rootCmd.Execute() + require.Error(t, err) + require.Equal(t, err.Error(), "not implemented yet") +} diff --git a/cli/commands/root.go b/cli/commands/root.go new file mode 100644 index 0000000..6faf389 --- /dev/null +++ b/cli/commands/root.go @@ -0,0 +1,23 @@ +package commands + +import ( + "github.com/spf13/cobra" +) + +func rootCmd() *cobra.Command { + rootCmd := &cobra.Command{ + Use: "bcdbadmin", + Short: "Config Orion via CLI.", + } + return rootCmd +} + +func InitializeOrionCli() *cobra.Command { + cmd := rootCmd() + cmd.AddCommand(versionCmd()) + cmd.AddCommand(configCmd()) + cmd.AddCommand(adminCmd()) + cmd.AddCommand(nodeCmd()) + cmd.AddCommand(casCmd()) + return cmd +} diff --git a/cli/commands/shared_config.go b/cli/commands/shared_config.go new file mode 100644 index 0000000..3594bfe --- /dev/null +++ b/cli/commands/shared_config.go @@ -0,0 +1,115 @@ +package commands + +import ( + orionconfig "github.com/hyperledger-labs/orion-server/config" + "github.com/pkg/errors" + "github.com/spf13/viper" +) + +// SharedConfiguration holds the initial configuration that will be converted into the ledger's genesis block and +// loaded into the database when the server starts with an empty ledger and database. +// +// This struct may also be used to bootstrap a new node into an existing cluster (not yet implemented). +// +// This part of the configuration is replicated and is common to all nodes. +// After the initial bootstrap, this part of the configuration can change only through configuration transactions. +type SharedConfiguration struct { + // Nodes carry the identity, endpoint, and certificate of each database node that serves to clients. + Nodes []*NodeConf + Consensus *ConsensusConf + CAConfig orionconfig.CAConfiguration + Admin []*AdminConf + Ledger LedgerConf +} + +// NodeConf carry the identity, endpoint, and certificate of a database node that serves to clients. +// The NodeID correlates the node definition here with the peer definition in the SharedConfiguration.Consensus. +// The Host and Port are those that are accessible from clients. +// The certificate is the one used to authenticate with clients and validate the server;s signature on +// blocks and transaction/query responses. +type NodeConf struct { + NodeID string + Host string + Port uint32 + CertificatePath string +} + +type ConsensusConf struct { + // The consensus algorithm, currently only "raft" is supported. + Algorithm string + // Peers that take part in consensus. + Members []*PeerConf + // Peers that are allowed to connect and fetch the ledger from members, but do not take part in consensus. + Observers []*PeerConf + // Raft protocol parameters. + RaftConfig *RaftConf +} + +type RaftConf struct { + // Time interval between two Node.Tick invocations. e.g. 100ms. + TickInterval string + // The number of Node.Tick invocations that must pass between elections. + // That is, if a follower does not receive any + // message from the leader of current term before ElectionTick has + // elapsed, it will become candidate and start an election. + // electionTicks must be greater than heartbeatTicks. + ElectionTicks uint32 + // The number of Node.Tick invocations that must + // pass between heartbeats. That is, a leader sends heartbeat + // messages to maintain its leadership every HeartbeatTick ticks. + HeartbeatTicks uint32 + // Limits the max number of in-flight blocks (i.e. Raft messages). + MaxInflightBlocks uint32 + // Take a snapshot when cumulative data since last snapshot exceeds a certain size in bytes. + SnapshotIntervalSize uint64 +} + +// PeerConf defines a server that takes part in consensus, or an observer. +type PeerConf struct { + // The node ID correlates the peer definition here with the NodeConfig.ID field. + NodeId string + // Raft ID must be >0 for members, or =0 for observers. + RaftId uint64 + // The host name or IP address that is used by other peers to connect to this peer. + PeerHost string + // The port that is used by other peers to connect to this peer. + PeerPort uint32 +} + +// AdminConf holds the credentials of the blockchain +// database cluster admin such as the ID and path to +// the x509 certificate +type AdminConf struct { + ID string + CertificatePath string +} + +// LedgerConf defines parameters on the distributed ledger capabilities and algorithms that must be defined uniformly across +// all servers. +type LedgerConf struct { + // StateMerklePatriciaTrieDisabled disables the state Merkle-Patricia-Trie construction. + // With MP-Trie construction disabled, the block's BlockHeader.StateMerkleTreeRootHash field will be nil. + // This flag takes effect on deployment (bootstrap) only, from the first (genesis) block. + // The value of this flag cannot be changed during run-time. + StateMerklePatriciaTrieDisabled bool +} + +// readSharedConfig reads the shared config from the file and returns it. +func readSharedConfig(sharedConfigFile string) (*SharedConfiguration, error) { + if sharedConfigFile == "" { + return nil, errors.New("path to the shared configuration file is empty") + } + + v := viper.New() + v.SetConfigFile(sharedConfigFile) + + if err := v.ReadInConfig(); err != nil { + return nil, errors.Wrapf(err, "error reading shared config file: %s", sharedConfigFile) + } + + sharedConf := &SharedConfiguration{} + if err := v.UnmarshalExact(sharedConf); err != nil { + return nil, errors.Wrapf(err, "unable to unmarshal shared config file: '%s' into struct", sharedConfigFile) + } + return sharedConf, nil +} diff --git a/cli/commands/version.go b/cli/commands/version.go new file mode 100644 index 0000000..d2a072e --- /dev/null +++ b/cli/commands/version.go @@ -0,0 +1,34 @@ +package commands + +import ( + "fmt" + "runtime/debug" + + "github.com/spf13/cobra" +) + +func versionCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "version", + Args: cobra.NoArgs, + Short: "Print the version of the CLI tool.", + RunE: func(cmd *cobra.Command, args []string) error { + bi, ok := debug.ReadBuildInfo() + if !ok { + return fmt.Errorf("failed to read build info") + } + + cmd.Printf("SDK version: %+v\n", bi.Main.Version) + + for _, dep := range bi.Deps { + if dep.Path == "github.com/hyperledger-labs/orion-server" { + cmd.Printf("Orion server version: %+v\n", dep.Version) + break + } + } + + return nil + }, + } + return cmd +} diff --git a/cli/commands/version_test.go b/cli/commands/version_test.go new file mode 100644 index 0000000..618cd61 --- /dev/null +++ b/cli/commands/version_test.go @@ -0,0 +1,30 @@ +package commands + +import ( + "os" + "strings" + "testing" + + "github.com/hyperledger-labs/orion-sdk-go/examples/util" + "github.com/stretchr/testify/require" +) + +func TestVersionCommand(t *testing.T) { + // 1. Create crypto material and start server + tempDir, err := os.MkdirTemp(os.TempDir(), "Cli-Version-Test") + require.NoError(t, err) + + testServer, _, _, err := util.SetupTestEnv(t, tempDir, uint32(6003)) + require.NoError(t, err) + defer testServer.Stop() + util.StartTestServer(t, testServer) + + // 2. Get the version of the CLI by the CLI Version command + rootCmd := InitializeOrionCli() + rootCmd.SetArgs([]string{"version"}) + b := &strings.Builder{} + rootCmd.SetOut(b) + err = rootCmd.Execute() + require.NoError(t, err) + require.Contains(t, b.String(), "SDK version: ") +} diff --git a/cli/main.go b/cli/main.go new file mode 100644 index 0000000..0616107 --- /dev/null +++ b/cli/main.go @@ -0,0 +1,17 @@ +// Copyright IBM Corp. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package main + +import ( + "os" + + "github.com/hyperledger-labs/orion-sdk-go/cli/commands" +) + +func main() { + cmd := commands.InitializeOrionCli() + if err := cmd.Execute(); err != nil { + os.Exit(1) + } +}