cloud/
local_driver_glue.rs1use std::collections::BTreeMap;
10use std::path::PathBuf;
11
12use anyhow::{bail, Context, Result};
13use local_driver::{LocalContainerSpec, RuntimePref};
14
15use crate::config::{Provider, ProviderConfig};
16
17pub fn local_container_spec_from_provider(p: &ProviderConfig) -> Result<LocalContainerSpec> {
21 if !matches!(p.kind, Provider::LocalContainer) {
22 bail!(
23 "provider {id} has kind={kind:?}; LocalContainerSpec requires kind=\"local-container\"",
24 id = p.id,
25 kind = p.kind,
26 );
27 }
28 let runtime = match p.fields.get("runtime").and_then(|v| v.as_str()) {
29 Some(s) => RuntimePref::parse(s)?,
30 None => RuntimePref::Auto,
31 };
32 let mut discovery = BTreeMap::new();
33 if let Some(table) = p.fields.get("discovery").and_then(|v| v.as_table()) {
34 for (k, v) in table.iter() {
35 let s = v
36 .as_str()
37 .with_context(|| format!("provider {}: discovery.{k} must be a string", p.id))?;
38 discovery.insert(k.clone(), PathBuf::from(s));
39 }
40 }
41 let custom_docker_host = p
42 .fields
43 .get("custom_docker_host")
44 .and_then(|v| v.as_str())
45 .map(|s| s.to_string());
46 Ok(LocalContainerSpec {
47 runtime,
48 discovery,
49 custom_docker_host,
50 })
51}
52
53#[cfg(test)]
54mod tests {
55 use super::*;
56 use crate::config::{Provider, ProviderConfig};
57 use local_driver::{
58 canonical_label, canonical_name, ContainerRunSpec, ContainerState, DetectedRuntime,
59 LocalRuntime,
60 };
61 use std::collections::BTreeMap;
62 use std::path::PathBuf;
63 use std::time::Duration;
64
65 fn provider_orbstack_toml() -> ProviderConfig {
66 let src = r#"
67schema_version = 1
68id = "orbstack"
69kind = "local-container"
70runtime = "auto"
71
72[discovery]
73orbstack = "~/.orbstack/run/docker.sock"
74colima = "~/.colima/default/docker.sock"
75docker = "/var/run/docker.sock"
76"#;
77 toml::from_str(src).unwrap()
78 }
79
80 #[test]
81 fn spec_from_provider_config_picks_up_runtime_and_discovery() {
82 let p = provider_orbstack_toml();
83 let spec = local_container_spec_from_provider(&p).unwrap();
84 assert_eq!(spec.runtime, RuntimePref::Auto);
85 assert_eq!(spec.discovery.len(), 3);
86 assert!(spec.discovery.contains_key("orbstack"));
87 assert!(spec.discovery.contains_key("colima"));
88 assert!(spec.discovery.contains_key("docker"));
89 assert!(spec.custom_docker_host.is_none());
90 }
91
92 #[test]
93 fn spec_from_provider_config_defaults_runtime_to_auto() {
94 let src = r#"
95schema_version = 1
96id = "orbstack"
97kind = "local-container"
98"#;
99 let p: ProviderConfig = toml::from_str(src).unwrap();
100 let spec = local_container_spec_from_provider(&p).unwrap();
101 assert_eq!(spec.runtime, RuntimePref::Auto);
102 assert!(spec.discovery.is_empty(), "no discovery table → empty map");
103 assert!(spec.custom_docker_host.is_none());
104 }
105
106 #[test]
107 fn spec_from_provider_config_parses_custom_docker_host() {
108 let src = r#"
109schema_version = 1
110id = "custom"
111kind = "local-container"
112runtime = "custom"
113custom_docker_host = "tcp://localhost:2375"
114"#;
115 let p: ProviderConfig = toml::from_str(src).unwrap();
116 let spec = local_container_spec_from_provider(&p).unwrap();
117 assert_eq!(spec.runtime, RuntimePref::Custom);
118 assert_eq!(
119 spec.custom_docker_host.as_deref(),
120 Some("tcp://localhost:2375")
121 );
122 }
123
124 #[test]
125 fn spec_rejects_non_local_container_provider() {
126 let p = ProviderConfig {
127 schema_version: 1,
128 id: "cloudflare".into(),
129 kind: Provider::Cloudflare,
130 credentials: None,
131 fields: BTreeMap::new(),
132 };
133 let err = local_container_spec_from_provider(&p)
134 .unwrap_err()
135 .to_string();
136 assert!(
137 err.contains("Cloudflare"),
138 "error should name the wrong kind, got: {err}"
139 );
140 }
141
142 #[test]
143 fn spec_rejects_bad_runtime_value() {
144 let src = r#"
145schema_version = 1
146id = "orbstack"
147kind = "local-container"
148runtime = "nonsense-runtime"
149"#;
150 let p: ProviderConfig = toml::from_str(src).unwrap();
151 let err = local_container_spec_from_provider(&p)
152 .unwrap_err()
153 .to_string();
154 assert!(
155 err.contains("nonsense-runtime"),
156 "error should name the bad value, got: {err}"
157 );
158 }
159
160 #[test]
161 fn spec_rejects_non_string_discovery_value() {
162 let src = r#"
163schema_version = 1
164id = "orbstack"
165kind = "local-container"
166
167[discovery]
168orbstack = 42
169"#;
170 let p: ProviderConfig = toml::from_str(src).unwrap();
171 let err = local_container_spec_from_provider(&p)
172 .unwrap_err()
173 .to_string();
174 assert!(
175 err.contains("orbstack"),
176 "error should name the bad key, got: {err}"
177 );
178 }
179
180 async fn detect_live() -> Option<LocalRuntime> {
183 let p = provider_orbstack_toml();
184 let spec = local_container_spec_from_provider(&p).ok()?;
185 LocalRuntime::detect(&spec).await.ok()
186 }
187
188 #[tokio::test]
189 #[ignore = "requires docker socket (orbstack/colima/docker running)"]
190 async fn image_cache_skip_pull_when_present() {
191 let Some(runtime) = detect_live().await else {
192 eprintln!("SKIP: no docker socket reachable");
193 return;
194 };
195 let pulled_first = runtime.ensure_image("hello-world:latest").await.unwrap();
196 let pulled_second = runtime.ensure_image("hello-world:latest").await.unwrap();
197 assert!(
198 !pulled_second,
199 "second ensure_image should skip the pull (cache hit); pulled_first={pulled_first}",
200 );
201 }
202
203 #[tokio::test]
204 #[ignore = "requires docker socket"]
205 async fn run_stop_lifecycle_is_idempotent() {
206 let Some(runtime) = detect_live().await else {
207 eprintln!("SKIP: no docker socket reachable");
208 return;
209 };
210 runtime.ensure_image("alpine:3.20").await.unwrap();
211 let spec = ContainerRunSpec {
212 name: canonical_name("itest", "test", "sleeper"),
213 image: "alpine:3.20".into(),
214 label: canonical_label("itest", "test", "sleeper"),
215 ports: vec![],
216 env: BTreeMap::new(),
217 volumes: vec![],
218 cmd: vec!["sleep".into(), "30".into()],
219 cap_add: vec![],
220 cgroupns: None,
221 network: None,
222 network_aliases: vec![],
223 extra_hosts: vec![],
224 };
225 runtime.run(&spec).await.unwrap();
226 let state = runtime.container_state(&spec.name).await.unwrap();
227 assert_eq!(state, Some(ContainerState::Running));
228 runtime.run(&spec).await.unwrap();
229 let state = runtime.container_state(&spec.name).await.unwrap();
230 assert_eq!(state, Some(ContainerState::Running));
231 let owned = runtime.list_owned().await.unwrap();
232 assert!(owned.iter().any(|c| c.name == spec.name));
233 runtime
234 .stop_and_remove(&spec.name, Duration::from_secs(2))
235 .await
236 .unwrap();
237 let state = runtime.container_state(&spec.name).await.unwrap();
238 assert_eq!(state, None);
239 let _ = PathBuf::new();
241 let _ = DetectedRuntime::Orbstack;
242 }
243}