feat: enhance relay flag handling to support multiple URLs

yoonhyunwoo committed Nov 17, 2025 at 15:58 UTC 52c3693b69e9644e86682cb660590d04f69e328f
2 files changed +62 -8
README.md
+1 -1
@@ -74,7 +74,7 @@ bin/portal-tunnel expose --config config.yaml
74 2. **Expose a single service directly**
75
76 ```bash
77 -bin/portal-tunnel expose --relay <url> --host localhost --port 8080 --name <service>
77 +bin/portal-tunnel expose --relay <url> [--relay <url> ...] --host localhost --port 8080 --name <service>
78 ```
79
80 ## Glossary
cmd/portal-tunnel/main.go
+61 -7
@@ -19,12 +19,66 @@ import (
19
20 var (
21 flagConfigPath string
22 - flagRelayURL string
22 + flagRelayURLs = newRelayListFlag("ws://localhost:4017/relay")
23 flagHost string
24 flagPort string
25 flagName string
26 )
27
28 +type relayListFlag struct {
29 + values []string
30 + explicit bool
31 +}
32 +
33 +func newRelayListFlag(defaults ...string) *relayListFlag {
34 + list := make([]string, 0, len(defaults))
35 + for _, value := range defaults {
36 + value = strings.TrimSpace(value)
37 + if value == "" {
38 + continue
39 + }
40 + list = append(list, value)
41 + }
42 + return &relayListFlag{values: list}
43 +}
44 +
45 +func (f *relayListFlag) Set(value string) error {
46 + if f == nil {
47 + return fmt.Errorf("relay list flag is not initialized")
48 + }
49 +
50 + if !f.explicit {
51 + f.values = nil
52 + f.explicit = true
53 + }
54 +
55 + parts := strings.Split(value, ",")
56 + for _, part := range parts {
57 + part = strings.TrimSpace(part)
58 + if part == "" {
59 + continue
60 + }
61 + f.values = append(f.values, part)
62 + }
63 + return nil
64 +}
65 +
66 +func (f *relayListFlag) String() string {
67 + if f == nil {
68 + return ""
69 + }
70 + return strings.Join(f.values, ",")
71 +}
72 +
73 +func (f *relayListFlag) Values() []string {
74 + if f == nil || len(f.values) == 0 {
75 + return nil
76 + }
77 + out := make([]string, len(f.values))
78 + copy(out, f.values)
79 + return out
80 +}
81 +
82 type serviceContext struct {
83 Name string
84 LocalAddr string
@@ -41,7 +95,7 @@ func main() {
95 case "expose":
96 fs := flag.NewFlagSet("expose", flag.ExitOnError)
97 fs.StringVar(&flagConfigPath, "config", "", "Path to portal-tunnel config file")
44 - fs.StringVar(&flagRelayURL, "relay", "ws://localhost:4017/relay", "Portal relay server URL when config is not provided")
98 + fs.Var(flagRelayURLs, "relay", "Portal relay server URL. Repeat or comma-separate to supply multiple servers when config is not provided")
99 fs.StringVar(&flagHost, "host", "localhost", "Local host to proxy to when config is not provided")
100 fs.StringVar(&flagPort, "port", "4018", "Local port to proxy to when config is not provided")
101 fs.StringVar(&flagName, "name", "", "Service name when config is not provided (auto-generated if empty)")
@@ -64,7 +118,7 @@ func printTunnelUsage() {
118 fmt.Println()
119 fmt.Println("Usage:")
120 fmt.Println(" portal-tunnel expose --config <file>")
67 - fmt.Println(" portal-tunnel expose [--relay URL] [--host HOST] [--port PORT] [--name NAME]")
121 + fmt.Println(" portal-tunnel expose [--relay URL ...] [--host HOST] [--port PORT] [--name NAME]")
122 }
123
124 func runExpose() error {
@@ -133,9 +187,9 @@ func runExposeWithConfig() error {
187 }
188
189 func runExposeWithFlags() error {
136 - relayURL := strings.TrimSpace(flagRelayURL)
137 - if relayURL == "" {
138 - return fmt.Errorf("--relay is required when --config is not provided")
190 + relayURLs := flagRelayURLs.Values()
191 + if len(relayURLs) == 0 {
192 + return fmt.Errorf("--relay must include at least one non-empty URL when --config is not provided")
193 }
194
195 host := strings.TrimSpace(flagHost)
@@ -158,7 +212,7 @@ func runExposeWithFlags() error {
212 relayDir := NewRelayDirectory([]RelayConfig{
213 {
214 Name: "flags",
161 - URLs: []string{relayURL},
215 + URLs: relayURLs,
216 },
217 })
218