1package domain
2
3import "time"
4
5// App state records are persistence shapes: the store adapter serializes them as
6// JSON, the apps use case orchestrates transitions. No secret VALUES
7// ever appear in these records — only secret paths.
8
9// AppStoreVersion is the single supported app-state format version.
10const AppStoreVersion = 1
11
12// AppDefaultRevisionRetention is the default count of unreferenced
13// revisions kept per app. Revisions referenced by desired state,
14// active services, or unfinished operations are always protected,
15// even beyond this limit. Configurable in main installation config.
16const AppDefaultRevisionRetention = 8
17
18// App intent states for the durable apply protocol.
19const (
20 AppIntentStaged = "staged"
21 AppIntentCommitted = "committed"
22 AppIntentApplied = "applied"
23)
24
25// App operation step states.
26const (
27 AppStepPending = "pending"
28 AppStepSucceeded = "succeeded"
29 AppStepFailed = "failed"
30 AppStepNotRun = "not-run"
31)
32
33// AppServiceUnchanged is the result of a deploy step that kept the running
34// container because its image, spec, and environment were already current.
35// A succeeded step whose Detail starts with it reports this result.
36const AppServiceUnchanged = "unchanged"
37
38// AppServiceRemoved is the result of a step that retired a service and its
39// container. A succeeded "service.<name>.remove" step reports this result.
40const AppServiceRemoved = "removed"
41
42// App operation outcomes over terminal per-service results.
43const (
44 AppOutcomeSuccess = "success"
45 AppOutcomePartial = "partial"
46 AppOutcomeFailed = "failed"
47)
48
49// App owned-resource states.
50const (
51 AppResourceAttached = "attached"
52 AppResourceRetained = "retained"
53 // AppResourceReleased marks a volume whose owning app explicitly
54 // released it. Only this state makes a volume prune-eligible, and
55 // only when the runtime labels agree with the durable record.
56 AppResourceReleased = "released"
57)
58
59// OwnerGordonBackend marks Gordon-generated loopback backend binds in
60// the global reservation checkpoint (plan D3: loopback-only,
61// generated, reserved, distinguishable from public routes).
62const OwnerGordonBackend = "gordon-backend"
63
64// AppListenerReservation is one global listener claim.
65type AppListenerReservation struct {
66 Proto string `json:"proto"`
67 IP string `json:"ip,omitempty"`
68 Port int `json:"port,omitempty"`
69 Host string `json:"host,omitempty"`
70 Service string `json:"service"`
71 App string `json:"app"`
72 Owner string `json:"owner,omitempty"`
73 Dual *bool `json:"dual,omitempty"`
74 // ContainerID ties a Gordon-generated backend claim to the exact
75 // container holding the bind, so old and replacement binds coexist
76 // and release targets exactly one retired container.
77 ContainerID string `json:"container_id,omitempty"`
78}
79
80// AppDesiredRevision is one accepted desired revision.
81type AppDesiredRevision struct {
82 Revision string `json:"revision"`
83 App string `json:"app"`
84 Supersedes string `json:"supersedes,omitempty"`
85 AcceptedAt time.Time `json:"accepted_at"`
86 SourceSHA256 string `json:"source_sha256"`
87 Spec AppSpec `json:"spec"`
88 Reservations []AppListenerReservation `json:"reservations"`
89 SecretsRequired []string `json:"secrets_required"`
90 SecretsEnv map[string]string `json:"secrets_env"`
91 Status string `json:"status"`
92}
93
94// AppBackend is one resolved service backend endpoint: the loopback
95// address plus the Gordon-published host port for one container port,
96// tied to the exact container holding the bind. The proxy and readiness
97// dial this endpoint rootless-first; a zero Backend means unbound
98// (fail closed, never fall back to container IPs).
99type AppBackend struct {
100 // Host is the loopback address (127.0.0.1) when bound, empty when not.
101 Host string `json:"host,omitempty"`
102 // Port is the Gordon-published host port.
103 Port int `json:"port,omitempty"`
104 // ContainerPort is the container port this endpoint serves.
105 ContainerPort int `json:"container_port,omitempty"`
106 // ContainerID is the exact container holding the bind.
107 ContainerID string `json:"container_id,omitempty"`
108}
109
110// Resolved reports whether the backend endpoint is dialable.
111func (b AppBackend) Resolved() bool {
112 return b.Host != "" && b.Port > 0 && b.ContainerID != ""
113}
114
115// BackendFor resolves one container port to its loopback endpoint from
116// the recorded binds. ContainerPort is always set (declared manifest
117// port); Host/Port/ContainerID resolve only when the bind is recorded
118// (zero Backend otherwise: fail closed).
119func (s AppEffectiveService) BackendFor(containerPort int) AppBackend {
120 return s.BackendForProtocol(containerPort, NetworkProtocolTCP)
121}
122
123// BackendForProtocol resolves a protocol-specific container port bind.
124func (s AppEffectiveService) BackendForProtocol(containerPort int, protocol NetworkProtocol) AppBackend {
125 backend := AppBackend{ContainerPort: containerPort}
126 binds := s.BackendBinds
127 if protocol == NetworkProtocolUDP {
128 binds = s.UDPBackendBinds
129 }
130 hostPort, ok := binds[containerPort]
131 if !ok || hostPort <= 0 || s.Container == "" {
132 return backend
133 }
134 backend.Host = "127.0.0.1"
135 backend.Port = hostPort
136 backend.ContainerID = s.Container
137 return backend
138}
139
140type AppEffectiveService struct {
141 EffectiveRevision string `json:"effective_revision"`
142 ActivatedBy string `json:"activated_by"`
143 ActivatedAt time.Time `json:"activated_at"`
144 Image string `json:"image"`
145 Digest string `json:"digest,omitempty"`
146 Container string `json:"container,omitempty"`
147 Spec AppService `json:"spec"`
148 // BackendBinds maps container port -> 127.0.0.1 host port for the
149 // Gordon-published loopback backends. Readiness and the proxy dial
150 // these binds: they work rootless-first (no container-IP route
151 // required) on Docker and Podman alike, and never expose backends
152 // beyond loopback (04-network.md §4).
153 BackendBinds map[int]int `json:"backend_binds,omitempty"`
154 UDPBackendBinds map[int]int `json:"udp_backend_binds,omitempty"`
155}
156
157// AppActive is the per-service effective state of one app.
158type AppActive struct {
159 App string `json:"app"`
160 ConvergedRevision string `json:"converged_revision"`
161 Converged bool `json:"converged"`
162 Services map[string]AppEffectiveService `json:"services"`
163 Networks []AppSharedNetwork `json:"networks,omitempty"`
164 StopIntent bool `json:"stop_intent"`
165}
166
167// AppStopIntent is the durable running/stopped intent.
168type AppStopIntent struct {
169 App string `json:"app"`
170 Stopped bool `json:"stopped"`
171 UpdatedBy string `json:"updated_by"`
172 UpdatedAt time.Time `json:"updated_at"`
173}
174
175// AppOperationStep is one durable journal step. Digest and Image are
176// structured fields: a step that acquires or replaces an image records
177// the pinned content digest and the runtime image reference here, so
178// prune protection never has to parse human-readable Detail.
179// AppOperationStep is one journaled step. Error carries a stable,
180// log-free message; Diagnostics carries bounded application log output
181// and is only returned to callers holding the logs read scope.
182type AppOperationStep struct {
183 ID string `json:"id"`
184 State string `json:"state"`
185 Detail string `json:"detail,omitempty"`
186 Error string `json:"error,omitempty"`
187 Before string `json:"before,omitempty"`
188 After string `json:"after,omitempty"`
189 // Digest pins the registry content digest this step resolved.
190 Digest string `json:"digest,omitempty"`
191 // Image is the runtime image reference this step acquired.
192 Image string `json:"image,omitempty"`
193 // Service names the app service this step applies to.
194 Service string `json:"service,omitempty"`
195 // Diagnostics is bounded, redacted failure output kept separate from
196 // the stable Error. Retrieval requires the logs read scope.
197 Diagnostics []string `json:"diagnostics,omitempty"`
198}
199
200// AppOperationRequest is the immutable identity of the mutation request
201// one journal record answers: kind, app, requested revision, requested
202// service. It is persisted with the claim so reusing one idempotency key
203// for a different request is rejected instead of silently replaying
204// another request's result. It never carries a secret value.
205type AppOperationRequest struct {
206 Kind string `json:"kind"`
207 App string `json:"app"`
208 Revision string `json:"revision,omitempty"`
209 Service string `json:"service,omitempty"`
210}
211
212// AppOperationRequestFor builds the request identity of one app
213// mutation from the caller's inputs: the resolved revision is never
214// part of it, so a repeat of the same request always matches.
215func AppOperationRequestFor(kind, app, revision, service string) AppOperationRequest {
216 return AppOperationRequest{Kind: kind, App: app, Revision: revision, Service: service}
217}
218
219// AppOperation is one durable operation journal record.
220type AppOperation struct {
221 Op string `json:"op"`
222 Kind string `json:"kind"`
223 App string `json:"app"`
224 InputRevision string `json:"input_revision"`
225 StartedAt time.Time `json:"started_at"`
226 Steps []AppOperationStep `json:"steps"`
227 Outcome string `json:"outcome,omitempty"`
228 // Warnings are bounded, operator-actionable leftovers of this
229 // operation: a container that could not be removed, or a backend
230 // claim that could not be released. They never carry log content.
231 Warnings []AppOperationWarning `json:"warnings,omitempty"`
232 // Request is the immutable request identity this journal answers.
233 // Records written by older binaries decode it as the zero value;
234 // such a record is only reachable through its own key.
235 Request AppOperationRequest `json:"request,omitempty"`
236}
237
238// AppOperationWarning records one bounded leftover of an operation.
239type AppOperationWarning struct {
240 Service string `json:"service,omitempty"`
241 Leftover string `json:"leftover,omitempty"`
242 Detail string `json:"detail"`
243}
244
245// Terminal reports whether the operation reached a terminal outcome. A
246// non-terminal operation is either in flight or was interrupted; its
247// effects must never be re-executed under the same key.
248func (o AppOperation) Terminal() bool {
249 return o.Outcome != ""
250}
251
252// AppApplyIntent is one durable apply intent.
253type AppApplyIntent struct {
254 Intent string `json:"intent"`
255 App string `json:"app"`
256 State string `json:"state"`
257 Revision string `json:"revision"`
258 Supersedes string `json:"supersedes,omitempty"`
259 SourceSHA256 string `json:"source_sha256"`
260 Spec AppSpec `json:"spec"`
261 Reservations []AppListenerReservation `json:"reservations"`
262 CreatedAt time.Time `json:"created_at"`
263}
264
265// AppOwnedVolume tracks one app-owned volume.
266type AppOwnedVolume struct {
267 Name string `json:"name"`
268 Service string `json:"service"`
269 RuntimeName string `json:"runtime_name"`
270 State string `json:"state"`
271}
272
273// AppOwnedSecret tracks one app-referenced secret (path only, never value).
274type AppOwnedSecret struct {
275 Service string `json:"service"`
276 Env string `json:"env"`
277 Name string `json:"name"`
278 Path string `json:"path"`
279 State string `json:"state"`
280}
281
282// AppOwnedNetwork tracks one app network.
283type AppOwnedNetwork struct {
284 Name string `json:"name"`
285 Role string `json:"role"`
286}
287
288// AppServiceRecovery tracks per-service restart-safety flags.
289type AppServiceRecovery struct {
290 RestartUnsafe bool `json:"restart_unsafe"`
291}
292
293// AppRecoveryInhibition is a durable, generation-scoped recovery
294// inhibition record. It names one exact container ID that must never be
295// started or restarted by boot or periodic recovery, because a
296// replacement may already have written to a volume that this generation
297// still owns. It carries no secret values. RestartUnsafe is a different
298// fact (a successful volume deployment also sets it) and never implies
299// inhibition.
300type AppRecoveryInhibition struct {
301 App string `json:"app"`
302 AppID string `json:"app_id,omitempty"`
303 Service string `json:"service"`
304 ContainerID string `json:"container_id"`
305 Reason string `json:"reason"`
306 Operation string `json:"operation,omitempty"`
307 CreatedAt time.Time `json:"created_at"`
308 // StopGrace is the effective stop grace of the generation to retire
309 // (retirement-pending only). Zero, as in records written before it
310 // existed, means the app default.
311 StopGrace time.Duration `json:"stop_grace,omitempty"`
312}
313
314// AppRecoveryInhibition reasons.
315const (
316 // AppInhibitReplacementPending marks the generation being replaced
317 // while a volume-owning candidate may already be writing.
318 AppInhibitReplacementPending = "replacement-pending"
319 // AppInhibitRetirementPending marks a superseded generation that an
320 // overlap replacement left running beside its published successor. The
321 // periodic reconciliation retires it, with StopGrace, once the
322 // successor is verified and routed; it is never retired, and the
323 // marker is dropped, while ACTIVE names it.
324 AppInhibitRetirementPending = "retirement-pending"
325)
326
327// AppOwnership is the ownership record for one app.
328// ID is the stable internal UUID, distinct from the public name.
329// Remove frees the name but retains volumes/secrets under the old ID
330// with their exact ownership records; a new app reusing the name
331// never implicitly adopts them.
332// AppOwnedImage tracks one app-pinned image (reference and digest only,
333// never credentials).
334//
335//nolint:revive // Stutter matches the sibling AppOwned* record names.
336type AppOwnedImage struct {
337 Service string `json:"service"`
338 Reference string `json:"reference"`
339 Digest string `json:"digest,omitempty"`
340 State string `json:"state"`
341}
342
343type AppOwnership struct {
344 App string `json:"app"`
345 ID string `json:"id,omitempty"`
346 Volumes []AppOwnedVolume `json:"volumes"`
347 Services map[string]AppServiceRecovery `json:"services"`
348 Secrets []AppOwnedSecret `json:"secrets"`
349 Networks []AppOwnedNetwork `json:"networks"`
350 Images []AppOwnedImage `json:"images"`
351}
352
353// AppStoreCheckpoint is the global reservation checkpoint.
354type AppStoreCheckpoint struct {
355 Version int `json:"version"`
356 Reservations []AppListenerReservation `json:"reservations"`
357}