use anyhow::Result;
use bollard::{
Docker, body_full,
models::ContainerCreateBody,
query_parameters::{
BuildImageOptionsBuilder, CreateContainerOptionsBuilder, InspectContainerOptions,
RemoveContainerOptions, StartContainerOptions, StopContainerOptions,
},
secret::{BuildInfo, HostConfig},
};
use futures_util::StreamExt;
use serde::Deserialize;
use std::fs::File;
use std::{io::Read, path::PathBuf};
use tar::Builder;
use tempfile::NamedTempFile;
use tokio_util::bytes::Bytes;
use tracing::info;
#[derive(Debug, Deserialize)]
pub struct ChallengeMeta {
pub name: String,
pub author: String,
pub category: String,
pub description: String,
pub flag: FlagMeta,
pub docker: Option<DockerMeta>,
pub attachment: Option<String>,
}
#[derive(Debug, Deserialize)]
pub struct FlagMeta {
pub value: String,
pub env_var: String,
}
#[derive(Debug, Deserialize)]
pub struct DockerMeta {
pub image_tag: String,
pub port: String, pub is_nc: Option<bool>,
}
impl ChallengeMeta {
pub fn from_toml_str(toml: &str) -> Result<Self, toml::de::Error> {
toml::from_str(toml)
}
pub async fn create_and_start(
&self,
docker: &Docker,
identifier: &str,
flag: &str,
) -> Result<u16> {
let docker_meta = self
.docker
.as_ref()
.ok_or_else(|| anyhow::anyhow!("No docker config found"))?;
let container_name = format!("{}", identifier);
let container_port = docker_meta.port.as_str();
info!(
"Creating container: {}, src_port:{}",
container_name, container_port
);
let options = CreateContainerOptionsBuilder::new()
.name(&container_name)
.build();
let config = ContainerCreateBody {
image: Some(docker_meta.image_tag.clone()),
env: Some(vec![format!("{}={}", self.flag.env_var, flag)]),
host_config: Some(HostConfig {
auto_remove: Some(true),
port_bindings: Some({
let mut map = std::collections::HashMap::new();
map.insert(
docker_meta.port.clone(),
Some(vec![bollard::models::PortBinding {
host_ip: Some("0.0.0.0".to_string()),
host_port: None,
}]),
);
map
}),
..Default::default()
}),
..Default::default()
};
let container = docker.create_container(Some(options), config).await?;
info!("Container created: {:?}", container);
docker
.start_container(&container.id, None::<StartContainerOptions>)
.await?;
let container_info = docker
.inspect_container(&container.id, None::<InspectContainerOptions>)
.await?;
let port_bindings = container_info
.network_settings
.as_ref()
.and_then(|n| n.ports.as_ref())
.ok_or_else(|| anyhow::anyhow!("No port binding info found"))?;
let host_port = port_bindings
.get(container_port)
.and_then(|v| v.as_ref())
.and_then(|bindings| bindings.get(0))
.and_then(|binding| binding.host_port.as_ref())
.ok_or_else(|| anyhow::anyhow!("Host port not found"))?
.parse::<u16>()?;
Ok(host_port)
}
pub async fn build_image(
&self,
docker: &Docker,
context_path: &PathBuf,
) -> Result<Vec<String>> {
let docker_meta = self
.docker
.as_ref()
.ok_or_else(|| anyhow::anyhow!("No docker config found"))?;
let options = BuildImageOptionsBuilder::default()
.t(&docker_meta.image_tag)
.build();
let tmp = NamedTempFile::new()?;
{
let file = File::create(tmp.path())?;
let mut tar_builder = Builder::new(file);
tar_builder.append_dir_all(".", context_path)?; tar_builder.finish()?;
}
let mut buf = Vec::new();
File::open(tmp.path())?.read_to_end(&mut buf)?;
let body = body_full(Bytes::from(buf));
let mut build_stream = docker.build_image(options, None, Some(body));
let mut infos = Vec::new();
while let Some(update) = build_stream.next().await {
let info: BuildInfo = update?;
if let Some(ref stream_msg) = info.stream {
infos.push(stream_msg.trim().to_owned());
}
if let Some(ref err) = info.error {
eprintln!("ERROR: {}", err);
}
}
Ok(infos)
}
}