ambient-ci 0.14.0

A continuous integration engine
Documentation
#![allow(clippy::result_large_err)]

use std::path::PathBuf;

use clap::Parser;
use clingwrap::config::ConfigLoader;
use directories::ProjectDirs;

use ambient_ci::{
    config::{Config, ConfigError, StoredConfig},
    runlog::{RunLog, RunLogSource},
};

mod cmd;
use cmd::{AmbientError, Leaf};

const QUAL: &str = "liw.fi";
const ORG: &str = "Ambient CI";
const APP: &str = env!("CARGO_BIN_NAME");

fn main() {
    let mut runlog = RunLog::default();
    if let Err(e) = fallible_main(&mut runlog) {
        eprintln!("ERROR: {e}");
        let mut source = e.source();
        while let Some(src) = source {
            eprintln!("caused by: {src}");
            source = src.source();
        }
        std::process::exit(1);
    }
}

fn fallible_main(runlog: &mut RunLog) -> Result<(), Box<dyn std::error::Error>> {
    let args = Args::parse();
    runlog.ambient_starts(RunLogSource::Prelude, APP, env!("CARGO_PKG_VERSION"));
    let config = args.config()?;
    runlog.ambient_runtime_config(RunLogSource::Prelude, &config);
    match &args.cmd {
        Command::Actions(x) => x.run(&config, runlog)?,
        Command::Config(x) => x.run(&config, runlog)?,
        Command::Image(x) => x.run(&config, runlog)?,
        Command::Log(x) => x.run(&config, runlog)?,
        Command::Plan(x) => x.run(&config, runlog)?,
        Command::Projects(x) => x.run(&config, runlog)?,
        Command::Qemu(x) => x.run(&config, runlog)?,
        Command::Run(x) => x.run(&config, runlog)?,
        Command::State(x) => x.run(&config, runlog)?,
    }
    runlog.ambient_ends_successfully(RunLogSource::Epilog);
    Ok(())
}

#[derive(Debug, Parser)]
#[clap(name = APP, version = env!("VERSION"))]
pub struct Args {
    /// Configuration files to use in addition of the default one,
    /// unless `--no-config` is used.
    #[clap(long)]
    config: Vec<PathBuf>,

    /// Don't load default configuration file, but do load any files
    /// specified with `--config`.
    #[clap(long)]
    no_config: bool,

    /// Operation
    #[clap(subcommand)]
    cmd: Command,
}

impl Args {
    fn config(&self) -> Result<Config, ConfigError> {
        let mut loader = ConfigLoader::default();
        if self.no_config {
        } else {
            let dirs = ProjectDirs::from(QUAL, ORG, APP).ok_or(ConfigError::ProjectDirs)?;
            let filename = dirs.config_dir().join("config.yaml");
            loader.allow_yaml(&filename);
        }

        for filename in self.config.iter() {
            loader.require_yaml(filename);
        }

        let validator = StoredConfig::default();
        let config = loader
            .load(None, None, &validator)
            .map_err(ConfigError::Load)?;

        Ok(config)
    }
}

#[derive(Debug, Parser)]
enum Command {
    Actions(cmd::actions::Actions),
    Image(ImageCmd),
    Config(cmd::config::ConfigCmd),
    Log(cmd::log::Log),
    Plan(cmd::plan::Plan),
    Projects(cmd::projects::ProjectsCmd),
    Qemu(cmd::qemu::QemuCmd),
    Run(cmd::run::Run),
    State(cmd::state::State),
}

#[derive(Debug, Parser)]
pub struct ImageCmd {
    #[clap(subcommand)]
    cmd: ImageSubCmd,
}

impl ImageCmd {
    fn run(&self, config: &Config, runlog: &mut RunLog) -> Result<(), AmbientError> {
        match &self.cmd {
            ImageSubCmd::CloudInit(x) => x.run(config, runlog)?,
            ImageSubCmd::Import(x) => x.run(config, runlog)?,
            ImageSubCmd::List(x) => x.run(config, runlog)?,
            ImageSubCmd::Prepare(x) => x.run(config, runlog)?,
            ImageSubCmd::Remove(x) => x.run(config, runlog)?,
            ImageSubCmd::Show(x) => x.run(config, runlog)?,
            ImageSubCmd::Verify(x) => x.run(config, runlog)?,
        }
        Ok(())
    }
}

#[derive(Debug, Parser)]
enum ImageSubCmd {
    CloudInit(cmd::image::CloudInit),
    Import(cmd::image::ImportImage),
    List(cmd::image::ListImages),
    Prepare(cmd::image::PrepareImage),
    Remove(cmd::image::RemoveImages),
    Show(cmd::image::ShowImage),
    Verify(cmd::image::VerifyImage),
}