cargo-docker-build 0.1.2

Build Dockerfile and script from Cargo.toml for a Rust workspace project
use clap::Parser;
use serde_derive::Deserialize;
use std::fs::File;
use std::io::prelude::*;

#[derive(Parser)]
#[command(name = "cargo")]
#[command(bin_name = "cargo")]
#[command(author, version, about, long_about)]
struct CargoCli {
    #[clap(subcommand)]
    docker_build: Option<Command>,
}

#[derive(clap::Subcommand)]
enum Command {
    DockerBuild,
}

fn main() {
    CargoCli::parse();
    write_dockerfile();
    write_build_script();
}

fn get_cargo_files() -> Vec<String> {
    let workspace_content = match std::fs::read_to_string("Cargo.toml") {
        Ok(content) => content,
        Err(e) => {
            panic!("Error: {e}: 「Cargo.toml」");
        }
    };
    match toml::from_str::<CargoWorkspace>(&workspace_content) {
        Ok(cargo_workspace) => cargo_workspace
            .workspace
            .members
            .iter()
            .filter(|member| !member.starts_with("lib"))
            .map(|member| format!("{member}/Cargo.toml"))
            .collect::<Vec<String>>(),
        _ => vec!["Cargo.toml".to_string()],
    }
}

fn write_dockerfile() {
    let dockerfile = "Dockerfile";
    println!("Generating {dockerfile}");
    let images = get_cargo_files()
        .iter()
        .map(|file| docker_image_section(file).unwrap())
        .collect::<Vec<String>>();

    let preable = r#"
# This file is generated by docker-build
# DO NOT EDIT BY HAND;
# Edit docker-build instead.
FROM rust:slim AS builder
WORKDIR /usr/src/myapp
# maybe use --link
COPY . .
LABEL stage="builder"
RUN cargo build --release
"#;
    let mut file = File::create(dockerfile).unwrap();
    file.write_all(preable.as_bytes()).unwrap();
    for image in images {
        file.write_all(image.as_bytes()).unwrap();
    }
}

#[derive(Debug, Deserialize)]
struct Package {
    name:        String,
    version:     String,
    authors:     Vec<String>,
    description: String,
    repository:  String,
}

#[derive(Debug, Deserialize)]
struct Component {
    package: Package,
}

#[derive(Debug, Deserialize)]
struct Workspace {
    members: Vec<String>,
}

#[derive(Debug, Deserialize)]
struct CargoWorkspace {
    workspace: Workspace,
}

fn get_config(file_name: &str) -> Result<Package, String> {
    let file_content = match std::fs::read_to_string(file_name) {
        Ok(content) => content,
        Err(e) => {
            eprintln!("Error: {e}{file_name}");
            return Err(e.to_string());
        }
    };
    let config: Component = match toml::from_str(&file_content) {
        Ok(config) => config,
        Err(e) => {
            eprintln!("Error: {e}{file_name}");
            panic!("{}", e.to_string());
        }
    };
    Ok(config.package)
}
fn docker_image_section(file_name: &str) -> Result<String, String> {
    let package = get_config(file_name)?;
    let name = package.name;
    let version = package.version;
    let authors = package.authors;
    let description = package.description.trim();
    let repository = package.repository;
    if authors.is_empty() {
        panic!("At least one author is needed, in 「{file_name}」");
    }
    let author = authors[0].clone();

    Ok(format!(
        r#"
### {name}
FROM debian:bullseye-slim
COPY --from=builder /usr/src/myapp/target/release/{name} /usr/local/bin/{name}
MAINTAINER {author}
LABEL maintainer="{author}" \
  version="{version}" \
  tag="{name}:v{version}" \
  description="{description}" \
  repository="{repository}" \
  name="{name}"\
  app="{name}"

#ENTRYPOINT ["/usr/local/bin/{name}"]
EXPOSE 8080/tcp 8081/tcp

CMD ["{name}"]

"#,
    ))
}

fn write_build_script() {
    let packages = get_cargo_files()
        .iter()
        .map(|file| get_config(file).unwrap())
        .collect::<Vec<Package>>();
    build_script(&packages);
}

fn build_script(packages: &[Package]) {
    let build_script = "build_docker.sh";
    println!("Generating {build_script}");
    let mut file = File::create(build_script).unwrap();
    let preable = r#"#!/usr/bin/env bash
# This file is generated by docker-build
# DO NOT EDIT BY HAND;
# Edit docker-build instead.

set -e

eval $(minikube docker-env)

docker build .
"#;
    file.write_all(preable.as_bytes()).unwrap();

    packages.iter().for_each(|package| {
        let name = &package.name;
        let version = &package.version;
        let image = format!(
            r#"
docker tag $(docker image ls --filter "label=tag={name}:v{version}" -q) {name}:v{version}"#
        );
        file.write_all(image.as_bytes()).unwrap();
    });

    let end = r#"

docker image prune --filter label=stage=builder -f
"#;
    file.write_all(end.as_bytes()).unwrap();
}