From 95c6d41c355d59a7d8217e3eb6bf3f33d978a7de Mon Sep 17 00:00:00 2001 From: Nicola Murino Date: Thu, 3 Dec 2020 17:16:35 +0100 Subject: [PATCH] config: make config file relative to the config dir a configuration parsing error is now fatal --- cmd/install_windows.go | 2 +- cmd/portable.go | 1 + cmd/root.go | 35 +++++++++++++++++------- config/config.go | 56 ++++++++++++++++++++++---------------- config/config_test.go | 41 ++++++++++++++++------------ docs/full-configuration.md | 5 ++-- service/service.go | 1 + 7 files changed, 87 insertions(+), 54 deletions(-) diff --git a/cmd/install_windows.go b/cmd/install_windows.go index 4a1a4606..5036831d 100644 --- a/cmd/install_windows.go +++ b/cmd/install_windows.go @@ -64,7 +64,7 @@ func getCustomServeFlags() []string { result = append(result, "--"+configDirFlag) result = append(result, configDir) } - if configFile != "" { + if configFile != defaultConfigFile { result = append(result, "--"+configFileFlag) result = append(result, configFile) } diff --git a/cmd/portable.go b/cmd/portable.go index c90b44d9..345088e5 100644 --- a/cmd/portable.go +++ b/cmd/portable.go @@ -124,6 +124,7 @@ Please take a look at the usage below to customize the serving parameters`, } service := service.Service{ ConfigDir: filepath.Clean(defaultConfigDir), + ConfigFile: defaultConfigFile, LogFilePath: portableLogFile, LogMaxSize: defaultLogMaxSize, LogMaxBackups: defaultLogMaxBackup, diff --git a/cmd/root.go b/cmd/root.go index 81f26be4..216fe7dd 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -15,6 +15,7 @@ const ( configDirFlag = "config-dir" configDirKey = "config_dir" configFileFlag = "config-file" + configFileKey = "config_file" logFilePathFlag = "log-file-path" logFilePathKey = "log_file_path" logMaxSizeFlag = "log-max-size" @@ -38,6 +39,7 @@ const ( loadDataCleanFlag = "loaddata-clean" loadDataCleanKey = "loaddata_clean" defaultConfigDir = "." + defaultConfigFile = "" defaultLogFile = "sftpgo.log" defaultLogMaxSize = 10 defaultLogMaxBackup = 5 @@ -92,22 +94,35 @@ func addConfigFlags(cmd *cobra.Command) { viper.SetDefault(configDirKey, defaultConfigDir) viper.BindEnv(configDirKey, "SFTPGO_CONFIG_DIR") //nolint:errcheck // err is not nil only if the key to bind is missing cmd.Flags().StringVarP(&configDir, configDirFlag, "c", viper.GetString(configDirKey), - `Location for SFTPGo config dir. This directory -should contain the "sftpgo" configuration file. -It is used as the base for files with a relative path -(eg. the private keys for the SFTP server, the SQLite -database if you use SQLite as data provider). + `Location for the config dir. This directory +is used as the base for files with a relative +path, eg. the private keys for the SFTP +server or the SQLite database if you use +SQLite as data provider. +The configuration file, if not explicitly set, +is looked for in this dir. We support reading +from JSON, TOML, YAML, HCL, envfile and Java +properties config files. The default config +file name is "sftpgo" and therefore +"sftpgo.json", "sftpgo.yaml" and so on are +searched. This flag can be set using SFTPGO_CONFIG_DIR env var too.`) viper.BindPFlag(configDirKey, cmd.Flags().Lookup(configDirFlag)) //nolint:errcheck - cmd.Flags().StringVar(&configFile, configFileFlag, os.Getenv("SFTPGO_CONFIG_FILE"), - `Path to SFTPGo configuration file. It must be -an absolute path to a file or a path relative to the working directory. -The specified file name must have a supported extension -(JSON, YAML, TOML or Java properties). + viper.SetDefault(configFileKey, defaultConfigFile) + viper.BindEnv(configFileKey, "SFTPGO_CONFIG_FILE") //nolint:errcheck + cmd.Flags().StringVar(&configFile, configFileFlag, viper.GetString(configFileKey), + `Path to SFTPGo configuration file. +This flag explicitly defines the path, name +and extension of the config file. If must be +an absolute path or a path relative to the +configuration directory. The specified file +name must have a supported extension (JSON, +YAML, TOML, HCL or Java properties). This flag can be set using SFTPGO_CONFIG_FILE env var too.`) + viper.BindPFlag(configFileKey, cmd.Flags().Lookup(configFileFlag)) //nolint:errcheck } func addServeFlags(cmd *cobra.Command) { diff --git a/config/config.go b/config/config.go index fceddf40..1da9322c 100644 --- a/config/config.go +++ b/config/config.go @@ -3,6 +3,7 @@ package config import ( "fmt" + "path/filepath" "strings" "github.com/spf13/viper" @@ -22,11 +23,11 @@ import ( const ( logSender = "config" - // configName defines the name for the default config file. - // This is the file name without extension, we use viper and so we - // support all the config files format supported by viper + // configName defines the name for config file. + // This name does not include the extension, viper will search for files + // with supported extensions such as "sftpgo.json", "sftpgo.yaml" and so on configName = "sftpgo" - // ConfigEnvPrefix defines a prefix that ENVIRONMENT variables will use + // ConfigEnvPrefix defines a prefix that environment variables will use configEnvPrefix = "sftpgo" ) @@ -286,26 +287,35 @@ func getRedactedGlobalConf() globalConfig { return conf } +func setConfigFile(configDir, configFile string) { + if configFile == "" { + return + } + if !filepath.IsAbs(configFile) && utils.IsFileInputValid(configFile) { + configFile = filepath.Join(configDir, configFile) + } + viper.SetConfigFile(configFile) +} + // LoadConfig loads the configuration // configDir will be added to the configuration search paths. // The search path contains by default the current directory and on linux it contains // $HOME/.config/sftpgo and /etc/sftpgo too. -// configFile is an absolute or relative path (to the working directory) to the configuration file. +// configFile is an absolute or relative path (to the config dir) to the configuration file. func LoadConfig(configDir, configFile string) error { var err error viper.AddConfigPath(configDir) setViperAdditionalConfigPaths() viper.AddConfigPath(".") - viper.SetConfigFile(configFile) + setConfigFile(configDir, configFile) if err = viper.ReadInConfig(); err != nil { logger.Warn(logSender, "", "error loading configuration file: %v", err) logger.WarnToConsole("error loading configuration file: %v", err) } err = viper.Unmarshal(&globalConf) if err != nil { - logger.Warn(logSender, "", "error parsing configuration file: %v. Default configuration will be used: %+v", - err, getRedactedGlobalConf()) - logger.WarnToConsole("error parsing configuration file: %v. Default configuration will be used.", err) + logger.Warn(logSender, "", "error parsing configuration file: %v", err) + logger.WarnToConsole("error parsing configuration file: %v", err) return err } checkCommonParamsCompatibility() @@ -322,34 +332,34 @@ func LoadConfig(configDir, configFile string) error { logger.WarnToConsole("Configuration error: %v", err) } if globalConf.Common.UploadMode < 0 || globalConf.Common.UploadMode > 2 { - err = fmt.Errorf("invalid upload_mode 0, 1 and 2 are supported, configured: %v reset upload_mode to 0", + warn := fmt.Sprintf("invalid upload_mode 0, 1 and 2 are supported, configured: %v reset upload_mode to 0", globalConf.Common.UploadMode) globalConf.Common.UploadMode = 0 - logger.Warn(logSender, "", "Configuration error: %v", err) - logger.WarnToConsole("Configuration error: %v", err) + logger.Warn(logSender, "", "Configuration error: %v", warn) + logger.WarnToConsole("Configuration error: %v", warn) } if globalConf.Common.ProxyProtocol < 0 || globalConf.Common.ProxyProtocol > 2 { - err = fmt.Errorf("invalid proxy_protocol 0, 1 and 2 are supported, configured: %v reset proxy_protocol to 0", + warn := fmt.Sprintf("invalid proxy_protocol 0, 1 and 2 are supported, configured: %v reset proxy_protocol to 0", globalConf.Common.ProxyProtocol) globalConf.Common.ProxyProtocol = 0 - logger.Warn(logSender, "", "Configuration error: %v", err) - logger.WarnToConsole("Configuration error: %v", err) + logger.Warn(logSender, "", "Configuration error: %v", warn) + logger.WarnToConsole("Configuration error: %v", warn) } if globalConf.ProviderConf.ExternalAuthScope < 0 || globalConf.ProviderConf.ExternalAuthScope > 7 { - err = fmt.Errorf("invalid external_auth_scope: %v reset to 0", globalConf.ProviderConf.ExternalAuthScope) + warn := fmt.Sprintf("invalid external_auth_scope: %v reset to 0", globalConf.ProviderConf.ExternalAuthScope) globalConf.ProviderConf.ExternalAuthScope = 0 - logger.Warn(logSender, "", "Configuration error: %v", err) - logger.WarnToConsole("Configuration error: %v", err) + logger.Warn(logSender, "", "Configuration error: %v", warn) + logger.WarnToConsole("Configuration error: %v", warn) } - if len(globalConf.ProviderConf.CredentialsPath) == 0 { - err = fmt.Errorf("invalid credentials path, reset to \"credentials\"") + if globalConf.ProviderConf.CredentialsPath == "" { + warn := "invalid credentials path, reset to \"credentials\"" globalConf.ProviderConf.CredentialsPath = "credentials" - logger.Warn(logSender, "", "Configuration error: %v", err) - logger.WarnToConsole("Configuration error: %v", err) + logger.Warn(logSender, "", "Configuration error: %v", warn) + logger.WarnToConsole("Configuration error: %v", warn) } checkHostKeyCompatibility() logger.Debug(logSender, "", "config file used: '%#v', config loaded: %+v", viper.ConfigFileUsed(), getRedactedGlobalConf()) - return err + return nil } func checkHostKeyCompatibility() { diff --git a/config/config_test.go b/config/config_test.go index 03a203b8..26b6aeab 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -42,16 +42,16 @@ func TestLoadConfigTest(t *testing.T) { assert.NotEqual(t, httpclient.Config{}, config.GetHTTPConfig()) confName := tempConfigName + ".json" configFilePath := filepath.Join(configDir, confName) - err = config.LoadConfig(configDir, configFilePath) + err = config.LoadConfig(configDir, confName) assert.NoError(t, err) err = ioutil.WriteFile(configFilePath, []byte("{invalid json}"), os.ModePerm) assert.NoError(t, err) - err = config.LoadConfig(configDir, configFilePath) + err = config.LoadConfig(configDir, confName) assert.NoError(t, err) err = ioutil.WriteFile(configFilePath, []byte("{\"sftpd\": {\"bind_port\": \"a\"}}"), os.ModePerm) assert.NoError(t, err) - err = config.LoadConfig(configDir, configFilePath) - assert.NotNil(t, err) + err = config.LoadConfig(configDir, confName) + assert.Error(t, err) err = os.Remove(configFilePath) assert.NoError(t, err) } @@ -71,7 +71,7 @@ func TestEmptyBanner(t *testing.T) { jsonConf, _ := json.Marshal(c) err = ioutil.WriteFile(configFilePath, jsonConf, os.ModePerm) assert.NoError(t, err) - err = config.LoadConfig(configDir, configFilePath) + err = config.LoadConfig(configDir, confName) assert.NoError(t, err) sftpdConf = config.GetSFTPDConfig() assert.NotEmpty(t, strings.TrimSpace(sftpdConf.Banner)) @@ -85,7 +85,7 @@ func TestEmptyBanner(t *testing.T) { jsonConf, _ = json.Marshal(c1) err = ioutil.WriteFile(configFilePath, jsonConf, os.ModePerm) assert.NoError(t, err) - err = config.LoadConfig(configDir, configFilePath) + err = config.LoadConfig(configDir, confName) assert.NoError(t, err) ftpdConf = config.GetFTPDConfig() assert.NotEmpty(t, strings.TrimSpace(ftpdConf.Banner)) @@ -109,8 +109,9 @@ func TestInvalidUploadMode(t *testing.T) { assert.NoError(t, err) err = ioutil.WriteFile(configFilePath, jsonConf, os.ModePerm) assert.NoError(t, err) - err = config.LoadConfig(configDir, configFilePath) - assert.NotNil(t, err) + err = config.LoadConfig(configDir, confName) + assert.NoError(t, err) + assert.Equal(t, 0, config.GetCommonConfig().UploadMode) err = os.Remove(configFilePath) assert.NoError(t, err) } @@ -131,8 +132,9 @@ func TestInvalidExternalAuthScope(t *testing.T) { assert.NoError(t, err) err = ioutil.WriteFile(configFilePath, jsonConf, os.ModePerm) assert.NoError(t, err) - err = config.LoadConfig(configDir, configFilePath) - assert.NotNil(t, err) + err = config.LoadConfig(configDir, confName) + assert.NoError(t, err) + assert.Equal(t, 0, config.GetProviderConf().ExternalAuthScope) err = os.Remove(configFilePath) assert.NoError(t, err) } @@ -153,8 +155,9 @@ func TestInvalidCredentialsPath(t *testing.T) { assert.NoError(t, err) err = ioutil.WriteFile(configFilePath, jsonConf, os.ModePerm) assert.NoError(t, err) - err = config.LoadConfig(configDir, configFilePath) - assert.NotNil(t, err) + err = config.LoadConfig(configDir, confName) + assert.NoError(t, err) + assert.Equal(t, "credentials", config.GetProviderConf().CredentialsPath) err = os.Remove(configFilePath) assert.NoError(t, err) } @@ -175,8 +178,9 @@ func TestInvalidProxyProtocol(t *testing.T) { assert.NoError(t, err) err = ioutil.WriteFile(configFilePath, jsonConf, os.ModePerm) assert.NoError(t, err) - err = config.LoadConfig(configDir, configFilePath) - assert.NotNil(t, err) + err = config.LoadConfig(configDir, confName) + assert.NoError(t, err) + assert.Equal(t, 0, config.GetCommonConfig().ProxyProtocol) err = os.Remove(configFilePath) assert.NoError(t, err) } @@ -197,8 +201,9 @@ func TestInvalidUsersBaseDir(t *testing.T) { assert.NoError(t, err) err = ioutil.WriteFile(configFilePath, jsonConf, os.ModePerm) assert.NoError(t, err) - err = config.LoadConfig(configDir, configFilePath) - assert.NotNil(t, err) + err = config.LoadConfig(configDir, confName) + assert.NoError(t, err) + assert.Empty(t, config.GetProviderConf().UsersBaseDir) err = os.Remove(configFilePath) assert.NoError(t, err) } @@ -225,7 +230,7 @@ func TestCommonParamsCompatibility(t *testing.T) { assert.NoError(t, err) err = ioutil.WriteFile(configFilePath, jsonConf, os.ModePerm) assert.NoError(t, err) - err = config.LoadConfig(configDir, configFilePath) + err = config.LoadConfig(configDir, confName) assert.NoError(t, err) commonConf := config.GetCommonConfig() assert.Equal(t, 21, commonConf.IdleTimeout) @@ -263,7 +268,7 @@ func TestHostKeyCompatibility(t *testing.T) { assert.NoError(t, err) err = ioutil.WriteFile(configFilePath, jsonConf, os.ModePerm) assert.NoError(t, err) - err = config.LoadConfig(configDir, configFilePath) + err = config.LoadConfig(configDir, confName) assert.NoError(t, err) sftpdConf = config.GetSFTPDConfig() assert.Equal(t, 2, len(sftpdConf.HostKeys)) diff --git a/docs/full-configuration.md b/docs/full-configuration.md index 29b83d91..3d02a9e0 100644 --- a/docs/full-configuration.md +++ b/docs/full-configuration.md @@ -24,8 +24,9 @@ Flags: The `serve` command supports the following flags: -- `--config-dir` string. Location of the config dir. This directory should contain the configuration file and is used as the base directory for any files that use a relative path (eg. the private keys for the SFTP server, the SQLite or bblot database if you use SQLite or bbolt as data provider). The default value is "." or the value of `SFTPGO_CONFIG_DIR` environment variable. -- `--config-file` string. Name of the configuration file. It must be the name of a file stored in `config-dir`, not the absolute path to the configuration file. The specified file name must have no extension because we automatically append JSON, YAML, TOML, HCL and Java extensions when we search for the file. The default value is "sftpgo" (and therefore `sftpgo.json`, `sftpgo.yaml` and so on are searched) or the value of `SFTPGO_CONFIG_FILE` environment variable. +- `--config-dir` string. Location of the config dir. This directory is used as the base for files with a relative path, eg. the private keys for the SFTP server or the SQLite database if you use SQLite as data provider. The configuration file, if not explicitly set, is looked for in this dir. We support reading from JSON, TOML, YAML, HCL, envfile and Java properties config files. The default config file name is `sftpgo` and therefore `sftpgo.json`, `sftpgo.yaml` and so on are searched. The default value is the working directory (".") or the value of `SFTPGO_CONFIG_DIR` environment variable. +- `--config-file` string. This flag explicitly defines the path, name and extension of the config file. If must be an absolute path or a path relative to the configuration directory. The specified file name must have a supported extension (JSON, YAML, TOML, HCL or Java properties). The default value is empty or the value of `SFTPGO_CONFIG_FILE` environment variable. +- `--loaddata-from` string. Load users and folders from this file. The file must be specified as absolute path and it must contain a backup obtained using the `dumpdata` REST API or compatible content. The default value is empty or the value of `SFTPGO_LOADDATA_FROM` environment variable. - `--loaddata-from` string. Load users and folders from this file. The file must be specified as absolute path and it must contain a backup obtained using the `dumpdata` REST API or compatible content. The default value is empty or the value of `SFTPGO_LOADDATA_FROM` environment variable. - `--loaddata-clean` boolean. Determine if the loaddata-from file should be removed after a successful load. Default `false` or the value of `SFTPGO_LOADDATA_CLEAN` environment variable (1 or `true`, 0 or `false`). - `--loaddata-mode`, integer. Restore mode for data to load. 0 means new users are added, existing users are updated. 1 means new users are added, existing users are not modified. Default 1 or the value of `SFTPGO_LOADDATA_MODE` environment variable. diff --git a/service/service.go b/service/service.go index 384d2df2..50e07168 100644 --- a/service/service.go +++ b/service/service.go @@ -72,6 +72,7 @@ func (s *Service) Start() error { err := config.LoadConfig(s.ConfigDir, s.ConfigFile) if err != nil { logger.Error(logSender, "", "error loading configuration: %v", err) + return err } } if !config.HasServicesToStart() {