diff --git a/server/doc.go b/server/doc.go new file mode 100644 index 0000000000..befac1302e --- /dev/null +++ b/server/doc.go @@ -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 diff --git a/server/util.go b/server/util.go index a91862b2fc..8407f9b492 100644 --- a/server/util.go +++ b/server/util.go @@ -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) } diff --git a/server/util_test.go b/server/util_test.go new file mode 100644 index 0000000000..7cc10b3542 --- /dev/null +++ b/server/util_test.go @@ -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") + } +}