builder_test.go RawHistoryFolder

1package traffic
2
3import (
4	"testing"
5	"time"
6
7	"github.com/stretchr/testify/require"
8
9	"github.com/bnema/gordon/internal/domain"
10)
11
12func TestBuildSmartTCPRoutesDefaultToEdge(t *testing.T) {
13	graph, err := Build(Input{
14		EntryPoints: map[string]EntryPointConfig{"edge": {Address: ":443", Protocol: domain.EntryPointProtocolSmartTCP}},
15		Routes:      []domain.Route{{Domain: "app.example.com"}},
16	})
17	require.NoError(t, err)
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}
20
21func 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
30func 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
42func 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
53func 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
62func TestBuildRawFallbackConfigMapsToEntryPoint(t *testing.T) {
63	graph, err := Build(Input{
64		EntryPoints: map[string]EntryPointConfig{"edge": {
65			Address:                 ":443",
66			Protocol:                domain.EntryPointProtocolSmartTCP,
67			RawFallback:             "ssh-fallback",
68			RawFallbackTrustedCIDRs: []string{"100.64.0.0/10"},
69			AllowPublicRawFallback:  true,
70		}},
71		Traffic:         Config{TCP: TCPConfig{Routers: []RouterConfig{{Name: "ssh-fallback", EntryPoint: "edge", Service: "network_service:ssh:ssh"}}}},
72		NetworkServices: []NetworkServiceConfig{{Name: "ssh", Ports: []PortConfig{{Name: "ssh", Container: 22, Protocol: domain.NetworkProtocolTCP}}}},
73	})
74	require.NoError(t, err)
75	require.Equal(t, []domain.EntryPoint{{
76		Name:                    "edge",
77		Address:                 ":443",
78		Protocol:                domain.EntryPointProtocolSmartTCP,
79		RawFallback:             "ssh-fallback",
80		RawFallbackTrustedCIDRs: []string{"100.64.0.0/10"},
81		AllowPublicRawFallback:  true,
82	}}, graph.EntryPoints)
83}
84
85func TestBuildSmartTCPRoutesRequireCompatibleEdge(t *testing.T) {
86	_, err := Build(Input{Routes: []domain.Route{{Domain: "app.example.com"}}})
87	require.ErrorContains(t, err, "entrypoint")
88}
89
90func TestBuildDefaultsAndHTTPRoutes(t *testing.T) {
91	graph, err := Build(Input{
92		EntryPoints:    map[string]EntryPointConfig{"edge": {Address: ":443", Protocol: domain.EntryPointProtocolSmartTCP}},
93		Routes:         []domain.Route{{Domain: "app.example.com"}, {Domain: "secure.example.com", HTTPS: true}},
94		ExternalRoutes: map[string]string{"ext.example.com": "backend.local:8080"},
95	})
96	require.NoError(t, err)
97
98	require.Equal(t, []domain.EntryPoint{{Name: "edge", Address: ":443", Protocol: domain.EntryPointProtocolSmartTCP}}, graph.EntryPoints)
99	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"})
100	require.Contains(t, graph.Routers, domain.TrafficRouter{Name: "route:secure.example.com", EntryPoint: "edge", Protocol: domain.RouterProtocolHTTP, Rule: domain.TrafficRule{Host: "secure.example.com"}, Service: "route:secure.example.com"})
101	require.Contains(t, graph.Routers, domain.TrafficRouter{Name: "external_route:ext.example.com", EntryPoint: "edge", Protocol: domain.RouterProtocolHTTP, Rule: domain.TrafficRule{Host: "ext.example.com"}, Service: "external_route:ext.example.com"})
102	require.Contains(t, graph.Services, domain.TrafficService{Name: "external_route:ext.example.com", Backends: []domain.TrafficBackend{{Name: "ext.example.com", Host: "backend.local", Port: 8080, Protocol: domain.NetworkProtocolTCP}}})
103}
104
105func TestBuildCustomEntryPointTrustedCIDRs(t *testing.T) {
106	graph, err := Build(Input{EntryPoints: map[string]EntryPointConfig{"tcp-public": {Address: ":5432", Protocol: domain.EntryPointProtocolTCP, TrustedCIDRs: []string{"10.0.0.0/8"}}}})
107	require.NoError(t, err)
108	require.Equal(t, []domain.EntryPoint{{Name: "tcp-public", Address: ":5432", Protocol: domain.EntryPointProtocolTCP, TrustedCIDRs: []string{"10.0.0.0/8"}}}, graph.EntryPoints)
109}
110
111func TestBuildExplicitTCPUDPTLSRouterResolution(t *testing.T) {
112	graph, err := Build(Input{
113		EntryPoints: map[string]EntryPointConfig{
114			"tcp-db":  {Address: ":5432", Protocol: domain.EntryPointProtocolTCP},
115			"udp-dns": {Address: ":5353", Protocol: domain.EntryPointProtocolUDP},
116			"tls-db":  {Address: ":15432", Protocol: domain.EntryPointProtocolTLSMux},
117		},
118		Traffic: Config{
119			TCP: TCPConfig{Routers: []RouterConfig{{Name: "db", EntryPoint: "tcp-db", Service: "network_service:postgres:sql"}}},
120			UDP: UDPConfig{Routers: []RouterConfig{{Name: "dns", EntryPoint: "udp-dns", Service: "network_service:coredns:dns"}}},
121			TLS: TLSConfig{Routers: []RouterConfig{{Name: "tlsdb", EntryPoint: "tls-db", SNI: "db.example.com", Service: "network_service:postgres:sql"}}},
122		},
123		NetworkServices: []NetworkServiceConfig{
124			{Name: "postgres", Ports: []PortConfig{{Name: "sql", Container: 5432, Protocol: domain.NetworkProtocolTCP}}},
125			{Name: "coredns", Ports: []PortConfig{{Name: "dns", Container: 53, Protocol: domain.NetworkProtocolUDP}}},
126		},
127	})
128	require.NoError(t, err)
129	require.Contains(t, graph.Routers, domain.TrafficRouter{Name: "db", EntryPoint: "tcp-db", Protocol: domain.RouterProtocolTCP, Service: "network_service:postgres:sql"})
130	require.Contains(t, graph.Routers, domain.TrafficRouter{Name: "dns", EntryPoint: "udp-dns", Protocol: domain.RouterProtocolUDP, Service: "network_service:coredns:dns"})
131	require.Contains(t, graph.Routers, domain.TrafficRouter{Name: "tlsdb", EntryPoint: "tls-db", Protocol: domain.RouterProtocolTLSPassthrough, Rule: domain.TrafficRule{SNI: "db.example.com"}, Service: "network_service:postgres:sql"})
132	require.Contains(t, graph.Services, domain.TrafficService{Name: "network_service:postgres:sql", Backends: []domain.TrafficBackend{{Name: "postgres:sql", Host: "postgres", Port: 5432, Protocol: domain.NetworkProtocolTCP}}})
133}
134
135func TestBuildRejectsStaticServiceRef(t *testing.T) {
136	_, err := Build(Input{
137		EntryPoints: map[string]EntryPointConfig{"tcp": {Address: ":1234", Protocol: domain.EntryPointProtocolTCP}},
138		Traffic:     Config{TCP: TCPConfig{Routers: []RouterConfig{{Name: "bad", EntryPoint: "tcp", Service: "static:legacy"}}}},
139	})
140	require.ErrorContains(t, err, "static")
141}
142
143func TestBuildRejectsTLSHTTPRouteConflict(t *testing.T) {
144	_, err := Build(Input{
145		EntryPoints:     map[string]EntryPointConfig{"edge": {Address: ":443", Protocol: domain.EntryPointProtocolSmartTCP}},
146		Routes:          []domain.Route{{Domain: "app.example.com", HTTPS: true}},
147		Traffic:         Config{TLS: TLSConfig{Routers: []RouterConfig{{Name: "passthrough", EntryPoint: "edge", SNI: "app.example.com", Service: "network_service:app:https"}}}},
148		NetworkServices: []NetworkServiceConfig{{Name: "app", Ports: []PortConfig{{Name: "https", Container: 443, Protocol: domain.NetworkProtocolTCP}}}},
149	})
150	require.ErrorContains(t, err, "http host conflicts")
151}
152
153func TestBuildOptionsParsing(t *testing.T) {
154	graph, err := Build(Input{Traffic: Config{TCP: TCPConfig{DialTimeout: "1s", IdleTimeout: "2m", DrainTimeout: "3s", MaxConnections: 12}, UDP: UDPConfig{IdleTimeout: "4s", DrainTimeout: "5s", MaxSessions: 34}}})
155	require.NoError(t, err)
156	require.Equal(t, time.Second, graph.Options.TCP.DialTimeout)
157	require.Equal(t, 2*time.Minute, graph.Options.TCP.IdleTimeout)
158	require.Equal(t, 3*time.Second, graph.Options.TCP.DrainTimeout)
159	require.Equal(t, 12, graph.Options.TCP.MaxConnections)
160	require.Equal(t, 4*time.Second, graph.Options.UDP.IdleTimeout)
161	require.Equal(t, 5*time.Second, graph.Options.UDP.DrainTimeout)
162	require.Equal(t, 34, graph.Options.UDP.MaxSessions)
163
164	defaults, err := Build(Input{})
165	require.NoError(t, err)
166	require.Equal(t, 10*time.Second, defaults.Options.TCP.DialTimeout)
167	require.Equal(t, 5*time.Minute, defaults.Options.TCP.IdleTimeout)
168	require.Equal(t, 30*time.Second, defaults.Options.TCP.DrainTimeout)
169	require.Equal(t, 30*time.Second, defaults.Options.UDP.IdleTimeout)
170	require.Equal(t, 30*time.Second, defaults.Options.UDP.DrainTimeout)
171
172	_, err = Build(Input{Traffic: Config{TCP: TCPConfig{DialTimeout: "bogus"}}})
173	require.ErrorContains(t, err, "traffic.tcp.dial_timeout")
174}
175
176func TestBuildRejectsAmbiguousNetworkServices(t *testing.T) {
177	t.Run("duplicate service name", func(t *testing.T) {
178		_, err := Build(Input{NetworkServices: []NetworkServiceConfig{
179			{Name: "db", Ports: []PortConfig{{Name: "sql", Container: 5432, Protocol: domain.NetworkProtocolTCP}}},
180			{Name: "db", Ports: []PortConfig{{Name: "sql", Container: 5433, Protocol: domain.NetworkProtocolTCP}}},
181		}})
182		require.ErrorContains(t, err, "duplicate network service")
183	})
184
185	t.Run("duplicate port name", func(t *testing.T) {
186		_, err := Build(Input{NetworkServices: []NetworkServiceConfig{
187			{Name: "db", Ports: []PortConfig{
188				{Name: "sql", Container: 5432, Protocol: domain.NetworkProtocolTCP},
189				{Name: "sql", Container: 5433, Protocol: domain.NetworkProtocolTCP},
190			}},
191		}})
192		require.ErrorContains(t, err, "duplicate port")
193	})
194
195	t.Run("unused invalid port rejected", func(t *testing.T) {
196		_, err := Build(Input{NetworkServices: []NetworkServiceConfig{
197			{Name: "db", Ports: []PortConfig{{Name: "unused", Container: 0, Protocol: domain.NetworkProtocolTCP}}},
198		}})
199		require.ErrorContains(t, err, "invalid container port")
200	})
201
202	t.Run("unused invalid protocol rejected", func(t *testing.T) {
203		_, err := Build(Input{NetworkServices: []NetworkServiceConfig{
204			{Name: "db", Ports: []PortConfig{{Name: "unused", Container: 5432, Protocol: domain.NetworkProtocol("sctp")}}},
205		}})
206		require.ErrorContains(t, err, "invalid protocol")
207	})
208}
209
210func TestBuildResolvesStandaloneServiceBackend(t *testing.T) {
211	graph, err := Build(Input{
212		EntryPoints: map[string]EntryPointConfig{"udp": {Address: ":28015", Protocol: domain.EntryPointProtocolUDP}},
213		Traffic:     Config{UDP: UDPConfig{Routers: []RouterConfig{{Name: "rust-game", EntryPoint: "udp", Service: "service:rust:game"}}}},
214		Services: []domain.StandaloneService{{
215			Name:    "rust",
216			Image:   "localhost/rust:latest",
217			Enabled: true,
218			Ports:   []domain.StandaloneServicePort{{Name: "game", Container: 28015, Protocol: domain.NetworkProtocolUDP, Publish: "127.0.0.1:38015"}},
219		}},
220	})
221	require.NoError(t, err)
222	require.Contains(t, graph.Services, domain.TrafficService{Name: "service:rust:game", Backends: []domain.TrafficBackend{{Name: "rust:game", Host: "127.0.0.1", Port: 38015, Protocol: domain.NetworkProtocolUDP}}})
223}
224
225func TestBuildRejectsInvalidStandaloneServiceRefs(t *testing.T) {
226	base := Input{
227		EntryPoints: map[string]EntryPointConfig{"tcp": {Address: ":1234", Protocol: domain.EntryPointProtocolTCP}},
228		Services: []domain.StandaloneService{{
229			Name:    "rust",
230			Image:   "localhost/rust:latest",
231			Enabled: true,
232			Ports:   []domain.StandaloneServicePort{{Name: "game", Container: 28015, Protocol: domain.NetworkProtocolUDP, Publish: "127.0.0.1:38015"}, {Name: "rcon", Container: 28016, Protocol: domain.NetworkProtocolTCP, Publish: "127.0.0.1:38016", Private: true, TrustedCIDRs: []string{"100.64.0.0/10"}}},
233		}},
234	}
235
236	t.Run("unknown service", func(t *testing.T) {
237		input := base
238		input.Traffic = Config{TCP: TCPConfig{Routers: []RouterConfig{{Name: "bad", EntryPoint: "tcp", Service: "service:missing:rcon"}}}}
239		_, err := Build(input)
240		require.ErrorContains(t, err, "router \"bad\": unknown service \"missing\"")
241	})
242
243	t.Run("unknown port", func(t *testing.T) {
244		input := base
245		input.Traffic = Config{TCP: TCPConfig{Routers: []RouterConfig{{Name: "bad", EntryPoint: "tcp", Service: "service:rust:missing"}}}}
246		_, err := Build(input)
247		require.ErrorContains(t, err, "unknown port \"missing\" on service \"rust\"")
248	})
249
250	t.Run("disabled service", func(t *testing.T) {
251		input := base
252		input.Services = append([]domain.StandaloneService(nil), base.Services...)
253		input.Services[0].Enabled = false
254		input.Traffic = Config{TCP: TCPConfig{Routers: []RouterConfig{{Name: "bad", EntryPoint: "tcp", Service: "service:rust:rcon"}}}}
255		_, err := Build(input)
256		require.ErrorContains(t, err, "unknown service \"rust\"")
257	})
258
259	t.Run("protocol mismatch", func(t *testing.T) {
260		input := base
261		input.Traffic = Config{TCP: TCPConfig{Routers: []RouterConfig{{Name: "bad", EntryPoint: "tcp", Service: "service:rust:game"}}}}
262		_, err := Build(input)
263		require.ErrorContains(t, err, "service \"rust\" port \"game\" protocol udp does not match router protocol tcp")
264	})
265
266	t.Run("missing publish", func(t *testing.T) {
267		input := base
268		input.EntryPoints = map[string]EntryPointConfig{"tcp": {Address: ":1234", Protocol: domain.EntryPointProtocolTCP, TrustedCIDRs: []string{"100.64.0.0/10"}}}
269		input.Services = append([]domain.StandaloneService(nil), base.Services...)
270		input.Services[0].Ports = append([]domain.StandaloneServicePort(nil), base.Services[0].Ports...)
271		input.Services[0].Ports[1].Publish = ""
272		input.Traffic = Config{TCP: TCPConfig{Routers: []RouterConfig{{Name: "bad", EntryPoint: "tcp", Service: "service:rust:rcon"}}}}
273		_, err := Build(input)
274		require.ErrorContains(t, err, "service \"rust\" port \"rcon\" publish address")
275	})
276
277	t.Run("invalid publish", func(t *testing.T) {
278		input := base
279		input.EntryPoints = map[string]EntryPointConfig{"tcp": {Address: ":1234", Protocol: domain.EntryPointProtocolTCP, TrustedCIDRs: []string{"100.64.0.0/10"}}}
280		input.Services = append([]domain.StandaloneService(nil), base.Services...)
281		input.Services[0].Ports = append([]domain.StandaloneServicePort(nil), base.Services[0].Ports...)
282		input.Services[0].Ports[1].Publish = "127.0.0.1:notaport"
283		input.Traffic = Config{TCP: TCPConfig{Routers: []RouterConfig{{Name: "bad", EntryPoint: "tcp", Service: "service:rust:rcon"}}}}
284		_, err := Build(input)
285		require.ErrorContains(t, err, "publish address")
286	})
287}
288
289func TestBuildWrapsStandaloneServiceValidationErrorsWithServiceName(t *testing.T) {
290	_, err := Build(Input{
291		EntryPoints: map[string]EntryPointConfig{"tcp": {Address: ":1234", Protocol: domain.EntryPointProtocolTCP}},
292		Services: []domain.StandaloneService{{
293			Name:    "rust",
294			Image:   "localhost/rust:latest",
295			Enabled: true,
296			Ports:   []domain.StandaloneServicePort{{Name: " admin ", Container: 28016, Protocol: domain.NetworkProtocolTCP}},
297		}},
298	})
299
300	require.ErrorContains(t, err, "standalone service \"rust\"")
301	require.ErrorContains(t, err, "leading or trailing whitespace")
302}
303
304func TestBuildEnforcesPrivateStandaloneServicePortRouting(t *testing.T) {
305	base := func() Input {
306		return Input{
307			EntryPoints: map[string]EntryPointConfig{"tcp": {Address: ":28016", Protocol: domain.EntryPointProtocolTCP, TrustedCIDRs: []string{"100.64.0.0/10"}}},
308			Traffic:     Config{TCP: TCPConfig{Routers: []RouterConfig{{Name: "rust-rcon", EntryPoint: "tcp", Service: "service:rust:rcon"}}}},
309			Services: []domain.StandaloneService{{
310				Name:    "rust",
311				Image:   "localhost/rust:latest",
312				Enabled: true,
313				Ports:   []domain.StandaloneServicePort{{Name: "rcon", Container: 28016, Protocol: domain.NetworkProtocolTCP, Publish: "127.0.0.1:38016", TrustedCIDRs: []string{"100.64.0.0/10"}}},
314			}},
315		}
316	}
317
318	t.Run("rcon defaults private and allows exact trusted cidrs", func(t *testing.T) {
319		_, err := Build(base())
320		require.NoError(t, err)
321	})
322
323	t.Run("private port rejects service port without trusted cidrs", func(t *testing.T) {
324		input := base()
325		input.Services[0].Ports[0].TrustedCIDRs = nil
326		_, err := Build(input)
327		require.ErrorContains(t, err, "requires non-empty service port trusted_cidrs")
328	})
329
330	t.Run("rcon rejects both service and entrypoint without trusted cidrs", func(t *testing.T) {
331		input := base()
332		input.EntryPoints = map[string]EntryPointConfig{"tcp": {Address: ":28016", Protocol: domain.EntryPointProtocolTCP}}
333		input.Services[0].Ports[0].TrustedCIDRs = nil
334		_, err := Build(input)
335		require.ErrorContains(t, err, "requires non-empty service port trusted_cidrs")
336	})
337
338	t.Run("explicit private non-rcon rejects service port without trusted cidrs", func(t *testing.T) {
339		input := base()
340		input.Services[0].Ports[0].Name = "admin"
341		input.Services[0].Ports[0].Private = true
342		input.Services[0].Ports[0].TrustedCIDRs = nil
343		input.Traffic.TCP.Routers[0].Service = "service:rust:admin"
344		_, err := Build(input)
345		require.ErrorContains(t, err, "requires non-empty service port trusted_cidrs")
346	})
347
348	t.Run("private port rejects entrypoint without trusted cidrs", func(t *testing.T) {
349		input := base()
350		input.EntryPoints = map[string]EntryPointConfig{"tcp": {Address: ":28016", Protocol: domain.EntryPointProtocolTCP}}
351		_, err := Build(input)
352		require.ErrorContains(t, err, "entrypoint \"tcp\" requires non-empty trusted_cidrs")
353	})
354
355	t.Run("private port rejects non-matching trusted cidrs", func(t *testing.T) {
356		input := base()
357		input.EntryPoints = map[string]EntryPointConfig{"tcp": {Address: ":28016", Protocol: domain.EntryPointProtocolTCP, TrustedCIDRs: []string{"10.0.0.0/8"}}}
358		_, err := Build(input)
359		require.ErrorContains(t, err, "must exactly match")
360	})
361
362	t.Run("private port compares canonical trusted cidrs", func(t *testing.T) {
363		input := base()
364		input.EntryPoints = map[string]EntryPointConfig{"tcp": {Address: ":28016", Protocol: domain.EntryPointProtocolTCP, TrustedCIDRs: []string{"100.64.0.1/10"}}}
365		_, err := Build(input)
366		require.NoError(t, err)
367	})
368
369	t.Run("explicit public rcon is allowed without trusted cidrs", func(t *testing.T) {
370		input := base()
371		input.EntryPoints = map[string]EntryPointConfig{"tcp": {Address: ":28016", Protocol: domain.EntryPointProtocolTCP}}
372		input.Services[0].Ports[0].Public = true
373		input.Services[0].Ports[0].TrustedCIDRs = nil
374		_, err := Build(input)
375		require.NoError(t, err)
376	})
377
378	t.Run("explicit public non-rcon is allowed without trusted cidrs", func(t *testing.T) {
379		input := base()
380		input.EntryPoints = map[string]EntryPointConfig{"tcp": {Address: ":28016", Protocol: domain.EntryPointProtocolTCP}}
381		input.Services[0].Ports[0].Name = "admin"
382		input.Services[0].Ports[0].Private = false
383		input.Services[0].Ports[0].Public = true
384		input.Services[0].Ports[0].TrustedCIDRs = nil
385		input.Traffic.TCP.Routers[0].Service = "service:rust:admin"
386		_, err := Build(input)
387		require.NoError(t, err)
388	})
389}
390
391func TestBuildRejectsInvalidServiceRefs(t *testing.T) {
392	base := Input{
393		EntryPoints:     map[string]EntryPointConfig{"tcp": {Address: ":1234", Protocol: domain.EntryPointProtocolTCP}},
394		NetworkServices: []NetworkServiceConfig{{Name: "svc", Ports: []PortConfig{{Name: "udp", Container: 53, Protocol: domain.NetworkProtocolUDP}}}},
395	}
396
397	t.Run("unknown service", func(t *testing.T) {
398		input := base
399		input.Traffic = Config{TCP: TCPConfig{Routers: []RouterConfig{{Name: "bad", EntryPoint: "tcp", Service: "network_service:missing:tcp"}}}}
400		_, err := Build(input)
401		require.ErrorContains(t, err, "unknown network service")
402	})
403
404	t.Run("unknown port", func(t *testing.T) {
405		input := base
406		input.Traffic = Config{TCP: TCPConfig{Routers: []RouterConfig{{Name: "bad", EntryPoint: "tcp", Service: "network_service:svc:missing"}}}}
407		_, err := Build(input)
408		require.ErrorContains(t, err, "unknown port")
409	})
410
411	t.Run("protocol mismatch", func(t *testing.T) {
412		input := base
413		input.Traffic = Config{TCP: TCPConfig{Routers: []RouterConfig{{Name: "bad", EntryPoint: "tcp", Service: "network_service:svc:udp"}}}}
414		_, err := Build(input)
415		require.ErrorContains(t, err, "does not match")
416	})
417}

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