app_reservations.go RawHistoryFolder

1package domain
2
3import (
4	"fmt"
5	"sort"
6)
7
8// ReservationsFor computes the global listener claims for a normalized
9// spec. It is the single owner of reservation derivation; the apps and
10// deployment use cases delegate to it. Only effective-public HTTP
11// interfaces claim a host: internal interfaces are never published, so
12// they hold no global listener reservation.
13func ReservationsFor(spec AppSpec) []AppListenerReservation {
14	var reservations []AppListenerReservation
15	for _, svc := range spec.Services {
16		for _, h := range svc.HTTP {
17			if !h.IsPublic() {
18				continue
19			}
20			reservations = append(reservations, AppListenerReservation{
21				Proto:   "http",
22				Host:    h.Host,
23				Service: svc.Name,
24				App:     spec.Name,
25			})
26		}
27		for _, t := range svc.TCP {
28			host, port, err := ParsePublish(t.Publish)
29			if err != nil {
30				continue
31			}
32			reservations = append(reservations, AppListenerReservation{
33				Proto:   "tcp",
34				IP:      reservationIP(host),
35				Port:    port,
36				Service: svc.Name,
37				App:     spec.Name,
38			})
39		}
40		for _, u := range svc.UDP {
41			host, port, err := ParsePublish(u.Publish)
42			if err != nil {
43				continue
44			}
45			reservations = append(reservations, AppListenerReservation{
46				Proto:   "udp",
47				IP:      reservationIP(host),
48				Port:    port,
49				Service: svc.Name,
50				App:     spec.Name,
51			})
52		}
53	}
54	sort.Slice(reservations, func(i, j int) bool {
55		if reservations[i].Proto != reservations[j].Proto {
56			return reservations[i].Proto < reservations[j].Proto
57		}
58		if reservations[i].Host != reservations[j].Host {
59			return reservations[i].Host < reservations[j].Host
60		}
61		if reservations[i].Port != reservations[j].Port {
62			return reservations[i].Port < reservations[j].Port
63		}
64		return reservations[i].IP < reservations[j].IP
65	})
66	return reservations
67}
68
69// reservationIP maps an empty publish host to the wildcard entry.
70func reservationIP(host string) string {
71	if host == "" {
72		return "dual"
73	}
74	return host
75}
76
77// CheckReservations validates a candidate reservation set against the
78// global checkpoint plus wildcard-aware overlay rules. Same-app entries
79// in existing are retained until withdrawal, so the candidate's own app
80// entries never self-conflict. Intra-candidate claims overlap like
81// cross-app claims: a wildcard L4 bind collides with a specific bind on
82// the same proto and port.
83func CheckReservations(existing []AppListenerReservation, candidate []AppListenerReservation, app string) error {
84	// Duplicate and overlapping claims within the candidate itself.
85	for i, res := range candidate {
86		for _, other := range candidate[:i] {
87			if ReservationsOverlap(res, other) {
88				return fmt.Errorf(
89					"%w: %s claims %s twice (service %s vs %s)",
90					ErrAppReservationConflict,
91					app, describeReservation(res), other.Service, res.Service,
92				)
93			}
94		}
95	}
96	for _, res := range candidate {
97		for _, other := range existing {
98			if other.App == app {
99				continue
100			}
101			if ReservationsOverlap(res, other) {
102				return fmt.Errorf(
103					"%w: %s/%s conflicts with %s/%s on %s",
104					ErrAppReservationConflict,
105					app, res.Service, other.App, other.Service, describeReservation(res),
106				)
107			}
108		}
109	}
110	return nil
111}
112
113// ReservationsOverlap reports wildcard/specific conflicts within one
114// proto namespace.
115func ReservationsOverlap(a, b AppListenerReservation) bool {
116	if a.Proto != b.Proto {
117		return false
118	}
119	if a.Proto == "http" {
120		return a.Host == b.Host
121	}
122	if a.Port != b.Port {
123		return false
124	}
125	if IsReservationWildcard(a.IP) || IsReservationWildcard(b.IP) {
126		return true
127	}
128	return a.IP == b.IP
129}
130
131// IsReservationWildcard matches wildcard and dual-family entries.
132func IsReservationWildcard(ip string) bool {
133	return ip == "" || ip == "0.0.0.0" || ip == "::" || ip == "dual"
134}
135
136// describeReservation renders a human-readable claim.
137func describeReservation(res AppListenerReservation) string {
138	if res.Proto == "http" {
139		return "http://" + res.Host
140	}
141	return fmt.Sprintf("%s://%s:%d", res.Proto, res.IP, res.Port)
142}

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