zlayer_types/api/containers.rs
1//! Raw container lifecycle API DTOs.
2//!
3//! Wire-format types shared between the daemon's `/api/v1/containers`
4//! endpoints and SDK clients. Moved out of `zlayer-api` so SDK crates can
5//! depend on them without pulling in the full server stack.
6
7use std::collections::HashMap;
8
9use serde::{Deserialize, Serialize};
10use utoipa::{IntoParams, ToSchema};
11
12/// Resource limits for a container
13#[derive(Debug, Clone, Deserialize, Serialize, ToSchema)]
14pub struct ContainerResourceLimits {
15 /// CPU limit in cores (e.g., 0.5, 1.0, 2.0)
16 #[serde(default, skip_serializing_if = "Option::is_none")]
17 pub cpu: Option<f64>,
18 /// Memory limit (e.g., "256Mi", "1Gi")
19 #[serde(default, skip_serializing_if = "Option::is_none")]
20 pub memory: Option<String>,
21}
22
23/// Volume mount kind discriminator.
24///
25/// Selects which [`zlayer_spec::StorageSpec`] variant [`VolumeMount`] is
26/// translated into by [`build_service_spec`]. When omitted on the wire,
27/// defaults to [`VolumeMountType::Bind`] (legacy behavior).
28#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize, ToSchema)]
29#[serde(rename_all = "snake_case")]
30pub enum VolumeMountType {
31 /// Host-path bind mount. `source` is an absolute host path.
32 Bind,
33 /// Named persistent volume. `source` is the volume name (managed by
34 /// `/api/v1/volumes`), not a host path.
35 Volume,
36 /// Memory-backed tmpfs mount. `source` must be empty/omitted.
37 Tmpfs,
38}
39
40/// Volume mount specification.
41///
42/// The `type` field (a Docker-compatible discriminator) selects how `source`
43/// is interpreted:
44/// - `"bind"` (default): `source` is an absolute host path.
45/// - `"volume"`: `source` is a named-volume identifier.
46/// - `"tmpfs"`: no `source`; a memory-backed mount is provisioned.
47#[derive(Debug, Clone, Deserialize, Serialize, ToSchema)]
48pub struct VolumeMount {
49 /// Mount kind. Omit (or `"bind"`) for legacy host-path binds.
50 #[serde(rename = "type", default, skip_serializing_if = "Option::is_none")]
51 pub mount_type: Option<VolumeMountType>,
52 /// Host path (bind), volume name (volume), or unused (tmpfs).
53 #[serde(default, skip_serializing_if = "Option::is_none")]
54 pub source: Option<String>,
55 /// Container mount path
56 pub target: String,
57 /// Mount as read-only
58 #[serde(default)]
59 pub readonly: bool,
60}
61
62/// Container health check request.
63///
64/// Mirrors the on-disk `HealthCheck` enum (see `zlayer_spec::HealthCheck`) as a
65/// discriminated union keyed on `type`. Translated to `zlayer_spec::HealthSpec`
66/// by `HealthCheckRequest::to_health_spec`. Durations are humantime strings
67/// (for example `"10s"`, `"500ms"`, `"1m"`).
68///
69/// ## Variants
70/// - `type: "tcp"` — requires `port` (1-65535).
71/// - `type: "http"` — requires `url`; `expect_status` defaults to 200.
72/// - `type: "command"` — requires `command` (array of argv tokens; joined with
73/// spaces and passed to `sh -c` by the health monitor, matching the existing
74/// compose-to-ZLayer conversion in `zlayer-docker`).
75#[derive(Debug, Clone, Deserialize, Serialize, ToSchema)]
76pub struct HealthCheckRequest {
77 /// Check variant: `"tcp"`, `"http"`, or `"command"`.
78 #[serde(rename = "type")]
79 pub check_type: String,
80 /// TCP port (required when `type == "tcp"`).
81 #[serde(default, skip_serializing_if = "Option::is_none")]
82 pub port: Option<u16>,
83 /// HTTP URL (required when `type == "http"`).
84 #[serde(default, skip_serializing_if = "Option::is_none")]
85 pub url: Option<String>,
86 /// HTTP status code expected from `url` (defaults to 200).
87 #[serde(default, skip_serializing_if = "Option::is_none")]
88 pub expect_status: Option<u16>,
89 /// Command argv (required when `type == "command"`). Joined with spaces
90 /// and passed to `sh -c`.
91 #[serde(default, skip_serializing_if = "Option::is_none")]
92 pub command: Option<Vec<String>>,
93 /// Interval between checks, humantime format (e.g. `"30s"`). Defaults to 30s.
94 #[serde(default, skip_serializing_if = "Option::is_none")]
95 pub interval: Option<String>,
96 /// Timeout per individual check, humantime format.
97 #[serde(default, skip_serializing_if = "Option::is_none")]
98 pub timeout: Option<String>,
99 /// Number of consecutive failures before marking unhealthy. Defaults to 3.
100 #[serde(default, skip_serializing_if = "Option::is_none")]
101 pub retries: Option<u32>,
102 /// Grace period before the first check runs, humantime format. Maps to
103 /// `HealthSpec::start_grace`.
104 #[serde(default, skip_serializing_if = "Option::is_none")]
105 pub start_period: Option<String>,
106}
107
108/// Request to create and start a container
109#[derive(Debug, Deserialize, ToSchema)]
110pub struct CreateContainerRequest {
111 /// OCI image reference (e.g., "nginx:latest", "ubuntu:22.04")
112 pub image: String,
113 /// Optional human-readable name
114 #[serde(default)]
115 pub name: Option<String>,
116 /// Image pull policy: "always", "`if_not_present`", or "never"
117 #[serde(default)]
118 pub pull_policy: Option<String>,
119 /// Environment variables
120 #[serde(default)]
121 pub env: HashMap<String, String>,
122 /// Command to run (overrides image entrypoint)
123 #[serde(default)]
124 pub command: Option<Vec<String>>,
125 /// Labels for filtering and grouping
126 #[serde(default)]
127 pub labels: HashMap<String, String>,
128 /// Resource limits (CPU, memory)
129 #[serde(default)]
130 pub resources: Option<ContainerResourceLimits>,
131 /// Volume mounts
132 #[serde(default)]
133 pub volumes: Vec<VolumeMount>,
134 /// Published ports (Docker's `-p host:container/proto`). When omitted,
135 /// the container is created without any host port publishing.
136 #[serde(default, skip_serializing_if = "Vec::is_empty")]
137 pub ports: Vec<crate::spec::PortMapping>,
138 /// Working directory inside the container
139 #[serde(default)]
140 pub work_dir: Option<String>,
141 /// Optional health check. When omitted, the daemon installs a no-op
142 /// placeholder (`HealthCheck::Tcp { port: 0 }`) matching the current
143 /// default; the health monitor treats `port == 0` as "skip".
144 #[serde(default, skip_serializing_if = "Option::is_none")]
145 pub health_check: Option<HealthCheckRequest>,
146 /// Optional container hostname (maps to Docker's `--hostname`).
147 #[serde(default, skip_serializing_if = "Option::is_none")]
148 pub hostname: Option<String>,
149 /// Additional DNS servers (maps to Docker's `--dns`). Each entry must be
150 /// a plausible IPv4 or IPv6 address.
151 #[serde(default, skip_serializing_if = "Vec::is_empty")]
152 pub dns: Vec<String>,
153 /// Extra `hostname:ip` entries appended to `/etc/hosts` (maps to Docker's
154 /// `--add-host`). The special literal `host-gateway` is accepted as the
155 /// `ip` half.
156 #[serde(default, skip_serializing_if = "Vec::is_empty")]
157 pub extra_hosts: Vec<String>,
158 /// Container restart policy (Docker-style). When omitted, the runtime
159 /// applies no explicit restart policy (Docker default: `"no"`).
160 #[serde(default, skip_serializing_if = "Option::is_none")]
161 pub restart_policy: Option<crate::spec::ContainerRestartPolicy>,
162 /// User-defined bridge/overlay networks to attach the newly-created
163 /// container to. Each entry references a network by id or name and is
164 /// attached after the container is successfully started. If any
165 /// attachment fails, the partially-started container is rolled back
166 /// (stopped + removed) and the request is failed.
167 #[serde(default, skip_serializing_if = "Vec::is_empty")]
168 pub networks: Vec<NetworkAttachmentRequest>,
169 // -- §3.10: registry auth ------------------------------------------------
170 /// Id of a persisted registry credential (from
171 /// `POST /api/v1/credentials/registry`) to use when pulling the image.
172 /// Ignored when [`Self::registry_auth`] is also supplied (inline auth
173 /// wins). Requires the daemon to be configured with a credential store
174 /// — otherwise the request is rejected with `400`.
175 #[serde(default, skip_serializing_if = "Option::is_none")]
176 pub registry_credential_id: Option<String>,
177 /// Inline Docker/OCI registry credentials used for this pull only. Not
178 /// persisted, never logged, never echoed back on a response. When both
179 /// `registry_credential_id` and `registry_auth` are set, this field
180 /// takes precedence.
181 #[serde(default, skip_serializing_if = "Option::is_none")]
182 pub registry_auth: Option<crate::spec::RegistryAuth>,
183}
184
185/// A request to attach a freshly-created container to a user-defined bridge
186/// or overlay network, mirroring the wire-shape used by `POST
187/// /api/v1/container-networks/{id_or_name}/connect`.
188///
189/// Included on [`CreateContainerRequest::networks`] so callers can wire up
190/// every attachment in a single call instead of issuing a separate connect
191/// request per network after container create.
192#[derive(Debug, Clone, Deserialize, Serialize, ToSchema)]
193pub struct NetworkAttachmentRequest {
194 /// Bridge-network id or name to attach to.
195 pub network: String,
196 /// Optional DNS aliases for this container on the network.
197 #[serde(default, skip_serializing_if = "Vec::is_empty")]
198 pub aliases: Vec<String>,
199 /// Optional static IPv4 to pin this container to. Validated as
200 /// [`std::net::Ipv4Addr`] before the runtime is called.
201 #[serde(default, skip_serializing_if = "Option::is_none")]
202 pub ipv4_address: Option<String>,
203}
204
205/// Container information returned by the API
206#[derive(Debug, Serialize, Deserialize, ToSchema)]
207pub struct ContainerInfo {
208 /// Container identifier
209 pub id: String,
210 /// Human-readable name (if set)
211 #[serde(skip_serializing_if = "Option::is_none")]
212 pub name: Option<String>,
213 /// OCI image reference
214 pub image: String,
215 /// Container state (pending, running, exited, failed)
216 pub state: String,
217 /// Labels
218 pub labels: HashMap<String, String>,
219 /// Creation timestamp (ISO 8601)
220 pub created_at: String,
221 /// Process ID (if running)
222 #[serde(skip_serializing_if = "Option::is_none")]
223 pub pid: Option<u32>,
224 // -- §3.15: rich inspect fields -----------------------------------------
225 /// Published port mappings (container → host). Populated from the
226 /// runtime's inspect response; empty when the runtime doesn't expose
227 /// port-level detail or the container has no published ports.
228 #[serde(default, skip_serializing_if = "Vec::is_empty")]
229 pub ports: Vec<crate::spec::PortMapping>,
230 /// Networks this container is attached to, with per-network aliases
231 /// and IPv4. Empty when the runtime doesn't surface network detail.
232 #[serde(default, skip_serializing_if = "Vec::is_empty")]
233 pub networks: Vec<NetworkAttachmentInfo>,
234 /// Primary IPv4 address (first non-empty IP across attached networks).
235 /// Docker's `bridge` network is preferred when present.
236 #[serde(default, skip_serializing_if = "Option::is_none")]
237 pub ipv4: Option<String>,
238 /// Runtime-native health status, when the container image declares a
239 /// `HEALTHCHECK` (or equivalent). `None` when the runtime doesn't track
240 /// health for this container.
241 #[serde(default, skip_serializing_if = "Option::is_none")]
242 pub health: Option<ContainerHealthInfo>,
243 /// Most-recent exit code. `None` for containers still running and for
244 /// containers that have never exited.
245 #[serde(default, skip_serializing_if = "Option::is_none")]
246 pub exit_code: Option<i32>,
247}
248
249/// Per-network attachment entry on [`ContainerInfo::networks`].
250///
251/// Populated from the runtime's inspect response — mirrors the subset of
252/// bollard's `EndpointSettings` that API clients need to correlate a container
253/// with its `container_networks` entries.
254#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
255pub struct NetworkAttachmentInfo {
256 /// Network name as reported by the runtime. Matches the `name` field on
257 /// entries returned by `GET /api/v1/container-networks`.
258 pub network: String,
259 /// DNS aliases the container answers to on this network.
260 #[serde(default, skip_serializing_if = "Vec::is_empty")]
261 pub aliases: Vec<String>,
262 /// Assigned IPv4 on this network, if any.
263 #[serde(default, skip_serializing_if = "Option::is_none")]
264 pub ipv4: Option<String>,
265}
266
267/// Runtime-native health snapshot on [`ContainerInfo::health`].
268///
269/// Sourced from bollard's `ContainerState.health` for Docker-backed
270/// containers. The internal `HealthMonitor` in
271/// `crates/zlayer-agent/src/health.rs` drives service-level health events
272/// against user-configured health specs; for standalone containers the API
273/// reports the runtime-native status instead so images with a baked-in
274/// `HEALTHCHECK` still surface correctly.
275#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
276pub struct ContainerHealthInfo {
277 /// One of `"none"`, `"starting"`, `"healthy"`, `"unhealthy"` (Docker
278 /// `HealthStatusEnum`). Empty / missing upstream values normalise to
279 /// `"none"`.
280 pub status: String,
281 /// Consecutive failing probe count, when the runtime tracks it.
282 #[serde(default, skip_serializing_if = "Option::is_none")]
283 pub failing_streak: Option<u32>,
284 /// Output from the most recent failing probe, when available.
285 #[serde(default, skip_serializing_if = "Option::is_none")]
286 pub last_output: Option<String>,
287}
288
289/// Query parameters for listing containers
290#[derive(Debug, Deserialize, IntoParams)]
291pub struct ListContainersQuery {
292 /// Filter by label (key=value format)
293 #[serde(default)]
294 pub label: Option<String>,
295}
296
297/// Query parameters for container logs
298#[derive(Debug, Deserialize, IntoParams)]
299pub struct ContainerLogQuery {
300 /// Number of tail lines to return
301 #[serde(default = "default_tail")]
302 pub tail: usize,
303 /// Follow logs (SSE stream)
304 #[serde(default)]
305 pub follow: bool,
306}
307
308fn default_tail() -> usize {
309 100
310}
311
312/// Exec request for running a command in a container
313#[derive(Debug, Deserialize, ToSchema)]
314pub struct ContainerExecRequest {
315 /// Command and arguments to execute
316 pub command: Vec<String>,
317}
318
319/// Query parameters for the exec endpoint.
320///
321/// When `stream=true` the handler returns a Server-Sent Events stream with
322/// one `stdout` / `stderr` event per line of output and a final `exit` event
323/// carrying the exit code as JSON. When `stream=false` (the default) the
324/// handler buffers the whole output and returns a single JSON
325/// [`ContainerExecResponse`] body.
326#[derive(Debug, Default, Deserialize, IntoParams)]
327pub struct ExecQuery {
328 /// Stream exec events as SSE instead of returning a buffered JSON body.
329 #[serde(default)]
330 pub stream: bool,
331}
332
333/// Exec response with command output
334#[derive(Debug, Serialize, Deserialize, ToSchema)]
335pub struct ContainerExecResponse {
336 /// Exit code from the command
337 pub exit_code: i32,
338 /// Standard output
339 pub stdout: String,
340 /// Standard error
341 pub stderr: String,
342}
343
344/// Request body for stopping a container. Matches the Docker-compat
345/// `POST /containers/{id}/stop` shape.
346#[derive(Debug, Default, Deserialize, ToSchema)]
347pub struct StopContainerRequest {
348 /// Graceful shutdown timeout in seconds before the runtime force-kills
349 /// the container. Defaults to 30 seconds when omitted.
350 #[serde(default)]
351 pub timeout: Option<u64>,
352}
353
354/// Request body for restarting a container. Matches the Docker-compat
355/// `POST /containers/{id}/restart` shape.
356#[derive(Debug, Default, Deserialize, ToSchema)]
357pub struct RestartContainerRequest {
358 /// Graceful shutdown timeout in seconds before the runtime force-kills
359 /// the container. Defaults to 30 seconds when omitted.
360 #[serde(default)]
361 pub timeout: Option<u64>,
362}
363
364/// Request body for killing (sending a signal to) a container. Matches the
365/// Docker-compat `POST /containers/{id}/kill` shape.
366#[derive(Debug, Default, Deserialize, ToSchema)]
367pub struct KillContainerRequest {
368 /// Signal name to send (e.g. `"SIGTERM"`, `"SIGINT"`). Accepts both the
369 /// `SIG`-prefixed and bare forms. When omitted, defaults to `SIGKILL`.
370 #[serde(default)]
371 pub signal: Option<String>,
372}
373
374/// Wait response with container exit code plus optional classification
375/// fields (added in §3.12 of the SDK-fixes spec).
376///
377/// The three optional fields (`reason`, `signal`, `finished_at`) are
378/// additive — clients that only read `exit_code` keep working unchanged.
379#[derive(Debug, Serialize, Deserialize, ToSchema)]
380pub struct ContainerWaitResponse {
381 /// Container identifier
382 pub id: String,
383 /// Exit code (0 = success). When the container was killed by signal
384 /// `N`, this is typically `128 + N`.
385 pub exit_code: i32,
386 /// Classification of the exit. One of `"exited"`, `"signal"`,
387 /// `"oom_killed"`, or `"runtime_error"`. Absent when the runtime
388 /// didn't classify the exit.
389 #[serde(default, skip_serializing_if = "Option::is_none")]
390 pub reason: Option<String>,
391 /// Signal name when `reason == "signal"`, e.g. `"SIGKILL"`. Absent
392 /// when the runtime couldn't determine it (or the exit wasn't a
393 /// signal death).
394 #[serde(default, skip_serializing_if = "Option::is_none")]
395 pub signal: Option<String>,
396 /// RFC3339 timestamp of when the container exited, if reported by
397 /// the runtime.
398 #[serde(default, skip_serializing_if = "Option::is_none")]
399 pub finished_at: Option<String>,
400}
401
402/// Container resource statistics
403#[derive(Debug, Serialize, Deserialize, ToSchema)]
404pub struct ContainerStatsResponse {
405 /// Container identifier
406 pub id: String,
407 /// CPU usage in microseconds
408 pub cpu_usage_usec: u64,
409 /// Current memory usage in bytes
410 pub memory_bytes: u64,
411 /// Memory limit in bytes (`u64::MAX` if unlimited)
412 pub memory_limit: u64,
413 /// Memory usage as percentage of limit
414 pub memory_percent: f64,
415}
416
417/// Query parameters for container stats.
418///
419/// When `stream=false` (default), the handler returns a single JSON
420/// [`ContainerStatsResponse`]. When `stream=true`, the handler switches to
421/// Server-Sent Events and emits one `ContainerStatsResponse` sample per
422/// `interval` seconds until the container exits or the client disconnects.
423///
424/// `interval` is clamped to `[1, 60]` seconds. Default interval is `2`.
425#[derive(Debug, Default, Deserialize, IntoParams)]
426pub struct StatsQuery {
427 /// Stream periodic samples as SSE events instead of a one-shot JSON
428 /// response.
429 #[serde(default)]
430 pub stream: bool,
431 /// Sample cadence in seconds (only used when `stream=true`). Clamped to
432 /// `[1, 60]`. Defaults to `2` seconds.
433 #[serde(default, alias = "interval_seconds")]
434 pub interval: Option<u32>,
435}