fcmc 0.9.4

A CLI tool to check whether CTF challenge meta.toml configs are valid, and optionally test Docker setup
Documentation
mod challenge;
mod gamebox;
use anyhow::Result;
use bollard::{
    Docker, body_full,
    query_parameters::{BuildImageOptionsBuilder, RemoveContainerOptions, StopContainerOptions},
    secret::BuildInfo,
};
#[allow(deprecated)]
use bollard::{network::CreateNetworkOptions, secret::Ipam};
pub use challenge::{ChallengeMeta, DockerMeta, FlagMeta};

use futures_util::StreamExt;
pub use gamebox::{GameBoxConfig, GameBoxMeta};

use std::collections::HashMap;
use std::fs::File;
use std::{io::Read, path::PathBuf};
use tar::Builder;
use tempfile::NamedTempFile;
use tokio_util::bytes::Bytes;
use tracing::{error, info, warn};

pub async fn stop_and_remove(docker: &bollard::Docker, identifier: &str) -> Result<()> {
    // 1. 停止容器
    // 在 AWD 中,我们通常追求速度,t: Some(0) 表示立即发送信号,不等待优雅退出时间
    let stop_options = StopContainerOptions {
        t: 0.into(), // 注意:最新版 bollard 这里可能是 i64 或 u64,视版本而定
        ..Default::default()
    };

    let container_name = identifier;

    // 尝试停止容器,如果容器已经停止,Docker API 会返回 304,
    // 我们在这里捕获错误,防止程序因为容器已停而中断
    if let Err(e) = docker
        .stop_container(container_name, Some(stop_options))
        .await
    {
        // 如果是 404 (不存在) 或 304 (已停止),我们选择忽略它,继续执行删除
        warn!(
            "⚠️ 停止容器 {} 时提示: {} (可能是已经停止了)",
            identifier, e
        );
    }

    // 2. 彻底删除容器
    let remove_options = RemoveContainerOptions {
        v: true,     // 关键:删除关联的匿名卷,防止磁盘空间被大量僵尸卷撑爆
        force: true, // 强制删除,即使停止失败也能强行抹除
        link: false,
    };
    // 执行删除
    match docker
        .remove_container(container_name, Some(remove_options))
        .await
    {
        Ok(_) => info!("🗑️ 容器 {} 已彻底清理", identifier),
        Err(e) => {
            error!("❌ 删除容器 {} 时出错: {}", identifier, e);
        }
    }

    Ok(())
}

pub async fn remove_and_create_bridge_net(
    docker: &Docker,
    bridge_name: String,
    cidr: String,
) -> anyhow::Result<()> {
    // docker network create --driver bridge --subnet $TARGET_SUBNET/$TARGET_PREFIX $DOCKER_NET
    let _ = docker.remove_network(&bridge_name).await;

    let mut network_options = HashMap::new();
    // 关键:这行代码决定了你在 ifconfig 或 ip addr 里看到的网卡名
    network_options.insert(
        "com.docker.network.bridge.name".to_string(),
        bridge_name.clone(),
    );
    #[allow(deprecated)] // 这个版本的bollard还没有更新到最新的api
    let conf = CreateNetworkOptions {
        name: bridge_name,
        driver: "bridge".to_string(),
        internal: true,
        check_duplicate: true,
        ipam: Ipam {
            config: Some(vec![bollard::secret::IpamConfig {
                subnet: cidr.into(),
                ..Default::default()
            }]),
            ..Default::default()
        },
        options: network_options,
        ..Default::default()
    };

    docker.create_network(conf).await?;

    Ok(())
}

pub async fn build_image(
    docker: &Docker,
    image_tag: &str,
    context_path: &PathBuf,
) -> anyhow::Result<()> {
    let options = BuildImageOptionsBuilder::default().t(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)?; // 把 context_path 整个目录打包进去
        tar_builder.finish()?;
    }

    // 2. 把 tar 文件读到内存
    let mut buf = Vec::new();
    File::open(tmp.path())?.read_to_end(&mut buf)?;

    // 3. 用 body_full 包装成 BodyType
    let body = body_full(Bytes::from(buf));

    // 4. 调用 build_image
    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?; // 拿到 BuildInfo

        if let Some(ref stream_msg) = info.stream {
            infos.push(stream_msg.trim().to_owned());
        }
        if let Some(ref err) = info.error {
            error!("ERROR: {}", err);
        }
    }

    for info in infos {
        println!("{}", info);
    }

    Ok(())
}