1package app
2
3import (
4 "context"
5 "net"
6 "testing"
7
8 "github.com/stretchr/testify/assert"
9 "github.com/stretchr/testify/require"
10
11 trafficadapter "github.com/bnema/gordon/internal/adapters/in/traffic"
12 inmocks "github.com/bnema/gordon/internal/boundaries/in/mocks"
13 "github.com/bnema/gordon/internal/domain"
14 servicecfg "github.com/bnema/gordon/internal/usecase/services"
15 "github.com/bnema/gordon/internal/usecase/traffic"
16)
17
18func TestTrafficRuntimeGraphKeepsSmartTCPHTTPAndTLSPassthroughRouters(t *testing.T) {
19 graph := domain.TrafficGraph{
20 EntryPoints: []domain.EntryPoint{
21 {Name: traffic.DefaultEdgeEntryPointName, Address: "127.0.0.1:8443", Protocol: domain.EntryPointProtocolSmartTCP, RawFallback: "ssh", RawFallbackTrustedCIDRs: []string{"127.0.0.0/8"}},
22 },
23 Routers: []domain.TrafficRouter{
24 {Name: "app", EntryPoint: traffic.DefaultEdgeEntryPointName, Protocol: domain.RouterProtocolHTTP, Rule: domain.TrafficRule{Host: "app.example.com"}, Service: "route:app.example.com"},
25 {Name: "tls-raw", EntryPoint: traffic.DefaultEdgeEntryPointName, Protocol: domain.RouterProtocolTLSPassthrough, Rule: domain.TrafficRule{SNI: "raw.example.com"}, Service: "network_service:raw:tls"},
26 {Name: "ssh", EntryPoint: traffic.DefaultEdgeEntryPointName, Protocol: domain.RouterProtocolTCP, Service: "network_service:ssh:ssh"},
27 },
28 Services: []domain.TrafficService{
29 {Name: "route:app.example.com"},
30 {Name: "network_service:raw:tls", Backends: []domain.TrafficBackend{{Name: "raw:tls", Host: "127.0.0.1", Port: 443, Protocol: domain.NetworkProtocolTCP}}},
31 {Name: "network_service:ssh:ssh", Backends: []domain.TrafficBackend{{Name: "ssh:ssh", Host: "127.0.0.1", Port: 22, Protocol: domain.NetworkProtocolTCP}}},
32 },
33 }
34
35 filtered, err := trafficRuntimeGraph(graph)
36 require.NoError(t, err)
37 assert.ElementsMatch(t, []string{traffic.DefaultEdgeEntryPointName}, entryPointNames(filtered.EntryPoints))
38 assert.ElementsMatch(t, []string{"app", "tls-raw", "ssh"}, routerNames(filtered.Routers))
39 assert.ElementsMatch(t, []string{"route:app.example.com", "network_service:raw:tls", "network_service:ssh:ssh"}, serviceNames(filtered.Services))
40}
41
42func TestTrafficRuntimeGraphHTTPEntrypointIsInvalidRatherThanSilentlyFiltered(t *testing.T) {
43 graph := domain.TrafficGraph{
44 EntryPoints: []domain.EntryPoint{{Name: "edge", Address: "127.0.0.1:8443", Protocol: domain.EntryPointProtocolHTTP}},
45 Routers: []domain.TrafficRouter{{Name: "app", EntryPoint: "edge", Protocol: domain.RouterProtocolHTTP, Service: "route:app.example.com"}},
46 Services: []domain.TrafficService{{Name: "route:app.example.com"}},
47 }
48
49 require.ErrorContains(t, graph.Validate(), "unsupported traffic entrypoint protocol \"http\"")
50}
51
52func TestTrafficRuntimeGraphKeepsDefaultTLSEntrypoint(t *testing.T) {
53 graph := domain.TrafficGraph{
54 EntryPoints: []domain.EntryPoint{
55 {Name: "websecure", Address: "127.0.0.1:8443", Protocol: domain.EntryPointProtocolTLSMux},
56 {Name: "postgres", Address: "127.0.0.1:15432", Protocol: domain.EntryPointProtocolTCP},
57 },
58 Routers: []domain.TrafficRouter{
59 {Name: "app-secure", EntryPoint: "websecure", Protocol: domain.RouterProtocolHTTP, Rule: domain.TrafficRule{Host: "app.example.com"}, Service: "route:app.example.com"},
60 {Name: "postgres", EntryPoint: "postgres", Protocol: domain.RouterProtocolTCP, Service: "network_service:postgres:db"},
61 },
62 Services: []domain.TrafficService{
63 {Name: "route:app.example.com"},
64 {Name: "network_service:postgres:db", Backends: []domain.TrafficBackend{{Name: "db", Host: "127.0.0.1", Port: 5432, Protocol: domain.NetworkProtocolTCP}}},
65 },
66 }
67
68 filtered, err := trafficRuntimeGraph(graph)
69 require.NoError(t, err)
70 assert.ElementsMatch(t, []string{"websecure", "postgres"}, entryPointNames(filtered.EntryPoints))
71 assert.ElementsMatch(t, []string{"app-secure", "postgres"}, routerNames(filtered.Routers))
72 assert.ElementsMatch(t, []string{"route:app.example.com", "network_service:postgres:db"}, serviceNames(filtered.Services))
73}
74
75func TestTrafficRuntimeGraphAllowsTLSPassthroughOnDefaultTLS(t *testing.T) {
76 graph := domain.TrafficGraph{
77 EntryPoints: []domain.EntryPoint{{Name: "websecure", Address: "127.0.0.1:8443", Protocol: domain.EntryPointProtocolTLSMux}},
78 Routers: []domain.TrafficRouter{{Name: "raw", EntryPoint: "websecure", Protocol: domain.RouterProtocolTLSPassthrough, Rule: domain.TrafficRule{SNI: "raw.example.com"}, Service: "network_service:raw:tls"}},
79 Services: []domain.TrafficService{{Name: "network_service:raw:tls", Backends: []domain.TrafficBackend{{Name: "raw:tls", Host: "127.0.0.1", Port: 443, Protocol: domain.NetworkProtocolTCP}}}},
80 }
81
82 filtered, err := trafficRuntimeGraph(graph)
83 require.NoError(t, err)
84 require.Len(t, filtered.EntryPoints, 1)
85 assert.Equal(t, "websecure", filtered.EntryPoints[0].Name)
86 require.Len(t, filtered.Routers, 1)
87 assert.Equal(t, "raw", filtered.Routers[0].Name)
88}
89
90func TestApplyTrafficRuntimeConfigAppliesSmartTCPEntrypointWithRawFallbackPolicy(t *testing.T) {
91 manager := trafficadapter.NewManager()
92 defer func() { require.NoError(t, manager.Shutdown(context.Background())) }()
93 cfg := Config{
94 EntryPoints: map[string]traffic.EntryPointConfig{
95 traffic.DefaultEdgeEntryPointName: {
96 Address: freeTCPAddress(t),
97 Protocol: domain.EntryPointProtocolSmartTCP,
98 RawFallback: "ssh",
99 RawFallbackTrustedCIDRs: []string{"127.0.0.0/8"},
100 },
101 },
102 NetworkServices: []traffic.NetworkServiceConfig{{
103 Name: "ssh",
104 Ports: []traffic.PortConfig{{Name: "ssh", Container: 22, Protocol: domain.NetworkProtocolTCP}},
105 }},
106 }
107 cfg.Traffic.TCP.Routers = []traffic.RouterConfig{{Name: "ssh", EntryPoint: traffic.DefaultEdgeEntryPointName, Service: "network_service:ssh:ssh"}}
108
109 configSvc := inmocks.NewMockConfigService(t)
110 configSvc.EXPECT().GetRoutes(context.Background()).Return([]domain.Route{{Domain: "app.example.com", HTTPS: true}})
111 configSvc.EXPECT().GetExternalRoutes().Return(nil)
112
113 require.NoError(t, applyTrafficRuntimeConfig(context.Background(), manager, cfg, configSvc))
114 status := manager.Status()
115 require.Len(t, status.EntryPoints, 1)
116 assert.Equal(t, traffic.DefaultEdgeEntryPointName, status.EntryPoints[0].Name)
117 assert.True(t, status.EntryPoints[0].Active)
118 assert.Equal(t, domain.EntryPointProtocolSmartTCP, status.EntryPoints[0].Protocol)
119}
120
121func TestApplyTrafficRuntimeConfigPassesStandaloneServicesToBuilder(t *testing.T) {
122 manager := trafficadapter.NewManager()
123 defer func() { require.NoError(t, manager.Shutdown(context.Background())) }()
124 cfg := Config{
125 EntryPoints: map[string]traffic.EntryPointConfig{
126 "rust": {Address: freeUDPAddress(t), Protocol: domain.EntryPointProtocolUDP},
127 },
128 Services: []servicecfg.Config{{
129 Name: "rust",
130 Image: "localhost/rust:latest",
131 Enabled: true,
132 Ports: []servicecfg.PortConfig{{Name: "game", Container: 28015, Protocol: domain.NetworkProtocolUDP, Publish: "127.0.0.1:38015"}},
133 }},
134 }
135 cfg.Traffic.UDP.Routers = []traffic.RouterConfig{{Name: "rust-game", EntryPoint: "rust", Service: "service:rust:game"}}
136
137 configSvc := inmocks.NewMockConfigService(t)
138 configSvc.EXPECT().GetRoutes(context.Background()).Return(nil)
139 configSvc.EXPECT().GetExternalRoutes().Return(nil)
140
141 require.NoError(t, applyTrafficRuntimeConfig(context.Background(), manager, cfg, configSvc))
142 status := manager.Status()
143 require.Len(t, status.EntryPoints, 1)
144 assert.Equal(t, "rust", status.EntryPoints[0].Name)
145 assert.True(t, status.EntryPoints[0].Active)
146}
147
148func TestApplyTrafficRuntimeConfigAppliesCustomL4Entrypoint(t *testing.T) {
149 manager := trafficadapter.NewManager()
150 defer func() { require.NoError(t, manager.Shutdown(context.Background())) }()
151 cfg := Config{
152 EntryPoints: map[string]traffic.EntryPointConfig{
153 "postgres": {Address: freeTCPAddress(t), Protocol: domain.EntryPointProtocolTCP},
154 },
155 NetworkServices: []traffic.NetworkServiceConfig{{
156 Name: "postgres",
157 Ports: []traffic.PortConfig{{Name: "db", Container: 5432, Protocol: domain.NetworkProtocolTCP}},
158 }},
159 }
160 cfg.Traffic.TCP.Routers = []traffic.RouterConfig{{Name: "postgres", EntryPoint: "postgres", Service: "network_service:postgres:db"}}
161
162 configSvc := inmocks.NewMockConfigService(t)
163 configSvc.EXPECT().GetRoutes(context.Background()).Return(nil)
164 configSvc.EXPECT().GetExternalRoutes().Return(nil)
165
166 require.NoError(t, applyTrafficRuntimeConfig(context.Background(), manager, cfg, configSvc))
167 status := manager.Status()
168 require.Len(t, status.EntryPoints, 1)
169 assert.Equal(t, "postgres", status.EntryPoints[0].Name)
170 assert.True(t, status.EntryPoints[0].Active)
171}
172
173func entryPointNames(entries []domain.EntryPoint) []string {
174 names := make([]string, 0, len(entries))
175 for _, entry := range entries {
176 names = append(names, entry.Name)
177 }
178 return names
179}
180
181func routerNames(routers []domain.TrafficRouter) []string {
182 names := make([]string, 0, len(routers))
183 for _, router := range routers {
184 names = append(names, router.Name)
185 }
186 return names
187}
188
189func serviceNames(services []domain.TrafficService) []string {
190 names := make([]string, 0, len(services))
191 for _, service := range services {
192 names = append(names, service.Name)
193 }
194 return names
195}
196
197func freeTCPAddress(t *testing.T) string {
198 t.Helper()
199 listener, err := net.Listen("tcp", "127.0.0.1:0")
200 require.NoError(t, err)
201 address := listener.Addr().String()
202 require.NoError(t, listener.Close())
203 return address
204}
205
206func freeUDPAddress(t *testing.T) string {
207 t.Helper()
208 packetConn, err := net.ListenPacket("udp", "127.0.0.1:0")
209 require.NoError(t, err)
210 address := packetConn.LocalAddr().String()
211 require.NoError(t, packetConn.Close())
212 return address
213}