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}