Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: Make config internal to CLI #2310

Merged
merged 13 commits into from
Feb 16, 2024
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,8 @@ In this document, we use the default configuration, which has the following beha

The GraphQL endpoint can be used with a GraphQL client (e.g., Altair) to conveniently perform requests (`query`, `mutation`) and obtain schema introspection.

Read more about the configuration [here](./docs/config.md).
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

praise: This is nice, and keeps both .md focused.


## External port binding

By default the HTTP API and P2P network will use localhost. If you want to expose the ports externally you need to specify the addresses in the config or command line parameters.
Expand Down
2 changes: 1 addition & 1 deletion cli/backup_export.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ Example: export data for the 'Users' collection:
defradb client export --collection Users user_data.json`,
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
store := mustGetStoreContext(cmd)
store := mustGetContextStore(cmd)

if !isValidExportFormat(format) {
return ErrInvalidExportFormat
Expand Down
2 changes: 1 addition & 1 deletion cli/backup_import.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ Example: import data to the database:
defradb client import user_data.json`,
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
store := mustGetStoreContext(cmd)
store := mustGetContextStore(cmd)
return store.BasicImport(cmd.Context(), args[0])
},
}
Expand Down
20 changes: 9 additions & 11 deletions cli/cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,13 @@ package cli
import (
"github.com/spf13/cobra"

"github.com/sourcenetwork/defradb/config"
"github.com/sourcenetwork/defradb/logging"
)

var log = logging.MustNewLogger("cli")

// NewDefraCommand returns the root command instanciated with its tree of subcommands.
func NewDefraCommand(cfg *config.Config) *cobra.Command {
func NewDefraCommand() *cobra.Command {
p2p_collection := MakeP2PCollectionCommand()
p2p_collection.AddCommand(
MakeP2PCollectionAddCommand(),
Expand Down Expand Up @@ -83,12 +82,12 @@ func NewDefraCommand(cfg *config.Config) *cobra.Command {

tx := MakeTxCommand()
tx.AddCommand(
MakeTxCreateCommand(cfg),
MakeTxCommitCommand(cfg),
MakeTxDiscardCommand(cfg),
MakeTxCreateCommand(),
MakeTxCommitCommand(),
MakeTxDiscardCommand(),
)

collection := MakeCollectionCommand(cfg)
collection := MakeCollectionCommand()
collection.AddCommand(
MakeCollectionGetCommand(),
MakeCollectionListDocIDsCommand(),
Expand All @@ -98,7 +97,7 @@ func NewDefraCommand(cfg *config.Config) *cobra.Command {
MakeCollectionDescribeCommand(),
)

client := MakeClientCommand(cfg)
client := MakeClientCommand()
client.AddCommand(
MakeDumpCommand(),
MakeRequestCommand(),
Expand All @@ -111,13 +110,12 @@ func NewDefraCommand(cfg *config.Config) *cobra.Command {
collection,
)

root := MakeRootCommand(cfg)
root := MakeRootCommand()
root.AddCommand(
client,
MakeStartCommand(cfg),
MakeServerDumpCmd(cfg),
MakeStartCommand(),
MakeServerDumpCmd(),
MakeVersionCommand(),
MakeInitCommand(cfg),
)

return root
Expand Down
13 changes: 7 additions & 6 deletions cli/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,25 +12,26 @@ package cli

import (
"github.com/spf13/cobra"

"github.com/sourcenetwork/defradb/config"
)

func MakeClientCommand(cfg *config.Config) *cobra.Command {
func MakeClientCommand() *cobra.Command {
var txID uint64
var cmd = &cobra.Command{
Use: "client",
Short: "Interact with a DefraDB node",
Long: `Interact with a DefraDB node.
Execute queries, add schema types, obtain node info, etc.`,
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
if err := loadConfig(cfg); err != nil {
if err := setContextRootDir(cmd); err != nil {
return err
}
if err := setContextConfig(cmd); err != nil {
return err
}
if err := setTransactionContext(cmd, cfg, txID); err != nil {
if err := setContextTransaction(cmd, txID); err != nil {
return err
}
return setStoreContext(cmd, cfg)
return setContextStore(cmd)
},
}
cmd.PersistentFlags().Uint64Var(&txID, "tx", 0, "Transaction ID")
Expand Down
14 changes: 8 additions & 6 deletions cli/collection.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,10 @@ import (
"github.com/spf13/cobra"

"github.com/sourcenetwork/defradb/client"
"github.com/sourcenetwork/defradb/config"
"github.com/sourcenetwork/defradb/datastore"
)

func MakeCollectionCommand(cfg *config.Config) *cobra.Command {
func MakeCollectionCommand() *cobra.Command {
var txID uint64
var name string
var schemaRoot string
Expand All @@ -31,16 +30,19 @@ func MakeCollectionCommand(cfg *config.Config) *cobra.Command {
Long: `Create, read, update, and delete documents within a collection.`,
PersistentPreRunE: func(cmd *cobra.Command, args []string) (err error) {
// cobra does not chain pre run calls so we have to run them again here
if err := loadConfig(cfg); err != nil {
if err := setContextRootDir(cmd); err != nil {
return err
}
if err := setTransactionContext(cmd, cfg, txID); err != nil {
if err := setContextConfig(cmd); err != nil {
return err
}
if err := setStoreContext(cmd, cfg); err != nil {
if err := setContextTransaction(cmd, txID); err != nil {
return err
}
store := mustGetStoreContext(cmd)
if err := setContextStore(cmd); err != nil {
return err
}
store := mustGetContextStore(cmd)

var col client.Collection
var cols []client.Collection
Expand Down
2 changes: 1 addition & 1 deletion cli/collection_create.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ Example: create from stdin
`,
Args: cobra.RangeArgs(0, 1),
RunE: func(cmd *cobra.Command, args []string) error {
col, ok := tryGetCollectionContext(cmd)
col, ok := tryGetContextCollection(cmd)
if !ok {
return cmd.Usage()
}
Expand Down
2 changes: 1 addition & 1 deletion cli/collection_delete.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ Example: delete by filter
defradb client collection delete --name User --filter '{ "_gte": { "points": 100 } }'
`,
RunE: func(cmd *cobra.Command, args []string) error {
col, ok := tryGetCollectionContext(cmd)
col, ok := tryGetContextCollection(cmd)
if !ok {
return cmd.Usage()
}
Expand Down
4 changes: 2 additions & 2 deletions cli/collection_describe.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,9 @@ Example: view collection by version id
defradb client collection describe --version bae123
`,
RunE: func(cmd *cobra.Command, args []string) error {
store := mustGetStoreContext(cmd)
store := mustGetContextStore(cmd)

col, ok := tryGetCollectionContext(cmd)
col, ok := tryGetContextCollection(cmd)
if ok {
return writeJSON(cmd, col.Definition())
}
Expand Down
2 changes: 1 addition & 1 deletion cli/collection_get.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ Example:
`,
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
col, ok := tryGetCollectionContext(cmd)
col, ok := tryGetContextCollection(cmd)
if !ok {
return cmd.Usage()
}
Expand Down
2 changes: 1 addition & 1 deletion cli/collection_list_doc_ids.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ Example:
defradb client collection docIDs --name User
`,
RunE: func(cmd *cobra.Command, args []string) error {
col, ok := tryGetCollectionContext(cmd)
col, ok := tryGetContextCollection(cmd)
if !ok {
return cmd.Usage()
}
Expand Down
2 changes: 1 addition & 1 deletion cli/collection_update.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ Example: update by docIDs
`,
Args: cobra.RangeArgs(0, 1),
RunE: func(cmd *cobra.Command, args []string) error {
col, ok := tryGetCollectionContext(cmd)
col, ok := tryGetContextCollection(cmd)
if !ok {
return cmd.Usage()
}
Expand Down
185 changes: 185 additions & 0 deletions cli/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
// Copyright 2024 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 (
"os"
"path/filepath"
"strings"

"github.com/spf13/pflag"
"github.com/spf13/viper"

"github.com/sourcenetwork/defradb/logging"
)

const (
configStoreBadger = "badger"
configStoreMemory = "memory"
configLogFormatJSON = "json"
configLogFormatCSV = "csv"
configLogLevelInfo = "info"
configLogLevelDebug = "debug"
configLogLevelError = "error"
configLogLevelFatal = "fatal"
)

// configPaths are config keys that will be made relative to the rootdir
var configPaths = []string{
"datastore.badger.path",
"api.pubkeypath",
"api.privkeypath",
}

// configFlags is a mapping of config keys to cli flags to bind to.
var configFlags = map[string]string{
"log.level": "loglevel",
"log.output": "logoutput",
"log.format": "logformat",
"log.stacktrace": "logtrace",
"log.nocolor": "lognocolor",
"api.address": "url",
"datastore.maxtxnretries": "max-txn-retries",
"datastore.store": "store",
"datastore.badger.valuelogfilesize": "valuelogfilesize",
"net.peers": "peers",
"net.p2paddresses": "p2paddr",
"net.p2pdisabled": "no-p2p",
"api.allowed-origins": "allowed-origins",
"api.pubkeypath": "pubkeypath",
"api.privkeypath": "privkeypath",
}

// defaultConfig returns a new config with default values.
func defaultConfig() *viper.Viper {
cfg := viper.New()

cfg.AutomaticEnv()
cfg.SetEnvPrefix("DEFRA")
cfg.SetEnvKeyReplacer(strings.NewReplacer(".", "_", "-", "_"))

cfg.SetConfigName("config")
cfg.SetConfigType("yaml")

cfg.SetDefault("datastore.badger.path", "data")
cfg.SetDefault("net.pubSubEnabled", true)
cfg.SetDefault("net.relay", false)
cfg.SetDefault("log.caller", false)

return cfg
}

// createConfig writes the default config file if one does not exist.
func createConfig(rootdir string, flags *pflag.FlagSet) error {
cfg := defaultConfig()
fredcarle marked this conversation as resolved.
Show resolved Hide resolved
cfg.AddConfigPath(rootdir)

if err := bindConfigFlags(cfg, flags); err != nil {
return err
}
// make sure rootdir exists
if err := os.MkdirAll(rootdir, 0755); err != nil {
return err
}
err := cfg.SafeWriteConfig()
// error type is known and shouldn't be wrapped
//
//nolint:errorlint
if _, ok := err.(viper.ConfigFileAlreadyExistsError); ok {
return nil
}
return err
}

// loadConfig returns a new config with values from the config in the given rootdir.
func loadConfig(rootdir string, flags *pflag.FlagSet) (*viper.Viper, error) {
cfg := defaultConfig()
cfg.AddConfigPath(rootdir)

// attempt to read the existing config
err := cfg.ReadInConfig()
// error type is known and shouldn't be wrapped
//
//nolint:errorlint
if _, ok := err.(viper.ConfigFileNotFoundError); err != nil && !ok {
return nil, err
}
// bind cli flags to config keys
if err := bindConfigFlags(cfg, flags); err != nil {
return nil, err
}

// make paths relative to the rootdir
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

praise: Thank you for comments like this, it helps a lot when the code itself is non-obvious.

for _, key := range configPaths {
path := cfg.GetString(key)
if path != "" && !filepath.IsAbs(path) {
cfg.Set(key, filepath.Join(rootdir, path))
}
}

logCfg := loggingConfig(cfg.Sub("log"))
logCfg.OverridesByLoggerName = make(map[string]logging.Config)

// apply named logging overrides
for key := range cfg.GetStringMap("log.overrides") {
logCfg.OverridesByLoggerName[key] = loggingConfig(cfg.Sub("log.overrides." + key))
}
logging.SetConfig(logCfg)

return cfg, nil
}

// bindConfigFlags binds the set of cli flags to config values.
func bindConfigFlags(cfg *viper.Viper, flags *pflag.FlagSet) error {
for key, flag := range configFlags {
err := cfg.BindPFlag(key, flags.Lookup(flag))
if err != nil {
return err
}
}
return nil
}

// loggingConfig returns a new logging config from the given config.
func loggingConfig(cfg *viper.Viper) logging.Config {
var level int8
switch value := cfg.GetString("level"); value {
case configLogLevelDebug:
level = logging.Debug
case configLogLevelInfo:
level = logging.Info
case configLogLevelError:
level = logging.Error
case configLogLevelFatal:
level = logging.Fatal
default:
level = logging.Info
}

var format logging.EncoderFormat
switch value := cfg.GetString("format"); value {
case configLogFormatJSON:
format = logging.JSON
case configLogFormatCSV:
format = logging.CSV
default:
format = logging.CSV
}

return logging.Config{
Level: logging.NewLogLevelOption(level),
EnableStackTrace: logging.NewEnableStackTraceOption(cfg.GetBool("stacktrace")),
DisableColor: logging.NewDisableColorOption(cfg.GetBool("nocolor")),
EncoderFormat: logging.NewEncoderFormatOption(format),
OutputPaths: []string{cfg.GetString("output")},
EnableCaller: logging.NewEnableCallerOption(cfg.GetBool("caller")),
}
}
Loading
Loading