mirror of
https://github.com/netbirdio/netbird.git
synced 2026-10-02 19:49:07 +02:00
[management,signal,proxy,relay,misc] Unify service configuration loading
Service entry points currently resolve defaults, files, environment variables, and flags differently, which makes precedence inconsistent and prevents some services from using config files. Introduce one Viper-backed loader and migrate Combined, Management, Relay, Signal, and Proxy while preserving compatibility aliases and Management template expansion.
This commit is contained in:
@@ -0,0 +1,238 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/netip"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/spf13/pflag"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
type testConfig struct {
|
||||
Server testServerConfig `yaml:"server"`
|
||||
Internal string `yaml:"-"`
|
||||
}
|
||||
|
||||
type testServerConfig struct {
|
||||
Address string `yaml:"address" env:"APP_SERVER_ADDRESS" flag:"address,legacy-address"`
|
||||
BindAddress netip.Addr `yaml:"bindAddress"`
|
||||
AdvertisedAddress netip.Addr `yaml:"advertisedAddress"`
|
||||
Enabled bool `yaml:"enabled"`
|
||||
LogLevel string `yaml:"logLevel"`
|
||||
Timeout time.Duration `yaml:"timeout"`
|
||||
Ports []int `yaml:"ports"`
|
||||
Owner *testOwnerConfig `yaml:"owner,omitempty"`
|
||||
TLS testTLSConfig `yaml:"tls"`
|
||||
JSONValue testJSONValue `yaml:"jsonValue"`
|
||||
}
|
||||
|
||||
type testOwnerConfig struct {
|
||||
Email string `yaml:"email"`
|
||||
}
|
||||
|
||||
type testTLSConfig struct {
|
||||
Enabled bool `yaml:"enabled"`
|
||||
}
|
||||
|
||||
type testJSONValue struct {
|
||||
Value string
|
||||
}
|
||||
|
||||
func (v *testJSONValue) UnmarshalJSON(data []byte) error {
|
||||
return json.Unmarshal(data, &v.Value)
|
||||
}
|
||||
|
||||
type recursiveConfig struct {
|
||||
Value string `yaml:"value"`
|
||||
Next *recursiveConfig `yaml:"next,omitempty"`
|
||||
}
|
||||
|
||||
func defaultTestConfig() *testConfig {
|
||||
return &testConfig{
|
||||
Server: testServerConfig{
|
||||
Address: ":443",
|
||||
LogLevel: "info",
|
||||
Timeout: 30 * time.Second,
|
||||
Ports: []int{443},
|
||||
},
|
||||
Internal: "default-internal",
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadAppliesFileEnvironmentAndDefaults(t *testing.T) {
|
||||
configPath := writeConfigFile(t, "config.conf", `
|
||||
server:
|
||||
bindAddress: 192.0.2.1
|
||||
enabled: yes
|
||||
logLevel: warn
|
||||
timeout: 5s
|
||||
ports: [80, 443]
|
||||
jsonValue: decoded
|
||||
internal: ignored
|
||||
`)
|
||||
t.Setenv("NB_SERVER_LOGLEVEL", "debug")
|
||||
t.Setenv("NB_SERVER_OWNER_EMAIL", "owner@example.com")
|
||||
t.Setenv("NB_SERVER_ADVERTISEDADDRESS", "198.51.100.1")
|
||||
|
||||
cfg, err := Load(configPath, defaultTestConfig(), Options{
|
||||
TagName: "yaml",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, ":443", cfg.Server.Address, "Defaults should survive decoding")
|
||||
assert.Equal(t, netip.MustParseAddr("192.0.2.1"), cfg.Server.BindAddress, "Text values from files should be decoded")
|
||||
assert.Equal(t, netip.MustParseAddr("198.51.100.1"), cfg.Server.AdvertisedAddress, "Text values from the environment should be decoded")
|
||||
assert.True(t, cfg.Server.Enabled, "Legacy YAML booleans should be decoded")
|
||||
assert.Equal(t, "debug", cfg.Server.LogLevel, "Environment should override the file")
|
||||
assert.Equal(t, 5*time.Second, cfg.Server.Timeout, "Durations should be decoded")
|
||||
assert.Equal(t, []int{80, 443}, cfg.Server.Ports, "Slices should be decoded")
|
||||
assert.Equal(t, "decoded", cfg.Server.JSONValue.Value, "JSON unmarshalers should be decoded")
|
||||
require.NotNil(t, cfg.Server.Owner, "Environment should create optional nested configuration")
|
||||
assert.Equal(t, "owner@example.com", cfg.Server.Owner.Email, "Nested environment values should be decoded")
|
||||
assert.Equal(t, "default-internal", cfg.Internal, "Ignored fields should retain their defaults")
|
||||
}
|
||||
|
||||
func TestLoadFlagPrecedence(t *testing.T) {
|
||||
configPath := writeConfigFile(t, "config.yaml", `
|
||||
server:
|
||||
address: ":8443"
|
||||
`)
|
||||
t.Setenv("APP_SERVER_ADDRESS", ":9443")
|
||||
flags := pflag.NewFlagSet("test", pflag.ContinueOnError)
|
||||
flags.String("address", ":443", "")
|
||||
flags.String("legacy-address", ":443", "")
|
||||
require.NoError(t, flags.Set("legacy-address", ":7443"))
|
||||
|
||||
cfg, err := Load(configPath, defaultTestConfig(), Options{
|
||||
TagName: "yaml",
|
||||
FlagSet: flags,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, ":7443", cfg.Server.Address, "Flags should override environment and file values")
|
||||
}
|
||||
|
||||
func TestLoadAllowsEmptyEnvironmentOverrides(t *testing.T) {
|
||||
configPath := writeConfigFile(t, "config.yaml", `
|
||||
server:
|
||||
address: ":8443"
|
||||
`)
|
||||
t.Setenv("APP_SERVER_ADDRESS", "")
|
||||
|
||||
cfg, err := Load(configPath, defaultTestConfig(), Options{
|
||||
TagName: "yaml",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
assert.Empty(t, cfg.Server.Address, "An explicitly empty environment value should clear the file value")
|
||||
}
|
||||
|
||||
func TestLoadTransformsConfig(t *testing.T) {
|
||||
t.Setenv("CONFIG_ADDRESS", ":8443")
|
||||
configPath := writeConfigFile(t, "config.yaml", `
|
||||
server:
|
||||
address: "{{ .CONFIG_ADDRESS }}"
|
||||
`)
|
||||
|
||||
cfg, err := Load(configPath, defaultTestConfig(), Options{
|
||||
TagName: "yaml",
|
||||
Transform: ExpandEnvTemplate,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, ":8443", cfg.Server.Address, "The transform should run before decoding")
|
||||
}
|
||||
|
||||
func TestLoadUsesRecognizedFileType(t *testing.T) {
|
||||
configPath := writeConfigFile(t, "config.toml", `
|
||||
[server]
|
||||
address = ":8443"
|
||||
enabled = true
|
||||
`)
|
||||
|
||||
cfg, err := Load(configPath, defaultTestConfig(), Options{TagName: "yaml"})
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, ":8443", cfg.Server.Address, "The file extension should select the decoder")
|
||||
assert.True(t, cfg.Server.Enabled, "TOML booleans should be decoded")
|
||||
}
|
||||
|
||||
func TestLoadAllowsMissingFile(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
path string
|
||||
}{
|
||||
{name: "empty path"},
|
||||
{name: "unknown extension", path: filepath.Join(t.TempDir(), "missing.conf")},
|
||||
}
|
||||
for _, testCase := range testCases {
|
||||
t.Run(testCase.name, func(t *testing.T) {
|
||||
t.Setenv("APP_SERVER_ADDRESS", ":9443")
|
||||
|
||||
cfg, err := Load(testCase.path, defaultTestConfig(), Options{
|
||||
TagName: "yaml",
|
||||
AllowMissing: true,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, ":9443", cfg.Server.Address, "Environment should override defaults without a file")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadSupportsRecursiveConfigTypes(t *testing.T) {
|
||||
configPath := writeConfigFile(t, "config.yaml", `
|
||||
value: first
|
||||
next:
|
||||
value: second
|
||||
`)
|
||||
|
||||
cfg, err := Load(configPath, &recursiveConfig{}, Options{
|
||||
TagName: "yaml",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "first", cfg.Value, "Root values should be decoded")
|
||||
require.NotNil(t, cfg.Next, "Recursive configuration should be decoded from the file")
|
||||
assert.Equal(t, "second", cfg.Next.Value, "Nested recursive values should be decoded")
|
||||
}
|
||||
|
||||
func TestLoadRejectsMissingFileByDefault(t *testing.T) {
|
||||
_, err := Load(filepath.Join(t.TempDir(), "missing.yaml"), defaultTestConfig(), Options{
|
||||
TagName: "yaml",
|
||||
})
|
||||
require.Error(t, err)
|
||||
}
|
||||
|
||||
func TestLoadStrictRejectsUnknownKeys(t *testing.T) {
|
||||
configPath := writeConfigFile(t, "config.yaml", "unknown: true\n")
|
||||
|
||||
_, err := Load(configPath, defaultTestConfig(), Options{
|
||||
TagName: "yaml",
|
||||
Strict: true,
|
||||
})
|
||||
require.Error(t, err)
|
||||
assert.ErrorContains(t, err, "invalid keys")
|
||||
}
|
||||
|
||||
func TestLoadUsesTagAsDefaultFileType(t *testing.T) {
|
||||
configPath := writeConfigFile(t, "config.conf", "server:\n address: :8443\n")
|
||||
|
||||
cfg, err := Load(configPath, defaultTestConfig(), Options{TagName: "yaml"})
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, ":8443", cfg.Server.Address, "The struct tag should select the fallback decoder")
|
||||
}
|
||||
|
||||
func TestLoadRejectsNilDefault(t *testing.T) {
|
||||
configPath := writeConfigFile(t, "config.yaml", "server: {}\n")
|
||||
|
||||
_, err := Load(configPath, (*testConfig)(nil), Options{TagName: "yaml"})
|
||||
require.Error(t, err)
|
||||
assert.ErrorContains(t, err, "default config is nil")
|
||||
}
|
||||
|
||||
func writeConfigFile(t *testing.T, name, contents string) string {
|
||||
t.Helper()
|
||||
|
||||
configPath := filepath.Join(t.TempDir(), name)
|
||||
require.NoError(t, os.WriteFile(configPath, []byte(contents), 0o600))
|
||||
return configPath
|
||||
}
|
||||
Reference in New Issue
Block a user