v3.3.2 9697f9d

projection.go RawHistoryFolder

1// Package apptraffic projects active app definitions into traffic
2// snapshot entries consumed by the traffic manager.
3package apptraffic
4
5import (
6	"fmt"
7	"sort"
8	"strconv"
9	"strings"
10
11	"github.com/bnema/gordon/internal/domain"
12)
13
14// EntrypointPolicy is the installation policy inherited by generated
15// listeners: the canonical public bind, trusted CIDRs, raw-fallback
16// behavior, and transport limits. The code never reads installation
17// config; the caller supplies it.
18type EntrypointPolicy struct {
19	Name                    string
20	Address                 string
21	Protocol                domain.EntryPointProtocol
22	TrustedCIDRs            []string
23	RawFallback             string
24	RawFallbackTrustedCIDRs []string
25	AllowPublicRawFallback  bool
26}
27
28// listener projects the policy to the domain listener contract used for
29// exact publish compatibility checks.
30func (p EntrypointPolicy) listener() domain.EntryPointListener {
31	return domain.EntryPointListener{Address: p.Address, Protocol: p.Protocol}
32}
33
34// RouteEntry is one projected traffic entry.
35type RouteEntry struct {
36	// Kind is http, tcp, or udp.
37	Kind string
38	// RouterName is the deterministic generated router name.
39	RouterName string
40	// Entrypoint names the installation entrypoint (empty for http).
41	Entrypoint string
42	// Host is the canonical HTTP host (http only).
43	Host string
44	// BindIP and BindPort are the literal public bind (tcp/udp only).
45	BindIP   string
46	BindPort int
47	// TLSMode is auto, always, or never (http only).
48	TLSMode string
49	// App and Service identify the owning workload.
50	App     string
51	Service string
52	// Backend is the resolved loopback endpoint for the interface's
53	// container port. Zero when unbound (proxy fails closed).
54	Backend domain.AppBackend
55}
56
57// Project maps one app's active definitions to traffic entries.
58// Entrypoints supplies installation policy by name; unknown entrypoint references are validation errors.
59// Loopback endpoints resolve from each service's recorded backend binds;
60// services without a recorded bind for an interface port project with an
61// empty BackendHost (proxy fails closed).
62func Project(app string, active domain.AppActive, entrypoints map[string]EntrypointPolicy) ([]RouteEntry, error) {
63	if active.App != "" && active.App != app {
64		return nil, fmt.Errorf("%w: projection for %q got active state of %q", domain.ErrAppTrafficProjection, app, active.App)
65	}
66	var entries []RouteEntry
67	for name, svc := range active.Services {
68		svcEntries, err := projectService(app, name, svc, entrypoints)
69		if err != nil {
70			return nil, err
71		}
72		entries = append(entries, svcEntries...)
73	}
74	sort.Slice(entries, func(i, j int) bool {
75		if entries[i].RouterName != entries[j].RouterName {
76			return entries[i].RouterName < entries[j].RouterName
77		}
78		return entries[i].Backend.ContainerPort < entries[j].Backend.ContainerPort
79	})
80	return entries, nil
81}
82
83// projectService maps one effective service to entries. Endpoints
84// resolve from the recorded backend binds; missing binds stay empty
85// (proxy fails closed, never falls back to container IPs).
86func projectService(app, name string, eff domain.AppEffectiveService, entrypoints map[string]EntrypointPolicy) ([]RouteEntry, error) {
87	spec := eff.Spec
88	spec.Name = name
89	var entries []RouteEntry
90	for _, h := range spec.HTTP {
91		// Internal HTTP is reachable only from the app private network: it
92		// is never routed, so it must not project a traffic entry or a
93		// certificate host. Fail closed by skipping it entirely.
94		if !h.IsPublic() {
95			continue
96		}
97		entries = append(entries, RouteEntry{
98			Kind:       "http",
99			RouterName: "app-" + app + "--" + spec.Name + "--http-" + sanitizeHost(h.Host),
100			Host:       h.Host,
101			TLSMode:    h.TLS,
102			App:        app,
103			Service:    spec.Name,
104			Backend:    eff.BackendFor(h.Port),
105		})
106	}
107	for _, t := range spec.TCP {
108		policy, err := lookupEntrypoint(entrypoints, t.Entrypoint, app, spec.Name, "tcp")
109		if err != nil {
110			return nil, err
111		}
112		host, port, err := domain.ParsePublish(t.Publish)
113		if err != nil {
114			return nil, err
115		}
116		if err := domain.ValidatePublishForListener(t.Publish, domain.NetworkProtocolTCP, policy.listener(), t.Entrypoint); err != nil {
117			return nil, fmt.Errorf("%w: service %q tcp interface: %s", domain.ErrAppTrafficProjection, spec.Name, err)
118		}
119		entries = append(entries, RouteEntry{
120			Kind:       "tcp",
121			RouterName: "app-" + app + "--" + spec.Name + "--tcp-" + itoa(t.Port),
122			Entrypoint: t.Entrypoint,
123			BindIP:     host,
124			BindPort:   port,
125			App:        app,
126			Service:    spec.Name,
127			Backend:    eff.BackendFor(t.Port),
128		})
129	}
130	for _, u := range spec.UDP {
131		policy, err := lookupEntrypoint(entrypoints, u.Entrypoint, app, spec.Name, "udp")
132		if err != nil {
133			return nil, err
134		}
135		host, port, err := domain.ParsePublish(u.Publish)
136		if err != nil {
137			return nil, err
138		}
139		if err := domain.ValidatePublishForListener(u.Publish, domain.NetworkProtocolUDP, policy.listener(), u.Entrypoint); err != nil {
140			return nil, fmt.Errorf("%w: service %q udp interface: %s", domain.ErrAppTrafficProjection, spec.Name, err)
141		}
142		entries = append(entries, RouteEntry{
143			Kind:       "udp",
144			RouterName: "app-" + app + "--" + spec.Name + "--udp-" + itoa(u.Port),
145			Entrypoint: u.Entrypoint,
146			BindIP:     host,
147			BindPort:   port,
148			App:        app,
149			Service:    spec.Name,
150			Backend:    eff.BackendForProtocol(u.Port, domain.NetworkProtocolUDP),
151		})
152	}
153	return entries, nil
154}
155
156// lookupEntrypoint resolves a named installation entrypoint.
157func lookupEntrypoint(entrypoints map[string]EntrypointPolicy, name, app, service, kind string) (EntrypointPolicy, error) {
158	policy, ok := entrypoints[name]
159	if !ok {
160		return EntrypointPolicy{}, fmt.Errorf(
161			"%w: service %q %s references unknown entrypoint %q",
162			domain.ErrAppTrafficProjection, service, kind, name,
163		)
164	}
165	_ = app
166	return policy, nil
167}
168
169// sanitizeHost maps a hostname to a router-name fragment.
170func sanitizeHost(host string) string {
171	fragment := strings.ReplaceAll(host, ".", "-")
172	fragment = strings.ReplaceAll(fragment, ":", "-")
173	return strings.ReplaceAll(fragment, "/", "-")
174}
175
176// itoa formats a port number.
177func itoa(n int) string {
178	return strconv.Itoa(n)
179}

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