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