use crate::{Client, Protocol};
use futures_util::stream::StreamExt;
use rand::{
distributions::{Alphanumeric, Distribution},
thread_rng,
};
use shiplift::{
rep::{ContainerCreateInfo, ContainerDetails},
ContainerOptions, PullOptions, RmContainerOptions,
};
use std::collections::HashMap;
#[derive(Debug, Clone, Copy)]
struct Port {
pub source: u32,
pub host: u32,
pub protocol: Protocol,
}
#[derive(Debug, Clone)]
pub struct Container {
pub(crate) details: ContainerDetails,
client: Client,
}
impl Container {
pub async fn new(image_name: impl Into<String>) -> Result<Container, shiplift::Error> {
Builder::new(image_name).build().await
}
pub async fn pull(image_name: impl Into<String>) -> Result<Container, shiplift::Error> {
Builder::new(image_name).pull_on_build().build().await
}
pub fn builder(image_name: impl Into<String>) -> Builder {
Builder::new(image_name)
}
#[must_use]
pub fn id(&self) -> &str {
&self.details.id
}
#[must_use]
pub fn ports_raw(&self) -> Option<&PortMap> {
self.details.network_settings.ports.as_ref()
}
pub async fn delete(self) -> Result<(), shiplift::Error> {
self.client
.containers()
.get(self.id())
.remove(RmContainerOptions::builder().force(true).build())
.await
}
}
pub type PortMap = HashMap<String, Option<Vec<HashMap<String, String>>>>;
#[derive(Debug)]
#[must_use]
pub struct Builder {
image_name: String,
image_tag: String,
name: Option<String>,
ports: Vec<Port>,
commands: Vec<String>,
environment_variables: Vec<String>,
client: Client,
pull_on_build: bool,
slug_length: usize,
}
impl Builder {
fn new(image_name: impl Into<String>) -> Self {
Self {
image_name: image_name.into(),
image_tag: String::from("latest"),
name: None,
ports: Vec::new(),
commands: Vec::new(),
environment_variables: Vec::new(),
client: Client::default(),
pull_on_build: false,
slug_length: 0,
}
}
fn image(&self) -> String {
format!("{}:{}", self.image_name, self.image_tag)
}
pub fn tag(mut self, tag: impl Into<String>) -> Self {
self.image_tag = tag.into();
self
}
pub fn client(mut self, client: impl Into<Client>) -> Self {
self.client = client.into();
self
}
pub fn name(mut self, name: impl Into<String>) -> Self {
self.name = Some(name.into());
self
}
pub fn slug_length(mut self, length: usize) -> Self {
self.slug_length = length;
self
}
fn slugged_name(&self) -> Option<String> {
let base_name = self.name.clone()?;
if self.slug_length > 0 {
let mut rng = thread_rng();
let slug: String = Alphanumeric
.sample_iter(&mut rng)
.take(self.slug_length)
.map(char::from)
.collect();
Some(base_name + "_" + &slug)
} else {
Some(base_name)
}
}
pub fn commands<I, S>(mut self, commands: I) -> Self
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
self.commands = commands.into_iter().map(Into::into).collect();
self
}
pub fn expose(mut self, src_port: u16, host_port: u16, protocol: Protocol) -> Self {
self.ports.push(Port {
source: src_port.into(),
host: host_port.into(),
protocol,
});
self
}
pub fn environment_variable(mut self, env: impl Into<String>) -> Self {
self.environment_variables.push(env.into());
self
}
pub fn pull_on_build(mut self) -> Self {
self.pull_on_build = true;
self
}
pub async fn build(self) -> Result<Container, shiplift::Error> {
let image = self.image();
let commands = self.commands.iter().map(AsRef::as_ref).collect();
if self.pull_on_build {
dbg!("pulling image");
pull_image(&self.client, &image).await?;
}
let create_info = create_container(
&self.client,
&image,
self.slugged_name(),
self.ports,
commands,
self.environment_variables,
)
.await?;
let id = create_info.id;
run_container(&self.client, &id).await?;
let details = inspect_container(&self.client, &id).await?;
Ok(Container {
details,
client: self.client,
})
}
}
async fn pull_image(client: &Client, image: &str) -> Result<(), shiplift::Error> {
log::info!("pulling image: {}", &image);
let mut stream = client
.images()
.pull(&PullOptions::builder().image(image).build());
while let Some(Ok(chunk)) = stream.next().await {
log::debug!("{}", chunk);
}
log::info!("pulled image: {}", &image);
Ok(())
}
async fn create_container<S: AsRef<str>>(
client: &Client,
image: &str,
container_name: Option<S>,
ports: impl IntoIterator<Item = Port>,
commands: Vec<&str>,
environment_variables: Vec<String>,
) -> Result<ContainerCreateInfo, shiplift::Error> {
let mut container_options = ContainerOptions::builder(image);
container_options.cmd(commands);
container_options.env(environment_variables);
if let Some(name) = container_name.as_ref() {
container_options.name(name.as_ref());
}
for port in ports {
container_options.expose(port.source, port.protocol.as_ref(), port.host);
}
client.containers().create(&container_options.build()).await
}
async fn run_container(client: &Client, id: &str) -> Result<(), shiplift::Error> {
client.containers().get(id).start().await
}
async fn inspect_container(client: &Client, id: &str) -> Result<ContainerDetails, shiplift::Error> {
client.containers().get(id).inspect().await
}