v3.3.1 31f1eec

activate.go RawHistoryFolder

1package apptraffic
2
3import (
4	"context"
5	"fmt"
6	"sort"
7	"sync"
8
9	"github.com/bnema/zerowrap"
10
11	"github.com/bnema/gordon/internal/boundaries/out"
12	"github.com/bnema/gordon/internal/domain"
13)
14
15// HostIndex is the proxy-facing read model: canonical HTTP host to the
16// resolved loopback backend. It derives from ACTIVE app state (bbolt
17// stays authoritative); the daemon rebuilds it after every activation
18// and at boot. Lookups never touch container IPs.
19type HostIndex struct {
20	mu      sync.RWMutex
21	byHost  map[string]RouteEntry
22	entries []RouteEntry
23}
24
25// NewHostIndex creates an empty host index.
26func NewHostIndex() *HostIndex {
27	return &HostIndex{byHost: map[string]RouteEntry{}}
28}
29
30// Replace installs a rebuilt host map.
31func (h *HostIndex) Replace(entries []RouteEntry) {
32	byHost := make(map[string]RouteEntry, len(entries))
33	for _, entry := range entries {
34		if entry.Kind != "http" || entry.Host == "" {
35			continue
36		}
37		byHost[entry.Host] = entry
38	}
39	h.mu.Lock()
40	defer h.mu.Unlock()
41	h.byHost = byHost
42	h.entries = append([]RouteEntry(nil), entries...)
43}
44
45// Entries returns a copy of the projected entries, for a caller that
46// builds a candidate index and publishes it through another one.
47func (h *HostIndex) Entries() []RouteEntry {
48	if h == nil {
49		return nil
50	}
51	h.mu.RLock()
52	defer h.mu.RUnlock()
53	return append([]RouteEntry(nil), h.entries...)
54}
55
56// Lookup returns the projected entry for a canonical host.
57func (h *HostIndex) Lookup(host string) (RouteEntry, bool) {
58	if h == nil {
59		return RouteEntry{}, false
60	}
61	h.mu.RLock()
62	defer h.mu.RUnlock()
63	entry, ok := h.byHost[host]
64	return entry, ok
65}
66
67// HostOwner returns the app and service serving a canonical host.
68func (h *HostIndex) HostOwner(host string) (app, service string, ok bool) {
69	entry, found := h.Lookup(host)
70	if !found {
71		return "", "", false
72	}
73	return entry.App, entry.Service, true
74}
75
76// LookupHost implements proxy.TargetProvider over the host index.
77// The proxy package owns the interface; apptraffic implements it to
78// keep app-state interpretation out of the proxy.
79func (h *HostIndex) LookupHost(host string) (domain.AppBackend, bool) {
80	if h == nil {
81		return domain.AppBackend{}, false
82	}
83	entry, ok := h.Lookup(host)
84	if !ok {
85		return domain.AppBackend{}, false
86	}
87	return entry.Backend, true
88}
89
90// AppHosts returns the sorted served hosts with resolved backends.
91// Unresolved entries (no recorded bind) are excluded: fail closed.
92// It implements out.AppHostSource: the shared read model for traffic,
93// PKI and public-TLS consumers.
94func (h *HostIndex) AppHosts() []out.AppHost {
95	if h == nil {
96		return nil
97	}
98	h.mu.RLock()
99	defer h.mu.RUnlock()
100	hosts := make([]out.AppHost, 0, len(h.byHost))
101	for host, entry := range h.byHost {
102		if !entry.Backend.Resolved() {
103			continue
104		}
105		hosts = append(hosts, out.AppHost{
106			Host:    host,
107			App:     entry.App,
108			Service: entry.Service,
109			TLSMode: entry.TLSMode,
110		})
111	}
112	sort.Slice(hosts, func(i, j int) bool { return hosts[i].Host < hosts[j].Host })
113	return hosts
114}
115
116// L4Entries returns resolved TCP and UDP projections.
117func (h *HostIndex) L4Entries() []RouteEntry {
118	if h == nil {
119		return nil
120	}
121	h.mu.RLock()
122	defer h.mu.RUnlock()
123	entries := make([]RouteEntry, 0, len(h.entries))
124	for _, entry := range h.entries {
125		if (entry.Kind == "tcp" || entry.Kind == "udp") && entry.Backend.Resolved() {
126			entries = append(entries, entry)
127		}
128	}
129	return entries
130}
131
132// Activator re-projects active app state into the host index.
133// It is the coordinated traffic activation for the deployment engine:
134// after each deployment the engine rebuilds the host index from ACTIVE
135// state so the proxy only serves current backends.
136type Activator struct {
137	log zerowrap.Logger
138}
139
140// NewActivator wires activation with its logger.
141func NewActivator(log zerowrap.Logger) *Activator {
142	return &Activator{log: log}
143}
144
145// RebuildHostIndex re-projects every app's ACTIVE state into the host
146// index. The daemon calls it after each activation and at boot.
147// STOPPED-intent apps are skipped: their backends must not stay
148// routable merely because ACTIVE retains their effective spec.
149func (a *Activator) RebuildHostIndex(
150	ctx context.Context,
151	index *HostIndex,
152	state out.AppStateReader,
153	entrypoints map[string]EntrypointPolicy,
154) error {
155	apps, err := state.ListApps(ctx)
156	if err != nil {
157		return fmt.Errorf("apptraffic: list apps for host index: %w", err)
158	}
159	actives := make(map[string]domain.AppActive, len(apps))
160	for _, app := range apps {
161		intent, err := state.LoadIntent(ctx, app)
162		if err != nil {
163			return fmt.Errorf("apptraffic: load intent for host index: %w", err)
164		}
165		if intent.Stopped {
166			continue
167		}
168		active, ok, err := state.LoadActive(ctx, app)
169		if err != nil {
170			return fmt.Errorf("apptraffic: load active for host index: %w", err)
171		}
172		if !ok {
173			continue
174		}
175		actives[app] = active
176	}
177	projections, err := ProjectAll(actives, entrypoints)
178	if err != nil {
179		return err
180	}
181	var entries []RouteEntry
182	for _, app := range sortedKeys(projections) {
183		entries = append(entries, projections[app]...)
184	}
185	index.Replace(entries)
186	a.log.Info().Int("hosts", len(entries)).Msg("apptraffic: host index rebuilt")
187	return nil
188}
189
190// AppStateReader is the ACTIVE/intent subset read paths need.
191// Narrower than out.AppState so read paths never gain write access.
192// (Re-exported alias: the canonical definition lives in out.)
193
194func sortedKeys(projections map[string][]RouteEntry) []string {
195	apps := make([]string, 0, len(projections))
196	for app := range projections {
197		apps = append(apps, app)
198	}
199	sort.Strings(apps)
200	return apps
201}
202func ProjectAll(
203	apps map[string]domain.AppActive,
204	entrypoints map[string]EntrypointPolicy,
205) (map[string][]RouteEntry, error) {
206	names := make([]string, 0, len(apps))
207	for app := range apps {
208		names = append(names, app)
209	}
210	sort.Strings(names)
211	projections := make(map[string][]RouteEntry, len(apps))
212	for _, app := range names {
213		entries, err := Project(app, apps[app], entrypoints)
214		if err != nil {
215			return nil, err
216		}
217		projections[app] = entries
218	}
219	return projections, nil
220}

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