ubi-rs 0.1.6

A Rust cli and library for ubicloud API
Documentation
use clap::{CommandFactory, Parser, Subcommand};

use clap_complete::{Generator, Shell, generate};
use ubi_rs::{UbiClient, models::ReqClusterCreate, serde_ext::SerdeExt};

fn print_completions<G: Generator>(gene: G, cmd: &mut clap::Command) {
    generate(
        gene,
        cmd,
        cmd.get_name().to_string(),
        &mut std::io::stdout(),
    );
}

#[derive(Parser)]
#[command(version, about, long_about = None)]
struct Cli {
    /// Turn debugging information on
    #[arg(short, long, action = clap::ArgAction::Count)]
    debug: u8,

    // If provided, outputs the completion file for given shell
    #[arg(long = "generate", value_enum)]
    generator: Option<Shell>,

    #[command(subcommand)]
    command: Commands,

    #[clap(
        long,
        short = 'b',
        env = "UBI_BASE_URL",
        default_value = "https://api.ubicloud.com"
    )]
    base_url: String,

    #[clap(long, short = 'v', env = "UBI_URL_VERSION", default_value = "")]
    url_version: String,

    /// API token for the Ubicloud API
    #[clap(long, short = 'a', env = "UBI_API_TOKEN")]
    token: Option<String>,
    // /// Project ID for e.g. pjp051xx4xfj2gfs7ad2m1d2t8
    // #[clap(long, env = "UBI_PROJECT_ID")]
    // pub project: Option<String>,

    // /// Location for the Kubernetes cluster, e.g. "eu-central-h1"
    // #[clap(long, env = "UBI_LOCATION")]
    // pub location: Option<String>,
}

/// Available commands
#[derive(Subcommand)]
enum Commands {
    /// Kubernetes cluster management commands
    #[command(name = "kc")]
    Kc {
        #[command(subcommand)]
        subcommand: KcCommand,
    },

    /// Project management commands
    #[command(name = "project")]
    Project {
        #[command(subcommand)]
        subcommand: ProjectCommand,
    },
}

#[derive(Debug, Subcommand)]
pub enum KcCommand {
    /// Create a worker
    Create {
        /// Cluster name
        #[arg(long)]
        cluster_name: String,

        /// Kubernetes version to use for e.g. v1.33
        #[arg(long)]
        version: String,

        /// Worker size to use for the cluster, e.g. "standard-2", "standard-4", etc.
        #[arg(long)]
        worker_size: String,

        /// Number of control plane nodes  e.g. 1, 3 etc.
        #[arg(long)]
        cp_nodes: u32,

        /// Number of worker nodes  e.g. 1, 2, 3 etc.
        #[arg(long)]
        worker_nodes: u32,

        /// Project ID to create the cluster in
        #[arg(long)]
        project: String,

        /// Location of the cluster, e.g. "eu-central-h1"
        #[arg(long)]
        location: String,
    },
    /// Delete a cluster
    Delete {
        /// Cluster name
        #[arg(long)]
        cluster_name: String,

        /// Project ID to delete the cluster from
        #[arg(long)]
        project: String,

        /// Location of the cluster, e.g. "eu-central-h1"
        #[arg(long)]
        location: String,
    },
    /// List all clusters or workers
    List {
        /// Project ID to filter clusters by
        #[arg(long)]
        project: String,
    },

    /// Download the kubeconfig file for a cluster
    DownloadKubeconfig {
        /// Cluster name to download the kubeconfig for
        #[arg(long)]
        cluster_name: String,
        /// Project ID to download the kubeconfig for
        /// This is the project ID where the cluster is located
        #[arg(long)]
        project: String,

        /// Location of the cluster, e.g. "eu-central-h1"
        #[arg(long)]
        location: String,
    },
}

#[derive(Debug, Subcommand)]
pub enum ProjectCommand {
    /// List all the projects visible to the user
    List,

    /// Create a new project
    Create {
        /// Name of the project to create
        #[arg(long)]
        name: String,
    },

    /// Delete a project
    Delete {
        /// ID of the project to delete
        #[arg(long)]
        project_id: String,
    },
}

/// Prompts the user for input and returns a boolean value based on the user's response.
///
/// This function displays the provided message to the user and waits for their input. If the user
/// enters "y" (case-insensitive), the function returns `true`. If the user enters "n" (case-insensitive),
/// the function returns `false`. If the user enters any other value, the function prints an error
/// message and recursively calls itself to prompt the user again.
///
/// # Arguments
///
/// * `msg` - The message to display to the user when prompting for input.
///
/// # Returns
///
/// A boolean value indicating the user's response.
fn ensure_input(msg: &str) -> bool {
    println!("{}", msg);
    let mut input = String::new();
    std::io::stdin().read_line(&mut input).unwrap();
    input = input.trim().to_string();
    if input.to_lowercase() == "y" {
        true
    } else if input.to_lowercase() == "n" {
        false
    } else {
        println!("Invalid input, please enter y or n");
        ensure_input(msg)
    }
}

/// Runs the CLI application.
///
/// This function is the entry point for the CLI application. It parses the command-line arguments,
/// sets up the logging, and then executes the appropriate command based on the user's input.
///
/// # Errors
///
/// This function can return any error that may occur during the execution of the CLI commands.
pub async fn run() -> Result<(), Box<dyn std::error::Error>> {
    // ensure that .env files are also supported
    dotenvy::dotenv().ok();

    let cli = Cli::parse();

    if cli.debug > 0 {
        unsafe {
            std::env::set_var("RUST_LOG", "debug");
        }
    } else {
        unsafe {
            std::env::set_var("RUST_LOG", "info");
        }
    }
    tracing_subscriber::fmt::init();

    if let Some(generator) = cli.generator {
        print_completions(generator, &mut Cli::command());
        return Ok(());
    }
    assert!(cli.token.is_some(), "API_TOKEN is not set");

    let client = UbiClient::new(&cli.base_url, &cli.url_version, &cli.token.unwrap());
    let resp: Box<dyn SerdeExt> = match cli.command {
        Commands::Kc { subcommand } => match subcommand {
            KcCommand::Create {
                cluster_name,
                version,
                worker_size,
                cp_nodes,
                worker_nodes,
                project,
                location,
            } => {
                let payload = ReqClusterCreate {
                    version: version.clone(),
                    worker_size,
                    cp_nodes,
                    worker_nodes,
                };
                client
                    .kc
                    .create_kubernetes_cluster(&project, &location, &cluster_name, &payload)
                    .await?
                    .boxed()
            }
            KcCommand::Delete {
                cluster_name,
                project,
                location,
            } => client
                .kc
                .delete_kubernetes_cluster(&project, &location, &cluster_name)
                .await?
                .boxed(),
            KcCommand::List { project } => client
                .kc
                .list_kubernetes_clusters(&project, None)
                .await?
                .boxed(),
            KcCommand::DownloadKubeconfig {
                cluster_name,
                project,
                location,
            } => {
                let kubeconfig = client
                    .kc
                    .download_kc_config(&project, &location, &cluster_name)
                    .await?;
                // Save the kubeconfig to a file
                let file_name = format!("{}.kubeconfig", cluster_name);
                tokio::fs::write(&file_name, kubeconfig).await?;
                println!("Kubeconfig saved to {}", file_name);
                Box::new(()) // Return an empty response since we saved the file
            }
        },
        Commands::Project { subcommand } => match subcommand {
            ProjectCommand::List => client.project.list_projects().await?.boxed(),
            ProjectCommand::Create { name } => client.project.create_project(&name).await?.boxed(),
            ProjectCommand::Delete { project_id } => {
                client.project.delete_project(&project_id).await?.boxed()
            }
        },
    };
    resp.pretty_print();
    Ok(())
}

pub fn main() {
    let rt = tokio::runtime::Runtime::new().unwrap();
    if let Err(e) = rt.block_on(run()) {
        eprintln!("Error: {}", e);
        std::process::exit(1);
    }
}