fix: Rework InterceptConfigsPreRunHandler to make sure configuration … (#7501)

* fix: Rework InterceptConfigsPreRunHandler to make sure configuration is pulled from all Viper sources

* docs: Add configuration documentation in a new file
This commit is contained in:
Eric 2020-10-16 08:56:10 -05:00 committed by GitHub
parent 6e569e1255
commit 8384a5a180
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 511 additions and 17 deletions

27
server/doc.go Normal file
View File

@ -0,0 +1,27 @@
/*
The commands from the SDK are defined with `cobra` and configured with the
`viper` package.
This takes place in the `InterceptConfigsPreRunHandler` function.
Since the `viper` package is used for configuration the precedence is dictated
by that package. That is
1. Command line switches
2. Environment variables
3. Files from configuration values
4. Default values
The global configuration instance exposed by the `viper` package is not
used by Cosmos SDK in this function. A new instance of `viper.Viper` is created
and the following is performed. The environmental variable prefix is set
to the current program name. Environmental variables consider the underscore
to be equivalent to the `.` or `-` character. This means that an configuration
value called `rpc.laddr` would be read from an environmental variable called
`MYTOOL_RPC_LADDR` if the current program name is `mytool`.
Running the `InterceptConfigsPreRunHandler` also reads `app.toml`
and `config.toml` from the home directory under the `config` directory.
If `config.toml` or `app.toml` do not exist then those files are created
and populated with default values.
*/
package server

View File

@ -7,8 +7,10 @@ import (
"net"
"os"
"os/signal"
"path"
"path/filepath"
"strconv"
"strings"
"syscall"
"time"
@ -65,15 +67,31 @@ func NewContext(v *viper.Viper, config *tmcfg.Config, logger log.Logger) *Contex
// the application configuration. Command handlers can fetch the server Context
// to get the Tendermint configuration or to get access to Viper.
func InterceptConfigsPreRunHandler(cmd *cobra.Command) error {
rootViper := viper.New()
rootViper.BindPFlags(cmd.Flags())
rootViper.BindPFlags(cmd.PersistentFlags())
serverCtx := NewDefaultContext()
config, err := interceptConfigs(serverCtx, rootViper)
// Get the executable name and configure the viper instance so that environmental
// variables are checked based off that name. The underscore character is used
// as a separator
executableName, err := os.Executable()
if err != nil {
return err
}
basename := path.Base(executableName)
// Configure the viper instance
serverCtx.Viper.BindPFlags(cmd.Flags())
serverCtx.Viper.BindPFlags(cmd.PersistentFlags())
serverCtx.Viper.SetEnvPrefix(basename)
serverCtx.Viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_", "-", "_"))
serverCtx.Viper.AutomaticEnv()
// Intercept configuration files, using both Viper instances separately
config, err := interceptConfigs(serverCtx.Viper)
if err != nil {
return err
}
// Return value is a tendermint configuration object
serverCtx.Config = config
logger := log.NewTMLogger(log.NewSyncWriter(os.Stdout))
logger, err = tmflags.ParseLogLevel(config.LogLevel, logger, tmcfg.DefaultLogLevel())
@ -81,11 +99,12 @@ func InterceptConfigsPreRunHandler(cmd *cobra.Command) error {
return err
}
if rootViper.GetBool(tmcli.TraceFlag) {
// Check if the tendermint flag for trace logging is set
// if it is then setup a tracing logger in this app as well
if serverCtx.Viper.GetBool(tmcli.TraceFlag) {
logger = log.NewTracingLogger(logger)
}
serverCtx.Config = config
serverCtx.Logger = logger.With("module", "main")
return SetCmdServerContext(cmd, serverCtx)
@ -120,7 +139,7 @@ func SetCmdServerContext(cmd *cobra.Command, serverCtx *Context) error {
// configuration file. The Tendermint configuration file is parsed given a root
// Viper object, whereas the application is parsed with the private package-aware
// viperCfg object.
func interceptConfigs(ctx *Context, rootViper *viper.Viper) (*tmcfg.Config, error) {
func interceptConfigs(rootViper *viper.Viper) (*tmcfg.Config, error) {
rootDir := rootViper.GetString(flags.FlagHome)
configPath := filepath.Join(rootDir, "config")
configFile := filepath.Join(configPath, "config.toml")
@ -146,17 +165,19 @@ func interceptConfigs(ctx *Context, rootViper *viper.Viper) (*tmcfg.Config, erro
if err := rootViper.ReadInConfig(); err != nil {
return nil, fmt.Errorf("failed to read in app.toml: %w", err)
}
if err := rootViper.Unmarshal(conf); err != nil {
return nil, err
}
}
// Read into the configuration whatever data the viper instance has for it
// This may come from the configuration file above but also any of the other sources
// viper uses
if err := rootViper.Unmarshal(conf); err != nil {
return nil, err
}
conf.SetRoot(rootDir)
appConfigFilePath := filepath.Join(configPath, "app.toml")
if _, err := os.Stat(appConfigFilePath); os.IsNotExist(err) {
appConf, err := config.ParseConfig(ctx.Viper)
appConf, err := config.ParseConfig(rootViper)
if err != nil {
return nil, fmt.Errorf("failed to parse app.toml: %w", err)
}
@ -164,10 +185,10 @@ func interceptConfigs(ctx *Context, rootViper *viper.Viper) (*tmcfg.Config, erro
config.WriteConfigFile(appConfigFilePath, appConf)
}
ctx.Viper.SetConfigType("toml")
ctx.Viper.SetConfigName("app")
ctx.Viper.AddConfigPath(configPath)
if err := ctx.Viper.ReadInConfig(); err != nil {
rootViper.SetConfigType("toml")
rootViper.SetConfigName("app")
rootViper.AddConfigPath(configPath)
if err := rootViper.ReadInConfig(); err != nil {
return nil, fmt.Errorf("failed to read in app.toml: %w", err)
}

446
server/util_test.go Normal file
View File

@ -0,0 +1,446 @@
package server
import (
"context"
"errors"
"fmt"
"os"
"path"
"strings"
"testing"
"github.com/cosmos/cosmos-sdk/client/flags"
"github.com/spf13/cobra"
)
var CancelledInPreRun = errors.New("Canelled in prerun")
// Used in each test to run the function under test via Cobra
// but to always halt the command
func preRunETestImpl(cmd *cobra.Command, args []string) error {
err := InterceptConfigsPreRunHandler(cmd)
if err != nil {
return err
}
return CancelledInPreRun
}
func TestInterceptConfigsPreRunHandlerCreatesConfigFilesWhenMissing(t *testing.T) {
tempDir := t.TempDir()
cmd := StartCmd(nil, "/foobar")
if err := cmd.Flags().Set(flags.FlagHome, tempDir); err != nil {
t.Fatalf("Could not set home flag [%T] %v", err, err)
}
cmd.PreRunE = preRunETestImpl
serverCtx := &Context{}
ctx := context.WithValue(context.Background(), ServerContextKey, serverCtx)
if err := cmd.ExecuteContext(ctx); err != CancelledInPreRun {
t.Fatalf("function failed with [%T] %v", err, err)
}
// Test that config.toml is created
configTomlPath := path.Join(tempDir, "config", "config.toml")
s, err := os.Stat(configTomlPath)
if err != nil {
t.Fatalf("Could not stat config.toml after run %v", err)
}
if !s.Mode().IsRegular() {
t.Fatal("config.toml not created as regular file")
}
if s.Size() == 0 {
t.Fatal("config.toml created as empty file")
}
// Test that tendermint config is initialized
if serverCtx.Config == nil {
t.Fatal("tendermint config not created")
}
// Test that app.toml is created
appTomlPath := path.Join(tempDir, "config", "app.toml")
s, err = os.Stat(appTomlPath)
if err != nil {
t.Fatalf("Could not stat app.toml after run %v", err)
}
if !s.Mode().IsRegular() {
t.Fatal("appp.toml not created as regular file")
}
if s.Size() == 0 {
t.Fatal("config.toml created as empty file")
}
// Test that the config for use in server/start.go is created
if serverCtx.Viper == nil {
t.Error("app config Viper instance not created")
}
}
func TestInterceptConfigsPreRunHandlerReadsConfigToml(t *testing.T) {
const testDbBackend = "awesome_test_db"
tempDir := t.TempDir()
err := os.Mkdir(path.Join(tempDir, "config"), os.ModePerm)
if err != nil {
t.Fatalf("creating config dir failed: %v", err)
}
configTomlPath := path.Join(tempDir, "config", "config.toml")
writer, err := os.Create(configTomlPath)
if err != nil {
t.Fatalf("creating config.toml file failed: %v", err)
}
_, err = writer.WriteString(fmt.Sprintf("db_backend = '%s'\n", testDbBackend))
if err != nil {
t.Fatalf("Failed writing string to config.toml: %v", err)
}
if err := writer.Close(); err != nil {
t.Fatalf("Failed closing config.toml: %v", err)
}
cmd := StartCmd(nil, "/foobar")
if err := cmd.Flags().Set(flags.FlagHome, tempDir); err != nil {
t.Fatalf("Could not set home flag [%T] %v", err, err)
}
cmd.PreRunE = preRunETestImpl
serverCtx := &Context{}
ctx := context.WithValue(context.Background(), ServerContextKey, serverCtx)
if err := cmd.ExecuteContext(ctx); err != CancelledInPreRun {
t.Fatalf("function failed with [%T] %v", err, err)
}
if testDbBackend != serverCtx.Config.DBBackend {
t.Error("DBPath was not set from config.toml")
}
}
func TestInterceptConfigsPreRunHandlerReadsAppToml(t *testing.T) {
const testHaltTime = 1337
tempDir := t.TempDir()
err := os.Mkdir(path.Join(tempDir, "config"), os.ModePerm)
if err != nil {
t.Fatalf("creating config dir failed: %v", err)
}
appTomlPath := path.Join(tempDir, "config", "app.toml")
writer, err := os.Create(appTomlPath)
if err != nil {
t.Fatalf("creating app.toml file failed: %v", err)
}
_, err = writer.WriteString(fmt.Sprintf("halt-time = %d\n", testHaltTime))
if err != nil {
t.Fatalf("Failed writing string to app.toml: %v", err)
}
if err := writer.Close(); err != nil {
t.Fatalf("Failed closing app.toml: %v", err)
}
cmd := StartCmd(nil, tempDir)
cmd.PreRunE = preRunETestImpl
serverCtx := &Context{}
ctx := context.WithValue(context.Background(), ServerContextKey, serverCtx)
if err := cmd.ExecuteContext(ctx); err != CancelledInPreRun {
t.Fatalf("function failed with [%T] %v", err, err)
}
if testHaltTime != serverCtx.Viper.GetInt("halt-time") {
t.Error("Halt time was not set from app.toml")
}
}
func TestInterceptConfigsPreRunHandlerDoesNotMixConfigFiles(t *testing.T) {
// The goal of this test is to make sure that app.toml and config.toml
// are separate files and that mixing values does not work
const testDbBackend = "awesome_test_db"
const testHaltTime = 1337
const testHaltHeight = 2001
tempDir := t.TempDir()
err := os.Mkdir(path.Join(tempDir, "config"), os.ModePerm)
if err != nil {
t.Fatalf("creating config dir failed: %v", err)
}
configTomlPath := path.Join(tempDir, "config", "config.toml")
writer, err := os.Create(configTomlPath)
if err != nil {
t.Fatalf("creating config.toml file failed: %v", err)
}
// Put a value in config.toml that should be in app.toml
_, err = writer.WriteString(fmt.Sprintf("halt-time = %d\ndb_backend = \"%s\"\n", testHaltTime, testDbBackend))
if err != nil {
t.Fatalf("Failed writing string to config.toml: %v", err)
}
if err := writer.Close(); err != nil {
t.Fatalf("Failed closing config.toml: %v", err)
}
appTomlPath := path.Join(tempDir, "config", "app.toml")
writer, err = os.Create(appTomlPath)
if err != nil {
t.Fatalf("creating app.toml file failed %v", err)
}
// Put a different value in app.toml
_, err = writer.WriteString(fmt.Sprintf("halt-height = %d\n", testHaltHeight))
if err != nil {
t.Fatalf("Failed writing string to app.toml: %v", err)
}
if err := writer.Close(); err != nil {
t.Fatalf("Failed closing app.toml: %v", err)
}
cmd := StartCmd(nil, tempDir)
cmd.PreRunE = preRunETestImpl
serverCtx := &Context{}
ctx := context.WithValue(context.Background(), ServerContextKey, serverCtx)
if err := cmd.ExecuteContext(ctx); err != CancelledInPreRun {
t.Fatalf("function failed with [%T] %v", err, err)
}
// check that the intended value from config.toml is used
if testDbBackend != serverCtx.Config.DBBackend {
t.Error("DBPath was not set from config.toml")
}
// The value from app.toml should be used for this
if testHaltHeight != serverCtx.Viper.GetInt("halt-height") {
t.Error("Halt height is not using provided value")
}
// The value from config.toml should not be used, default is used instead
if 0 != serverCtx.Viper.GetInt("halt-time") {
t.Error("Halt time is not using default")
}
}
func TestInterceptConfigsPreRunHandlerReadsFlags(t *testing.T) {
const testAddr = "tcp://127.1.2.3:12345"
tempDir := t.TempDir()
cmd := StartCmd(nil, "/foobar")
if err := cmd.Flags().Set(flags.FlagHome, tempDir); err != nil {
t.Fatalf("Could not set home flag [%T] %v", err, err)
}
// This flag is added by tendermint
if err := cmd.Flags().Set("rpc.laddr", testAddr); err != nil {
t.Fatalf("Could not set address flag [%T] %v", err, err)
}
cmd.PreRunE = preRunETestImpl
serverCtx := &Context{}
ctx := context.WithValue(context.Background(), ServerContextKey, serverCtx)
if err := cmd.ExecuteContext(ctx); err != CancelledInPreRun {
t.Fatalf("function failed with [%T] %v", err, err)
}
if testAddr != serverCtx.Config.RPC.ListenAddress {
t.Error("RPCListenAddress was not set from command flags")
}
}
func TestInterceptConfigsPreRunHandlerReadsEnvVars(t *testing.T) {
const testAddr = "tcp://127.1.2.3:12345"
tempDir := t.TempDir()
cmd := StartCmd(nil, "/foobar")
if err := cmd.Flags().Set(flags.FlagHome, tempDir); err != nil {
t.Fatalf("Could not set home flag [%T] %v", err, err)
}
executableName, err := os.Executable()
if err != nil {
t.Fatalf("Could not get executable name: %v", err)
}
basename := path.Base(executableName)
basename = strings.ReplaceAll(basename, ".", "_")
// This is added by tendermint
envVarName := fmt.Sprintf("%s_RPC_LADDR", strings.ToUpper(basename))
os.Setenv(envVarName, testAddr)
t.Cleanup(func() {
os.Unsetenv(envVarName)
})
cmd.PreRunE = preRunETestImpl
serverCtx := &Context{}
ctx := context.WithValue(context.Background(), ServerContextKey, serverCtx)
if err := cmd.ExecuteContext(ctx); err != CancelledInPreRun {
t.Fatalf("function failed with [%T] %v", err, err)
}
if testAddr != serverCtx.Config.RPC.ListenAddress {
t.Errorf("RPCListenAddress was not set from env. var. %q", envVarName)
}
}
/*
The following tests are here to check the precedence of each
of the configuration sources. A common setup functionality is used
to avoid duplication of code between tests.
*/
var (
TestAddrExpected = "tcp://127.126.125.124:12345" // expected to be used in test
TestAddrNotExpected = "tcp://127.127.127.127:11111" // not expected to be used in test
)
type precedenceCommon struct {
envVarName string
flagName string
configTomlPath string
cmd *cobra.Command
}
func newPrecedenceCommon(t *testing.T) precedenceCommon {
retval := precedenceCommon{}
// Determine the env. var. name based off the executable name
executableName, err := os.Executable()
if err != nil {
t.Fatalf("Could not get executable name: %v", err)
}
basename := path.Base(executableName)
basename = strings.ReplaceAll(basename, ".", "_")
basename = strings.ReplaceAll(basename, "-", "_")
// Store the name of the env. var.
retval.envVarName = fmt.Sprintf("%s_RPC_LADDR", strings.ToUpper(basename))
// Store the flag name. This flag is added by tendermint
retval.flagName = "rpc.laddr"
// Create a tempdir and create './config' under that
tempDir := t.TempDir()
err = os.Mkdir(path.Join(tempDir, "config"), os.ModePerm)
if err != nil {
t.Fatalf("creating config dir failed: %v", err)
}
// Store the path for config.toml
retval.configTomlPath = path.Join(tempDir, "config", "config.toml")
// always remove the env. var. after each test execution
t.Cleanup(func() {
// This should not fail but if it does just panic
if err := os.Unsetenv(retval.envVarName); err != nil {
panic("Could not clear configuration env. var. used in test")
}
})
// Set up the command object that is used in this test
retval.cmd = StartCmd(nil, tempDir)
retval.cmd.PreRunE = preRunETestImpl
return retval
}
func (v precedenceCommon) setAll(t *testing.T, setFlag *string, setEnvVar *string, setConfigFile *string) {
if setFlag != nil {
if err := v.cmd.Flags().Set(v.flagName, *setFlag); err != nil {
t.Fatalf("Failed setting flag %q", v.flagName)
}
}
if setEnvVar != nil {
os.Setenv(v.envVarName, *setEnvVar)
}
if setConfigFile != nil {
writer, err := os.Create(v.configTomlPath)
if err != nil {
t.Fatalf("creating config.toml file failed: %v", err)
}
_, err = writer.WriteString(fmt.Sprintf("[rpc]\nladdr = \"%s\"\n", *setConfigFile))
if err != nil {
t.Fatalf("Failed writing string to config.toml: %v", err)
}
if err := writer.Close(); err != nil {
t.Fatalf("Failed closing config.toml: %v", err)
}
}
}
func TestInterceptConfigsPreRunHandlerPrecedenceFlag(t *testing.T) {
testCommon := newPrecedenceCommon(t)
testCommon.setAll(t, &TestAddrExpected, &TestAddrNotExpected, &TestAddrNotExpected)
serverCtx := &Context{}
ctx := context.WithValue(context.Background(), ServerContextKey, serverCtx)
if err := testCommon.cmd.ExecuteContext(ctx); err != CancelledInPreRun {
t.Fatalf("function failed with [%T] %v", err, err)
}
if TestAddrExpected != serverCtx.Config.RPC.ListenAddress {
t.Fatalf("RPCListenAddress was not set from flag %q", testCommon.flagName)
}
}
func TestInterceptConfigsPreRunHandlerPrecedenceEnvVar(t *testing.T) {
testCommon := newPrecedenceCommon(t)
testCommon.setAll(t, nil, &TestAddrExpected, &TestAddrNotExpected)
serverCtx := &Context{}
ctx := context.WithValue(context.Background(), ServerContextKey, serverCtx)
if err := testCommon.cmd.ExecuteContext(ctx); err != CancelledInPreRun {
t.Fatalf("function failed with [%T] %v", err, err)
}
if TestAddrExpected != serverCtx.Config.RPC.ListenAddress {
t.Errorf("RPCListenAddress was not set from env. var. %q", testCommon.envVarName)
}
}
func TestInterceptConfigsPreRunHandlerPrecedenceConfigFile(t *testing.T) {
testCommon := newPrecedenceCommon(t)
testCommon.setAll(t, nil, nil, &TestAddrExpected)
serverCtx := &Context{}
ctx := context.WithValue(context.Background(), ServerContextKey, serverCtx)
if err := testCommon.cmd.ExecuteContext(ctx); err != CancelledInPreRun {
t.Fatalf("function failed with [%T] %v", err, err)
}
if TestAddrExpected != serverCtx.Config.RPC.ListenAddress {
t.Errorf("RPCListenAddress was not read from file %q", testCommon.configTomlPath)
}
}
func TestInterceptConfigsPreRunHandlerPrecedenceConfigDefault(t *testing.T) {
testCommon := newPrecedenceCommon(t)
// Do not set anything
serverCtx := &Context{}
ctx := context.WithValue(context.Background(), ServerContextKey, serverCtx)
if err := testCommon.cmd.ExecuteContext(ctx); err != CancelledInPreRun {
t.Fatalf("function failed with [%T] %v", err, err)
}
if "tcp://127.0.0.1:26657" != serverCtx.Config.RPC.ListenAddress {
t.Error("RPCListenAddress is not using default")
}
}