Skip to main content

ci_engine/
service.rs

1// SPDX-License-Identifier: Apache-2.0
2//! Service-container provisioning boundary harvested from treadle.
3
4mod docker;
5mod fake;
6
7use std::process::Command;
8
9use ci_config::Service;
10pub use docker::DockerProvider;
11pub use fake::FakeProvider;
12use thiserror::Error;
13
14/// Provider handles for services started for one check.
15#[derive(Debug, Default)]
16pub struct RunningServices {
17    /// Provider-specific handles in authored order.
18    pub handles: Vec<String>,
19}
20
21/// Service provisioning failure.
22#[derive(Debug, Error)]
23pub enum ServiceError {
24    /// The selected provider cannot run the requested service.
25    #[error(
26        "service container {name:?} ({image}) requested, but this provider cannot run services"
27    )]
28    Unsupported {
29        /// Authored service name.
30        name: String,
31        /// Authored image.
32        image: String,
33    },
34    /// Container startup failed.
35    #[error("service provisioning failed: {0}")]
36    Provision(String),
37    /// Readiness deadline expired.
38    #[error("service {name:?} ({image}) did not become ready within {timeout_secs}s")]
39    NotReady {
40        /// Authored service name.
41        name: String,
42        /// Authored image.
43        image: String,
44        /// Elapsed readiness deadline.
45        timeout_secs: u64,
46    },
47}
48
49/// Service lifecycle provider used by the executor.
50pub trait ServiceProvider {
51    /// Start all requested services.
52    fn up(&self, services: &[Service]) -> Result<RunningServices, ServiceError>;
53    /// Stop services from one successful `up` call.
54    fn down(&self, running: RunningServices) -> Result<(), ServiceError>;
55}
56
57/// Provider for local mode: no implicit container runtime.
58#[derive(Debug, Clone, Copy, Default)]
59pub struct NoopProvider;
60
61impl ServiceProvider for NoopProvider {
62    fn up(&self, services: &[Service]) -> Result<RunningServices, ServiceError> {
63        match services.first() {
64            Some(service) => Err(ServiceError::Unsupported {
65                name: service.name.clone(),
66                image: service.image.clone(),
67            }),
68            None => Ok(RunningServices::default()),
69        }
70    }
71
72    fn down(&self, _running: RunningServices) -> Result<(), ServiceError> {
73        Ok(())
74    }
75}
76
77/// Captured outcome of one external runtime command.
78#[derive(Debug, Clone, PartialEq, Eq)]
79pub struct CommandOutcome {
80    /// Whether the command exited successfully.
81    pub success: bool,
82    /// Combined diagnostic text.
83    pub output: String,
84}
85
86/// Boundary around external container-runtime commands.
87pub trait CommandRunner: Send + Sync {
88    /// Run one argv.
89    fn run(&self, program: &str, args: &[String]) -> Result<CommandOutcome, ServiceError>;
90}
91
92/// Real external-command boundary used by [`DockerProvider`].
93#[derive(Debug, Clone, Copy, Default)]
94pub struct RealCommandRunner;
95
96impl CommandRunner for RealCommandRunner {
97    fn run(&self, program: &str, args: &[String]) -> Result<CommandOutcome, ServiceError> {
98        let output = Command::new(program)
99            .args(args)
100            .output()
101            .map_err(|error| ServiceError::Provision(format!("spawn {program}: {error}")))?;
102        let mut text = String::from_utf8_lossy(&output.stdout).into_owned();
103        text.push_str(&String::from_utf8_lossy(&output.stderr));
104        Ok(CommandOutcome {
105            success: output.status.success(),
106            output: text,
107        })
108    }
109}