rexcli 0.18.4

Replix admin CLI tool
//
// Copyright (c) 2021 RepliXio Ltd. All rights reserved.
// Use is subject to license terms.
//

#![cfg_attr(feature = "pedantic", warn(clippy::pedantic))]
#![warn(clippy::use_self)]
#![warn(deprecated_in_future)]
#![warn(future_incompatible)]
#![warn(unreachable_pub)]
#![warn(missing_debug_implementations)]
#![warn(rust_2018_compatibility)]
#![warn(rust_2018_idioms)]
#![warn(unused)]
#![deny(warnings)]

use std::path::PathBuf;
use std::process;

use structopt::StructOpt;
use uuid::Uuid;

mod api;
mod error;
mod fmt;
mod output;
mod show;
mod status;
mod util;
pub mod v20201231;
pub mod v20210228;

use self::v20210228 as latest;

#[cfg(test)]
mod tests;

const REXCLI_ABOUT: &str = "Replix Admin CLI tool";

#[derive(Debug, StructOpt)]
#[structopt(about = REXCLI_ABOUT)]
struct RexCli {
    #[structopt(
        help = "Management server URL or address",
        default_value = "https://api.replix.io",
        short,
        long,
        env = "REX_MGMT"
    )]
    management: String,
    #[structopt(help = "Authentication token", short, long, env = "REX_TOKEN")]
    token: Option<String>,
    #[structopt(
        long,
        global = true,
        conflicts_with = "json",
        help = "Show raw JSON output (different from '--json')"
    )]
    raw: bool,
    #[structopt(
        long,
        global = true,
        conflicts_with = "raw",
        help = "Show results as JSON (different from '--raw')"
    )]
    json: bool,
    #[structopt(short, long, global = true)]
    verbose: bool,
    #[structopt(
        help = "API version to use",
        long,
        global = true,
        default_value,
        env = "REX_API"
    )]
    api: api::ApiEndpoint,
    #[structopt(subcommand)]
    command: Command,
}

#[derive(Debug, StructOpt)]
enum Command {
    #[structopt(about = "Realm operations")]
    Realm {
        #[structopt(subcommand)]
        command: RealmCommand,
    },
    #[structopt(about = "Volume operations")]
    Volume {
        #[structopt(subcommand)]
        command: VolumeCommand,
    },
    #[structopt(about = "Snapshot operations", alias = "snap")]
    Snapshot {
        #[structopt(subcommand)]
        command: SnapshotCommand,
    },
    #[structopt(about = "Job inspection")]
    Job {
        #[structopt(help = "Get all jobs", short, long)]
        all: bool,
        #[structopt(help = "Get this job", short, long)]
        job_id: Option<Uuid>,
        #[structopt(help = "Filter by job type", short, long)]
        r#type: Option<String>,
        #[structopt(
            help = "Filter by the job status",
            default_value = "in_progress",
            short,
            long,
            parse(try_from_str = status::job),
        )]
        status: String,
    },
    #[structopt(about = "Query the version of the system")]
    Version {
        #[structopt(help = "Show crates metadata", long, short)]
        crates: bool,
    },
    #[structopt(external_subcommand)]
    External(Vec<String>),
}

#[derive(Debug, StructOpt)]
#[structopt(about = "Realm management")]
enum RealmCommand {
    #[structopt(about = "Create new realm")]
    Create {
        #[structopt(help = "Realm name to create")]
        realm: String,
        #[structopt(subcommand)]
        spec: RealmSpec,
    },
    #[structopt(about = "Delete existing realm")]
    Delete {
        #[structopt(help = "Realm to delete")]
        realm: String,
    },
    #[structopt(about = "List all existing realms")]
    List,
    #[structopt(about = "Show realm details")]
    Show {
        #[structopt(help = "Realm to show")]
        realm: String,
    },
}

#[derive(Clone, Debug, StructOpt)]
#[structopt(about = "Realm Definition")]
enum RealmSpec {
    #[structopt(about = "Create AWS realm")]
    Aws {
        #[structopt(help = "Role ARN")]
        role_arn: String,
        #[structopt(help = "External ID")]
        external_id: String,
    },
    #[structopt(about = "Create Azure realm")]
    Azure {
        #[structopt(help = "Subscription ID")]
        subscription_id: String,
        #[structopt(help = "Tenant ID")]
        tenant_id: String,
        #[structopt(help = "Client ID")]
        client_id: String,
        #[structopt(help = "Client Secret")]
        client_secret: String,
    },
}

#[derive(Debug, StructOpt)]
#[structopt(about = "Snapshot management")]
enum SnapshotCommand {
    #[structopt(about = "List all the snapshots of this volume")]
    List {
        #[structopt(help = "Volume to show")]
        volume: String,
    },
    #[structopt(about = "Create snapshot")]
    Create {
        #[structopt(help = "Volume to snapshot")]
        volume: String,
        #[structopt(help = "Snapshot name")]
        snapshot: String,
        #[structopt(help = "Wait for snapshot creation to complete", short, long)]
        wait: bool,
    },
    #[structopt(about = "Delete snapshot")]
    Delete {
        #[structopt(help = "Volume to delete snapshot from")]
        volume: String,
        #[structopt(help = "Snapshot name")]
        snapshot: String,
        #[structopt(help = "Wait for snapshot delete to complete", short, long)]
        wait: bool,
    },
    #[structopt(about = "Export snapshot")]
    Export {
        #[structopt(help = "Volume to export snapshot from")]
        volume: String,
        #[structopt(help = "Snapshot name")]
        snapshot: String,
        #[structopt(help = "Realm name", long)]
        realm: String,
        #[structopt(help = "AWS Region name", long)]
        region: String,
    },
}

#[derive(Debug, StructOpt)]
#[structopt(about = "Volume management")]
enum VolumeCommand {
    #[structopt(about = "Show volume details")]
    Show {
        #[structopt(help = "Volume to show")]
        volume: String,
    },
    #[structopt(about = "Create new volume")]
    Create {
        // #[structopt(
        //     help = "Use json file for request body",
        //     conflicts_with = "template",
        //     short,
        //     long
        // )]
        // body: Option<PathBuf>,
        // #[structopt(
        //     help = "Create template json file for request body",
        //     conflicts_with = "body",
        //     short,
        //     long
        // )]
        // template: Option<PathBuf>,
        #[structopt(flatten)]
        create: VolumeCreate,
        #[structopt(help = "Wait for volume creation to complete", short, long)]
        wait: bool,
    },
    #[structopt(about = "Delete volume")]
    Delete {
        volume: String,
        #[structopt(short, long)]
        delete_native: bool,
        #[structopt(short, long)]
        force: bool,
        #[structopt(help = "Wait for volume deletion to complete", short, long)]
        wait: bool,
    },
    #[structopt(about = "List volumes")]
    List,
    #[structopt(about = "Set primary replica")]
    SetPrimary { volume: String, replica: String },
    #[structopt(about = "Add a replica")]
    AddReplica {
        #[structopt(help = "Volume to add replica to")]
        volume: String,
        #[structopt(help = "Use json file for request body", short, long)]
        body: PathBuf,
        #[structopt(help = "Wait for add replica to complete", short, long)]
        wait: bool,
    },
    #[structopt(about = "Remove a replica")]
    RemoveReplica {
        #[structopt(help = "Volume to remove replica from")]
        volume: String,
        #[structopt(help = "Replica to remove")]
        replica: String,
        #[structopt(help = "Wait for replica removal to complete", short, long)]
        wait: bool,
    },
    #[structopt(about = "Wait until volume reaches a given status")]
    Wait {
        volume: String,
        #[structopt(short, long, help = "Wait for a given replica")]
        replica: Option<String>,
        #[structopt(short, long, help = "Make sure this replica is primary")]
        primary: bool,
        #[structopt(default_value = "online", short, long, parse(try_from_str = status::volume))]
        status: String,
    },
}

#[derive(Debug, StructOpt)]
enum VolumeCreate {
    #[structopt(about = "Create volume using JSON file as VOLUME CREATE API call body")]
    Body {
        #[structopt(help = "file name")]
        path: PathBuf,
    },
    #[structopt(about = "Generate body template for VOLUME CREATE API JSON file")]
    Template {
        #[structopt(help = "file name", default_value = "create.json")]
        path: PathBuf,
    },
}

impl RexCli {
    fn api(&self) -> api::Api {
        api::Api::new(
            &self.management,
            self.api,
            &self.token,
            self.json,
            self.raw,
            self.verbose,
        )
    }

    fn execute(self) -> Result<(), anyhow::Error> {
        let output = match self.command {
            Command::Realm { ref command } => self.realm(command),
            Command::Volume { ref command } => self.volume(command),
            Command::Snapshot { ref command } => self.snapshot(command),
            Command::Job {
                all,
                job_id,
                ref r#type,
                ref status,
            } => self.api().job(all, job_id, r#type.as_deref(), status),
            Command::Version { crates } => self.api().version(crates),
            Command::External(_) => self.external(),
        };

        output.map(|text| println!("{}", text))
    }

    fn realm(&self, command: &RealmCommand) -> Result<String, anyhow::Error> {
        match command {
            RealmCommand::Create { realm, spec } => self.api().realm().create(realm, spec.to_api()),
            RealmCommand::Delete { realm } => self.api().realm().delete(realm),
            RealmCommand::List => self.api().realm().list(),
            RealmCommand::Show { realm } => self.api().realm().show(realm),
        }
    }

    fn volume(&self, command: &VolumeCommand) -> Result<String, anyhow::Error> {
        match command {
            VolumeCommand::Show { volume } => self.api().volume().show(volume),
            VolumeCommand::Create { create, wait } => self.api().volume().create(create, *wait),
            VolumeCommand::Delete {
                volume,
                delete_native,
                force,
                wait,
            } => self
                .api()
                .volume()
                .delete(volume, *delete_native, *force, *wait),
            VolumeCommand::List => self.api().volume().list(),
            VolumeCommand::SetPrimary { volume, replica } => {
                self.api().volume().set_primary(volume, replica)
            }
            VolumeCommand::AddReplica { volume, body, wait } => self
                .api()
                .volume()
                .add_replica_from_file(volume, body, *wait),
            VolumeCommand::RemoveReplica {
                volume,
                replica,
                wait,
            } => self.api().volume().remove_replica(volume, replica, *wait),
            VolumeCommand::Wait {
                volume,
                replica,
                primary,
                status,
            } => self.api().volume().wait_for_volume_status(
                volume,
                replica.as_deref(),
                *primary,
                status,
            ),
        }
    }

    fn snapshot(&self, command: &SnapshotCommand) -> Result<String, anyhow::Error> {
        match command {
            SnapshotCommand::List { volume } => self.api().volume().snapshot_list(volume),
            SnapshotCommand::Create {
                volume,
                snapshot,
                wait,
            } => self.api().volume().snapshot_create(volume, snapshot, *wait),
            SnapshotCommand::Delete {
                volume,
                snapshot,
                wait,
            } => self.api().volume().snapshot_delete(volume, snapshot, *wait),
            SnapshotCommand::Export {
                volume,
                snapshot,
                realm,
                region,
            } => self
                .api()
                .volume()
                .snapshot_export_aws(volume, snapshot, realm, region),
        }
    }

    fn external(&self) -> Result<String, anyhow::Error> {
        if let Command::External(ref args) = self.command {
            let plugin = format!("rexcli-{}", args[0]);
            if let Ok(mut command) = which::which(plugin).map(process::Command::new) {
                command.args(&args[1..]).spawn()?.wait()?;
            } else {
                Self::clap().print_long_help()?;
            }
        }

        Ok(String::new())
    }
}

fn main() -> Result<(), anyhow::Error> {
    dotenv::dotenv().ok();
    env_logger::init();
    let about = format!(
        "{}\nAPI version {}",
        env!("CARGO_PKG_VERSION"),
        latest::VERSION
    );
    let matches = RexCli::clap().long_version(about.as_str()).get_matches();
    RexCli::from_clap(&matches).execute()
}