v3.3.1 31f1eec

networks.go RawHistoryFolder

1package deployment
2
3import (
4	"context"
5	"fmt"
6
7	"github.com/bnema/gordon/internal/domain"
8)
9
10// ensureIncarnationID returns the app's stable internal UUID, assigning
11// and persisting one on first use. Every incarnation-scoped resource
12// (network, volumes, secrets) keys off this ID, never the public name, so
13// removing and re-adding an app under the same name can never inherit the
14// old incarnation's resources.
15func (s *Service) ensureIncarnationID(ctx context.Context, app string) (string, error) {
16	ownership, err := s.deps.State.LoadOwnership(ctx, app)
17	if err != nil {
18		return "", fmt.Errorf("deployment: load ownership: %w", err)
19	}
20	if ownership.ID != "" {
21		return ownership.ID, nil
22	}
23	ownership.App = app
24	if err := s.deps.State.SaveOwnership(ctx, ownership); err != nil {
25		return "", fmt.Errorf("deployment: assign incarnation: %w", err)
26	}
27	saved, err := s.deps.State.LoadOwnership(ctx, app)
28	if err != nil {
29		return "", fmt.Errorf("deployment: reload ownership: %w", err)
30	}
31	if saved.ID == "" {
32		return "", fmt.Errorf("deployment: app %q has no incarnation id: %w", app, domain.ErrAppStateConflict)
33	}
34	return saved.ID, nil
35}
36
37// appNetworks is the runtime name set one service joins.
38type appNetworks struct {
39	// private is the incarnation-owned network every service of the app
40	// joins; no other app shares it.
41	private string
42	// shared are the declared cross-app memberships resolved to runtime
43	// names.
44	shared []sharedNetwork
45}
46
47// sharedNetwork ties a declared membership to its derived runtime name.
48type sharedNetwork struct {
49	declared string
50	runtime  string
51}
52
53// resolveAppNetworks derives runtime network names from the app UUID and
54// the service's declared shared memberships. Deriving (never accepting a
55// caller-supplied runtime name) keeps recovery and deploy in agreement and
56// prevents a manifest from naming a foreign network.
57func (s *Service) resolveAppNetworks(appID string, memberships []domain.AppSharedNetwork) appNetworks {
58	prefix := s.deps.Networks.Prefix
59	resolved := appNetworks{private: domain.AppPrivateNetworkName(prefix, appID)}
60	for _, membership := range memberships {
61		resolved.shared = append(resolved.shared, sharedNetwork{
62			declared: membership.Network,
63			runtime:  domain.AppSharedNetworkName(prefix, membership.Network),
64		})
65	}
66	return resolved
67}
68
69// ensureAppNetworks verifies or creates the incarnation network and every
70// declared shared network BEFORE any container is created. An existing
71// network is reused only when its labels prove Gordon ownership; any other
72// network using the derived name is refused fail-closed. Networks are
73// never removed here: an empty owned network is inert, and during a
74// replacement the superseded generation may still be attached to it.
75func (s *Service) ensureAppNetworks(ctx context.Context, app, appID string, nets appNetworks) error {
76	observed, err := s.deps.Runtime.ListNetworks(ctx)
77	if err != nil {
78		return fmt.Errorf("deployment: list networks: %w", err)
79	}
80	byName := make(map[string]*domain.NetworkInfo, len(observed))
81	for _, network := range observed {
82		if network == nil {
83			continue
84		}
85		byName[network.Name] = network
86	}
87	if err := s.ensureNetwork(ctx, byName, nets.private, domain.AppPrivateNetworkLabels(app, appID)); err != nil {
88		return err
89	}
90	for _, shared := range nets.shared {
91		if err := s.ensureNetwork(ctx, byName, shared.runtime, domain.AppSharedNetworkLabels(shared.declared)); err != nil {
92			return err
93		}
94	}
95	return nil
96}
97
98// ensureNetwork reuses an owned network or creates it.
99func (s *Service) ensureNetwork(ctx context.Context, byName map[string]*domain.NetworkInfo, name string, labels map[string]string) error {
100	if existing, ok := byName[name]; ok {
101		if !domain.NetworkOwnedBy(existing.Labels, labels) {
102			return fmt.Errorf("deployment: network %q exists without Gordon ownership: %w", name, domain.ErrAppStateConflict)
103		}
104		return nil
105	}
106	config := domain.NetworkConfig{Driver: "bridge", Internal: s.deps.Networks.Internal, Labels: labels}
107	if err := s.deps.Runtime.CreateNetwork(ctx, name, config); err != nil {
108		return fmt.Errorf("deployment: create network %q: %w", name, err)
109	}
110	return nil
111}
112
113// connectSharedNetworks attaches a created container to every declared
114// shared network. The private network is attached at create through
115// NetworkMode; shared memberships are explicit so an app joins only the
116// networks its manifest declares.
117func (s *Service) connectSharedNetworks(ctx context.Context, containerID string, nets appNetworks) error {
118	for _, shared := range nets.shared {
119		if err := s.deps.Runtime.ConnectContainerToNetwork(ctx, containerID, shared.runtime); err != nil {
120			return fmt.Errorf("deployment: connect %s to network %q: %w", containerID, shared.runtime, err)
121		}
122	}
123	return nil
124}
125
126// recordNetworkOwnership stamps the incarnation network and every declared
127// shared membership into the ownership record so prune/remove can see
128// exactly which networks this app created or joined.
129func recordNetworkOwnership(ownership *domain.AppOwnership, prefix string, shared []domain.AppSharedNetwork) {
130	if ownership.ID == "" {
131		return
132	}
133	entries := []domain.AppOwnedNetwork{{Name: domain.AppPrivateNetworkName(prefix, ownership.ID), Role: domain.AppNetworkRolePrivate}}
134	for _, membership := range shared {
135		entries = append(entries, domain.AppOwnedNetwork{
136			Name: domain.AppSharedNetworkName(prefix, membership.Network),
137			Role: domain.AppNetworkRoleShared,
138		})
139	}
140	seen := make(map[string]struct{}, len(entries))
141	merged := make([]domain.AppOwnedNetwork, 0, len(entries))
142	for _, entry := range entries {
143		if _, ok := seen[entry.Name]; ok {
144			continue
145		}
146		seen[entry.Name] = struct{}{}
147		merged = append(merged, entry)
148	}
149	for _, existing := range ownership.Networks {
150		if _, ok := seen[existing.Name]; ok {
151			continue
152		}
153		seen[existing.Name] = struct{}{}
154		merged = append(merged, existing)
155	}
156	ownership.Networks = merged
157}
158
159// reclaimPrivateNetworks removes the incarnation-owned private networks of
160// one app immediately after every exact container is confirmed gone, while
161// the ownership record is still live. The runtime name is re-derived from
162// the incarnation UUID and the observed labels must prove that exact
163// ownership, so a name reused by another incarnation can never be
164// deleted. Shared networks are never removed: they belong to the
165// installation, and other apps may still be attached. An unattached,
166// ownership-verified network is removed; anything else is left in place
167// and reported as a bounded warning, never silently deleted.
168func (s *Service) reclaimPrivateNetworks(ctx context.Context, app string, ownership domain.AppOwnership) ([]CleanupWarning, error) {
169	if ownership.ID == "" {
170		return nil, nil
171	}
172	expectedName := domain.AppPrivateNetworkName(s.deps.Networks.Prefix, ownership.ID)
173	private := make([]string, 0, len(ownership.Networks))
174	for _, network := range ownership.Networks {
175		if network.Role != domain.AppNetworkRolePrivate {
176			continue
177		}
178		if network.Name != expectedName {
179			// A record naming anything but this incarnation's derived
180			// network is never deleted: the name may belong to a
181			// different incarnation.
182			continue
183		}
184		private = append(private, network.Name)
185	}
186	if len(private) == 0 {
187		return nil, nil
188	}
189	observed, err := s.deps.Runtime.ListNetworks(ctx)
190	if err != nil {
191		return nil, fmt.Errorf("deployment: list networks for reclamation: %w", err)
192	}
193	byName := make(map[string]*domain.NetworkInfo, len(observed))
194	for _, network := range observed {
195		if network == nil {
196			continue
197		}
198		byName[network.Name] = network
199	}
200	expected := domain.AppPrivateNetworkLabels(app, ownership.ID)
201	var warnings []CleanupWarning
202	for _, name := range private {
203		network, ok := byName[name]
204		if !ok {
205			continue // already gone
206		}
207		if !domain.NetworkOwnedBy(network.Labels, expected) {
208			warnings = append(warnings, CleanupWarning{
209				Leftover: name,
210				Detail:   "private network labels do not prove this incarnation; left in place",
211			})
212			continue
213		}
214		if len(network.Containers) > 0 {
215			warnings = append(warnings, CleanupWarning{
216				Leftover: name,
217				Detail:   "private network still has attached containers; left in place",
218			})
219			continue
220		}
221		if err := s.deps.Runtime.RemoveNetwork(ctx, name); err != nil {
222			return warnings, fmt.Errorf("deployment: remove private network %q: %w", name, err)
223		}
224	}
225	return warnings, nil
226}

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