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}