koi_runtime/backend.rs
1//! Runtime backend trait.
2//!
3//! Each container/VM/service runtime implements this trait to provide
4//! lifecycle events and instance metadata in a normalized format.
5
6use tokio::sync::mpsc;
7
8use crate::error::RuntimeError;
9use crate::instance::Instance;
10
11/// Lifecycle event emitted by a runtime backend.
12#[derive(Debug, Clone)]
13pub enum RuntimeEvent {
14 /// A new instance was detected or an existing one started.
15 Started(Instance),
16 /// An instance stopped or was destroyed.
17 Stopped {
18 /// Runtime-assigned instance ID.
19 id: String,
20 /// Human-readable name.
21 name: String,
22 },
23 /// An instance's metadata or ports changed (e.g., Docker network reconnect).
24 Updated(Instance),
25 /// The backend lost connection to the runtime API.
26 BackendDisconnected { backend: String, reason: String },
27 /// The backend reconnected and completed reconciliation.
28 BackendReconnected { backend: String },
29}
30
31/// A runtime backend that watches lifecycle events and resolves instance metadata.
32///
33/// Implementations are expected to:
34/// - Connect to the runtime API on `connect()`
35/// - Stream lifecycle events via the `mpsc::Sender` in `watch()`
36/// - Handle reconnection internally (emit `BackendDisconnected`/`BackendReconnected`)
37/// - Provide a point-in-time snapshot via `list_instances()`
38#[async_trait::async_trait]
39pub trait RuntimeBackend: Send + Sync {
40 /// Backend name for logging and status (e.g., "docker", "podman", "systemd").
41 fn name(&self) -> &'static str;
42
43 /// Attempt to connect to the runtime API.
44 ///
45 /// Returns an error if the runtime is not available (socket missing,
46 /// permission denied, API unreachable).
47 async fn connect(&mut self) -> Result<(), RuntimeError>;
48
49 /// List all currently running instances.
50 ///
51 /// Used for reconciliation on startup: the caller diffs this list
52 /// against Koi's current registrations.
53 async fn list_instances(&self) -> Result<Vec<Instance>, RuntimeError>;
54
55 /// Watch for lifecycle events, sending them to the provided channel.
56 ///
57 /// This method should run until the cancellation token is triggered
58 /// or the channel is closed. It should handle transient failures
59 /// (API disconnects) by emitting `BackendDisconnected`, reconnecting,
60 /// and emitting `BackendReconnected` with a fresh reconciliation.
61 async fn watch(
62 &self,
63 tx: mpsc::Sender<RuntimeEvent>,
64 cancel: tokio_util::sync::CancellationToken,
65 ) -> Result<(), RuntimeError>;
66}
67
68/// Selectable runtime backend kinds.
69#[derive(Debug, Clone, Copy, PartialEq, Eq)]
70pub enum RuntimeBackendKind {
71 /// Auto-detect available runtime (Docker → Podman → systemd → Incus).
72 Auto,
73 /// Docker Engine API.
74 Docker,
75 /// Podman (Docker-compatible API, different default socket).
76 Podman,
77 /// systemd D-Bus.
78 Systemd,
79 /// Incus/LXD REST API.
80 Incus,
81 /// Kubernetes watch API.
82 Kubernetes,
83}
84
85impl RuntimeBackendKind {
86 /// Parse from a CLI string.
87 pub fn from_str_loose(s: &str) -> Option<Self> {
88 match s.to_lowercase().as_str() {
89 "auto" => Some(Self::Auto),
90 "docker" => Some(Self::Docker),
91 "podman" => Some(Self::Podman),
92 "systemd" => Some(Self::Systemd),
93 "incus" | "lxc" | "lxd" => Some(Self::Incus),
94 "kubernetes" | "k8s" => Some(Self::Kubernetes),
95 _ => None,
96 }
97 }
98}
99
100impl std::fmt::Display for RuntimeBackendKind {
101 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
102 match self {
103 Self::Auto => write!(f, "auto"),
104 Self::Docker => write!(f, "docker"),
105 Self::Podman => write!(f, "podman"),
106 Self::Systemd => write!(f, "systemd"),
107 Self::Incus => write!(f, "incus"),
108 Self::Kubernetes => write!(f, "kubernetes"),
109 }
110 }
111}