[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:
jnfrati
2026-08-24 18:18:59 +02:00
parent f03853867b
commit c8cd6b4dca
27 changed files with 1296 additions and 339 deletions
+45
View File
@@ -0,0 +1,45 @@
package cmd
import (
"os"
"runtime"
"github.com/spf13/cobra"
configloader "github.com/netbirdio/netbird/util/config"
)
// Config contains Signal service startup configuration.
type Config struct {
Port int `yaml:"port" env:"NB_PORT" flag:"port"`
MetricsPort int `yaml:"metricsPort" env:"NB_METRICS_PORT" flag:"metrics-port"`
LetsencryptDomain string `yaml:"letsencryptDomain" env:"NB_LETSENCRYPT_DOMAIN" flag:"letsencrypt-domain"`
LetsencryptEmail string `yaml:"letsencryptEmail" env:"NB_LETSENCRYPT_EMAIL" flag:"letsencrypt-email"`
LetsencryptDataDir string `yaml:"letsencryptDataDir" env:"NB_LETSENCRYPT_DATA_DIR,NB_SSL_DIR" flag:"letsencrypt-data-dir,ssl-dir"`
CertFile string `yaml:"certFile" env:"NB_CERT_FILE" flag:"cert-file"`
CertKey string `yaml:"certKey" env:"NB_CERT_KEY" flag:"cert-key"`
LogLevel string `yaml:"logLevel" env:"NB_LOG_LEVEL" flag:"log-level"`
LogFile string `yaml:"logFile" env:"NB_LOG_FILE" flag:"log-file"`
PprofAddress string `yaml:"pprofAddress" env:"NB_PPROF_ADDR"`
}
func defaultConfig() *Config {
logFile := "/var/log/netbird/signal.log"
if runtime.GOOS == "windows" {
logFile = os.Getenv("PROGRAMDATA") + "\\Netbird\\signal.log"
}
return &Config{
MetricsPort: 9090,
LogLevel: "info",
LogFile: logFile,
}
}
func loadConfig(cmd *cobra.Command, configPath string) (*Config, error) {
return configloader.Load(configPath, defaultConfig(), configloader.Options{
TagName: "yaml",
AllowMissing: configPath == "",
FlagSet: cmd.Flags(),
Strict: true,
})
}
+41
View File
@@ -0,0 +1,41 @@
package cmd
import (
"os"
"path/filepath"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestLoadConfigPrecedence(t *testing.T) {
configPath := filepath.Join(t.TempDir(), "signal.yaml")
require.NoError(t, os.WriteFile(configPath, []byte(`
port: 10001
metricsPort: 9091
logLevel: warn
pprofAddress: localhost:6060
`), 0o600))
t.Setenv("NB_METRICS_PORT", "9191")
t.Setenv("NB_SSL_DIR", "/legacy-certs")
require.NoError(t, runCmd.ParseFlags(nil))
portFlag := runCmd.Flags().Lookup("port")
oldPort := portFlag.Value.String()
oldChanged := portFlag.Changed
t.Cleanup(func() {
require.NoError(t, portFlag.Value.Set(oldPort))
portFlag.Changed = oldChanged
})
require.NoError(t, portFlag.Value.Set("10002"))
portFlag.Changed = true
cfg, err := loadConfig(runCmd, configPath)
require.NoError(t, err)
assert.Equal(t, 10002, cfg.Port, "Flags should override the configuration file")
assert.Equal(t, 9191, cfg.MetricsPort, "Environment should override the configuration file")
assert.Equal(t, "warn", cfg.LogLevel, "File values should override defaults")
assert.Equal(t, "/legacy-certs", cfg.LetsencryptDataDir, "Legacy environment aliases should remain supported")
assert.Equal(t, "localhost:6060", cfg.PprofAddress, "Environment-only settings should load from the file")
}
-35
View File
@@ -1,35 +0,0 @@
package cmd
import (
"os"
"strings"
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
)
// setFlagsFromEnvVars reads and updates flag values from environment variables with prefix NB_
func setFlagsFromEnvVars(cmd *cobra.Command) {
flags := cmd.PersistentFlags()
flags.VisitAll(func(f *pflag.Flag) {
newEnvVar := flagNameToEnvVar(f.Name, "NB_")
value, present := os.LookupEnv(newEnvVar)
if !present {
return
}
err := flags.Set(f.Name, value)
if err != nil {
log.Infof("unable to configure flag %s using variable %s, err: %v", f.Name, newEnvVar, err)
}
})
}
// flagNameToEnvVar converts flag name to environment var name adding a prefix,
// replacing dashes and making all uppercase (e.g. setup-keys is converted to NB_SETUP_KEYS according to the input prefix)
func flagNameToEnvVar(cmdFlag string, prefix string) string {
parsed := strings.ReplaceAll(cmdFlag, "-", "_")
upper := strings.ToUpper(parsed)
return prefix + upper
}
+5 -12
View File
@@ -4,7 +4,6 @@ import (
"fmt"
"os"
"os/signal"
"runtime"
"github.com/spf13/cobra"
@@ -17,9 +16,8 @@ const (
)
var (
logLevel string
defaultLogFile string
logFile string
logLevel string
logFile string
rootCmd = &cobra.Command{
Use: "netbird-signal",
@@ -39,14 +37,9 @@ func Execute() error {
func init() {
stopCh = make(chan int)
defaultLogFile = "/var/log/netbird/signal.log"
if runtime.GOOS == "windows" {
defaultLogFile = os.Getenv("PROGRAMDATA") + "\\Netbird\\" + "signal.log"
}
rootCmd.PersistentFlags().StringVar(&logLevel, "log-level", "info", "")
rootCmd.PersistentFlags().StringVar(&logFile, "log-file", defaultLogFile, "sets Netbird log path. If console is specified the log will be output to stdout")
defaults := defaultConfig()
rootCmd.PersistentFlags().StringVar(&logLevel, "log-level", defaults.LogLevel, "")
rootCmd.PersistentFlags().StringVar(&logFile, "log-file", defaults.LogFile, "sets Netbird log path. If console is specified the log will be output to stdout")
rootCmd.AddCommand(runCmd)
}
+32 -22
View File
@@ -10,7 +10,6 @@ import (
"net/http"
// nolint:gosec
_ "net/http/pprof"
"os"
"time"
"go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc"
@@ -46,6 +45,8 @@ var (
signalLetsencryptDataDir string
signalCertFile string
signalCertKey string
signalConfigPath string
signalPprofAddress string
signalKaep = grpc.KeepaliveEnforcementPolicy(keepalive.EnforcementPolicy{
MinTime: 5 * time.Second,
@@ -64,30 +65,25 @@ var (
Short: "start NetBird Signal Server daemon",
SilenceUsage: true,
PreRunE: func(cmd *cobra.Command, args []string) error {
err := util.InitLog(logLevel, logFile)
cfg, err := loadConfig(cmd, signalConfigPath)
if err != nil {
return fmt.Errorf("failed initializing log: %w", err)
return fmt.Errorf("load config: %w", err)
}
applyConfig(cfg)
flag.Parse()
// detect whether user specified a port
userPort := cmd.Flag("port").Changed
var tlsEnabled bool
if signalLetsencryptDomain != "" || (signalCertFile != "" && signalCertKey != "") {
tlsEnabled = true
}
if !userPort {
// different defaults for signalPort
if tlsEnabled {
if signalPort == 0 {
if signalLetsencryptDomain != "" || (signalCertFile != "" && signalCertKey != "") {
signalPort = 443
} else {
signalPort = 80
}
}
if err := util.InitLog(logLevel, logFile); err != nil {
return fmt.Errorf("initialize log: %w", err)
}
flag.Parse()
return nil
},
RunE: func(cmd *cobra.Command, args []string) error {
@@ -196,10 +192,10 @@ var (
)
func startPprof() {
if pprofAddr := os.Getenv("NB_PPROF_ADDR"); pprofAddr != "" {
log.Infof("pprof enabled, listening on: %s", pprofAddr)
if signalPprofAddress != "" {
log.Infof("pprof enabled, listening on: %s", signalPprofAddress)
go func() {
if err := http.ListenAndServe(pprofAddr, nil); err != nil {
if err := http.ListenAndServe(signalPprofAddress, nil); err != nil {
log.Fatalf("pprof server failed: %v", err)
}
}()
@@ -328,13 +324,27 @@ func loadTLSConfig(certFile string, certKey string) (*tls.Config, error) {
}
func init() {
runCmd.PersistentFlags().IntVar(&signalPort, "port", 80, "Server port to listen on (defaults to 443 if TLS is enabled, 80 otherwise")
runCmd.Flags().IntVar(&metricsPort, "metrics-port", 9090, "metrics endpoint http port. Metrics are accessible under host:metrics-port/metrics")
defaults := defaultConfig()
runCmd.PersistentFlags().StringVar(&signalConfigPath, "config", "", "path to configuration file")
runCmd.PersistentFlags().IntVar(&signalPort, "port", defaults.Port, "Server port to listen on (defaults to 443 if TLS is enabled, 80 otherwise)")
runCmd.Flags().IntVar(&metricsPort, "metrics-port", defaults.MetricsPort, "metrics endpoint http port. Metrics are accessible under host:metrics-port/metrics")
runCmd.PersistentFlags().StringVar(&signalLetsencryptDataDir, "letsencrypt-data-dir", "", "a directory to store Let's Encrypt data. Required if Let's Encrypt is enabled.")
runCmd.PersistentFlags().StringVar(&signalLetsencryptDataDir, "ssl-dir", "", "server ssl directory location. *Required only for Let's Encrypt certificates. Deprecated: use --letsencrypt-data-dir")
runCmd.PersistentFlags().StringVar(&signalLetsencryptDomain, "letsencrypt-domain", "", "a domain to issue Let's Encrypt certificate for. Enables TLS using Let's Encrypt. Will fetch and renew certificate, and run the server with TLS")
runCmd.PersistentFlags().StringVar(&signalLetsencryptEmail, "letsencrypt-email", "", "email address to use for Let's Encrypt certificate registration")
runCmd.PersistentFlags().StringVar(&signalCertFile, "cert-file", "", "Location of your SSL certificate. Can be used when you have an existing certificate and don't want a new certificate be generated automatically. If letsencrypt-domain is specified this property has no effect")
runCmd.PersistentFlags().StringVar(&signalCertKey, "cert-key", "", "Location of your SSL certificate private key. Can be used when you have an existing certificate and don't want a new certificate be generated automatically. If letsencrypt-domain is specified this property has no effect")
setFlagsFromEnvVars(runCmd)
}
func applyConfig(cfg *Config) {
signalPort = cfg.Port
metricsPort = cfg.MetricsPort
signalLetsencryptDomain = cfg.LetsencryptDomain
signalLetsencryptEmail = cfg.LetsencryptEmail
signalLetsencryptDataDir = cfg.LetsencryptDataDir
signalCertFile = cfg.CertFile
signalCertKey = cfg.CertKey
logLevel = cfg.LogLevel
logFile = cfg.LogFile
signalPprofAddress = cfg.PprofAddress
}