Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions internal/config/manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"sync"
"time"

"github.com/go-viper/mapstructure/v2"
"github.com/javi11/altmount/internal/utils"
"github.com/javi11/nntppool/v4"
"github.com/jinzhu/copier"
Expand Down Expand Up @@ -1348,6 +1349,8 @@ func (m *Manager) ReloadConfig() error {
return fmt.Errorf("error reading config file %s: %w", m.configFile, err)
}

warnUnknownConfigKeys()

// Create default config and unmarshal into it
config := DefaultConfig()
if err := viper.Unmarshal(config); err != nil {
Expand Down Expand Up @@ -1766,6 +1769,23 @@ func SaveToFile(config *Config, filename string) error {
}

// LoadConfig loads configuration from file and merges with defaults
// warnUnknownConfigKeys logs config keys that do not map to any field on Config.
// These are almost always settings removed or renamed in a past release: viper
// ignores them silently, so a stale key looks live while having no effect.
//
// This warns rather than fails — rejecting unknown keys would break upgrades for
// anyone whose config still carries a retired setting.
func warnUnknownConfigKeys() {
probe := DefaultConfig()
err := viper.Unmarshal(probe, func(dc *mapstructure.DecoderConfig) {
dc.ErrorUnused = true
})
if err != nil {
slog.Warn("Configuration contains keys this version does not use; they have no effect and can be removed",
"detail", err)
}
}

func LoadConfig(configFile string) (*Config, error) {
config := DefaultConfig()

Expand Down Expand Up @@ -1808,6 +1828,8 @@ func LoadConfig(configFile string) (*Config, error) {
}
}

warnUnknownConfigKeys()

// Unmarshal the config
if err := viper.Unmarshal(config); err != nil {
return nil, fmt.Errorf("error unmarshaling config: %w", err)
Expand Down
68 changes: 68 additions & 0 deletions internal/config/unknown_keys_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package config

import (
"bytes"
"log/slog"
"os"
"path/filepath"
"strings"
"testing"

"github.com/spf13/viper"
)

// captureWarn runs fn with slog routed to a buffer and returns what was logged.
func captureWarn(t *testing.T, fn func()) string {
t.Helper()
var buf bytes.Buffer
prev := slog.Default()
slog.SetDefault(slog.New(slog.NewTextHandler(&buf, &slog.HandlerOptions{Level: slog.LevelWarn})))
defer slog.SetDefault(prev)
fn()
return buf.String()
}

func writeConfig(t *testing.T, body string) string {
t.Helper()
dir := t.TempDir()
path := filepath.Join(dir, "config.yaml")
if err := os.WriteFile(path, []byte(body), 0o600); err != nil {
t.Fatalf("write config: %v", err)
}
return path
}

func TestWarnUnknownConfigKeys_ReportsRetiredKey(t *testing.T) {
path := writeConfig(t, `
import:
max_processor_workers: 4
max_import_connections: 60
`)
viper.Reset()
viper.SetConfigFile(path)
if err := viper.ReadInConfig(); err != nil {
t.Fatalf("read config: %v", err)
}

out := captureWarn(t, warnUnknownConfigKeys)
if !strings.Contains(out, "max_import_connections") {
t.Errorf("warning did not name the retired key; got: %s", out)
}
}

func TestWarnUnknownConfigKeys_SilentOnValidConfig(t *testing.T) {
path := writeConfig(t, `
import:
max_processor_workers: 4
segment_sample_percentage: 25
`)
viper.Reset()
viper.SetConfigFile(path)
if err := viper.ReadInConfig(); err != nil {
t.Fatalf("read config: %v", err)
}

if out := captureWarn(t, warnUnknownConfigKeys); out != "" {
t.Errorf("expected no warning for a valid config, got: %s", out)
}
}
Loading