Skip to main content

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}