v3.0.0-alpha.1 6fdddb8

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// LookupHost implements proxy.TargetProvider over the host index.
68// The proxy package owns the interface; apptraffic implements it to
69// keep app-state interpretation out of the proxy.
70func (h *HostIndex) LookupHost(host string) (domain.AppBackend, bool) {
71	if h == nil {
72		return domain.AppBackend{}, false
73	}
74	entry, ok := h.Lookup(host)
75	if !ok {
76		return domain.AppBackend{}, false
77	}
78	return entry.Backend, true
79}
80
81// AppHosts returns the sorted served hosts with resolved backends.
82// Unresolved entries (no recorded bind) are excluded: fail closed.
83// It implements out.AppHostSource: the shared read model for traffic,
84// PKI and public-TLS consumers.
85func (h *HostIndex) AppHosts() []out.AppHost {
86	if h == nil {
87		return nil
88	}
89	h.mu.RLock()
90	defer h.mu.RUnlock()
91	hosts := make([]out.AppHost, 0, len(h.byHost))
92	for host, entry := range h.byHost {
93		if !entry.Backend.Resolved() {
94			continue
95		}
96		hosts = append(hosts, out.AppHost{
97			Host:    host,
98			App:     entry.App,
99			Service: entry.Service,
100			TLSMode: entry.TLSMode,
101		})
102	}
103	sort.Slice(hosts, func(i, j int) bool { return hosts[i].Host < hosts[j].Host })
104	return hosts
105}
106
107// L4Entries returns resolved TCP and UDP projections.
108func (h *HostIndex) L4Entries() []RouteEntry {
109	if h == nil {
110		return nil
111	}
112	h.mu.RLock()
113	defer h.mu.RUnlock()
114	entries := make([]RouteEntry, 0, len(h.entries))
115	for _, entry := range h.entries {
116		if (entry.Kind == "tcp" || entry.Kind == "udp") && entry.Backend.Resolved() {
117			entries = append(entries, entry)
118		}
119	}
120	return entries
121}
122
123// Activator re-projects active app state into the host index.
124// It is the coordinated traffic activation for the deployment engine:
125// after each deployment the engine rebuilds the host index from ACTIVE
126// state so the proxy only serves current backends.
127type Activator struct {
128	log zerowrap.Logger
129}
130
131// NewActivator wires activation with its logger.
132func NewActivator(log zerowrap.Logger) *Activator {
133	return &Activator{log: log}
134}
135
136// RebuildHostIndex re-projects every app's ACTIVE state into the host
137// index. The daemon calls it after each activation and at boot.
138// STOPPED-intent apps are skipped: their backends must not stay
139// routable merely because ACTIVE retains their effective spec.
140func (a *Activator) RebuildHostIndex(
141	ctx context.Context,
142	index *HostIndex,
143	state out.AppStateReader,
144	entrypoints map[string]EntrypointPolicy,
145) error {
146	apps, err := state.ListApps(ctx)
147	if err != nil {
148		return fmt.Errorf("apptraffic: list apps for host index: %w", err)
149	}
150	actives := make(map[string]domain.AppActive, len(apps))
151	for _, app := range apps {
152		intent, err := state.LoadIntent(ctx, app)
153		if err != nil {
154			return fmt.Errorf("apptraffic: load intent for host index: %w", err)
155		}
156		if intent.Stopped {
157			continue
158		}
159		active, ok, err := state.LoadActive(ctx, app)
160		if err != nil {
161			return fmt.Errorf("apptraffic: load active for host index: %w", err)
162		}
163		if !ok {
164			continue
165		}
166		actives[app] = active
167	}
168	projections, err := ProjectAll(actives, entrypoints)
169	if err != nil {
170		return err
171	}
172	var entries []RouteEntry
173	for _, app := range sortedKeys(projections) {
174		entries = append(entries, projections[app]...)
175	}
176	index.Replace(entries)
177	a.log.Info().Int("hosts", len(entries)).Msg("apptraffic: host index rebuilt")
178	return nil
179}
180
181// AppStateReader is the ACTIVE/intent subset read paths need.
182// Narrower than out.AppState so read paths never gain write access.
183// (Re-exported alias: the canonical definition lives in out.)
184
185func sortedKeys(projections map[string][]RouteEntry) []string {
186	apps := make([]string, 0, len(projections))
187	for app := range projections {
188		apps = append(apps, app)
189	}
190	sort.Strings(apps)
191	return apps
192}
193func ProjectAll(
194	apps map[string]domain.AppActive,
195	entrypoints map[string]EntrypointPolicy,
196) (map[string][]RouteEntry, error) {
197	names := make([]string, 0, len(apps))
198	for app := range apps {
199		names = append(names, app)
200	}
201	sort.Strings(names)
202	projections := make(map[string][]RouteEntry, len(apps))
203	for _, app := range names {
204		entries, err := Project(app, apps[app], entrypoints)
205		if err != nil {
206			return nil, err
207		}
208		projections[app] = entries
209	}
210	return projections, nil
211}

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