use std::collections::HashMap;
use std::error::Error as StdError;
use std::fmt;
use std::time::Duration;
#[derive(Debug)]
pub struct Error {
message: String,
source: Option<Box<dyn StdError + Send + Sync>>,
}
impl Error {
pub fn new(message: impl Into<String>) -> Self {
Self {
message: message.into(),
source: None,
}
}
pub fn with_source(
message: impl Into<String>,
source: impl StdError + Send + Sync + 'static,
) -> Self {
Self {
message: message.into(),
source: Some(Box::new(source)),
}
}
}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.message)
}
}
impl StdError for Error {
fn source(&self) -> Option<&(dyn StdError + 'static)> {
self.source.as_ref().map(|e| e.as_ref() as _)
}
}
#[derive(Debug, Clone)]
pub struct TaskSpec {
pub name: String,
pub command: String,
pub image: Option<String>,
pub workdir: Option<String>,
pub env: HashMap<String, String>,
pub mounts: Vec<MountSpec>,
pub timeout: Option<u32>,
pub services: Vec<ServiceSpec>,
}
impl TaskSpec {
pub fn new(name: impl Into<String>, command: impl Into<String>) -> Self {
Self {
name: name.into(),
command: command.into(),
image: None,
workdir: None,
env: HashMap::new(),
mounts: Vec::new(),
timeout: None,
services: Vec::new(),
}
}
}
#[derive(Debug, Clone)]
pub struct MountSpec {
pub source: String,
pub target: String,
pub mount_type: MountType,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MountType {
Directory,
Cache,
}
#[derive(Debug, Clone)]
pub struct ServiceSpec {
pub name: String,
pub image: String,
}
#[derive(Debug, Clone)]
pub struct Result {
pub success: bool,
pub exit_code: i32,
pub output: String,
pub duration: Duration,
pub error: Option<String>,
}
impl Result {
pub fn success() -> Self {
Self {
success: true,
exit_code: 0,
output: String::new(),
duration: Duration::ZERO,
error: None,
}
}
pub fn success_with_output(output: impl Into<String>, duration: Duration) -> Self {
Self {
success: true,
exit_code: 0,
output: output.into(),
duration,
error: None,
}
}
pub fn failure(exit_code: i32, output: impl Into<String>) -> Self {
Self {
success: false,
exit_code,
output: output.into(),
duration: Duration::ZERO,
error: None,
}
}
pub fn error(message: impl Into<String>) -> Self {
Self {
success: false,
exit_code: 1,
output: String::new(),
duration: Duration::ZERO,
error: Some(message.into()),
}
}
}
pub trait Target {
fn run_task(&self, task: &TaskSpec) -> Result;
}
pub trait Lifecycle {
fn setup(&mut self) -> std::result::Result<(), Error>;
fn teardown(&mut self) -> std::result::Result<(), Error>;
}
pub trait Secrets {
fn resolve_secret(&self, name: &str) -> std::result::Result<String, Error>;
}
#[derive(Debug, Clone)]
pub struct Volume {
pub id: String,
pub host_path: Option<String>,
pub reference: String,
}
#[derive(Debug, Clone, Default)]
pub struct VolumeOptions {
pub size: Option<String>,
}
pub trait Storage {
fn create_volume(&self, name: &str, opts: &VolumeOptions)
-> std::result::Result<Volume, Error>;
fn artifact_path(&self, task_name: &str, artifact_name: &str) -> String;
fn copy_artifact(&self, src: &str, dst: &str) -> std::result::Result<(), Error>;
}
#[derive(Debug, Clone)]
pub struct NetworkInfo {
pub network: String,
pub containers: Vec<String>,
}
pub trait Services {
fn start_services(
&self,
task_name: &str,
services: &[ServiceSpec],
) -> std::result::Result<NetworkInfo, Error>;
fn stop_services(&self, network_info: &NetworkInfo) -> std::result::Result<(), Error>;
}
pub fn has_lifecycle<T: Target + ?Sized>(_target: &T) -> bool {
false
}
#[derive(Debug, Clone, Default)]
pub struct EnvSecrets;
impl Secrets for EnvSecrets {
fn resolve_secret(&self, name: &str) -> std::result::Result<String, Error> {
std::env::var(name).map_err(|_| Error::new(format!("secret not found: {}", name)))
}
}
#[cfg(test)]
mod tests {
use super::*;
struct TestTarget;
impl Target for TestTarget {
fn run_task(&self, _task: &TaskSpec) -> Result {
Result::success()
}
}
#[test]
fn test_target_trait() {
let target = TestTarget;
let task = TaskSpec::new("test", "echo hello");
let result = target.run_task(&task);
assert!(result.success);
}
#[test]
fn test_env_secrets() {
std::env::set_var("TEST_SECRET", "secret_value");
let secrets = EnvSecrets;
let value = secrets.resolve_secret("TEST_SECRET").unwrap();
assert_eq!(value, "secret_value");
std::env::remove_var("TEST_SECRET");
}
#[test]
fn test_env_secrets_not_found() {
let secrets = EnvSecrets;
let result = secrets.resolve_secret("NONEXISTENT_SECRET");
assert!(result.is_err());
}
#[test]
fn test_result_success() {
let result = Result::success();
assert!(result.success);
assert_eq!(result.exit_code, 0);
}
#[test]
fn test_result_failure() {
let result = Result::failure(1, "error output");
assert!(!result.success);
assert_eq!(result.exit_code, 1);
assert_eq!(result.output, "error output");
}
}