v3.4.0 593c1c3

backend_binds.go RawHistoryFolder

1package deployment
2
3import (
4	"context"
5	"fmt"
6	"sort"
7
8	"github.com/bnema/gordon/internal/domain"
9)
10
11// backendPorts collects every interface container port for loopback
12// publication: public HTTP + TCP interfaces on tcp plus UDP interfaces on
13// udp, plus an explicit TCP readiness port. Internal HTTP ports are
14// excluded: they are reached only over the private network and must keep
15// no host binding. Each publish is on 127.0.0.1 ephemeral (never public).
16// Deduplicated by (protocol, container port).
17func backendPorts(spec domain.AppService) []domain.ContainerBackendPort {
18	seen := map[domain.ContainerBackendPort]struct{}{}
19	var ports []domain.ContainerBackendPort
20	add := func(port int, protocol domain.NetworkProtocol) {
21		if port <= 0 {
22			return
23		}
24		key := domain.ContainerBackendPort{ContainerPort: port, Protocol: protocol}
25		if _, ok := seen[key]; !ok {
26			seen[key] = struct{}{}
27			ports = append(ports, key)
28		}
29	}
30	for _, h := range spec.HTTP {
31		if !h.IsPublic() {
32			continue
33		}
34		add(h.Port, domain.NetworkProtocolTCP)
35	}
36	for _, t := range spec.TCP {
37		add(t.Port, domain.NetworkProtocolTCP)
38	}
39	for _, u := range spec.UDP {
40		add(u.Port, domain.NetworkProtocolUDP)
41	}
42	// Readiness probes are TCP-only (validation rejects UDP-only
43	// services with tcp/http readiness); the readiness port matches a
44	// declared TCP container port. An internal-only port is never
45	// published, so readiness metadata cannot create a host binding for
46	// it.
47	if spec.Readiness.Port > 0 && !spec.InternallyOnlyPort(spec.Readiness.Port) {
48		add(spec.Readiness.Port, domain.NetworkProtocolTCP)
49	}
50	sort.Slice(ports, func(i, j int) bool {
51		if ports[i].Protocol != ports[j].Protocol {
52			return ports[i].Protocol < ports[j].Protocol
53		}
54		return ports[i].ContainerPort < ports[j].ContainerPort
55	})
56	return ports
57}
58
59// backendPublishes maps backend ports to 127.0.0.1 ephemeral publishes.
60func backendPublishes(spec domain.AppService) []domain.ContainerPortPublish {
61	ports := backendPorts(spec)
62	publishes := make([]domain.ContainerPortPublish, 0, len(ports))
63	for _, port := range ports {
64		publishes = append(publishes, domain.ContainerPortPublish{
65			HostIP:        "127.0.0.1",
66			HostPort:      0,
67			ContainerPort: port.ContainerPort,
68			Protocol:      port.Protocol,
69		})
70	}
71	return publishes
72}
73
74// readBackendBinds resolves each published container port to its
75// 127.0.0.1 host port and registers the Gordon-generated claims
76// (owner gordon-backend) in the global checkpoint. Missing binds fail:
77// an unbound backend can serve neither readiness nor proxy traffic.
78// A registration conflict removes the CANDIDATE container and fails —
79// callers must only pass freshly created candidates here, never an
80// existing ACTIVE container (see inspectBackendBinds).
81func (s *Service) readBackendBinds(ctx context.Context, app, service, containerID string, ports []domain.ContainerBackendPort) (map[int]int, map[int]int, error) {
82	if len(ports) == 0 {
83		return map[int]int{}, map[int]int{}, nil
84	}
85	observed, err := s.deps.Runtime.GetContainerBackendBinds(ctx, containerID, ports)
86	if err != nil {
87		return nil, nil, fmt.Errorf("deployment: backend binds: %w", err)
88	}
89	binds, udpBinds := splitBackendBinds(observed)
90	claims := backendClaims(app, service, containerID, observed)
91	if len(claims) == 0 {
92		return binds, udpBinds, nil
93	}
94	if err := s.deps.State.RegisterBackendBinds(ctx, claims); err != nil {
95		s.retireCandidate(ctx, app, service, containerID)
96		return nil, nil, err
97	}
98	return binds, udpBinds, nil
99}
100
101// inspectBackendBinds re-inspects the loopback publishes of an EXISTING
102// container and registers the Gordon-generated claims. Unlike
103// readBackendBinds it never stops or removes the container: a
104// registration conflict fails without touching the ACTIVE workload.
105// Missing binds fail: an unbound backend must not stay routable.
106func (s *Service) inspectBackendBinds(ctx context.Context, app, service, containerID string, ports []domain.ContainerBackendPort) (map[int]int, map[int]int, error) {
107	if len(ports) == 0 {
108		return map[int]int{}, map[int]int{}, nil
109	}
110	observed, err := s.deps.Runtime.GetContainerBackendBinds(ctx, containerID, ports)
111	if err != nil {
112		return nil, nil, fmt.Errorf("deployment: backend binds: %w", err)
113	}
114	binds, udpBinds := splitBackendBinds(observed)
115	claims := backendClaims(app, service, containerID, observed)
116	if len(claims) == 0 {
117		return binds, udpBinds, nil
118	}
119	if err := s.deps.State.RegisterBackendBinds(ctx, claims); err != nil {
120		return nil, nil, err
121	}
122	return binds, udpBinds, nil
123}
124
125// splitBackendBinds partitions observed binds by protocol. TCP feeds
126// readiness and HTTP/TCP proxying; UDP feeds UDP relaying.
127func splitBackendBinds(observed []domain.ContainerBackendBind) (map[int]int, map[int]int) {
128	binds := map[int]int{}
129	udpBinds := map[int]int{}
130	for _, bind := range observed {
131		if bind.Protocol == domain.NetworkProtocolUDP {
132			udpBinds[bind.ContainerPort] = bind.HostPort
133		} else {
134			binds[bind.ContainerPort] = bind.HostPort
135		}
136	}
137	return binds, udpBinds
138}
139
140// backendClaims maps observed binds to Gordon-generated loopback claims
141// with the exact protocol (tcp/udp) for the global checkpoint.
142func backendClaims(app, service, containerID string, observed []domain.ContainerBackendBind) []domain.AppListenerReservation {
143	claims := make([]domain.AppListenerReservation, 0, len(observed))
144	for _, bind := range observed {
145		claims = append(claims, domain.AppListenerReservation{
146			Proto:       string(bind.Protocol),
147			IP:          "127.0.0.1",
148			Port:        bind.HostPort,
149			Service:     service,
150			App:         app,
151			Owner:       domain.OwnerGordonBackend,
152			ContainerID: containerID,
153		})
154	}
155	return claims
156}

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