sync: clean stale local pricelists and migrate runtime config handling
This commit is contained in:
66
cmd/qfs/config_migration_test.go
Normal file
66
cmd/qfs/config_migration_test.go
Normal file
@@ -0,0 +1,66 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"git.mchus.pro/mchus/quoteforge/internal/config"
|
||||
)
|
||||
|
||||
func TestMigrateConfigFileToRuntimeShapeDropsDeprecatedSections(t *testing.T) {
|
||||
t.Helper()
|
||||
dir := t.TempDir()
|
||||
path := filepath.Join(dir, "config.yaml")
|
||||
|
||||
legacy := `server:
|
||||
host: "0.0.0.0"
|
||||
port: 9191
|
||||
database:
|
||||
host: "legacy-db"
|
||||
port: 3306
|
||||
name: "RFQ_LOG"
|
||||
user: "old"
|
||||
password: "REDACTED_TEST_PASSWORD"
|
||||
pricing:
|
||||
default_method: "median"
|
||||
logging:
|
||||
level: "debug"
|
||||
format: "text"
|
||||
output: "stdout"
|
||||
`
|
||||
if err := os.WriteFile(path, []byte(legacy), 0644); err != nil {
|
||||
t.Fatalf("write legacy config: %v", err)
|
||||
}
|
||||
|
||||
cfg, err := config.Load(path)
|
||||
if err != nil {
|
||||
t.Fatalf("load legacy config: %v", err)
|
||||
}
|
||||
setConfigDefaults(cfg)
|
||||
if err := migrateConfigFileToRuntimeShape(path, cfg); err != nil {
|
||||
t.Fatalf("migrate config: %v", err)
|
||||
}
|
||||
|
||||
got, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
t.Fatalf("read migrated config: %v", err)
|
||||
}
|
||||
text := string(got)
|
||||
if strings.Contains(text, "database:") {
|
||||
t.Fatalf("migrated config still contains deprecated database section:\n%s", text)
|
||||
}
|
||||
if strings.Contains(text, "pricing:") {
|
||||
t.Fatalf("migrated config still contains deprecated pricing section:\n%s", text)
|
||||
}
|
||||
if !strings.Contains(text, "server:") || !strings.Contains(text, "logging:") {
|
||||
t.Fatalf("migrated config missing required sections:\n%s", text)
|
||||
}
|
||||
if !strings.Contains(text, "port: 9191") {
|
||||
t.Fatalf("migrated config did not preserve server port:\n%s", text)
|
||||
}
|
||||
if !strings.Contains(text, "level: debug") {
|
||||
t.Fatalf("migrated config did not preserve logging level:\n%s", text)
|
||||
}
|
||||
}
|
||||
112
cmd/qfs/main.go
112
cmd/qfs/main.go
@@ -1,6 +1,7 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"flag"
|
||||
@@ -34,6 +35,7 @@ import (
|
||||
"git.mchus.pro/mchus/quoteforge/internal/services"
|
||||
"git.mchus.pro/mchus/quoteforge/internal/services/sync"
|
||||
"github.com/gin-gonic/gin"
|
||||
"gopkg.in/yaml.v3"
|
||||
"gorm.io/driver/mysql"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/logger"
|
||||
@@ -62,18 +64,18 @@ func main() {
|
||||
slog.Info("starting qfs", "version", Version, "executable", exePath)
|
||||
appmeta.SetVersion(Version)
|
||||
|
||||
resolvedConfigPath, err := appstate.ResolveConfigPath(*configPath)
|
||||
if err != nil {
|
||||
slog.Error("failed to resolve config path", "error", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
resolvedLocalDBPath, err := appstate.ResolveDBPath(*localDBPath)
|
||||
if err != nil {
|
||||
slog.Error("failed to resolve local database path", "error", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
resolvedConfigPath, err := appstate.ResolveConfigPathNearDB(*configPath, resolvedLocalDBPath)
|
||||
if err != nil {
|
||||
slog.Error("failed to resolve config path", "error", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Migrate legacy project-local config path to the user state directory when using defaults.
|
||||
if *configPath == "" && os.Getenv("QFS_CONFIG_PATH") == "" {
|
||||
migratedFrom, migrateErr := appstate.MigrateLegacyFile(resolvedConfigPath, []string{"config.yaml"})
|
||||
@@ -113,6 +115,10 @@ func main() {
|
||||
}
|
||||
|
||||
// Load config for server settings (optional)
|
||||
if err := ensureDefaultConfigFile(resolvedConfigPath); err != nil {
|
||||
slog.Error("failed to ensure default config file", "path", resolvedConfigPath, "error", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
cfg, err := config.Load(resolvedConfigPath)
|
||||
if err != nil {
|
||||
if errors.Is(err, fs.ErrNotExist) {
|
||||
@@ -125,6 +131,10 @@ func main() {
|
||||
}
|
||||
}
|
||||
setConfigDefaults(cfg)
|
||||
if err := migrateConfigFileToRuntimeShape(resolvedConfigPath, cfg); err != nil {
|
||||
slog.Error("failed to migrate config file format", "path", resolvedConfigPath, "error", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
slog.Info("resolved runtime files", "config_path", resolvedConfigPath, "localdb_path", resolvedLocalDBPath)
|
||||
|
||||
setupLogger(cfg.Logging)
|
||||
@@ -316,6 +326,96 @@ func setConfigDefaults(cfg *config.Config) {
|
||||
}
|
||||
}
|
||||
|
||||
func ensureDefaultConfigFile(configPath string) error {
|
||||
if strings.TrimSpace(configPath) == "" {
|
||||
return fmt.Errorf("config path is empty")
|
||||
}
|
||||
if _, err := os.Stat(configPath); err == nil {
|
||||
return nil
|
||||
} else if !errors.Is(err, os.ErrNotExist) {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := os.MkdirAll(filepath.Dir(configPath), 0755); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
const defaultConfigYAML = `server:
|
||||
host: "127.0.0.1"
|
||||
port: 8080
|
||||
mode: "release"
|
||||
read_timeout: 30s
|
||||
write_timeout: 30s
|
||||
|
||||
logging:
|
||||
level: "info"
|
||||
format: "json"
|
||||
output: "stdout"
|
||||
`
|
||||
if err := os.WriteFile(configPath, []byte(defaultConfigYAML), 0644); err != nil {
|
||||
return err
|
||||
}
|
||||
slog.Info("created default config file", "path", configPath)
|
||||
return nil
|
||||
}
|
||||
|
||||
type runtimeServerConfig struct {
|
||||
Host string `yaml:"host"`
|
||||
Port int `yaml:"port"`
|
||||
Mode string `yaml:"mode"`
|
||||
ReadTimeout time.Duration `yaml:"read_timeout"`
|
||||
WriteTimeout time.Duration `yaml:"write_timeout"`
|
||||
}
|
||||
|
||||
type runtimeLoggingConfig struct {
|
||||
Level string `yaml:"level"`
|
||||
Format string `yaml:"format"`
|
||||
Output string `yaml:"output"`
|
||||
}
|
||||
|
||||
type runtimeConfigFile struct {
|
||||
Server runtimeServerConfig `yaml:"server"`
|
||||
Logging runtimeLoggingConfig `yaml:"logging"`
|
||||
}
|
||||
|
||||
// migrateConfigFileToRuntimeShape rewrites config.yaml in a minimal runtime format.
|
||||
// Deprecated sections from legacy configs are intentionally dropped.
|
||||
func migrateConfigFileToRuntimeShape(configPath string, cfg *config.Config) error {
|
||||
if cfg == nil {
|
||||
return fmt.Errorf("config is nil")
|
||||
}
|
||||
|
||||
runtimeCfg := runtimeConfigFile{
|
||||
Server: runtimeServerConfig{
|
||||
Host: cfg.Server.Host,
|
||||
Port: cfg.Server.Port,
|
||||
Mode: cfg.Server.Mode,
|
||||
ReadTimeout: cfg.Server.ReadTimeout,
|
||||
WriteTimeout: cfg.Server.WriteTimeout,
|
||||
},
|
||||
Logging: runtimeLoggingConfig{
|
||||
Level: cfg.Logging.Level,
|
||||
Format: cfg.Logging.Format,
|
||||
Output: cfg.Logging.Output,
|
||||
},
|
||||
}
|
||||
|
||||
rendered, err := yaml.Marshal(&runtimeCfg)
|
||||
if err != nil {
|
||||
return fmt.Errorf("marshal runtime config: %w", err)
|
||||
}
|
||||
|
||||
current, err := os.ReadFile(configPath)
|
||||
if err == nil && bytes.Equal(bytes.TrimSpace(current), bytes.TrimSpace(rendered)) {
|
||||
return nil
|
||||
}
|
||||
if err := os.WriteFile(configPath, rendered, 0644); err != nil {
|
||||
return fmt.Errorf("write runtime config: %w", err)
|
||||
}
|
||||
slog.Info("migrated config.yaml to runtime format", "path", configPath)
|
||||
return nil
|
||||
}
|
||||
|
||||
// runSetupMode starts a minimal server that only serves the setup page
|
||||
func runSetupMode(local *localdb.LocalDB) {
|
||||
restartSig := make(chan struct{}, 1)
|
||||
|
||||
Reference in New Issue
Block a user