From 1fd230394e57e09895deb339d4445c6039cb0afd Mon Sep 17 00:00:00 2001 From: Nate Date: Thu, 19 Dec 2024 14:27:52 -0800 Subject: [PATCH] config: attempt to upgrade existing configs instead of erroring --- ...ade_existing_configs_instead_of_exiting.md | 5 + cmd/hostd/main.go | 26 +-- config/compat.go | 172 ++++++++++++++++++ config/config.go | 35 +++- 4 files changed, 212 insertions(+), 26 deletions(-) create mode 100644 .changeset/attempt_to_upgrade_existing_configs_instead_of_exiting.md create mode 100644 config/compat.go diff --git a/.changeset/attempt_to_upgrade_existing_configs_instead_of_exiting.md b/.changeset/attempt_to_upgrade_existing_configs_instead_of_exiting.md new file mode 100644 index 00000000..41db4015 --- /dev/null +++ b/.changeset/attempt_to_upgrade_existing_configs_instead_of_exiting.md @@ -0,0 +1,5 @@ +--- +default: minor +--- + +# Attempt to upgrade existing configs instead of exiting diff --git a/cmd/hostd/main.go b/cmd/hostd/main.go index cb3861bc..f14424a0 100644 --- a/cmd/hostd/main.go +++ b/cmd/hostd/main.go @@ -18,7 +18,6 @@ import ( "go.sia.tech/hostd/persist/sqlite" "go.uber.org/zap" "go.uber.org/zap/zapcore" - "gopkg.in/yaml.v3" "lukechampine.com/flagg" ) @@ -65,7 +64,6 @@ var ( }, }, Log: config.Log{ - Path: os.Getenv(logFileEnvVar), // deprecated. included for compatibility. Level: "info", File: config.LogFile{ Enabled: true, @@ -104,20 +102,8 @@ func tryLoadConfig() { configPath = str } - // If the config file doesn't exist, don't try to load it. - if _, err := os.Stat(configPath); os.IsNotExist(err) { - return - } - - f, err := os.Open(configPath) - checkFatalError("failed to open config file", err) - defer f.Close() - - dec := yaml.NewDecoder(f) - dec.KnownFields(true) - - if err := dec.Decode(&cfg); err != nil { - fmt.Println("failed to decode config file:", err) + if err := config.LoadFile(configPath, &cfg); err != nil && !errors.Is(err, os.ErrNotExist) { + fmt.Println("failed to load config file:", err) os.Exit(1) } } @@ -427,13 +413,7 @@ func main() { // normalize log path if cfg.Log.File.Path == "" { - // If the log path is not set, try the deprecated log path. If that - // is also not set, default to hostd.log in the data directory. - if cfg.Log.Path != "" { - cfg.Log.File.Path = filepath.Join(cfg.Log.Path, "hostd.log") - } else { - cfg.Log.File.Path = filepath.Join(cfg.Directory, "hostd.log") - } + cfg.Log.File.Path = filepath.Join(cfg.Directory, "hostd.log") } // configure file logging diff --git a/config/compat.go b/config/compat.go new file mode 100644 index 00000000..f3ea2696 --- /dev/null +++ b/config/compat.go @@ -0,0 +1,172 @@ +package config + +import ( + "fmt" + "io" + "os" + + "gopkg.in/yaml.v3" +) + +type configV112 struct { + Name string `yaml:"name,omitempty"` + Directory string `yaml:"directory,omitempty"` + RecoveryPhrase string `yaml:"recoveryPhrase,omitempty"` + AutoOpenWebUI bool `yaml:"autoOpenWebUI,omitempty"` + + HTTP struct { + Address string `yaml:"address,omitempty"` + Password string `yaml:"password,omitempty"` + } `yaml:"http,omitempty"` + Consensus struct { + GatewayAddress string `yaml:"gatewayAddress,omitempty"` + Bootstrap bool `yaml:"bootstrap,omitempty"` + Peers []string `yaml:"peers,omitempty"` + } `yaml:"consensus,omitempty"` + Explorer struct { + Disable bool `yaml:"disable,omitempty"` + URL string `yaml:"url,omitempty"` + } `yaml:"explorer,omitempty"` + RHP2 struct { + Address string `yaml:"address,omitempty"` + } `yaml:"rhp2,omitempty"` + RHP3 struct { + TCPAddress string `yaml:"tcp,omitempty"` + WebSocketAddress string `yaml:"websocket,omitempty"` + CertPath string `yaml:"certPath,omitempty"` + KeyPath string `yaml:"keyPath,omitempty"` + } `yaml:"rhp3,omitempty"` + Log struct { + // Path is the directory to store the hostd.log file. + // Deprecated: use File.Path instead. + Path string `yaml:"path,omitempty"` + Level string `yaml:"level,omitempty"` // global log level + StdOut struct { + Level string `yaml:"level,omitempty"` // override the stdout log level + Enabled bool `yaml:"enabled,omitempty"` + Format string `yaml:"format,omitempty"` + EnableANSI bool `yaml:"enableANSI,omitempty"` //nolint:tagliatelle + } `yaml:"stdout,omitempty"` + File struct { + Enabled bool `yaml:"enabled,omitempty"` + Level string `yaml:"level,omitempty"` // override the file log level + Format string `yaml:"format,omitempty"` + // Path is the path of the log file. + Path string `yaml:"path,omitempty"` + } `yaml:"file,omitempty"` + } `yaml:"log,omitempty"` +} + +// updateConfigV112 updates the config file from v1.1.2 to the latest version. +// It returns an error if the config file cannot be updated. +func updateConfigV112(fp string, r io.Reader, cfg *Config) error { + dec := yaml.NewDecoder(r) + + old := configV112{ + Consensus: struct { + GatewayAddress string `yaml:"gatewayAddress,omitempty"` + Bootstrap bool `yaml:"bootstrap,omitempty"` + Peers []string `yaml:"peers,omitempty"` + }{ + GatewayAddress: ":9981", + Bootstrap: true, + }, + RHP2: struct { + Address string `yaml:"address,omitempty"` + }{ + Address: ":9982", + }, + RHP3: struct { + TCPAddress string `yaml:"tcp,omitempty"` + WebSocketAddress string `yaml:"websocket,omitempty"` + CertPath string `yaml:"certPath,omitempty"` + KeyPath string `yaml:"keyPath,omitempty"` + }{ + TCPAddress: ":9983", + }, + Log: struct { + Path string `yaml:"path,omitempty"` + Level string `yaml:"level,omitempty"` + StdOut struct { + Level string `yaml:"level,omitempty"` // override the stdout log level + Enabled bool `yaml:"enabled,omitempty"` + Format string `yaml:"format,omitempty"` + EnableANSI bool `yaml:"enableANSI,omitempty"` //nolint:tagliatelle + } `yaml:"stdout,omitempty"` + File struct { + Enabled bool `yaml:"enabled,omitempty"` + Level string `yaml:"level,omitempty"` // override the file log level + Format string `yaml:"format,omitempty"` + // Path is the path of the log file. + Path string `yaml:"path,omitempty"` + } `yaml:"file,omitempty"` + }{ + Level: "info", + StdOut: struct { + Level string `yaml:"level,omitempty"` + Enabled bool `yaml:"enabled,omitempty"` + Format string `yaml:"format,omitempty"` + EnableANSI bool `yaml:"enableANSI,omitempty"` + }{ + Level: "info", + Enabled: true, + Format: "human", + }, + File: struct { + Enabled bool `yaml:"enabled,omitempty"` + Level string `yaml:"level,omitempty"` // override the file log level + Format string `yaml:"format,omitempty"` + // Path is the path of the log file. + Path string `yaml:"path,omitempty"` + }{ + Enabled: true, + Level: "info", + Format: "json", + }, + }, + } + if err := dec.Decode(&old); err != nil { + return fmt.Errorf("failed to decode config file: %w", err) + } + + cfg.Name = old.Name + cfg.Directory = old.Directory + cfg.RecoveryPhrase = old.RecoveryPhrase + cfg.AutoOpenWebUI = old.AutoOpenWebUI + cfg.HTTP.Address = old.HTTP.Address + cfg.HTTP.Password = old.HTTP.Password + cfg.Syncer.Address = old.Consensus.GatewayAddress + cfg.Syncer.Bootstrap = old.Consensus.Bootstrap + cfg.Syncer.Peers = old.Consensus.Peers + cfg.Explorer.Disable = old.Explorer.Disable + cfg.Explorer.URL = old.Explorer.URL + cfg.RHP2.Address = old.RHP2.Address + cfg.RHP3.TCPAddress = old.RHP3.TCPAddress + cfg.Log.Level = old.Log.Level + if cfg.Log.File.Path != "" { + cfg.Log.File.Path = old.Log.File.Path + } else { + cfg.Log.File.Path = old.Log.Path + } + cfg.Log.StdOut.Level = old.Log.StdOut.Level + cfg.Log.StdOut.Enabled = old.Log.StdOut.Enabled + cfg.Log.StdOut.Format = old.Log.StdOut.Format + cfg.Log.StdOut.EnableANSI = old.Log.StdOut.EnableANSI + cfg.Log.File.Enabled = old.Log.File.Enabled + cfg.Log.File.Level = old.Log.File.Level + cfg.Log.File.Format = old.Log.File.Format + cfg.Log.File.Path = old.Log.File.Path + + f, err := os.Create(fp) + if err != nil { + return fmt.Errorf("failed to open file: %w", err) + } + defer f.Close() + + enc := yaml.NewEncoder(f) + enc.SetIndent(2) + if err := enc.Encode(cfg); err != nil { + return fmt.Errorf("failed to encode config file: %w", err) + } + return nil +} diff --git a/config/config.go b/config/config.go index f899eee5..676a5ffa 100644 --- a/config/config.go +++ b/config/config.go @@ -1,5 +1,13 @@ package config +import ( + "bytes" + "fmt" + "os" + + "gopkg.in/yaml.v3" +) + type ( // HTTP contains the configuration for the HTTP server. HTTP struct { @@ -67,9 +75,6 @@ type ( // Log contains the configuration for the logger. Log struct { - // Path is the directory to store the hostd.log file. - // Deprecated: use File.Path instead. - Path string `yaml:"path,omitempty"` Level string `yaml:"level,omitempty"` // global log level StdOut StdOut `yaml:"stdout,omitempty"` File LogFile `yaml:"file,omitempty"` @@ -92,3 +97,27 @@ type ( Log Log `yaml:"log,omitempty"` } ) + +// LoadFile loads the configuration from the provided file path. +// If the file does not exist, an error is returned. +// If the file exists but cannot be decoded, the function will attempt +// to upgrade the config file. +func LoadFile(fp string, cfg *Config) error { + buf, err := os.ReadFile(fp) + if err != nil { + return fmt.Errorf("failed to read config file: %w", err) + } + + r := bytes.NewReader(buf) + dec := yaml.NewDecoder(r) + dec.KnownFields(true) + + if err := dec.Decode(cfg); err != nil { + r.Reset(buf) + if upgradeErr := updateConfigV112(fp, r, cfg); upgradeErr == nil { + return nil + } + return fmt.Errorf("failed to decode config file: %w", err) + } + return nil +}