detached f7cd760

fix(traffic): clean up entrypoint semantics

commit
f7cd760b3d854089414e67550adac60aee616c64
author
brice · · 2026-06-30 14:27 UTC
parent
d88f58b
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
8 files · +111 -29
M docs/config/reference.md +2 -2
@@ -230,8 +230,8 @@ keep_last = 3 # Keep N newest tags per repository
230230 | `server.registry_allowed_ips` | `[]` | IPs or CIDR ranges allowed to access the registry (empty = allow all) |
231231 | `server.proxy_allowed_ips` | `[]` | IPs or CIDR ranges allowed to reach the proxy (empty = allow all) |
232232 | `server.registry_listen_address` | `""` | Bind address for registry (empty = all interfaces) |
233-| `entrypoints.<name>.address` | none | Deployment-selected listen address; `edge` is conventional but has no built-in port |
234-| `entrypoints.<name>.protocol` | none | Entrypoint protocol: `smart_tcp`, `http`, `tls_mux`, `tcp`, or `udp` |
233+| `entrypoints.<name>.address` | none | Deployment-selected listen address; `edge` is conventional for route-capable entrypoints but is not required when exactly one `smart_tcp` or `tls_mux` entrypoint exists |
234+| `entrypoints.<name>.protocol` | none | Entrypoint protocol: `smart_tcp`, `tls_mux`, `tcp`, or `udp` |
235235 | `entrypoints.<name>.trusted_cidrs` | `[]` | Peer socket IP allowlist for all traffic on the entrypoint |
236236 | `entrypoints.<name>.raw_fallback` | `""` | TCP router used by smart TCP for unknown non-HTTP/non-TLS bytes |
237237 | `entrypoints.<name>.raw_fallback_trusted_cidrs` | `[]` | Peer socket IP allowlist for smart TCP raw fallback |
@@ -230,8 +230,8 @@ keep_last = 3 # Keep N newest tags per repository
230 | `server.registry_allowed_ips` | `[]` | IPs or CIDR ranges allowed to access the registry (empty = allow all) |230 | `server.registry_allowed_ips` | `[]` | IPs or CIDR ranges allowed to access the registry (empty = allow all) |
231 | `server.proxy_allowed_ips` | `[]` | IPs or CIDR ranges allowed to reach the proxy (empty = allow all) |231 | `server.proxy_allowed_ips` | `[]` | IPs or CIDR ranges allowed to reach the proxy (empty = allow all) |
232 | `server.registry_listen_address` | `""` | Bind address for registry (empty = all interfaces) |232 | `server.registry_listen_address` | `""` | Bind address for registry (empty = all interfaces) |
233-| `entrypoints.<name>.address` | none | Deployment-selected listen address; `edge` is conventional but has no built-in port |233+| `entrypoints.<name>.address` | none | Deployment-selected listen address; `edge` is conventional for route-capable entrypoints but is not required when exactly one `smart_tcp` or `tls_mux` entrypoint exists |
234-| `entrypoints.<name>.protocol` | none | Entrypoint protocol: `smart_tcp`, `http`, `tls_mux`, `tcp`, or `udp` |234+| `entrypoints.<name>.protocol` | none | Entrypoint protocol: `smart_tcp`, `tls_mux`, `tcp`, or `udp` |
235 | `entrypoints.<name>.trusted_cidrs` | `[]` | Peer socket IP allowlist for all traffic on the entrypoint |235 | `entrypoints.<name>.trusted_cidrs` | `[]` | Peer socket IP allowlist for all traffic on the entrypoint |
236 | `entrypoints.<name>.raw_fallback` | `""` | TCP router used by smart TCP for unknown non-HTTP/non-TLS bytes |236 | `entrypoints.<name>.raw_fallback` | `""` | TCP router used by smart TCP for unknown non-HTTP/non-TLS bytes |
237 | `entrypoints.<name>.raw_fallback_trusted_cidrs` | `[]` | Peer socket IP allowlist for smart TCP raw fallback |237 | `entrypoints.<name>.raw_fallback_trusted_cidrs` | `[]` | Peer socket IP allowlist for smart TCP raw fallback |
M docs/config/traffic.md +2 -2
@@ -4,7 +4,7 @@ Gordon models network listeners as a traffic graph: entrypoints receive packets
44
55 ## Smart TCP Edge Entrypoint
66
7-Public application traffic is normally exposed with a `smart_tcp` entrypoint. The conventional name is `edge`, but Gordon does not assign a built-in public port. Choose the address that matches your deployment, firewall, and container mapping:
7+Public application traffic is normally exposed with a `smart_tcp` entrypoint. The conventional route-capable entrypoint name is `edge`, but Gordon does not require that name or assign a built-in public port. When exactly one route-capable `smart_tcp` or `tls_mux` entrypoint exists, normal Gordon routes use it even if it has a custom name. Choose the address that matches your deployment, firewall, and container mapping:
88
99 ```toml
1010 [entrypoints.edge]
@@ -15,7 +15,7 @@ trusted_cidrs = []
1515
1616 Do not treat `entrypoints.edge.address` as an HTTP port or an HTTPS port. It is one TCP socket that sniffs each new connection and dispatches the original byte stream.
1717
18-Supported entrypoint protocols are `smart_tcp`, `http`, `tls_mux`, `tcp`, and `udp`. `smart_tcp` is the primary public edge model; `tcp` and `udp` are for explicit L4 services, and UDP remains separate from the TCP entrypoint.
18+Supported entrypoint protocols are `smart_tcp`, `tls_mux`, `tcp`, and `udp`. `smart_tcp` is the primary public edge model; `tls_mux` can also serve normal Gordon routes through TLS fallback, `tcp` and `udp` are for explicit L4 services, and UDP remains separate from the TCP entrypoint.
1919
2020 ## Smart TCP Dispatch Order
2121
@@ -4,7 +4,7 @@ Gordon models network listeners as a traffic graph: entrypoints receive packets
4 4
5 ## Smart TCP Edge Entrypoint5 ## Smart TCP Edge Entrypoint
6 6
7-Public application traffic is normally exposed with a `smart_tcp` entrypoint. The conventional name is `edge`, but Gordon does not assign a built-in public port. Choose the address that matches your deployment, firewall, and container mapping:7+Public application traffic is normally exposed with a `smart_tcp` entrypoint. The conventional route-capable entrypoint name is `edge`, but Gordon does not require that name or assign a built-in public port. When exactly one route-capable `smart_tcp` or `tls_mux` entrypoint exists, normal Gordon routes use it even if it has a custom name. Choose the address that matches your deployment, firewall, and container mapping:
8 8
9 ```toml9 ```toml
10 [entrypoints.edge]10 [entrypoints.edge]
@@ -15,7 +15,7 @@ trusted_cidrs = []
15 15
16 Do not treat `entrypoints.edge.address` as an HTTP port or an HTTPS port. It is one TCP socket that sniffs each new connection and dispatches the original byte stream.16 Do not treat `entrypoints.edge.address` as an HTTP port or an HTTPS port. It is one TCP socket that sniffs each new connection and dispatches the original byte stream.
17 17
18-Supported entrypoint protocols are `smart_tcp`, `http`, `tls_mux`, `tcp`, and `udp`. `smart_tcp` is the primary public edge model; `tcp` and `udp` are for explicit L4 services, and UDP remains separate from the TCP entrypoint.18+Supported entrypoint protocols are `smart_tcp`, `tls_mux`, `tcp`, and `udp`. `smart_tcp` is the primary public edge model; `tls_mux` can also serve normal Gordon routes through TLS fallback, `tcp` and `udp` are for explicit L4 services, and UDP remains separate from the TCP entrypoint.
19 19
20 ## Smart TCP Dispatch Order20 ## Smart TCP Dispatch Order
21 21
M gordon.toml.example +2 -0
@@ -18,6 +18,8 @@ gordon_domain = "gordon.example.com"
1818 # max_proxy_response_size = "1GB"
1919 # max_concurrent_connections = 10000
2020
21+# "edge" is the conventional route-capable entrypoint name, but a single
22+# custom smart_tcp or tls_mux entrypoint can also serve normal Gordon routes.
2123 [entrypoints.edge]
2224 address = ":443"
2325 protocol = "smart_tcp"
@@ -18,6 +18,8 @@ gordon_domain = "gordon.example.com"
18 # max_proxy_response_size = "1GB"18 # max_proxy_response_size = "1GB"
19 # max_concurrent_connections = 1000019 # max_concurrent_connections = 10000
20 20
21+# "edge" is the conventional route-capable entrypoint name, but a single
22+# custom smart_tcp or tls_mux entrypoint can also serve normal Gordon routes.
21 [entrypoints.edge]23 [entrypoints.edge]
22 address = ":443"24 address = ":443"
23 protocol = "smart_tcp"25 protocol = "smart_tcp"
M internal/app/traffic_test.go +2 -9
@@ -17,17 +17,14 @@ import (
1717 func TestTrafficRuntimeGraphKeepsSmartTCPHTTPAndTLSPassthroughRouters(t *testing.T) {
1818 graph := domain.TrafficGraph{
1919 EntryPoints: []domain.EntryPoint{
20- {Name: "web", Address: "127.0.0.1:8080", Protocol: domain.EntryPointProtocolHTTP},
2120 {Name: traffic.DefaultEdgeEntryPointName, Address: "127.0.0.1:8443", Protocol: domain.EntryPointProtocolSmartTCP, RawFallback: "ssh", RawFallbackTrustedCIDRs: []string{"127.0.0.0/8"}},
2221 },
2322 Routers: []domain.TrafficRouter{
24- {Name: "legacy-http", EntryPoint: "web", Protocol: domain.RouterProtocolHTTP, Service: "route:legacy.example.com"},
2523 {Name: "app", EntryPoint: traffic.DefaultEdgeEntryPointName, Protocol: domain.RouterProtocolHTTP, Rule: domain.TrafficRule{Host: "app.example.com"}, Service: "route:app.example.com"},
2624 {Name: "tls-raw", EntryPoint: traffic.DefaultEdgeEntryPointName, Protocol: domain.RouterProtocolTLSPassthrough, Rule: domain.TrafficRule{SNI: "raw.example.com"}, Service: "network_service:raw:tls"},
2725 {Name: "ssh", EntryPoint: traffic.DefaultEdgeEntryPointName, Protocol: domain.RouterProtocolTCP, Service: "network_service:ssh:ssh"},
2826 },
2927 Services: []domain.TrafficService{
30- {Name: "route:legacy.example.com"},
3128 {Name: "route:app.example.com"},
3229 {Name: "network_service:raw:tls", Backends: []domain.TrafficBackend{{Name: "raw:tls", Host: "127.0.0.1", Port: 443, Protocol: domain.NetworkProtocolTCP}}},
3330 {Name: "network_service:ssh:ssh", Backends: []domain.TrafficBackend{{Name: "ssh:ssh", Host: "127.0.0.1", Port: 22, Protocol: domain.NetworkProtocolTCP}}},
@@ -41,27 +38,23 @@ func TestTrafficRuntimeGraphKeepsSmartTCPHTTPAndTLSPassthroughRouters(t *testing
4138 assert.ElementsMatch(t, []string{"route:app.example.com", "network_service:raw:tls", "network_service:ssh:ssh"}, serviceNames(filtered.Services))
4239 }
4340
44-func TestTrafficRuntimeGraphRejectsHTTPRouterOnlyWhenItTargetsManagerProtocolEntryPointThatIsNotOwned(t *testing.T) {
41+func TestTrafficRuntimeGraphHTTPEntrypointIsInvalidRatherThanSilentlyFiltered(t *testing.T) {
4542 graph := domain.TrafficGraph{
4643 EntryPoints: []domain.EntryPoint{{Name: "edge", Address: "127.0.0.1:8443", Protocol: domain.EntryPointProtocolHTTP}},
4744 Routers: []domain.TrafficRouter{{Name: "app", EntryPoint: "edge", Protocol: domain.RouterProtocolHTTP, Service: "route:app.example.com"}},
4845 Services: []domain.TrafficService{{Name: "route:app.example.com"}},
4946 }
5047
51- filtered, err := trafficRuntimeGraph(graph)
52- require.NoError(t, err)
53- assert.Empty(t, filtered.Routers)
48+ require.ErrorContains(t, graph.Validate(), "unsupported traffic entrypoint protocol \"http\"")
5449 }
5550
5651 func TestTrafficRuntimeGraphKeepsDefaultTLSEntrypoint(t *testing.T) {
5752 graph := domain.TrafficGraph{
5853 EntryPoints: []domain.EntryPoint{
59- {Name: "web", Address: "127.0.0.1:8080", Protocol: domain.EntryPointProtocolHTTP},
6054 {Name: "websecure", Address: "127.0.0.1:8443", Protocol: domain.EntryPointProtocolTLSMux},
6155 {Name: "postgres", Address: "127.0.0.1:15432", Protocol: domain.EntryPointProtocolTCP},
6256 },
6357 Routers: []domain.TrafficRouter{
64- {Name: "app", EntryPoint: "web", Protocol: domain.RouterProtocolHTTP, Service: "route:app.example.com"},
6558 {Name: "app-secure", EntryPoint: "websecure", Protocol: domain.RouterProtocolHTTP, Rule: domain.TrafficRule{Host: "app.example.com"}, Service: "route:app.example.com"},
6659 {Name: "postgres", EntryPoint: "postgres", Protocol: domain.RouterProtocolTCP, Service: "network_service:postgres:db"},
6760 },
@@ -17,17 +17,14 @@ import (
17 func TestTrafficRuntimeGraphKeepsSmartTCPHTTPAndTLSPassthroughRouters(t *testing.T) {17 func TestTrafficRuntimeGraphKeepsSmartTCPHTTPAndTLSPassthroughRouters(t *testing.T) {
18 graph := domain.TrafficGraph{18 graph := domain.TrafficGraph{
19 EntryPoints: []domain.EntryPoint{19 EntryPoints: []domain.EntryPoint{
20- {Name: "web", Address: "127.0.0.1:8080", Protocol: domain.EntryPointProtocolHTTP},
21 {Name: traffic.DefaultEdgeEntryPointName, Address: "127.0.0.1:8443", Protocol: domain.EntryPointProtocolSmartTCP, RawFallback: "ssh", RawFallbackTrustedCIDRs: []string{"127.0.0.0/8"}},20 {Name: traffic.DefaultEdgeEntryPointName, Address: "127.0.0.1:8443", Protocol: domain.EntryPointProtocolSmartTCP, RawFallback: "ssh", RawFallbackTrustedCIDRs: []string{"127.0.0.0/8"}},
22 },21 },
23 Routers: []domain.TrafficRouter{22 Routers: []domain.TrafficRouter{
24- {Name: "legacy-http", EntryPoint: "web", Protocol: domain.RouterProtocolHTTP, Service: "route:legacy.example.com"},
25 {Name: "app", EntryPoint: traffic.DefaultEdgeEntryPointName, Protocol: domain.RouterProtocolHTTP, Rule: domain.TrafficRule{Host: "app.example.com"}, Service: "route:app.example.com"},23 {Name: "app", EntryPoint: traffic.DefaultEdgeEntryPointName, Protocol: domain.RouterProtocolHTTP, Rule: domain.TrafficRule{Host: "app.example.com"}, Service: "route:app.example.com"},
26 {Name: "tls-raw", EntryPoint: traffic.DefaultEdgeEntryPointName, Protocol: domain.RouterProtocolTLSPassthrough, Rule: domain.TrafficRule{SNI: "raw.example.com"}, Service: "network_service:raw:tls"},24 {Name: "tls-raw", EntryPoint: traffic.DefaultEdgeEntryPointName, Protocol: domain.RouterProtocolTLSPassthrough, Rule: domain.TrafficRule{SNI: "raw.example.com"}, Service: "network_service:raw:tls"},
27 {Name: "ssh", EntryPoint: traffic.DefaultEdgeEntryPointName, Protocol: domain.RouterProtocolTCP, Service: "network_service:ssh:ssh"},25 {Name: "ssh", EntryPoint: traffic.DefaultEdgeEntryPointName, Protocol: domain.RouterProtocolTCP, Service: "network_service:ssh:ssh"},
28 },26 },
29 Services: []domain.TrafficService{27 Services: []domain.TrafficService{
30- {Name: "route:legacy.example.com"},
31 {Name: "route:app.example.com"},28 {Name: "route:app.example.com"},
32 {Name: "network_service:raw:tls", Backends: []domain.TrafficBackend{{Name: "raw:tls", Host: "127.0.0.1", Port: 443, Protocol: domain.NetworkProtocolTCP}}},29 {Name: "network_service:raw:tls", Backends: []domain.TrafficBackend{{Name: "raw:tls", Host: "127.0.0.1", Port: 443, Protocol: domain.NetworkProtocolTCP}}},
33 {Name: "network_service:ssh:ssh", Backends: []domain.TrafficBackend{{Name: "ssh:ssh", Host: "127.0.0.1", Port: 22, Protocol: domain.NetworkProtocolTCP}}},30 {Name: "network_service:ssh:ssh", Backends: []domain.TrafficBackend{{Name: "ssh:ssh", Host: "127.0.0.1", Port: 22, Protocol: domain.NetworkProtocolTCP}}},
@@ -41,27 +38,23 @@ func TestTrafficRuntimeGraphKeepsSmartTCPHTTPAndTLSPassthroughRouters(t *testing
41 assert.ElementsMatch(t, []string{"route:app.example.com", "network_service:raw:tls", "network_service:ssh:ssh"}, serviceNames(filtered.Services))38 assert.ElementsMatch(t, []string{"route:app.example.com", "network_service:raw:tls", "network_service:ssh:ssh"}, serviceNames(filtered.Services))
42 }39 }
43 40
44-func TestTrafficRuntimeGraphRejectsHTTPRouterOnlyWhenItTargetsManagerProtocolEntryPointThatIsNotOwned(t *testing.T) {41+func TestTrafficRuntimeGraphHTTPEntrypointIsInvalidRatherThanSilentlyFiltered(t *testing.T) {
45 graph := domain.TrafficGraph{42 graph := domain.TrafficGraph{
46 EntryPoints: []domain.EntryPoint{{Name: "edge", Address: "127.0.0.1:8443", Protocol: domain.EntryPointProtocolHTTP}},43 EntryPoints: []domain.EntryPoint{{Name: "edge", Address: "127.0.0.1:8443", Protocol: domain.EntryPointProtocolHTTP}},
47 Routers: []domain.TrafficRouter{{Name: "app", EntryPoint: "edge", Protocol: domain.RouterProtocolHTTP, Service: "route:app.example.com"}},44 Routers: []domain.TrafficRouter{{Name: "app", EntryPoint: "edge", Protocol: domain.RouterProtocolHTTP, Service: "route:app.example.com"}},
48 Services: []domain.TrafficService{{Name: "route:app.example.com"}},45 Services: []domain.TrafficService{{Name: "route:app.example.com"}},
49 }46 }
50 47
51- filtered, err := trafficRuntimeGraph(graph)48+ require.ErrorContains(t, graph.Validate(), "unsupported traffic entrypoint protocol \"http\"")
52- require.NoError(t, err)
53- assert.Empty(t, filtered.Routers)
54 }49 }
55 50
56 func TestTrafficRuntimeGraphKeepsDefaultTLSEntrypoint(t *testing.T) {51 func TestTrafficRuntimeGraphKeepsDefaultTLSEntrypoint(t *testing.T) {
57 graph := domain.TrafficGraph{52 graph := domain.TrafficGraph{
58 EntryPoints: []domain.EntryPoint{53 EntryPoints: []domain.EntryPoint{
59- {Name: "web", Address: "127.0.0.1:8080", Protocol: domain.EntryPointProtocolHTTP},
60 {Name: "websecure", Address: "127.0.0.1:8443", Protocol: domain.EntryPointProtocolTLSMux},54 {Name: "websecure", Address: "127.0.0.1:8443", Protocol: domain.EntryPointProtocolTLSMux},
61 {Name: "postgres", Address: "127.0.0.1:15432", Protocol: domain.EntryPointProtocolTCP},55 {Name: "postgres", Address: "127.0.0.1:15432", Protocol: domain.EntryPointProtocolTCP},
62 },56 },
63 Routers: []domain.TrafficRouter{57 Routers: []domain.TrafficRouter{
64- {Name: "app", EntryPoint: "web", Protocol: domain.RouterProtocolHTTP, Service: "route:app.example.com"},
65 {Name: "app-secure", EntryPoint: "websecure", Protocol: domain.RouterProtocolHTTP, Rule: domain.TrafficRule{Host: "app.example.com"}, Service: "route:app.example.com"},58 {Name: "app-secure", EntryPoint: "websecure", Protocol: domain.RouterProtocolHTTP, Rule: domain.TrafficRule{Host: "app.example.com"}, Service: "route:app.example.com"},
66 {Name: "postgres", EntryPoint: "postgres", Protocol: domain.RouterProtocolTCP, Service: "network_service:postgres:db"},59 {Name: "postgres", EntryPoint: "postgres", Protocol: domain.RouterProtocolTCP, Service: "network_service:postgres:db"},
67 },60 },
M internal/domain/traffic.go +2 -2
@@ -617,10 +617,10 @@ func validateRawFallbackRouters(entryPoints map[string]EntryPoint, routers map[s
617617
618618 func validateEntryPointProtocol(protocol EntryPointProtocol) error {
619619 switch protocol {
620- case EntryPointProtocolHTTP, EntryPointProtocolTLSMux, EntryPointProtocolSmartTCP, EntryPointProtocolTCP, EntryPointProtocolUDP:
620+ case EntryPointProtocolTLSMux, EntryPointProtocolSmartTCP, EntryPointProtocolTCP, EntryPointProtocolUDP:
621621 return nil
622622 default:
623- return fmt.Errorf("%q", protocol)
623+ return fmt.Errorf("unsupported traffic entrypoint protocol %q", protocol)
624624 }
625625 }
626626
@@ -617,10 +617,10 @@ func validateRawFallbackRouters(entryPoints map[string]EntryPoint, routers map[s
617 617
618 func validateEntryPointProtocol(protocol EntryPointProtocol) error {618 func validateEntryPointProtocol(protocol EntryPointProtocol) error {
619 switch protocol {619 switch protocol {
620- case EntryPointProtocolHTTP, EntryPointProtocolTLSMux, EntryPointProtocolSmartTCP, EntryPointProtocolTCP, EntryPointProtocolUDP:620+ case EntryPointProtocolTLSMux, EntryPointProtocolSmartTCP, EntryPointProtocolTCP, EntryPointProtocolUDP:
621 return nil621 return nil
622 default:622 default:
623- return fmt.Errorf("%q", protocol)623+ return fmt.Errorf("unsupported traffic entrypoint protocol %q", protocol)
624 }624 }
625 }625 }
626 626
M internal/domain/traffic_test.go +13 -11
@@ -102,24 +102,31 @@ func TestTrafficGraphValidateEntryPoints(t *testing.T) {
102102 {
103103 name: "duplicate entrypoint names",
104104 graph: TrafficGraph{EntryPoints: []EntryPoint{
105- {Name: "web", Address: ":80", Protocol: EntryPointProtocolHTTP},
106- {Name: "web", Address: ":81", Protocol: EntryPointProtocolHTTP},
105+ {Name: "web", Address: ":80", Protocol: EntryPointProtocolTLSMux},
106+ {Name: "web", Address: ":81", Protocol: EntryPointProtocolTLSMux},
107107 }},
108108 wantErr: "duplicate entrypoint name",
109109 },
110110 {
111111 name: "invalid entrypoint address",
112112 graph: TrafficGraph{EntryPoints: []EntryPoint{
113- {Name: "web", Address: "not-a-listen-address", Protocol: EntryPointProtocolHTTP},
113+ {Name: "web", Address: "not-a-listen-address", Protocol: EntryPointProtocolTLSMux},
114114 }},
115115 wantErr: "invalid entrypoint address",
116116 },
117+ {
118+ name: "http entrypoint protocol is unsupported",
119+ graph: TrafficGraph{EntryPoints: []EntryPoint{
120+ {Name: "web", Address: ":80", Protocol: EntryPointProtocolHTTP},
121+ }},
122+ wantErr: "unsupported traffic entrypoint protocol \"http\"",
123+ },
117124 {
118125 name: "invalid entrypoint protocol",
119126 graph: TrafficGraph{EntryPoints: []EntryPoint{
120127 {Name: "web", Address: ":80", Protocol: EntryPointProtocol("smtp")},
121128 }},
122- wantErr: "invalid entrypoint protocol",
129+ wantErr: "unsupported traffic entrypoint protocol \"smtp\"",
123130 },
124131 {
125132 name: "duplicate tcp address rejected",
@@ -188,7 +195,6 @@ func TestTrafficGraphValidateRouterEntryPointProtocolCompatibility(t *testing.T)
188195 entryProtocol EntryPointProtocol
189196 wantErr bool
190197 }{
191- {name: "http router accepts http entrypoint", routerProtocol: RouterProtocolHTTP, entryProtocol: EntryPointProtocolHTTP},
192198 {name: "http router accepts tls mux entrypoint", routerProtocol: RouterProtocolHTTP, entryProtocol: EntryPointProtocolTLSMux},
193199 {name: "tls passthrough router accepts tls mux entrypoint", routerProtocol: RouterProtocolTLSPassthrough, entryProtocol: EntryPointProtocolTLSMux},
194200 {name: "tcp router accepts tcp entrypoint", routerProtocol: RouterProtocolTCP, entryProtocol: EntryPointProtocolTCP},
@@ -196,12 +202,9 @@ func TestTrafficGraphValidateRouterEntryPointProtocolCompatibility(t *testing.T)
196202 {name: "http router rejects udp entrypoint", routerProtocol: RouterProtocolHTTP, entryProtocol: EntryPointProtocolUDP, wantErr: true},
197203 {name: "http router rejects tcp entrypoint", routerProtocol: RouterProtocolHTTP, entryProtocol: EntryPointProtocolTCP, wantErr: true},
198204 {name: "udp router rejects tcp entrypoint", routerProtocol: RouterProtocolUDP, entryProtocol: EntryPointProtocolTCP, wantErr: true},
199- {name: "udp router rejects http entrypoint", routerProtocol: RouterProtocolUDP, entryProtocol: EntryPointProtocolHTTP, wantErr: true},
200205 {name: "udp router rejects tls mux entrypoint", routerProtocol: RouterProtocolUDP, entryProtocol: EntryPointProtocolTLSMux, wantErr: true},
201206 {name: "tcp router rejects udp entrypoint", routerProtocol: RouterProtocolTCP, entryProtocol: EntryPointProtocolUDP, wantErr: true},
202- {name: "tcp router rejects http entrypoint", routerProtocol: RouterProtocolTCP, entryProtocol: EntryPointProtocolHTTP, wantErr: true},
203207 {name: "tcp router rejects tls mux entrypoint", routerProtocol: RouterProtocolTCP, entryProtocol: EntryPointProtocolTLSMux, wantErr: true},
204- {name: "tls passthrough router rejects http entrypoint", routerProtocol: RouterProtocolTLSPassthrough, entryProtocol: EntryPointProtocolHTTP, wantErr: true},
205208 {name: "tls passthrough router rejects udp entrypoint", routerProtocol: RouterProtocolTLSPassthrough, entryProtocol: EntryPointProtocolUDP, wantErr: true},
206209 {name: "tls passthrough router rejects tcp entrypoint per tls mux spec", routerProtocol: RouterProtocolTLSPassthrough, entryProtocol: EntryPointProtocolTCP, wantErr: true},
207210 }
@@ -261,7 +264,7 @@ func TestTrafficGraphValidateRouterNames(t *testing.T) {
261264 for _, tt := range tests {
262265 t.Run(tt.name, func(t *testing.T) {
263266 graph := TrafficGraph{
264- EntryPoints: []EntryPoint{{Name: "web", Address: ":80", Protocol: EntryPointProtocolHTTP}},
267+ EntryPoints: []EntryPoint{{Name: "web", Address: ":443", Protocol: EntryPointProtocolTLSMux}},
265268 Routers: tt.routers,
266269 Services: []TrafficService{service},
267270 }
@@ -277,7 +280,6 @@ func TestTrafficGraphValidateDuplicateHTTPHosts(t *testing.T) {
277280 entryProtocol EntryPointProtocol
278281 entryAddress string
279282 }{
280- {name: "http entrypoint", entryProtocol: EntryPointProtocolHTTP, entryAddress: ":80"},
281283 {name: "tls mux entrypoint", entryProtocol: EntryPointProtocolTLSMux, entryAddress: ":443"},
282284 }
283285
@@ -522,7 +524,7 @@ func TestTrafficGraphValidateHTTPServiceRefs(t *testing.T) {
522524 for _, tt := range tests {
523525 t.Run(tt.name, func(t *testing.T) {
524526 graph := TrafficGraph{
525- EntryPoints: []EntryPoint{{Name: "web", Address: ":80", Protocol: EntryPointProtocolHTTP}},
527+ EntryPoints: []EntryPoint{{Name: "web", Address: ":443", Protocol: EntryPointProtocolTLSMux}},
526528 Routers: []TrafficRouter{{Name: "web", EntryPoint: "web", Protocol: RouterProtocolHTTP, Rule: TrafficRule{Host: "app.example.com"}, Service: tt.service}},
527529 Services: []TrafficService{{Name: tt.service, Backends: []TrafficBackend{{Name: "app", Host: "10.0.0.2", Port: 8080, Protocol: NetworkProtocolTCP}}}},
528530 }
@@ -102,24 +102,31 @@ func TestTrafficGraphValidateEntryPoints(t *testing.T) {
102 {102 {
103 name: "duplicate entrypoint names",103 name: "duplicate entrypoint names",
104 graph: TrafficGraph{EntryPoints: []EntryPoint{104 graph: TrafficGraph{EntryPoints: []EntryPoint{
105- {Name: "web", Address: ":80", Protocol: EntryPointProtocolHTTP},105+ {Name: "web", Address: ":80", Protocol: EntryPointProtocolTLSMux},
106- {Name: "web", Address: ":81", Protocol: EntryPointProtocolHTTP},106+ {Name: "web", Address: ":81", Protocol: EntryPointProtocolTLSMux},
107 }},107 }},
108 wantErr: "duplicate entrypoint name",108 wantErr: "duplicate entrypoint name",
109 },109 },
110 {110 {
111 name: "invalid entrypoint address",111 name: "invalid entrypoint address",
112 graph: TrafficGraph{EntryPoints: []EntryPoint{112 graph: TrafficGraph{EntryPoints: []EntryPoint{
113- {Name: "web", Address: "not-a-listen-address", Protocol: EntryPointProtocolHTTP},113+ {Name: "web", Address: "not-a-listen-address", Protocol: EntryPointProtocolTLSMux},
114 }},114 }},
115 wantErr: "invalid entrypoint address",115 wantErr: "invalid entrypoint address",
116 },116 },
117+ {
118+ name: "http entrypoint protocol is unsupported",
119+ graph: TrafficGraph{EntryPoints: []EntryPoint{
120+ {Name: "web", Address: ":80", Protocol: EntryPointProtocolHTTP},
121+ }},
122+ wantErr: "unsupported traffic entrypoint protocol \"http\"",
123+ },
117 {124 {
118 name: "invalid entrypoint protocol",125 name: "invalid entrypoint protocol",
119 graph: TrafficGraph{EntryPoints: []EntryPoint{126 graph: TrafficGraph{EntryPoints: []EntryPoint{
120 {Name: "web", Address: ":80", Protocol: EntryPointProtocol("smtp")},127 {Name: "web", Address: ":80", Protocol: EntryPointProtocol("smtp")},
121 }},128 }},
122- wantErr: "invalid entrypoint protocol",129+ wantErr: "unsupported traffic entrypoint protocol \"smtp\"",
123 },130 },
124 {131 {
125 name: "duplicate tcp address rejected",132 name: "duplicate tcp address rejected",
@@ -188,7 +195,6 @@ func TestTrafficGraphValidateRouterEntryPointProtocolCompatibility(t *testing.T)
188 entryProtocol EntryPointProtocol195 entryProtocol EntryPointProtocol
189 wantErr bool196 wantErr bool
190 }{197 }{
191- {name: "http router accepts http entrypoint", routerProtocol: RouterProtocolHTTP, entryProtocol: EntryPointProtocolHTTP},
192 {name: "http router accepts tls mux entrypoint", routerProtocol: RouterProtocolHTTP, entryProtocol: EntryPointProtocolTLSMux},198 {name: "http router accepts tls mux entrypoint", routerProtocol: RouterProtocolHTTP, entryProtocol: EntryPointProtocolTLSMux},
193 {name: "tls passthrough router accepts tls mux entrypoint", routerProtocol: RouterProtocolTLSPassthrough, entryProtocol: EntryPointProtocolTLSMux},199 {name: "tls passthrough router accepts tls mux entrypoint", routerProtocol: RouterProtocolTLSPassthrough, entryProtocol: EntryPointProtocolTLSMux},
194 {name: "tcp router accepts tcp entrypoint", routerProtocol: RouterProtocolTCP, entryProtocol: EntryPointProtocolTCP},200 {name: "tcp router accepts tcp entrypoint", routerProtocol: RouterProtocolTCP, entryProtocol: EntryPointProtocolTCP},
@@ -196,12 +202,9 @@ func TestTrafficGraphValidateRouterEntryPointProtocolCompatibility(t *testing.T)
196 {name: "http router rejects udp entrypoint", routerProtocol: RouterProtocolHTTP, entryProtocol: EntryPointProtocolUDP, wantErr: true},202 {name: "http router rejects udp entrypoint", routerProtocol: RouterProtocolHTTP, entryProtocol: EntryPointProtocolUDP, wantErr: true},
197 {name: "http router rejects tcp entrypoint", routerProtocol: RouterProtocolHTTP, entryProtocol: EntryPointProtocolTCP, wantErr: true},203 {name: "http router rejects tcp entrypoint", routerProtocol: RouterProtocolHTTP, entryProtocol: EntryPointProtocolTCP, wantErr: true},
198 {name: "udp router rejects tcp entrypoint", routerProtocol: RouterProtocolUDP, entryProtocol: EntryPointProtocolTCP, wantErr: true},204 {name: "udp router rejects tcp entrypoint", routerProtocol: RouterProtocolUDP, entryProtocol: EntryPointProtocolTCP, wantErr: true},
199- {name: "udp router rejects http entrypoint", routerProtocol: RouterProtocolUDP, entryProtocol: EntryPointProtocolHTTP, wantErr: true},
200 {name: "udp router rejects tls mux entrypoint", routerProtocol: RouterProtocolUDP, entryProtocol: EntryPointProtocolTLSMux, wantErr: true},205 {name: "udp router rejects tls mux entrypoint", routerProtocol: RouterProtocolUDP, entryProtocol: EntryPointProtocolTLSMux, wantErr: true},
201 {name: "tcp router rejects udp entrypoint", routerProtocol: RouterProtocolTCP, entryProtocol: EntryPointProtocolUDP, wantErr: true},206 {name: "tcp router rejects udp entrypoint", routerProtocol: RouterProtocolTCP, entryProtocol: EntryPointProtocolUDP, wantErr: true},
202- {name: "tcp router rejects http entrypoint", routerProtocol: RouterProtocolTCP, entryProtocol: EntryPointProtocolHTTP, wantErr: true},
203 {name: "tcp router rejects tls mux entrypoint", routerProtocol: RouterProtocolTCP, entryProtocol: EntryPointProtocolTLSMux, wantErr: true},207 {name: "tcp router rejects tls mux entrypoint", routerProtocol: RouterProtocolTCP, entryProtocol: EntryPointProtocolTLSMux, wantErr: true},
204- {name: "tls passthrough router rejects http entrypoint", routerProtocol: RouterProtocolTLSPassthrough, entryProtocol: EntryPointProtocolHTTP, wantErr: true},
205 {name: "tls passthrough router rejects udp entrypoint", routerProtocol: RouterProtocolTLSPassthrough, entryProtocol: EntryPointProtocolUDP, wantErr: true},208 {name: "tls passthrough router rejects udp entrypoint", routerProtocol: RouterProtocolTLSPassthrough, entryProtocol: EntryPointProtocolUDP, wantErr: true},
206 {name: "tls passthrough router rejects tcp entrypoint per tls mux spec", routerProtocol: RouterProtocolTLSPassthrough, entryProtocol: EntryPointProtocolTCP, wantErr: true},209 {name: "tls passthrough router rejects tcp entrypoint per tls mux spec", routerProtocol: RouterProtocolTLSPassthrough, entryProtocol: EntryPointProtocolTCP, wantErr: true},
207 }210 }
@@ -261,7 +264,7 @@ func TestTrafficGraphValidateRouterNames(t *testing.T) {
261 for _, tt := range tests {264 for _, tt := range tests {
262 t.Run(tt.name, func(t *testing.T) {265 t.Run(tt.name, func(t *testing.T) {
263 graph := TrafficGraph{266 graph := TrafficGraph{
264- EntryPoints: []EntryPoint{{Name: "web", Address: ":80", Protocol: EntryPointProtocolHTTP}},267+ EntryPoints: []EntryPoint{{Name: "web", Address: ":443", Protocol: EntryPointProtocolTLSMux}},
265 Routers: tt.routers,268 Routers: tt.routers,
266 Services: []TrafficService{service},269 Services: []TrafficService{service},
267 }270 }
@@ -277,7 +280,6 @@ func TestTrafficGraphValidateDuplicateHTTPHosts(t *testing.T) {
277 entryProtocol EntryPointProtocol280 entryProtocol EntryPointProtocol
278 entryAddress string281 entryAddress string
279 }{282 }{
280- {name: "http entrypoint", entryProtocol: EntryPointProtocolHTTP, entryAddress: ":80"},
281 {name: "tls mux entrypoint", entryProtocol: EntryPointProtocolTLSMux, entryAddress: ":443"},283 {name: "tls mux entrypoint", entryProtocol: EntryPointProtocolTLSMux, entryAddress: ":443"},
282 }284 }
283 285
@@ -522,7 +524,7 @@ func TestTrafficGraphValidateHTTPServiceRefs(t *testing.T) {
522 for _, tt := range tests {524 for _, tt := range tests {
523 t.Run(tt.name, func(t *testing.T) {525 t.Run(tt.name, func(t *testing.T) {
524 graph := TrafficGraph{526 graph := TrafficGraph{
525- EntryPoints: []EntryPoint{{Name: "web", Address: ":80", Protocol: EntryPointProtocolHTTP}},527+ EntryPoints: []EntryPoint{{Name: "web", Address: ":443", Protocol: EntryPointProtocolTLSMux}},
526 Routers: []TrafficRouter{{Name: "web", EntryPoint: "web", Protocol: RouterProtocolHTTP, Rule: TrafficRule{Host: "app.example.com"}, Service: tt.service}},528 Routers: []TrafficRouter{{Name: "web", EntryPoint: "web", Protocol: RouterProtocolHTTP, Rule: TrafficRule{Host: "app.example.com"}, Service: tt.service}},
527 Services: []TrafficService{{Name: tt.service, Backends: []TrafficBackend{{Name: "app", Host: "10.0.0.2", Port: 8080, Protocol: NetworkProtocolTCP}}}},529 Services: []TrafficService{{Name: tt.service, Backends: []TrafficBackend{{Name: "app", Host: "10.0.0.2", Port: 8080, Protocol: NetworkProtocolTCP}}}},
528 }530 }
M internal/usecase/traffic/builder.go +47 -3
@@ -137,9 +137,15 @@ func (b *builder) buildEntryPoints() []domain.EntryPoint {
137137
138138 func (b *builder) addHTTPRoutes(graph *domain.TrafficGraph) error {
139139 routes := append([]domain.Route{}, b.input.Routes...)
140+ if len(routes) == 0 {
141+ return nil
142+ }
143+ entryPoint, err := b.routeEntryPoint()
144+ if err != nil {
145+ return err
146+ }
140147 sort.Slice(routes, func(i, j int) bool { return routes[i].Domain < routes[j].Domain })
141148 for _, route := range routes {
142- entryPoint := DefaultEdgeEntryPointName
143149 serviceName := string(domain.TrafficServiceRefRoute) + ":" + route.Domain
144150 b.addService(domain.TrafficService{Name: serviceName})
145151 graph.Routers = append(graph.Routers, domain.TrafficRouter{Name: "route:" + route.Domain, EntryPoint: entryPoint, Protocol: domain.RouterProtocolHTTP, Rule: domain.TrafficRule{Host: route.Domain}, Service: serviceName})
@@ -148,7 +154,15 @@ func (b *builder) addHTTPRoutes(graph *domain.TrafficGraph) error {
148154 }
149155
150156 func (b *builder) addExternalRoutes(graph *domain.TrafficGraph) error {
151- for _, hostName := range sortedKeys(b.input.ExternalRoutes) {
157+ hostNames := sortedKeys(b.input.ExternalRoutes)
158+ if len(hostNames) == 0 {
159+ return nil
160+ }
161+ entryPoint, err := b.routeEntryPoint()
162+ if err != nil {
163+ return err
164+ }
165+ for _, hostName := range hostNames {
152166 target := b.input.ExternalRoutes[hostName]
153167 host, port, err := parseBackendAddress(target)
154168 if err != nil {
@@ -156,11 +170,41 @@ func (b *builder) addExternalRoutes(graph *domain.TrafficGraph) error {
156170 }
157171 serviceName := string(domain.TrafficServiceRefExternalRoute) + ":" + hostName
158172 b.addService(domain.TrafficService{Name: serviceName, Backends: []domain.TrafficBackend{{Name: hostName, Host: host, Port: port, Protocol: domain.NetworkProtocolTCP}}})
159- graph.Routers = append(graph.Routers, domain.TrafficRouter{Name: "external_route:" + hostName, EntryPoint: DefaultEdgeEntryPointName, Protocol: domain.RouterProtocolHTTP, Rule: domain.TrafficRule{Host: hostName}, Service: serviceName})
173+ graph.Routers = append(graph.Routers, domain.TrafficRouter{Name: "external_route:" + hostName, EntryPoint: entryPoint, Protocol: domain.RouterProtocolHTTP, Rule: domain.TrafficRule{Host: hostName}, Service: serviceName})
160174 }
161175 return nil
162176 }
163177
178+func (b *builder) routeEntryPoint() (string, error) {
179+ if cfg, ok := b.input.EntryPoints[DefaultEdgeEntryPointName]; ok && routeCapableEntryPoint(cfg.Protocol) {
180+ return DefaultEdgeEntryPointName, nil
181+ }
182+
183+ candidates := make([]string, 0)
184+ for _, name := range sortedKeys(b.input.EntryPoints) {
185+ if routeCapableEntryPoint(b.input.EntryPoints[name].Protocol) {
186+ candidates = append(candidates, name)
187+ }
188+ }
189+ switch len(candidates) {
190+ case 0:
191+ return DefaultEdgeEntryPointName, nil
192+ case 1:
193+ return candidates[0], nil
194+ default:
195+ return "", fmt.Errorf("multiple route-capable entrypoints configured without %q: %s", DefaultEdgeEntryPointName, strings.Join(candidates, ", "))
196+ }
197+}
198+
199+func routeCapableEntryPoint(protocol domain.EntryPointProtocol) bool {
200+ switch protocol {
201+ case domain.EntryPointProtocolSmartTCP, domain.EntryPointProtocolTLSMux:
202+ return true
203+ default:
204+ return false
205+ }
206+}
207+
164208 func (b *builder) addExplicitRouters(graph *domain.TrafficGraph) error {
165209 if err := b.addL4Routers(graph, b.input.Traffic.TCP.Routers, domain.RouterProtocolTCP, domain.NetworkProtocolTCP); err != nil {
166210 return err
@@ -137,9 +137,15 @@ func (b *builder) buildEntryPoints() []domain.EntryPoint {
137 137
138 func (b *builder) addHTTPRoutes(graph *domain.TrafficGraph) error {138 func (b *builder) addHTTPRoutes(graph *domain.TrafficGraph) error {
139 routes := append([]domain.Route{}, b.input.Routes...)139 routes := append([]domain.Route{}, b.input.Routes...)
140+ if len(routes) == 0 {
141+ return nil
142+ }
143+ entryPoint, err := b.routeEntryPoint()
144+ if err != nil {
145+ return err
146+ }
140 sort.Slice(routes, func(i, j int) bool { return routes[i].Domain < routes[j].Domain })147 sort.Slice(routes, func(i, j int) bool { return routes[i].Domain < routes[j].Domain })
141 for _, route := range routes {148 for _, route := range routes {
142- entryPoint := DefaultEdgeEntryPointName
143 serviceName := string(domain.TrafficServiceRefRoute) + ":" + route.Domain149 serviceName := string(domain.TrafficServiceRefRoute) + ":" + route.Domain
144 b.addService(domain.TrafficService{Name: serviceName})150 b.addService(domain.TrafficService{Name: serviceName})
145 graph.Routers = append(graph.Routers, domain.TrafficRouter{Name: "route:" + route.Domain, EntryPoint: entryPoint, Protocol: domain.RouterProtocolHTTP, Rule: domain.TrafficRule{Host: route.Domain}, Service: serviceName})151 graph.Routers = append(graph.Routers, domain.TrafficRouter{Name: "route:" + route.Domain, EntryPoint: entryPoint, Protocol: domain.RouterProtocolHTTP, Rule: domain.TrafficRule{Host: route.Domain}, Service: serviceName})
@@ -148,7 +154,15 @@ func (b *builder) addHTTPRoutes(graph *domain.TrafficGraph) error {
148 }154 }
149 155
150 func (b *builder) addExternalRoutes(graph *domain.TrafficGraph) error {156 func (b *builder) addExternalRoutes(graph *domain.TrafficGraph) error {
151- for _, hostName := range sortedKeys(b.input.ExternalRoutes) {157+ hostNames := sortedKeys(b.input.ExternalRoutes)
158+ if len(hostNames) == 0 {
159+ return nil
160+ }
161+ entryPoint, err := b.routeEntryPoint()
162+ if err != nil {
163+ return err
164+ }
165+ for _, hostName := range hostNames {
152 target := b.input.ExternalRoutes[hostName]166 target := b.input.ExternalRoutes[hostName]
153 host, port, err := parseBackendAddress(target)167 host, port, err := parseBackendAddress(target)
154 if err != nil {168 if err != nil {
@@ -156,11 +170,41 @@ func (b *builder) addExternalRoutes(graph *domain.TrafficGraph) error {
156 }170 }
157 serviceName := string(domain.TrafficServiceRefExternalRoute) + ":" + hostName171 serviceName := string(domain.TrafficServiceRefExternalRoute) + ":" + hostName
158 b.addService(domain.TrafficService{Name: serviceName, Backends: []domain.TrafficBackend{{Name: hostName, Host: host, Port: port, Protocol: domain.NetworkProtocolTCP}}})172 b.addService(domain.TrafficService{Name: serviceName, Backends: []domain.TrafficBackend{{Name: hostName, Host: host, Port: port, Protocol: domain.NetworkProtocolTCP}}})
159- graph.Routers = append(graph.Routers, domain.TrafficRouter{Name: "external_route:" + hostName, EntryPoint: DefaultEdgeEntryPointName, Protocol: domain.RouterProtocolHTTP, Rule: domain.TrafficRule{Host: hostName}, Service: serviceName})173+ graph.Routers = append(graph.Routers, domain.TrafficRouter{Name: "external_route:" + hostName, EntryPoint: entryPoint, Protocol: domain.RouterProtocolHTTP, Rule: domain.TrafficRule{Host: hostName}, Service: serviceName})
160 }174 }
161 return nil175 return nil
162 }176 }
163 177
178+func (b *builder) routeEntryPoint() (string, error) {
179+ if cfg, ok := b.input.EntryPoints[DefaultEdgeEntryPointName]; ok && routeCapableEntryPoint(cfg.Protocol) {
180+ return DefaultEdgeEntryPointName, nil
181+ }
182+
183+ candidates := make([]string, 0)
184+ for _, name := range sortedKeys(b.input.EntryPoints) {
185+ if routeCapableEntryPoint(b.input.EntryPoints[name].Protocol) {
186+ candidates = append(candidates, name)
187+ }
188+ }
189+ switch len(candidates) {
190+ case 0:
191+ return DefaultEdgeEntryPointName, nil
192+ case 1:
193+ return candidates[0], nil
194+ default:
195+ return "", fmt.Errorf("multiple route-capable entrypoints configured without %q: %s", DefaultEdgeEntryPointName, strings.Join(candidates, ", "))
196+ }
197+}
198+
199+func routeCapableEntryPoint(protocol domain.EntryPointProtocol) bool {
200+ switch protocol {
201+ case domain.EntryPointProtocolSmartTCP, domain.EntryPointProtocolTLSMux:
202+ return true
203+ default:
204+ return false
205+ }
206+}
207+
164 func (b *builder) addExplicitRouters(graph *domain.TrafficGraph) error {208 func (b *builder) addExplicitRouters(graph *domain.TrafficGraph) error {
165 if err := b.addL4Routers(graph, b.input.Traffic.TCP.Routers, domain.RouterProtocolTCP, domain.NetworkProtocolTCP); err != nil {209 if err := b.addL4Routers(graph, b.input.Traffic.TCP.Routers, domain.RouterProtocolTCP, domain.NetworkProtocolTCP); err != nil {
166 return err210 return err
M internal/usecase/traffic/builder_test.go +41 -0
@@ -18,6 +18,47 @@ func TestBuildSmartTCPRoutesDefaultToEdge(t *testing.T) {
1818 require.Contains(t, graph.Routers, domain.TrafficRouter{Name: "route:app.example.com", EntryPoint: "edge", Protocol: domain.RouterProtocolHTTP, Rule: domain.TrafficRule{Host: "app.example.com"}, Service: "route:app.example.com"})
1919 }
2020
21+func TestBuildManagedRoutesUseOnlyCustomSmartTCPEntrypoint(t *testing.T) {
22+ graph, err := Build(Input{
23+ EntryPoints: map[string]EntryPointConfig{"public": {Address: ":443", Protocol: domain.EntryPointProtocolSmartTCP}},
24+ Routes: []domain.Route{{Domain: "app.example.com", HTTPS: true}},
25+ })
26+ require.NoError(t, err)
27+ require.Contains(t, graph.Routers, domain.TrafficRouter{Name: "route:app.example.com", EntryPoint: "public", Protocol: domain.RouterProtocolHTTP, Rule: domain.TrafficRule{Host: "app.example.com"}, Service: "route:app.example.com"})
28+}
29+
30+func TestBuildRouteEntrypointPrefersEdge(t *testing.T) {
31+ graph, err := Build(Input{
32+ EntryPoints: map[string]EntryPointConfig{
33+ "edge": {Address: ":443", Protocol: domain.EntryPointProtocolSmartTCP},
34+ "public": {Address: ":8443", Protocol: domain.EntryPointProtocolSmartTCP},
35+ },
36+ Routes: []domain.Route{{Domain: "app.example.com", HTTPS: true}},
37+ })
38+ require.NoError(t, err)
39+ require.Contains(t, graph.Routers, domain.TrafficRouter{Name: "route:app.example.com", EntryPoint: "edge", Protocol: domain.RouterProtocolHTTP, Rule: domain.TrafficRule{Host: "app.example.com"}, Service: "route:app.example.com"})
40+}
41+
42+func TestBuildRoutesRejectAmbiguousRouteEntrypoints(t *testing.T) {
43+ _, err := Build(Input{
44+ EntryPoints: map[string]EntryPointConfig{
45+ "public-a": {Address: ":443", Protocol: domain.EntryPointProtocolSmartTCP},
46+ "public-b": {Address: ":8443", Protocol: domain.EntryPointProtocolTLSMux},
47+ },
48+ Routes: []domain.Route{{Domain: "app.example.com", HTTPS: true}},
49+ })
50+ require.ErrorContains(t, err, "multiple route-capable entrypoints")
51+}
52+
53+func TestBuildExternalRoutesUseResolvedRouteEntryPoint(t *testing.T) {
54+ graph, err := Build(Input{
55+ EntryPoints: map[string]EntryPointConfig{"public": {Address: ":443", Protocol: domain.EntryPointProtocolTLSMux}},
56+ ExternalRoutes: map[string]string{"ext.example.com": "backend.local:8080"},
57+ })
58+ require.NoError(t, err)
59+ require.Contains(t, graph.Routers, domain.TrafficRouter{Name: "external_route:ext.example.com", EntryPoint: "public", Protocol: domain.RouterProtocolHTTP, Rule: domain.TrafficRule{Host: "ext.example.com"}, Service: "external_route:ext.example.com"})
60+}
61+
2162 func TestBuildRawFallbackConfigMapsToEntryPoint(t *testing.T) {
2263 graph, err := Build(Input{
2364 EntryPoints: map[string]EntryPointConfig{"edge": {
@@ -18,6 +18,47 @@ func TestBuildSmartTCPRoutesDefaultToEdge(t *testing.T) {
18 require.Contains(t, graph.Routers, domain.TrafficRouter{Name: "route:app.example.com", EntryPoint: "edge", Protocol: domain.RouterProtocolHTTP, Rule: domain.TrafficRule{Host: "app.example.com"}, Service: "route:app.example.com"})18 require.Contains(t, graph.Routers, domain.TrafficRouter{Name: "route:app.example.com", EntryPoint: "edge", Protocol: domain.RouterProtocolHTTP, Rule: domain.TrafficRule{Host: "app.example.com"}, Service: "route:app.example.com"})
19 }19 }
20 20
21+func TestBuildManagedRoutesUseOnlyCustomSmartTCPEntrypoint(t *testing.T) {
22+ graph, err := Build(Input{
23+ EntryPoints: map[string]EntryPointConfig{"public": {Address: ":443", Protocol: domain.EntryPointProtocolSmartTCP}},
24+ Routes: []domain.Route{{Domain: "app.example.com", HTTPS: true}},
25+ })
26+ require.NoError(t, err)
27+ require.Contains(t, graph.Routers, domain.TrafficRouter{Name: "route:app.example.com", EntryPoint: "public", Protocol: domain.RouterProtocolHTTP, Rule: domain.TrafficRule{Host: "app.example.com"}, Service: "route:app.example.com"})
28+}
29+
30+func TestBuildRouteEntrypointPrefersEdge(t *testing.T) {
31+ graph, err := Build(Input{
32+ EntryPoints: map[string]EntryPointConfig{
33+ "edge": {Address: ":443", Protocol: domain.EntryPointProtocolSmartTCP},
34+ "public": {Address: ":8443", Protocol: domain.EntryPointProtocolSmartTCP},
35+ },
36+ Routes: []domain.Route{{Domain: "app.example.com", HTTPS: true}},
37+ })
38+ require.NoError(t, err)
39+ require.Contains(t, graph.Routers, domain.TrafficRouter{Name: "route:app.example.com", EntryPoint: "edge", Protocol: domain.RouterProtocolHTTP, Rule: domain.TrafficRule{Host: "app.example.com"}, Service: "route:app.example.com"})
40+}
41+
42+func TestBuildRoutesRejectAmbiguousRouteEntrypoints(t *testing.T) {
43+ _, err := Build(Input{
44+ EntryPoints: map[string]EntryPointConfig{
45+ "public-a": {Address: ":443", Protocol: domain.EntryPointProtocolSmartTCP},
46+ "public-b": {Address: ":8443", Protocol: domain.EntryPointProtocolTLSMux},
47+ },
48+ Routes: []domain.Route{{Domain: "app.example.com", HTTPS: true}},
49+ })
50+ require.ErrorContains(t, err, "multiple route-capable entrypoints")
51+}
52+
53+func TestBuildExternalRoutesUseResolvedRouteEntryPoint(t *testing.T) {
54+ graph, err := Build(Input{
55+ EntryPoints: map[string]EntryPointConfig{"public": {Address: ":443", Protocol: domain.EntryPointProtocolTLSMux}},
56+ ExternalRoutes: map[string]string{"ext.example.com": "backend.local:8080"},
57+ })
58+ require.NoError(t, err)
59+ require.Contains(t, graph.Routers, domain.TrafficRouter{Name: "external_route:ext.example.com", EntryPoint: "public", Protocol: domain.RouterProtocolHTTP, Rule: domain.TrafficRule{Host: "ext.example.com"}, Service: "external_route:ext.example.com"})
60+}
61+
21 func TestBuildRawFallbackConfigMapsToEntryPoint(t *testing.T) {62 func TestBuildRawFallbackConfigMapsToEntryPoint(t *testing.T) {
22 graph, err := Build(Input{63 graph, err := Build(Input{
23 EntryPoints: map[string]EntryPointConfig{"edge": {64 EntryPoints: map[string]EntryPointConfig{"edge": {

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 7

Tags 106

↑↓ moveenter openesc close