diff --git a/tests/e2e/harness/env.go b/tests/e2e/harness/env.go index 649e78d83..31fd893cd 100644 --- a/tests/e2e/harness/env.go +++ b/tests/e2e/harness/env.go @@ -81,6 +81,20 @@ func LoadEnv(t *testing.T) *Env { } wanHost := getenv("SPINIFEX_WAN_IP", "") + if wanHost == "" && mode == ModeSingle { + // Prefer the node's advertised address so multi-bridge shared hosts + // resolve the advertised WAN bridge, not an arbitrary co-tenant bridge + // picked by interface-enumeration order. + wanHost = advertiseIP(configDir) + } + if wanHost == "" { + for _, ip := range nodeIPs { + if !strings.HasPrefix(ip, "127.") { + wanHost = ip + break + } + } + } if wanHost == "" && len(nodeIPs) > 0 { wanHost = nodeIPs[0] } @@ -196,3 +210,54 @@ func awsgwBindIP(configDir string) string { } return "" } + +var ( + tomlNodeLine = regexp.MustCompile(`(?i)^\s*node\s*=\s*"([^"]+)"`) + tomlAdvertiseLine = regexp.MustCompile(`(?i)^\s*advertise\s*=\s*"([^":]+)`) + tomlHostLine = regexp.MustCompile(`(?i)^\s*host\s*=\s*"([^":]+)`) +) + +// advertiseIP returns the externally-reachable address of the local node from +// spinifex.toml (nodes..advertise, or that node's host when advertise is +// unset). The local node is named by the top-level node key. Returns "" when +// the file or node cannot be resolved so callers fall back to interface scan. +func advertiseIP(configDir string) string { + if configDir == "" { + return "" + } + f, err := os.Open(filepath.Join(configDir, "spinifex.toml")) + if err != nil { + return "" + } + defer f.Close() + + var node, advertise, host string + inNode := false + scanner := bufio.NewScanner(f) + for scanner.Scan() { + line := strings.TrimSpace(scanner.Text()) + if strings.HasPrefix(line, "[") { + // Match only the [nodes.] top-level table, not sub-tables + // like [nodes..awsgw]. + inNode = node != "" && line == "[nodes."+node+"]" + continue + } + if !inNode { + if node == "" { + if m := tomlNodeLine.FindStringSubmatch(line); m != nil { + node = m[1] + } + } + continue + } + if m := tomlAdvertiseLine.FindStringSubmatch(line); m != nil { + advertise = m[1] + } else if m := tomlHostLine.FindStringSubmatch(line); m != nil { + host = m[1] + } + } + if advertise != "" { + return advertise + } + return host +} diff --git a/tests/e2e/harness/wanhost_test.go b/tests/e2e/harness/wanhost_test.go new file mode 100644 index 000000000..787aed375 --- /dev/null +++ b/tests/e2e/harness/wanhost_test.go @@ -0,0 +1,76 @@ +//go:build e2e + +package harness + +import ( + "os" + "path/filepath" + "testing" +) + +func TestAdvertiseIP(t *testing.T) { + cases := []struct { + name string + toml string + want string + }{ + { + name: "advertise preferred over host and ignores sub-tables", + toml: `node = "wattle" +[nodes.wattle] +host = "10.105.1.3" +advertise = "192.168.1.13" +[nodes.wattle.awsgw] +host = "10.105.1.3:9999" +`, + want: "192.168.1.13", + }, + { + name: "falls back to host when advertise unset", + toml: `node = "dev1" +[nodes.dev1] +host = "192.168.1.20" +`, + want: "192.168.1.20", + }, + { + name: "resolves only the local node, not co-tenant nodes", + toml: `node = "n2" +[nodes.n1] +host = "10.0.0.1" +advertise = "1.1.1.1" +[nodes.n2] +host = "10.0.0.2" +advertise = "2.2.2.2" +`, + want: "2.2.2.2", + }, + { + name: "empty when top-level node key is absent", + toml: `[nodes.x] +advertise = "9.9.9.9" +`, + want: "", + }, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + dir := t.TempDir() + if err := os.WriteFile(filepath.Join(dir, "spinifex.toml"), []byte(tc.toml), 0o600); err != nil { + t.Fatalf("write toml: %v", err) + } + if got := advertiseIP(dir); got != tc.want { + t.Fatalf("advertiseIP = %q, want %q", got, tc.want) + } + }) + } +} + +func TestAdvertiseIPMissing(t *testing.T) { + if got := advertiseIP(""); got != "" { + t.Fatalf("advertiseIP(\"\") = %q, want empty", got) + } + if got := advertiseIP(t.TempDir()); got != "" { + t.Fatalf("advertiseIP(missing file) = %q, want empty", got) + } +}