diff --git a/internal/telemetry/metrics.go b/internal/telemetry/metrics.go index 6c34724..70be83b 100644 --- a/internal/telemetry/metrics.go +++ b/internal/telemetry/metrics.go @@ -87,6 +87,18 @@ func attrsWithSite(extra ...attribute.KeyValue) []attribute.KeyValue { return attrs } +// EnsureInstruments registers this package's instruments against whichever +// MeterProvider is globally active (the no-op provider if Init has not been +// called), so that the Inc*/Observe* recording functions below are always +// safe to call. Idempotent and cheap to call repeatedly or before Init - Init +// calls it too, and OTel's global API transparently upgrades instruments +// created this way once a real MeterProvider is later installed via +// otel.SetMeterProvider, so calling it early does not affect metrics +// exported once Init runs. +func EnsureInstruments() error { + return registerInstruments() +} + func registerInstruments() error { var err error initOnce.Do(func() { diff --git a/main.go b/main.go index 38c0583..67190d9 100644 --- a/main.go +++ b/main.go @@ -16,6 +16,7 @@ import ( "github.com/fosrl/newt/internal/telemetry" "github.com/fosrl/newt/logger" newtpkg "github.com/fosrl/newt/newt" + "github.com/fosrl/newt/newtconfig" "github.com/fosrl/newt/updates" "github.com/fosrl/newt/websocket" "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp" @@ -62,7 +63,14 @@ func main() { func runNewtMain(ctx context.Context) { logger.Init(nil) - cfg := loadNewtConfig() + cfg, err := newtconfig.Load(newtconfig.Options{ + Args: os.Args[1:], + Version: newtVersion, + Platform: newtPlatform, + }) + if err != nil { + logger.Fatal("Configuration error: %v", err) + } if cfg.UseNativeMainInterface { if err := permissions.CheckNativeInterfacePermissions(); err != nil { @@ -70,10 +78,6 @@ func runNewtMain(ctx context.Context) { } } - if err := validateTLSConfig(cfg); err != nil { - logger.Fatal("TLS configuration error: %v", err) - } - logger.Debug("Endpoint: %v", cfg.Endpoint) logger.Debug("Log Level: %v", cfg.LogLevel) logger.Debug("Docker Network Validation Enabled: %v", cfg.DockerEnforceNetworkValidation) diff --git a/newt/newt.go b/newt/newt.go index 4710569..4ad2319 100644 --- a/newt/newt.go +++ b/newt/newt.go @@ -13,6 +13,7 @@ import ( wgclients "github.com/fosrl/newt/clients" "github.com/fosrl/newt/docker" "github.com/fosrl/newt/healthcheck" + "github.com/fosrl/newt/internal/telemetry" "github.com/fosrl/newt/logger" "github.com/fosrl/newt/nativessh" "github.com/fosrl/newt/proxy" @@ -82,6 +83,14 @@ type Newt struct { func Init(ctx context.Context, cfg Config) (*Newt, error) { n := &Newt{config: cfg} + // Metric-recording calls throughout the websocket/proxy/tunnel code are + // unconditional, not gated on cfg.MetricsEnabled, so the instruments + // must exist even when the caller never sets up telemetry exporters + // (e.g. an embedder that only calls Init/Start, like the Pangolin CLI). + if err := telemetry.EnsureInstruments(); err != nil { + return nil, fmt.Errorf("failed to initialize telemetry instruments: %w", err) + } + n.loggerLevel = util.ParseLogLevel(cfg.LogLevel) if !cfg.DisableSSH { diff --git a/config.go b/newtconfig/newtconfig.go similarity index 78% rename from config.go rename to newtconfig/newtconfig.go index ab88137..5468f03 100644 --- a/config.go +++ b/newtconfig/newtconfig.go @@ -1,4 +1,10 @@ -package main +// Package newtconfig resolves Newt's runtime configuration (CLI flags, env +// vars, and config file) into a newt.Config. It is the same logic used by +// the newt binary's entrypoint, factored out so other programs (such as the +// Pangolin CLI, which embeds Newt as a library) can load configuration the +// exact same way, from an explicit argument list rather than the process's +// global os.Args/flag.CommandLine. +package newtconfig import ( "encoding/json" @@ -94,8 +100,9 @@ type fileSettings struct { // resolveConfigFilePath determines the settings/credentials file path using // the same precedence as every other setting: CLI > env > OS default. -// It has to run before flag.Parse (which needs the file-resolved defaults), -// so it scans os.Args directly instead of using the flag package. +// It has to run before the flag set is parsed (which needs the +// file-resolved defaults), so it scans args directly instead of using the +// flag package. func resolveConfigFilePath(args []string) string { for i, a := range args { if a == "--config-file" || a == "-config-file" { @@ -235,14 +242,28 @@ func parseDurationEnvOrFlag(s string, defaultVal time.Duration, label string) ti return d } -// loadNewtConfig resolves configuration with priority cli > env > file > -// default, then returns a populated newtpkg.Config. This function calls -// flag.Parse internally and will exit the process if --version or -// --show-config is passed. -func loadNewtConfig() newtpkg.Config { +// Options controls how Load resolves configuration. +type Options struct { + // Args are the newt command-line arguments, i.e. os.Args[1:] when newt + // is run as its own binary, or whatever arguments were passed to a + // subcommand that embeds newt as a library. + Args []string + // Version and Platform populate the resulting Config's build info and + // are printed by --version. + Version string + Platform string +} + +// Load resolves configuration with priority cli > env > file > default, +// validates it (e.g. TLS flag consistency and referenced file existence), +// and returns a populated newtpkg.Config. This function parses Args with a +// dedicated flag.FlagSet (safe to call more than once per process) and will +// exit the process if --version or --show-config is passed, matching the +// newt binary's own CLI behavior exactly. +func Load(opts Options) (newtpkg.Config, error) { sources := make(map[string]string) - configPath := resolveConfigFilePath(os.Args[1:]) + configPath := resolveConfigFilePath(opts.Args) fileCfg, err := loadFileSettings(configPath) if err != nil { logger.Fatal("Failed to load config file: %v", err) @@ -250,8 +271,8 @@ func loadNewtConfig() newtpkg.Config { // ---- defaults ---- cfg := newtpkg.Config{ - Version: newtVersion, - Platform: newtPlatform, + Version: opts.Version, + Platform: opts.Platform, DNS: "9.9.9.9", LogLevel: "INFO", @@ -438,60 +459,62 @@ func loadNewtConfig() newtpkg.Config { origADKey, origADPrincipals, origADCACert := cfg.AuthDaemonKey, cfg.AuthDaemonPrincipalsFile, cfg.AuthDaemonCACertPath origADRandomPass := cfg.AuthDaemonGenerateRandomPassword - flag.StringVar(&cfg.Endpoint, "endpoint", cfg.Endpoint, "Endpoint of your pangolin server") - flag.StringVar(&cfg.ID, "id", cfg.ID, "Newt ID") - flag.StringVar(&cfg.Secret, "secret", cfg.Secret, "Newt secret") - flag.StringVar(&mtuStr, "mtu", mtuStr, "MTU to use") - flag.StringVar(&cfg.DNS, "dns", cfg.DNS, "DNS server to use") - flag.StringVar(&cfg.LogLevel, "log-level", cfg.LogLevel, "Log level (DEBUG, INFO, WARN, ERROR, FATAL)") - flag.StringVar(&cfg.UpdownScript, "updown", cfg.UpdownScript, "Path to updown script to be called when targets are added or removed") - flag.StringVar(&cfg.InterfaceName, "interface", cfg.InterfaceName, "Name of the WireGuard interface") - flag.StringVar(&portStr, "port", portStr, "Port for client WireGuard interface") - flag.BoolVar(&cfg.UseNativeInterface, "native", cfg.UseNativeInterface, "Use native WireGuard interface for client tunnels") - flag.BoolVar(&cfg.UseNativeMainInterface, "native-main", cfg.UseNativeMainInterface, "Use native WireGuard interface for the main tunnel (instead of netstack)") + fs := flag.NewFlagSet("newt", flag.ExitOnError) + + fs.StringVar(&cfg.Endpoint, "endpoint", cfg.Endpoint, "Endpoint of your pangolin server") + fs.StringVar(&cfg.ID, "id", cfg.ID, "Newt ID") + fs.StringVar(&cfg.Secret, "secret", cfg.Secret, "Newt secret") + fs.StringVar(&mtuStr, "mtu", mtuStr, "MTU to use") + fs.StringVar(&cfg.DNS, "dns", cfg.DNS, "DNS server to use") + fs.StringVar(&cfg.LogLevel, "log-level", cfg.LogLevel, "Log level (DEBUG, INFO, WARN, ERROR, FATAL)") + fs.StringVar(&cfg.UpdownScript, "updown", cfg.UpdownScript, "Path to updown script to be called when targets are added or removed") + fs.StringVar(&cfg.InterfaceName, "interface", cfg.InterfaceName, "Name of the WireGuard interface") + fs.StringVar(&portStr, "port", portStr, "Port for client WireGuard interface") + fs.BoolVar(&cfg.UseNativeInterface, "native", cfg.UseNativeInterface, "Use native WireGuard interface for client tunnels") + fs.BoolVar(&cfg.UseNativeMainInterface, "native-main", cfg.UseNativeMainInterface, "Use native WireGuard interface for the main tunnel (instead of netstack)") // making this the same as above should prevent them from running together - flag.StringVar(&cfg.NativeMainInterfaceName, "interface-main", cfg.NativeMainInterfaceName, "Name of the native main tunnel WireGuard interface (used with --native-main)") - flag.BoolVar(&cfg.DisableClients, "disable-clients", cfg.DisableClients, "Disable clients on the WireGuard interface") - flag.BoolVar(&cfg.DisableSSH, "disable-ssh", cfg.DisableSSH, "Disable SSH auth daemon and native SSH mode (remote auth daemon still works)") - flag.BoolVar(&cfg.EnforceHealthcheckCert, "enforce-hc-cert", cfg.EnforceHealthcheckCert, "Enforce certificate validation for health checks (default: false, accepts any cert)") - flag.StringVar(&cfg.DockerSocket, "docker-socket", cfg.DockerSocket, "Path or address to Docker socket (typically unix:///var/run/docker.sock)") - flag.StringVar(&pingIntervalStr, "ping-interval", pingIntervalStr, "Interval for pinging the server (default 15s)") - flag.StringVar(&pingTimeoutStr, "ping-timeout", pingTimeoutStr, "Timeout for each ping (default 7s)") - flag.StringVar(&udpProxyIdleTimeoutStr, "udp-proxy-idle-timeout", udpProxyIdleTimeoutStr, "Idle timeout for UDP proxied client flows before cleanup") - flag.StringVar(&cfg.PreferEndpoint, "prefer-endpoint", cfg.PreferEndpoint, "Prefer this endpoint for the connection (if set, will override the endpoint from the server)") - flag.StringVar(&localEndpointInterfacesStr, "local-endpoint-interfaces", localEndpointInterfacesStr, "Comma-separated list of network interface names to restrict reported local endpoints to (default: report all interfaces)") - flag.StringVar(&cfg.ProvisioningKey, "provisioning-key", cfg.ProvisioningKey, "One-time provisioning key used to obtain a newt ID and secret from the server") - flag.StringVar(&cfg.NewtName, "name", cfg.NewtName, "Name for the site created during provisioning (supports {{env.VAR}} interpolation)") - flag.StringVar(&cfg.ConfigFile, "config-file", configPath, "Path to config file (overrides CONFIG_FILE env var and default location)") - flag.StringVar(&cfg.TLSClientCert, "tls-client-cert-file", cfg.TLSClientCert, "Path to client certificate file (PEM/DER format)") - flag.StringVar(&cfg.TLSClientKey, "tls-client-key", cfg.TLSClientKey, "Path to client private key file (PEM/DER format)") + fs.StringVar(&cfg.NativeMainInterfaceName, "interface-main", cfg.NativeMainInterfaceName, "Name of the native main tunnel WireGuard interface (used with --native-main)") + fs.BoolVar(&cfg.DisableClients, "disable-clients", cfg.DisableClients, "Disable clients on the WireGuard interface") + fs.BoolVar(&cfg.DisableSSH, "disable-ssh", cfg.DisableSSH, "Disable SSH auth daemon and native SSH mode (remote auth daemon still works)") + fs.BoolVar(&cfg.EnforceHealthcheckCert, "enforce-hc-cert", cfg.EnforceHealthcheckCert, "Enforce certificate validation for health checks (default: false, accepts any cert)") + fs.StringVar(&cfg.DockerSocket, "docker-socket", cfg.DockerSocket, "Path or address to Docker socket (typically unix:///var/run/docker.sock)") + fs.StringVar(&pingIntervalStr, "ping-interval", pingIntervalStr, "Interval for pinging the server (default 15s)") + fs.StringVar(&pingTimeoutStr, "ping-timeout", pingTimeoutStr, "Timeout for each ping (default 7s)") + fs.StringVar(&udpProxyIdleTimeoutStr, "udp-proxy-idle-timeout", udpProxyIdleTimeoutStr, "Idle timeout for UDP proxied client flows before cleanup") + fs.StringVar(&cfg.PreferEndpoint, "prefer-endpoint", cfg.PreferEndpoint, "Prefer this endpoint for the connection (if set, will override the endpoint from the server)") + fs.StringVar(&localEndpointInterfacesStr, "local-endpoint-interfaces", localEndpointInterfacesStr, "Comma-separated list of network interface names to restrict reported local endpoints to (default: report all interfaces)") + fs.StringVar(&cfg.ProvisioningKey, "provisioning-key", cfg.ProvisioningKey, "One-time provisioning key used to obtain a newt ID and secret from the server") + fs.StringVar(&cfg.NewtName, "name", cfg.NewtName, "Name for the site created during provisioning (supports {{env.VAR}} interpolation)") + fs.StringVar(&cfg.ConfigFile, "config-file", configPath, "Path to config file (overrides CONFIG_FILE env var and default location)") + fs.StringVar(&cfg.TLSClientCert, "tls-client-cert-file", cfg.TLSClientCert, "Path to client certificate file (PEM/DER format)") + fs.StringVar(&cfg.TLSClientKey, "tls-client-key", cfg.TLSClientKey, "Path to client private key file (PEM/DER format)") // Backward-compat dummy flag (auth daemon is always enabled now) - flag.Bool("auth-daemon", false, "Enable auth daemon mode (deprecated, always enabled)") + fs.Bool("auth-daemon", false, "Enable auth daemon mode (deprecated, always enabled)") var tlsClientCAsFlag stringSlice - flag.Var(&tlsClientCAsFlag, "tls-client-ca", "Path to CA certificate file for validating remote certificates (can be specified multiple times)") + fs.Var(&tlsClientCAsFlag, "tls-client-ca", "Path to CA certificate file for validating remote certificates (can be specified multiple times)") - flag.StringVar(&cfg.TLSPrivateKey, "tls-client-cert", cfg.TLSPrivateKey, "Path to client certificate (PKCS12 format) - DEPRECATED: use --tls-client-cert-file and --tls-client-key instead") - flag.StringVar(&dockerEnforceStr, "docker-enforce-network-validation", dockerEnforceStr, "Enforce validation of container on newt network (true or false)") - flag.StringVar(&cfg.HealthFile, "health-file", cfg.HealthFile, "Path to health file (if unset, health file won't be written)") - flag.StringVar(&cfg.BlueprintFile, "blueprint-file", cfg.BlueprintFile, "Path to blueprint file (if unset, no blueprint will be applied)") - flag.StringVar(&cfg.ProvisioningBlueprintFile, "provisioning-blueprint-file", cfg.ProvisioningBlueprintFile, "Path to blueprint file applied once after a provisioning credential exchange (if unset, no provisioning blueprint will be applied)") - flag.BoolVar(&cfg.NoCloud, "no-cloud", cfg.NoCloud, "Disable cloud failover") - flag.BoolVar(&cfg.MetricsEnabled, "metrics", cfg.MetricsEnabled, "Enable Prometheus metrics exporter") - flag.BoolVar(&cfg.OTLPEnabled, "otlp", cfg.OTLPEnabled, "Enable OTLP exporters (metrics/traces) to OTEL_EXPORTER_OTLP_ENDPOINT") - flag.StringVar(&cfg.AdminAddr, "metrics-admin-addr", cfg.AdminAddr, "Admin/metrics bind address") - flag.BoolVar(&cfg.MetricsAsyncBytes, "metrics-async-bytes", cfg.MetricsAsyncBytes, "Enable async bytes counting (background flush; lower hot path overhead)") - flag.BoolVar(&cfg.PprofEnabled, "pprof", cfg.PprofEnabled, "Enable pprof debug endpoints on admin server") - flag.StringVar(&cfg.Region, "region", cfg.Region, "Optional region resource attribute (also NEWT_REGION)") - flag.StringVar(&cfg.AuthDaemonKey, "ad-pre-shared-key", cfg.AuthDaemonKey, "Pre-shared key for auth daemon authentication") - flag.StringVar(&cfg.AuthDaemonPrincipalsFile, "ad-principals-file", cfg.AuthDaemonPrincipalsFile, "Path to the principals file for auth daemon") - flag.StringVar(&cfg.AuthDaemonCACertPath, "ad-ca-cert-path", cfg.AuthDaemonCACertPath, "Path to the CA certificate file for auth daemon") - flag.BoolVar(&cfg.AuthDaemonGenerateRandomPassword, "ad-generate-random-password", cfg.AuthDaemonGenerateRandomPassword, "Generate a random password for authenticated users") + fs.StringVar(&cfg.TLSPrivateKey, "tls-client-cert", cfg.TLSPrivateKey, "Path to client certificate (PKCS12 format) - DEPRECATED: use --tls-client-cert-file and --tls-client-key instead") + fs.StringVar(&dockerEnforceStr, "docker-enforce-network-validation", dockerEnforceStr, "Enforce validation of container on newt network (true or false)") + fs.StringVar(&cfg.HealthFile, "health-file", cfg.HealthFile, "Path to health file (if unset, health file won't be written)") + fs.StringVar(&cfg.BlueprintFile, "blueprint-file", cfg.BlueprintFile, "Path to blueprint file (if unset, no blueprint will be applied)") + fs.StringVar(&cfg.ProvisioningBlueprintFile, "provisioning-blueprint-file", cfg.ProvisioningBlueprintFile, "Path to blueprint file applied once after a provisioning credential exchange (if unset, no provisioning blueprint will be applied)") + fs.BoolVar(&cfg.NoCloud, "no-cloud", cfg.NoCloud, "Disable cloud failover") + fs.BoolVar(&cfg.MetricsEnabled, "metrics", cfg.MetricsEnabled, "Enable Prometheus metrics exporter") + fs.BoolVar(&cfg.OTLPEnabled, "otlp", cfg.OTLPEnabled, "Enable OTLP exporters (metrics/traces) to OTEL_EXPORTER_OTLP_ENDPOINT") + fs.StringVar(&cfg.AdminAddr, "metrics-admin-addr", cfg.AdminAddr, "Admin/metrics bind address") + fs.BoolVar(&cfg.MetricsAsyncBytes, "metrics-async-bytes", cfg.MetricsAsyncBytes, "Enable async bytes counting (background flush; lower hot path overhead)") + fs.BoolVar(&cfg.PprofEnabled, "pprof", cfg.PprofEnabled, "Enable pprof debug endpoints on admin server") + fs.StringVar(&cfg.Region, "region", cfg.Region, "Optional region resource attribute (also NEWT_REGION)") + fs.StringVar(&cfg.AuthDaemonKey, "ad-pre-shared-key", cfg.AuthDaemonKey, "Pre-shared key for auth daemon authentication") + fs.StringVar(&cfg.AuthDaemonPrincipalsFile, "ad-principals-file", cfg.AuthDaemonPrincipalsFile, "Path to the principals file for auth daemon") + fs.StringVar(&cfg.AuthDaemonCACertPath, "ad-ca-cert-path", cfg.AuthDaemonCACertPath, "Path to the CA certificate file for auth daemon") + fs.BoolVar(&cfg.AuthDaemonGenerateRandomPassword, "ad-generate-random-password", cfg.AuthDaemonGenerateRandomPassword, "Generate a random password for authenticated users") - version := flag.Bool("version", false, "Print the version") - showConfig := flag.Bool("show-config", false, "Show configuration values and their sources, then exit") + version := fs.Bool("version", false, "Print the version") + showConfig := fs.Bool("show-config", false, "Show configuration values and their sources, then exit") - flag.Parse() + fs.Parse(opts.Args) // ---- post-parse processing ---- @@ -552,7 +575,7 @@ func loadNewtConfig() newtpkg.Config { // Version check (exits process) if *version { - fmt.Println("Newt version " + newtVersion) + fmt.Println("Newt version " + opts.Version) os.Exit(0) } @@ -561,7 +584,7 @@ func loadNewtConfig() newtpkg.Config { os.Exit(0) } - logger.Info("Newt version %s", newtVersion) + logger.Info("Newt version %s", opts.Version) // Parse port if portStr != "" { @@ -573,7 +596,7 @@ func loadNewtConfig() newtpkg.Config { } } - // Parse local endpoint interface allowlist (after flag.Parse so CLI takes effect) + // Parse local endpoint interface allowlist (after flag parsing so CLI takes effect) if localEndpointInterfacesStr != "" { var names []string for _, n := range strings.Split(localEndpointInterfacesStr, ",") { @@ -604,12 +627,16 @@ func loadNewtConfig() newtpkg.Config { cfg.DockerEnforceNetworkValidation = false } - // Parse durations (after flag.Parse so CLI flags take effect) + // Parse durations (after flag parsing so CLI flags take effect) cfg.PingInterval = parseDurationEnvOrFlag(pingIntervalStr, 15*time.Second, "PING_INTERVAL") cfg.PingTimeout = parseDurationEnvOrFlag(pingTimeoutStr, 7*time.Second, "PING_TIMEOUT") cfg.UDPProxyIdleTimeout = parseDurationEnvOrFlag(udpProxyIdleTimeoutStr, 90*time.Second, "NEWT_UDP_PROXY_IDLE_TIMEOUT") - return cfg + if err := validateTLSConfig(cfg); err != nil { + return newtpkg.Config{}, err + } + + return cfg, nil } // printShowConfig prints the resolved configuration and the source of each value diff --git a/config_test.go b/newtconfig/newtconfig_test.go similarity index 78% rename from config_test.go rename to newtconfig/newtconfig_test.go index 13f83a0..b313276 100644 --- a/config_test.go +++ b/newtconfig/newtconfig_test.go @@ -1,25 +1,11 @@ -package main +package newtconfig import ( - "flag" "os" "path/filepath" "testing" ) -// resetFlags allows flag.Parse() to be called again in each test, since -// loadNewtConfig registers flags on the global flag.CommandLine. -func resetFlags(t *testing.T) { - t.Helper() - oldArgs := os.Args - oldCommandLine := flag.CommandLine - t.Cleanup(func() { - os.Args = oldArgs - flag.CommandLine = oldCommandLine - }) - flag.CommandLine = flag.NewFlagSet(os.Args[0], flag.ExitOnError) -} - func clearNewtEnv(t *testing.T) { t.Helper() for _, k := range []string{ @@ -32,11 +18,12 @@ func clearNewtEnv(t *testing.T) { } func TestLoadNewtConfig_Defaults(t *testing.T) { - resetFlags(t) clearNewtEnv(t) - os.Args = []string{"newt", "--config-file", filepath.Join(t.TempDir(), "missing.json")} - cfg := loadNewtConfig() + cfg, err := Load(Options{Args: []string{"--config-file", filepath.Join(t.TempDir(), "missing.json")}}) + if err != nil { + t.Fatalf("Load returned error: %v", err) + } if cfg.DNS != "9.9.9.9" { t.Errorf("expected default dns, got %q", cfg.DNS) @@ -50,16 +37,17 @@ func TestLoadNewtConfig_Defaults(t *testing.T) { } func TestLoadNewtConfig_FileOverridesDefault(t *testing.T) { - resetFlags(t) clearNewtEnv(t) configPath := filepath.Join(t.TempDir(), "config.json") if err := os.WriteFile(configPath, []byte(`{"dns":"1.1.1.1","mtu":1300,"disableSsh":true}`), 0o644); err != nil { t.Fatalf("failed to write config file: %v", err) } - os.Args = []string{"newt", "--config-file", configPath} - cfg := loadNewtConfig() + cfg, err := Load(Options{Args: []string{"--config-file", configPath}}) + if err != nil { + t.Fatalf("Load returned error: %v", err) + } if cfg.DNS != "1.1.1.1" { t.Errorf("expected dns from file, got %q", cfg.DNS) @@ -73,7 +61,6 @@ func TestLoadNewtConfig_FileOverridesDefault(t *testing.T) { } func TestLoadNewtConfig_EnvOverridesFile(t *testing.T) { - resetFlags(t) clearNewtEnv(t) configPath := filepath.Join(t.TempDir(), "config.json") @@ -81,9 +68,11 @@ func TestLoadNewtConfig_EnvOverridesFile(t *testing.T) { t.Fatalf("failed to write config file: %v", err) } t.Setenv("DNS", "8.8.4.4") - os.Args = []string{"newt", "--config-file", configPath} - cfg := loadNewtConfig() + cfg, err := Load(Options{Args: []string{"--config-file", configPath}}) + if err != nil { + t.Fatalf("Load returned error: %v", err) + } if cfg.DNS != "8.8.4.4" { t.Errorf("expected env to override file dns, got %q", cfg.DNS) @@ -91,7 +80,6 @@ func TestLoadNewtConfig_EnvOverridesFile(t *testing.T) { } func TestLoadNewtConfig_CLIOverridesEnv(t *testing.T) { - resetFlags(t) clearNewtEnv(t) configPath := filepath.Join(t.TempDir(), "config.json") @@ -99,9 +87,11 @@ func TestLoadNewtConfig_CLIOverridesEnv(t *testing.T) { t.Fatalf("failed to write config file: %v", err) } t.Setenv("DNS", "8.8.4.4") - os.Args = []string{"newt", "--config-file", configPath, "--dns", "4.2.2.2"} - cfg := loadNewtConfig() + cfg, err := Load(Options{Args: []string{"--config-file", configPath, "--dns", "4.2.2.2"}}) + if err != nil { + t.Fatalf("Load returned error: %v", err) + } if cfg.DNS != "4.2.2.2" { t.Errorf("expected cli to override env dns, got %q", cfg.DNS) @@ -109,22 +99,28 @@ func TestLoadNewtConfig_CLIOverridesEnv(t *testing.T) { } func TestLoadNewtConfig_TLSClientCAMergesAcrossSources(t *testing.T) { - resetFlags(t) clearNewtEnv(t) tmpDir := t.TempDir() caFromFile := filepath.Join(tmpDir, "file-ca.pem") caFromEnv := filepath.Join(tmpDir, "env-ca.pem") caFromCLI := filepath.Join(tmpDir, "cli-ca.pem") + for _, ca := range []string{caFromFile, caFromEnv, caFromCLI} { + if err := os.WriteFile(ca, []byte("test"), 0o644); err != nil { + t.Fatalf("failed to write CA file: %v", err) + } + } configPath := filepath.Join(tmpDir, "config.json") if err := os.WriteFile(configPath, []byte(`{"tlsClientCa":["`+caFromFile+`"]}`), 0o644); err != nil { t.Fatalf("failed to write config file: %v", err) } t.Setenv("TLS_CLIENT_CAS", caFromEnv) - os.Args = []string{"newt", "--config-file", configPath, "--tls-client-ca", caFromCLI} - cfg := loadNewtConfig() + cfg, err := Load(Options{Args: []string{"--config-file", configPath, "--tls-client-ca", caFromCLI}}) + if err != nil { + t.Fatalf("Load returned error: %v", err) + } want := map[string]bool{caFromFile: true, caFromEnv: true, caFromCLI: true} if len(cfg.TLSClientCAs) != len(want) {