v3.0.0 f19f453

traffic.go RawHistoryFolder

1package app
2
3import (
4	"context"
5	"fmt"
6
7	trafficadapter "github.com/bnema/gordon/internal/adapters/in/traffic"
8	"github.com/bnema/gordon/internal/boundaries/in"
9	"github.com/bnema/gordon/internal/boundaries/out"
10	"github.com/bnema/gordon/internal/domain"
11	"github.com/bnema/gordon/internal/usecase/apptraffic"
12	servicecfg "github.com/bnema/gordon/internal/usecase/services"
13	trafficbuilder "github.com/bnema/gordon/internal/usecase/traffic"
14)
15
16// appHostRoutes adapts ACTIVE-derived hosts to the traffic builder's
17// route input: host-only entries, Image explicitly unused (no app
18// meaning). Stopped-intent and unresolved hosts never reach the graph.
19type appHostRoutes interface {
20	AppHosts() []out.AppHost
21	L4Entries() []apptraffic.RouteEntry
22}
23
24func applyTrafficRuntimeConfig(ctx context.Context, manager *trafficadapter.Manager, cfg Config, configSvc in.ConfigService, hosts appHostRoutes) error {
25	if manager == nil || configSvc == nil {
26		return nil
27	}
28	standaloneServices, err := servicecfg.ToDomain(cfg.Services)
29	if err != nil {
30		return fmt.Errorf("convert standalone service config: %w", err)
31	}
32	graph, err := trafficbuilder.Build(trafficbuilder.Input{
33		EntryPoints:     cfg.EntryPoints,
34		Traffic:         cfg.Traffic,
35		Routes:          appHostDomainRoutes(hosts),
36		ExternalRoutes:  configSvc.GetExternalRoutes(),
37		NetworkServices: cfg.NetworkServices,
38		Services:        standaloneServices,
39	})
40	if err != nil {
41		return fmt.Errorf("build traffic graph: %w", err)
42	}
43	if err := addAppL4Routes(&graph, hosts); err != nil {
44		return fmt.Errorf("add app L4 routes: %w", err)
45	}
46	owned, err := trafficRuntimeGraph(graph)
47	if err != nil {
48		return fmt.Errorf("filter traffic graph for runtime ownership: %w", err)
49	}
50	if err := manager.Apply(ctx, &owned); err != nil {
51		return fmt.Errorf("apply traffic graph: %w", err)
52	}
53	return nil
54}
55
56func addAppL4Routes(graph *domain.TrafficGraph, routes appHostRoutes) error {
57	if routes == nil {
58		return nil
59	}
60	existing := map[string]struct{}{}
61	for _, router := range graph.Routers {
62		existing["router:"+router.Name] = struct{}{}
63	}
64	for _, service := range graph.Services {
65		existing["service:"+service.Name] = struct{}{}
66	}
67	entrypointAddress := make(map[string]string, len(graph.EntryPoints))
68	for _, entryPoint := range graph.EntryPoints {
69		entrypointAddress[entryPoint.Name] = entryPoint.Address
70	}
71	for _, entry := range routes.L4Entries() {
72		protocol := domain.RouterProtocolTCP
73		backendProtocol := domain.NetworkProtocolTCP
74		if entry.Kind == "udp" {
75			protocol = domain.RouterProtocolUDP
76			backendProtocol = domain.NetworkProtocolUDP
77		}
78		if !entry.Backend.Resolved() {
79			return fmt.Errorf("app L4 route %q has no resolved backend", entry.RouterName)
80		}
81		// The declared bind must be the listener this router actually
82		// attaches to: the runtime binds the entrypoint address, so a
83		// narrower declaration must have been rejected earlier, never
84		// silently widened here.
85		address, ok := entrypointAddress[entry.Entrypoint]
86		if !ok {
87			return fmt.Errorf("app L4 route %q references entrypoint %q absent from the traffic graph", entry.RouterName, entry.Entrypoint)
88		}
89		listenerHost, listenerPort, err := domain.SplitListenerAddress(address)
90		if err != nil {
91			return fmt.Errorf("app L4 entrypoint %q has invalid listener address %q: %w", entry.Entrypoint, address, err)
92		}
93		if listenerPort != entry.BindPort || domain.CanonicalBindHost(listenerHost) != domain.CanonicalBindHost(entry.BindIP) {
94			return fmt.Errorf(
95				"app L4 route %q declares bind %s:%d but entrypoint %q binds %s",
96				entry.RouterName, entry.BindIP, entry.BindPort, entry.Entrypoint, address,
97			)
98		}
99		// service:<app>--<service>:<kind>-<port> satisfies the
100		// service-ref grammar (no colon in the port name) and stays
101		// unique per interface port. Historical builder services
102		// never collide: they use route:/external_route:/
103		// network_service: kinds or standalone names.
104		serviceName := fmt.Sprintf("service:%s--%s:%s-%d", entry.App, entry.Service, entry.Kind, entry.Backend.ContainerPort)
105		if _, ok := existing["router:"+entry.RouterName]; ok {
106			return fmt.Errorf("app L4 router %q collides with a configured route", entry.RouterName)
107		}
108		if _, ok := existing["service:"+serviceName]; ok {
109			return fmt.Errorf("app L4 service %q collides with a configured service", serviceName)
110		}
111		existing["router:"+entry.RouterName] = struct{}{}
112		existing["service:"+serviceName] = struct{}{}
113		graph.Routers = append(graph.Routers, domain.TrafficRouter{
114			Name: entry.RouterName, EntryPoint: entry.Entrypoint,
115			Protocol: protocol, Service: serviceName,
116		})
117		graph.Services = append(graph.Services, domain.TrafficService{
118			Name: serviceName,
119			Backends: []domain.TrafficBackend{{
120				Name: entry.App + "--" + entry.Service,
121				Host: entry.Backend.Host, Port: entry.Backend.Port, Protocol: backendProtocol,
122			}},
123		})
124	}
125	return graph.Validate()
126}
127
128// appHostDomainRoutes maps served hosts to builder route inputs.
129func appHostDomainRoutes(hosts appHostRoutes) []domain.Route {
130	if hosts == nil {
131		return nil
132	}
133	appHosts := hosts.AppHosts()
134	routes := make([]domain.Route, 0, len(appHosts))
135	for _, h := range appHosts {
136		routes = append(routes, domain.Route{Domain: h.Host})
137	}
138	return routes
139}
140
141func trafficRuntimeGraph(graph domain.TrafficGraph) (domain.TrafficGraph, error) {
142	ownedEntryPoints := map[string]struct{}{}
143	filtered := domain.TrafficGraph{Options: graph.Options}
144	for _, entryPoint := range graph.EntryPoints {
145		if trafficManagerOwnsEntryPoint(entryPoint) {
146			filtered.EntryPoints = append(filtered.EntryPoints, entryPoint)
147			ownedEntryPoints[entryPoint.Name] = struct{}{}
148		}
149	}
150
151	usedServices := map[string]struct{}{}
152	for _, router := range graph.Routers {
153		if _, ok := ownedEntryPoints[router.EntryPoint]; !ok {
154			if trafficManagerRouterProtocol(router.Protocol) {
155				return domain.TrafficGraph{}, fmt.Errorf("traffic router %q targets entrypoint %q, which is not owned by the traffic manager", router.Name, router.EntryPoint)
156			}
157			continue
158		}
159		filtered.Routers = append(filtered.Routers, router)
160		usedServices[router.Service] = struct{}{}
161	}
162	for _, service := range graph.Services {
163		if _, ok := usedServices[service.Name]; ok {
164			filtered.Services = append(filtered.Services, service)
165		}
166	}
167	return filtered, nil
168}
169
170func trafficManagerRouterProtocol(protocol domain.RouterProtocol) bool {
171	switch protocol {
172	case domain.RouterProtocolTCP, domain.RouterProtocolUDP, domain.RouterProtocolTLSPassthrough:
173		return true
174	default:
175		return false
176	}
177}
178
179func trafficManagerOwnsEntryPoint(entryPoint domain.EntryPoint) bool {
180	switch entryPoint.Protocol {
181	case domain.EntryPointProtocolTLSMux, domain.EntryPointProtocolSmartTCP, domain.EntryPointProtocolTCP, domain.EntryPointProtocolUDP:
182		return true
183	default:
184		return false
185	}
186}

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