docker-pose 0.5.0

Command line tool to play with 🐳 Docker Compose files.
Documentation
//! `pose` is a command line tool to play with 🐳 Docker Compose files.

use clap::Parser;
use colored::*;
use std::env;
use std::{fs, process};

//mod lib;
//use crate::lib::ComposeYaml;
use docker_pose::{
    Args, Commands, ComposeYaml, DockerCommand, GitCommand, Objects, POSE_COMPLETE, ReplaceTag,
    Verbosity, cmd_get_success_output_or_fail, get_and_save, get_slug, get_yml_content,
    print_names, print_service_not_found, print_services_not_found, unwrap_filter_regex,
    unwrap_filter_tag,
};

fn main() {
    setup_terminal();
    let args = Args::try_parse().unwrap_or_else(|e| {
        let pose_complete = env::var("_POSE_ARGCOMPLETE").unwrap_or("".to_string());
        if pose_complete == "source" {
            // output bash tab autocompletion script
            println!("{}", POSE_COMPLETE);
            process::exit(1);
        }
        e.exit();
    });
    let verbosity = args.get_verbosity();
    // TODO check here Commands::Get to avoid compose parsing
    if let Commands::Slug { text } = args.command {
        if let Some(t) = text {
            println!("{}", get_slug(&t));
        } else {
            let command = GitCommand::new(verbosity.clone());
            let result_output = command.get_current_branch();
            match result_output {
                Ok(output) => {
                    // git was successfully called by pose, but docker compose
                    // could either succeed or fail executing its task
                    let slug = cmd_get_success_output_or_fail(
                        &command.git_bin,
                        "rev-parse",
                        output,
                        args.quiet,
                    );
                    println!("{}", get_slug(&slug));
                }
                Err(e) => {
                    // git couldn't be called by pose or the OS
                    eprintln!("{}: calling git: {}", "ERROR".red(), e);
                    process::exit(21);
                }
            }
        }
        process::exit(0)
    } else if let Commands::Get {
        url,
        script,
        output,
        timeout_connect,
        max_time,
        headers,
    } = args.command
    {
        get_and_save(
            &url,
            &script,
            &output,
            timeout_connect,
            max_time,
            &headers,
            verbosity.clone(),
        );
        process::exit(0)
    }
    if args.filenames.len() > 1 && args.no_docker {
        eprintln!(
            "{}: multiple '{}' arguments cannot be used with '{}'",
            "ERROR".red(),
            "--file".yellow(),
            "--no-docker".yellow()
        );
        process::exit(2);
    }
    let yaml_content = match args.no_docker {
        true => get_yml_content(args.filenames.first().map(AsRef::as_ref), verbosity.clone()),
        false => {
            let command = DockerCommand::new(verbosity.clone());
            let result_output = command.call_compose_config(
                &args.filenames.iter().map(AsRef::as_ref).collect::<Vec<_>>(),
                args.no_consistency,
                args.no_interpolate,
                false,
                false,
            );
            match result_output {
                Ok(output) => {
                    // docker was successfully called by pose, but docker compose
                    // could either succeed or fail executing its task
                    cmd_get_success_output_or_fail(
                        &command.docker_bin,
                        "compose",
                        output,
                        args.quiet,
                    )
                }
                Err(e) => {
                    // docker couldn't be called by pose or the OS
                    eprintln!("{}: calling compose: {}", "ERROR".red(), e);
                    eprintln!(
                        "{}: parsing will be executed without compose",
                        "WARN".yellow()
                    );
                    get_yml_content(args.filenames.first().map(AsRef::as_ref), verbosity.clone())
                }
            }
        }
    };
    let mut compose = ComposeYaml::new(&yaml_content).unwrap_or_else(|err| {
        if err.to_string().starts_with("invalid type") {
            eprintln!(
                "{}: parsing compose YAML file: invalid content",
                "ERROR".red()
            );
            process::exit(13);
        }
        eprintln!("{}: parsing YAML file: {}", "ERROR".red(), err);
        process::exit(15);
    });
    match args.command {
        Commands::List { object, pretty } => match object {
            Objects::Envs { service } => {
                let serv = compose
                    .get_service(&service)
                    .unwrap_or_else(|| print_service_not_found(&service));
                let envs_op = compose.get_service_envs(serv);
                if let Some(envs) = envs_op {
                    envs.iter().for_each(|env| println!("{}", env));
                }
            }
            Objects::Depends { services } => {
                let all_deps_op = compose
                    .get_services_depends_on(&services)
                    .unwrap_or_else(print_services_not_found);
                let names = all_deps_op.iter().map(|s| s.as_str());
                print_names(names, pretty);
            }
            Objects::Dependents { services } => {
                let all_deps_op = compose.get_services_dependants(&services);
                if let Some(deps) = all_deps_op {
                    let names = deps.iter().map(|s| s.as_str());
                    print_names(names, pretty);
                }
            }
            Objects::Profiles => {
                let op = compose.get_profiles_names();
                match op {
                    None => {
                        eprintln!("{}: No profiles section found", "ERROR".red());
                        process::exit(15);
                    }
                    Some(profiles) => {
                        print_names(profiles.into_iter(), pretty);
                    }
                }
            }
            Objects::Images {
                filter,
                tag,
                tag_filter,
                ignore_unauthorized,
                progress,
                no_slug,
                offline,
                threads,
            } => {
                let regex = unwrap_filter_regex(tag_filter.as_deref());
                let replace_tag = tag.map(|tag| ReplaceTag {
                    tag,
                    ignore_unauthorized,
                    threads,
                    no_slug,
                    offline,
                    tag_filter: regex,
                    verbosity: verbosity.clone(),
                    progress_verbosity: match progress {
                        true => Verbosity::Verbose,
                        false => Verbosity::Quiet,
                    },
                });
                let filter_by_tag = unwrap_filter_tag(filter.as_deref());
                let op = compose.get_images(filter_by_tag, replace_tag.as_ref());
                match op {
                    None => {
                        eprintln!("{}: No services section found", "ERROR".red());
                        process::exit(15);
                    }
                    Some(images) => {
                        let images_list = images.iter().map(|i| i.as_str());
                        print_names(images_list.into_iter(), pretty);
                    }
                }
            }
            Objects::Services { filter } => {
                let filter_by_tag = unwrap_filter_tag(filter.as_deref());
                if let Some(tag_name) = filter_by_tag {
                    let services = compose.filter_services_by_image_tag(tag_name);
                    let service_names_iter = services.iter().map(|s| s.0.as_str());
                    print_names(service_names_iter, pretty);
                } else {
                    let el_iter = compose.get_root_element_names("services").into_iter();
                    print_names(el_iter, pretty);
                }
            }
            Objects::Volumes | Objects::Networks | Objects::Configs | Objects::Secrets => {
                let root_element = object.to_string().to_lowercase();
                let el_iter = compose.get_root_element_names(&root_element).into_iter();
                print_names(el_iter, pretty);
            }
        },
        Commands::Config {
            output,
            tag,
            tag_filter,
            ignore_unauthorized,
            progress,
            no_slug,
            offline,
            threads,
        } => {
            let regex = unwrap_filter_regex(tag_filter.as_deref());
            let replace_tag = tag.map(|tag| ReplaceTag {
                tag,
                ignore_unauthorized,
                offline,
                threads,
                no_slug,
                tag_filter: regex,
                verbosity: verbosity.clone(),
                progress_verbosity: match progress {
                    true => Verbosity::Verbose,
                    false => Verbosity::Quiet,
                },
            });
            if let Some(remote_t) = replace_tag {
                compose.update_images_tag(&remote_t);
            }
            let result = compose.to_string().unwrap_or_else(|err| {
                eprintln!("{}: {}", "ERROR".red(), err);
                process::exit(20);
            });
            if let Some(file) = output {
                fs::write(&file, result).unwrap_or_else(|e| {
                    eprintln!(
                        "{}: writing output to '{}' file: {}",
                        "ERROR".red(),
                        file.yellow(),
                        e
                    );
                    process::exit(18);
                });
            } else {
                println!("{}", result);
            }
        }
        Commands::Slug { .. } | Commands::Get { .. } => {
            // This was attended above in the code
        }
    }
}

#[cfg(target_os = "windows")]
fn setup_terminal() {
    control::set_virtual_terminal(true).unwrap();
}

#[cfg(not(target_os = "windows"))]
fn setup_terminal() {
    // nothing is needed in *nix systems
}