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
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 @@

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 := setRootDirContext(cmd); err != nil {
return err
}

Check warning on line 27 in cli/client.go

View check run for this annotation

Codecov / codecov/patch

cli/client.go#L26-L27

Added lines #L26 - L27 were not covered by tests
if err := setConfigContext(cmd); err != nil {
return err
}
if err := setTransactionContext(cmd, cfg, txID); err != nil {
if err := setTransactionContext(cmd, txID); err != nil {
return err
}
return setStoreContext(cmd, cfg)
return setStoreContext(cmd)
},
}
cmd.PersistentFlags().Uint64Var(&txID, "tx", 0, "Transaction ID")
Expand Down
12 changes: 7 additions & 5 deletions cli/collection.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,10 @@
"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,13 +30,16 @@
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 := setRootDirContext(cmd); err != nil {
return err
}
if err := setTransactionContext(cmd, cfg, txID); err != nil {
if err := setConfigContext(cmd); err != nil {
return err
}
if err := setStoreContext(cmd, cfg); err != nil {
if err := setTransactionContext(cmd, txID); err != nil {
return err
}

Check warning on line 41 in cli/collection.go

View check run for this annotation

Codecov / codecov/patch

cli/collection.go#L40-L41

Added lines #L40 - L41 were not covered by tests
if err := setStoreContext(cmd); err != nil {
return err
}
store := mustGetStoreContext(cmd)
Expand Down
178 changes: 178 additions & 0 deletions cli/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
// 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 (
_ "embed"
fredcarle marked this conversation as resolved.
Show resolved Hide resolved
"os"
"path/filepath"
"strings"

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

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

// 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.badger.inMemory": "in-memory",
"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.maxTxnRetries", 5)

cfg.SetDefault("datastore.badger.path", "data")
cfg.SetDefault("datastore.badger.inMemory", false)
cfg.SetDefault("datastore.badger.valueLogFileSize", 1<<30)

cfg.SetDefault("api.address", "127.0.0.1:9181")
cfg.SetDefault("api.pubKeyPath", "")
cfg.SetDefault("api.privKeyPath", "")
cfg.SetDefault("api.allowed-origins", []string{})

cfg.SetDefault("net.p2pDisabled", false)
cfg.SetDefault("net.p2pAddresses", []any{"/ip4/127.0.0.1/tcp/9171"})
cfg.SetDefault("net.pubSubEnabled", true)
cfg.SetDefault("net.relay", false)
cfg.SetDefault("net.peers", []string{})

cfg.SetDefault("log.level", "info")
cfg.SetDefault("log.stackTrace", true)
cfg.SetDefault("log.format", "csv")
cfg.SetDefault("log.output", "stderr")
cfg.SetDefault("log.noColor", false)
cfg.SetDefault("log.caller", false)

return cfg
}

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

// make sure rootdir exists
if err := os.MkdirAll(rootdir, 0755); err != nil {
return err
}

Check warning on line 97 in cli/config.go

View check run for this annotation

Codecov / codecov/patch

cli/config.go#L96-L97

Added lines #L96 - L97 were not covered by tests
err := cfg.SafeWriteConfig()
if _, ok := err.(viper.ConfigFileAlreadyExistsError); ok { //nolint:errorlint
Copy link
Contributor

Choose a reason for hiding this comment

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

todo: Can you please document the reasons for the nolint stuff with code-comments please (in all locations).

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()
if _, ok := err.(viper.ConfigFileNotFoundError); err != nil && !ok { //nolint:errorlint
return nil, err
}

Check warning on line 114 in cli/config.go

View check run for this annotation

Codecov / codecov/patch

cli/config.go#L113-L114

Added lines #L113 - L114 were not covered by tests

// bind cli flags to config keys
for key, flag := range configFlags {
err := cfg.BindPFlag(key, flags.Lookup(flag))
if err != nil {
return nil, err
}

Check warning on line 121 in cli/config.go

View check run for this annotation

Codecov / codecov/patch

cli/config.go#L120-L121

Added lines #L120 - L121 were not covered by tests
}

// 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))
}

Check warning on line 138 in cli/config.go

View check run for this annotation

Codecov / codecov/patch

cli/config.go#L137-L138

Added lines #L137 - L138 were not covered by tests
logging.SetConfig(logCfg)

return cfg, 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 "debug":
level = logging.Debug

Check warning on line 149 in cli/config.go

View check run for this annotation

Codecov / codecov/patch

cli/config.go#L148-L149

Added lines #L148 - L149 were not covered by tests
case "info":
level = logging.Info
case "error":
level = logging.Error
case "fatal":
level = logging.Fatal
default:
level = logging.Info

Check warning on line 157 in cli/config.go

View check run for this annotation

Codecov / codecov/patch

cli/config.go#L152-L157

Added lines #L152 - L157 were not covered by tests
}

var format logging.EncoderFormat
switch value := cfg.GetString("format"); value {
case "json": //nolint:goconst
Copy link
Contributor

Choose a reason for hiding this comment

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

suggestion: It is annoying that the linter flags this, but I think that using a const (even though that is also bad here) is slightly less bad that a nolint.

Copy link
Member Author

Choose a reason for hiding this comment

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

I moved all of them to const for now. They should be removed when the new logging package is ready.

format = logging.JSON

Check warning on line 163 in cli/config.go

View check run for this annotation

Codecov / codecov/patch

cli/config.go#L162-L163

Added lines #L162 - L163 were not covered by tests
case "csv":
format = logging.CSV
default:
format = logging.CSV

Check warning on line 167 in cli/config.go

View check run for this annotation

Codecov / codecov/patch

cli/config.go#L166-L167

Added lines #L166 - L167 were not covered by tests
}

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")),
}
}
61 changes: 61 additions & 0 deletions cli/config_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
// 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 (
"path/filepath"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestCreateConfig(t *testing.T) {
rootdir := t.TempDir()
err := createConfig(rootdir)
require.NoError(t, err)

// ensure no errors when config already exists
err = createConfig(rootdir)
require.NoError(t, err)

assert.FileExists(t, filepath.Join(rootdir, "config.yaml"))
}

func TestLoadConfigNotExist(t *testing.T) {
rootdir := t.TempDir()
cfg, err := loadConfig(rootdir, NewDefraCommand().PersistentFlags())
require.NoError(t, err)

assert.Equal(t, 5, cfg.GetInt("datastore.maxtxnretries"))

assert.Equal(t, filepath.Join(rootdir, "data"), cfg.GetString("datastore.badger.path"))
assert.Equal(t, 1<<30, cfg.GetInt("datastore.badger.valuelogfilesize"))
assert.Equal(t, false, cfg.GetBool("datastore.badger.inmemory"))

assert.Equal(t, "127.0.0.1:9181", cfg.GetString("api.address"))
assert.Equal(t, []string{}, cfg.GetStringSlice("api.allowed-origins"))
assert.Equal(t, "", cfg.GetString("api.pubkeypath"))
assert.Equal(t, "", cfg.GetString("api.privkeypath"))

assert.Equal(t, false, cfg.GetBool("net.p2pdisabled"))
assert.Equal(t, []string{"/ip4/127.0.0.1/tcp/9171"}, cfg.GetStringSlice("net.p2paddresses"))
assert.Equal(t, true, cfg.GetBool("net.pubsubenabled"))
assert.Equal(t, false, cfg.GetBool("net.relay"))
assert.Equal(t, []string{}, cfg.GetStringSlice("net.peers"))

assert.Equal(t, "info", cfg.GetString("log.level"))
assert.Equal(t, true, cfg.GetBool("log.stacktrace"))
assert.Equal(t, "csv", cfg.GetString("log.format"))
assert.Equal(t, "stderr", cfg.GetString("log.output"))
assert.Equal(t, false, cfg.GetBool("log.nocolor"))
assert.Equal(t, false, cfg.GetBool("log.caller"))
}
Loading
Loading