use foundation_netio::shared::http::SimpleHeader;
use foundation_netio::{DynNetClient, HttpClientBuilder, PreparedRequestBuilder};
use serde::{Deserialize, Serialize};
use std::path::{Path, PathBuf};
use crate::error::DockerError;
use crate::generated::auth::AuthResponse;
use crate::generated::containers::{container_delete_request, ContainerDeleteArgs};
use crate::generated::create::{container_create_request, ContainerCreateArgs, ContainerCreateResponse};
use crate::generated::json::{container_inspect_request, ContainerInspectArgs, ContainerInspectResponse};
use crate::generated::pause::{container_pause_request, ContainerPauseArgs};
use crate::generated::shared::{ApiError, IDResponse};
use crate::generated::start::{container_start_request, ContainerStartArgs};
use crate::generated::stop::{container_stop_request, ContainerStopArgs};
use crate::generated::unpause::{container_unpause_request, ContainerUnpauseArgs};
use crate::generated::version::{system_version_request, SystemVersion, SystemVersionArgs};
use crate::generated::wait::{container_wait_request, ContainerWaitArgs, ContainerWaitResponse};
pub const DEFAULT_DOCKER_SOCKET: &str = "/var/run/docker.sock";
pub const DEFAULT_API_VERSION: &str = "1.53";
pub(crate) type NoMod = fn(&mut PreparedRequestBuilder);
#[derive(Debug, Clone, Default, Serialize)]
pub struct ContainerCreateBody {
#[serde(flatten)]
pub config: crate::generated::json::ContainerConfig,
#[serde(
rename = "HostConfig",
skip_serializing_if = "Option::is_none"
)]
pub host_config: Option<ContainerHostConfig>,
#[serde(
rename = "NetworkingConfig",
skip_serializing_if = "Option::is_none"
)]
pub networking_config: Option<NetworkingConfig>,
}
impl ContainerCreateBody {
#[must_use]
pub fn new(image: impl Into<String>) -> Self {
let mut config = crate::generated::json::ContainerConfig::default();
config.image = Some(image.into());
Self { config, host_config: None, networking_config: None }
}
#[must_use]
pub fn with_cmd(mut self, cmd: Vec<String>) -> Self {
self.config.cmd = Some(cmd);
self
}
#[must_use]
pub fn with_env(mut self, env: Vec<(String, String)>) -> Self {
self.config.env = Some(env.into_iter().map(|(k, v)| format!("{k}={v}")).collect());
self
}
#[must_use]
pub fn with_exposed_ports(mut self, ports: serde_json::Value) -> Self {
self.config.exposed_ports = Some(ports);
self
}
#[must_use]
pub fn with_labels(mut self, labels: serde_json::Value) -> Self {
self.config.labels = Some(labels);
self
}
#[must_use]
pub fn with_host_config(mut self, hc: ContainerHostConfig) -> Self {
self.host_config = Some(hc);
self
}
}
#[derive(Debug, Clone, Default, Serialize)]
pub struct ContainerHostConfig {
#[serde(flatten)]
pub base: crate::generated::json::HostConfig,
#[serde(flatten)]
pub resources: crate::generated::json::Resources,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct NetworkingConfig {
#[serde(rename = "EndpointsConfig")]
pub endpoints_config: std::collections::HashMap<String, crate::generated::connect::EndpointSettings>,
}
pub mod containers_lifecycle;
pub mod networks;
pub mod volumes;
pub mod images;
pub mod build_context;
pub mod exec;
pub mod system;
#[cfg(feature = "ssh")]
pub mod ssh;
#[derive(Clone)]
pub struct DockerClient {
http: DynNetClient,
socket_path: PathBuf,
api_version: String,
remote_host: Option<String>,
tls: bool,
}
#[derive(Clone)]
pub struct DockerTls {
pub ca_pem: Option<Vec<u8>>,
pub cert_pem: Vec<u8>,
pub key_pem: Vec<u8>,
pub verify: bool,
}
impl DockerTls {
pub fn from_cert_dir(dir: impl AsRef<Path>, verify: bool) -> Result<Self, DockerError> {
let dir = dir.as_ref();
let read = |name: &str| {
std::fs::read(dir.join(name))
.map_err(|e| DockerError::Unavailable(format!("read {name}: {e}")))
};
Ok(Self {
ca_pem: std::fs::read(dir.join("ca.pem")).ok(),
cert_pem: read("cert.pem")?,
key_pem: read("key.pem")?,
verify,
})
}
}
impl std::fmt::Debug for DockerClient {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("DockerClient")
.field("socket_path", &self.socket_path)
.field("api_version", &self.api_version)
.field("remote_host", &self.remote_host)
.finish_non_exhaustive()
}
}
impl DockerClient {
#[must_use]
pub fn connect_unix(socket_path: impl Into<PathBuf>) -> Self {
let socket_path = socket_path.into();
let http = HttpClientBuilder::new()
.unix_socket(&socket_path)
.read_timeout(std::time::Duration::from_secs(120))
.build();
Self {
http,
socket_path,
api_version: DEFAULT_API_VERSION.to_string(),
remote_host: None,
tls: false,
}
}
#[must_use]
pub fn connect_tcp(host: &str) -> Self {
let socket_path = PathBuf::from(format!("tcp://{host}"));
let http = HttpClientBuilder::new()
.read_timeout(std::time::Duration::from_secs(120))
.build();
Self {
http,
socket_path,
api_version: DEFAULT_API_VERSION.to_string(),
remote_host: Some(host.to_string()),
tls: false,
}
}
pub fn connect_tls(host: &str, tls: DockerTls) -> Result<Self, DockerError> {
use foundation_netio::netcap::ssl::SSLConnector;
let connector = SSLConnector::from_client_mutual_pem(
tls.ca_pem.as_deref(),
&tls.cert_pem,
&tls.key_pem,
tls.verify,
)
.map_err(|e| DockerError::Unavailable(format!("build TLS connector: {e}")))?;
let http = HttpClientBuilder::new()
.with_tls_connector(connector)
.read_timeout(std::time::Duration::from_secs(120))
.build();
Ok(Self {
http,
socket_path: PathBuf::from(format!("tcp://{host}")),
api_version: DEFAULT_API_VERSION.to_string(),
remote_host: Some(host.to_string()),
tls: true,
})
}
#[cfg(feature = "ssh")]
pub fn connect_ssh(url: &str) -> Result<Self, DockerError> {
use foundation_sshkit::Host;
let spec = url.strip_prefix("ssh://").unwrap_or(url);
let host = Host::resolve(spec);
let connector = std::sync::Arc::new(ssh::SshConnector::new(host));
let http = HttpClientBuilder::new()
.with_connector(connector)
.read_timeout(std::time::Duration::from_secs(120))
.build();
Ok(Self {
http,
socket_path: PathBuf::from(url),
api_version: DEFAULT_API_VERSION.to_string(),
remote_host: None,
tls: false,
})
}
#[must_use]
pub fn with_remote_host(mut self, host: &str) -> Self {
self.remote_host = Some(host.to_string());
self
}
pub fn connect_with_defaults() -> Result<Self, DockerError> {
let host = std::env::var("DOCKER_HOST").unwrap_or_default();
if host.is_empty() {
return Ok(Self::connect_unix(DEFAULT_DOCKER_SOCKET));
}
if let Some(path) = host.strip_prefix("unix://") {
return Ok(Self::connect_unix(path));
}
if let Some(addr) = host.strip_prefix("tcp://") {
let verify = std::env::var("DOCKER_TLS_VERIFY").is_ok_and(|v| v != "0" && !v.is_empty());
let cert_dir = std::env::var("DOCKER_CERT_PATH").ok();
if verify || cert_dir.is_some() {
let dir = cert_dir.ok_or_else(|| {
DockerError::Unavailable(
"DOCKER_TLS_VERIFY set but DOCKER_CERT_PATH is missing".to_string(),
)
})?;
let tls = DockerTls::from_cert_dir(&dir, verify)?;
return Self::connect_tls(addr, tls);
}
return Ok(Self::connect_tcp(addr));
}
if host.starts_with("ssh://") {
#[cfg(feature = "ssh")]
{
return Self::connect_ssh(&host);
}
#[cfg(not(feature = "ssh"))]
{
return Err(DockerError::Unavailable(format!(
"DOCKER_HOST is {host}, but this build has the `ssh` feature off — \
enable foundation_deployment_docker/ssh (note it cannot be combined \
with `buildkit`: libssh2/OpenSSL vs BoringSSL)"
)));
}
}
Err(DockerError::Unavailable(format!(
"unrecognized DOCKER_HOST transport: {host}"
)))
}
#[must_use]
pub fn http(&self) -> DynNetClient {
self.http.clone()
}
#[must_use]
pub fn api_version(&self) -> &str {
&self.api_version
}
#[must_use]
pub fn base_url(&self) -> String {
let host = self.remote_host.as_deref().unwrap_or("localhost");
let scheme = if self.tls { "https" } else { "http" };
format!("{scheme}://{host}/v{}", self.api_version)
}
#[must_use]
pub fn socket_path(&self) -> &std::path::Path {
&self.socket_path
}
pub async fn version(&self) -> Result<SystemVersion, DockerError> {
let response =
system_version_request(self.http(), &SystemVersionArgs::default(), &self.base_url(), None::<NoMod>)
.await?;
Ok(response.body)
}
pub async fn negotiate_version(&mut self) -> Result<(), DockerError> {
let version = self.version().await?;
if let Some(server) = version.api_version {
if version_lt(&server, &self.api_version) {
self.api_version = server;
}
}
Ok(())
}
pub async fn create_container(
&self,
config: &impl Serialize,
name: Option<&str>,
) -> Result<ContainerCreateResponse, DockerError> {
let json = serde_json::to_value(config)
.map_err(|e| DockerError::JsonParse(format!("serialize container config: {e}")))?;
let args = ContainerCreateArgs {
name: name.map(str::to_string),
platform: None,
};
let response = container_create_request(
self.http(),
&args,
&self.base_url(),
Some(move |b: &mut PreparedRequestBuilder| {
b.set_header(SimpleHeader::CONTENT_TYPE, "application/json");
let _ = b.set_body_json(&json);
}),
)
.await?;
Ok(response.body)
}
pub async fn start_container(&self, id: &str) -> Result<(), DockerError> {
let args = ContainerStartArgs {
id: id.to_string(),
detach_keys: None,
};
container_start_request(self.http(), &args, &self.base_url(), None::<NoMod>).await?;
Ok(())
}
pub async fn wait_container(
&self,
id: &str,
condition: Option<&str>,
) -> Result<ContainerWaitResponse, DockerError> {
let args = ContainerWaitArgs {
id: id.to_string(),
condition: condition.map(str::to_string),
};
let response = container_wait_request(self.http(), &args, &self.base_url(), None::<NoMod>).await?;
Ok(response.body)
}
pub async fn stop_container(
&self,
id: &str,
timeout_secs: Option<u32>,
) -> Result<(), DockerError> {
let args = ContainerStopArgs {
id: id.to_string(),
signal: None,
t: timeout_secs.map(|t| t.to_string()),
};
match container_stop_request(self.http(), &args, &self.base_url(), None::<NoMod>).await {
Ok(_) => Ok(()),
Err(ApiError::HttpStatus { code: 304, .. }) => Ok(()),
Err(e) => Err(e.into()),
}
}
pub async fn remove_container(&self, id: &str, force: bool) -> Result<(), DockerError> {
let args = ContainerDeleteArgs {
id: id.to_string(),
v: None,
force: Some(force.to_string()),
link: None,
};
container_delete_request(self.http(), &args, &self.base_url(), None::<NoMod>).await?;
Ok(())
}
pub async fn inspect_container(
&self,
id: &str,
) -> Result<ContainerInspectResponse, DockerError> {
let args = ContainerInspectArgs {
id: id.to_string(),
size: None,
};
let response = container_inspect_request(self.http(), &args, &self.base_url(), None::<NoMod>).await?;
Ok(response.body)
}
pub async fn pause_container(&self, id: &str) -> Result<(), DockerError> {
let args = ContainerPauseArgs {
id: id.to_string(),
};
container_pause_request(self.http(), &args, &self.base_url(), None::<NoMod>).await?;
Ok(())
}
pub async fn unpause_container(&self, id: &str) -> Result<(), DockerError> {
let args = ContainerUnpauseArgs {
id: id.to_string(),
};
container_unpause_request(self.http(), &args, &self.base_url(), None::<NoMod>).await?;
Ok(())
}
pub async fn exec_create(&self, id: &str, cmd: &[&str]) -> Result<IDResponse, DockerError> {
exec::exec_create(self, id, cmd).await
}
pub async fn exec_start(&self, id: &str, detach: bool) -> Result<Vec<u8>, DockerError> {
exec::exec_start(self, id, detach).await
}
pub async fn exec_inspect(&self, id: &str) -> Result<serde_json::Value, DockerError> {
exec::exec_inspect(self, id).await
}
pub async fn exec_resize(&self, id: &str, h: u32, w: u32) -> Result<(), DockerError> {
exec::exec_resize(self, id, h, w).await
}
pub async fn system_info(&self) -> Result<serde_json::Value, DockerError> {
system::system_info(self).await
}
pub async fn system_ping(&self) -> Result<(), DockerError> {
system::system_ping(self).await
}
pub async fn system_events(
&self,
since: Option<&str>,
until: Option<&str>,
filters: Option<&str>,
) -> Result<Vec<u8>, DockerError> {
system::system_events(self, since, until, filters).await
}
pub async fn system_data_usage(&self, types: Option<&str>) -> Result<serde_json::Value, DockerError> {
system::system_data_usage(self, types).await
}
pub async fn system_auth(&self, username: &str, password: &str) -> Result<AuthResponse, DockerError> {
system::system_auth(self, username, password).await
}
}
fn version_lt(a: &str, b: &str) -> bool {
let parse = |s: &str| -> Vec<u32> {
s.split('.').map(|p| p.parse().unwrap_or(0)).collect()
};
parse(a) < parse(b)
}