fix(traffic): clean up entrypoint semantics
- commit
- f7cd760b3d854089414e67550adac60aee616c64
- author
- brice · · 2026-06-30 14:27 UTC
- parent
- d88f58b
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· 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 | |||
| 230 | 230 | | `server.registry_allowed_ips` | `[]` | IPs or CIDR ranges allowed to access the registry (empty = allow all) | | |
| 231 | 231 | | `server.proxy_allowed_ips` | `[]` | IPs or CIDR ranges allowed to reach the proxy (empty = allow all) | | |
| 232 | 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 | | |
| 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` | | |
| 235 | 235 | | `entrypoints.<name>.trusted_cidrs` | `[]` | Peer socket IP allowlist for all traffic on the entrypoint | | |
| 236 | 236 | | `entrypoints.<name>.raw_fallback` | `""` | TCP router used by smart TCP for unknown non-HTTP/non-TLS bytes | | |
| 237 | 237 | | `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 | |||
| 4 | 4 | ||
| 5 | 5 | ## 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 | 9 | ```toml | |
| 10 | 10 | [entrypoints.edge] | |
| @@ -15,7 +15,7 @@ trusted_cidrs = [] | |||
| 15 | 15 | ||
| 16 | 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 | 20 | ## Smart TCP Dispatch Order | |
| 21 | 21 | ||
| @@ -4,7 +4,7 @@ Gordon models network listeners as a traffic graph: entrypoints receive packets | |||||
| 4 | 4 | ||||
| 5 | ## Smart TCP Edge Entrypoint | 5 | ## 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 | ```toml | 9 | ```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 Order | 20 | ## Smart TCP Dispatch Order | ||
| 21 | 21 | ||||
M gordon.toml.example +2 -0
| @@ -18,6 +18,8 @@ gordon_domain = "gordon.example.com" | |||
| 18 | 18 | # max_proxy_response_size = "1GB" | |
| 19 | 19 | # 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 | 23 | [entrypoints.edge] | |
| 22 | 24 | address = ":443" | |
| 23 | 25 | 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 = 10000 | 19 | # 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 ( | |||
| 17 | 17 | func TestTrafficRuntimeGraphKeepsSmartTCPHTTPAndTLSPassthroughRouters(t *testing.T) { | |
| 18 | 18 | graph := domain.TrafficGraph{ | |
| 19 | 19 | EntryPoints: []domain.EntryPoint{ | |
| 20 | - | {Name: "web", Address: "127.0.0.1:8080", Protocol: domain.EntryPointProtocolHTTP}, | |
| 21 | 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 | 22 | Routers: []domain.TrafficRouter{ | |
| 24 | - | {Name: "legacy-http", EntryPoint: "web", Protocol: domain.RouterProtocolHTTP, Service: "route:legacy.example.com"}, | |
| 25 | 23 | {Name: "app", EntryPoint: traffic.DefaultEdgeEntryPointName, Protocol: domain.RouterProtocolHTTP, Rule: domain.TrafficRule{Host: "app.example.com"}, Service: "route:app.example.com"}, | |
| 26 | 24 | {Name: "tls-raw", EntryPoint: traffic.DefaultEdgeEntryPointName, Protocol: domain.RouterProtocolTLSPassthrough, Rule: domain.TrafficRule{SNI: "raw.example.com"}, Service: "network_service:raw:tls"}, | |
| 27 | 25 | {Name: "ssh", EntryPoint: traffic.DefaultEdgeEntryPointName, Protocol: domain.RouterProtocolTCP, Service: "network_service:ssh:ssh"}, | |
| 28 | 26 | }, | |
| 29 | 27 | Services: []domain.TrafficService{ | |
| 30 | - | {Name: "route:legacy.example.com"}, | |
| 31 | 28 | {Name: "route:app.example.com"}, | |
| 32 | 29 | {Name: "network_service:raw:tls", Backends: []domain.TrafficBackend{{Name: "raw:tls", Host: "127.0.0.1", Port: 443, Protocol: domain.NetworkProtocolTCP}}}, | |
| 33 | 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 | 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 | 42 | graph := domain.TrafficGraph{ | |
| 46 | 43 | EntryPoints: []domain.EntryPoint{{Name: "edge", Address: "127.0.0.1:8443", Protocol: domain.EntryPointProtocolHTTP}}, | |
| 47 | 44 | Routers: []domain.TrafficRouter{{Name: "app", EntryPoint: "edge", Protocol: domain.RouterProtocolHTTP, Service: "route:app.example.com"}}, | |
| 48 | 45 | Services: []domain.TrafficService{{Name: "route:app.example.com"}}, | |
| 49 | 46 | } | |
| 50 | 47 | ||
| 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\"") | |
| 54 | 49 | } | |
| 55 | 50 | ||
| 56 | 51 | func TestTrafficRuntimeGraphKeepsDefaultTLSEntrypoint(t *testing.T) { | |
| 57 | 52 | graph := domain.TrafficGraph{ | |
| 58 | 53 | EntryPoints: []domain.EntryPoint{ | |
| 59 | - | {Name: "web", Address: "127.0.0.1:8080", Protocol: domain.EntryPointProtocolHTTP}, | |
| 60 | 54 | {Name: "websecure", Address: "127.0.0.1:8443", Protocol: domain.EntryPointProtocolTLSMux}, | |
| 61 | 55 | {Name: "postgres", Address: "127.0.0.1:15432", Protocol: domain.EntryPointProtocolTCP}, | |
| 62 | 56 | }, | |
| 63 | 57 | Routers: []domain.TrafficRouter{ | |
| 64 | - | {Name: "app", EntryPoint: "web", Protocol: domain.RouterProtocolHTTP, Service: "route:app.example.com"}, | |
| 65 | 58 | {Name: "app-secure", EntryPoint: "websecure", Protocol: domain.RouterProtocolHTTP, Rule: domain.TrafficRule{Host: "app.example.com"}, Service: "route:app.example.com"}, | |
| 66 | 59 | {Name: "postgres", EntryPoint: "postgres", Protocol: domain.RouterProtocolTCP, Service: "network_service:postgres:db"}, | |
| 67 | 60 | }, | |
| @@ -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 | |||
| 617 | 617 | ||
| 618 | 618 | func validateEntryPointProtocol(protocol EntryPointProtocol) error { | |
| 619 | 619 | switch protocol { | |
| 620 | - | case EntryPointProtocolHTTP, EntryPointProtocolTLSMux, EntryPointProtocolSmartTCP, EntryPointProtocolTCP, EntryPointProtocolUDP: | |
| 620 | + | case EntryPointProtocolTLSMux, EntryPointProtocolSmartTCP, EntryPointProtocolTCP, EntryPointProtocolUDP: | |
| 621 | 621 | return nil | |
| 622 | 622 | default: | |
| 623 | - | return fmt.Errorf("%q", protocol) | |
| 623 | + | return fmt.Errorf("unsupported traffic entrypoint protocol %q", protocol) | |
| 624 | 624 | } | |
| 625 | 625 | } | |
| 626 | 626 | ||
| @@ -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 nil | 621 | 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) { | |||
| 102 | 102 | { | |
| 103 | 103 | name: "duplicate entrypoint names", | |
| 104 | 104 | 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}, | |
| 107 | 107 | }}, | |
| 108 | 108 | wantErr: "duplicate entrypoint name", | |
| 109 | 109 | }, | |
| 110 | 110 | { | |
| 111 | 111 | name: "invalid entrypoint address", | |
| 112 | 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 | 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 | 125 | name: "invalid entrypoint protocol", | |
| 119 | 126 | graph: TrafficGraph{EntryPoints: []EntryPoint{ | |
| 120 | 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 | 132 | name: "duplicate tcp address rejected", | |
| @@ -188,7 +195,6 @@ func TestTrafficGraphValidateRouterEntryPointProtocolCompatibility(t *testing.T) | |||
| 188 | 195 | entryProtocol EntryPointProtocol | |
| 189 | 196 | wantErr bool | |
| 190 | 197 | }{ | |
| 191 | - | {name: "http router accepts http entrypoint", routerProtocol: RouterProtocolHTTP, entryProtocol: EntryPointProtocolHTTP}, | |
| 192 | 198 | {name: "http router accepts tls mux entrypoint", routerProtocol: RouterProtocolHTTP, entryProtocol: EntryPointProtocolTLSMux}, | |
| 193 | 199 | {name: "tls passthrough router accepts tls mux entrypoint", routerProtocol: RouterProtocolTLSPassthrough, entryProtocol: EntryPointProtocolTLSMux}, | |
| 194 | 200 | {name: "tcp router accepts tcp entrypoint", routerProtocol: RouterProtocolTCP, entryProtocol: EntryPointProtocolTCP}, | |
| @@ -196,12 +202,9 @@ func TestTrafficGraphValidateRouterEntryPointProtocolCompatibility(t *testing.T) | |||
| 196 | 202 | {name: "http router rejects udp entrypoint", routerProtocol: RouterProtocolHTTP, entryProtocol: EntryPointProtocolUDP, wantErr: true}, | |
| 197 | 203 | {name: "http router rejects tcp entrypoint", routerProtocol: RouterProtocolHTTP, entryProtocol: EntryPointProtocolTCP, wantErr: true}, | |
| 198 | 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 | 205 | {name: "udp router rejects tls mux entrypoint", routerProtocol: RouterProtocolUDP, entryProtocol: EntryPointProtocolTLSMux, wantErr: true}, | |
| 201 | 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 | 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 | 208 | {name: "tls passthrough router rejects udp entrypoint", routerProtocol: RouterProtocolTLSPassthrough, entryProtocol: EntryPointProtocolUDP, wantErr: true}, | |
| 206 | 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 | 264 | for _, tt := range tests { | |
| 262 | 265 | t.Run(tt.name, func(t *testing.T) { | |
| 263 | 266 | graph := TrafficGraph{ | |
| 264 | - | EntryPoints: []EntryPoint{{Name: "web", Address: ":80", Protocol: EntryPointProtocolHTTP}}, | |
| 267 | + | EntryPoints: []EntryPoint{{Name: "web", Address: ":443", Protocol: EntryPointProtocolTLSMux}}, | |
| 265 | 268 | Routers: tt.routers, | |
| 266 | 269 | Services: []TrafficService{service}, | |
| 267 | 270 | } | |
| @@ -277,7 +280,6 @@ func TestTrafficGraphValidateDuplicateHTTPHosts(t *testing.T) { | |||
| 277 | 280 | entryProtocol EntryPointProtocol | |
| 278 | 281 | entryAddress string | |
| 279 | 282 | }{ | |
| 280 | - | {name: "http entrypoint", entryProtocol: EntryPointProtocolHTTP, entryAddress: ":80"}, | |
| 281 | 283 | {name: "tls mux entrypoint", entryProtocol: EntryPointProtocolTLSMux, entryAddress: ":443"}, | |
| 282 | 284 | } | |
| 283 | 285 | ||
| @@ -522,7 +524,7 @@ func TestTrafficGraphValidateHTTPServiceRefs(t *testing.T) { | |||
| 522 | 524 | for _, tt := range tests { | |
| 523 | 525 | t.Run(tt.name, func(t *testing.T) { | |
| 524 | 526 | graph := TrafficGraph{ | |
| 525 | - | EntryPoints: []EntryPoint{{Name: "web", Address: ":80", Protocol: EntryPointProtocolHTTP}}, | |
| 527 | + | EntryPoints: []EntryPoint{{Name: "web", Address: ":443", Protocol: EntryPointProtocolTLSMux}}, | |
| 526 | 528 | Routers: []TrafficRouter{{Name: "web", EntryPoint: "web", Protocol: RouterProtocolHTTP, Rule: TrafficRule{Host: "app.example.com"}, Service: tt.service}}, | |
| 527 | 529 | Services: []TrafficService{{Name: tt.service, Backends: []TrafficBackend{{Name: "app", Host: "10.0.0.2", Port: 8080, Protocol: NetworkProtocolTCP}}}}, | |
| 528 | 530 | } | |
| @@ -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 EntryPointProtocol | 195 | entryProtocol EntryPointProtocol | ||
| 189 | wantErr bool | 196 | 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 EntryPointProtocol | 280 | entryProtocol EntryPointProtocol | ||
| 278 | entryAddress string | 281 | 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 { | |||
| 137 | 137 | ||
| 138 | 138 | func (b *builder) addHTTPRoutes(graph *domain.TrafficGraph) error { | |
| 139 | 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 | 147 | sort.Slice(routes, func(i, j int) bool { return routes[i].Domain < routes[j].Domain }) | |
| 141 | 148 | for _, route := range routes { | |
| 142 | - | entryPoint := DefaultEdgeEntryPointName | |
| 143 | 149 | serviceName := string(domain.TrafficServiceRefRoute) + ":" + route.Domain | |
| 144 | 150 | b.addService(domain.TrafficService{Name: serviceName}) | |
| 145 | 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 | 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 | 166 | target := b.input.ExternalRoutes[hostName] | |
| 153 | 167 | host, port, err := parseBackendAddress(target) | |
| 154 | 168 | if err != nil { | |
| @@ -156,11 +170,41 @@ func (b *builder) addExternalRoutes(graph *domain.TrafficGraph) error { | |||
| 156 | 170 | } | |
| 157 | 171 | serviceName := string(domain.TrafficServiceRefExternalRoute) + ":" + hostName | |
| 158 | 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 | 175 | 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 | 208 | func (b *builder) addExplicitRouters(graph *domain.TrafficGraph) error { | |
| 165 | 209 | if err := b.addL4Routers(graph, b.input.Traffic.TCP.Routers, domain.RouterProtocolTCP, domain.NetworkProtocolTCP); err != nil { | |
| 166 | 210 | 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.Domain | 149 | 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) + ":" + hostName | 171 | 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 nil | 175 | 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 err | 210 | return err | ||
M internal/usecase/traffic/builder_test.go +41 -0
| @@ -18,6 +18,47 @@ func TestBuildSmartTCPRoutesDefaultToEdge(t *testing.T) { | |||
| 18 | 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 | 62 | func TestBuildRawFallbackConfigMapsToEntryPoint(t *testing.T) { | |
| 22 | 63 | graph, err := Build(Input{ | |
| 23 | 64 | 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": { | ||