use std::path::PathBuf;
use foundation_db::core::state::FileStateStore;
use foundation_deployment::provider_client::ProviderClient;
use foundation_deployment::traits::{BoxFuture, Deployable};
use foundation_netio::shared::client::dns::SystemDnsResolver;
use serde::{Deserialize, Serialize};
use crate::client::{DockerClient, DEFAULT_DOCKER_SOCKET};
use crate::error::DockerError;
#[derive(Debug, Clone)]
pub struct ContainerDeployment {
pub image: String,
pub name: Option<String>,
pub cmd: Vec<String>,
pub env: Vec<String>,
pub socket_path: PathBuf,
}
impl ContainerDeployment {
#[must_use]
pub fn new(image: impl Into<String>) -> Self {
Self {
image: image.into(),
name: None,
cmd: Vec::new(),
env: Vec::new(),
socket_path: PathBuf::from(DEFAULT_DOCKER_SOCKET),
}
}
#[must_use]
pub fn with_name(mut self, name: impl Into<String>) -> Self {
self.name = Some(name.into());
self
}
#[must_use]
pub fn with_cmd<I, S>(mut self, cmd: I) -> Self
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
self.cmd = cmd.into_iter().map(Into::into).collect();
self
}
#[must_use]
pub fn with_env(mut self, entry: impl Into<String>) -> Self {
self.env.push(entry.into());
self
}
#[must_use]
pub fn with_socket(mut self, socket_path: impl Into<PathBuf>) -> Self {
self.socket_path = socket_path.into();
self
}
fn create_body(&self) -> serde_json::Value {
let mut body = serde_json::json!({ "Image": self.image });
if !self.cmd.is_empty() {
body["Cmd"] = serde_json::json!(self.cmd);
}
if !self.env.is_empty() {
body["Env"] = serde_json::json!(self.env);
}
body
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ContainerDeployOutput {
pub container_id: String,
}
impl Deployable for ContainerDeployment {
const NAMESPACE: &'static str = "docker/containers";
type DeployOutput = ContainerDeployOutput;
type DestroyOutput = ();
type Error = DockerError;
type Store = FileStateStore;
type Resolver = SystemDnsResolver;
fn deploy(
&self,
instance_id: usize,
client: ProviderClient<Self::Store, Self::Resolver>,
) -> BoxFuture<'static, Result<Self::DeployOutput, Self::Error>> {
let docker = DockerClient::connect_unix(&self.socket_path);
let body = self.create_body();
let name = self.name.clone();
let store = self.store(&client);
Box::pin(async move {
let created = docker.create_container(&body, name.as_deref()).await?;
docker.start_container(&created.id).await?;
let output = ContainerDeployOutput { container_id: created.id };
store
.store_typed(&instance_id.to_string(), &output)
.map_err(|e| DockerError::Unavailable(format!("persist deploy state: {e}")))?;
Ok(output)
})
}
fn destroy(
&self,
instance_id: usize,
client: ProviderClient<Self::Store, Self::Resolver>,
) -> BoxFuture<'static, Result<Self::DestroyOutput, Self::Error>> {
let docker = DockerClient::connect_unix(&self.socket_path);
let store = self.store(&client);
Box::pin(async move {
let key = instance_id.to_string();
let output: ContainerDeployOutput = store
.get_typed(&key)
.map_err(|e| DockerError::Unavailable(format!("load deploy state: {e}")))?
.ok_or_else(|| {
DockerError::Unavailable(format!(
"no recorded container for instance {instance_id} — nothing to destroy"
))
})?;
docker.stop_container(&output.container_id, Some(10)).await?;
docker.remove_container(&output.container_id, true).await?;
store
.remove(&key)
.map_err(|e| DockerError::Unavailable(format!("clear deploy state: {e}")))?;
Ok(())
})
}
}