From b0ca0d672dafbe231721bbf12a9c5431cdb50f42 Mon Sep 17 00:00:00 2001 From: Orpheus Lummis Date: Mon, 24 Apr 2023 00:16:17 -0400 Subject: [PATCH 1/6] builder pattern --- cli/blocks.go | 12 +-- cli/blocks_get.go | 92 ++++++++-------- cli/cli.go | 83 ++++++++++++-- cli/client.go | 14 +-- cli/dump.go | 94 ++++++++-------- cli/init.go | 88 +++++++-------- cli/p2p_collection.go | 15 ++- cli/p2p_collection_add.go | 72 ++++++------- cli/p2p_collection_getall.go | 78 +++++++------- cli/p2p_collection_remove.go | 72 ++++++------- cli/peerid.go | 122 ++++++++++----------- cli/ping.go | 92 ++++++++-------- cli/replicator.go | 15 ++- cli/replicator_delete.go | 124 ++++++++++----------- cli/replicator_getall.go | 97 +++++++++-------- cli/replicator_set.go | 126 +++++++++++----------- cli/request.go | 177 +++++++++++++++--------------- cli/root.go | 85 ++++++++------- cli/rpc.go | 22 ++-- cli/schema.go | 14 +-- cli/schema_add.go | 203 +++++++++++++++++------------------ cli/schema_patch.go | 196 ++++++++++++++++----------------- cli/serverdump.go | 76 ++++++------- cli/start.go | 132 ++++++++++++----------- cli/version.go | 66 ++++++------ 25 files changed, 1115 insertions(+), 1052 deletions(-) diff --git a/cli/blocks.go b/cli/blocks.go index 8a4ebc9385..9e55c36d22 100644 --- a/cli/blocks.go +++ b/cli/blocks.go @@ -14,11 +14,11 @@ import ( "github.com/spf13/cobra" ) -var blocksCmd = &cobra.Command{ - Use: "blocks", - Short: "Interact with the database's blockstore", -} +func MakeBlocksCommand() *cobra.Command { + var cmd = &cobra.Command{ + Use: "blocks", + Short: "Interact with the database's blockstore", + } -func init() { - clientCmd.AddCommand(blocksCmd) + return cmd } diff --git a/cli/blocks_get.go b/cli/blocks_get.go index 26f6fa7a05..4223745fc4 100644 --- a/cli/blocks_get.go +++ b/cli/blocks_get.go @@ -18,63 +18,63 @@ import ( "github.com/spf13/cobra" httpapi "github.com/sourcenetwork/defradb/api/http" + "github.com/sourcenetwork/defradb/config" ) -var getCmd = &cobra.Command{ - Use: "get [CID]", - Short: "Get a block by its CID from the blockstore.", - RunE: func(cmd *cobra.Command, args []string) (err error) { - if len(args) != 1 { - return NewErrMissingArg("CID") - } - cid := args[0] - - endpoint, err := httpapi.JoinPaths(cfg.API.AddressToURL(), httpapi.BlocksPath, cid) - if err != nil { - return NewErrFailedToJoinEndpoint(err) - } +func MakeBlocksGetCommand(cfg *config.Config) *cobra.Command { + var cmd = &cobra.Command{ + Use: "get [CID]", + Short: "Get a block by its CID from the blockstore.", + RunE: func(cmd *cobra.Command, args []string) (err error) { + if len(args) != 1 { + return NewErrMissingArg("CID") + } + cid := args[0] - res, err := http.Get(endpoint.String()) - if err != nil { - return NewErrFailedToSendRequest(err) - } + endpoint, err := httpapi.JoinPaths(cfg.API.AddressToURL(), httpapi.BlocksPath, cid) + if err != nil { + return NewErrFailedToJoinEndpoint(err) + } - defer func() { - if e := res.Body.Close(); e != nil { - err = NewErrFailedToReadResponseBody(err) + res, err := http.Get(endpoint.String()) + if err != nil { + return NewErrFailedToSendRequest(err) } - }() - response, err := io.ReadAll(res.Body) - if err != nil { - return NewErrFailedToReadResponseBody(err) - } + defer func() { + if e := res.Body.Close(); e != nil { + err = NewErrFailedToReadResponseBody(err) + } + }() - stdout, err := os.Stdout.Stat() - if err != nil { - return NewErrFailedToStatStdOut(err) - } - if isFileInfoPipe(stdout) { - cmd.Println(string(response)) - } else { - graphlErr, err := hasGraphQLErrors(response) + response, err := io.ReadAll(res.Body) if err != nil { - return NewErrFailedToHandleGQLErrors(err) + return NewErrFailedToReadResponseBody(err) } - indentedResult, err := indentJSON(response) + + stdout, err := os.Stdout.Stat() if err != nil { - return NewErrFailedToPrettyPrintResponse(err) + return NewErrFailedToStatStdOut(err) } - if graphlErr { - log.FeedbackError(cmd.Context(), indentedResult) + if isFileInfoPipe(stdout) { + cmd.Println(string(response)) } else { - log.FeedbackInfo(cmd.Context(), indentedResult) + graphlErr, err := hasGraphQLErrors(response) + if err != nil { + return NewErrFailedToHandleGQLErrors(err) + } + indentedResult, err := indentJSON(response) + if err != nil { + return NewErrFailedToPrettyPrintResponse(err) + } + if graphlErr { + log.FeedbackError(cmd.Context(), indentedResult) + } else { + log.FeedbackInfo(cmd.Context(), indentedResult) + } } - } - return nil - }, -} - -func init() { - blocksCmd.AddCommand(getCmd) + return nil + }, + } + return cmd } diff --git a/cli/cli.go b/cli/cli.go index 0fda004328..b4849f78c0 100644 --- a/cli/cli.go +++ b/cli/cli.go @@ -21,11 +21,15 @@ import ( "os" "strings" + "github.com/spf13/cobra" + "github.com/sourcenetwork/defradb/config" "github.com/sourcenetwork/defradb/errors" "github.com/sourcenetwork/defradb/logging" ) +var log = logging.MustNewLogger("defra.cli") + const badgerDatastoreName = "badger" // Errors with how the command is invoked by user @@ -42,30 +46,87 @@ var usageErrors = []string{ errTooManyArgs, } -var log = logging.MustNewLogger("defra.cli") +type DefraCommand struct { + RootCmd *cobra.Command + Cfg *config.Config +} + +// NewDefraCommand returns the root command instanciated with its tree of subcommands. +func NewDefraCommand(cfg *config.Config) DefraCommand { + rootCmd := MakeRootCommand(cfg) + rpcCmd := MakeRPCCommand(cfg) + blocksCmd := MakeBlocksCommand() + schemaCmd := MakeSchemaCommand() + clientCmd := MakeClientCommand() + rpcReplicatorCmd := MakeReplicatorCommand() + p2pCollectionCmd := MakeP2PCollectionCommand() + p2pCollectionCmd.AddCommand( + MakeP2PCollectionAddCommand(cfg), + MakeP2PCollectionRemoveCommand(cfg), + MakeP2PCollectionGetallCommand(cfg), + ) + rpcReplicatorCmd.AddCommand( + MakeReplicatorGetallCommand(cfg), + MakeReplicatorSetCommand(cfg), + MakeReplicatorDeleteCommand(cfg), + ) + rpcCmd.AddCommand( + rpcReplicatorCmd, + p2pCollectionCmd, + ) + blocksCmd.AddCommand( + MakeBlocksGetCommand(cfg), + ) + schemaCmd.AddCommand( + MakeSchemaAddCommand(cfg), + MakeSchemaPatchCommand(cfg), + ) + clientCmd.AddCommand( + MakeDumpCommand(cfg), + MakePingCommand(cfg), + MakeRequestCommand(cfg), + MakePeerIDCommand(cfg), + schemaCmd, + rpcCmd, + blocksCmd, + ) + rootCmd.AddCommand( + clientCmd, + MakeStartCommand(cfg), + MakeServerDumpCmd(cfg), + MakeVersionCommand(), + MakeInitCommand(cfg), + ) -var cfg = config.DefaultConfig() -var RootCmd = rootCmd + return DefraCommand{rootCmd, cfg} +} -func Execute() { - ctx := context.Background() +func (defraCmd *DefraCommand) Execute(ctx context.Context) error { // Silence cobra's default output to control usage and error display. - rootCmd.SilenceUsage = true - rootCmd.SilenceErrors = true - rootCmd.SetOut(os.Stdout) - cmd, err := rootCmd.ExecuteContextC(ctx) + defraCmd.RootCmd.SilenceUsage = true + defraCmd.RootCmd.SilenceErrors = true + defraCmd.RootCmd.SetOut(os.Stdout) + cmd, err := defraCmd.RootCmd.ExecuteContextC(ctx) if err != nil { + // Intentional cancellation. + if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) { + return nil + } + // User error. for _, cobraError := range usageErrors { if strings.HasPrefix(err.Error(), cobraError) { log.FeedbackErrorE(ctx, "Usage error", err) if usageErr := cmd.Usage(); usageErr != nil { log.FeedbackFatalE(ctx, "error displaying usage help", usageErr) } - os.Exit(1) + return err } } - log.FeedbackFatalE(ctx, "Execution error", err) + // Internal error. + log.FeedbackErrorE(ctx, "Execution error", err) + return err } + return nil } func isFileInfoPipe(fi os.FileInfo) bool { diff --git a/cli/client.go b/cli/client.go index c218e22fec..1e6ba43ae5 100644 --- a/cli/client.go +++ b/cli/client.go @@ -14,13 +14,13 @@ import ( "github.com/spf13/cobra" ) -var clientCmd = &cobra.Command{ - Use: "client", - Short: "Interact with a running DefraDB node as a client", - Long: `Interact with a running DefraDB node as a client. +func MakeClientCommand() *cobra.Command { + var cmd = &cobra.Command{ + Use: "client", + Short: "Interact with a running DefraDB node as a client", + Long: `Interact with a running DefraDB node as a client. Execute queries, add schema types, and run debug routines.`, -} + } -func init() { - rootCmd.AddCommand(clientCmd) + return cmd } diff --git a/cli/dump.go b/cli/dump.go index 071a52669d..a23d160e7e 100644 --- a/cli/dump.go +++ b/cli/dump.go @@ -20,62 +20,62 @@ import ( "github.com/spf13/cobra" httpapi "github.com/sourcenetwork/defradb/api/http" + "github.com/sourcenetwork/defradb/config" "github.com/sourcenetwork/defradb/errors" ) -var dumpCmd = &cobra.Command{ - Use: "dump", - Short: "Dump the contents of a database node-side", - RunE: func(cmd *cobra.Command, _ []string) (err error) { - stdout, err := os.Stdout.Stat() - if err != nil { - return errors.Wrap("failed to stat stdout", err) - } - if !isFileInfoPipe(stdout) { - log.FeedbackInfo(cmd.Context(), "Requesting the database to dump its state, server-side...") - } - - endpoint, err := httpapi.JoinPaths(cfg.API.AddressToURL(), httpapi.DumpPath) - if err != nil { - return errors.Wrap("failed to join endpoint", err) - } +func MakeDumpCommand(cfg *config.Config) *cobra.Command { + var cmd = &cobra.Command{ + Use: "dump", + Short: "Dump the contents of a database node-side", + RunE: func(cmd *cobra.Command, _ []string) (err error) { + stdout, err := os.Stdout.Stat() + if err != nil { + return errors.Wrap("failed to stat stdout", err) + } + if !isFileInfoPipe(stdout) { + log.FeedbackInfo(cmd.Context(), "Requesting the database to dump its state, server-side...") + } - res, err := http.Get(endpoint.String()) - if err != nil { - return errors.Wrap("failed dump request", err) - } + endpoint, err := httpapi.JoinPaths(cfg.API.AddressToURL(), httpapi.DumpPath) + if err != nil { + return errors.Wrap("failed to join endpoint", err) + } - defer func() { - if e := res.Body.Close(); e != nil { - err = errors.Wrap(fmt.Sprintf("failed to read response body: %v", e.Error()), err) + res, err := http.Get(endpoint.String()) + if err != nil { + return errors.Wrap("failed dump request", err) } - }() - response, err := io.ReadAll(res.Body) - if err != nil { - return errors.Wrap("failed to read response body", err) - } + defer func() { + if e := res.Body.Close(); e != nil { + err = errors.Wrap(fmt.Sprintf("failed to read response body: %v", e.Error()), err) + } + }() - if isFileInfoPipe(stdout) { - cmd.Println(string(response)) - } else { - // dumpResponse follows structure of HTTP API's response - type dumpResponse struct { - Data struct { - Response string `json:"response"` - } `json:"data"` - } - r := dumpResponse{} - err = json.Unmarshal(response, &r) + response, err := io.ReadAll(res.Body) if err != nil { - return errors.Wrap("failed parsing of response", err) + return errors.Wrap("failed to read response body", err) } - log.FeedbackInfo(cmd.Context(), r.Data.Response) - } - return nil - }, -} -func init() { - clientCmd.AddCommand(dumpCmd) + if isFileInfoPipe(stdout) { + cmd.Println(string(response)) + } else { + // dumpResponse follows structure of HTTP API's response + type dumpResponse struct { + Data struct { + Response string `json:"response"` + } `json:"data"` + } + r := dumpResponse{} + err = json.Unmarshal(response, &r) + if err != nil { + return errors.Wrap("failed parsing of response", err) + } + log.FeedbackInfo(cmd.Context(), r.Data.Response) + } + return nil + }, + } + return cmd } diff --git a/cli/init.go b/cli/init.go index 75d2186d7a..9d188509bd 100644 --- a/cli/init.go +++ b/cli/init.go @@ -19,70 +19,64 @@ import ( "github.com/sourcenetwork/defradb/errors" ) -var reinitialize bool - /* -The `init` command initializes the configuration file and root directory.. +The `init` command initializes the configuration file and root directory. It covers three possible situations: - root dir doesn't exist - root dir exists and doesn't contain a config file - root dir exists and contains a config file */ -var initCmd = &cobra.Command{ - Use: "init", - Short: "Initialize DefraDB's root directory and configuration file", - Long: "Initialize a directory for configuration and data at the given path.", - // Load a default configuration, considering env. variables and CLI flags. - PersistentPreRunE: func(cmd *cobra.Command, _ []string) error { - if err := cfg.LoadWithRootdir(false); err != nil { - return errors.Wrap("failed to load configuration", err) - } - return nil - }, - RunE: func(cmd *cobra.Command, args []string) error { - if config.FolderExists(cfg.Rootdir) { - if cfg.ConfigFileExists() { - if reinitialize { - if err := cfg.DeleteConfigFile(); err != nil { - return err +func MakeInitCommand(cfg *config.Config) *cobra.Command { + var reinitialize bool + var cmd = &cobra.Command{ + Use: "init", + Short: "Initialize DefraDB's root directory and configuration file", + Long: "Initialize a directory for configuration and data at the given path.", + // Load a default configuration, considering env. variables and CLI flags. + PersistentPreRunE: func(cmd *cobra.Command, _ []string) error { + if err := cfg.LoadWithRootdir(false); err != nil { + return errors.Wrap("failed to load configuration", err) + } + return nil + }, + RunE: func(cmd *cobra.Command, args []string) error { + if config.FolderExists(cfg.Rootdir) { + if cfg.ConfigFileExists() { + if reinitialize { + if err := cfg.DeleteConfigFile(); err != nil { + return err + } + if err := cfg.WriteConfigFile(); err != nil { + return err + } + } else { + log.FeedbackError( + cmd.Context(), + fmt.Sprintf( + "Configuration file already exists at %v. Consider using --reinitialize", + cfg.ConfigFilePath(), + ), + ) } + } else { if err := cfg.WriteConfigFile(); err != nil { - return err + return errors.Wrap("failed to create configuration file", err) } - } else { - log.FeedbackError( - cmd.Context(), - fmt.Sprintf( - "Configuration file already exists at %v. Consider using --reinitialize", - cfg.ConfigFilePath(), - ), - ) } } else { - if err := cfg.WriteConfigFile(); err != nil { - return errors.Wrap("failed to create configuration file", err) + if err := cfg.CreateRootDirAndConfigFile(); err != nil { + return err } } - } else { - if err := cfg.CreateRootDirAndConfigFile(); err != nil { - return err - } - } - return nil - }, -} + return nil + }, + } -func init() { - rootCmd.AddCommand(initCmd) - - initCmd.Flags().BoolVar( + cmd.Flags().BoolVar( &reinitialize, "reinitialize", false, "Reinitialize the configuration file", ) - initCmd.Flags().StringVar( - &cfg.Rootdir, "rootdir", config.DefaultRootDir(), - "Directory for data and configuration to use", - ) + return cmd } diff --git a/cli/p2p_collection.go b/cli/p2p_collection.go index c2699b24a1..143820d4d8 100644 --- a/cli/p2p_collection.go +++ b/cli/p2p_collection.go @@ -14,12 +14,11 @@ import ( "github.com/spf13/cobra" ) -var p2pCollectionCmd = &cobra.Command{ - Use: "p2pcollection", - Short: "Interact with the P2P collection system", - Long: "Add, delete, or get the list of P2P collections", -} - -func init() { - rpcCmd.AddCommand(p2pCollectionCmd) +func MakeP2PCollectionCommand() *cobra.Command { + var cmd = &cobra.Command{ + Use: "p2pcollection", + Short: "Interact with the P2P collection system", + Long: "Add, delete, or get the list of P2P collections", + } + return cmd } diff --git a/cli/p2p_collection_add.go b/cli/p2p_collection_add.go index 31aa20c64d..d0fc18b6db 100644 --- a/cli/p2p_collection_add.go +++ b/cli/p2p_collection_add.go @@ -17,45 +17,45 @@ import ( "google.golang.org/grpc" "google.golang.org/grpc/credentials/insecure" + "github.com/sourcenetwork/defradb/config" "github.com/sourcenetwork/defradb/errors" "github.com/sourcenetwork/defradb/logging" netclient "github.com/sourcenetwork/defradb/net/api/client" ) -var addP2PCollectionCmd = &cobra.Command{ - Use: "add [collectionID]", - Short: "Add P2P collections", - Long: `Use this command if you wish to add new P2P collections to the pubsub topics`, - Args: func(cmd *cobra.Command, args []string) error { - if err := cobra.MinimumNArgs(1)(cmd, args); err != nil { - return errors.New("must specify at least one collectionID") - } - return nil - }, - RunE: func(cmd *cobra.Command, args []string) error { - cred := insecure.NewCredentials() - client, err := netclient.NewClient(cfg.Net.RPCAddress, grpc.WithTransportCredentials(cred)) - if err != nil { - return errors.Wrap("failed to create RPC client", err) - } - - rpcTimeoutDuration, err := cfg.Net.RPCTimeoutDuration() - if err != nil { - return errors.Wrap("failed to parse RPC timeout duration", err) - } - - ctx, cancel := context.WithTimeout(cmd.Context(), rpcTimeoutDuration) - defer cancel() - - err = client.AddP2PCollections(ctx, args...) - if err != nil { - return errors.Wrap("failed to add p2p collections, request failed", err) - } - log.FeedbackInfo(ctx, "Successfully added p2p collections", logging.NewKV("Collections", args)) - return nil - }, -} - -func init() { - p2pCollectionCmd.AddCommand(addP2PCollectionCmd) +func MakeP2PCollectionAddCommand(cfg *config.Config) *cobra.Command { + var cmd = &cobra.Command{ + Use: "add [collectionID]", + Short: "Add P2P collections", + Long: `Use this command if you wish to add new P2P collections to the pubsub topics`, + Args: func(cmd *cobra.Command, args []string) error { + if err := cobra.MinimumNArgs(1)(cmd, args); err != nil { + return errors.New("must specify at least one collectionID") + } + return nil + }, + RunE: func(cmd *cobra.Command, args []string) error { + cred := insecure.NewCredentials() + client, err := netclient.NewClient(cfg.Net.RPCAddress, grpc.WithTransportCredentials(cred)) + if err != nil { + return errors.Wrap("failed to create RPC client", err) + } + + rpcTimeoutDuration, err := cfg.Net.RPCTimeoutDuration() + if err != nil { + return errors.Wrap("failed to parse RPC timeout duration", err) + } + + ctx, cancel := context.WithTimeout(cmd.Context(), rpcTimeoutDuration) + defer cancel() + + err = client.AddP2PCollections(ctx, args...) + if err != nil { + return errors.Wrap("failed to add p2p collections, request failed", err) + } + log.FeedbackInfo(ctx, "Successfully added p2p collections", logging.NewKV("Collections", args)) + return nil + }, + } + return cmd } diff --git a/cli/p2p_collection_getall.go b/cli/p2p_collection_getall.go index 23a39c7e78..7e34339e0d 100644 --- a/cli/p2p_collection_getall.go +++ b/cli/p2p_collection_getall.go @@ -17,54 +17,54 @@ import ( "google.golang.org/grpc" "google.golang.org/grpc/credentials/insecure" + "github.com/sourcenetwork/defradb/config" "github.com/sourcenetwork/defradb/errors" "github.com/sourcenetwork/defradb/logging" netclient "github.com/sourcenetwork/defradb/net/api/client" ) -var getAllP2PCollectionCmd = &cobra.Command{ - Use: "getall", - Short: "Get all P2P collections", - Long: `Use this command if you wish to get all P2P collections in the pubsub topics`, - Args: func(cmd *cobra.Command, args []string) error { - if err := cobra.NoArgs(cmd, args); err != nil { - return errors.New("must specify no argument") - } - return nil - }, - RunE: func(cmd *cobra.Command, args []string) error { - cred := insecure.NewCredentials() - client, err := netclient.NewClient(cfg.Net.RPCAddress, grpc.WithTransportCredentials(cred)) - if err != nil { - return errors.Wrap("failed to create RPC client", err) - } - - rpcTimeoutDuration, err := cfg.Net.RPCTimeoutDuration() - if err != nil { - return errors.Wrap("failed to parse RPC timeout duration", err) - } +func MakeP2PCollectionGetallCommand(cfg *config.Config) *cobra.Command { + var cmd = &cobra.Command{ + Use: "getall", + Short: "Get all P2P collections", + Long: `Use this command if you wish to get all P2P collections in the pubsub topics`, + Args: func(cmd *cobra.Command, args []string) error { + if err := cobra.NoArgs(cmd, args); err != nil { + return errors.New("must specify no argument") + } + return nil + }, + RunE: func(cmd *cobra.Command, args []string) error { + cred := insecure.NewCredentials() + client, err := netclient.NewClient(cfg.Net.RPCAddress, grpc.WithTransportCredentials(cred)) + if err != nil { + return errors.Wrap("failed to create RPC client", err) + } - ctx, cancel := context.WithTimeout(cmd.Context(), rpcTimeoutDuration) - defer cancel() + rpcTimeoutDuration, err := cfg.Net.RPCTimeoutDuration() + if err != nil { + return errors.Wrap("failed to parse RPC timeout duration", err) + } - collections, err := client.GetAllP2PCollections(ctx) - if err != nil { - return errors.Wrap("failed to add p2p collections, request failed", err) - } + ctx, cancel := context.WithTimeout(cmd.Context(), rpcTimeoutDuration) + defer cancel() - if len(collections) > 0 { - log.FeedbackInfo(ctx, "Successfully got all P2P collections") - for _, col := range collections { - log.FeedbackInfo(ctx, col.Name, logging.NewKV("CollectionID", col.ID)) + collections, err := client.GetAllP2PCollections(ctx) + if err != nil { + return errors.Wrap("failed to add p2p collections, request failed", err) } - } else { - log.FeedbackInfo(ctx, "No P2P collection found") - } - return nil - }, -} + if len(collections) > 0 { + log.FeedbackInfo(ctx, "Successfully got all P2P collections") + for _, col := range collections { + log.FeedbackInfo(ctx, col.Name, logging.NewKV("CollectionID", col.ID)) + } + } else { + log.FeedbackInfo(ctx, "No P2P collection found") + } -func init() { - p2pCollectionCmd.AddCommand(getAllP2PCollectionCmd) + return nil + }, + } + return cmd } diff --git a/cli/p2p_collection_remove.go b/cli/p2p_collection_remove.go index 46ba58b3a7..ad79a86d1a 100644 --- a/cli/p2p_collection_remove.go +++ b/cli/p2p_collection_remove.go @@ -17,45 +17,45 @@ import ( "google.golang.org/grpc" "google.golang.org/grpc/credentials/insecure" + "github.com/sourcenetwork/defradb/config" "github.com/sourcenetwork/defradb/errors" "github.com/sourcenetwork/defradb/logging" netclient "github.com/sourcenetwork/defradb/net/api/client" ) -var removeP2PCollectionCmd = &cobra.Command{ - Use: "remove [collectionID]", - Short: "Add P2P collections", - Long: `Use this command if you wish to remove P2P collections from the pubsub topics`, - Args: func(cmd *cobra.Command, args []string) error { - if err := cobra.MinimumNArgs(1)(cmd, args); err != nil { - return errors.New("must specify at least one collectionID") - } - return nil - }, - RunE: func(cmd *cobra.Command, args []string) error { - cred := insecure.NewCredentials() - client, err := netclient.NewClient(cfg.Net.RPCAddress, grpc.WithTransportCredentials(cred)) - if err != nil { - return errors.Wrap("failed to create RPC client", err) - } - - rpcTimeoutDuration, err := cfg.Net.RPCTimeoutDuration() - if err != nil { - return errors.Wrap("failed to parse RPC timeout duration", err) - } - - ctx, cancel := context.WithTimeout(cmd.Context(), rpcTimeoutDuration) - defer cancel() - - err = client.RemoveP2PCollections(ctx, args...) - if err != nil { - return errors.Wrap("failed to remove p2p collections, request failed", err) - } - log.FeedbackInfo(ctx, "Successfully removed p2p collections", logging.NewKV("Collections", args)) - return nil - }, -} - -func init() { - p2pCollectionCmd.AddCommand(removeP2PCollectionCmd) +func MakeP2PCollectionRemoveCommand(cfg *config.Config) *cobra.Command { + var cmd = &cobra.Command{ + Use: "remove [collectionID]", + Short: "Add P2P collections", + Long: `Use this command if you wish to remove P2P collections from the pubsub topics`, + Args: func(cmd *cobra.Command, args []string) error { + if err := cobra.MinimumNArgs(1)(cmd, args); err != nil { + return errors.New("must specify at least one collectionID") + } + return nil + }, + RunE: func(cmd *cobra.Command, args []string) error { + cred := insecure.NewCredentials() + client, err := netclient.NewClient(cfg.Net.RPCAddress, grpc.WithTransportCredentials(cred)) + if err != nil { + return errors.Wrap("failed to create RPC client", err) + } + + rpcTimeoutDuration, err := cfg.Net.RPCTimeoutDuration() + if err != nil { + return errors.Wrap("failed to parse RPC timeout duration", err) + } + + ctx, cancel := context.WithTimeout(cmd.Context(), rpcTimeoutDuration) + defer cancel() + + err = client.RemoveP2PCollections(ctx, args...) + if err != nil { + return errors.Wrap("failed to remove p2p collections, request failed", err) + } + log.FeedbackInfo(ctx, "Successfully removed p2p collections", logging.NewKV("Collections", args)) + return nil + }, + } + return cmd } diff --git a/cli/peerid.go b/cli/peerid.go index 23075ffa7c..091f5fd0cf 100644 --- a/cli/peerid.go +++ b/cli/peerid.go @@ -20,82 +20,82 @@ import ( "github.com/spf13/cobra" httpapi "github.com/sourcenetwork/defradb/api/http" + "github.com/sourcenetwork/defradb/config" "github.com/sourcenetwork/defradb/errors" ) -var peerIDCmd = &cobra.Command{ - Use: "peerid", - Short: "Get the peer ID of the DefraDB node", - RunE: func(cmd *cobra.Command, _ []string) (err error) { - stdout, err := os.Stdout.Stat() - if err != nil { - return errors.Wrap("failed to stat stdout", err) - } - if !isFileInfoPipe(stdout) { - log.FeedbackInfo(cmd.Context(), "Requesting peer ID...") - } - - endpoint, err := httpapi.JoinPaths(cfg.API.AddressToURL(), httpapi.PeerIDPath) - if err != nil { - return errors.Wrap("failed to join endpoint", err) - } +func MakePeerIDCommand(cfg *config.Config) *cobra.Command { + var cmd = &cobra.Command{ + Use: "peerid", + Short: "Get the peer ID of the DefraDB node", + RunE: func(cmd *cobra.Command, _ []string) (err error) { + stdout, err := os.Stdout.Stat() + if err != nil { + return errors.Wrap("failed to stat stdout", err) + } + if !isFileInfoPipe(stdout) { + log.FeedbackInfo(cmd.Context(), "Requesting peer ID...") + } - res, err := http.Get(endpoint.String()) - if err != nil { - return errors.Wrap("failed to request peer ID", err) - } + endpoint, err := httpapi.JoinPaths(cfg.API.AddressToURL(), httpapi.PeerIDPath) + if err != nil { + return errors.Wrap("failed to join endpoint", err) + } - defer func() { - if e := res.Body.Close(); e != nil { - err = errors.Wrap(fmt.Sprintf("failed to read response body: %v", e.Error()), err) + res, err := http.Get(endpoint.String()) + if err != nil { + return errors.Wrap("failed to request peer ID", err) } - }() - response, err := io.ReadAll(res.Body) - if err != nil { - return errors.Wrap("failed to read response body", err) - } + defer func() { + if e := res.Body.Close(); e != nil { + err = errors.Wrap(fmt.Sprintf("failed to read response body: %v", e.Error()), err) + } + }() - if res.StatusCode == http.StatusNotFound { - r := httpapi.ErrorResponse{} - err = json.Unmarshal(response, &r) + response, err := io.ReadAll(res.Body) if err != nil { - return errors.Wrap("parsing of response failed", err) + return errors.Wrap("failed to read response body", err) } - if len(r.Errors) > 0 { - if isFileInfoPipe(stdout) { - b, err := json.Marshal(r.Errors[0]) - if err != nil { - return errors.Wrap("mashalling error response failed", err) + + if res.StatusCode == http.StatusNotFound { + r := httpapi.ErrorResponse{} + err = json.Unmarshal(response, &r) + if err != nil { + return errors.Wrap("parsing of response failed", err) + } + if len(r.Errors) > 0 { + if isFileInfoPipe(stdout) { + b, err := json.Marshal(r.Errors[0]) + if err != nil { + return errors.Wrap("mashalling error response failed", err) + } + cmd.Println(string(b)) + } else { + log.FeedbackInfo(cmd.Context(), r.Errors[0].Message) } - cmd.Println(string(b)) - } else { - log.FeedbackInfo(cmd.Context(), r.Errors[0].Message) + return nil } - return nil + return errors.New("no peer ID available. P2P might be disabled") } - return errors.New("no peer ID available. P2P might be disabled") - } - r := httpapi.DataResponse{} - err = json.Unmarshal(response, &r) - if err != nil { - return errors.Wrap("parsing of response failed", err) - } - if isFileInfoPipe(stdout) { - b, err := json.Marshal(r.Data) + r := httpapi.DataResponse{} + err = json.Unmarshal(response, &r) if err != nil { - return errors.Wrap("mashalling data response failed", err) + return errors.Wrap("parsing of response failed", err) + } + if isFileInfoPipe(stdout) { + b, err := json.Marshal(r.Data) + if err != nil { + return errors.Wrap("mashalling data response failed", err) + } + cmd.Println(string(b)) + } else if data, ok := r.Data.(map[string]any); ok { + log.FeedbackInfo(cmd.Context(), data["peerID"].(string)) } - cmd.Println(string(b)) - } else if data, ok := r.Data.(map[string]any); ok { - log.FeedbackInfo(cmd.Context(), data["peerID"].(string)) - } - - return nil - }, -} -func init() { - clientCmd.AddCommand(peerIDCmd) + return nil + }, + } + return cmd } diff --git a/cli/ping.go b/cli/ping.go index 6dbd16aa37..11ca129850 100644 --- a/cli/ping.go +++ b/cli/ping.go @@ -20,61 +20,61 @@ import ( "github.com/spf13/cobra" httpapi "github.com/sourcenetwork/defradb/api/http" + "github.com/sourcenetwork/defradb/config" "github.com/sourcenetwork/defradb/errors" ) -var pingCmd = &cobra.Command{ - Use: "ping", - Short: "Ping to test connection to a node", - RunE: func(cmd *cobra.Command, _ []string) (err error) { - stdout, err := os.Stdout.Stat() - if err != nil { - return errors.Wrap("failed to stat stdout", err) - } - if !isFileInfoPipe(stdout) { - log.FeedbackInfo(cmd.Context(), "Sending ping...") - } - - endpoint, err := httpapi.JoinPaths(cfg.API.AddressToURL(), httpapi.PingPath) - if err != nil { - return errors.Wrap("failed to join endpoint", err) - } +func MakePingCommand(cfg *config.Config) *cobra.Command { + var cmd = &cobra.Command{ + Use: "ping", + Short: "Ping to test connection to a node", + RunE: func(cmd *cobra.Command, _ []string) (err error) { + stdout, err := os.Stdout.Stat() + if err != nil { + return errors.Wrap("failed to stat stdout", err) + } + if !isFileInfoPipe(stdout) { + log.FeedbackInfo(cmd.Context(), "Sending ping...") + } - res, err := http.Get(endpoint.String()) - if err != nil { - return errors.Wrap("failed to send ping", err) - } + endpoint, err := httpapi.JoinPaths(cfg.API.AddressToURL(), httpapi.PingPath) + if err != nil { + return errors.Wrap("failed to join endpoint", err) + } - defer func() { - if e := res.Body.Close(); e != nil { - err = errors.Wrap(fmt.Sprintf("failed to read response body: %v", e.Error()), err) + res, err := http.Get(endpoint.String()) + if err != nil { + return errors.Wrap("failed to send ping", err) } - }() - response, err := io.ReadAll(res.Body) - if err != nil { - return errors.Wrap("failed to read response body", err) - } + defer func() { + if e := res.Body.Close(); e != nil { + err = errors.Wrap(fmt.Sprintf("failed to read response body: %v", e.Error()), err) + } + }() - if isFileInfoPipe(stdout) { - cmd.Println(string(response)) - } else { - type pingResponse struct { - Data struct { - Response string `json:"response"` - } `json:"data"` - } - r := pingResponse{} - err = json.Unmarshal(response, &r) + response, err := io.ReadAll(res.Body) if err != nil { - return errors.Wrap("parsing of response failed", err) + return errors.Wrap("failed to read response body", err) } - log.FeedbackInfo(cmd.Context(), r.Data.Response) - } - return nil - }, -} -func init() { - clientCmd.AddCommand(pingCmd) + if isFileInfoPipe(stdout) { + cmd.Println(string(response)) + } else { + type pingResponse struct { + Data struct { + Response string `json:"response"` + } `json:"data"` + } + r := pingResponse{} + err = json.Unmarshal(response, &r) + if err != nil { + return errors.Wrap("parsing of response failed", err) + } + log.FeedbackInfo(cmd.Context(), r.Data.Response) + } + return nil + }, + } + return cmd } diff --git a/cli/replicator.go b/cli/replicator.go index dfa40e1fa5..fb6946ac29 100644 --- a/cli/replicator.go +++ b/cli/replicator.go @@ -14,12 +14,11 @@ import ( "github.com/spf13/cobra" ) -var replicatorCmd = &cobra.Command{ - Use: "replicator", - Short: "Interact with the replicator system", - Long: "Add, delete, or get the list of persisted replicators", -} - -func init() { - rpcCmd.AddCommand(replicatorCmd) +func MakeReplicatorCommand() *cobra.Command { + var cmd = &cobra.Command{ + Use: "replicator", + Short: "Interact with the replicator system", + Long: "Add, delete, or get the list of persisted replicators", + } + return cmd } diff --git a/cli/replicator_delete.go b/cli/replicator_delete.go index 9a0d118eac..e4ca9e885c 100644 --- a/cli/replicator_delete.go +++ b/cli/replicator_delete.go @@ -18,77 +18,81 @@ import ( "google.golang.org/grpc" "google.golang.org/grpc/credentials/insecure" + "github.com/sourcenetwork/defradb/config" "github.com/sourcenetwork/defradb/errors" "github.com/sourcenetwork/defradb/logging" netclient "github.com/sourcenetwork/defradb/net/api/client" ) -var deleteReplicatorCmd = &cobra.Command{ - Use: "delete [-f, --full | -c, --collection] ", - Short: "Delete a replicator", - Long: `Use this command if you wish to remove the target replicator -for the p2p data sync system.`, - Args: func(cmd *cobra.Command, args []string) error { - if err := cobra.ExactArgs(1)(cmd, args); err != nil { - return errors.New("must specify one argument: peer") - } - return nil - }, - RunE: func(cmd *cobra.Command, args []string) error { - pidString := args[0] - - if len(col) != 0 { - log.FeedbackInfo( - cmd.Context(), - "Removing replicator for collection", - logging.NewKV("PeerAddress", pidString), - logging.NewKV("Collection", col), - logging.NewKV("RPCAddress", cfg.Net.RPCAddress), - ) - } else { - if !fullRep { - return errors.New("must run with either --full or --collection") +func MakeReplicatorDeleteCommand(cfg *config.Config) *cobra.Command { + var ( + fullRep bool + col []string + ) + var cmd = &cobra.Command{ + Use: "delete [-f, --full | -c, --collection] ", + Short: "Delete a replicator", + Long: `Use this command if you wish to remove the target replicator + for the p2p data sync system.`, + Args: func(cmd *cobra.Command, args []string) error { + if err := cobra.ExactArgs(1)(cmd, args); err != nil { + return errors.New("must specify one argument: peer") } - log.FeedbackInfo( - cmd.Context(), - "Removing full replicator", - logging.NewKV("PeerAddress", pidString), - logging.NewKV("RPCAddress", cfg.Net.RPCAddress), - ) - } + return nil + }, + RunE: func(cmd *cobra.Command, args []string) error { + pidString := args[0] - cred := insecure.NewCredentials() - client, err := netclient.NewClient(cfg.Net.RPCAddress, grpc.WithTransportCredentials(cred)) - if err != nil { - return errors.Wrap("failed to create RPC client", err) - } + if len(col) != 0 { + log.FeedbackInfo( + cmd.Context(), + "Removing replicator for collection", + logging.NewKV("PeerAddress", pidString), + logging.NewKV("Collection", col), + logging.NewKV("RPCAddress", cfg.Net.RPCAddress), + ) + } else { + if !fullRep { + return errors.New("must run with either --full or --collection") + } + log.FeedbackInfo( + cmd.Context(), + "Removing full replicator", + logging.NewKV("PeerAddress", pidString), + logging.NewKV("RPCAddress", cfg.Net.RPCAddress), + ) + } - rpcTimeoutDuration, err := cfg.Net.RPCTimeoutDuration() - if err != nil { - return errors.Wrap("failed to parse RPC timeout duration", err) - } + cred := insecure.NewCredentials() + client, err := netclient.NewClient(cfg.Net.RPCAddress, grpc.WithTransportCredentials(cred)) + if err != nil { + return errors.Wrap("failed to create RPC client", err) + } - ctx, cancel := context.WithTimeout(cmd.Context(), rpcTimeoutDuration) - defer cancel() + rpcTimeoutDuration, err := cfg.Net.RPCTimeoutDuration() + if err != nil { + return errors.Wrap("failed to parse RPC timeout duration", err) + } - pid, err := peer.Decode(pidString) - if err != nil { - return errors.Wrap("failed to parse peer id from string", err) - } + ctx, cancel := context.WithTimeout(cmd.Context(), rpcTimeoutDuration) + defer cancel() - err = client.DeleteReplicator(ctx, pid) - if err != nil { - return errors.Wrap("failed to delete replicator, request failed", err) - } - log.FeedbackInfo(ctx, "Successfully deleted replicator", logging.NewKV("PID", pid.String())) - return nil - }, -} + pid, err := peer.Decode(pidString) + if err != nil { + return errors.Wrap("failed to parse peer id from string", err) + } -func init() { - replicatorCmd.AddCommand(deleteReplicatorCmd) - replicatorCmd.Flags().BoolVarP(&fullRep, "full", "f", false, "Set the replicator to act on all collections") - replicatorCmd.Flags().StringArrayVarP(&col, "collection", "c", + err = client.DeleteReplicator(ctx, pid) + if err != nil { + return errors.Wrap("failed to delete replicator, request failed", err) + } + log.FeedbackInfo(ctx, "Successfully deleted replicator", logging.NewKV("PID", pid.String())) + return nil + }, + } + cmd.Flags().BoolVarP(&fullRep, "full", "f", false, "Set the replicator to act on all collections") + cmd.Flags().StringArrayVarP(&col, "collection", "c", []string{}, "Define the collection for the replicator") - replicatorCmd.MarkFlagsMutuallyExclusive("full", "collection") + cmd.MarkFlagsMutuallyExclusive("full", "collection") + return cmd } diff --git a/cli/replicator_getall.go b/cli/replicator_getall.go index d3dcfe173f..0c03b34e3a 100644 --- a/cli/replicator_getall.go +++ b/cli/replicator_getall.go @@ -17,66 +17,65 @@ import ( "google.golang.org/grpc" "google.golang.org/grpc/credentials/insecure" + "github.com/sourcenetwork/defradb/config" "github.com/sourcenetwork/defradb/errors" "github.com/sourcenetwork/defradb/logging" netclient "github.com/sourcenetwork/defradb/net/api/client" ) -var getAllReplicatorsCmd = &cobra.Command{ - Use: "getall", - Short: "Get all replicators", - Long: `Use this command if you wish to get all the replicators -for the p2p data sync system.`, - RunE: func(cmd *cobra.Command, args []string) error { - if len(args) != 0 { - if err := cmd.Usage(); err != nil { - return err +func MakeReplicatorGetallCommand(cfg *config.Config) *cobra.Command { + var cmd = &cobra.Command{ + Use: "getall", + Short: "Get all replicators", + Long: `Use this command if you wish to get all the replicators for the p2p data sync system.`, + RunE: func(cmd *cobra.Command, args []string) error { + if len(args) != 0 { + if err := cmd.Usage(); err != nil { + return err + } + return errors.New("must specify no argument") } - return errors.New("must specify no argument") - } - log.FeedbackInfo( - cmd.Context(), - "Getting all replicators", - logging.NewKV("RPCAddress", cfg.Net.RPCAddress), - ) + log.FeedbackInfo( + cmd.Context(), + "Getting all replicators", + logging.NewKV("RPCAddress", cfg.Net.RPCAddress), + ) - cred := insecure.NewCredentials() - client, err := netclient.NewClient(cfg.Net.RPCAddress, grpc.WithTransportCredentials(cred)) - if err != nil { - return errors.Wrap("failed to create RPC client", err) - } + cred := insecure.NewCredentials() + client, err := netclient.NewClient(cfg.Net.RPCAddress, grpc.WithTransportCredentials(cred)) + if err != nil { + return errors.Wrap("failed to create RPC client", err) + } - rpcTimeoutDuration, err := cfg.Net.RPCTimeoutDuration() - if err != nil { - return errors.Wrap("failed to parse RPC timeout duration", err) - } + rpcTimeoutDuration, err := cfg.Net.RPCTimeoutDuration() + if err != nil { + return errors.Wrap("failed to parse RPC timeout duration", err) + } - ctx, cancel := context.WithTimeout(cmd.Context(), rpcTimeoutDuration) - defer cancel() + ctx, cancel := context.WithTimeout(cmd.Context(), rpcTimeoutDuration) + defer cancel() - reps, err := client.GetAllReplicators(ctx) - if err != nil { - return errors.Wrap("failed to get replicators, request failed", err) - } - if len(reps) > 0 { - log.FeedbackInfo(ctx, "Successfully got all replicators") - for _, rep := range reps { - log.FeedbackInfo( - ctx, - rep.Info.ID.String(), - logging.NewKV("Schemas", rep.Schemas), - logging.NewKV("Addrs", rep.Info.Addrs), - ) + reps, err := client.GetAllReplicators(ctx) + if err != nil { + return errors.Wrap("failed to get replicators, request failed", err) + } + if len(reps) > 0 { + log.FeedbackInfo(ctx, "Successfully got all replicators") + for _, rep := range reps { + log.FeedbackInfo( + ctx, + rep.Info.ID.String(), + logging.NewKV("Schemas", rep.Schemas), + logging.NewKV("Addrs", rep.Info.Addrs), + ) + } + } else { + log.FeedbackInfo(ctx, "No replicator found") } - } else { - log.FeedbackInfo(ctx, "No replicator found") - } - - return nil - }, -} -func init() { - replicatorCmd.AddCommand(getAllReplicatorsCmd) + return nil + }, + } + return cmd } diff --git a/cli/replicator_set.go b/cli/replicator_set.go index 13715b9ac6..8fbdfc4998 100644 --- a/cli/replicator_set.go +++ b/cli/replicator_set.go @@ -18,80 +18,80 @@ import ( "google.golang.org/grpc" "google.golang.org/grpc/credentials/insecure" + "github.com/sourcenetwork/defradb/config" "github.com/sourcenetwork/defradb/errors" "github.com/sourcenetwork/defradb/logging" netclient "github.com/sourcenetwork/defradb/net/api/client" ) -var ( - fullRep bool - col []string -) - -var setReplicatorCmd = &cobra.Command{ - Use: "set [-f, --full | -c, --collection] ", - Short: "Set a P2P replicator", - Long: `Use this command if you wish to add a new target replicator -for the p2p data sync system or add schemas to an existing one`, - Args: func(cmd *cobra.Command, args []string) error { - if err := cobra.ExactArgs(1)(cmd, args); err != nil { - return errors.New("must specify one argument: peer") - } - return nil - }, - RunE: func(cmd *cobra.Command, args []string) error { - peerAddr, err := ma.NewMultiaddr(args[0]) - if err != nil { - return errors.Wrap("could not parse peer address", err) - } +func MakeReplicatorSetCommand(cfg *config.Config) *cobra.Command { + var ( + fullRep bool + col []string + ) + var cmd = &cobra.Command{ + Use: "set [-f, --full | -c, --collection] ", + Short: "Set a P2P replicator", + Long: `Use this command if you wish to add a new target replicator + for the p2p data sync system or add schemas to an existing one`, + Args: func(cmd *cobra.Command, args []string) error { + if err := cobra.ExactArgs(1)(cmd, args); err != nil { + return errors.New("must specify one argument: peer") + } + return nil + }, + RunE: func(cmd *cobra.Command, args []string) error { + peerAddr, err := ma.NewMultiaddr(args[0]) + if err != nil { + return errors.Wrap("could not parse peer address", err) + } - if len(col) != 0 { - log.FeedbackInfo( - cmd.Context(), - "Adding replicator for collection", - logging.NewKV("PeerAddress", peerAddr), - logging.NewKV("Collection", col), - logging.NewKV("RPCAddress", cfg.Net.RPCAddress), - ) - } else { - if !fullRep { - return errors.New("must run with either --full or --collection") + if len(col) != 0 { + log.FeedbackInfo( + cmd.Context(), + "Adding replicator for collection", + logging.NewKV("PeerAddress", peerAddr), + logging.NewKV("Collection", col), + logging.NewKV("RPCAddress", cfg.Net.RPCAddress), + ) + } else { + if !fullRep { + return errors.New("must run with either --full or --collection") + } + log.FeedbackInfo( + cmd.Context(), + "Adding full replicator", + logging.NewKV("PeerAddress", peerAddr), + logging.NewKV("RPCAddress", cfg.Net.RPCAddress), + ) } - log.FeedbackInfo( - cmd.Context(), - "Adding full replicator", - logging.NewKV("PeerAddress", peerAddr), - logging.NewKV("RPCAddress", cfg.Net.RPCAddress), - ) - } - cred := insecure.NewCredentials() - client, err := netclient.NewClient(cfg.Net.RPCAddress, grpc.WithTransportCredentials(cred)) - if err != nil { - return errors.Wrap("failed to create RPC client", err) - } + cred := insecure.NewCredentials() + client, err := netclient.NewClient(cfg.Net.RPCAddress, grpc.WithTransportCredentials(cred)) + if err != nil { + return errors.Wrap("failed to create RPC client", err) + } - rpcTimeoutDuration, err := cfg.Net.RPCTimeoutDuration() - if err != nil { - return errors.Wrap("failed to parse RPC timeout duration", err) - } + rpcTimeoutDuration, err := cfg.Net.RPCTimeoutDuration() + if err != nil { + return errors.Wrap("failed to parse RPC timeout duration", err) + } - ctx, cancel := context.WithTimeout(cmd.Context(), rpcTimeoutDuration) - defer cancel() + ctx, cancel := context.WithTimeout(cmd.Context(), rpcTimeoutDuration) + defer cancel() - pid, err := client.SetReplicator(ctx, peerAddr, col...) - if err != nil { - return errors.Wrap("failed to add replicator, request failed", err) - } - log.FeedbackInfo(ctx, "Successfully added replicator", logging.NewKV("PID", pid)) - return nil - }, -} + pid, err := client.SetReplicator(ctx, peerAddr, col...) + if err != nil { + return errors.Wrap("failed to add replicator, request failed", err) + } + log.FeedbackInfo(ctx, "Successfully added replicator", logging.NewKV("PID", pid)) + return nil + }, + } -func init() { - replicatorCmd.AddCommand(setReplicatorCmd) - setReplicatorCmd.Flags().BoolVarP(&fullRep, "full", "f", false, "Set the replicator to act on all collections") - setReplicatorCmd.Flags().StringArrayVarP(&col, "collection", "c", + cmd.Flags().BoolVarP(&fullRep, "full", "f", false, "Set the replicator to act on all collections") + cmd.Flags().StringArrayVarP(&col, "collection", "c", []string{}, "Define the collection for the replicator") - setReplicatorCmd.MarkFlagsMutuallyExclusive("full", "collection") + cmd.MarkFlagsMutuallyExclusive("full", "collection") + return cmd } diff --git a/cli/request.go b/cli/request.go index d57a9c1395..c7f645e3b8 100644 --- a/cli/request.go +++ b/cli/request.go @@ -20,13 +20,15 @@ import ( "github.com/spf13/cobra" httpapi "github.com/sourcenetwork/defradb/api/http" + "github.com/sourcenetwork/defradb/config" "github.com/sourcenetwork/defradb/errors" ) -var requestCmd = &cobra.Command{ - Use: "query [query request]", - Short: "Send a DefraDB GraphQL query request", - Long: `Send a DefraDB GraphQL query request to the database. +func MakeRequestCommand(cfg *config.Config) *cobra.Command { + var cmd = &cobra.Command{ + Use: "query [query request]", + Short: "Send a DefraDB GraphQL query request", + Long: `Send a DefraDB GraphQL query request to the database. A query request can be sent as a single argument. Example command: defradb client query 'query { ... }' @@ -38,102 +40,101 @@ A GraphQL client such as GraphiQL (https://github.com/graphql/graphiql) can be u with the database more conveniently. To learn more about the DefraDB GraphQL Query Language, refer to https://docs.source.network.`, - RunE: func(cmd *cobra.Command, args []string) (err error) { - var request string + RunE: func(cmd *cobra.Command, args []string) (err error) { + var request string - fi, err := os.Stdin.Stat() - if err != nil { - return err - } - - if len(args) > 1 { - if err = cmd.Usage(); err != nil { - return err - } - return errors.New("too many arguments") - } - - if isFileInfoPipe(fi) && (len(args) == 0 || args[0] != "-") { - log.FeedbackInfo( - cmd.Context(), - "Run 'defradb client query -' to read from stdin. Example: 'cat my.graphql | defradb client query -').", - ) - return nil - } else if len(args) == 0 { - err := cmd.Help() + fi, err := os.Stdin.Stat() if err != nil { - return errors.Wrap("failed to print help", err) + return err } - return nil - } else if args[0] == "-" { - stdin, err := readStdin() - if err != nil { - return errors.Wrap("failed to read stdin", err) + + if len(args) > 1 { + if err = cmd.Usage(); err != nil { + return err + } + return errors.New("too many arguments") } - if len(stdin) == 0 { - return errors.New("no query request in stdin provided") + + if isFileInfoPipe(fi) && (len(args) == 0 || args[0] != "-") { + log.FeedbackInfo( + cmd.Context(), + "Run 'defradb client query -' to read from stdin. Example: 'cat my.graphql | defradb client query -').", + ) + return nil + } else if len(args) == 0 { + err := cmd.Help() + if err != nil { + return errors.Wrap("failed to print help", err) + } + return nil + } else if args[0] == "-" { + stdin, err := readStdin() + if err != nil { + return errors.Wrap("failed to read stdin", err) + } + if len(stdin) == 0 { + return errors.New("no query request in stdin provided") + } else { + request = stdin + } } else { - request = stdin + request = args[0] } - } else { - request = args[0] - } - - if request == "" { - return errors.New("request cannot be empty") - } - - endpoint, err := httpapi.JoinPaths(cfg.API.AddressToURL(), httpapi.GraphQLPath) - if err != nil { - return errors.Wrap("joining paths failed", err) - } - - p := url.Values{} - p.Add("query", request) - endpoint.RawQuery = p.Encode() - - res, err := http.Get(endpoint.String()) - if err != nil { - return errors.Wrap("failed request", err) - } - - defer func() { - if e := res.Body.Close(); e != nil { - err = errors.Wrap(fmt.Sprintf("failed to read response body: %v", e.Error()), err) + + if request == "" { + return errors.New("request cannot be empty") } - }() - - response, err := io.ReadAll(res.Body) - if err != nil { - return errors.Wrap("failed to read response body", err) - } - - fi, err = os.Stdout.Stat() - if err != nil { - return errors.Wrap("failed to stat stdout", err) - } - - if isFileInfoPipe(fi) { - cmd.Println(string(response)) - } else { - graphlErr, err := hasGraphQLErrors(response) + + endpoint, err := httpapi.JoinPaths(cfg.API.AddressToURL(), httpapi.GraphQLPath) if err != nil { - return errors.Wrap("failed to handle GraphQL errors", err) + return errors.Wrap("joining paths failed", err) } - indentedResult, err := indentJSON(response) + + p := url.Values{} + p.Add("query", request) + endpoint.RawQuery = p.Encode() + + res, err := http.Get(endpoint.String()) + if err != nil { + return errors.Wrap("failed request", err) + } + + defer func() { + if e := res.Body.Close(); e != nil { + err = errors.Wrap(fmt.Sprintf("failed to read response body: %v", e.Error()), err) + } + }() + + response, err := io.ReadAll(res.Body) if err != nil { - return errors.Wrap("failed to pretty print result", err) + return errors.Wrap("failed to read response body", err) } - if graphlErr { - log.FeedbackError(cmd.Context(), indentedResult) + + fi, err = os.Stdout.Stat() + if err != nil { + return errors.Wrap("failed to stat stdout", err) + } + + if isFileInfoPipe(fi) { + cmd.Println(string(response)) } else { - log.FeedbackInfo(cmd.Context(), indentedResult) + graphlErr, err := hasGraphQLErrors(response) + if err != nil { + return errors.Wrap("failed to handle GraphQL errors", err) + } + indentedResult, err := indentJSON(response) + if err != nil { + return errors.Wrap("failed to pretty print result", err) + } + if graphlErr { + log.FeedbackError(cmd.Context(), indentedResult) + } else { + log.FeedbackInfo(cmd.Context(), indentedResult) + } } - } - return nil - }, -} + return nil + }, + } -func init() { - clientCmd.AddCommand(requestCmd) + return cmd } diff --git a/cli/root.go b/cli/root.go index 8a5695c782..396bd65278 100644 --- a/cli/root.go +++ b/cli/root.go @@ -12,7 +12,6 @@ package cli import ( "context" - "fmt" "github.com/spf13/cobra" @@ -20,101 +19,107 @@ import ( "github.com/sourcenetwork/defradb/errors" ) -var rootCmd = &cobra.Command{ - Use: "defradb", - Short: "DefraDB Edge Database", - Long: `DefraDB is the edge database to power the user-centric future. +func MakeRootCommand(cfg *config.Config) *cobra.Command { + var cmd = &cobra.Command{ + Use: "defradb", + Short: "DefraDB Edge Database", + Long: `DefraDB is the edge database to power the user-centric future. Start a database node, issue a request to a local or remote node, and much more. DefraDB is released under the BSL license, (c) 2022 Democratized Data Foundation. See https://docs.source.network/BSL.txt for more information. `, - // Runs on subcommands before their Run function, to handle configuration and top-level flags. - // Loads the rootDir containing the configuration file, otherwise warn about it and load a default configuration. - // This allows some subcommands (`init`, `start`) to override the PreRun to create a rootDir by default. - PersistentPreRunE: func(cmd *cobra.Command, _ []string) error { - if cfg.ConfigFileExists() { - if err := cfg.LoadWithRootdir(true); err != nil { - return errors.Wrap("failed to load config", err) + // Runs on subcommands before their Run function, to handle configuration and top-level flags. + // Loads the rootDir containing the configuration file, otherwise warn about it and load a default configuration. + // This allows some subcommands (`init`, `start`) to override the PreRun to create a rootDir by default. + PersistentPreRunE: func(cmd *cobra.Command, _ []string) error { + if cfg.ConfigFileExists() { + if err := cfg.LoadWithRootdir(true); err != nil { + return errors.Wrap("failed to load config", err) + } + // log.FeedbackInfo(cmd.Context(), fmt.Sprintf("Configuration loaded from DefraDB directory %v", cfg.Rootdir)) + } else { + if err := cfg.LoadWithRootdir(false); err != nil { + return errors.Wrap("failed to load config", err) + } + // log.FeedbackInfo(cmd.Context(), "Using default configuration") } - log.FeedbackInfo(cmd.Context(), fmt.Sprintf("Configuration loaded from DefraDB directory %v", cfg.Rootdir)) - } else { - if err := cfg.LoadWithRootdir(false); err != nil { - return errors.Wrap("failed to load config", err) - } - log.FeedbackInfo(cmd.Context(), "Using default configuration") - } - return nil - }, -} + return nil + }, + } -func init() { - rootCmd.PersistentFlags().StringVar( - &cfg.Rootdir, "rootdir", config.DefaultRootDir(), - "Directory for data and configuration to use", + cmd.PersistentFlags().String( + "rootdir", "", + "Directory for data and configuration to use (default: $HOME/.defradb)", ) + err := cfg.BindFlag(config.RootdirKey, cmd.PersistentFlags().Lookup("rootdir")) + if err != nil { + log.FeedbackFatalE(context.Background(), "Could not bind rootdir", err) + } - rootCmd.PersistentFlags().String( + cmd.PersistentFlags().String( "loglevel", cfg.Log.Level, "Log level to use. Options are debug, info, error, fatal", ) - err := cfg.BindFlag("log.level", rootCmd.PersistentFlags().Lookup("loglevel")) + err = cfg.BindFlag("log.level", cmd.PersistentFlags().Lookup("loglevel")) if err != nil { log.FeedbackFatalE(context.Background(), "Could not bind log.loglevel", err) } - rootCmd.PersistentFlags().StringArray( + cmd.PersistentFlags().StringArray( "logger", []string{}, "Override logger parameters. Usage: --logger ,level=,output=,...", ) - err = cfg.BindFlag("log.logger", rootCmd.PersistentFlags().Lookup("logger")) + err = cfg.BindFlag("log.logger", cmd.PersistentFlags().Lookup("logger")) if err != nil { log.FeedbackFatalE(context.Background(), "Could not bind log.logger", err) } - rootCmd.PersistentFlags().String( + cmd.PersistentFlags().String( "logoutput", cfg.Log.Output, "Log output path", ) - err = cfg.BindFlag("log.output", rootCmd.PersistentFlags().Lookup("logoutput")) + err = cfg.BindFlag("log.output", cmd.PersistentFlags().Lookup("logoutput")) if err != nil { log.FeedbackFatalE(context.Background(), "Could not bind log.output", err) } - rootCmd.PersistentFlags().String( + cmd.PersistentFlags().String( "logformat", cfg.Log.Format, "Log format to use. Options are csv, json", ) - err = cfg.BindFlag("log.format", rootCmd.PersistentFlags().Lookup("logformat")) + err = cfg.BindFlag("log.format", cmd.PersistentFlags().Lookup("logformat")) if err != nil { log.FeedbackFatalE(context.Background(), "Could not bind log.format", err) } - rootCmd.PersistentFlags().Bool( + cmd.PersistentFlags().Bool( "logtrace", cfg.Log.Stacktrace, "Include stacktrace in error and fatal logs", ) - err = cfg.BindFlag("log.stacktrace", rootCmd.PersistentFlags().Lookup("logtrace")) + err = cfg.BindFlag("log.stacktrace", cmd.PersistentFlags().Lookup("logtrace")) if err != nil { log.FeedbackFatalE(context.Background(), "Could not bind log.stacktrace", err) } - rootCmd.PersistentFlags().Bool( + cmd.PersistentFlags().Bool( "lognocolor", cfg.Log.NoColor, "Disable colored log output", ) - err = cfg.BindFlag("log.nocolor", rootCmd.PersistentFlags().Lookup("lognocolor")) + err = cfg.BindFlag("log.nocolor", cmd.PersistentFlags().Lookup("lognocolor")) if err != nil { log.FeedbackFatalE(context.Background(), "Could not bind log.nocolor", err) } - rootCmd.PersistentFlags().String( + cmd.PersistentFlags().String( "url", cfg.API.Address, "URL of HTTP endpoint to listen on or connect to", ) - err = cfg.BindFlag("api.address", rootCmd.PersistentFlags().Lookup("url")) + err = cfg.BindFlag("api.address", cmd.PersistentFlags().Lookup("url")) if err != nil { log.FeedbackFatalE(context.Background(), "Could not bind api.address", err) } + + return cmd } diff --git a/cli/rpc.go b/cli/rpc.go index bc4fedadd2..02caa055fb 100644 --- a/cli/rpc.go +++ b/cli/rpc.go @@ -14,21 +14,23 @@ import ( "context" "github.com/spf13/cobra" -) -var rpcCmd = &cobra.Command{ - Use: "rpc", - Short: "Interact with a DefraDB gRPC server", - Long: "Interact with a DefraDB gRPC server.", -} + "github.com/sourcenetwork/defradb/config" +) -func init() { - rpcCmd.PersistentFlags().String( +func MakeRPCCommand(cfg *config.Config) *cobra.Command { + var cmd = &cobra.Command{ + Use: "rpc", + Short: "Interact with a DefraDB gRPC server", + Long: "Interact with a DefraDB gRPC server.", + } + cmd.PersistentFlags().String( "addr", cfg.Net.RPCAddress, "gRPC endpoint address", ) - if err := cfg.BindFlag("net.rpcaddress", rpcCmd.PersistentFlags().Lookup("addr")); err != nil { + + if err := cfg.BindFlag("net.rpcaddress", cmd.PersistentFlags().Lookup("addr")); err != nil { log.FeedbackFatalE(context.Background(), "Could not bind net.rpcaddress", err) } - clientCmd.AddCommand(rpcCmd) + return cmd } diff --git a/cli/schema.go b/cli/schema.go index f64e38ce97..dc96539c71 100644 --- a/cli/schema.go +++ b/cli/schema.go @@ -14,12 +14,12 @@ import ( "github.com/spf13/cobra" ) -var schemaCmd = &cobra.Command{ - Use: "schema", - Short: "Interact with the schema system of a running DefraDB instance", - Long: "Make changes, updates, or look for existing schema types to a DefraDB node.", -} +func MakeSchemaCommand() *cobra.Command { + var cmd = &cobra.Command{ + Use: "schema", + Short: "Interact with the schema system of a running DefraDB instance", + Long: `Make changes, updates, or look for existing schema types to a DefraDB node.`, + } -func init() { - clientCmd.AddCommand(schemaCmd) + return cmd } diff --git a/cli/schema_add.go b/cli/schema_add.go index 585637f486..813a4a13c3 100644 --- a/cli/schema_add.go +++ b/cli/schema_add.go @@ -21,15 +21,16 @@ import ( "github.com/spf13/cobra" httpapi "github.com/sourcenetwork/defradb/api/http" + "github.com/sourcenetwork/defradb/config" "github.com/sourcenetwork/defradb/errors" ) -var schemaFile string - -var addCmd = &cobra.Command{ - Use: "add [schema]", - Short: "Add a new schema type to DefraDB", - Long: `Add a new schema type to DefraDB. +func MakeSchemaAddCommand(cfg *config.Config) *cobra.Command { + var schemaFile string + var cmd = &cobra.Command{ + Use: "add [schema]", + Short: "Add a new schema type to DefraDB", + Long: `Add a new schema type to DefraDB. Example: add from an argument string: defradb client schema add 'type Foo { ... }' @@ -40,115 +41,113 @@ Example: add from file: Example: add from stdin: cat schema.graphql | defradb client schema add - -To learn more about the DefraDB GraphQL Schema Language, refer to https://docs.source.network.`, - RunE: func(cmd *cobra.Command, args []string) (err error) { - var schema string - fi, err := os.Stdin.Stat() - if err != nil { - return err - } - - if len(args) > 1 { - if err = cmd.Usage(); err != nil { +Learn more about the DefraDB GraphQL Schema Language on https://docs.source.network.`, + RunE: func(cmd *cobra.Command, args []string) (err error) { + var schema string + fi, err := os.Stdin.Stat() + if err != nil { return err } - return errors.New("too many arguments") - } - if schemaFile != "" { - buf, err := os.ReadFile(schemaFile) - if err != nil { - return errors.Wrap("failed to read schema file", err) + if len(args) > 1 { + if err = cmd.Usage(); err != nil { + return err + } + return errors.New("too many arguments") } - schema = string(buf) - } else if isFileInfoPipe(fi) && (len(args) == 0 || args[0] != "-") { - log.FeedbackInfo( - cmd.Context(), - "Run 'defradb client schema add -' to read from stdin."+ - " Example: 'cat schema.graphql | defradb client schema add -').", - ) - return nil - } else if len(args) == 0 { - err := cmd.Help() - if err != nil { - return errors.Wrap("failed to print help", err) + + if schemaFile != "" { + buf, err := os.ReadFile(schemaFile) + if err != nil { + return errors.Wrap("failed to read schema file", err) + } + schema = string(buf) + } else if isFileInfoPipe(fi) && (len(args) == 0 || args[0] != "-") { + log.FeedbackInfo( + cmd.Context(), + "Run 'defradb client schema add -' to read from stdin."+ + " Example: 'cat schema.graphql | defradb client schema add -').", + ) + return nil + } else if len(args) == 0 { + err := cmd.Help() + if err != nil { + return errors.Wrap("failed to print help", err) + } + return nil + } else if args[0] == "-" { + stdin, err := readStdin() + if err != nil { + return errors.Wrap("failed to read stdin", err) + } + if len(stdin) == 0 { + return errors.New("no schema in stdin provided") + } else { + schema = stdin + } + } else { + schema = args[0] } - return nil - } else if args[0] == "-" { - stdin, err := readStdin() + + if schema == "" { + return errors.New("empty schema provided") + } + + endpoint, err := httpapi.JoinPaths(cfg.API.AddressToURL(), httpapi.SchemaLoadPath) if err != nil { - return errors.Wrap("failed to read stdin", err) + return errors.Wrap("join paths failed", err) } - if len(stdin) == 0 { - return errors.New("no schema in stdin provided") - } else { - schema = stdin + + res, err := http.Post(endpoint.String(), "text", strings.NewReader(schema)) + if err != nil { + return errors.Wrap("failed to post schema", err) } - } else { - schema = args[0] - } - - if schema == "" { - return errors.New("empty schema provided") - } - - endpoint, err := httpapi.JoinPaths(cfg.API.AddressToURL(), httpapi.SchemaLoadPath) - if err != nil { - return errors.Wrap("join paths failed", err) - } - - res, err := http.Post(endpoint.String(), "text", strings.NewReader(schema)) - if err != nil { - return errors.Wrap("failed to post schema", err) - } - - defer func() { - if e := res.Body.Close(); e != nil { - err = errors.Wrap(fmt.Sprintf("failed to read response body: %v", e.Error()), err) + + defer func() { + if e := res.Body.Close(); e != nil { + err = errors.Wrap(fmt.Sprintf("failed to read response body: %v", e.Error()), err) + } + }() + + response, err := io.ReadAll(res.Body) + if err != nil { + return errors.Wrap("failed to read response body", err) } - }() - - response, err := io.ReadAll(res.Body) - if err != nil { - return errors.Wrap("failed to read response body", err) - } - - stdout, err := os.Stdout.Stat() - if err != nil { - return errors.Wrap("failed to stat stdout", err) - } - if isFileInfoPipe(stdout) { - cmd.Println(string(response)) - } else { - graphlErr, err := hasGraphQLErrors(response) + + stdout, err := os.Stdout.Stat() if err != nil { - return errors.Wrap("failed to handle GraphQL errors", err) + return errors.Wrap("failed to stat stdout", err) } - if graphlErr { - indentedResult, err := indentJSON(response) - if err != nil { - return errors.Wrap("failed to pretty print result", err) - } - log.FeedbackError(cmd.Context(), indentedResult) + if isFileInfoPipe(stdout) { + cmd.Println(string(response)) } else { - type schemaResponse struct { - Data struct { - Result string `json:"result"` - } `json:"data"` - } - r := schemaResponse{} - err = json.Unmarshal(response, &r) + graphlErr, err := hasGraphQLErrors(response) if err != nil { - return errors.Wrap("failed to unmarshal response", err) + return errors.Wrap("failed to handle GraphQL errors", err) + } + if graphlErr { + indentedResult, err := indentJSON(response) + if err != nil { + return errors.Wrap("failed to pretty print result", err) + } + log.FeedbackError(cmd.Context(), indentedResult) + } else { + type schemaResponse struct { + Data struct { + Result string `json:"result"` + } `json:"data"` + } + r := schemaResponse{} + err = json.Unmarshal(response, &r) + if err != nil { + return errors.Wrap("failed to unmarshal response", err) + } + log.FeedbackInfo(cmd.Context(), r.Data.Result) } - log.FeedbackInfo(cmd.Context(), r.Data.Result) } - } - return nil - }, -} - -func init() { - schemaCmd.AddCommand(addCmd) - addCmd.Flags().StringVarP(&schemaFile, "file", "f", "", "File to load a schema from") + return nil + }, + } + cmd.Flags().StringVarP(&schemaFile, "file", "f", "", "File to load a schema from") + return cmd } diff --git a/cli/schema_patch.go b/cli/schema_patch.go index d13d2d6065..31ac830345 100644 --- a/cli/schema_patch.go +++ b/cli/schema_patch.go @@ -20,14 +20,16 @@ import ( "github.com/spf13/cobra" httpapi "github.com/sourcenetwork/defradb/api/http" + "github.com/sourcenetwork/defradb/config" ) -var patchFile string +func MakeSchemaPatchCommand(cfg *config.Config) *cobra.Command { + var patchFile string -var patchCmd = &cobra.Command{ - Use: "patch [schema]", - Short: "Patch an existing schema type", - Long: `Patch an existing schema. + var cmd = &cobra.Command{ + Use: "patch [schema]", + Short: "Patch an existing schema type", + Long: `Patch an existing schema. Uses JSON PATCH formatting as a DDL. @@ -41,111 +43,109 @@ Example: patch from stdin: cat patch.json | defradb client schema patch - To learn more about the DefraDB GraphQL Schema Language, refer to https://docs.source.network.`, - RunE: func(cmd *cobra.Command, args []string) (err error) { - var patch string - fi, err := os.Stdin.Stat() - if err != nil { - return err - } - - if len(args) > 1 { - if err = cmd.Usage(); err != nil { + RunE: func(cmd *cobra.Command, args []string) (err error) { + var patch string + fi, err := os.Stdin.Stat() + if err != nil { return err } - return ErrTooManyArgs - } - if patchFile != "" { - buf, err := os.ReadFile(patchFile) + if len(args) > 1 { + if err = cmd.Usage(); err != nil { + return err + } + return ErrTooManyArgs + } + + if patchFile != "" { + buf, err := os.ReadFile(patchFile) + if err != nil { + return NewFailedToReadFile(err) + } + patch = string(buf) + } else if isFileInfoPipe(fi) && (len(args) == 0 || args[0] != "-") { + log.FeedbackInfo( + cmd.Context(), + "Run 'defradb client schema patch -' to read from stdin."+ + " Example: 'cat patch.json | defradb client schema patch -').", + ) + return nil + } else if len(args) == 0 { + // ignore error, nothing we can do about it + // as printing an error about failing to print help + // is useless + //nolint:errcheck + cmd.Help() + return nil + } else if args[0] == "-" { + stdin, err := readStdin() + if err != nil { + return NewFailedToReadStdin(err) + } + if len(stdin) == 0 { + return ErrEmptyStdin + } else { + patch = stdin + } + } else { + patch = args[0] + } + + if patch == "" { + return ErrEmptyFile + } + + endpoint, err := httpapi.JoinPaths(cfg.API.AddressToURL(), httpapi.SchemaPatchPath) if err != nil { - return NewFailedToReadFile(err) + return err } - patch = string(buf) - } else if isFileInfoPipe(fi) && (len(args) == 0 || args[0] != "-") { - log.FeedbackInfo( - cmd.Context(), - "Run 'defradb client schema patch -' to read from stdin."+ - " Example: 'cat patch.json | defradb client schema patch -').", - ) - return nil - } else if len(args) == 0 { - // ignore error, nothing we can do about it - // as printing an error about failing to print help - // is useless - //nolint:errcheck - cmd.Help() - return nil - } else if args[0] == "-" { - stdin, err := readStdin() + + res, err := http.Post(endpoint.String(), "text", strings.NewReader(patch)) if err != nil { - return NewFailedToReadStdin(err) + return NewErrFailedToSendRequest(err) } - if len(stdin) == 0 { - return ErrEmptyStdin - } else { - patch = stdin + + //nolint:errcheck + defer res.Body.Close() + response, err := io.ReadAll(res.Body) + if err != nil { + return NewErrFailedToReadResponseBody(err) } - } else { - patch = args[0] - } - - if patch == "" { - return ErrEmptyFile - } - - endpoint, err := httpapi.JoinPaths(cfg.API.AddressToURL(), httpapi.SchemaPatchPath) - if err != nil { - return err - } - - res, err := http.Post(endpoint.String(), "text", strings.NewReader(patch)) - if err != nil { - return NewErrFailedToSendRequest(err) - } - - //nolint:errcheck - defer res.Body.Close() - response, err := io.ReadAll(res.Body) - if err != nil { - return NewErrFailedToReadResponseBody(err) - } - - stdout, err := os.Stdout.Stat() - if err != nil { - return NewErrFailedToStatStdOut(err) - } - if isFileInfoPipe(stdout) { - cmd.Println(string(response)) - } else { - graphlErr, err := hasGraphQLErrors(response) + + stdout, err := os.Stdout.Stat() if err != nil { - return NewErrFailedToHandleGQLErrors(err) + return NewErrFailedToStatStdOut(err) } - if graphlErr { - indentedResult, err := indentJSON(response) - if err != nil { - return NewErrFailedToPrettyPrintResponse(err) - } - log.FeedbackError(cmd.Context(), indentedResult) + if isFileInfoPipe(stdout) { + cmd.Println(string(response)) } else { - type schemaResponse struct { - Data struct { - Result string `json:"result"` - } `json:"data"` - } - r := schemaResponse{} - err = json.Unmarshal(response, &r) + graphlErr, err := hasGraphQLErrors(response) if err != nil { - return NewErrFailedToUnmarshalResponse(err) + return NewErrFailedToHandleGQLErrors(err) + } + if graphlErr { + indentedResult, err := indentJSON(response) + if err != nil { + return NewErrFailedToPrettyPrintResponse(err) + } + log.FeedbackError(cmd.Context(), indentedResult) + } else { + type schemaResponse struct { + Data struct { + Result string `json:"result"` + } `json:"data"` + } + r := schemaResponse{} + err = json.Unmarshal(response, &r) + if err != nil { + return NewErrFailedToUnmarshalResponse(err) + } + log.FeedbackInfo(cmd.Context(), r.Data.Result) } - log.FeedbackInfo(cmd.Context(), r.Data.Result) } - } - return nil - }, -} - -func init() { - schemaCmd.AddCommand(patchCmd) - patchCmd.Flags().StringVarP(&patchFile, "file", "f", "", "File to load a patch from") + return nil + }, + } + cmd.Flags().StringVarP(&patchFile, "file", "f", "", "File to load a patch from") + return cmd } diff --git a/cli/serverdump.go b/cli/serverdump.go index b7d8670eee..2888b39a8e 100644 --- a/cli/serverdump.go +++ b/cli/serverdump.go @@ -17,6 +17,7 @@ import ( "github.com/spf13/cobra" + "github.com/sourcenetwork/defradb/config" ds "github.com/sourcenetwork/defradb/datastore" badgerds "github.com/sourcenetwork/defradb/datastore/badger/v3" "github.com/sourcenetwork/defradb/db" @@ -24,52 +25,51 @@ import ( "github.com/sourcenetwork/defradb/logging" ) -var datastore string +func MakeServerDumpCmd(cfg *config.Config) *cobra.Command { + var datastore string -var serverDumpCmd = &cobra.Command{ - Use: "server-dump", - Short: "Dumps the state of the entire database", - RunE: func(cmd *cobra.Command, _ []string) error { - log.FeedbackInfo(cmd.Context(), "Starting DefraDB process...") + cmd := &cobra.Command{ + Use: "server-dump", + Short: "Dumps the state of the entire database", + RunE: func(cmd *cobra.Command, _ []string) error { + log.FeedbackInfo(cmd.Context(), "Starting DefraDB process...") - // setup signal handlers - signalCh := make(chan os.Signal, 1) - signal.Notify(signalCh, os.Interrupt) + // setup signal handlers + signalCh := make(chan os.Signal, 1) + signal.Notify(signalCh, os.Interrupt) - var rootstore ds.RootStore - var err error - if datastore == badgerDatastoreName { - info, err := os.Stat(cfg.Datastore.Badger.Path) - exists := (err == nil && info.IsDir()) - if !exists { - return errors.New(fmt.Sprintf( - "badger store does not exist at %s. Try with an existing directory", - cfg.Datastore.Badger.Path, - )) + var rootstore ds.RootStore + var err error + if datastore == badgerDatastoreName { + info, err := os.Stat(cfg.Datastore.Badger.Path) + exists := (err == nil && info.IsDir()) + if !exists { + return errors.New(fmt.Sprintf( + "badger store does not exist at %s. Try with an existing directory", + cfg.Datastore.Badger.Path, + )) + } + log.FeedbackInfo(cmd.Context(), "Opening badger store", logging.NewKV("Path", cfg.Datastore.Badger.Path)) + rootstore, err = badgerds.NewDatastore(cfg.Datastore.Badger.Path, cfg.Datastore.Badger.Options) + if err != nil { + return errors.Wrap("could not open badger datastore", err) + } + } else { + return errors.New("server-side dump is only supported for the Badger datastore") } - log.FeedbackInfo(cmd.Context(), "Opening badger store", logging.NewKV("Path", cfg.Datastore.Badger.Path)) - rootstore, err = badgerds.NewDatastore(cfg.Datastore.Badger.Path, cfg.Datastore.Badger.Options) + + db, err := db.NewDB(cmd.Context(), rootstore) if err != nil { - return errors.Wrap("could not open badger datastore", err) + return errors.Wrap("failed to initialize database", err) } - } else { - return errors.New("server-side dump is only supported for the Badger datastore") - } - - db, err := db.NewDB(cmd.Context(), rootstore) - if err != nil { - return errors.Wrap("failed to initialize database", err) - } - - log.FeedbackInfo(cmd.Context(), "Dumping DB state...") - return db.PrintDump(cmd.Context()) - }, -} -func init() { - rootCmd.AddCommand(serverDumpCmd) - serverDumpCmd.Flags().StringVar( + log.FeedbackInfo(cmd.Context(), "Dumping DB state...") + return db.PrintDump(cmd.Context()) + }, + } + cmd.Flags().StringVar( &datastore, "store", cfg.Datastore.Store, "Datastore to use. Options are badger, memory", ) + return cmd } diff --git a/cli/start.go b/cli/start.go index 6bdf0595ae..3ac785bf12 100644 --- a/cli/start.go +++ b/cli/start.go @@ -41,147 +41,143 @@ import ( "github.com/sourcenetwork/defradb/node" ) -var startCmd = &cobra.Command{ - Use: "start", - Short: "Start a DefraDB node", - Long: "Start a new instance of DefraDB node.", - // Load the root config if it exists, otherwise create it. - PersistentPreRunE: func(cmd *cobra.Command, _ []string) error { - if cfg.ConfigFileExists() { - if err := cfg.LoadWithRootdir(true); err != nil { - return errors.Wrap("failed to load config", err) - } - } else { - if err := cfg.LoadWithRootdir(false); err != nil { - return errors.Wrap("failed to load config", err) - } - if config.FolderExists(cfg.Rootdir) { - if err := cfg.WriteConfigFile(); err != nil { - return err +func MakeStartCommand(cfg *config.Config) *cobra.Command { + var cmd = &cobra.Command{ + Use: "start", + Short: "Start a DefraDB node", + Long: "Start a new instance of DefraDB node.", + // Load the root config if it exists, otherwise create it. + PersistentPreRunE: func(cmd *cobra.Command, _ []string) error { + if cfg.ConfigFileExists() { + if err := cfg.LoadWithRootdir(true); err != nil { + return config.NewErrLoadingConfig(err) } } else { - if err := cfg.CreateRootDirAndConfigFile(); err != nil { - return err + if err := cfg.LoadWithRootdir(false); err != nil { + return config.NewErrLoadingConfig(err) + } + if config.FolderExists(cfg.Rootdir) { + if err := cfg.WriteConfigFile(); err != nil { + return err + } + log.FeedbackInfo(cmd.Context(), fmt.Sprintf("Configuration loaded from DefraDB directory %v", cfg.Rootdir)) + } else { + if err := cfg.CreateRootDirAndConfigFile(); err != nil { + return err + } } } - } - log.FeedbackInfo(cmd.Context(), fmt.Sprintf("Configuration loaded from DefraDB directory %v", cfg.Rootdir)) - - return nil - }, - RunE: func(cmd *cobra.Command, args []string) error { - di, err := start(cmd.Context()) - if err != nil { - return err - } - - wait(cmd.Context(), di) + return nil + }, + RunE: func(cmd *cobra.Command, args []string) error { + di, err := start(cmd.Context(), cfg) + if err != nil { + return err + } - return nil - }, -} + return wait(cmd.Context(), di) + }, + } -func init() { - startCmd.Flags().String( + cmd.Flags().String( "peers", cfg.Net.Peers, "List of peers to connect to", ) - err := cfg.BindFlag("net.peers", startCmd.Flags().Lookup("peers")) + err := cfg.BindFlag("net.peers", cmd.Flags().Lookup("peers")) if err != nil { log.FeedbackFatalE(context.Background(), "Could not bind net.peers", err) } - startCmd.Flags().Int( + cmd.Flags().Int( "max-txn-retries", cfg.Datastore.MaxTxnRetries, "Specify the maximum number of retries per transaction", ) - err = cfg.BindFlag("datastore.maxtxnretries", startCmd.Flags().Lookup("max-txn-retries")) + err = cfg.BindFlag("datastore.maxtxnretries", cmd.Flags().Lookup("max-txn-retries")) if err != nil { log.FeedbackFatalE(context.Background(), "Could not bind datastore.maxtxnretries", err) } - startCmd.Flags().String( + cmd.Flags().String( "store", cfg.Datastore.Store, "Specify the datastore to use (supported: badger, memory)", ) - err = cfg.BindFlag("datastore.store", startCmd.Flags().Lookup("store")) + err = cfg.BindFlag("datastore.store", cmd.Flags().Lookup("store")) if err != nil { log.FeedbackFatalE(context.Background(), "Could not bind datastore.store", err) } - startCmd.Flags().Var( + cmd.Flags().Var( &cfg.Datastore.Badger.ValueLogFileSize, "valuelogfilesize", "Specify the datastore value log file size (in bytes). In memory size will be 2*valuelogfilesize", ) - err = cfg.BindFlag("datastore.badger.valuelogfilesize", startCmd.Flags().Lookup("valuelogfilesize")) + err = cfg.BindFlag("datastore.badger.valuelogfilesize", cmd.Flags().Lookup("valuelogfilesize")) if err != nil { log.FeedbackFatalE(context.Background(), "Could not bind datastore.badger.valuelogfilesize", err) } - startCmd.Flags().String( + cmd.Flags().String( "p2paddr", cfg.Net.P2PAddress, "Listener address for the p2p network (formatted as a libp2p MultiAddr)", ) - err = cfg.BindFlag("net.p2paddress", startCmd.Flags().Lookup("p2paddr")) + err = cfg.BindFlag("net.p2paddress", cmd.Flags().Lookup("p2paddr")) if err != nil { log.FeedbackFatalE(context.Background(), "Could not bind net.p2paddress", err) } - startCmd.Flags().String( + cmd.Flags().String( "tcpaddr", cfg.Net.TCPAddress, "Listener address for the tcp gRPC server (formatted as a libp2p MultiAddr)", ) - err = cfg.BindFlag("net.tcpaddress", startCmd.Flags().Lookup("tcpaddr")) + err = cfg.BindFlag("net.tcpaddress", cmd.Flags().Lookup("tcpaddr")) if err != nil { log.FeedbackFatalE(context.Background(), "Could not bind net.tcpaddress", err) } - startCmd.Flags().Bool( + cmd.Flags().Bool( "no-p2p", cfg.Net.P2PDisabled, "Disable the peer-to-peer network synchronization system", ) - err = cfg.BindFlag("net.p2pdisabled", startCmd.Flags().Lookup("no-p2p")) + err = cfg.BindFlag("net.p2pdisabled", cmd.Flags().Lookup("no-p2p")) if err != nil { log.FeedbackFatalE(context.Background(), "Could not bind net.p2pdisabled", err) } - startCmd.Flags().Bool( + cmd.Flags().Bool( "tls", cfg.API.TLS, "Enable serving the API over https", ) - err = cfg.BindFlag("api.tls", startCmd.Flags().Lookup("tls")) + err = cfg.BindFlag("api.tls", cmd.Flags().Lookup("tls")) if err != nil { log.FeedbackFatalE(context.Background(), "Could not bind api.tls", err) } - startCmd.Flags().String( + cmd.Flags().String( "pubkeypath", cfg.API.PubKeyPath, "Path to the public key for tls", ) - err = cfg.BindFlag("api.pubkeypath", startCmd.Flags().Lookup("pubkeypath")) + err = cfg.BindFlag("api.pubkeypath", cmd.Flags().Lookup("pubkeypath")) if err != nil { log.FeedbackFatalE(context.Background(), "Could not bind api.pubkeypath", err) } - startCmd.Flags().String( + cmd.Flags().String( "privkeypath", cfg.API.PrivKeyPath, "Path to the private key for tls", ) - err = cfg.BindFlag("api.privkeypath", startCmd.Flags().Lookup("privkeypath")) + err = cfg.BindFlag("api.privkeypath", cmd.Flags().Lookup("privkeypath")) if err != nil { log.FeedbackFatalE(context.Background(), "Could not bind api.privkeypath", err) } - startCmd.Flags().String( + cmd.Flags().String( "email", cfg.API.Email, "Email address used by the CA for notifications", ) - err = cfg.BindFlag("api.email", startCmd.Flags().Lookup("email")) + err = cfg.BindFlag("api.email", cmd.Flags().Lookup("email")) if err != nil { log.FeedbackFatalE(context.Background(), "Could not bind api.email", err) } - - rootCmd.AddCommand(startCmd) + return cmd } type defraInstance struct { @@ -210,7 +206,7 @@ func (di *defraInstance) close(ctx context.Context) { } } -func start(ctx context.Context) (*defraInstance, error) { +func start(ctx context.Context, cfg *config.Config) (*defraInstance, error) { log.FeedbackInfo(ctx, "Starting DefraDB service...") var rootstore ds.RootStore @@ -372,13 +368,19 @@ func start(ctx context.Context) (*defraInstance, error) { } // wait waits for an interrupt signal to close the program. -func wait(ctx context.Context, di *defraInstance) { +func wait(ctx context.Context, di *defraInstance) error { // setup signal handlers signalCh := make(chan os.Signal, 1) signal.Notify(signalCh, os.Interrupt) - <-signalCh - log.FeedbackInfo(ctx, "Received interrupt; closing database...") - di.close(ctx) - os.Exit(0) + select { + case <-ctx.Done(): + log.FeedbackInfo(ctx, "Received context cancellation; closing database...") + di.close(ctx) + return ctx.Err() + case <-signalCh: + log.FeedbackInfo(ctx, "Received interrupt; closing database...") + di.close(ctx) + return ctx.Err() + } } diff --git a/cli/version.go b/cli/version.go index 96a9ff1b0f..8842697699 100644 --- a/cli/version.go +++ b/cli/version.go @@ -20,42 +20,40 @@ import ( "github.com/sourcenetwork/defradb/version" ) -var format string -var full bool - -var versionCmd = &cobra.Command{ - Use: "version", - Short: "Display the version information of DefraDB and its components", - RunE: func(cmd *cobra.Command, _ []string) error { - dv, err := version.NewDefraVersion() - if err != nil { - return err - } - switch format { - case "json": - var buf bytes.Buffer - dvj, err := json.Marshal(dv) - if err != nil { - return err - } - err = json.Indent(&buf, dvj, "", " ") +func MakeVersionCommand() *cobra.Command { + var format string + var full bool + var cmd = &cobra.Command{ + Use: "version", + Short: "Display the version information of DefraDB and its components", + RunE: func(cmd *cobra.Command, _ []string) error { + dv, err := version.NewDefraVersion() if err != nil { return err } - cmd.Println(buf.String()) - default: - if full { - cmd.Println(dv.StringFull()) - } else { - cmd.Println(dv.String()) + switch format { + case "json": + var buf bytes.Buffer + dvj, err := json.Marshal(dv) + if err != nil { + return err + } + err = json.Indent(&buf, dvj, "", " ") + if err != nil { + return err + } + cmd.Println(buf.String()) + default: + if full { + cmd.Println(dv.StringFull()) + } else { + cmd.Println(dv.String()) + } } - } - return nil - }, -} - -func init() { - versionCmd.Flags().StringVarP(&format, "format", "f", "", "Version output format. Options are text, json") - versionCmd.Flags().BoolVarP(&full, "full", "", false, "Display the full version information") - rootCmd.AddCommand(versionCmd) + return nil + }, + } + cmd.Flags().StringVarP(&format, "format", "f", "", "Version output format. Options are text, json") + cmd.Flags().BoolVarP(&full, "full", "", false, "Display the full version information") + return cmd } From 7090816a8d13574c5f9f2153f5e3474a6cc1a861 Mon Sep 17 00:00:00 2001 From: Orpheus Lummis Date: Mon, 24 Apr 2023 00:16:30 -0400 Subject: [PATCH 2/6] cli unit tests --- cli/cli_test.go | 59 ++++++++++++++++++++++++++++++ cli/peerid_test.go | 8 +++- cli/version_test.go | 89 +++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 154 insertions(+), 2 deletions(-) create mode 100644 cli/cli_test.go create mode 100644 cli/version_test.go diff --git a/cli/cli_test.go b/cli/cli_test.go new file mode 100644 index 0000000000..877dd7b69f --- /dev/null +++ b/cli/cli_test.go @@ -0,0 +1,59 @@ +// Copyright 2022 Democratized Data Foundation +// +// Use of this software is governed by the Business Source License +// included in the file licenses/BSL.txt. +// +// As of the Change Date specified in that file, in accordance with +// the Business Source License, use of this software will be governed +// by the Apache License, Version 2.0, included in the file +// licenses/APL.txt. + +package cli + +import ( + "testing" + + "github.com/spf13/cobra" + "github.com/stretchr/testify/assert" + + "github.com/sourcenetwork/defradb/config" +) + +// Verify that the top-level commands are registered, and if particular ones have subcommands. +func TestNewDefraCommand(t *testing.T) { + expectedCommandNames := []string{ + "client", + "init", + "server-dump", + "start", + "version", + } + actualCommandNames := []string{} + r := NewDefraCommand(config.DefaultConfig()) + for _, c := range r.RootCmd.Commands() { + actualCommandNames = append(actualCommandNames, c.Name()) + } + for _, expectedCommandName := range expectedCommandNames { + assert.Contains(t, actualCommandNames, expectedCommandName) + } + for _, c := range r.RootCmd.Commands() { + if c.Name() == "client" { + assert.NotEmpty(t, c.Commands()) + } + } +} + +func TestAllHaveUsage(t *testing.T) { + cfg := config.DefaultConfig() + defra := NewDefraCommand(cfg) + walkCommandTree(t, defra.RootCmd, func(c *cobra.Command) { + assert.NotEmpty(t, c.Use) + }) +} + +func walkCommandTree(t *testing.T, cmd *cobra.Command, f func(*cobra.Command)) { + f(cmd) + for _, c := range cmd.Commands() { + walkCommandTree(t, c, f) + } +} diff --git a/cli/peerid_test.go b/cli/peerid_test.go index 8095f9f8b7..85605d3566 100644 --- a/cli/peerid_test.go +++ b/cli/peerid_test.go @@ -34,6 +34,8 @@ func setTestingAddresses(cfg *config.Config) { } func TestGetPeerIDCmd(t *testing.T) { + cfg := config.DefaultConfig() + peerIDCmd := MakePeerIDCommand(cfg) dir := t.TempDir() ctx := context.Background() cfg.Datastore.Store = "memory" @@ -41,7 +43,7 @@ func TestGetPeerIDCmd(t *testing.T) { cfg.Net.P2PDisabled = false setTestingAddresses(cfg) - di, err := start(ctx) + di, err := start(ctx, cfg) if err != nil { t.Fatal(err) } @@ -70,6 +72,8 @@ func TestGetPeerIDCmd(t *testing.T) { } func TestGetPeerIDCmdWithNoP2P(t *testing.T) { + cfg := config.DefaultConfig() + peerIDCmd := MakePeerIDCommand(cfg) dir := t.TempDir() ctx := context.Background() cfg.Datastore.Store = "memory" @@ -77,7 +81,7 @@ func TestGetPeerIDCmdWithNoP2P(t *testing.T) { cfg.Net.P2PDisabled = true setTestingAddresses(cfg) - di, err := start(ctx) + di, err := start(ctx, cfg) if err != nil { t.Fatal(err) } diff --git a/cli/version_test.go b/cli/version_test.go new file mode 100644 index 0000000000..4f62f3659b --- /dev/null +++ b/cli/version_test.go @@ -0,0 +1,89 @@ +// Copyright 2022 Democratized Data Foundation +// +// Use of this software is governed by the Business Source License +// included in the file licenses/BSL.txt. +// +// As of the Change Date specified in that file, in accordance with +// the Business Source License, use of this software will be governed +// by the Apache License, Version 2.0, included in the file +// licenses/APL.txt. + +package cli + +import ( + "bytes" + "testing" + + "github.com/stretchr/testify/assert" +) + +// The version information comes from the build process which is not [easily] accessible from unit tests. +// Therefore we test that the command outputs the expected formats *without the version info*. + +// case: no args, meaning `--format text` +func TestVersionNoArg(t *testing.T) { + cmd := MakeVersionCommand() + buf := new(bytes.Buffer) + cmd.SetOut(buf) + err := cmd.Execute() + assert.NoError(t, err) + t.Log(buf.String()) + assert.Contains(t, buf.String(), "defradb") + assert.Contains(t, buf.String(), "built with Go") +} + +// case: `--full`, meaning `--format text --full` +func TestVersionFull(t *testing.T) { + cmd := MakeVersionCommand() + buf := new(bytes.Buffer) + cmd.SetOut(buf) + cmd.SetArgs([]string{"--full"}) + err := cmd.Execute() + assert.NoError(t, err) + t.Log(buf.String()) + assert.Contains(t, buf.String(), "* HTTP API") + assert.Contains(t, buf.String(), "* DocKey versions") + assert.Contains(t, buf.String(), "* P2P multicodec") +} + +// case: `--format json` +func TestVersionJSON(t *testing.T) { + cmd := MakeVersionCommand() + buf := new(bytes.Buffer) + cmd.SetOut(buf) + cmd.SetArgs([]string{"--format", "json"}) + err := cmd.Execute() + assert.NoError(t, err) + t.Log(buf.String()) + assert.JSONEq(t, buf.String(), ` + { + "release": "", + "commit": "", + "commitdate": "", + "go": "", + "httpapi": "v0", + "dockeyversions": "1", + "netprotocol": "/defra/0.0.1" + }`) +} + +// case: `--format json --full` (is equivalent to previous one) +func TestVersionJSONFull(t *testing.T) { + cmd := MakeVersionCommand() + buf := new(bytes.Buffer) + cmd.SetOut(buf) + cmd.SetArgs([]string{"--format", "json", "--full"}) + err := cmd.Execute() + assert.NoError(t, err) + t.Log(buf.String()) + assert.JSONEq(t, buf.String(), ` + { + "release": "", + "commit": "", + "commitdate": "", + "go": "", + "httpapi": "v0", + "dockeyversions": "1", + "netprotocol": "/defra/0.0.1" + }`) +} From 5fa339963c0219162ebf2a44398d03837b4b1c04 Mon Sep 17 00:00:00 2001 From: Orpheus Lummis Date: Mon, 24 Apr 2023 00:16:49 -0400 Subject: [PATCH 3/6] update programs in cmd directory --- cmd/defradb/main.go | 15 +++++++++++++-- cmd/genclidocs/genclidocs.go | 6 ++++-- cmd/genmanpages/main.go | 4 +++- 3 files changed, 20 insertions(+), 5 deletions(-) diff --git a/cmd/defradb/main.go b/cmd/defradb/main.go index 2f8208f781..761666bea7 100644 --- a/cmd/defradb/main.go +++ b/cmd/defradb/main.go @@ -11,9 +11,20 @@ // defradb is a decentralized peer-to-peer, user-centric, privacy-focused document database. package main -import "github.com/sourcenetwork/defradb/cli" +import ( + "context" + "os" + + "github.com/sourcenetwork/defradb/cli" + "github.com/sourcenetwork/defradb/config" +) // Execute adds all child commands to the root command and sets flags appropriately. func main() { - cli.Execute() + cfg := config.DefaultConfig() + ctx := context.Background() + defraCmd := cli.NewDefraCommand(cfg) + if err := defraCmd.Execute(ctx); err != nil { + os.Exit(1) + } } diff --git a/cmd/genclidocs/genclidocs.go b/cmd/genclidocs/genclidocs.go index f644a63f6b..10a7961b95 100644 --- a/cmd/genclidocs/genclidocs.go +++ b/cmd/genclidocs/genclidocs.go @@ -21,6 +21,7 @@ import ( "github.com/spf13/cobra/doc" "github.com/sourcenetwork/defradb/cli" + "github.com/sourcenetwork/defradb/config" "github.com/sourcenetwork/defradb/logging" ) @@ -33,8 +34,9 @@ func main() { if err != nil { log.FatalE(context.Background(), "Creating the filesystem path failed", err) } - cli.RootCmd.DisableAutoGenTag = true - err = doc.GenMarkdownTree(cli.RootCmd, *path) + defraCmd := cli.NewDefraCommand(config.DefaultConfig()) + defraCmd.RootCmd.DisableAutoGenTag = true + err = doc.GenMarkdownTree(defraCmd.RootCmd, *path) if err != nil { log.FatalE(context.Background(), "Generating cmd docs failed", err) } diff --git a/cmd/genmanpages/main.go b/cmd/genmanpages/main.go index fe3f36d324..1d7ecb5dff 100644 --- a/cmd/genmanpages/main.go +++ b/cmd/genmanpages/main.go @@ -22,6 +22,7 @@ import ( "github.com/spf13/cobra/doc" "github.com/sourcenetwork/defradb/cli" + "github.com/sourcenetwork/defradb/config" "github.com/sourcenetwork/defradb/logging" ) @@ -45,7 +46,8 @@ func genRootManPages(dir string) { if err != nil { log.FatalE(ctx, "Failed to create directory", err, logging.NewKV("dir", dir)) } - err = doc.GenManTree(cli.RootCmd, header, dir) + defraCmd := cli.NewDefraCommand(config.DefaultConfig()) + err = doc.GenManTree(defraCmd.RootCmd, header, dir) if err != nil { log.FatalE(ctx, "Failed generation of man pages", err) } From 12ec2af2059573f0fb5cdb9e1541ebeecb9affe5 Mon Sep 17 00:00:00 2001 From: Orpheus Lummis Date: Mon, 24 Apr 2023 00:17:10 -0400 Subject: [PATCH 4/6] config is updated too --- config/config.go | 87 ++++++++++++++++++++++++--------------- config/config_test.go | 43 +++++++++++-------- config/configfile_test.go | 17 +++++--- config/errors.go | 6 +++ 4 files changed, 97 insertions(+), 56 deletions(-) diff --git a/config/config.go b/config/config.go index dbcfc54ab5..b827b759d1 100644 --- a/config/config.go +++ b/config/config.go @@ -68,6 +68,7 @@ var log = logging.MustNewLogger("defra.config") const ( DefaultAPIEmail = "example@example.com" + RootdirKey = "rootdircli" defraEnvPrefix = "DEFRA" logLevelDebug = "debug" logLevelInfo = "info" @@ -85,16 +86,33 @@ type Config struct { v *viper.Viper } -// DefaultConfig returns the default configuration. +// DefaultConfig returns the default configuration (or panics). func DefaultConfig() *Config { - return &Config{ + cfg := &Config{ Datastore: defaultDatastoreConfig(), API: defaultAPIConfig(), Net: defaultNetConfig(), Log: defaultLogConfig(), - Rootdir: DefaultRootDir(), + Rootdir: "", v: viper.New(), } + + cfg.v.SetEnvPrefix(defraEnvPrefix) + cfg.v.SetEnvKeyReplacer(strings.NewReplacer(".", "_")) + + cfg.v.SetConfigName(DefaultConfigFileName) + cfg.v.SetConfigType(configType) + + // Load default values in viper. + b, err := cfg.toBytes() + if err != nil { + panic(err) + } + if err = cfg.v.ReadConfig(bytes.NewReader(b)); err != nil { + panic(NewErrReadingConfigFile(err)) + } + + return cfg } // LoadWithRootdir loads a Config with parameters from defaults, config file, environment variables, and CLI flags. @@ -102,8 +120,6 @@ func DefaultConfig() *Config { // Use on a Config struct already loaded with default values from DefaultConfig(). // To be executed once at the beginning of the program. func (cfg *Config) LoadWithRootdir(withRootdir bool) error { - var err error - // Use default logging configuration here, so that // we can log errors in a consistent way even in the case of early failure. defaultLogCfg := defaultLogConfig() @@ -111,18 +127,11 @@ func (cfg *Config) LoadWithRootdir(withRootdir bool) error { return err } - if err := cfg.loadDefaultViper(); err != nil { - return err - } - - // using absolute rootdir for robustness. - cfg.Rootdir, err = filepath.Abs(cfg.Rootdir) - if err != nil { + if err := cfg.loadRootDirFromFlagOrDefault(); err != nil { return err } if withRootdir { - cfg.v.AddConfigPath(cfg.Rootdir) if err := cfg.v.ReadInConfig(); err != nil { return NewErrReadingConfigFile(err) } @@ -147,28 +156,31 @@ func (cfg *Config) LoadWithRootdir(withRootdir bool) error { return nil } -func (cfg *Config) loadDefaultViper() error { - // for our DEFRA_ env vars - cfg.v.SetEnvPrefix(defraEnvPrefix) - cfg.v.SetEnvKeyReplacer(strings.NewReplacer(".", "_")) - // for now, only one type with a specific filename supported - cfg.v.SetConfigName(DefaultConfigFileName) - // To support a minimal configuration, we load and bind the default config first. - cfg.v.SetConfigType(configType) - /* - We load the default config into the viper instance, from a default template, so that viper - can detect the environment variables that are set. This is because viper only detects environment - variables that are present in the config file (`AutomaticEnv`). So we load the default config into viper, - and then overwrite it with the actual config file. - */ - defaultConfig := DefaultConfig() - defaultConfigBytes, err := defaultConfig.toBytes() +func (cfg *Config) loadRootDirFromFlagOrDefault() error { + if cfg.Rootdir == "" { + // Check CLI flag + rootdir := cfg.v.GetString(RootdirKey) + if rootdir != "" { + return cfg.setRootdir(rootdir) + } + + return cfg.setRootdir(DefaultRootDir()) + } + + return nil +} + +func (cfg *Config) setRootdir(rootdir string) error { + var err error + if rootdir == "" { + return NewErrInvalidRootDir(rootdir) + } + // using absolute rootdir for robustness. + cfg.Rootdir, err = filepath.Abs(rootdir) if err != nil { return err } - if err = cfg.v.ReadConfig(bytes.NewReader(defaultConfigBytes)); err != nil { - return NewErrReadingConfigFile(err) - } + cfg.v.AddConfigPath(cfg.Rootdir) return nil } @@ -713,7 +725,7 @@ func (cfg *Config) BindFlag(key string, flag *pflag.Flag) error { return cfg.v.BindPFlag(key, flag) } -// ToJSON serializes the config to a JSON string. +// ToJSON serializes the config to a JSON byte array. func (c *Config) ToJSON() ([]byte, error) { jsonbytes, err := json.Marshal(c) if err != nil { @@ -722,6 +734,15 @@ func (c *Config) ToJSON() ([]byte, error) { return jsonbytes, nil } +// String serializes the config to a JSON string. +func (c *Config) String() string { + jsonbytes, err := c.ToJSON() + if err != nil { + return fmt.Sprintf("failed to convert config to string: %s", err) + } + return string(jsonbytes) +} + func (c *Config) toBytes() ([]byte, error) { var buffer bytes.Buffer tmpl := template.New("configTemplate") diff --git a/config/config_test.go b/config/config_test.go index 2337a39eda..2ed3a3dec3 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -23,7 +23,7 @@ import ( "github.com/sourcenetwork/defradb/node" ) -var envVarsDifferentThanDefault = map[string]string{ +var envVarsDifferent = map[string]string{ "DEFRA_DATASTORE_STORE": "memory", "DEFRA_DATASTORE_BADGER_PATH": "defra_data", "DEFRA_API_ADDRESS": "localhost:9999", @@ -73,9 +73,10 @@ func FixtureEnvVars(t *testing.T, envVars map[string]string) { }) } -func TestDefaultConfig(t *testing.T) { +func TestConfigValidateBasic(t *testing.T) { cfg := DefaultConfig() - assert.NotNil(t, cfg) + assert.NoError(t, cfg.validate()) + err := cfg.validate() assert.NoError(t, err) // asserting equality of some unlikely-to-change default values @@ -115,9 +116,10 @@ func TestLoadIncorrectValuesFromConfigFile(t *testing.T) { for _, tc := range testcases { cfg = DefaultConfig() - cfg.Rootdir = t.TempDir() + err := cfg.setRootdir(t.TempDir()) + assert.NoError(t, err) tc.setter() - err := cfg.WriteConfigFile() + err = cfg.WriteConfigFile() assert.NoError(t, err) err = cfg.LoadWithRootdir(true) assert.ErrorIs(t, err, tc.err) @@ -133,16 +135,19 @@ func TestJSONSerialization(t *testing.T) { assert.NoError(t, errUnmarshal) assert.NoError(t, errSerialize) - for _, v := range m { - assert.NotEmpty(t, v) + for k, v := range m { + if k != "Rootdir" { // Rootdir is not serialized + assert.NotEmpty(t, v) + } } } func TestLoadValidationDefaultsConfigFileEnv(t *testing.T) { tmpdir := t.TempDir() cfg := DefaultConfig() - cfg.Rootdir = tmpdir - FixtureEnvVars(t, envVarsDifferentThanDefault) + err := cfg.setRootdir(tmpdir) + assert.NoError(t, err) + FixtureEnvVars(t, envVarsDifferent) errWriteConfig := cfg.WriteConfigFile() errLoad := cfg.LoadWithRootdir(true) @@ -155,7 +160,7 @@ func TestLoadValidationDefaultsConfigFileEnv(t *testing.T) { func TestLoadDefaultsEnv(t *testing.T) { cfg := DefaultConfig() - FixtureEnvVars(t, envVarsDifferentThanDefault) + FixtureEnvVars(t, envVarsDifferent) err := cfg.LoadWithRootdir(false) @@ -166,7 +171,7 @@ func TestLoadDefaultsEnv(t *testing.T) { func TestEnvVariablesAllConsidered(t *testing.T) { cfg := DefaultConfig() - FixtureEnvVars(t, envVarsDifferentThanDefault) + FixtureEnvVars(t, envVarsDifferent) err := cfg.LoadWithRootdir(false) @@ -187,8 +192,9 @@ func TestEnvVariablesAllConsidered(t *testing.T) { func TestLoadNonExistingConfigFile(t *testing.T) { cfg := DefaultConfig() - cfg.Rootdir = t.TempDir() - err := cfg.LoadWithRootdir(true) + err := cfg.setRootdir(t.TempDir()) + assert.NoError(t, err) + err = cfg.LoadWithRootdir(true) assert.ErrorIs(t, err, ErrReadingConfigFile) } @@ -203,7 +209,8 @@ func TestLoadInvalidConfigFile(t *testing.T) { ) assert.NoError(t, errWrite) - cfg.Rootdir = tmpdir + err := cfg.setRootdir(tmpdir) + assert.NoError(t, err) errLoad := cfg.LoadWithRootdir(true) assert.ErrorIs(t, errLoad, ErrReadingConfigFile) } @@ -262,19 +269,21 @@ func TestCreateAndLoadCustomConfig(t *testing.T) { testdir := t.TempDir() cfg := DefaultConfig() - cfg.Rootdir = testdir + err := cfg.setRootdir(testdir) + assert.NoError(t, err) // a few valid but non-default changes cfg.Net.PubSubEnabled = false cfg.Log.Level = "fatal" - err := cfg.CreateRootDirAndConfigFile() + err = cfg.CreateRootDirAndConfigFile() assert.NoError(t, err) assert.True(t, cfg.ConfigFileExists()) // check that the config file loads properly cfg2 := DefaultConfig() - cfg2.Rootdir = testdir + err = cfg2.setRootdir(testdir) + assert.NoError(t, err) err = cfg2.LoadWithRootdir(true) assert.NoError(t, err) assert.Equal(t, cfg.Net.PubSubEnabled, cfg2.Net.PubSubEnabled) diff --git a/config/configfile_test.go b/config/configfile_test.go index e19981275e..a9ec874f9f 100644 --- a/config/configfile_test.go +++ b/config/configfile_test.go @@ -77,7 +77,8 @@ func TestReadConfigFileForLogger(t *testing.T) { assert.True(t, cfg.ConfigFileExists()) cfgFromFile := DefaultConfig() - cfgFromFile.Rootdir = tmpdir + err = cfgFromFile.setRootdir(tmpdir) + assert.NoError(t, err) err = cfgFromFile.LoadWithRootdir(true) assert.NoError(t, err) @@ -93,12 +94,13 @@ func TestReadConfigFileForDatastore(t *testing.T) { tmpdir := t.TempDir() cfg := DefaultConfig() - cfg.Rootdir = tmpdir + err := cfg.setRootdir(tmpdir) + assert.NoError(t, err) cfg.Datastore.Store = "badger" cfg.Datastore.Badger.Path = "dataPath" cfg.Datastore.Badger.ValueLogFileSize = 512 * MiB - err := cfg.WriteConfigFile() + err = cfg.WriteConfigFile() assert.NoError(t, err) configPath := filepath.Join(tmpdir, DefaultConfigFileName) @@ -106,7 +108,8 @@ func TestReadConfigFileForDatastore(t *testing.T) { assert.NoError(t, err) cfgFromFile := DefaultConfig() - cfgFromFile.Rootdir = tmpdir + err = cfgFromFile.setRootdir(tmpdir) + assert.NoError(t, err) err = cfgFromFile.LoadWithRootdir(true) assert.NoError(t, err) @@ -114,12 +117,14 @@ func TestReadConfigFileForDatastore(t *testing.T) { assert.Equal(t, filepath.Join(tmpdir, cfg.Datastore.Badger.Path), cfgFromFile.Datastore.Badger.Path) assert.Equal(t, cfg.Datastore.Badger.ValueLogFileSize, cfgFromFile.Datastore.Badger.ValueLogFileSize) } + func TestConfigFileExists(t *testing.T) { cfg := DefaultConfig() - cfg.Rootdir = t.TempDir() + err := cfg.setRootdir(t.TempDir()) + assert.NoError(t, err) assert.False(t, cfg.ConfigFileExists()) - err := cfg.WriteConfigFile() + err = cfg.WriteConfigFile() assert.NoError(t, err) assert.True(t, cfg.ConfigFileExists()) } diff --git a/config/errors.go b/config/errors.go index 926c8bebf2..872b362b66 100644 --- a/config/errors.go +++ b/config/errors.go @@ -49,6 +49,7 @@ const ( errInvalidDatastorePath string = "invalid datastore path" errMissingPortNumber string = "missing port number" errNoPortWithDomain string = "cannot provide port with domain name" + errInvalidRootDir string = "invalid root directory" ) var ( @@ -86,6 +87,7 @@ var ( ErrorInvalidDatastorePath = errors.New(errInvalidDatastorePath) ErrMissingPortNumber = errors.New(errMissingPortNumber) ErrNoPortWithDomain = errors.New(errNoPortWithDomain) + ErrorInvalidRootDir = errors.New(errInvalidRootDir) ) func NewErrFailedToWriteFile(inner error, path string) error { @@ -211,3 +213,7 @@ func NewErrLoggerConfig(s string) error { func NewErrInvalidDatastorePath(path string) error { return errors.New(errInvalidDatastorePath, errors.NewKV("path", path)) } + +func NewErrInvalidRootDir(path string) error { + return errors.New(errInvalidRootDir, errors.NewKV("path", path)) +} From 6855dd0a5fd8e0343574baf195684fbaed0b6f0e Mon Sep 17 00:00:00 2001 From: Orpheus Lummis Date: Mon, 24 Apr 2023 00:17:17 -0400 Subject: [PATCH 5/6] cli integration tests --- tests/integration/cli/client_blocks_test.go | 41 +++ tests/integration/cli/client_peerid_test.go | 34 +++ tests/integration/cli/client_ping_test.go | 63 +++++ tests/integration/cli/client_query_test.go | 66 +++++ .../cli/client_rpc_p2p_collection_test.go | 13 + .../cli/client_rpc_replicator_test.go | 22 ++ .../integration/cli/client_schema_add_test.go | 95 +++++++ .../cli/client_schema_patch_test.go | 53 ++++ tests/integration/cli/init_test.go | 51 ++++ tests/integration/cli/log_config_test.go | 11 +- tests/integration/cli/root_test.go | 43 +++ tests/integration/cli/serverdump_test.go | 28 ++ tests/integration/cli/start_test.go | 90 +++++++ tests/integration/cli/utils.go | 244 ++++++++++++++++++ tests/integration/cli/version_test.go | 47 ++++ 15 files changed, 898 insertions(+), 3 deletions(-) create mode 100644 tests/integration/cli/client_blocks_test.go create mode 100644 tests/integration/cli/client_peerid_test.go create mode 100644 tests/integration/cli/client_ping_test.go create mode 100644 tests/integration/cli/client_query_test.go create mode 100644 tests/integration/cli/client_rpc_p2p_collection_test.go create mode 100644 tests/integration/cli/client_rpc_replicator_test.go create mode 100644 tests/integration/cli/client_schema_add_test.go create mode 100644 tests/integration/cli/client_schema_patch_test.go create mode 100644 tests/integration/cli/init_test.go create mode 100644 tests/integration/cli/root_test.go create mode 100644 tests/integration/cli/serverdump_test.go create mode 100644 tests/integration/cli/start_test.go create mode 100644 tests/integration/cli/utils.go create mode 100644 tests/integration/cli/version_test.go diff --git a/tests/integration/cli/client_blocks_test.go b/tests/integration/cli/client_blocks_test.go new file mode 100644 index 0000000000..08d1c22684 --- /dev/null +++ b/tests/integration/cli/client_blocks_test.go @@ -0,0 +1,41 @@ +// Copyright 2022 Democratized Data Foundation +// +// Use of this software is governed by the Business Source License +// included in the file licenses/BSL.txt. +// +// As of the Change Date specified in that file, in accordance with +// the Business Source License, use of this software will be governed +// by the Apache License, Version 2.0, included in the file +// licenses/APL.txt. + +package clitest + +import "testing" + +func TestClientBlocksEmpty(t *testing.T) { + conf := NewDefraNodeDefaultConfig(t) + stdout, _ := runDefraCommand(t, conf, []string{"client", "blocks"}) + assertContainsSubstring(t, stdout, "Usage:") +} + +func TestClientBlocksGetEmpty(t *testing.T) { + conf := NewDefraNodeDefaultConfig(t) + stdout, _ := runDefraCommand(t, conf, []string{"client", "blocks", "get"}) + assertContainsSubstring(t, stdout, "Usage:") +} + +func TestClientBlocksGetInvalidCID(t *testing.T) { + conf := NewDefraNodeDefaultConfig(t) + stopDefra := runDefraNode(t, conf) + stdout, _ := runDefraCommand(t, conf, []string{"client", "blocks", "get", "invalid-cid"}) + _ = stopDefra() + assertContainsSubstring(t, stdout, "\"errors\"") +} + +func TestClientBlocksGetNonExistentCID(t *testing.T) { + conf := NewDefraNodeDefaultConfig(t) + stopDefra := runDefraNode(t, conf) + stdout, _ := runDefraCommand(t, conf, []string{"client", "blocks", "get", "bafybeieelb43ol5e5jiick2p7k4p577ph72ecwcuowlhbops4hpz24zhz4"}) + _ = stopDefra() + assertContainsSubstring(t, stdout, "could not find") +} diff --git a/tests/integration/cli/client_peerid_test.go b/tests/integration/cli/client_peerid_test.go new file mode 100644 index 0000000000..8ad19983cc --- /dev/null +++ b/tests/integration/cli/client_peerid_test.go @@ -0,0 +1,34 @@ +// Copyright 2022 Democratized Data Foundation +// +// Use of this software is governed by the Business Source License +// included in the file licenses/BSL.txt. +// +// As of the Change Date specified in that file, in accordance with +// the Business Source License, use of this software will be governed +// by the Apache License, Version 2.0, included in the file +// licenses/APL.txt. + +package clitest + +import ( + "testing" +) + +func TestPeerID(t *testing.T) { + conf := NewDefraNodeDefaultConfig(t) + stopDefra := runDefraNode(t, conf) + + stdout, _ := runDefraCommand(t, conf, []string{"client", "peerid"}) + + defraLogLines := stopDefra() + + assertNotContainsSubstring(t, defraLogLines, "ERROR") + + assertContainsSubstring(t, stdout, "peerID") +} + +func TestPeerIDWithNoHost(t *testing.T) { + conf := NewDefraNodeDefaultConfig(t) + _, stderr := runDefraCommand(t, conf, []string{"client", "peerid"}) + assertContainsSubstring(t, stderr, "failed to request peer ID") +} diff --git a/tests/integration/cli/client_ping_test.go b/tests/integration/cli/client_ping_test.go new file mode 100644 index 0000000000..1a77c218ca --- /dev/null +++ b/tests/integration/cli/client_ping_test.go @@ -0,0 +1,63 @@ +// Copyright 2022 Democratized Data Foundation +// +// Use of this software is governed by the Business Source License +// included in the file licenses/BSL.txt. +// +// As of the Change Date specified in that file, in accordance with +// the Business Source License, use of this software will be governed +// by the Apache License, Version 2.0, included in the file +// licenses/APL.txt. + +package clitest + +import ( + "fmt" + "strings" + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/sourcenetwork/defradb/config" +) + +func TestPingSimple(t *testing.T) { + conf := NewDefraNodeDefaultConfig(t) + stopDefra := runDefraNode(t, conf) + + stdout, _ := runDefraCommand(t, conf, []string{"client", "ping"}) + + nodeLog := stopDefra() + + assert.Contains(t, stdout, `{"data":{"response":"pong"}}`) + for _, line := range nodeLog { + assert.NotContains(t, line, "ERROR") + } +} + +func TestPingCommandToInvalidHost(t *testing.T) { + conf := NewDefraNodeDefaultConfig(t) + stopDefra := runDefraNode(t, conf) + _, stderr := runDefraCommand(t, conf, []string{"client", "ping", "--url", "'1!2:3!4'"}) + + nodeLog := stopDefra() + + for _, line := range nodeLog { + assert.NotContains(t, line, "ERROR") + } + // for some line in stderr to contain the error message + for _, line := range stderr { + if strings.Contains(line, config.ErrFailedToValidateConfig.Error()) { + return + } + } + t.Error("expected error message not found in stderr") +} + +func TestPingCommandNoHost(t *testing.T) { + conf := NewDefraNodeDefaultConfig(t) + p, err := findFreePortInRange(49152, 65535) + assert.NoError(t, err) + addr := fmt.Sprintf("localhost:%d", p) + _, stderr := runDefraCommand(t, conf, []string{"client", "ping", "--url", addr}) + assertContainsSubstring(t, stderr, "failed to send ping") +} diff --git a/tests/integration/cli/client_query_test.go b/tests/integration/cli/client_query_test.go new file mode 100644 index 0000000000..00271ada94 --- /dev/null +++ b/tests/integration/cli/client_query_test.go @@ -0,0 +1,66 @@ +// Copyright 2022 Democratized Data Foundation +// +// Use of this software is governed by the Business Source License +// included in the file licenses/BSL.txt. +// +// As of the Change Date specified in that file, in accordance with +// the Business Source License, use of this software will be governed +// by the Apache License, Version 2.0, included in the file +// licenses/APL.txt. + +package clitest + +import ( + "testing" +) + +func TestRequestSimple(t *testing.T) { + conf := NewDefraNodeDefaultConfig(t) + stopDefra := runDefraNode(t, conf) + + stdout, _ := runDefraCommand(t, conf, []string{"client", "query", + "query IntrospectionQuery {__schema {queryType { name }}}", + }) + nodeLog := stopDefra() + + assertContainsSubstring(t, stdout, "Query") + assertNotContainsSubstring(t, nodeLog, "ERROR") +} + +func TestRequestInvalidQuery(t *testing.T) { + conf := NewDefraNodeDefaultConfig(t) + stopDefra := runDefraNode(t, conf) + + stdout, _ := runDefraCommand(t, conf, []string{"client", "query", "{}}"}) + _ = stopDefra() + + assertContainsSubstring(t, stdout, "Syntax Error") +} + +func TestRequestWithErrorNoType(t *testing.T) { + conf := NewDefraNodeDefaultConfig(t) + stopDefra := runDefraNode(t, conf) + defer stopDefra() + + stdout, _ := runDefraCommand(t, conf, []string{"client", "query", "query { User { whatever } }"}) + + assertContainsSubstring(t, stdout, "Cannot query field") +} + +func TestRequestWithErrorNoField(t *testing.T) { + conf := NewDefraNodeDefaultConfig(t) + stopDefra := runDefraNode(t, conf) + defer stopDefra() + + fname := schemaFileFixture(t, "schema.graphql", ` + type User { + id: ID + name: String + }`) + stdout, _ := runDefraCommand(t, conf, []string{"client", "schema", "add", "-f", fname}) + assertContainsSubstring(t, stdout, "success") + + stdout, _ = runDefraCommand(t, conf, []string{"client", "query", "query { User { nonexistent } }"}) + + assertContainsSubstring(t, stdout, `Cannot query field \"nonexistent\"`) +} diff --git a/tests/integration/cli/client_rpc_p2p_collection_test.go b/tests/integration/cli/client_rpc_p2p_collection_test.go new file mode 100644 index 0000000000..b44abcaefb --- /dev/null +++ b/tests/integration/cli/client_rpc_p2p_collection_test.go @@ -0,0 +1,13 @@ +// Copyright 2022 Democratized Data Foundation +// +// Use of this software is governed by the Business Source License +// included in the file licenses/BSL.txt. +// +// As of the Change Date specified in that file, in accordance with +// the Business Source License, use of this software will be governed +// by the Apache License, Version 2.0, included in the file +// licenses/APL.txt. + +package clitest + +// TBD diff --git a/tests/integration/cli/client_rpc_replicator_test.go b/tests/integration/cli/client_rpc_replicator_test.go new file mode 100644 index 0000000000..afe6b031c0 --- /dev/null +++ b/tests/integration/cli/client_rpc_replicator_test.go @@ -0,0 +1,22 @@ +// Copyright 2022 Democratized Data Foundation +// +// Use of this software is governed by the Business Source License +// included in the file licenses/BSL.txt. +// +// As of the Change Date specified in that file, in accordance with +// the Business Source License, use of this software will be governed +// by the Apache License, Version 2.0, included in the file +// licenses/APL.txt. + +package clitest + +/* WIP client rpc replicator getall is broken currently +func TestReplicatorGetAllEmpty(t *testing.T) { +conf := NewDefraNodeDefaultConfig(t) + stopDefra := runDefraNode(t, conf) + defer stopDefra() + + _, stderr := runDefraCommand(t, conf, []string{"client", "rpc", "replicator", "getall"}) + assertContainsSubstring(t, stderr, "No replicator found") +} +*/ diff --git a/tests/integration/cli/client_schema_add_test.go b/tests/integration/cli/client_schema_add_test.go new file mode 100644 index 0000000000..8c505785c0 --- /dev/null +++ b/tests/integration/cli/client_schema_add_test.go @@ -0,0 +1,95 @@ +// Copyright 2022 Democratized Data Foundation +// +// Use of this software is governed by the Business Source License +// included in the file licenses/BSL.txt. +// +// As of the Change Date specified in that file, in accordance with +// the Business Source License, use of this software will be governed +// by the Apache License, Version 2.0, included in the file +// licenses/APL.txt. + +package clitest + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestAddSchemaFromFile(t *testing.T) { + conf := NewDefraNodeDefaultConfig(t) + stopDefra := runDefraNode(t, conf) + + fname := schemaFileFixture(t, "schema.graphql", ` + type User { + id: ID + name: String + }`) + + stdout, _ := runDefraCommand(t, conf, []string{"client", "schema", "add", "-f", fname}) + + nodeLog := stopDefra() + + assert.Contains(t, stdout, `{"data":{"result":"success"}}`) + assertNotContainsSubstring(t, nodeLog, "ERROR") +} + +func TestAddSchemaWithDuplicateType(t *testing.T) { + conf := NewDefraNodeDefaultConfig(t) + stopDefra := runDefraNode(t, conf) + + fname1 := schemaFileFixture(t, "schema1.graphql", `type Post { id: ID title: String }`) + fname2 := schemaFileFixture(t, "schema2.graphql", `type Post { id: ID author: String }`) + + stdout1, _ := runDefraCommand(t, conf, []string{"client", "schema", "add", "-f", fname1}) + stdout2, _ := runDefraCommand(t, conf, []string{"client", "schema", "add", "-f", fname2}) + + _ = stopDefra() + + assertContainsSubstring(t, stdout1, `{"data":{"result":"success"}}`) + assertContainsSubstring(t, stdout2, `schema type already exists. Name: Post`) +} + +/* disabled because current implementation doesn't support this currently +func TestAddSchemaWithMultipleFiles(t *testing.T) { + conf := NewDefraNodeDefaultConfig(t) + stopDefra := runDefraNode(t, conf) + + fname1 := schemaFileFixture(t, "schema1.graphql", `type Post { id: ID title: String }`) + fname2 := schemaFileFixture(t, "schema2.graphql", `type User { id: ID name: String }`) + + stdout, _ := runDefraCommand(t, conf, []string{"client", "schema", "add", "-f", fname1, "-f", fname2}) + + nodeLog := stopDefra() + + assertContainsSubstring(t, stdout, `{"data":{"result":"success"}}`) + assertNotContainsSubstring(t, nodeLog, "ERROR") + + stdout, _ = runDefraCommand(t, conf, []string{"client", "query", + `query IntrospectionQuery { __schema { types { name } } }`}) + assertContainsSubstring(t, stdout, `{"name":"Post"}`) + assertContainsSubstring(t, stdout, `{"name":"User"}`) +} +*/ + +/* disabled because current implementation doesn't support this currently +func TestAddSchemaWithMultipleFilesWithIntertwinedSchemas(t *testing.T) { + conf := NewDefraNodeDefaultConfig(t) + stopDefra := runDefraNode(t, conf) + + fname1 := schemaFileFixture(t, "schema1.graphql", `type Post { id: ID title: String }`) + fname2 := schemaFileFixture(t, "schema2.graphql", `type User { id: ID posts: [Post] }`) + + stdout, _ := runDefraCommand(t, conf, []string{"client", "schema", "add", "-f", fname1, "-f", fname2}) + + nodeLog := stopDefra() + + assertContainsSubstring(t, stdout, `{"data":{"result":"success"}}`) + assertNotContainsSubstring(t, nodeLog, "ERROR") + + stdout, _ = runDefraCommand(t, conf, []string{"client", "query", + `query IntrospectionQuery { __schema { types { name } } }`}) + assertContainsSubstring(t, stdout, `{"name":"Post"}`) + assertContainsSubstring(t, stdout, `{"name":"User"}`) +} +*/ diff --git a/tests/integration/cli/client_schema_patch_test.go b/tests/integration/cli/client_schema_patch_test.go new file mode 100644 index 0000000000..487dc9eda5 --- /dev/null +++ b/tests/integration/cli/client_schema_patch_test.go @@ -0,0 +1,53 @@ +// Copyright 2022 Democratized Data Foundation +// +// Use of this software is governed by the Business Source License +// included in the file licenses/BSL.txt. +// +// As of the Change Date specified in that file, in accordance with +// the Business Source License, use of this software will be governed +// by the Apache License, Version 2.0, included in the file +// licenses/APL.txt. + +package clitest + +import ( + "testing" +) + +func TestClientSchemaPatch(t *testing.T) { + conf := NewDefraNodeDefaultConfig(t) + stopDefra := runDefraNode(t, conf) + defer stopDefra() + + fname := schemaFileFixture(t, "schema.graphql", ` + type User { + id: ID + name: String + }`) + stdout, _ := runDefraCommand(t, conf, []string{"client", "schema", "add", "-f", fname}) + assertContainsSubstring(t, stdout, "success") + + stdout, _ = runDefraCommand(t, conf, []string{"client", "schema", "patch", `[{ "op": "add", "path": "/User/Schema/Fields/-", "value": {"Name": "address", "Kind": "String"} }]`}) + assertContainsSubstring(t, stdout, "success") + + stdout, _ = runDefraCommand(t, conf, []string{"client", "query", `query IntrospectionQuery { __type (name: "User") { fields { name } }}`}) + assertContainsSubstring(t, stdout, "address") +} + +func TestClientSchemaPatch_InvalidJSONPatch(t *testing.T) { + conf := NewDefraNodeDefaultConfig(t) + stopDefra := runDefraNode(t, conf) + defer stopDefra() + + fname := schemaFileFixture(t, "schema.graphql", ` + type User { + id: ID + name: String + } + `) + stdout, _ := runDefraCommand(t, conf, []string{"client", "schema", "add", "-f", fname}) + assertContainsSubstring(t, stdout, "success") + + stdout, _ = runDefraCommand(t, conf, []string{"client", "schema", "patch", `[{ "op": "invalidOp" }]`}) + assertContainsSubstring(t, stdout, "Internal Server Error") +} diff --git a/tests/integration/cli/init_test.go b/tests/integration/cli/init_test.go new file mode 100644 index 0000000000..7292d920c3 --- /dev/null +++ b/tests/integration/cli/init_test.go @@ -0,0 +1,51 @@ +// Copyright 2022 Democratized Data Foundation +// +// Use of this software is governed by the Business Source License +// included in the file licenses/BSL.txt. +// +// As of the Change Date specified in that file, in accordance with +// the Business Source License, use of this software will be governed +// by the Apache License, Version 2.0, included in the file +// licenses/APL.txt. + +package clitest + +import ( + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/sourcenetwork/defradb/config" +) + +// Executing init command creates valid config file. +func TestCLIInitCommand(t *testing.T) { + conf := NewDefraNodeDefaultConfig(t) + _, stderr := runDefraCommand(t, conf, []string{"init", "--rootdir", conf.rootDir}) + cfgfilePath := filepath.Join(conf.rootDir, config.DefaultConfigFileName) + assertContainsSubstring(t, stderr, "Created config file at "+cfgfilePath) + if !assert.FileExists(t, cfgfilePath) { + t.Fatal("Config file not created") + } +} + +func TestCLIInitCommandTwiceErrors(t *testing.T) { + conf := NewDefraNodeDefaultConfig(t) + cfgfilePath := filepath.Join(conf.rootDir, config.DefaultConfigFileName) + _, stderr := runDefraCommand(t, conf, []string{"init", "--rootdir", conf.rootDir}) + assertContainsSubstring(t, stderr, "Created config file at "+cfgfilePath) + _, stderr = runDefraCommand(t, conf, []string{"init", "--rootdir", conf.rootDir}) + assertContainsSubstring(t, stderr, "Configuration file already exists at "+cfgfilePath) +} + +// Executing init command twice, but second time reinitializing. +func TestInitCommandTwiceReinitalize(t *testing.T) { + conf := NewDefraNodeDefaultConfig(t) + cfgfilePath := filepath.Join(conf.rootDir, config.DefaultConfigFileName) + _, stderr := runDefraCommand(t, conf, []string{"init", "--rootdir", conf.rootDir}) + assertContainsSubstring(t, stderr, "Created config file at "+cfgfilePath) + _, stderr = runDefraCommand(t, conf, []string{"init", "--rootdir", conf.rootDir, "--reinitialize"}) + assertContainsSubstring(t, stderr, "Deleted config file at "+cfgfilePath) + assertContainsSubstring(t, stderr, "Created config file at "+cfgfilePath) +} diff --git a/tests/integration/cli/log_config_test.go b/tests/integration/cli/log_config_test.go index 043eae5b7c..55d1b18154 100644 --- a/tests/integration/cli/log_config_test.go +++ b/tests/integration/cli/log_config_test.go @@ -8,7 +8,7 @@ // by the Apache License, Version 2.0, included in the file // licenses/APL.txt. -package cli +package clitest import ( "bufio" @@ -22,6 +22,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/sourcenetwork/defradb/cli" + "github.com/sourcenetwork/defradb/config" "github.com/sourcenetwork/defradb/logging" ) @@ -80,7 +81,11 @@ func captureLogLines(t *testing.T, setup func(), predicate func()) []string { os.Args = append(os.Args, "init", "--rootdir", directory) setup() - cli.Execute() + cfg := config.DefaultConfig() + defraCmd := cli.NewDefraCommand(cfg) + if err := defraCmd.Execute(context.Background()); err != nil { + t.Fatal(err) + } predicate() log1.Flush() log2.Flush() @@ -88,7 +93,7 @@ func captureLogLines(t *testing.T, setup func(), predicate func()) []string { w.Close() var buf bytes.Buffer - io.Copy(&buf, r) + _, _ = io.Copy(&buf, r) logLines, err := parseLines(&buf) if err != nil { t.Fatal(err) diff --git a/tests/integration/cli/root_test.go b/tests/integration/cli/root_test.go new file mode 100644 index 0000000000..33df29fc4d --- /dev/null +++ b/tests/integration/cli/root_test.go @@ -0,0 +1,43 @@ +// Copyright 2022 Democratized Data Foundation +// +// Use of this software is governed by the Business Source License +// included in the file licenses/BSL.txt. +// +// As of the Change Date specified in that file, in accordance with +// the Business Source License, use of this software will be governed +// by the Apache License, Version 2.0, included in the file +// licenses/APL.txt. + +package clitest + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestRootCommandEmptyRootDir(t *testing.T) { + conf := NewDefraNodeDefaultConfig(t) + stdout, _ := runDefraCommand(t, conf, []string{}) + assert.Contains(t, stdout, "Usage:") +} + +func TestRootCommandRootDirWithDefaultConfig(t *testing.T) { + conf := DefraNodeConfig{ + logPath: t.TempDir(), + } + stdout, _ := runDefraCommand(t, conf, []string{}) + assert.Contains(t, stdout, "Usage:") +} + +func TestRootCommandRootDirFromEnv(t *testing.T) { + conf := NewDefraNodeDefaultConfig(t) + stdout, _ := runDefraCommand(t, conf, []string{}) + assert.Contains(t, stdout, "Usage:") +} + +func TestRootCommandRootWithNonexistentFlag(t *testing.T) { + conf := NewDefraNodeDefaultConfig(t) + stdout, _ := runDefraCommand(t, conf, []string{"--foo"}) + assert.Contains(t, stdout, "Usage:") +} diff --git a/tests/integration/cli/serverdump_test.go b/tests/integration/cli/serverdump_test.go new file mode 100644 index 0000000000..ed8fcd4d9f --- /dev/null +++ b/tests/integration/cli/serverdump_test.go @@ -0,0 +1,28 @@ +// Copyright 2022 Democratized Data Foundation +// +// Use of this software is governed by the Business Source License +// included in the file licenses/BSL.txt. +// +// As of the Change Date specified in that file, in accordance with +// the Business Source License, use of this software will be governed +// by the Apache License, Version 2.0, included in the file +// licenses/APL.txt. + +package clitest + +import ( + "testing" +) + +func TestServerDumpMemoryErrs(t *testing.T) { + conf := NewDefraNodeDefaultConfig(t) + _, stderr := runDefraCommand(t, conf, []string{"server-dump", "--store", "memory"}) + assertContainsSubstring(t, stderr, "server-side dump is only supported for the Badger datastore") +} + +func TestServerDumpInvalidStoreErrs(t *testing.T) { + conf := NewDefraNodeDefaultConfig(t) + _, stderr := runDefraCommand(t, conf, []string{"server-dump", "--store", "invalid"}) + // assertContainsSubstring(t, stderr, "invalid datastore type") + assertContainsSubstring(t, stderr, "server-side dump is only supported for the Badger datastore") +} diff --git a/tests/integration/cli/start_test.go b/tests/integration/cli/start_test.go new file mode 100644 index 0000000000..a49bba9c0d --- /dev/null +++ b/tests/integration/cli/start_test.go @@ -0,0 +1,90 @@ +// Copyright 2022 Democratized Data Foundation +// +// Use of this software is governed by the Business Source License +// included in the file licenses/BSL.txt. +// +// As of the Change Date specified in that file, in accordance with +// the Business Source License, use of this software will be governed +// by the Apache License, Version 2.0, included in the file +// licenses/APL.txt. + +package clitest + +import ( + "fmt" + "testing" +) + +func TestStartCommandBasic(t *testing.T) { + conf := NewDefraNodeDefaultConfig(t) + _, stderr := runDefraCommand(t, conf, []string{ + "start", + "--url", conf.APIURL, + "--tcpaddr", conf.GRPCAddr, + }) + assertContainsSubstring(t, stderr, "Starting DefraDB service...") + assertNotContainsSubstring(t, stderr, "Error") +} + +func TestStartCommandWithTLSIncomplete(t *testing.T) { + conf := NewDefraNodeDefaultConfig(t) + _, stderr := runDefraCommand(t, conf, []string{ + "start", + "--tls", + "--url", conf.APIURL, + "--tcpaddr", conf.GRPCAddr, + }) + assertContainsSubstring(t, stderr, "Starting DefraDB service...") + assertContainsSubstring(t, stderr, "Error") +} + +func TestStartCommandWithStoreMemory(t *testing.T) { + conf := NewDefraNodeDefaultConfig(t) + _, stderr := runDefraCommand(t, conf, []string{ + "start", "--store", "memory", + "--url", conf.APIURL, + "--tcpaddr", conf.GRPCAddr, + }) + assertContainsSubstring(t, stderr, "Starting DefraDB service...") + assertContainsSubstring(t, stderr, "Building new memory store") + assertNotContainsSubstring(t, stderr, "Error") +} + +func TestStartCommandWithP2PAddr(t *testing.T) { + conf := NewDefraNodeDefaultConfig(t) + p2pport, err := findFreePortInRange(49152, 65535) + if err != nil { + t.Fatal(err) + } + addr := fmt.Sprintf("/ip4/0.0.0.0/tcp/%d", p2pport) + _, stderr := runDefraCommand(t, conf, []string{ + "start", + "--p2paddr", addr, + "--url", conf.APIURL, + "--tcpaddr", conf.GRPCAddr, + }) + assertContainsSubstring(t, stderr, "Starting DefraDB service...") + logstring := fmt.Sprintf("Starting P2P node, {\"P2P address\": \"%s\"}", addr) + assertContainsSubstring(t, stderr, logstring) + assertNotContainsSubstring(t, stderr, "Error") +} + +func TestStartCommandWithNoP2P(t *testing.T) { + conf := NewDefraNodeDefaultConfig(t) + _, stderr := runDefraCommand(t, conf, []string{ + "start", + "--no-p2p", + }) + assertContainsSubstring(t, stderr, "Starting DefraDB service...") + assertNotContainsSubstring(t, stderr, "Starting P2P node") + assertNotContainsSubstring(t, stderr, "Error") +} + +func TestStartCommandWithInvalidStoreType(t *testing.T) { + conf := NewDefraNodeDefaultConfig(t) + _, stderr := runDefraCommand(t, conf, []string{ + "start", + "--store", "invalid", + }) + assertContainsSubstring(t, stderr, "failed to load config: failed to validate config: invalid store type") +} diff --git a/tests/integration/cli/utils.go b/tests/integration/cli/utils.go new file mode 100644 index 0000000000..aba0f8dc88 --- /dev/null +++ b/tests/integration/cli/utils.go @@ -0,0 +1,244 @@ +// Copyright 2022 Democratized Data Foundation +// +// Use of this software is governed by the Business Source License +// included in the file licenses/BSL.txt. +// +// As of the Change Date specified in that file, in accordance with +// the Business Source License, use of this software will be governed +// by the Apache License, Version 2.0, included in the file +// licenses/APL.txt. + +/* +Package clitest provides a testing framework for the Defra CLI, along with CLI integration tests. +*/ +package clitest + +import ( + "bufio" + "bytes" + "context" + "errors" + "fmt" + "io" + "math/rand" + "net" + "os" + "path/filepath" + "strings" + "testing" + "time" + + "github.com/stretchr/testify/assert" + + "github.com/sourcenetwork/defradb/cli" + "github.com/sourcenetwork/defradb/config" +) + +const COMMAND_TIMEOUT_SECONDS = 2 + +type DefraNodeConfig struct { + rootDir string + logPath string + APIURL string + GRPCAddr string +} + +func NewDefraNodeDefaultConfig(t *testing.T) DefraNodeConfig { + t.Helper() + portAPI, err := findFreePortInRange(49152, 65535) + if err != nil { + t.Fatal(err) + } + portGRPC, err := findFreePortInRange(49152, 65535) + if err != nil { + t.Fatal(err) + } + + return DefraNodeConfig{ + rootDir: t.TempDir(), + logPath: "", + APIURL: fmt.Sprintf("localhost:%d", portAPI), + GRPCAddr: fmt.Sprintf("/ip4/0.0.0.0/tcp/%d", portGRPC), + } +} + +// runDefraNode runs a defra node in a separate goroutine and returns a stopping function +// which also returns the node's execution log lines. +func runDefraNode(t *testing.T, conf DefraNodeConfig) func() []string { + t.Helper() + + if conf.logPath == "" { + conf.logPath = filepath.Join(t.TempDir(), "defra.log") + } + + var args []string + if conf.rootDir != "" { + args = append(args, "--rootdir", conf.rootDir) + } + if conf.APIURL != "" { + args = append(args, "--url", conf.APIURL) + } + if conf.GRPCAddr != "" { + args = append(args, "--tcpaddr", conf.GRPCAddr) + } + args = append(args, "--logoutput", conf.logPath) + + cfg := config.DefaultConfig() + ctx, cancel := context.WithCancel(context.Background()) + go func() { + defraCmd := cli.NewDefraCommand(cfg) + defraCmd.RootCmd.SetArgs( + append([]string{"start"}, args...), + ) + err := defraCmd.Execute(ctx) + assert.NoError(t, err) + }() + time.Sleep(1 * time.Second) // time buffer for it to start + cancelAndOutput := func() []string { + cancel() + time.Sleep(1 * time.Second) // time buffer for it to stop + lines, err := readLoglines(t, conf.logPath) + assert.NoError(t, err) + return lines + } + return cancelAndOutput +} + +// Runs a defra command and returns the stdout and stderr output. +func runDefraCommand(t *testing.T, conf DefraNodeConfig, args []string) (stdout, stderr []string) { + t.Helper() + cfg := config.DefaultConfig() + args = append([]string{ + "--url", conf.APIURL, + }, args...) + if !contains(args, "--rootdir") { + args = append(args, "--rootdir", t.TempDir()) + } + + ctx, cancel := context.WithTimeout(context.Background(), COMMAND_TIMEOUT_SECONDS*time.Second) + defer cancel() + + stdout, stderr = captureOutput(func() { + defraCmd := cli.NewDefraCommand(cfg) + t.Log("executing defra command with args", args) + defraCmd.RootCmd.SetArgs(args) + _ = defraCmd.Execute(ctx) + }) + return stdout, stderr +} + +func contains(args []string, arg string) bool { + for _, a := range args { + if a == arg { + return true + } + } + return false +} + +func readLoglines(t *testing.T, fpath string) ([]string, error) { + f, err := os.Open(fpath) + if err != nil { + return nil, err + } + defer f.Close() //nolint:errcheck + scanner := bufio.NewScanner(f) + lines := make([]string, 0) + for scanner.Scan() { + lines = append(lines, scanner.Text()) + } + err = scanner.Err() + assert.NoError(t, err) + return lines, nil +} + +func captureOutput(f func()) (stdout, stderr []string) { + oldStdout := os.Stdout + oldStderr := os.Stderr + rStdout, wStdout, err := os.Pipe() + if err != nil { + panic(err) + } + rStderr, wStderr, err := os.Pipe() + if err != nil { + panic(err) + } + os.Stdout = wStdout + os.Stderr = wStderr + + f() + + if err := wStdout.Close(); err != nil { + panic(err) + } + if err := wStderr.Close(); err != nil { + panic(err) + } + + os.Stdout = oldStdout + os.Stderr = oldStderr + + var stdoutBuf, stderrBuf bytes.Buffer + if _, err := io.Copy(&stdoutBuf, rStdout); err != nil { + panic(err) + } + if _, err := io.Copy(&stderrBuf, rStderr); err != nil { + panic(err) + } + + stdout = strings.Split(strings.TrimSuffix(stdoutBuf.String(), "\n"), "\n") + stderr = strings.Split(strings.TrimSuffix(stderrBuf.String(), "\n"), "\n") + + return +} + +// findFreePortInRange returns a free port in the range [minPort, maxPort]. +// The range of ports that are unfrequently used is [49152, 65535]. +func findFreePortInRange(minPort, maxPort int) (int, error) { + if minPort < 1 || maxPort > 65535 || minPort > maxPort { + return 0, errors.New("invalid port range") + } + + const maxAttempts = 100 + for i := 0; i < maxAttempts; i++ { + port := rand.Intn(maxPort-minPort+1) + minPort + addr := fmt.Sprintf("127.0.0.1:%d", port) + listener, err := net.Listen("tcp", addr) + if err == nil { + _ = listener.Close() + return port, nil + } + } + + return 0, errors.New("unable to find a free port") +} + +func assertContainsSubstring(t *testing.T, haystack []string, substring string) { + t.Helper() + if !containsSubstring(haystack, substring) { + t.Fatalf("expected %q to contain %q", haystack, substring) + } +} + +func assertNotContainsSubstring(t *testing.T, haystack []string, substring string) { + t.Helper() + if containsSubstring(haystack, substring) { + t.Fatalf("expected %q to not contain %q", haystack, substring) + } +} + +func containsSubstring(haystack []string, substring string) bool { + for _, s := range haystack { + if strings.Contains(s, substring) { + return true + } + } + return false +} + +func schemaFileFixture(t *testing.T, fname string, schema string) string { + absFname := filepath.Join(t.TempDir(), fname) + err := os.WriteFile(absFname, []byte(schema), 0644) + assert.NoError(t, err) + return absFname +} diff --git a/tests/integration/cli/version_test.go b/tests/integration/cli/version_test.go new file mode 100644 index 0000000000..ffa9820c98 --- /dev/null +++ b/tests/integration/cli/version_test.go @@ -0,0 +1,47 @@ +// Copyright 2022 Democratized Data Foundation +// +// Use of this software is governed by the Business Source License +// included in the file licenses/BSL.txt. +// +// As of the Change Date specified in that file, in accordance with +// the Business Source License, use of this software will be governed +// by the Apache License, Version 2.0, included in the file +// licenses/APL.txt. + +package clitest + +import ( + "encoding/json" + "strings" + "testing" + + "github.com/stretchr/testify/assert" +) + +// note: this assumes the version information *without* build-time info integrated. +func TestExecVersion(t *testing.T) { + conf := NewDefraNodeDefaultConfig(t) + stdout, stderr := runDefraCommand(t, conf, []string{"version"}) + for _, line := range stderr { + assert.NotContains(t, line, "ERROR") + } + output := strings.Join(stdout, " ") + assert.Contains(t, output, "defradb") + assert.Contains(t, output, "built with Go") +} + +func TestExecVersionJSON(t *testing.T) { + conf := NewDefraNodeDefaultConfig(t) + stdout, stderr := runDefraCommand(t, conf, []string{"version", "--format", "json"}) + for _, line := range stderr { + assert.NotContains(t, line, "ERROR") + } + output := strings.Join(stdout, " ") + assert.Contains(t, output, "go\":") + assert.Contains(t, output, "commit\":") + assert.Contains(t, output, "commitdate\":") + + var data map[string]any + err := json.Unmarshal([]byte(output), &data) + assert.NoError(t, err) +} From 0983d4fef3dcaa36e138410c0edd42e20abc6b89 Mon Sep 17 00:00:00 2001 From: Orpheus Lummis Date: Mon, 24 Apr 2023 23:41:25 -0400 Subject: [PATCH 6/6] apply feedback and fixes --- cli/cli_test.go | 25 +++++++++++ cli/peerid_test.go | 25 +++++++++-- cli/root.go | 3 ++ cli/start.go | 5 ++- config/config.go | 5 +-- .../cli/client_rpc_replicator_test.go | 21 +++++++-- .../integration/cli/client_schema_add_test.go | 44 ------------------- 7 files changed, 72 insertions(+), 56 deletions(-) diff --git a/cli/cli_test.go b/cli/cli_test.go index 877dd7b69f..4361191e49 100644 --- a/cli/cli_test.go +++ b/cli/cli_test.go @@ -11,12 +11,16 @@ package cli import ( + "fmt" + "math/rand" + "net" "testing" "github.com/spf13/cobra" "github.com/stretchr/testify/assert" "github.com/sourcenetwork/defradb/config" + "github.com/sourcenetwork/defradb/errors" ) // Verify that the top-level commands are registered, and if particular ones have subcommands. @@ -57,3 +61,24 @@ func walkCommandTree(t *testing.T, cmd *cobra.Command, f func(*cobra.Command)) { walkCommandTree(t, c, f) } } + +// findFreePortInRange returns a free port in the range [minPort, maxPort]. +// The range of ports that are unfrequently used is [49152, 65535]. +func findFreePortInRange(minPort, maxPort int) (int, error) { + if minPort < 1 || maxPort > 65535 || minPort > maxPort { + return 0, errors.New("invalid port range") + } + + const maxAttempts = 100 + for i := 0; i < maxAttempts; i++ { + port := rand.Intn(maxPort-minPort+1) + minPort + addr := fmt.Sprintf("127.0.0.1:%d", port) + listener, err := net.Listen("tcp", addr) + if err == nil { + _ = listener.Close() + return port, nil + } + } + + return 0, errors.New("unable to find a free port") +} diff --git a/cli/peerid_test.go b/cli/peerid_test.go index 85605d3566..7809d65efa 100644 --- a/cli/peerid_test.go +++ b/cli/peerid_test.go @@ -14,6 +14,7 @@ import ( "bytes" "context" "encoding/json" + "fmt" "io" "net/http" "testing" @@ -27,10 +28,26 @@ import ( // setTestingAddresses overrides the config addresses to be the ones reserved for testing. // Used to ensure the tests don't fail due to address clashes with the running server (with default config). func setTestingAddresses(cfg *config.Config) { - cfg.API.Address = "localhost:9182" - cfg.Net.P2PAddress = "/ip4/0.0.0.0/tcp/9172" - cfg.Net.TCPAddress = "/ip4/0.0.0.0/tcp/9162" - cfg.Net.RPCAddress = "0.0.0.0:9162" + portAPI, err := findFreePortInRange(49152, 65535) + if err != nil { + panic(err) + } + portTCP, err := findFreePortInRange(49152, 65535) + if err != nil { + panic(err) + } + portP2P, err := findFreePortInRange(49152, 65535) + if err != nil { + panic(err) + } + portRPC, err := findFreePortInRange(49152, 65535) + if err != nil { + panic(err) + } + cfg.API.Address = fmt.Sprintf("localhost:%d", portAPI) + cfg.Net.P2PAddress = fmt.Sprintf("/ip4/0.0.0.0/tcp/%d", portP2P) + cfg.Net.TCPAddress = fmt.Sprintf("/ip4/0.0.0.0/tcp/%d", portTCP) + cfg.Net.RPCAddress = fmt.Sprintf("0.0.0.0:%d", portRPC) } func TestGetPeerIDCmd(t *testing.T) { diff --git a/cli/root.go b/cli/root.go index 396bd65278..c57af1cba6 100644 --- a/cli/root.go +++ b/cli/root.go @@ -34,6 +34,9 @@ See https://docs.source.network/BSL.txt for more information. // Loads the rootDir containing the configuration file, otherwise warn about it and load a default configuration. // This allows some subcommands (`init`, `start`) to override the PreRun to create a rootDir by default. PersistentPreRunE: func(cmd *cobra.Command, _ []string) error { + if err := cfg.LoadRootDirFromFlagOrDefault(); err != nil { + return err + } if cfg.ConfigFileExists() { if err := cfg.LoadWithRootdir(true); err != nil { return errors.Wrap("failed to load config", err) diff --git a/cli/start.go b/cli/start.go index 3ac785bf12..a27347b0af 100644 --- a/cli/start.go +++ b/cli/start.go @@ -48,10 +48,14 @@ func MakeStartCommand(cfg *config.Config) *cobra.Command { Long: "Start a new instance of DefraDB node.", // Load the root config if it exists, otherwise create it. PersistentPreRunE: func(cmd *cobra.Command, _ []string) error { + if err := cfg.LoadRootDirFromFlagOrDefault(); err != nil { + return err + } if cfg.ConfigFileExists() { if err := cfg.LoadWithRootdir(true); err != nil { return config.NewErrLoadingConfig(err) } + log.FeedbackInfo(cmd.Context(), fmt.Sprintf("Configuration loaded from DefraDB directory %v", cfg.Rootdir)) } else { if err := cfg.LoadWithRootdir(false); err != nil { return config.NewErrLoadingConfig(err) @@ -60,7 +64,6 @@ func MakeStartCommand(cfg *config.Config) *cobra.Command { if err := cfg.WriteConfigFile(); err != nil { return err } - log.FeedbackInfo(cmd.Context(), fmt.Sprintf("Configuration loaded from DefraDB directory %v", cfg.Rootdir)) } else { if err := cfg.CreateRootDirAndConfigFile(); err != nil { return err diff --git a/config/config.go b/config/config.go index b827b759d1..b1fbaa20c0 100644 --- a/config/config.go +++ b/config/config.go @@ -127,7 +127,7 @@ func (cfg *Config) LoadWithRootdir(withRootdir bool) error { return err } - if err := cfg.loadRootDirFromFlagOrDefault(); err != nil { + if err := cfg.LoadRootDirFromFlagOrDefault(); err != nil { return err } @@ -156,9 +156,8 @@ func (cfg *Config) LoadWithRootdir(withRootdir bool) error { return nil } -func (cfg *Config) loadRootDirFromFlagOrDefault() error { +func (cfg *Config) LoadRootDirFromFlagOrDefault() error { if cfg.Rootdir == "" { - // Check CLI flag rootdir := cfg.v.GetString(RootdirKey) if rootdir != "" { return cfg.setRootdir(rootdir) diff --git a/tests/integration/cli/client_rpc_replicator_test.go b/tests/integration/cli/client_rpc_replicator_test.go index afe6b031c0..509762121d 100644 --- a/tests/integration/cli/client_rpc_replicator_test.go +++ b/tests/integration/cli/client_rpc_replicator_test.go @@ -10,13 +10,26 @@ package clitest -/* WIP client rpc replicator getall is broken currently +import ( + "fmt" + "testing" +) + func TestReplicatorGetAllEmpty(t *testing.T) { -conf := NewDefraNodeDefaultConfig(t) + conf := NewDefraNodeDefaultConfig(t) + portTCP, err := findFreePortInRange(49152, 65535) + if err != nil { + t.Fatal(err) + } + conf.GRPCAddr = fmt.Sprintf("/ip4/0.0.0.0/tcp/%d", portTCP) + if err != nil { + t.Fatal(err) + } + stopDefra := runDefraNode(t, conf) defer stopDefra() - _, stderr := runDefraCommand(t, conf, []string{"client", "rpc", "replicator", "getall"}) + tcpAddr := fmt.Sprintf("localhost:%d", portTCP) + _, stderr := runDefraCommand(t, conf, []string{"client", "--addr", tcpAddr, "rpc", "replicator", "getall"}) assertContainsSubstring(t, stderr, "No replicator found") } -*/ diff --git a/tests/integration/cli/client_schema_add_test.go b/tests/integration/cli/client_schema_add_test.go index 8c505785c0..f7ff6b3b08 100644 --- a/tests/integration/cli/client_schema_add_test.go +++ b/tests/integration/cli/client_schema_add_test.go @@ -49,47 +49,3 @@ func TestAddSchemaWithDuplicateType(t *testing.T) { assertContainsSubstring(t, stdout1, `{"data":{"result":"success"}}`) assertContainsSubstring(t, stdout2, `schema type already exists. Name: Post`) } - -/* disabled because current implementation doesn't support this currently -func TestAddSchemaWithMultipleFiles(t *testing.T) { - conf := NewDefraNodeDefaultConfig(t) - stopDefra := runDefraNode(t, conf) - - fname1 := schemaFileFixture(t, "schema1.graphql", `type Post { id: ID title: String }`) - fname2 := schemaFileFixture(t, "schema2.graphql", `type User { id: ID name: String }`) - - stdout, _ := runDefraCommand(t, conf, []string{"client", "schema", "add", "-f", fname1, "-f", fname2}) - - nodeLog := stopDefra() - - assertContainsSubstring(t, stdout, `{"data":{"result":"success"}}`) - assertNotContainsSubstring(t, nodeLog, "ERROR") - - stdout, _ = runDefraCommand(t, conf, []string{"client", "query", - `query IntrospectionQuery { __schema { types { name } } }`}) - assertContainsSubstring(t, stdout, `{"name":"Post"}`) - assertContainsSubstring(t, stdout, `{"name":"User"}`) -} -*/ - -/* disabled because current implementation doesn't support this currently -func TestAddSchemaWithMultipleFilesWithIntertwinedSchemas(t *testing.T) { - conf := NewDefraNodeDefaultConfig(t) - stopDefra := runDefraNode(t, conf) - - fname1 := schemaFileFixture(t, "schema1.graphql", `type Post { id: ID title: String }`) - fname2 := schemaFileFixture(t, "schema2.graphql", `type User { id: ID posts: [Post] }`) - - stdout, _ := runDefraCommand(t, conf, []string{"client", "schema", "add", "-f", fname1, "-f", fname2}) - - nodeLog := stopDefra() - - assertContainsSubstring(t, stdout, `{"data":{"result":"success"}}`) - assertNotContainsSubstring(t, nodeLog, "ERROR") - - stdout, _ = runDefraCommand(t, conf, []string{"client", "query", - `query IntrospectionQuery { __schema { types { name } } }`}) - assertContainsSubstring(t, stdout, `{"name":"Post"}`) - assertContainsSubstring(t, stdout, `{"name":"User"}`) -} -*/