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