mirror of
https://github.com/netbirdio/netbird.git
synced 2026-10-09 23:19:11 +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:
+8
-19
@@ -5,20 +5,18 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
"os"
|
|
||||||
filePath "path/filepath"
|
filePath "path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
"gopkg.in/yaml.v3"
|
|
||||||
|
|
||||||
|
nbconfig "github.com/netbirdio/netbird/management/internals/server/config"
|
||||||
"github.com/netbirdio/netbird/management/server/idp"
|
"github.com/netbirdio/netbird/management/server/idp"
|
||||||
"github.com/netbirdio/netbird/management/server/types"
|
"github.com/netbirdio/netbird/management/server/types"
|
||||||
"github.com/netbirdio/netbird/util"
|
"github.com/netbirdio/netbird/util"
|
||||||
|
configloader "github.com/netbirdio/netbird/util/config"
|
||||||
"github.com/netbirdio/netbird/util/crypt"
|
"github.com/netbirdio/netbird/util/crypt"
|
||||||
|
|
||||||
nbconfig "github.com/netbirdio/netbird/management/internals/server/config"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// CombinedConfig is the root configuration for the combined server.
|
// CombinedConfig is the root configuration for the combined server.
|
||||||
@@ -440,26 +438,17 @@ func (c *CombinedConfig) autoConfigureClientSettings(exposedProto, exposedHost,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// LoadConfig loads configuration from a YAML file
|
// LoadConfig loads the combined server configuration.
|
||||||
func LoadConfig(configPath string) (*CombinedConfig, error) {
|
func LoadConfig(configPath string) (*CombinedConfig, error) {
|
||||||
cfg := DefaultConfig()
|
cfg, err := configloader.Load(configPath, DefaultConfig(), configloader.Options{
|
||||||
|
TagName: "yaml",
|
||||||
if configPath == "" {
|
AllowMissing: configPath == "",
|
||||||
return cfg, nil
|
})
|
||||||
}
|
|
||||||
|
|
||||||
data, err := os.ReadFile(configPath)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to read config file: %w", err)
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := yaml.Unmarshal(data, cfg); err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to parse config file: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Populate internal configs from server settings
|
|
||||||
cfg.ApplySimplifiedDefaults()
|
cfg.ApplySimplifiedDefaults()
|
||||||
|
|
||||||
return cfg, nil
|
return cfg, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,113 @@
|
|||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestLoadConfigEnvironmentWithoutFile(t *testing.T) {
|
||||||
|
t.Setenv("NB_SERVER_LOGLEVEL", "debug")
|
||||||
|
|
||||||
|
cfg, err := LoadConfig("")
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, "debug", cfg.Server.LogLevel, "Environment should override defaults without a file")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLoadConfigEnvironmentOverridesFileAndDefaults(t *testing.T) {
|
||||||
|
configPath := writeCombinedConfig(t, "config.yaml", `
|
||||||
|
server:
|
||||||
|
exposedAddress: "https://netbird.example.com"
|
||||||
|
authSecret: "file-secret"
|
||||||
|
logLevel: "info"
|
||||||
|
relay:
|
||||||
|
logLevel: "ignored-relay-level"
|
||||||
|
signal:
|
||||||
|
logLevel: "ignored-signal-level"
|
||||||
|
management:
|
||||||
|
logLevel: "ignored-management-level"
|
||||||
|
`)
|
||||||
|
t.Setenv("NB_SERVER_LOGLEVEL", "debug")
|
||||||
|
t.Setenv("NB_SERVER_METRICSPORT", "9191")
|
||||||
|
|
||||||
|
cfg, err := LoadConfig(configPath)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, "debug", cfg.Server.LogLevel, "Environment should override the config file")
|
||||||
|
assert.Equal(t, 9191, cfg.Server.MetricsPort, "Environment should override a default absent from the file")
|
||||||
|
assert.Equal(t, ":443", cfg.Server.ListenAddress, "Unchanged defaults should be preserved")
|
||||||
|
assert.Equal(t, "debug", cfg.Relay.LogLevel, "Internal relay configuration should not be decoded")
|
||||||
|
assert.Equal(t, "debug", cfg.Signal.LogLevel, "Internal signal configuration should not be decoded")
|
||||||
|
assert.Equal(t, "debug", cfg.Management.LogLevel, "Internal management configuration should not be decoded")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLoadConfigEnvironmentCreatesOptionalConfig(t *testing.T) {
|
||||||
|
configPath := writeCombinedConfig(t, "config.yaml", `
|
||||||
|
server:
|
||||||
|
exposedAddress: "https://netbird.example.com"
|
||||||
|
authSecret: "file-secret"
|
||||||
|
`)
|
||||||
|
t.Setenv("NB_SERVER_AUTH_OWNER_EMAIL", "owner@example.com")
|
||||||
|
t.Setenv("NB_SERVER_AUTH_OWNER_PASSWORD", "password-hash")
|
||||||
|
|
||||||
|
cfg, err := LoadConfig(configPath)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotNil(t, cfg.Server.Auth.Owner, "Environment should create the optional owner configuration")
|
||||||
|
assert.Equal(t, "owner@example.com", cfg.Server.Auth.Owner.Email, "Owner email should come from the environment")
|
||||||
|
assert.Equal(t, "password-hash", cfg.Server.Auth.Owner.Password, "Owner password should come from the environment")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLoadConfigSupportsYAMLWithoutKnownExtension(t *testing.T) {
|
||||||
|
for _, name := range []string{"config", "config.conf"} {
|
||||||
|
t.Run(name, func(t *testing.T) {
|
||||||
|
configPath := writeCombinedConfig(t, name, `
|
||||||
|
server:
|
||||||
|
exposedAddress: "https://netbird.example.com"
|
||||||
|
authSecret: "yaml-secret"
|
||||||
|
`)
|
||||||
|
|
||||||
|
cfg, err := LoadConfig(configPath)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, "yaml-secret", cfg.Server.AuthSecret, "Unknown extensions should remain YAML-compatible")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLoadConfigSupportsLegacyYAMLBooleans(t *testing.T) {
|
||||||
|
configPath := writeCombinedConfig(t, "config.yaml", `
|
||||||
|
server:
|
||||||
|
exposedAddress: "https://netbird.example.com"
|
||||||
|
authSecret: "file-secret"
|
||||||
|
disableAnonymousMetrics: yes
|
||||||
|
`)
|
||||||
|
|
||||||
|
cfg, err := LoadConfig(configPath)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.True(t, cfg.Server.DisableAnonymousMetrics, "Legacy YAML boolean values should remain supported")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLoadConfigSupportsTOML(t *testing.T) {
|
||||||
|
configPath := writeCombinedConfig(t, "config.toml", `
|
||||||
|
[server]
|
||||||
|
exposedAddress = "https://netbird.example.com"
|
||||||
|
authSecret = "toml-secret"
|
||||||
|
logLevel = "warn"
|
||||||
|
`)
|
||||||
|
|
||||||
|
cfg, err := LoadConfig(configPath)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, "https://netbird.example.com", cfg.Server.ExposedAddress, "Exposed address should be decoded from TOML")
|
||||||
|
assert.Equal(t, "toml-secret", cfg.Server.AuthSecret, "Auth secret should be decoded from TOML")
|
||||||
|
assert.Equal(t, "warn", cfg.Server.LogLevel, "Log level should be decoded from TOML")
|
||||||
|
assert.Equal(t, ":443", cfg.Server.ListenAddress, "Defaults should survive decoding")
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeCombinedConfig(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
|
||||||
|
}
|
||||||
@@ -56,7 +56,8 @@ var (
|
|||||||
All services (Management, Signal, Relay) are multiplexed on a single port.
|
All services (Management, Signal, Relay) are multiplexed on a single port.
|
||||||
Optional STUN server runs on separate UDP ports.
|
Optional STUN server runs on separate UDP ports.
|
||||||
|
|
||||||
Configuration is loaded from a YAML file specified with --config.`,
|
Configuration is loaded from a file specified with --config. Values can be
|
||||||
|
overridden with NB_-prefixed environment variables, such as NB_SERVER_LOGLEVEL.`,
|
||||||
SilenceUsage: true,
|
SilenceUsage: true,
|
||||||
SilenceErrors: true,
|
SilenceErrors: true,
|
||||||
RunE: execute,
|
RunE: execute,
|
||||||
@@ -64,7 +65,7 @@ Configuration is loaded from a YAML file specified with --config.`,
|
|||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
rootCmd.PersistentFlags().StringVarP(&configPath, "config", "c", "", "path to YAML configuration file (required)")
|
rootCmd.PersistentFlags().StringVarP(&configPath, "config", "c", "", "path to configuration file (required)")
|
||||||
_ = rootCmd.MarkPersistentFlagRequired("config")
|
_ = rootCmd.MarkPersistentFlagRequired("config")
|
||||||
|
|
||||||
rootCmd.AddCommand(newAdminCommands())
|
rootCmd.AddCommand(newAdminCommands())
|
||||||
|
|||||||
@@ -58,6 +58,7 @@ require (
|
|||||||
github.com/gliderlabs/ssh v0.3.8
|
github.com/gliderlabs/ssh v0.3.8
|
||||||
github.com/go-jose/go-jose/v4 v4.1.4
|
github.com/go-jose/go-jose/v4 v4.1.4
|
||||||
github.com/go-ole/go-ole v1.3.0
|
github.com/go-ole/go-ole v1.3.0
|
||||||
|
github.com/go-viper/mapstructure/v2 v2.5.0
|
||||||
github.com/gobwas/ws v1.4.0
|
github.com/gobwas/ws v1.4.0
|
||||||
github.com/goccy/go-yaml v1.18.0
|
github.com/goccy/go-yaml v1.18.0
|
||||||
github.com/godbus/dbus/v5 v5.2.2
|
github.com/godbus/dbus/v5 v5.2.2
|
||||||
@@ -105,6 +106,7 @@ require (
|
|||||||
github.com/shirou/gopsutil/v4 v4.25.8
|
github.com/shirou/gopsutil/v4 v4.25.8
|
||||||
github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966
|
github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966
|
||||||
github.com/songgao/water v0.0.0-20200317203138-2b4b6d7c09d8
|
github.com/songgao/water v0.0.0-20200317203138-2b4b6d7c09d8
|
||||||
|
github.com/spf13/viper v1.21.0
|
||||||
github.com/stretchr/testify v1.11.1
|
github.com/stretchr/testify v1.11.1
|
||||||
github.com/testcontainers/testcontainers-go v0.37.0
|
github.com/testcontainers/testcontainers-go v0.37.0
|
||||||
github.com/testcontainers/testcontainers-go/modules/mysql v0.37.0
|
github.com/testcontainers/testcontainers-go/modules/mysql v0.37.0
|
||||||
@@ -209,7 +211,6 @@ require (
|
|||||||
github.com/go-openapi/swag v0.23.1 // indirect
|
github.com/go-openapi/swag v0.23.1 // indirect
|
||||||
github.com/go-openapi/validate v0.24.0 // indirect
|
github.com/go-openapi/validate v0.24.0 // indirect
|
||||||
github.com/go-sql-driver/mysql v1.9.3 // indirect
|
github.com/go-sql-driver/mysql v1.9.3 // indirect
|
||||||
github.com/go-viper/mapstructure/v2 v2.5.0 // indirect
|
|
||||||
github.com/go-webauthn/webauthn v0.16.4 // indirect
|
github.com/go-webauthn/webauthn v0.16.4 // indirect
|
||||||
github.com/go-webauthn/x v0.2.3 // indirect
|
github.com/go-webauthn/x v0.2.3 // indirect
|
||||||
github.com/gobwas/httphead v0.1.0 // indirect
|
github.com/gobwas/httphead v0.1.0 // indirect
|
||||||
@@ -279,6 +280,7 @@ require (
|
|||||||
github.com/openbao/openbao/api/v2 v2.5.1 // indirect
|
github.com/openbao/openbao/api/v2 v2.5.1 // indirect
|
||||||
github.com/opencontainers/go-digest v1.0.0 // indirect
|
github.com/opencontainers/go-digest v1.0.0 // indirect
|
||||||
github.com/opencontainers/image-spec v1.1.1 // indirect
|
github.com/opencontainers/image-spec v1.1.1 // indirect
|
||||||
|
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
||||||
github.com/philhofer/fwd v1.2.0 // indirect
|
github.com/philhofer/fwd v1.2.0 // indirect
|
||||||
github.com/pion/dtls/v2 v2.2.10 // indirect
|
github.com/pion/dtls/v2 v2.2.10 // indirect
|
||||||
github.com/pion/dtls/v3 v3.0.9 // indirect
|
github.com/pion/dtls/v3 v3.0.9 // indirect
|
||||||
@@ -295,9 +297,13 @@ require (
|
|||||||
github.com/prometheus/procfs v0.19.2 // indirect
|
github.com/prometheus/procfs v0.19.2 // indirect
|
||||||
github.com/russellhaering/goxmldsig v1.6.0 // indirect
|
github.com/russellhaering/goxmldsig v1.6.0 // indirect
|
||||||
github.com/ryanuber/go-glob v1.0.0 // indirect
|
github.com/ryanuber/go-glob v1.0.0 // indirect
|
||||||
|
github.com/sagikazarmark/locafero v0.11.0 // indirect
|
||||||
github.com/shopspring/decimal v1.4.0 // indirect
|
github.com/shopspring/decimal v1.4.0 // indirect
|
||||||
|
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect
|
||||||
|
github.com/spf13/afero v1.15.0 // indirect
|
||||||
github.com/spf13/cast v1.10.0 // indirect
|
github.com/spf13/cast v1.10.0 // indirect
|
||||||
github.com/stretchr/objx v0.5.2 // indirect
|
github.com/stretchr/objx v0.5.2 // indirect
|
||||||
|
github.com/subosito/gotenv v1.6.0 // indirect
|
||||||
github.com/tinylib/msgp v1.6.3 // indirect
|
github.com/tinylib/msgp v1.6.3 // indirect
|
||||||
github.com/tklauser/go-sysconf v0.3.15 // indirect
|
github.com/tklauser/go-sysconf v0.3.15 // indirect
|
||||||
github.com/tklauser/numcpus v0.10.0 // indirect
|
github.com/tklauser/numcpus v0.10.0 // indirect
|
||||||
@@ -313,6 +319,7 @@ require (
|
|||||||
go.opentelemetry.io/otel/trace v1.43.0 // indirect
|
go.opentelemetry.io/otel/trace v1.43.0 // indirect
|
||||||
go.uber.org/multierr v1.11.0 // indirect
|
go.uber.org/multierr v1.11.0 // indirect
|
||||||
go.yaml.in/yaml/v2 v2.4.3 // indirect
|
go.yaml.in/yaml/v2 v2.4.3 // indirect
|
||||||
|
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
||||||
golang.org/x/text v0.41.0 // indirect
|
golang.org/x/text v0.41.0 // indirect
|
||||||
golang.org/x/tools v0.49.0 // indirect
|
golang.org/x/tools v0.49.0 // indirect
|
||||||
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect
|
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect
|
||||||
|
|||||||
@@ -595,6 +595,8 @@ github.com/russellhaering/goxmldsig v1.6.0/go.mod h1:TrnaquDcYxWXfJrOjeMBTX4mLBe
|
|||||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||||
github.com/ryanuber/go-glob v1.0.0 h1:iQh3xXAumdQ+4Ufa5b25cRpC5TYKlno6hsv6Cb3pkBk=
|
github.com/ryanuber/go-glob v1.0.0 h1:iQh3xXAumdQ+4Ufa5b25cRpC5TYKlno6hsv6Cb3pkBk=
|
||||||
github.com/ryanuber/go-glob v1.0.0/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc=
|
github.com/ryanuber/go-glob v1.0.0/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc=
|
||||||
|
github.com/sagikazarmark/locafero v0.11.0 h1:1iurJgmM9G3PA/I+wWYIOw/5SyBtxapeHDcg+AAIFXc=
|
||||||
|
github.com/sagikazarmark/locafero v0.11.0/go.mod h1:nVIGvgyzw595SUSUE6tvCp3YYTeHs15MvlmU87WwIik=
|
||||||
github.com/shirou/gopsutil/v4 v4.25.8 h1:NnAsw9lN7587WHxjJA9ryDnqhJpFH6A+wagYWTOH970=
|
github.com/shirou/gopsutil/v4 v4.25.8 h1:NnAsw9lN7587WHxjJA9ryDnqhJpFH6A+wagYWTOH970=
|
||||||
github.com/shirou/gopsutil/v4 v4.25.8/go.mod h1:q9QdMmfAOVIw7a+eF86P7ISEU6ka+NLgkUxlopV4RwI=
|
github.com/shirou/gopsutil/v4 v4.25.8/go.mod h1:q9QdMmfAOVIw7a+eF86P7ISEU6ka+NLgkUxlopV4RwI=
|
||||||
github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k=
|
github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k=
|
||||||
@@ -605,6 +607,10 @@ github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966 h1:JIAuq3EE
|
|||||||
github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966/go.mod h1:sUM3LWHvSMaG192sy56D9F7CNvL7jUJVXoqM1QKLnog=
|
github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966/go.mod h1:sUM3LWHvSMaG192sy56D9F7CNvL7jUJVXoqM1QKLnog=
|
||||||
github.com/songgao/water v0.0.0-20200317203138-2b4b6d7c09d8 h1:TG/diQgUe0pntT/2D9tmUCz4VNwm9MfrtPr0SU2qSX8=
|
github.com/songgao/water v0.0.0-20200317203138-2b4b6d7c09d8 h1:TG/diQgUe0pntT/2D9tmUCz4VNwm9MfrtPr0SU2qSX8=
|
||||||
github.com/songgao/water v0.0.0-20200317203138-2b4b6d7c09d8/go.mod h1:P5HUIBuIWKbyjl083/loAegFkfbFNx5i2qEP4CNbm7E=
|
github.com/songgao/water v0.0.0-20200317203138-2b4b6d7c09d8/go.mod h1:P5HUIBuIWKbyjl083/loAegFkfbFNx5i2qEP4CNbm7E=
|
||||||
|
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 h1:+jumHNA0Wrelhe64i8F6HNlS8pkoyMv5sreGx2Ry5Rw=
|
||||||
|
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8/go.mod h1:3n1Cwaq1E1/1lhQhtRK2ts/ZwZEhjcQeJQ1RuC6Q/8U=
|
||||||
|
github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=
|
||||||
|
github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg=
|
||||||
github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY=
|
github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY=
|
||||||
github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo=
|
github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo=
|
||||||
github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
|
github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
|
||||||
@@ -612,6 +618,8 @@ github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiT
|
|||||||
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||||
github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
|
github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
|
||||||
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||||
|
github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU=
|
||||||
|
github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY=
|
||||||
github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad/go.mod h1:qLr4V1qq6nMqFKkMo8ZTx3f+BZEkzsRUY10Xsm2mwU0=
|
github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad/go.mod h1:qLr4V1qq6nMqFKkMo8ZTx3f+BZEkzsRUY10Xsm2mwU0=
|
||||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
|
github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
|
||||||
@@ -630,6 +638,8 @@ github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXl
|
|||||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||||
|
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
|
||||||
|
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
|
||||||
github.com/testcontainers/testcontainers-go v0.37.0 h1:L2Qc0vkTw2EHWQ08djon0D2uw7Z/PtHS/QzZZ5Ra/hg=
|
github.com/testcontainers/testcontainers-go v0.37.0 h1:L2Qc0vkTw2EHWQ08djon0D2uw7Z/PtHS/QzZZ5Ra/hg=
|
||||||
github.com/testcontainers/testcontainers-go v0.37.0/go.mod h1:QPzbxZhQ6Bclip9igjLFj6z0hs01bU8lrl2dHQmgFGM=
|
github.com/testcontainers/testcontainers-go v0.37.0/go.mod h1:QPzbxZhQ6Bclip9igjLFj6z0hs01bU8lrl2dHQmgFGM=
|
||||||
github.com/testcontainers/testcontainers-go/modules/mysql v0.37.0 h1:LqUos1oR5iuuzorFnSvxsHNdYdCHB/DfI82CuT58wbI=
|
github.com/testcontainers/testcontainers-go/modules/mysql v0.37.0 h1:LqUos1oR5iuuzorFnSvxsHNdYdCHB/DfI82CuT58wbI=
|
||||||
@@ -714,6 +724,7 @@ go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
|
|||||||
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
|
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
|
||||||
go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0=
|
go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0=
|
||||||
go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8=
|
go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8=
|
||||||
|
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
|
||||||
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
||||||
goauthentik.io/api/v3 v3.2023051.3 h1:NebAhD/TeTWNo/9X3/Uj+rM5fG1HaiLOlKTNLQv9Qq4=
|
goauthentik.io/api/v3 v3.2023051.3 h1:NebAhD/TeTWNo/9X3/Uj+rM5fG1HaiLOlKTNLQv9Qq4=
|
||||||
goauthentik.io/api/v3 v3.2023051.3/go.mod h1:nYECml4jGbp/541hj8GcylKQG1gVBsKppHy4+7G8u4U=
|
goauthentik.io/api/v3 v3.2023051.3/go.mod h1:nYECml4jGbp/541hj8GcylKQG1gVBsKppHy4+7G8u4U=
|
||||||
|
|||||||
@@ -114,8 +114,8 @@ func withAdminConfig(cmd *cobra.Command, applyIDPDefaults bool, fn func(ctx cont
|
|||||||
}
|
}
|
||||||
|
|
||||||
func loadAdminMgmtConfig(ctx context.Context, applyIDPDefaults bool) (*nbconfig.Config, string, error) {
|
func loadAdminMgmtConfig(ctx context.Context, applyIDPDefaults bool) (*nbconfig.Config, string, error) {
|
||||||
config := &nbconfig.Config{}
|
config, err := loadManagementConfig(nbconfig.MgmtConfigPath)
|
||||||
if _, err := util.ReadJsonWithEnvSub(nbconfig.MgmtConfigPath, config); err != nil {
|
if err != nil {
|
||||||
return nil, "", err
|
return nil, "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
nbconfig "github.com/netbirdio/netbird/management/internals/server/config"
|
||||||
|
configloader "github.com/netbirdio/netbird/util/config"
|
||||||
|
)
|
||||||
|
|
||||||
|
func loadManagementConfig(configPath string) (*nbconfig.Config, error) {
|
||||||
|
return configloader.Load(configPath, &nbconfig.Config{Datadir: defaultMgmtDataDir}, configloader.Options{
|
||||||
|
TagName: "json",
|
||||||
|
Transform: configloader.ExpandEnvTemplate,
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestLoadManagementConfigSources(t *testing.T) {
|
||||||
|
t.Setenv("MANAGEMENT_DATA_DIR", "/template-data")
|
||||||
|
t.Setenv("MANAGEMENT_ENCRYPTION_KEY", "template-key")
|
||||||
|
t.Setenv("NB_DATADIR", "/environment-data")
|
||||||
|
t.Setenv("NB_HTTPCONFIG_AUTHAUDIENCE", "environment-audience")
|
||||||
|
configPath := filepath.Join(t.TempDir(), "management.json")
|
||||||
|
require.NoError(t, os.WriteFile(configPath, []byte(`{
|
||||||
|
"Datadir": "{{ .MANAGEMENT_DATA_DIR }}",
|
||||||
|
"DataStoreEncryptionKey": "{{ .MANAGEMENT_ENCRYPTION_KEY }}",
|
||||||
|
"HttpConfig": {
|
||||||
|
"AuthAudience": "file-audience"
|
||||||
|
},
|
||||||
|
"TURNConfig": {
|
||||||
|
"CredentialsTTL": "1h"
|
||||||
|
}
|
||||||
|
}`), 0o600))
|
||||||
|
|
||||||
|
cfg, err := loadManagementConfig(configPath)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, "/environment-data", cfg.Datadir, "Bound environment values should override template values")
|
||||||
|
assert.Equal(t, "template-key", cfg.DataStoreEncryptionKey, "Template environment values should be expanded")
|
||||||
|
require.NotNil(t, cfg.HttpConfig, "Nested configuration should be decoded")
|
||||||
|
assert.Equal(t, "environment-audience", cfg.HttpConfig.AuthAudience, "Bound environment values should override the file")
|
||||||
|
require.NotNil(t, cfg.TURNConfig, "TURN configuration should be decoded")
|
||||||
|
assert.Equal(t, time.Hour, cfg.TURNConfig.CredentialsTTL.Duration, "JSON duration types should be decoded")
|
||||||
|
|
||||||
|
datadirFlag := mgmtCmd.Flags().Lookup("datadir")
|
||||||
|
oldDatadir := datadirFlag.Value.String()
|
||||||
|
oldChanged := datadirFlag.Changed
|
||||||
|
t.Cleanup(func() {
|
||||||
|
require.NoError(t, datadirFlag.Value.Set(oldDatadir))
|
||||||
|
datadirFlag.Changed = oldChanged
|
||||||
|
})
|
||||||
|
require.NoError(t, datadirFlag.Value.Set("/flag-data"))
|
||||||
|
datadirFlag.Changed = true
|
||||||
|
ApplyCommandLineOverrides(cfg, mgmtCmd.Flags())
|
||||||
|
assert.Equal(t, "/flag-data", cfg.Datadir, "Flags should override environment values")
|
||||||
|
}
|
||||||
@@ -19,6 +19,7 @@ import (
|
|||||||
|
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
|
"github.com/spf13/pflag"
|
||||||
|
|
||||||
"github.com/netbirdio/netbird/management/server/types"
|
"github.com/netbirdio/netbird/management/server/types"
|
||||||
|
|
||||||
@@ -60,7 +61,7 @@ var (
|
|||||||
// detect whether user specified a port
|
// detect whether user specified a port
|
||||||
userPort := cmd.Flag("port").Changed
|
userPort := cmd.Flag("port").Changed
|
||||||
|
|
||||||
config, err = LoadMgmtConfig(ctx, nbconfig.MgmtConfigPath)
|
config, err = LoadMgmtConfig(ctx, nbconfig.MgmtConfigPath, cmd.Flags())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed reading provided config file: %s: %v", nbconfig.MgmtConfigPath, err)
|
return fmt.Errorf("failed reading provided config file: %s: %v", nbconfig.MgmtConfigPath, err)
|
||||||
}
|
}
|
||||||
@@ -70,7 +71,7 @@ var (
|
|||||||
}
|
}
|
||||||
|
|
||||||
var tlsEnabled bool
|
var tlsEnabled bool
|
||||||
if mgmtLetsencryptDomain != "" || (config.HttpConfig.CertFile != "" && config.HttpConfig.CertKey != "") {
|
if config.HttpConfig.LetsEncryptDomain != "" || (config.HttpConfig.CertFile != "" && config.HttpConfig.CertKey != "") {
|
||||||
tlsEnabled = true
|
tlsEnabled = true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -171,15 +172,15 @@ var (
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
func LoadMgmtConfig(ctx context.Context, mgmtConfigPath string) (*nbconfig.Config, error) {
|
func LoadMgmtConfig(ctx context.Context, mgmtConfigPath string, flags *pflag.FlagSet) (*nbconfig.Config, error) {
|
||||||
loadedConfig := &nbconfig.Config{}
|
loadedConfig, err := loadManagementConfig(mgmtConfigPath)
|
||||||
if _, err := util.ReadJsonWithEnvSub(mgmtConfigPath, loadedConfig); err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
ApplyCommandLineOverrides(loadedConfig)
|
ApplyCommandLineOverrides(loadedConfig, flags)
|
||||||
|
|
||||||
err := grpc.ValidateSyncMessageVersion(loadedConfig.HighestSupportedSyncMessageVersion)
|
err = grpc.ValidateSyncMessageVersion(loadedConfig.HighestSupportedSyncMessageVersion)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -211,14 +212,18 @@ func LoadMgmtConfig(ctx context.Context, mgmtConfigPath string) (*nbconfig.Confi
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ApplyCommandLineOverrides applies command-line flag overrides to the config
|
// ApplyCommandLineOverrides applies command-line flag overrides to the config
|
||||||
func ApplyCommandLineOverrides(cfg *nbconfig.Config) {
|
func ApplyCommandLineOverrides(cfg *nbconfig.Config, flags *pflag.FlagSet) {
|
||||||
if mgmtLetsencryptDomain != "" {
|
hasCertOverride := flags.Changed("cert-key") && flags.Changed("cert-file")
|
||||||
|
if (flags.Changed("letsencrypt-domain") || hasCertOverride) && cfg.HttpConfig == nil {
|
||||||
|
cfg.HttpConfig = &nbconfig.HttpServerConfig{}
|
||||||
|
}
|
||||||
|
if flags.Changed("letsencrypt-domain") {
|
||||||
cfg.HttpConfig.LetsEncryptDomain = mgmtLetsencryptDomain
|
cfg.HttpConfig.LetsEncryptDomain = mgmtLetsencryptDomain
|
||||||
}
|
}
|
||||||
if mgmtDataDir != "" {
|
if flags.Changed("datadir") {
|
||||||
cfg.Datadir = mgmtDataDir
|
cfg.Datadir = mgmtDataDir
|
||||||
}
|
}
|
||||||
if certKey != "" && certFile != "" {
|
if hasCertOverride {
|
||||||
cfg.HttpConfig.CertFile = certFile
|
cfg.HttpConfig.CertFile = certFile
|
||||||
cfg.HttpConfig.CertKey = certKey
|
cfg.HttpConfig.CertKey = certKey
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ func Test_LoadMgmtConfig(t *testing.T) {
|
|||||||
tmpFile, err := createConfig(exampleConfig)
|
tmpFile, err := createConfig(exampleConfig)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
cfg, err := LoadMgmtConfig(context.Background(), tmpFile)
|
cfg, err := LoadMgmtConfig(context.Background(), tmpFile, mgmtCmd.Flags())
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.NotEmpty(t, cfg.Relay)
|
assert.NotEmpty(t, cfg.Relay)
|
||||||
assert.NotEmpty(t, cfg.Relay.Addresses)
|
assert.NotEmpty(t, cfg.Relay.Addresses)
|
||||||
@@ -54,7 +54,7 @@ func Test_LoadMgmtConfig_Empty(t *testing.T) {
|
|||||||
}`)
|
}`)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
cfg, err := LoadMgmtConfig(context.Background(), tmpFile)
|
cfg, err := LoadMgmtConfig(context.Background(), tmpFile, mgmtCmd.Flags())
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Nil(t, cfg.HighestSupportedSyncMessageVersion)
|
assert.Nil(t, cfg.HighestSupportedSyncMessageVersion)
|
||||||
assert.Nil(t, cfg.PerAccountHighestSupportedSyncMessageVersion)
|
assert.Nil(t, cfg.PerAccountHighestSupportedSyncMessageVersion)
|
||||||
|
|||||||
@@ -0,0 +1,47 @@
|
|||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
"golang.org/x/crypto/acme"
|
||||||
|
|
||||||
|
"github.com/netbirdio/netbird/proxy"
|
||||||
|
configloader "github.com/netbirdio/netbird/util/config"
|
||||||
|
)
|
||||||
|
|
||||||
|
type commandConfig struct {
|
||||||
|
proxy.Config `yaml:",squash"`
|
||||||
|
|
||||||
|
LogLevel string `yaml:"logLevel" env:"NB_PROXY_LOG_LEVEL" flag:"log-level"`
|
||||||
|
PreallocatedBuffers *uint32 `yaml:"preallocatedBuffers" env:"NB_PROXY_PREALLOCATED_BUFFERS"`
|
||||||
|
MaxBatchSize *uint32 `yaml:"maxBatchSize" env:"NB_PROXY_MAX_BATCH_SIZE"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func defaultConfig() *commandConfig {
|
||||||
|
return &commandConfig{
|
||||||
|
Config: proxy.Config{
|
||||||
|
ListenAddr: ":443",
|
||||||
|
ManagementAddress: DefaultManagementURL,
|
||||||
|
CertificateDirectory: "./certs",
|
||||||
|
CertificateFile: "tls.crt",
|
||||||
|
CertificateKeyFile: "tls.key",
|
||||||
|
ACMEChallengeAddress: ":80",
|
||||||
|
ACMEDirectory: acme.LetsEncryptURL,
|
||||||
|
ACMEChallengeType: "tls-alpn-01",
|
||||||
|
CertLockMethod: "auto",
|
||||||
|
HealthAddr: "localhost:8080",
|
||||||
|
ForwardedProto: "auto",
|
||||||
|
SupportsCustomPorts: true,
|
||||||
|
GeoDataDir: "/var/lib/netbird/geolocation",
|
||||||
|
},
|
||||||
|
LogLevel: "info",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadConfig(cmd *cobra.Command, configPath string) (*commandConfig, error) {
|
||||||
|
return configloader.Load(configPath, defaultConfig(), configloader.Options{
|
||||||
|
TagName: "yaml",
|
||||||
|
AllowMissing: configPath == "",
|
||||||
|
FlagSet: cmd.Flags(),
|
||||||
|
Strict: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestLoadConfigPrecedence(t *testing.T) {
|
||||||
|
configPath := filepath.Join(t.TempDir(), "proxy.yaml")
|
||||||
|
require.NoError(t, os.WriteFile(configPath, []byte(`
|
||||||
|
domain: file.example.com
|
||||||
|
trustedProxies: 192.0.2.0/24
|
||||||
|
maxDialTimeout: 5s
|
||||||
|
`), 0o600))
|
||||||
|
t.Setenv("NB_PROXY_TOKEN", "environment-token")
|
||||||
|
require.NoError(t, rootCmd.ParseFlags(nil))
|
||||||
|
|
||||||
|
domainFlag := rootCmd.Flags().Lookup("domain")
|
||||||
|
oldDomain := domainFlag.Value.String()
|
||||||
|
oldChanged := domainFlag.Changed
|
||||||
|
t.Cleanup(func() {
|
||||||
|
require.NoError(t, domainFlag.Value.Set(oldDomain))
|
||||||
|
domainFlag.Changed = oldChanged
|
||||||
|
})
|
||||||
|
require.NoError(t, domainFlag.Value.Set("flag.example.com"))
|
||||||
|
domainFlag.Changed = true
|
||||||
|
|
||||||
|
cfg, err := loadConfig(rootCmd, configPath)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, "flag.example.com", cfg.ProxyURL, "Flags should override the configuration file")
|
||||||
|
assert.Equal(t, "environment-token", cfg.ProxyToken, "Environment should populate secrets")
|
||||||
|
assert.Equal(t, 5*time.Second, cfg.MaxDialTimeout, "Durations should be decoded from the file")
|
||||||
|
require.NotNil(t, cfg.TrustedProxies, "Trusted proxies should be decoded")
|
||||||
|
assert.False(t, cfg.TrustedProxies.Empty(), "Configured trusted proxies should not be empty")
|
||||||
|
}
|
||||||
+48
-105
@@ -15,22 +15,13 @@ import (
|
|||||||
|
|
||||||
"github.com/netbirdio/netbird/shared/management/domain"
|
"github.com/netbirdio/netbird/shared/management/domain"
|
||||||
|
|
||||||
"github.com/netbirdio/netbird/client/embed"
|
|
||||||
"github.com/netbirdio/netbird/proxy"
|
"github.com/netbirdio/netbird/proxy"
|
||||||
nbacme "github.com/netbirdio/netbird/proxy/internal/acme"
|
|
||||||
"github.com/netbirdio/netbird/trustedproxy"
|
|
||||||
"github.com/netbirdio/netbird/util"
|
"github.com/netbirdio/netbird/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
// envPreallocatedBuffers caps the per-tunnel buffer pool. Zero (unset)
|
||||||
// envPreallocatedBuffers caps the per-tunnel buffer pool. Zero (unset)
|
// keeps the upstream uncapped default.
|
||||||
// keeps the upstream uncapped default.
|
const envPreallocatedBuffers = "NB_PROXY_PREALLOCATED_BUFFERS"
|
||||||
envPreallocatedBuffers = "NB_PROXY_PREALLOCATED_BUFFERS"
|
|
||||||
// envMaxBatchSize overrides the per-tunnel batch size, which controls
|
|
||||||
// how many buffers each receive/TUN worker eagerly allocates. Zero
|
|
||||||
// (unset) keeps the platform default.
|
|
||||||
envMaxBatchSize = "NB_PROXY_MAX_BATCH_SIZE"
|
|
||||||
)
|
|
||||||
|
|
||||||
const DefaultManagementURL = "https://api.netbird.io:443"
|
const DefaultManagementURL = "https://api.netbird.io:443"
|
||||||
|
|
||||||
@@ -79,6 +70,7 @@ var (
|
|||||||
geoDataDir string
|
geoDataDir string
|
||||||
crowdsecAPIURL string
|
crowdsecAPIURL string
|
||||||
crowdsecAPIKey string
|
crowdsecAPIKey string
|
||||||
|
configPath string
|
||||||
)
|
)
|
||||||
|
|
||||||
var rootCmd = &cobra.Command{
|
var rootCmd = &cobra.Command{
|
||||||
@@ -91,6 +83,7 @@ var rootCmd = &cobra.Command{
|
|||||||
}
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
|
rootCmd.Flags().StringVar(&configPath, "config", "", "path to configuration file")
|
||||||
rootCmd.PersistentFlags().StringVar(&logLevel, "log-level", envStringOrDefault("NB_PROXY_LOG_LEVEL", "info"), "Log level: panic, fatal, error, warn, info, debug, trace")
|
rootCmd.PersistentFlags().StringVar(&logLevel, "log-level", envStringOrDefault("NB_PROXY_LOG_LEVEL", "info"), "Log level: panic, fatal, error, warn, info, debug, trace")
|
||||||
rootCmd.PersistentFlags().BoolVar(&debugLogs, "debug", envBoolOrDefault("NB_PROXY_DEBUG_LOGS", false), "Enable debug logs")
|
rootCmd.PersistentFlags().BoolVar(&debugLogs, "debug", envBoolOrDefault("NB_PROXY_DEBUG_LOGS", false), "Enable debug logs")
|
||||||
_ = rootCmd.PersistentFlags().MarkDeprecated("debug", "use --log-level instead")
|
_ = rootCmd.PersistentFlags().MarkDeprecated("debug", "use --log-level instead")
|
||||||
@@ -145,118 +138,68 @@ func SetVersionInfo(version, commit, buildDate, goVersion string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func runServer(cmd *cobra.Command, args []string) error {
|
func runServer(cmd *cobra.Command, args []string) error {
|
||||||
proxyToken := os.Getenv(envProxyToken)
|
cfg, err := loadConfig(cmd, configPath)
|
||||||
if proxyToken == "" {
|
if err != nil {
|
||||||
return fmt.Errorf("proxy token is required: set %s environment variable", envProxyToken)
|
return fmt.Errorf("load config: %w", err)
|
||||||
|
}
|
||||||
|
if cfg.ProxyToken == "" {
|
||||||
|
return fmt.Errorf("proxy token is required: set proxyToken or %s", envProxyToken)
|
||||||
}
|
}
|
||||||
|
|
||||||
level := logLevel
|
level := cfg.LogLevel
|
||||||
if debugLogs {
|
if debugLogs && (!cmd.Flags().Changed("log-level") || cmd.Flags().Changed("debug")) {
|
||||||
level = "debug"
|
level = "debug"
|
||||||
}
|
}
|
||||||
logger := log.New()
|
logger := log.New()
|
||||||
|
|
||||||
_ = util.InitLogger(logger, level, util.LogConsole)
|
_ = util.InitLogger(logger, level, util.LogConsole)
|
||||||
|
|
||||||
logger.Infof("configured log level: %s", level)
|
logger.Infof("configured log level: %s", level)
|
||||||
|
|
||||||
var wgPool, wgBatch uint64
|
proxyConfig := cfg.Config
|
||||||
var perf embed.Performance
|
proxyConfig.Logger = logger
|
||||||
if raw := os.Getenv(envPreallocatedBuffers); raw != "" {
|
proxyConfig.Version = Version
|
||||||
n, err := strconv.ParseUint(raw, 10, 32)
|
applyPerformanceConfig(&proxyConfig, cfg, logger)
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("invalid %s %q: %w", envPreallocatedBuffers, raw, err)
|
|
||||||
}
|
|
||||||
wgPool = n
|
|
||||||
v := uint32(n)
|
|
||||||
perf.PreallocatedBuffersPerPool = &v
|
|
||||||
logger.Infof("tunnel preallocated buffers per pool: %d", n)
|
|
||||||
}
|
|
||||||
if raw := os.Getenv(envMaxBatchSize); raw != "" {
|
|
||||||
n, err := strconv.ParseUint(raw, 10, 32)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("invalid %s %q: %w", envMaxBatchSize, raw, err)
|
|
||||||
}
|
|
||||||
wgBatch = n
|
|
||||||
v := uint32(n)
|
|
||||||
perf.MaxBatchSize = &v
|
|
||||||
logger.Infof("tunnel max batch size override: %d", n)
|
|
||||||
}
|
|
||||||
if wgPool > 0 {
|
|
||||||
// Each bind recv goroutine (IPv4 + IPv6 + ICE relay) plus
|
|
||||||
// RoutineReadFromTUN eagerly reserves `batch` message buffers for
|
|
||||||
// the lifetime of the Device. A pool cap below that floor blocks
|
|
||||||
// the receive pipeline at startup.
|
|
||||||
batch := wgBatch
|
|
||||||
if batch == 0 {
|
|
||||||
batch = 128
|
|
||||||
}
|
|
||||||
const recvGoroutines = 4
|
|
||||||
floor := batch * recvGoroutines
|
|
||||||
if wgPool < floor {
|
|
||||||
logger.Warnf("%s=%d is below the eager-allocation floor (~%d for batch=%d); startup may deadlock",
|
|
||||||
envPreallocatedBuffers, wgPool, floor, batch)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
switch forwardedProto {
|
switch proxyConfig.ForwardedProto {
|
||||||
case "auto", "http", "https":
|
case "auto", "http", "https":
|
||||||
default:
|
default:
|
||||||
return fmt.Errorf("invalid --forwarded-proto value %q: must be auto, http, or https", forwardedProto)
|
return fmt.Errorf("invalid --forwarded-proto value %q: must be auto, http, or https", proxyConfig.ForwardedProto)
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err := domain.ValidateDomains([]string{proxyDomain})
|
if _, err := domain.ValidateDomains([]string{proxyConfig.ProxyURL}); err != nil {
|
||||||
if err != nil {
|
return fmt.Errorf("invalid domain value %q: %w", proxyConfig.ProxyURL, err)
|
||||||
return fmt.Errorf("invalid domain value %q: %w", proxyDomain, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
parsedTrustedProxies, err := trustedproxy.Parse(trustedProxies)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("invalid --trusted-proxies: %w", err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGTERM, syscall.SIGINT)
|
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGTERM, syscall.SIGINT)
|
||||||
defer stop()
|
defer stop()
|
||||||
|
|
||||||
srv := proxy.New(ctx, proxy.Config{
|
srv := proxy.New(ctx, proxyConfig)
|
||||||
ListenAddr: addr,
|
return srv.ListenAndServe(ctx, proxyConfig.ListenAddr)
|
||||||
Logger: logger,
|
}
|
||||||
Version: Version,
|
|
||||||
ManagementAddress: mgmtAddr,
|
|
||||||
ProxyURL: proxyDomain,
|
|
||||||
ProxyToken: proxyToken,
|
|
||||||
CertificateDirectory: certDir,
|
|
||||||
CertificateFile: certFile,
|
|
||||||
CertificateKeyFile: certKeyFile,
|
|
||||||
GenerateACMECertificates: acmeCerts,
|
|
||||||
ACMEChallengeAddress: acmeAddr,
|
|
||||||
ACMEDirectory: acmeDir,
|
|
||||||
ACMEEABKID: acmeEABKID,
|
|
||||||
ACMEEABHMACKey: acmeEABHMACKey,
|
|
||||||
ACMEChallengeType: acmeChallengeType,
|
|
||||||
DebugEndpointEnabled: debugEndpoint,
|
|
||||||
DebugEndpointAddress: debugEndpointAddr,
|
|
||||||
HealthAddr: healthAddr,
|
|
||||||
ForwardedProto: forwardedProto,
|
|
||||||
TrustedProxies: parsedTrustedProxies,
|
|
||||||
CertLockMethod: nbacme.CertLockMethod(certLockMethod),
|
|
||||||
WildcardCertDir: wildcardCertDir,
|
|
||||||
WireguardPort: wgPort,
|
|
||||||
Performance: perf,
|
|
||||||
ProxyProtocol: proxyProtocol,
|
|
||||||
PreSharedKey: preSharedKey,
|
|
||||||
SupportsCustomPorts: supportsCustomPorts,
|
|
||||||
RequireSubdomain: requireSubdomain,
|
|
||||||
Private: private,
|
|
||||||
MaxDialTimeout: maxDialTimeout,
|
|
||||||
MaxSessionIdleTimeout: maxSessionIdleTimeout,
|
|
||||||
MappingBatchWatchdog: envDurationOrDefault("NB_PROXY_MAPPING_BATCH_WATCHDOG", 0),
|
|
||||||
GeoDataDir: geoDataDir,
|
|
||||||
CrowdSecAPIURL: crowdsecAPIURL,
|
|
||||||
CrowdSecAPIKey: crowdsecAPIKey,
|
|
||||||
})
|
|
||||||
|
|
||||||
return srv.ListenAndServe(ctx, addr)
|
func applyPerformanceConfig(proxyConfig *proxy.Config, cfg *commandConfig, logger *log.Logger) {
|
||||||
|
if cfg.PreallocatedBuffers != nil {
|
||||||
|
proxyConfig.Performance.PreallocatedBuffersPerPool = cfg.PreallocatedBuffers
|
||||||
|
logger.Infof("tunnel preallocated buffers per pool: %d", *cfg.PreallocatedBuffers)
|
||||||
|
}
|
||||||
|
if cfg.MaxBatchSize != nil {
|
||||||
|
proxyConfig.Performance.MaxBatchSize = cfg.MaxBatchSize
|
||||||
|
logger.Infof("tunnel max batch size override: %d", *cfg.MaxBatchSize)
|
||||||
|
}
|
||||||
|
if cfg.PreallocatedBuffers == nil || *cfg.PreallocatedBuffers == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
batch := uint64(128)
|
||||||
|
if cfg.MaxBatchSize != nil && *cfg.MaxBatchSize > 0 {
|
||||||
|
batch = uint64(*cfg.MaxBatchSize)
|
||||||
|
}
|
||||||
|
const recvGoroutines = 4
|
||||||
|
floor := batch * recvGoroutines
|
||||||
|
pool := uint64(*cfg.PreallocatedBuffers)
|
||||||
|
if pool < floor {
|
||||||
|
logger.Warnf("%s=%d is below the eager-allocation floor (~%d for batch=%d); startup may deadlock",
|
||||||
|
envPreallocatedBuffers, pool, floor, batch)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func envBoolOrDefault(key string, def bool) bool {
|
func envBoolOrDefault(key string, def bool) bool {
|
||||||
|
|||||||
+36
-36
@@ -20,112 +20,112 @@ import (
|
|||||||
// adding fields here must not change the zero-value behaviour of Server.
|
// adding fields here must not change the zero-value behaviour of Server.
|
||||||
type Config struct {
|
type Config struct {
|
||||||
// ListenAddr is the TCP address the main listener binds. Required.
|
// ListenAddr is the TCP address the main listener binds. Required.
|
||||||
ListenAddr string
|
ListenAddr string `yaml:"listenAddress" env:"NB_PROXY_ADDRESS" flag:"addr"`
|
||||||
// ID identifies this proxy instance to management. Empty values are
|
// ID identifies this proxy instance to management. Empty values are
|
||||||
// replaced with a timestamped default at Server.Start time (see
|
// replaced with a timestamped default at Server.Start time (see
|
||||||
// initDefaults), not in New.
|
// initDefaults), not in New.
|
||||||
ID string
|
ID string `yaml:"id" env:"NB_PROXY_ID"`
|
||||||
// Logger is the logrus logger used everywhere. Empty values fall
|
// Logger is the logrus logger used everywhere. Empty values fall
|
||||||
// back to log.StandardLogger() at Server.Start time (see
|
// back to log.StandardLogger() at Server.Start time (see
|
||||||
// initDefaults), not in New.
|
// initDefaults), not in New.
|
||||||
Logger *log.Logger
|
Logger *log.Logger `yaml:"-" env:"-" flag:"-"`
|
||||||
// Version is the build version string reported to management. Empty
|
// Version is the build version string reported to management. Empty
|
||||||
// values are replaced with "dev" at Server.Start time (see
|
// values are replaced with "dev" at Server.Start time (see
|
||||||
// initDefaults), not in New.
|
// initDefaults), not in New.
|
||||||
Version string
|
Version string `yaml:"-" env:"-" flag:"-"`
|
||||||
// ProxyURL is the public address operators use to reach this proxy.
|
// ProxyURL is the public address operators use to reach this proxy.
|
||||||
ProxyURL string
|
ProxyURL string `yaml:"domain" env:"NB_PROXY_DOMAIN" flag:"domain"`
|
||||||
// ManagementAddress is the gRPC URL of the management server.
|
// ManagementAddress is the gRPC URL of the management server.
|
||||||
ManagementAddress string
|
ManagementAddress string `yaml:"managementAddress" env:"NB_PROXY_MANAGEMENT_ADDRESS" flag:"mgmt"`
|
||||||
// ProxyToken authenticates this proxy with the management server.
|
// ProxyToken authenticates this proxy with the management server.
|
||||||
ProxyToken string
|
ProxyToken string `yaml:"proxyToken" env:"NB_PROXY_TOKEN"`
|
||||||
|
|
||||||
// CertificateDirectory is the directory holding TLS certificate
|
// CertificateDirectory is the directory holding TLS certificate
|
||||||
// material (static or ACME-provisioned).
|
// material (static or ACME-provisioned).
|
||||||
CertificateDirectory string
|
CertificateDirectory string `yaml:"certificateDirectory" env:"NB_PROXY_CERTIFICATE_DIRECTORY" flag:"cert-dir"`
|
||||||
// CertificateFile is the certificate filename within
|
// CertificateFile is the certificate filename within
|
||||||
// CertificateDirectory.
|
// CertificateDirectory.
|
||||||
CertificateFile string
|
CertificateFile string `yaml:"certificateFile" env:"NB_PROXY_CERTIFICATE_FILE" flag:"cert-file"`
|
||||||
// CertificateKeyFile is the private key filename within
|
// CertificateKeyFile is the private key filename within
|
||||||
// CertificateDirectory.
|
// CertificateDirectory.
|
||||||
CertificateKeyFile string
|
CertificateKeyFile string `yaml:"certificateKeyFile" env:"NB_PROXY_CERTIFICATE_KEY_FILE" flag:"cert-key-file"`
|
||||||
// GenerateACMECertificates toggles ACME certificate provisioning.
|
// GenerateACMECertificates toggles ACME certificate provisioning.
|
||||||
GenerateACMECertificates bool
|
GenerateACMECertificates bool `yaml:"generateACMECertificates" env:"NB_PROXY_ACME_CERTIFICATES" flag:"acme-certs"`
|
||||||
// ACMEChallengeAddress is the listen address for HTTP-01 challenges.
|
// ACMEChallengeAddress is the listen address for HTTP-01 challenges.
|
||||||
ACMEChallengeAddress string
|
ACMEChallengeAddress string `yaml:"acmeChallengeAddress" env:"NB_PROXY_ACME_ADDRESS" flag:"acme-addr"`
|
||||||
// ACMEDirectory is the ACME directory URL (Let's Encrypt by default).
|
// ACMEDirectory is the ACME directory URL (Let's Encrypt by default).
|
||||||
ACMEDirectory string
|
ACMEDirectory string `yaml:"acmeDirectory" env:"NB_PROXY_ACME_DIRECTORY" flag:"acme-dir"`
|
||||||
// ACMEEABKID is the External Account Binding Key ID for CAs that
|
// ACMEEABKID is the External Account Binding Key ID for CAs that
|
||||||
// require EAB (e.g. ZeroSSL).
|
// require EAB (e.g. ZeroSSL).
|
||||||
ACMEEABKID string
|
ACMEEABKID string `yaml:"acmeEABKID" env:"NB_PROXY_ACME_EAB_KID" flag:"acme-eab-kid"`
|
||||||
// ACMEEABHMACKey is the External Account Binding HMAC key for CAs
|
// ACMEEABHMACKey is the External Account Binding HMAC key for CAs
|
||||||
// that require EAB.
|
// that require EAB.
|
||||||
ACMEEABHMACKey string
|
ACMEEABHMACKey string `yaml:"acmeEABHMACKey" env:"NB_PROXY_ACME_EAB_HMAC_KEY" flag:"acme-eab-hmac-key"`
|
||||||
// ACMEChallengeType is the ACME challenge type ("tls-alpn-01" or
|
// ACMEChallengeType is the ACME challenge type ("tls-alpn-01" or
|
||||||
// "http-01"). Empty defaults to "tls-alpn-01".
|
// "http-01"). Empty defaults to "tls-alpn-01".
|
||||||
ACMEChallengeType string
|
ACMEChallengeType string `yaml:"acmeChallengeType" env:"NB_PROXY_ACME_CHALLENGE_TYPE" flag:"acme-challenge-type"`
|
||||||
// CertLockMethod controls how ACME certificate locks are coordinated
|
// CertLockMethod controls how ACME certificate locks are coordinated
|
||||||
// across replicas.
|
// across replicas.
|
||||||
CertLockMethod acme.CertLockMethod
|
CertLockMethod acme.CertLockMethod `yaml:"certLockMethod" env:"NB_PROXY_CERT_LOCK_METHOD" flag:"cert-lock-method"`
|
||||||
// WildcardCertDir is an optional directory containing static wildcard
|
// WildcardCertDir is an optional directory containing static wildcard
|
||||||
// certificates that override ACME for matching domains.
|
// certificates that override ACME for matching domains.
|
||||||
WildcardCertDir string
|
WildcardCertDir string `yaml:"wildcardCertDir" env:"NB_PROXY_WILDCARD_CERT_DIR" flag:"wildcard-cert-dir"`
|
||||||
|
|
||||||
// DebugEndpointEnabled toggles the debug HTTP endpoint.
|
// DebugEndpointEnabled toggles the debug HTTP endpoint.
|
||||||
DebugEndpointEnabled bool
|
DebugEndpointEnabled bool `yaml:"debugEndpointEnabled" env:"NB_PROXY_DEBUG_ENDPOINT" flag:"debug-endpoint"`
|
||||||
// DebugEndpointAddress is the bind address for the debug endpoint.
|
// DebugEndpointAddress is the bind address for the debug endpoint.
|
||||||
DebugEndpointAddress string
|
DebugEndpointAddress string `yaml:"debugEndpointAddress" env:"NB_PROXY_DEBUG_ENDPOINT_ADDRESS" flag:"debug-endpoint-addr"`
|
||||||
// HealthAddr is the bind address for the health probe and metrics
|
// HealthAddr is the bind address for the health probe and metrics
|
||||||
// surface. Empty disables the health probe entirely (library callers
|
// surface. Empty disables the health probe entirely (library callers
|
||||||
// can attach their own).
|
// can attach their own).
|
||||||
HealthAddr string
|
HealthAddr string `yaml:"healthAddress" env:"NB_PROXY_HEALTH_ADDRESS" flag:"health-addr"`
|
||||||
|
|
||||||
// ForwardedProto overrides the X-Forwarded-Proto value sent to
|
// ForwardedProto overrides the X-Forwarded-Proto value sent to
|
||||||
// backends. Valid values: "auto", "http", "https".
|
// backends. Valid values: "auto", "http", "https".
|
||||||
ForwardedProto string
|
ForwardedProto string `yaml:"forwardedProto" env:"NB_PROXY_FORWARDED_PROTO" flag:"forwarded-proto"`
|
||||||
// TrustedProxies is the set of trusted upstream proxies that may set
|
// TrustedProxies is the set of trusted upstream proxies that may set
|
||||||
// forwarding headers.
|
// forwarding headers.
|
||||||
TrustedProxies *trustedproxy.List
|
TrustedProxies *trustedproxy.List `yaml:"trustedProxies" env:"NB_PROXY_TRUSTED_PROXIES" flag:"trusted-proxies"`
|
||||||
// WireguardPort is the UDP port for the embedded NetBird tunnel.
|
// WireguardPort is the UDP port for the embedded NetBird tunnel.
|
||||||
// Zero asks the OS for a random port.
|
// Zero asks the OS for a random port.
|
||||||
WireguardPort uint16
|
WireguardPort uint16 `yaml:"wireguardPort" env:"NB_PROXY_WG_PORT" flag:"wg-port"`
|
||||||
// ProxyProtocol enables PROXY protocol (v1/v2) on TCP listeners.
|
// ProxyProtocol enables PROXY protocol (v1/v2) on TCP listeners.
|
||||||
ProxyProtocol bool
|
ProxyProtocol bool `yaml:"proxyProtocol" env:"NB_PROXY_PROXY_PROTOCOL" flag:"proxy-protocol"`
|
||||||
// PreSharedKey is the WireGuard pre-shared key used between the
|
// PreSharedKey is the WireGuard pre-shared key used between the
|
||||||
// proxy's embedded clients and peers.
|
// proxy's embedded clients and peers.
|
||||||
PreSharedKey string
|
PreSharedKey string `yaml:"preSharedKey" env:"NB_PROXY_PRESHARED_KEY" flag:"preshared-key"`
|
||||||
// Performance configures the tunnel pool/batch sizes for every
|
// Performance configures the tunnel pool/batch sizes for every
|
||||||
// embedded client this proxy creates. Zero values fall back to
|
// embedded client this proxy creates. Zero values fall back to
|
||||||
// upstream defaults.
|
// upstream defaults.
|
||||||
Performance embed.Performance
|
Performance embed.Performance `yaml:"performance" env:"-" flag:"-"`
|
||||||
|
|
||||||
// SupportsCustomPorts indicates whether the proxy can bind arbitrary
|
// SupportsCustomPorts indicates whether the proxy can bind arbitrary
|
||||||
// ports for TCP/UDP/TLS services.
|
// ports for TCP/UDP/TLS services.
|
||||||
SupportsCustomPorts bool
|
SupportsCustomPorts bool `yaml:"supportsCustomPorts" env:"NB_PROXY_SUPPORTS_CUSTOM_PORTS" flag:"supports-custom-ports"`
|
||||||
// RequireSubdomain forces accounts to use a subdomain in front of
|
// RequireSubdomain forces accounts to use a subdomain in front of
|
||||||
// the proxy's cluster domain.
|
// the proxy's cluster domain.
|
||||||
RequireSubdomain bool
|
RequireSubdomain bool `yaml:"requireSubdomain" env:"NB_PROXY_REQUIRE_SUBDOMAIN" flag:"require-subdomain"`
|
||||||
// Private flags this proxy as embedded in a netbird client and
|
// Private flags this proxy as embedded in a netbird client and
|
||||||
// serving exclusively over the WireGuard tunnel. Also enables
|
// serving exclusively over the WireGuard tunnel. Also enables
|
||||||
// per-account inbound listeners on each embedded client's netstack.
|
// per-account inbound listeners on each embedded client's netstack.
|
||||||
Private bool
|
Private bool `yaml:"private" env:"NB_PROXY_PRIVATE" flag:"private"`
|
||||||
|
|
||||||
// MaxDialTimeout caps the per-service backend dial timeout.
|
// MaxDialTimeout caps the per-service backend dial timeout.
|
||||||
MaxDialTimeout time.Duration
|
MaxDialTimeout time.Duration `yaml:"maxDialTimeout" env:"NB_PROXY_MAX_DIAL_TIMEOUT" flag:"max-dial-timeout"`
|
||||||
// MaxSessionIdleTimeout caps the per-service session idle timeout.
|
// MaxSessionIdleTimeout caps the per-service session idle timeout.
|
||||||
MaxSessionIdleTimeout time.Duration
|
MaxSessionIdleTimeout time.Duration `yaml:"maxSessionIdleTimeout" env:"NB_PROXY_MAX_SESSION_IDLE_TIMEOUT" flag:"max-session-idle-timeout"`
|
||||||
// MappingBatchWatchdog bounds how long a single mapping batch may spend
|
// MappingBatchWatchdog bounds how long a single mapping batch may spend
|
||||||
// being applied before the receive loop reconnects to resync. Zero falls
|
// being applied before the receive loop reconnects to resync. Zero falls
|
||||||
// back to the internal default.
|
// back to the internal default.
|
||||||
MappingBatchWatchdog time.Duration
|
MappingBatchWatchdog time.Duration `yaml:"mappingBatchWatchdog" env:"NB_PROXY_MAPPING_BATCH_WATCHDOG"`
|
||||||
|
|
||||||
// GeoDataDir is the directory containing GeoLite2 MMDB files.
|
// GeoDataDir is the directory containing GeoLite2 MMDB files.
|
||||||
GeoDataDir string
|
GeoDataDir string `yaml:"geoDataDir" env:"NB_PROXY_GEO_DATA_DIR" flag:"geo-data-dir"`
|
||||||
// CrowdSecAPIURL is the CrowdSec LAPI URL. Empty disables CrowdSec.
|
// CrowdSecAPIURL is the CrowdSec LAPI URL. Empty disables CrowdSec.
|
||||||
CrowdSecAPIURL string
|
CrowdSecAPIURL string `yaml:"crowdSecAPIURL" env:"NB_PROXY_CROWDSEC_API_URL" flag:"crowdsec-api-url"`
|
||||||
// CrowdSecAPIKey is the CrowdSec bouncer API key. Empty disables
|
// CrowdSecAPIKey is the CrowdSec bouncer API key. Empty disables
|
||||||
// CrowdSec.
|
// CrowdSec.
|
||||||
CrowdSecAPIKey string
|
CrowdSecAPIKey string `yaml:"crowdSecAPIKey" env:"NB_PROXY_CROWDSEC_API_KEY" flag:"crowdsec-api-key"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// New builds a Server from cfg without performing any I/O. No goroutines
|
// New builds a Server from cfg without performing any I/O. No goroutines
|
||||||
|
|||||||
@@ -0,0 +1,48 @@
|
|||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestLoadConfigPrecedence(t *testing.T) {
|
||||||
|
path := filepath.Join(t.TempDir(), "relay.yaml")
|
||||||
|
require.NoError(t, os.WriteFile(path, []byte(`
|
||||||
|
listenAddress: ":8443"
|
||||||
|
exposedAddress: "relay.example.com:443"
|
||||||
|
authSecret: file-secret
|
||||||
|
metricsPort: 9091
|
||||||
|
enableSTUN: true
|
||||||
|
stunPorts: [3478, 3479]
|
||||||
|
`), 0o600))
|
||||||
|
t.Setenv("NB_METRICS_PORT", "9191")
|
||||||
|
require.NoError(t, rootCmd.ParseFlags(nil))
|
||||||
|
|
||||||
|
listenFlag := rootCmd.Flags().Lookup("listen-address")
|
||||||
|
oldListen := listenFlag.Value.String()
|
||||||
|
oldChanged := listenFlag.Changed
|
||||||
|
t.Cleanup(func() {
|
||||||
|
require.NoError(t, listenFlag.Value.Set(oldListen))
|
||||||
|
listenFlag.Changed = oldChanged
|
||||||
|
})
|
||||||
|
require.NoError(t, listenFlag.Value.Set(":7443"))
|
||||||
|
listenFlag.Changed = true
|
||||||
|
|
||||||
|
oldConfigPath := configPath
|
||||||
|
configPath = path
|
||||||
|
t.Cleanup(func() {
|
||||||
|
configPath = oldConfigPath
|
||||||
|
})
|
||||||
|
|
||||||
|
cfg, err := loadConfig(rootCmd)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, ":7443", cfg.ListenAddress, "Flags should override the configuration file")
|
||||||
|
assert.Equal(t, 9191, cfg.MetricsPort, "Environment should override the configuration file")
|
||||||
|
assert.Equal(t, "relay.example.com:443", cfg.ExposedAddress, "File values should override defaults")
|
||||||
|
assert.True(t, cfg.EnableSTUN, "STUN should be configurable from the file")
|
||||||
|
assert.Equal(t, []int{3478, 3479}, cfg.STUNPorts, "STUN ports should load from the 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
|
|
||||||
}
|
|
||||||
+57
-28
@@ -26,33 +26,47 @@ import (
|
|||||||
"github.com/netbirdio/netbird/stun"
|
"github.com/netbirdio/netbird/stun"
|
||||||
"github.com/netbirdio/netbird/trustedproxy"
|
"github.com/netbirdio/netbird/trustedproxy"
|
||||||
"github.com/netbirdio/netbird/util"
|
"github.com/netbirdio/netbird/util"
|
||||||
|
configloader "github.com/netbirdio/netbird/util/config"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Config contains relay service startup configuration.
|
||||||
type Config struct {
|
type Config struct {
|
||||||
ListenAddress string
|
ListenAddress string `yaml:"listenAddress" env:"NB_LISTEN_ADDRESS" flag:"listen-address"`
|
||||||
// in HA every peer connect to a common domain, the instance domain has been distributed during the p2p connection
|
// in HA every peer connect to a common domain, the instance domain has been distributed during the p2p connection
|
||||||
// it is a domain:port or ip:port
|
// it is a domain:port or ip:port
|
||||||
ExposedAddress string
|
ExposedAddress string `yaml:"exposedAddress" env:"NB_EXPOSED_ADDRESS" flag:"exposed-address"`
|
||||||
MetricsPort int
|
MetricsPort int `yaml:"metricsPort" env:"NB_METRICS_PORT" flag:"metrics-port"`
|
||||||
LetsencryptEmail string
|
LetsencryptEmail string `yaml:"letsencryptEmail" env:"NB_LETSENCRYPT_EMAIL" flag:"letsencrypt-email"`
|
||||||
LetsencryptDataDir string
|
LetsencryptDataDir string `yaml:"letsencryptDataDir" env:"NB_LETSENCRYPT_DATA_DIR" flag:"letsencrypt-data-dir"`
|
||||||
LetsencryptDomains []string
|
LetsencryptDomains []string `yaml:"letsencryptDomains" env:"NB_LETSENCRYPT_DOMAINS" flag:"letsencrypt-domains"`
|
||||||
// in case of using Route 53 for DNS challenge the credentials should be provided in the environment variables or
|
// in case of using Route 53 for DNS challenge the credentials should be provided in the environment variables or
|
||||||
// in the AWS credentials file
|
// in the AWS credentials file
|
||||||
LetsencryptAWSRoute53 bool
|
LetsencryptAWSRoute53 bool `yaml:"letsencryptAWSRoute53" env:"NB_LETSENCRYPT_AWS_ROUTE53" flag:"letsencrypt-aws-route53"`
|
||||||
TlsCertFile string
|
TlsCertFile string `yaml:"tlsCertFile" env:"NB_TLS_CERT_FILE" flag:"tls-cert-file"`
|
||||||
TlsKeyFile string
|
TlsKeyFile string `yaml:"tlsKeyFile" env:"NB_TLS_KEY_FILE" flag:"tls-key-file"`
|
||||||
AuthSecret string
|
AuthSecret string `yaml:"authSecret" env:"NB_AUTH_SECRET" flag:"auth-secret"`
|
||||||
LogLevel string
|
LogLevel string `yaml:"logLevel" env:"NB_LOG_LEVEL" flag:"log-level"`
|
||||||
LogFile string
|
LogFile string `yaml:"logFile" env:"NB_LOG_FILE" flag:"log-file"`
|
||||||
HealthcheckListenAddress string
|
HealthcheckListenAddress string `yaml:"healthcheckListenAddress" env:"NB_HEALTH_LISTEN_ADDRESS" flag:"health-listen-address"`
|
||||||
// TrustedProxies is a comma-separated list of upstream proxy CIDRs/IPs whose
|
// TrustedProxies is a comma-separated list of upstream proxy CIDRs/IPs whose
|
||||||
// X-Real-Ip/X-Real-Port headers are trusted. Empty means never trust these headers.
|
// X-Real-Ip/X-Real-Port headers are trusted. Empty means never trust these headers.
|
||||||
TrustedProxies string
|
TrustedProxies string `yaml:"trustedProxies" env:"NB_TRUSTED_PROXIES" flag:"trusted-proxies"`
|
||||||
// STUN server configuration
|
// STUN server configuration
|
||||||
EnableSTUN bool
|
EnableSTUN bool `yaml:"enableSTUN" env:"NB_ENABLE_STUN" flag:"enable-stun"`
|
||||||
STUNPorts []int
|
STUNPorts []int `yaml:"stunPorts" env:"NB_STUN_PORTS" flag:"stun-ports"`
|
||||||
STUNLogLevel string
|
STUNLogLevel string `yaml:"stunLogLevel" env:"NB_STUN_LOG_LEVEL" flag:"stun-log-level"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func defaultConfig() *Config {
|
||||||
|
return &Config{
|
||||||
|
ListenAddress: ":443",
|
||||||
|
MetricsPort: 9090,
|
||||||
|
LogLevel: "info",
|
||||||
|
LogFile: "console",
|
||||||
|
HealthcheckListenAddress: ":9000",
|
||||||
|
STUNPorts: []int{3478},
|
||||||
|
STUNLogLevel: "info",
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c Config) Validate() error {
|
func (c Config) Validate() error {
|
||||||
@@ -93,6 +107,7 @@ func (c Config) HasLetsEncrypt() bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
configPath string
|
||||||
cobraConfig *Config
|
cobraConfig *Config
|
||||||
rootCmd = &cobra.Command{
|
rootCmd = &cobra.Command{
|
||||||
Use: "relay",
|
Use: "relay",
|
||||||
@@ -106,10 +121,11 @@ var (
|
|||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
_ = util.InitLog("trace", util.LogConsole)
|
_ = util.InitLog("trace", util.LogConsole)
|
||||||
cobraConfig = &Config{}
|
cobraConfig = defaultConfig()
|
||||||
rootCmd.PersistentFlags().StringVarP(&cobraConfig.ListenAddress, "listen-address", "l", ":443", "listen address")
|
rootCmd.PersistentFlags().StringVar(&configPath, "config", "", "path to configuration file")
|
||||||
|
rootCmd.PersistentFlags().StringVarP(&cobraConfig.ListenAddress, "listen-address", "l", cobraConfig.ListenAddress, "listen address")
|
||||||
rootCmd.PersistentFlags().StringVarP(&cobraConfig.ExposedAddress, "exposed-address", "e", "", "instance domain address (or ip) and port, it will be distributes between peers")
|
rootCmd.PersistentFlags().StringVarP(&cobraConfig.ExposedAddress, "exposed-address", "e", "", "instance domain address (or ip) and port, it will be distributes between peers")
|
||||||
rootCmd.PersistentFlags().IntVar(&cobraConfig.MetricsPort, "metrics-port", 9090, "metrics endpoint http port. Metrics are accessible under host:metrics-port/metrics")
|
rootCmd.PersistentFlags().IntVar(&cobraConfig.MetricsPort, "metrics-port", cobraConfig.MetricsPort, "metrics endpoint http port. Metrics are accessible under host:metrics-port/metrics")
|
||||||
rootCmd.PersistentFlags().StringVarP(&cobraConfig.LetsencryptDataDir, "letsencrypt-data-dir", "d", "", "a directory to store Let's Encrypt data. Required if Let's Encrypt is enabled.")
|
rootCmd.PersistentFlags().StringVarP(&cobraConfig.LetsencryptDataDir, "letsencrypt-data-dir", "d", "", "a directory to store Let's Encrypt data. Required if Let's Encrypt is enabled.")
|
||||||
rootCmd.PersistentFlags().StringSliceVarP(&cobraConfig.LetsencryptDomains, "letsencrypt-domains", "a", nil, "list of domains to issue Let's Encrypt certificate for. Enables TLS using Let's Encrypt. Will fetch and renew certificate, and run the server with TLS")
|
rootCmd.PersistentFlags().StringSliceVarP(&cobraConfig.LetsencryptDomains, "letsencrypt-domains", "a", nil, "list of domains to issue Let's Encrypt certificate for. Enables TLS using Let's Encrypt. Will fetch and renew certificate, and run the server with TLS")
|
||||||
rootCmd.PersistentFlags().StringVar(&cobraConfig.LetsencryptEmail, "letsencrypt-email", "", "email address to use for Let's Encrypt certificate registration")
|
rootCmd.PersistentFlags().StringVar(&cobraConfig.LetsencryptEmail, "letsencrypt-email", "", "email address to use for Let's Encrypt certificate registration")
|
||||||
@@ -117,15 +133,13 @@ func init() {
|
|||||||
rootCmd.PersistentFlags().StringVarP(&cobraConfig.TlsCertFile, "tls-cert-file", "c", "", "")
|
rootCmd.PersistentFlags().StringVarP(&cobraConfig.TlsCertFile, "tls-cert-file", "c", "", "")
|
||||||
rootCmd.PersistentFlags().StringVarP(&cobraConfig.TlsKeyFile, "tls-key-file", "k", "", "")
|
rootCmd.PersistentFlags().StringVarP(&cobraConfig.TlsKeyFile, "tls-key-file", "k", "", "")
|
||||||
rootCmd.PersistentFlags().StringVarP(&cobraConfig.AuthSecret, "auth-secret", "s", "", "auth secret")
|
rootCmd.PersistentFlags().StringVarP(&cobraConfig.AuthSecret, "auth-secret", "s", "", "auth secret")
|
||||||
rootCmd.PersistentFlags().StringVar(&cobraConfig.LogLevel, "log-level", "info", "log level")
|
rootCmd.PersistentFlags().StringVar(&cobraConfig.LogLevel, "log-level", cobraConfig.LogLevel, "log level")
|
||||||
rootCmd.PersistentFlags().StringVar(&cobraConfig.LogFile, "log-file", "console", "log file")
|
rootCmd.PersistentFlags().StringVar(&cobraConfig.LogFile, "log-file", cobraConfig.LogFile, "log file")
|
||||||
rootCmd.PersistentFlags().StringVarP(&cobraConfig.HealthcheckListenAddress, "health-listen-address", "H", ":9000", "listen address of healthcheck server")
|
rootCmd.PersistentFlags().StringVarP(&cobraConfig.HealthcheckListenAddress, "health-listen-address", "H", cobraConfig.HealthcheckListenAddress, "listen address of healthcheck server")
|
||||||
rootCmd.PersistentFlags().StringVar(&cobraConfig.TrustedProxies, "trusted-proxies", "", "comma-separated list of upstream proxy CIDRs or IPs whose X-Real-Ip/X-Real-Port headers are trusted; leave empty to always use the direct connection address")
|
rootCmd.PersistentFlags().StringVar(&cobraConfig.TrustedProxies, "trusted-proxies", "", "comma-separated list of upstream proxy CIDRs or IPs whose X-Real-Ip/X-Real-Port headers are trusted; leave empty to always use the direct connection address")
|
||||||
rootCmd.PersistentFlags().BoolVar(&cobraConfig.EnableSTUN, "enable-stun", false, "enable embedded STUN server")
|
rootCmd.PersistentFlags().BoolVar(&cobraConfig.EnableSTUN, "enable-stun", false, "enable embedded STUN server")
|
||||||
rootCmd.PersistentFlags().IntSliceVar(&cobraConfig.STUNPorts, "stun-ports", []int{3478}, "ports for the embedded STUN server (can be specified multiple times or comma-separated)")
|
rootCmd.PersistentFlags().IntSliceVar(&cobraConfig.STUNPorts, "stun-ports", cobraConfig.STUNPorts, "ports for the embedded STUN server (can be specified multiple times or comma-separated)")
|
||||||
rootCmd.PersistentFlags().StringVar(&cobraConfig.STUNLogLevel, "stun-log-level", "info", "log level for STUN server (panic, fatal, error, warn, info, debug, trace)")
|
rootCmd.PersistentFlags().StringVar(&cobraConfig.STUNLogLevel, "stun-log-level", cobraConfig.STUNLogLevel, "log level for STUN server (panic, fatal, error, warn, info, debug, trace)")
|
||||||
|
|
||||||
setFlagsFromEnvVars(rootCmd)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func Execute() error {
|
func Execute() error {
|
||||||
@@ -139,8 +153,14 @@ func waitForExitSignal() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func execute(cmd *cobra.Command, args []string) error {
|
func execute(cmd *cobra.Command, args []string) error {
|
||||||
|
loadedConfig, err := loadConfig(cmd)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("load config: %w", err)
|
||||||
|
}
|
||||||
|
cobraConfig = loadedConfig
|
||||||
|
|
||||||
wg := sync.WaitGroup{}
|
wg := sync.WaitGroup{}
|
||||||
err := cobraConfig.Validate()
|
err = cobraConfig.Validate()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Debugf("invalid config: %s", err)
|
log.Debugf("invalid config: %s", err)
|
||||||
return fmt.Errorf("invalid config: %s", err)
|
return fmt.Errorf("invalid config: %s", err)
|
||||||
@@ -228,6 +248,15 @@ func execute(cmd *cobra.Command, args []string) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func loadConfig(cmd *cobra.Command) (*Config, error) {
|
||||||
|
return configloader.Load(configPath, defaultConfig(), configloader.Options{
|
||||||
|
TagName: "yaml",
|
||||||
|
AllowMissing: configPath == "",
|
||||||
|
FlagSet: cmd.Flags(),
|
||||||
|
Strict: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func startServers(wg *sync.WaitGroup, metricsServer *metrics.Metrics, srv *server.Server, srvListenerCfg server.ListenerConfig, httpHealthcheck *healthcheck.Server, stunServer *stun.Server) {
|
func startServers(wg *sync.WaitGroup, metricsServer *metrics.Metrics, srv *server.Server, srvListenerCfg server.ListenerConfig, httpHealthcheck *healthcheck.Server, stunServer *stun.Server) {
|
||||||
wg.Add(1)
|
wg.Add(1)
|
||||||
go func() {
|
go func() {
|
||||||
|
|||||||
@@ -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,
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -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")
|
||||||
|
}
|
||||||
@@ -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
@@ -4,7 +4,6 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
"runtime"
|
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
|
|
||||||
@@ -17,9 +16,8 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
logLevel string
|
logLevel string
|
||||||
defaultLogFile string
|
logFile string
|
||||||
logFile string
|
|
||||||
|
|
||||||
rootCmd = &cobra.Command{
|
rootCmd = &cobra.Command{
|
||||||
Use: "netbird-signal",
|
Use: "netbird-signal",
|
||||||
@@ -39,14 +37,9 @@ func Execute() error {
|
|||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
stopCh = make(chan int)
|
stopCh = make(chan int)
|
||||||
defaultLogFile = "/var/log/netbird/signal.log"
|
defaults := defaultConfig()
|
||||||
|
rootCmd.PersistentFlags().StringVar(&logLevel, "log-level", defaults.LogLevel, "")
|
||||||
if runtime.GOOS == "windows" {
|
rootCmd.PersistentFlags().StringVar(&logFile, "log-file", defaults.LogFile, "sets Netbird log path. If console is specified the log will be output to stdout")
|
||||||
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")
|
|
||||||
rootCmd.AddCommand(runCmd)
|
rootCmd.AddCommand(runCmd)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+32
-22
@@ -10,7 +10,6 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
// nolint:gosec
|
// nolint:gosec
|
||||||
_ "net/http/pprof"
|
_ "net/http/pprof"
|
||||||
"os"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc"
|
"go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc"
|
||||||
@@ -46,6 +45,8 @@ var (
|
|||||||
signalLetsencryptDataDir string
|
signalLetsencryptDataDir string
|
||||||
signalCertFile string
|
signalCertFile string
|
||||||
signalCertKey string
|
signalCertKey string
|
||||||
|
signalConfigPath string
|
||||||
|
signalPprofAddress string
|
||||||
|
|
||||||
signalKaep = grpc.KeepaliveEnforcementPolicy(keepalive.EnforcementPolicy{
|
signalKaep = grpc.KeepaliveEnforcementPolicy(keepalive.EnforcementPolicy{
|
||||||
MinTime: 5 * time.Second,
|
MinTime: 5 * time.Second,
|
||||||
@@ -64,30 +65,25 @@ var (
|
|||||||
Short: "start NetBird Signal Server daemon",
|
Short: "start NetBird Signal Server daemon",
|
||||||
SilenceUsage: true,
|
SilenceUsage: true,
|
||||||
PreRunE: func(cmd *cobra.Command, args []string) error {
|
PreRunE: func(cmd *cobra.Command, args []string) error {
|
||||||
err := util.InitLog(logLevel, logFile)
|
cfg, err := loadConfig(cmd, signalConfigPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed initializing log: %w", err)
|
return fmt.Errorf("load config: %w", err)
|
||||||
}
|
}
|
||||||
|
applyConfig(cfg)
|
||||||
|
|
||||||
flag.Parse()
|
if signalPort == 0 {
|
||||||
|
if signalLetsencryptDomain != "" || (signalCertFile != "" && signalCertKey != "") {
|
||||||
// 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 {
|
|
||||||
signalPort = 443
|
signalPort = 443
|
||||||
} else {
|
} else {
|
||||||
signalPort = 80
|
signalPort = 80
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err := util.InitLog(logLevel, logFile); err != nil {
|
||||||
|
return fmt.Errorf("initialize log: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
flag.Parse()
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
@@ -196,10 +192,10 @@ var (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func startPprof() {
|
func startPprof() {
|
||||||
if pprofAddr := os.Getenv("NB_PPROF_ADDR"); pprofAddr != "" {
|
if signalPprofAddress != "" {
|
||||||
log.Infof("pprof enabled, listening on: %s", pprofAddr)
|
log.Infof("pprof enabled, listening on: %s", signalPprofAddress)
|
||||||
go func() {
|
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)
|
log.Fatalf("pprof server failed: %v", err)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
@@ -328,13 +324,27 @@ func loadTLSConfig(certFile string, certKey string) (*tls.Config, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
runCmd.PersistentFlags().IntVar(&signalPort, "port", 80, "Server port to listen on (defaults to 443 if TLS is enabled, 80 otherwise")
|
defaults := defaultConfig()
|
||||||
runCmd.Flags().IntVar(&metricsPort, "metrics-port", 9090, "metrics endpoint http port. Metrics are accessible under host:metrics-port/metrics")
|
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, "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(&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(&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(&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(&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")
|
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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -49,6 +49,16 @@ func Parse(raw string) (*List, error) {
|
|||||||
return &List{prefixes: prefixes}, nil
|
return &List{prefixes: prefixes}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// UnmarshalText parses trusted proxy prefixes from text configuration.
|
||||||
|
func (l *List) UnmarshalText(text []byte) error {
|
||||||
|
parsed, err := Parse(string(text))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
*l = *parsed
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// FromPrefixes wraps an already-parsed set of prefixes in a List.
|
// FromPrefixes wraps an already-parsed set of prefixes in a List.
|
||||||
func FromPrefixes(prefixes []netip.Prefix) *List {
|
func FromPrefixes(prefixes []netip.Prefix) *List {
|
||||||
return &List{prefixes: prefixes}
|
return &List{prefixes: prefixes}
|
||||||
|
|||||||
@@ -0,0 +1,384 @@
|
|||||||
|
// Package config loads service configuration from defaults, files, environment
|
||||||
|
// variables, and command-line flags. Values are applied in that order, so an
|
||||||
|
// explicitly changed flag has the highest precedence.
|
||||||
|
//
|
||||||
|
// Configuration keys come from the struct tag selected by [Options.TagName],
|
||||||
|
// which defaults to "mapstructure". Environment variable names are inferred
|
||||||
|
// from those keys with the NB prefix. For example, server.listen-address maps
|
||||||
|
// to NB_SERVER_LISTEN_ADDRESS. The env and flag tags can provide explicit names,
|
||||||
|
// comma-separated compatibility aliases, or "-" to disable a source.
|
||||||
|
//
|
||||||
|
// A typical service configuration can be loaded as follows:
|
||||||
|
//
|
||||||
|
// type Config struct {
|
||||||
|
// Address string `yaml:"address" env:"NB_ADDRESS" flag:"address"`
|
||||||
|
// Timeout time.Duration `yaml:"timeout"`
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// flags := pflag.NewFlagSet("service", pflag.ContinueOnError)
|
||||||
|
// flags.String("address", ":443", "service listen address")
|
||||||
|
//
|
||||||
|
// cfg, err := config.Load("config.yaml", &Config{
|
||||||
|
// Address: ":443",
|
||||||
|
// Timeout: 30 * time.Second,
|
||||||
|
// }, config.Options{
|
||||||
|
// TagName: "yaml",
|
||||||
|
// FlagSet: flags,
|
||||||
|
// Strict: true,
|
||||||
|
// })
|
||||||
|
//
|
||||||
|
// Set [Options.AllowMissing] when the service must start without a configuration
|
||||||
|
// file. [Options.Transform] can preprocess file contents before decoding, such
|
||||||
|
// as with [ExpandEnvTemplate].
|
||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io/fs"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"reflect"
|
||||||
|
"slices"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/go-viper/mapstructure/v2"
|
||||||
|
"github.com/spf13/pflag"
|
||||||
|
"github.com/spf13/viper"
|
||||||
|
)
|
||||||
|
|
||||||
|
const envPrefix = "NB"
|
||||||
|
|
||||||
|
var (
|
||||||
|
textUnmarshalerType = reflect.TypeFor[encoding.TextUnmarshaler]()
|
||||||
|
jsonUnmarshalerType = reflect.TypeFor[json.Unmarshaler]()
|
||||||
|
)
|
||||||
|
|
||||||
|
// Options controls how Load resolves files and fields.
|
||||||
|
type Options struct {
|
||||||
|
// TagName selects the struct tag used for configuration keys.
|
||||||
|
TagName string
|
||||||
|
// AllowMissing permits an empty path or a file that does not exist.
|
||||||
|
AllowMissing bool
|
||||||
|
// FlagSet provides command-line flags referenced by `flag` struct tags.
|
||||||
|
FlagSet *pflag.FlagSet
|
||||||
|
// Transform rewrites configuration file contents before decoding.
|
||||||
|
Transform func([]byte) ([]byte, error)
|
||||||
|
// Strict rejects configuration keys that are not represented by the target type.
|
||||||
|
Strict bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load reads configuration into a default-initialized value. Environment values
|
||||||
|
// override file values, and file values override defaults.
|
||||||
|
func Load[T any](configPath string, cfg *T, options Options) (*T, error) {
|
||||||
|
if cfg == nil {
|
||||||
|
return nil, fmt.Errorf("default config is nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
configType := reflect.TypeFor[T]()
|
||||||
|
if configType.Kind() != reflect.Struct {
|
||||||
|
return nil, fmt.Errorf("config type %s must be a struct", configType)
|
||||||
|
}
|
||||||
|
|
||||||
|
if configPath == "" && !options.AllowMissing {
|
||||||
|
return nil, errors.New("config file path is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
tagName := options.TagName
|
||||||
|
if tagName == "" {
|
||||||
|
tagName = "mapstructure"
|
||||||
|
}
|
||||||
|
|
||||||
|
configData, err := readConfigFile(configPath, options.AllowMissing)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
configFormat := ""
|
||||||
|
if configData != nil {
|
||||||
|
configFormat, err = resolveConfigType(configPath, tagName)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if options.Transform != nil {
|
||||||
|
configData, err = options.Transform(configData)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("transform config: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
v := viper.New()
|
||||||
|
if configFormat != "" {
|
||||||
|
v.SetConfigType(configFormat)
|
||||||
|
}
|
||||||
|
v.SetEnvPrefix(envPrefix)
|
||||||
|
v.SetEnvKeyReplacer(strings.NewReplacer(".", "_", "-", "_"))
|
||||||
|
v.AllowEmptyEnv(true)
|
||||||
|
v.AutomaticEnv()
|
||||||
|
|
||||||
|
if err := bindConfigSources(
|
||||||
|
v,
|
||||||
|
configType,
|
||||||
|
"",
|
||||||
|
tagName,
|
||||||
|
options.FlagSet,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
make(map[reflect.Type]bool),
|
||||||
|
); err != nil {
|
||||||
|
return nil, fmt.Errorf("bind config sources: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if configData != nil {
|
||||||
|
if err := v.ReadConfig(bytes.NewReader(configData)); err != nil {
|
||||||
|
return nil, fmt.Errorf("read config: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var unmarshalErr error
|
||||||
|
if options.Strict {
|
||||||
|
unmarshalErr = v.UnmarshalExact(cfg, decoderConfig(tagName))
|
||||||
|
} else {
|
||||||
|
unmarshalErr = v.Unmarshal(cfg, decoderConfig(tagName))
|
||||||
|
}
|
||||||
|
if unmarshalErr != nil {
|
||||||
|
return nil, fmt.Errorf("unmarshal config: %w", unmarshalErr)
|
||||||
|
}
|
||||||
|
return cfg, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func readConfigFile(configPath string, allowMissing bool) ([]byte, error) {
|
||||||
|
if configPath == "" {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := os.ReadFile(configPath)
|
||||||
|
if err == nil {
|
||||||
|
return data, nil
|
||||||
|
}
|
||||||
|
if allowMissing && errors.Is(err, fs.ErrNotExist) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("read config file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func resolveConfigType(configPath, fallbackType string) (string, error) {
|
||||||
|
extension := strings.TrimPrefix(strings.ToLower(filepath.Ext(configPath)), ".")
|
||||||
|
if slices.Contains(viper.SupportedExts, extension) {
|
||||||
|
return extension, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
fallbackType = strings.TrimPrefix(strings.ToLower(strings.TrimSpace(fallbackType)), ".")
|
||||||
|
if fallbackType != "" {
|
||||||
|
if !slices.Contains(viper.SupportedExts, fallbackType) {
|
||||||
|
return "", fmt.Errorf("unsupported default config type %q", fallbackType)
|
||||||
|
}
|
||||||
|
return fallbackType, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if extension == "" {
|
||||||
|
return "", errors.New("config file extension is required")
|
||||||
|
}
|
||||||
|
return "", fmt.Errorf("unsupported config file extension %q", extension)
|
||||||
|
}
|
||||||
|
|
||||||
|
func decoderConfig(tagName string) viper.DecoderConfigOption {
|
||||||
|
return func(config *mapstructure.DecoderConfig) {
|
||||||
|
config.TagName = tagName
|
||||||
|
config.DecodeHook = mapstructure.ComposeDecodeHookFunc(
|
||||||
|
decodeLegacyBoolean,
|
||||||
|
mapstructure.TextUnmarshallerHookFunc(),
|
||||||
|
jsonUnmarshallerHook,
|
||||||
|
config.DecodeHook,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func bindConfigSources(
|
||||||
|
v *viper.Viper,
|
||||||
|
configType reflect.Type,
|
||||||
|
prefix string,
|
||||||
|
tagName string,
|
||||||
|
flagSet *pflag.FlagSet,
|
||||||
|
bindEnvironment bool,
|
||||||
|
bindFlags bool,
|
||||||
|
visiting map[reflect.Type]bool,
|
||||||
|
) error {
|
||||||
|
for configType.Kind() == reflect.Pointer {
|
||||||
|
configType = configType.Elem()
|
||||||
|
}
|
||||||
|
visiting[configType] = true
|
||||||
|
defer delete(visiting, configType)
|
||||||
|
|
||||||
|
for i := range configType.NumField() {
|
||||||
|
field := configType.Field(i)
|
||||||
|
if !field.IsExported() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
key, inline, skip := configFieldKey(field, tagName)
|
||||||
|
if skip {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if inline {
|
||||||
|
key = prefix
|
||||||
|
} else if prefix != "" {
|
||||||
|
key = prefix + "." + key
|
||||||
|
}
|
||||||
|
|
||||||
|
fieldEnvironment := field.Tag.Get("env")
|
||||||
|
fieldFlags := field.Tag.Get("flag")
|
||||||
|
bindFieldEnvironment := bindEnvironment && fieldEnvironment != "-"
|
||||||
|
bindFieldFlags := bindFlags && fieldFlags != "-"
|
||||||
|
|
||||||
|
fieldType := field.Type
|
||||||
|
for fieldType.Kind() == reflect.Pointer {
|
||||||
|
fieldType = fieldType.Elem()
|
||||||
|
}
|
||||||
|
if fieldType.Kind() == reflect.Struct && !isScalarUnmarshaler(fieldType) {
|
||||||
|
if !visiting[fieldType] {
|
||||||
|
if err := bindConfigSources(
|
||||||
|
v,
|
||||||
|
fieldType,
|
||||||
|
key,
|
||||||
|
tagName,
|
||||||
|
flagSet,
|
||||||
|
bindFieldEnvironment,
|
||||||
|
bindFieldFlags,
|
||||||
|
visiting,
|
||||||
|
); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if key == "" {
|
||||||
|
return fmt.Errorf("empty config key for field %s", field.Name)
|
||||||
|
}
|
||||||
|
if bindFieldEnvironment {
|
||||||
|
if err := bindEnvironmentVariable(v, key, fieldEnvironment); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if bindFieldFlags && flagSet != nil && fieldFlags != "" {
|
||||||
|
flagName, flag, err := selectFlag(flagSet, fieldFlags)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("config field %s: %w", field.Name, err)
|
||||||
|
}
|
||||||
|
if err := v.BindPFlag(key, flag); err != nil {
|
||||||
|
return fmt.Errorf("bind flag %s: %w", flagName, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func configFieldKey(field reflect.StructField, tagName string) (key string, inline, skip bool) {
|
||||||
|
tagParts := strings.Split(field.Tag.Get(tagName), ",")
|
||||||
|
key = tagParts[0]
|
||||||
|
if key == "-" {
|
||||||
|
return "", false, true
|
||||||
|
}
|
||||||
|
if key == "" {
|
||||||
|
key = field.Name
|
||||||
|
}
|
||||||
|
for _, option := range tagParts[1:] {
|
||||||
|
if option == "inline" || option == "squash" {
|
||||||
|
inline = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return key, inline, false
|
||||||
|
}
|
||||||
|
|
||||||
|
func selectFlag(flagSet *pflag.FlagSet, names string) (string, *pflag.Flag, error) {
|
||||||
|
var selected *pflag.Flag
|
||||||
|
selectedName := ""
|
||||||
|
for _, name := range strings.Split(names, ",") {
|
||||||
|
flag := flagSet.Lookup(name)
|
||||||
|
if flag == nil {
|
||||||
|
return "", nil, fmt.Errorf("references unknown flag %q", name)
|
||||||
|
}
|
||||||
|
if selected == nil || flag.Changed {
|
||||||
|
selected = flag
|
||||||
|
selectedName = name
|
||||||
|
}
|
||||||
|
if flag.Changed {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return selectedName, selected, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func bindEnvironmentVariable(v *viper.Viper, key, environmentName string) error {
|
||||||
|
var err error
|
||||||
|
if environmentName == "" {
|
||||||
|
err = v.BindEnv(key)
|
||||||
|
} else {
|
||||||
|
names := strings.Split(environmentName, ",")
|
||||||
|
arguments := append([]string{key}, names...)
|
||||||
|
err = v.BindEnv(arguments...)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("bind environment for %s: %w", key, err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func isScalarUnmarshaler(configType reflect.Type) bool {
|
||||||
|
return implements(configType, textUnmarshalerType) ||
|
||||||
|
implements(configType, jsonUnmarshalerType)
|
||||||
|
}
|
||||||
|
|
||||||
|
func implements(configType, interfaceType reflect.Type) bool {
|
||||||
|
return configType.Implements(interfaceType) ||
|
||||||
|
reflect.PointerTo(configType).Implements(interfaceType)
|
||||||
|
}
|
||||||
|
|
||||||
|
func jsonUnmarshallerHook(from, to reflect.Type, data any) (any, error) {
|
||||||
|
if !implements(to, jsonUnmarshalerType) {
|
||||||
|
return data, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
raw, err := json.Marshal(data)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
targetType := to
|
||||||
|
if targetType.Kind() == reflect.Pointer {
|
||||||
|
targetType = targetType.Elem()
|
||||||
|
}
|
||||||
|
target := reflect.New(targetType)
|
||||||
|
unmarshaler, ok := target.Interface().(json.Unmarshaler)
|
||||||
|
if !ok {
|
||||||
|
return data, nil
|
||||||
|
}
|
||||||
|
if err := unmarshaler.UnmarshalJSON(raw); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if to.Kind() == reflect.Pointer {
|
||||||
|
return target.Interface(), nil
|
||||||
|
}
|
||||||
|
return target.Elem().Interface(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func decodeLegacyBoolean(from, to reflect.Kind, data any) (any, error) {
|
||||||
|
if from != reflect.String || to != reflect.Bool {
|
||||||
|
return data, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
switch strings.ToLower(data.(string)) {
|
||||||
|
case "y", "yes", "on":
|
||||||
|
return true, nil
|
||||||
|
case "n", "no", "off":
|
||||||
|
return false, nil
|
||||||
|
default:
|
||||||
|
return data, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
"text/template"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ExpandEnvTemplate substitutes Go-template references with environment values.
|
||||||
|
func ExpandEnvTemplate(data []byte) ([]byte, error) {
|
||||||
|
tmpl, err := template.New("config").Parse(string(data))
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("parse environment template: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var output bytes.Buffer
|
||||||
|
if err := tmpl.Execute(&output, environmentMap()); err != nil {
|
||||||
|
return nil, fmt.Errorf("execute environment template: %w", err)
|
||||||
|
}
|
||||||
|
return output.Bytes(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func environmentMap() map[string]string {
|
||||||
|
environment := make(map[string]string)
|
||||||
|
for _, entry := range os.Environ() {
|
||||||
|
key, value, ok := strings.Cut(entry, "=")
|
||||||
|
if ok {
|
||||||
|
environment[key] = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return environment
|
||||||
|
}
|
||||||
+5
-29
@@ -1,7 +1,6 @@
|
|||||||
package util
|
package util
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
@@ -10,10 +9,10 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
|
||||||
"text/template"
|
|
||||||
|
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
|
|
||||||
|
configloader "github.com/netbirdio/netbird/util/config"
|
||||||
)
|
)
|
||||||
|
|
||||||
func WriteBytesWithRestrictedPermission(ctx context.Context, file string, bs []byte) error {
|
func WriteBytesWithRestrictedPermission(ctx context.Context, file string, bs []byte) error {
|
||||||
@@ -233,8 +232,6 @@ func ListFiles(dir, pattern string) ([]string, error) {
|
|||||||
|
|
||||||
// ReadJsonWithEnvSub reads JSON config file and maps to a provided interface with environment variable substitution
|
// ReadJsonWithEnvSub reads JSON config file and maps to a provided interface with environment variable substitution
|
||||||
func ReadJsonWithEnvSub(file string, res interface{}) (interface{}, error) {
|
func ReadJsonWithEnvSub(file string, res interface{}) (interface{}, error) {
|
||||||
envVars := getEnvMap()
|
|
||||||
|
|
||||||
f, err := os.Open(file)
|
f, err := os.Open(file)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -246,19 +243,12 @@ func ReadJsonWithEnvSub(file string, res interface{}) (interface{}, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
t, err := template.New("").Parse(string(bs))
|
output, err := configloader.ExpandEnvTemplate(bs)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("error parsing template: %v", err)
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
var output bytes.Buffer
|
err = json.Unmarshal(output, &res)
|
||||||
// Execute the template, substituting environment variables
|
|
||||||
err = t.Execute(&output, envVars)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("error executing template: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
err = json.Unmarshal(output.Bytes(), &res)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed parsing Json file after template was executed, err: %v", err)
|
return nil, fmt.Errorf("failed parsing Json file after template was executed, err: %v", err)
|
||||||
}
|
}
|
||||||
@@ -266,20 +256,6 @@ func ReadJsonWithEnvSub(file string, res interface{}) (interface{}, error) {
|
|||||||
return res, nil
|
return res, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// getEnvMap Convert the output of os.Environ() to a map
|
|
||||||
func getEnvMap() map[string]string {
|
|
||||||
envMap := make(map[string]string)
|
|
||||||
|
|
||||||
for _, env := range os.Environ() {
|
|
||||||
parts := strings.SplitN(env, "=", 2)
|
|
||||||
if len(parts) == 2 {
|
|
||||||
envMap[parts[0]] = parts[1]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return envMap
|
|
||||||
}
|
|
||||||
|
|
||||||
// CopyFileContents copies contents of the given src file to the dst file
|
// CopyFileContents copies contents of the given src file to the dst file
|
||||||
func CopyFileContents(src, dst string) (err error) {
|
func CopyFileContents(src, dst string) (err error) {
|
||||||
in, err := os.Open(src)
|
in, err := os.Open(src)
|
||||||
|
|||||||
Reference in New Issue
Block a user