Skip to content

Commit 9336e90

Browse files
yroblataskbotCopilot
authored
feat: allow to specify extra args: url, traffic policy, pooled for ngrok (#1345)
Co-authored-by: taskbot <[email protected]> Co-authored-by: Copilot <[email protected]>
1 parent 22394eb commit 9336e90

File tree

4 files changed

+78
-16
lines changed

4 files changed

+78
-16
lines changed

cmd/thv/app/proxy_tunnel.go

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,15 @@ Examples:
3636
3737
Flags:
3838
--tunnel-provider string The provider to use for the tunnel (e.g., "ngrok") - mandatory
39-
--provider-args string JSON object with provider-specific arguments (default "{}")
39+
--provider-args string JSON object with provider-specific arguments: auth-token (mandatory),
40+
url, pooling, traffic-policy-file
41+
--dry-run If set, only validate the configuration without starting the tunnel
42+
43+
Examples:
44+
thv proxy tunnel --tunnel-provider ngrok --provider-args '{"auth-token": "your-token",
45+
"url": "https://example.com", "pooling": true}' http://localhost:8080 my-server
46+
thv proxy tunnel --tunnel-provider ngrok --provider-args '{"auth-token": "your-token",
47+
"traffic-policy-file": "/path/to/policy.yml"}' my-workload my-server
4048
`,
4149
Args: cobra.ExactArgs(2),
4250
RunE: proxyTunnelCmdFunc,

docs/cli/thv_proxy_tunnel.md

Lines changed: 9 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pkg/transport/tunnel/ngrok/tunnel_provider.go

Lines changed: 55 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,12 @@ package ngrok
44
import (
55
"context"
66
"fmt"
7+
"os"
8+
"path/filepath"
9+
"strings"
710

811
"golang.ngrok.com/ngrok/v2"
12+
"gopkg.in/yaml.v2"
913

1014
"github.com/stacklok/toolhive/pkg/logger"
1115
)
@@ -17,31 +21,67 @@ type TunnelProvider struct {
1721

1822
// TunnelConfig holds configuration options for the ngrok tunnel provider.
1923
type TunnelConfig struct {
20-
AuthToken string
21-
Domain string // Optional: specify custom domain
22-
DryRun bool
24+
AuthToken string
25+
URL string // Optional: specify custom URL
26+
TrafficPolicy string // Optional: specify traffic policy
27+
PoolingEnabled bool // Optional: enable pooling
28+
DryRun bool
29+
}
30+
31+
// loadTrafficPolicyFile reads a YAML file, ensures it's .yml/.yaml,
32+
// validates its contents, and returns its text.
33+
func loadTrafficPolicyFile(path string) (string, error) {
34+
ext := strings.ToLower(filepath.Ext(path))
35+
if ext != ".yml" && ext != ".yaml" {
36+
return "", fmt.Errorf("traffic policy file must be .yml or .yaml, got %q", ext)
37+
}
38+
39+
cleanPath := filepath.Clean(path)
40+
b, err := os.ReadFile(cleanPath)
41+
if err != nil {
42+
return "", fmt.Errorf("reading traffic policy file: %w", err)
43+
}
44+
45+
var tmp any
46+
if err := yaml.Unmarshal(b, &tmp); err != nil {
47+
return "", fmt.Errorf("invalid YAML in traffic policy file: %w", err)
48+
}
49+
50+
return string(b), nil
2351
}
2452

2553
// ParseConfig parses the configuration for the ngrok tunnel provider from a map.
2654
func (p *TunnelProvider) ParseConfig(raw map[string]any) error {
27-
token, ok := raw["ngrok-auth-token"].(string)
55+
token, ok := raw["auth-token"].(string)
2856
if !ok || token == "" {
29-
return fmt.Errorf("ngrok-auth-token is required")
57+
return fmt.Errorf("auth-token is required")
3058
}
3159

3260
cfg := TunnelConfig{
3361
AuthToken: token,
3462
}
3563

36-
if domain, ok := raw["ngrok-domain"].(string); ok {
37-
cfg.Domain = domain
64+
// optional settings: url, traffic policy, pooling
65+
if url, ok := raw["url"].(string); ok {
66+
cfg.URL = url
67+
}
68+
if path, ok := raw["traffic-policy-file"].(string); ok && path != "" {
69+
policyText, err := loadTrafficPolicyFile(path)
70+
if err != nil {
71+
return err
72+
}
73+
cfg.TrafficPolicy = policyText
74+
}
75+
if pooling, ok := raw["pooling"].(bool); ok {
76+
cfg.PoolingEnabled = pooling
3877
}
3978

4079
p.config = cfg
4180

4281
if dr, ok := raw["dry-run"].(bool); ok {
4382
p.config.DryRun = dr
4483
}
84+
4585
return nil
4686
}
4787

@@ -69,8 +109,14 @@ func (p *TunnelProvider) StartTunnel(ctx context.Context, name, targetURI string
69109
endpointOpts := []ngrok.EndpointOption{
70110
ngrok.WithDescription("tunnel proxy for " + name),
71111
}
72-
if p.config.Domain != "" {
73-
endpointOpts = append(endpointOpts, ngrok.WithURL(p.config.Domain))
112+
if p.config.URL != "" {
113+
endpointOpts = append(endpointOpts, ngrok.WithURL(p.config.URL))
114+
}
115+
if p.config.TrafficPolicy != "" {
116+
endpointOpts = append(endpointOpts, ngrok.WithTrafficPolicy(p.config.TrafficPolicy))
117+
}
118+
if p.config.PoolingEnabled {
119+
endpointOpts = append(endpointOpts, ngrok.WithPoolingEnabled(true))
74120
}
75121

76122
forwarder, err := agent.Forward(ctx,

test/e2e/proxy_tunnel_e2e_test.go

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -111,14 +111,14 @@ var _ = Describe("Proxy Tunnel E2E", Serial, func() {
111111
"definitely-not-a-workload",
112112
proxyServerName,
113113
"--tunnel-provider", "ngrok",
114-
`--provider-args`, `{"ngrok-auth-token":"dummy","dry-run":true}`,
114+
`--provider-args`, `{"auth-token":"dummy","dry-run":true}`,
115115
).ExpectFailure()
116116

117117
// The exact text may vary a bit; cover both likely messages.
118118
Expect(stderr).To(MatchRegexp(`failed to get workload|workload .* has empty URL`))
119119
})
120120

121-
It("fails when ngrok args are incorrect (missing ngrok-auth-token)", func() {
121+
It("fails when ngrok args are incorrect (missing auth-token)", func() {
122122
osvServerURL, err := e2e.GetMCPServerURL(config, osvServerName)
123123
Expect(err).ToNot(HaveOccurred())
124124
base := mustBaseURL(osvServerURL)
@@ -133,7 +133,7 @@ var _ = Describe("Proxy Tunnel E2E", Serial, func() {
133133
).ExpectFailure()
134134

135135
// ParseConfig should surface this
136-
Expect(stderr).To(MatchRegexp(`invalid provider config:.*ngrok-auth-token is required`))
136+
Expect(stderr).To(MatchRegexp(`invalid provider config:.*auth-token is required`))
137137
})
138138
})
139139

@@ -144,7 +144,7 @@ var _ = Describe("Proxy Tunnel E2E", Serial, func() {
144144
base := mustBaseURL(osvServerURL)
145145

146146
// Use dry-run to skip real network calls
147-
argsJSON := `{"ngrok-auth-token":"dummy-token","dry-run":true}`
147+
argsJSON := `{"auth-token":"dummy-token","dry-run":true}`
148148

149149
By("Starting the proxy tunnel (URL target, dry-run ngrok)")
150150
proxyTunnelCmd = startProxyTunnel(config, proxyServerName, base, "ngrok", argsJSON)
@@ -165,7 +165,7 @@ var _ = Describe("Proxy Tunnel E2E", Serial, func() {
165165
})
166166

167167
It("starts a tunnel when target is a workload name", func() {
168-
argsJSON := `{"ngrok-auth-token":"dummy-token","dry-run":true}`
168+
argsJSON := `{"auth-token":"dummy-token","dry-run":true}`
169169

170170
By("Starting the proxy tunnel (workload target, dry-run ngrok)")
171171
proxyTunnelCmd = startProxyTunnel(config, proxyServerName, osvServerName, "ngrok", argsJSON)

0 commit comments

Comments
 (0)