Support importing into the cli

This commit is contained in:
Owen
2026-09-15 11:08:47 -04:00
parent acb014e23e
commit 9e29d0b4ff
5 changed files with 146 additions and 98 deletions
+12
View File
@@ -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() {
+9 -5
View File
@@ -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)
+9
View File
@@ -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 {
+90 -63
View File
@@ -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
@@ -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) {