1use std::collections::{BTreeMap, BTreeSet};
4use std::path::{Path, PathBuf};
5use std::sync::{Arc, Mutex, MutexGuard};
6
7use async_trait::async_trait;
8use futures::StreamExt;
9use tokio::sync::broadcast;
10
11use crate::engine::{ComposeEngine, DownOptions, EngineStream, LogOptions, UpOptions};
12use crate::error::{Error, Result};
13use crate::events::{EngineEvent, EventAction, EventFilter};
14use crate::logs::LogLine;
15use crate::status::{Health, ProjectStatus, ServiceStatus, State};
16
17#[derive(Clone, Debug, PartialEq, Eq)]
19pub enum FakeCall {
20 Up {
22 dir: PathBuf,
24 project: String,
26 env: BTreeMap<String, String>,
28 },
29 Down {
31 project: String,
33 volumes: bool,
35 },
36 NetworkRm(String),
38}
39
40#[derive(Default)]
41struct Inner {
42 projects: BTreeMap<String, Vec<ServiceStatus>>,
43 dirs: BTreeMap<String, PathBuf>,
45 scripts: BTreeMap<String, Vec<(String, Health)>>,
47 fail_up: BTreeSet<String>,
48 networks: BTreeSet<String>,
49 unavailable: bool,
50 calls: Vec<FakeCall>,
51 clock: i64,
52}
53
54#[derive(Clone)]
62pub struct FakeEngine {
63 inner: Arc<Mutex<Inner>>,
64 events: broadcast::Sender<EngineEvent>,
65}
66
67impl Default for FakeEngine {
68 fn default() -> Self {
69 FakeEngine {
70 inner: Arc::default(),
71 events: broadcast::channel(1024).0,
72 }
73 }
74}
75
76impl FakeEngine {
77 pub fn new() -> Self {
79 Self::default()
80 }
81
82 fn lock(&self) -> MutexGuard<'_, Inner> {
83 self.inner.lock().unwrap_or_else(|e| e.into_inner())
84 }
85
86 pub fn script(
88 &self,
89 project: &str,
90 services: impl IntoIterator<Item = (&'static str, Health)>,
91 ) {
92 self.lock().scripts.insert(
93 project.to_owned(),
94 services
95 .into_iter()
96 .map(|(s, h)| (s.to_owned(), h))
97 .collect(),
98 );
99 }
100
101 pub fn fail_up(&self, project: &str) {
103 self.lock().fail_up.insert(project.to_owned());
104 }
105
106 pub fn set_unavailable(&self, unavailable: bool) {
108 self.lock().unavailable = unavailable;
109 }
110
111 pub fn adopt(&self, project: &str) {
113 let services = self.services_for(project);
114 self.lock().projects.insert(project.to_owned(), services);
115 }
116
117 pub fn add_network(&self, name: &str) {
119 self.lock().networks.insert(name.to_owned());
120 }
121
122 pub fn networks(&self) -> BTreeSet<String> {
124 self.lock().networks.clone()
125 }
126
127 pub fn projects(&self) -> BTreeSet<String> {
129 self.lock().projects.keys().cloned().collect()
130 }
131
132 pub fn calls(&self) -> Vec<FakeCall> {
134 self.lock().calls.clone()
135 }
136
137 pub fn set_health(&self, project: &str, service: &str, health: Health) {
139 let mut inner = self.lock();
140 let Some(container) = inner.projects.get_mut(project).and_then(|services| {
141 let s = services.iter_mut().find(|s| s.service == service)?;
142 s.health = health;
143 Some(s.container.clone())
144 }) else {
145 return;
146 };
147 let event = next_event(
148 &mut inner,
149 project,
150 service,
151 &container,
152 EventAction::Health(health),
153 );
154 drop(inner);
155 let _ = self.events.send(event);
156 }
157
158 fn services_for(&self, project: &str) -> Vec<ServiceStatus> {
159 let script = self
160 .lock()
161 .scripts
162 .get(project)
163 .cloned()
164 .unwrap_or_else(|| vec![("app".to_owned(), Health::Healthy)]);
165 script
166 .into_iter()
167 .map(|(service, health)| ServiceStatus {
168 container: format!("{project}-{service}-1"),
169 service,
170 state: State::Running,
171 health,
172 exit_code: 0,
173 status: "Up".into(),
174 published: Vec::new(),
175 })
176 .collect()
177 }
178
179 fn check_available(&self) -> Result<()> {
180 if self.lock().unavailable {
181 return Err(Error::Unavailable {
182 reason: "the fake Docker daemon is down".into(),
183 });
184 }
185 Ok(())
186 }
187
188 fn emit(&self, project: &str, services: &[ServiceStatus], actions: &[fn() -> EventAction]) {
189 let mut inner = self.lock();
190 let mut events = Vec::new();
191 for s in services {
192 for action in actions {
193 events.push(next_event(
194 &mut inner,
195 project,
196 &s.service,
197 &s.container,
198 action(),
199 ));
200 }
201 }
202 drop(inner);
203 for e in events {
204 let _ = self.events.send(e);
205 }
206 }
207}
208
209fn next_event(
210 inner: &mut Inner,
211 project: &str,
212 service: &str,
213 container: &str,
214 action: EventAction,
215) -> EngineEvent {
216 inner.clock += 1;
217 EngineEvent {
218 project: project.to_owned(),
219 service: service.to_owned(),
220 container: container.to_owned(),
221 action,
222 time_nanos: inner.clock,
223 }
224}
225
226fn project_in(dir: &Path) -> Result<String> {
228 let path = dir.join("compose.yaml");
229 let text = std::fs::read_to_string(&path).map_err(|source| Error::Io {
230 command: format!("read {}", path.display()),
231 source,
232 })?;
233 text.lines()
234 .find_map(|l| l.strip_prefix("name:"))
235 .map(|n| n.trim().trim_matches('"').to_owned())
236 .ok_or_else(|| Error::Io {
237 command: format!("read {}", path.display()),
238 source: std::io::Error::other("no `name:` in the head compose.yaml"),
239 })
240}
241
242#[async_trait]
243impl ComposeEngine for FakeEngine {
244 async fn up(&self, dir: &Path, opts: &UpOptions) -> Result<()> {
245 self.check_available()?;
246 let project = project_in(dir)?;
247 self.lock().calls.push(FakeCall::Up {
248 dir: dir.to_owned(),
249 project: project.clone(),
250 env: opts.env.clone(),
251 });
252 let services = self.services_for(&project);
253 let failed = self.lock().fail_up.remove(&project);
254 self.lock()
255 .projects
256 .insert(project.clone(), services.clone());
257 self.lock().dirs.insert(project.clone(), dir.to_owned());
258 self.emit(
259 &project,
260 &services,
261 &[|| EventAction::Create, || EventAction::Start],
262 );
263 if failed {
264 return Err(Error::Failed {
265 command: "compose up -d --wait".into(),
266 status: exit_status(1),
267 stderr: format!("container {project}-app-1 is unhealthy"),
268 });
269 }
270 Ok(())
271 }
272
273 async fn down(&self, dir: &Path, opts: &DownOptions) -> Result<()> {
274 self.check_available()?;
275 let project = project_in(dir)?;
276 self.lock().calls.push(FakeCall::Down {
277 project: project.clone(),
278 volumes: opts.volumes,
279 });
280 let removed = self.lock().projects.remove(&project);
281 self.lock().dirs.remove(&project);
282 if let Some(services) = removed {
283 self.emit(
284 &project,
285 &services,
286 &[
287 || EventAction::Stop,
288 || EventAction::Die { exit_code: Some(0) },
289 || EventAction::Destroy,
290 ],
291 );
292 }
293 Ok(())
294 }
295
296 async fn ls(&self) -> Result<Vec<ProjectStatus>> {
297 self.check_available()?;
298 let inner = self.lock();
299 Ok(inner
300 .projects
301 .iter()
302 .map(|(name, services)| {
303 let mut states = BTreeMap::new();
304 for s in services {
305 let state = match &s.state {
306 State::Running => "running",
307 State::Exited => "exited",
308 _ => "created",
309 };
310 *states.entry(state.to_owned()).or_insert(0) += 1;
311 }
312 let status = states
313 .iter()
314 .map(|(s, n)| format!("{s}({n})"))
315 .collect::<Vec<_>>()
316 .join(", ");
317 ProjectStatus {
318 name: name.clone(),
319 status,
320 states,
321 config_files: inner
322 .dirs
323 .get(name)
324 .map(|d| d.join("compose.yaml").display().to_string())
325 .unwrap_or_default(),
326 }
327 })
328 .collect())
329 }
330
331 async fn ps(&self, project: &str) -> Result<Vec<ServiceStatus>> {
332 self.check_available()?;
333 Ok(self
334 .lock()
335 .projects
336 .get(project)
337 .cloned()
338 .unwrap_or_default())
339 }
340
341 async fn logs(&self, project: &str, _opts: &LogOptions) -> Result<EngineStream<LogLine>> {
342 self.check_available()?;
343 let lines: Vec<Result<LogLine>> = self
344 .lock()
345 .projects
346 .get(project)
347 .into_iter()
348 .flatten()
349 .map(|s| {
350 Ok(LogLine {
351 container: format!("{}-1", s.service),
352 timestamp: None,
353 text: format!("{} started", s.service),
354 })
355 })
356 .collect();
357 Ok(futures::stream::iter(lines).boxed())
358 }
359
360 async fn events(&self, filter: &EventFilter) -> Result<EngineStream<EngineEvent>> {
361 self.check_available()?;
362 let filter = filter.clone();
363 let rx = self.events.subscribe();
364 let stream = futures::stream::unfold(rx, |mut rx| async move {
365 loop {
366 match rx.recv().await {
367 Ok(event) => return Some((event, rx)),
368 Err(broadcast::error::RecvError::Lagged(_)) => continue,
369 Err(broadcast::error::RecvError::Closed) => return None,
370 }
371 }
372 })
373 .filter(move |e| futures::future::ready(filter.matches(e)))
374 .map(Ok);
375 Ok(stream.boxed())
376 }
377
378 async fn network_rm(&self, name: &str) -> Result<bool> {
379 self.check_available()?;
380 let mut inner = self.lock();
381 inner.calls.push(FakeCall::NetworkRm(name.to_owned()));
382 Ok(inner.networks.remove(name))
383 }
384}
385
386#[cfg(unix)]
387fn exit_status(code: i32) -> std::process::ExitStatus {
388 use std::os::unix::process::ExitStatusExt;
389 std::process::ExitStatus::from_raw(code << 8)
390}
391
392#[cfg(windows)]
393fn exit_status(code: i32) -> std::process::ExitStatus {
394 use std::os::windows::process::ExitStatusExt;
395 std::process::ExitStatus::from_raw(code as u32)
396}