v3.3.0 b164fe9

traffic_test.go RawHistoryFolder

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/boundaries/out"
14	"github.com/bnema/gordon/internal/domain"
15	"github.com/bnema/gordon/internal/usecase/apptraffic"
16	servicecfg "github.com/bnema/gordon/internal/usecase/services"
17	"github.com/bnema/gordon/internal/usecase/traffic"
18)
19
20func TestTrafficRuntimeGraphKeepsSmartTCPHTTPAndTLSPassthroughRouters(t *testing.T) {
21	graph := domain.TrafficGraph{
22		EntryPoints: []domain.EntryPoint{
23			{Name: traffic.DefaultEdgeEntryPointName, Address: "127.0.0.1:8443", Protocol: domain.EntryPointProtocolSmartTCP, RawFallback: "ssh", RawFallbackTrustedCIDRs: []string{"127.0.0.0/8"}},
24		},
25		Routers: []domain.TrafficRouter{
26			{Name: "app", EntryPoint: traffic.DefaultEdgeEntryPointName, Protocol: domain.RouterProtocolHTTP, Rule: domain.TrafficRule{Host: "app.example.com"}, Service: "route:app.example.com"},
27			{Name: "tls-raw", EntryPoint: traffic.DefaultEdgeEntryPointName, Protocol: domain.RouterProtocolTLSPassthrough, Rule: domain.TrafficRule{SNI: "raw.example.com"}, Service: "network_service:raw:tls"},
28			{Name: "ssh", EntryPoint: traffic.DefaultEdgeEntryPointName, Protocol: domain.RouterProtocolTCP, Service: "network_service:ssh:ssh"},
29		},
30		Services: []domain.TrafficService{
31			{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}}},
33			{Name: "network_service:ssh:ssh", Backends: []domain.TrafficBackend{{Name: "ssh:ssh", Host: "127.0.0.1", Port: 22, Protocol: domain.NetworkProtocolTCP}}},
34		},
35	}
36
37	filtered, err := trafficRuntimeGraph(graph)
38	require.NoError(t, err)
39	assert.ElementsMatch(t, []string{traffic.DefaultEdgeEntryPointName}, entryPointNames(filtered.EntryPoints))
40	assert.ElementsMatch(t, []string{"app", "tls-raw", "ssh"}, routerNames(filtered.Routers))
41	assert.ElementsMatch(t, []string{"route:app.example.com", "network_service:raw:tls", "network_service:ssh:ssh"}, serviceNames(filtered.Services))
42}
43
44func TestTrafficRuntimeGraphHTTPEntrypointIsInvalidRatherThanSilentlyFiltered(t *testing.T) {
45	graph := domain.TrafficGraph{
46		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"}},
48		Services:    []domain.TrafficService{{Name: "route:app.example.com"}},
49	}
50
51	require.ErrorContains(t, graph.Validate(), "unsupported traffic entrypoint protocol \"http\"")
52}
53
54func TestTrafficRuntimeGraphKeepsDefaultTLSEntrypoint(t *testing.T) {
55	graph := domain.TrafficGraph{
56		EntryPoints: []domain.EntryPoint{
57			{Name: "websecure", Address: "127.0.0.1:8443", Protocol: domain.EntryPointProtocolTLSMux},
58			{Name: "postgres", Address: "127.0.0.1:15432", Protocol: domain.EntryPointProtocolTCP},
59		},
60		Routers: []domain.TrafficRouter{
61			{Name: "app-secure", EntryPoint: "websecure", Protocol: domain.RouterProtocolHTTP, Rule: domain.TrafficRule{Host: "app.example.com"}, Service: "route:app.example.com"},
62			{Name: "postgres", EntryPoint: "postgres", Protocol: domain.RouterProtocolTCP, Service: "network_service:postgres:db"},
63		},
64		Services: []domain.TrafficService{
65			{Name: "route:app.example.com"},
66			{Name: "network_service:postgres:db", Backends: []domain.TrafficBackend{{Name: "db", Host: "127.0.0.1", Port: 5432, Protocol: domain.NetworkProtocolTCP}}},
67		},
68	}
69
70	filtered, err := trafficRuntimeGraph(graph)
71	require.NoError(t, err)
72	assert.ElementsMatch(t, []string{"websecure", "postgres"}, entryPointNames(filtered.EntryPoints))
73	assert.ElementsMatch(t, []string{"app-secure", "postgres"}, routerNames(filtered.Routers))
74	assert.ElementsMatch(t, []string{"route:app.example.com", "network_service:postgres:db"}, serviceNames(filtered.Services))
75}
76
77func TestTrafficRuntimeGraphAllowsTLSPassthroughOnDefaultTLS(t *testing.T) {
78	graph := domain.TrafficGraph{
79		EntryPoints: []domain.EntryPoint{{Name: "websecure", Address: "127.0.0.1:8443", Protocol: domain.EntryPointProtocolTLSMux}},
80		Routers:     []domain.TrafficRouter{{Name: "raw", EntryPoint: "websecure", Protocol: domain.RouterProtocolTLSPassthrough, Rule: domain.TrafficRule{SNI: "raw.example.com"}, Service: "network_service:raw:tls"}},
81		Services:    []domain.TrafficService{{Name: "network_service:raw:tls", Backends: []domain.TrafficBackend{{Name: "raw:tls", Host: "127.0.0.1", Port: 443, Protocol: domain.NetworkProtocolTCP}}}},
82	}
83
84	filtered, err := trafficRuntimeGraph(graph)
85	require.NoError(t, err)
86	require.Len(t, filtered.EntryPoints, 1)
87	assert.Equal(t, "websecure", filtered.EntryPoints[0].Name)
88	require.Len(t, filtered.Routers, 1)
89	assert.Equal(t, "raw", filtered.Routers[0].Name)
90}
91
92// stubHosts builds an ACTIVE-derived host source for traffic tests.
93func stubHosts(domains ...string) *stubAppRoutes {
94	hosts := make([]out.AppHost, 0, len(domains))
95	for _, d := range domains {
96		hosts = append(hosts, out.AppHost{Host: d})
97	}
98	return &stubAppRoutes{hosts: hosts}
99}
100
101func TestApplyTrafficRuntimeConfigAppliesSmartTCPEntrypointWithRawFallbackPolicy(t *testing.T) {
102	manager := trafficadapter.NewManager()
103	defer func() { require.NoError(t, manager.Shutdown(context.Background())) }()
104	cfg := Config{
105		EntryPoints: map[string]traffic.EntryPointConfig{
106			traffic.DefaultEdgeEntryPointName: {
107				Address:                 freeTCPAddress(t),
108				Protocol:                domain.EntryPointProtocolSmartTCP,
109				RawFallback:             "ssh",
110				RawFallbackTrustedCIDRs: []string{"127.0.0.0/8"},
111			},
112		},
113		NetworkServices: []traffic.NetworkServiceConfig{{
114			Name:  "ssh",
115			Ports: []traffic.PortConfig{{Name: "ssh", Container: 22, Protocol: domain.NetworkProtocolTCP}},
116		}},
117	}
118	cfg.Traffic.TCP.Routers = []traffic.RouterConfig{{Name: "ssh", EntryPoint: traffic.DefaultEdgeEntryPointName, Service: "network_service:ssh:ssh"}}
119
120	configSvc := inmocks.NewMockConfigService(t)
121	configSvc.EXPECT().GetExternalRoutes().Return(nil)
122
123	require.NoError(t, applyTrafficRuntimeConfig(context.Background(), manager, cfg, configSvc, stubHosts("app.example.com")))
124	status := manager.Status()
125	require.Len(t, status.EntryPoints, 1)
126	assert.Equal(t, traffic.DefaultEdgeEntryPointName, status.EntryPoints[0].Name)
127	assert.True(t, status.EntryPoints[0].Active)
128	assert.Equal(t, domain.EntryPointProtocolSmartTCP, status.EntryPoints[0].Protocol)
129}
130
131func TestApplyTrafficRuntimeConfigPassesStandaloneServicesToBuilder(t *testing.T) {
132	manager := trafficadapter.NewManager()
133	defer func() { require.NoError(t, manager.Shutdown(context.Background())) }()
134	cfg := Config{
135		EntryPoints: map[string]traffic.EntryPointConfig{
136			"rust": {Address: freeUDPAddress(t), Protocol: domain.EntryPointProtocolUDP},
137		},
138		Services: []servicecfg.Config{{
139			Name:    "rust",
140			Image:   "localhost/rust:latest",
141			Enabled: true,
142			Ports:   []servicecfg.PortConfig{{Name: "game", Container: 28015, Protocol: domain.NetworkProtocolUDP, Publish: "127.0.0.1:38015"}},
143		}},
144	}
145	cfg.Traffic.UDP.Routers = []traffic.RouterConfig{{Name: "rust-game", EntryPoint: "rust", Service: "service:rust:game"}}
146
147	configSvc := inmocks.NewMockConfigService(t)
148	configSvc.EXPECT().GetExternalRoutes().Return(nil)
149
150	require.NoError(t, applyTrafficRuntimeConfig(context.Background(), manager, cfg, configSvc, stubHosts()))
151	status := manager.Status()
152	require.Len(t, status.EntryPoints, 1)
153	assert.Equal(t, "rust", status.EntryPoints[0].Name)
154	assert.True(t, status.EntryPoints[0].Active)
155}
156
157func TestApplyTrafficRuntimeConfigAppliesCustomL4Entrypoint(t *testing.T) {
158	manager := trafficadapter.NewManager()
159	defer func() { require.NoError(t, manager.Shutdown(context.Background())) }()
160	cfg := Config{
161		EntryPoints: map[string]traffic.EntryPointConfig{
162			"postgres": {Address: freeTCPAddress(t), Protocol: domain.EntryPointProtocolTCP},
163		},
164		NetworkServices: []traffic.NetworkServiceConfig{{
165			Name:  "postgres",
166			Ports: []traffic.PortConfig{{Name: "db", Container: 5432, Protocol: domain.NetworkProtocolTCP}},
167		}},
168	}
169	cfg.Traffic.TCP.Routers = []traffic.RouterConfig{{Name: "postgres", EntryPoint: "postgres", Service: "network_service:postgres:db"}}
170
171	configSvc := inmocks.NewMockConfigService(t)
172	configSvc.EXPECT().GetExternalRoutes().Return(nil)
173
174	require.NoError(t, applyTrafficRuntimeConfig(context.Background(), manager, cfg, configSvc, stubHosts()))
175	status := manager.Status()
176	require.Len(t, status.EntryPoints, 1)
177	assert.Equal(t, "postgres", status.EntryPoints[0].Name)
178	assert.True(t, status.EntryPoints[0].Active)
179}
180
181// stubL4Hosts is an ACTIVE-derived source with resolved TCP+UDP entries.
182type stubL4Hosts struct {
183	stubAppRoutes
184	entries []apptraffic.RouteEntry
185}
186
187func (s *stubL4Hosts) L4Entries() []apptraffic.RouteEntry { return s.entries }
188
189// TestApplyTrafficRuntimeConfigFusesAppL4Routes proves ACTIVE TCP+UDP
190// projections join the applied graph as validated router/service pairs
191// alongside historical routes, with exact-protocol loopback backends.
192func TestApplyTrafficRuntimeConfigFusesAppL4Routes(t *testing.T) {
193	manager := trafficadapter.NewManager()
194	defer func() { require.NoError(t, manager.Shutdown(context.Background())) }()
195	tcpAddress := freeTCPAddress(t)
196	udpAddress := freeUDPAddress(t)
197	tcpHost, tcpPort, err := domain.SplitListenerAddress(tcpAddress)
198	require.NoError(t, err)
199	udpHost, udpPort, err := domain.SplitListenerAddress(udpAddress)
200	require.NoError(t, err)
201	cfg := Config{
202		EntryPoints: map[string]traffic.EntryPointConfig{
203			"tcp": {Address: tcpAddress, Protocol: domain.EntryPointProtocolTCP},
204			"udp": {Address: udpAddress, Protocol: domain.EntryPointProtocolUDP},
205		},
206	}
207	hosts := &stubL4Hosts{
208		entries: []apptraffic.RouteEntry{
209			{
210				Kind: "tcp", RouterName: "app-game--server--tcp-9000", Entrypoint: "tcp",
211				BindIP: tcpHost, BindPort: tcpPort,
212				App: "game", Service: "server",
213				Backend: domain.AppBackend{Host: "127.0.0.1", Port: 19000, ContainerPort: 9000, ContainerID: "c-game"},
214			},
215			{
216				Kind: "udp", RouterName: "app-game--server--udp-9000", Entrypoint: "udp",
217				BindIP: udpHost, BindPort: udpPort,
218				App: "game", Service: "server",
219				Backend: domain.AppBackend{Host: "127.0.0.1", Port: 19001, ContainerPort: 9000, ContainerID: "c-game"},
220			},
221		},
222	}
223
224	configSvc := inmocks.NewMockConfigService(t)
225	configSvc.EXPECT().GetExternalRoutes().Return(nil)
226
227	require.NoError(t, applyTrafficRuntimeConfig(context.Background(), manager, cfg, configSvc, hosts))
228	status := manager.Status()
229	assert.Equal(t, "ok", status.LastReloadStatus)
230	routers := map[string]domain.TrafficRouterStatus{}
231	for _, router := range status.Routers {
232		routers[router.Name] = router
233	}
234	require.Contains(t, routers, "app-game--server--tcp-9000")
235	require.Contains(t, routers, "app-game--server--udp-9000")
236	assert.Equal(t, domain.RouterProtocolTCP, routers["app-game--server--tcp-9000"].Protocol)
237	assert.Equal(t, domain.RouterProtocolUDP, routers["app-game--server--udp-9000"].Protocol)
238	assert.True(t, routers["app-game--server--tcp-9000"].Active)
239	assert.True(t, routers["app-game--server--udp-9000"].Active)
240	services := map[string]domain.TrafficServiceStatus{}
241	for _, service := range status.Services {
242		services[service.Name] = service
243	}
244	tcpSvc, ok := services["service:game--server:tcp-9000"]
245	require.True(t, ok)
246	require.Len(t, tcpSvc.Backends, 1)
247	assert.Equal(t, "127.0.0.1", tcpSvc.Backends[0].Host)
248	assert.Equal(t, 19000, tcpSvc.Backends[0].Port)
249	assert.Equal(t, domain.NetworkProtocolTCP, tcpSvc.Backends[0].Protocol)
250	udpSvc, ok := services["service:game--server:udp-9000"]
251	require.True(t, ok)
252	require.Len(t, udpSvc.Backends, 1)
253	assert.Equal(t, "127.0.0.1", udpSvc.Backends[0].Host)
254	assert.Equal(t, 19001, udpSvc.Backends[0].Port)
255	assert.Equal(t, domain.NetworkProtocolUDP, udpSvc.Backends[0].Protocol)
256}
257
258// TestApplyTrafficRuntimeConfigRejectsMismatchedAppL4Bind proves the graph
259// stage refuses a projected bind that differs from the entrypoint listener,
260// so a mismatched declaration can never be silently widened.
261func TestApplyTrafficRuntimeConfigRejectsMismatchedAppL4Bind(t *testing.T) {
262	manager := trafficadapter.NewManager()
263	defer func() { require.NoError(t, manager.Shutdown(context.Background())) }()
264	tcpAddress := freeTCPAddress(t)
265	cfg := Config{
266		EntryPoints: map[string]traffic.EntryPointConfig{
267			"tcp": {Address: tcpAddress, Protocol: domain.EntryPointProtocolTCP},
268		},
269	}
270	hosts := &stubL4Hosts{
271		entries: []apptraffic.RouteEntry{{
272			Kind: "tcp", RouterName: "app-game--server--tcp-9000", Entrypoint: "tcp",
273			BindIP: "127.0.0.1", BindPort: 1,
274			App: "game", Service: "server",
275			Backend: domain.AppBackend{Host: "127.0.0.1", Port: 19000, ContainerPort: 9000, ContainerID: "c-game"},
276		}},
277	}
278	configSvc := inmocks.NewMockConfigService(t)
279	configSvc.EXPECT().GetExternalRoutes().Return(nil)
280
281	err := applyTrafficRuntimeConfig(context.Background(), manager, cfg, configSvc, hosts)
282	require.Error(t, err)
283}
284
285func entryPointNames(entries []domain.EntryPoint) []string {
286	names := make([]string, 0, len(entries))
287	for _, entry := range entries {
288		names = append(names, entry.Name)
289	}
290	return names
291}
292
293func routerNames(routers []domain.TrafficRouter) []string {
294	names := make([]string, 0, len(routers))
295	for _, router := range routers {
296		names = append(names, router.Name)
297	}
298	return names
299}
300
301func serviceNames(services []domain.TrafficService) []string {
302	names := make([]string, 0, len(services))
303	for _, service := range services {
304		names = append(names, service.Name)
305	}
306	return names
307}
308
309func freeTCPAddress(t *testing.T) string {
310	t.Helper()
311	listener, err := net.Listen("tcp", "127.0.0.1:0")
312	require.NoError(t, err)
313	address := listener.Addr().String()
314	require.NoError(t, listener.Close())
315	return address
316}
317
318func freeUDPAddress(t *testing.T) string {
319	t.Helper()
320	packetConn, err := net.ListenPacket("udp", "127.0.0.1:0")
321	require.NoError(t, err)
322	address := packetConn.LocalAddr().String()
323	require.NoError(t, packetConn.Close())
324	return address
325}

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