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