Skip to main content

lightshuttle_runtime/
runtime.rs

1//! Container runtime abstraction and its supporting domain types.
2//!
3//! Defines the [`ContainerRuntime`] trait and the value types it operates on:
4//! [`ContainerId`], [`ContainerStatus`], [`LogChunk`], [`LogStream`], and the
5//! [`LogChunkStream`] type alias. Concrete implementations (e.g.
6//! [`crate::DockerRuntime`]) live in sibling modules.
7
8use std::pin::Pin;
9use std::time::{Duration, SystemTime};
10
11use futures::stream::Stream;
12
13use crate::error::Result;
14use lightshuttle_spec::ContainerSpec;
15
16/// Opaque identifier for a container managed by the runtime.
17///
18/// The internal representation is whatever string the underlying daemon
19/// uses (Docker returns 64-character hexadecimal hashes); callers must
20/// not depend on the format.
21#[derive(Debug, Clone, PartialEq, Eq, Hash)]
22pub struct ContainerId(String);
23
24impl ContainerId {
25    /// Build a [`ContainerId`] from a daemon-supplied string.
26    #[must_use]
27    pub fn new(id: impl Into<String>) -> Self {
28        Self(id.into())
29    }
30
31    /// Borrow the raw identifier string.
32    #[must_use]
33    pub fn as_str(&self) -> &str {
34        &self.0
35    }
36}
37
38impl std::fmt::Display for ContainerId {
39    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
40        f.write_str(&self.0)
41    }
42}
43
44/// Lifecycle status reported by the runtime when inspecting a container.
45#[derive(Debug, Clone, PartialEq, Eq)]
46pub enum ContainerStatus {
47    /// The runtime has accepted the start request but the container is
48    /// not yet running.
49    Starting,
50
51    /// The container is running and either has no healthcheck or has
52    /// not produced a healthcheck result yet.
53    Running,
54
55    /// The container is running and reports a healthy healthcheck.
56    Healthy,
57
58    /// The container is running and reports an unhealthy healthcheck.
59    Unhealthy,
60
61    /// The container has exited.
62    Stopped {
63        /// Exit code reported by the container, when known.
64        exit_code: Option<i32>,
65    },
66}
67
68/// Which stream a log chunk came from.
69#[derive(Debug, Clone, Copy, PartialEq, Eq)]
70pub enum LogStream {
71    /// Standard output.
72    Stdout,
73    /// Standard error.
74    Stderr,
75}
76
77/// One chunk of streamed log output.
78#[derive(Debug, Clone)]
79pub struct LogChunk {
80    /// Source stream of the chunk.
81    pub stream: LogStream,
82    /// Wall-clock timestamp reported by the runtime.
83    pub timestamp: SystemTime,
84    /// Raw bytes of the chunk; may or may not end with a newline.
85    pub bytes: Vec<u8>,
86}
87
88/// Boxed, pinned stream of [`LogChunk`] items for a single container.
89///
90/// Returned by [`ContainerRuntime::logs`]. The stream is `Send` so it can be
91/// forwarded across async task boundaries (e.g. from a worker task to an HTTP
92/// response body or a WebSocket session).
93pub type LogChunkStream = Pin<Box<dyn Stream<Item = Result<LogChunk>> + Send>>;
94
95/// Container runtime abstraction.
96///
97/// The trait is intentionally narrow: it exposes only the operations
98/// that the lifecycle manager needs. Daemon-specific capabilities
99/// (network inspection, image management) stay private to each
100/// implementation.
101///
102/// Implementations live in submodules such as [`crate::DockerRuntime`].
103pub trait ContainerRuntime: Send + Sync {
104    /// Start a container according to `spec`. Pulls the image if not
105    /// already present locally.
106    fn start(
107        &self,
108        spec: &ContainerSpec,
109    ) -> impl std::future::Future<Output = Result<ContainerId>> + Send;
110
111    /// Stop a container, sending `SIGTERM` and then `SIGKILL` after
112    /// `grace`. Idempotent: stopping an already stopped container is a
113    /// no-op.
114    fn stop(
115        &self,
116        id: &ContainerId,
117        grace: Duration,
118    ) -> impl std::future::Future<Output = Result<()>> + Send;
119
120    /// Remove a container by name, forcing removal even if it is still
121    /// running. Idempotent: removing a container that does not exist is a
122    /// no-op. Named volumes are preserved.
123    ///
124    /// The lifecycle manager calls this before every `start` so that a
125    /// re-up or restart replaces the previous container instead of
126    /// colliding with its name.
127    fn remove(&self, name: &str) -> impl std::future::Future<Output = Result<()>> + Send;
128
129    /// Report the current status of a container.
130    fn inspect(
131        &self,
132        id: &ContainerId,
133    ) -> impl std::future::Future<Output = Result<ContainerStatus>> + Send;
134
135    /// Block until the container reports a healthy status or `timeout`
136    /// elapses. Returns [`crate::RuntimeError::Timeout`] in the latter
137    /// case.
138    fn wait_healthy(
139        &self,
140        id: &ContainerId,
141        timeout: Duration,
142    ) -> impl std::future::Future<Output = Result<()>> + Send;
143
144    /// Stream logs from a container. When `follow` is true the stream
145    /// stays open and emits new chunks as they arrive; when false the
146    /// stream completes after the existing logs are drained.
147    fn logs(
148        &self,
149        id: &ContainerId,
150        follow: bool,
151    ) -> impl std::future::Future<Output = Result<LogChunkStream>> + Send;
152
153    /// Ensure a per-project user-defined bridge network exists, creating
154    /// it when absent. Idempotent: concurrent calls are safe because a
155    /// `409 Conflict` response (network already exists) is treated as
156    /// success. Containers attached to this network can reach each other
157    /// by their container name, enabling `resources.<name>.url` hostnames
158    /// to resolve without extra configuration.
159    fn ensure_project_network(
160        &self,
161        project: &str,
162    ) -> impl std::future::Future<Output = Result<()>> + Send;
163
164    /// Remove the per-project bridge network. Idempotent: a `404 Not
165    /// Found` response is treated as success. Call after all containers
166    /// belonging to the project have been removed; Docker refuses to
167    /// delete a network that still has active endpoints.
168    fn teardown_project_network(
169        &self,
170        project: &str,
171    ) -> impl std::future::Future<Output = Result<()>> + Send;
172}