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