use super::super::{Workflow, WorkflowArgs, WorkflowContext, WorkflowResult};
use crate::error::RedisCtlError;
use anyhow::Result;
use clap::Args;
use indicatif::{ProgressBar, ProgressStyle};
use redisctl_core::cloud::quick_database::{QuickDatabaseError, QuickDatabaseParams, provision};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::future::Future;
use std::path::PathBuf;
use std::pin::Pin;
use std::time::Duration;
pub const REPORT_KEY: &str = "report";
#[derive(Args, Debug, Clone, Serialize, Deserialize)]
pub struct QuickDatabaseArgs {
#[arg(long)]
pub name: String,
#[arg(long, default_value = "./.env")]
#[serde(default = "default_output")]
pub output_credentials: PathBuf,
#[arg(long, default_value = "REDIS_URL")]
#[serde(default = "default_variable")]
pub variable: String,
#[arg(long, default_value = "600")]
#[serde(default = "default_wait_timeout")]
pub wait_timeout: u32,
#[arg(long, default_value = "5")]
#[serde(default = "default_wait_interval")]
pub wait_interval: u32,
}
impl QuickDatabaseArgs {
fn to_params(&self) -> QuickDatabaseParams {
QuickDatabaseParams {
name: self.name.clone(),
output_credentials: self.output_credentials.clone(),
variable: self.variable.clone(),
wait_timeout: self.wait_timeout,
wait_interval: self.wait_interval,
}
}
}
#[derive(Args, Debug, Clone)]
pub struct DatabaseCredentialsArgs {
#[arg(long)]
pub subscription_id: i32,
#[arg(long)]
pub database_id: i32,
#[arg(long, default_value = "./.env")]
pub output_credentials: PathBuf,
#[arg(long, default_value = "REDIS_URL")]
pub variable: String,
#[arg(long, default_value = "600")]
pub wait_timeout: u32,
#[arg(long, default_value = "5")]
pub wait_interval: u32,
}
impl DatabaseCredentialsArgs {
pub(crate) fn to_params(&self) -> QuickDatabaseParams {
QuickDatabaseParams {
name: format!("database-{}", self.database_id),
output_credentials: self.output_credentials.clone(),
variable: self.variable.clone(),
wait_timeout: self.wait_timeout,
wait_interval: self.wait_interval,
}
}
}
fn default_output() -> PathBuf {
PathBuf::from("./.env")
}
fn default_variable() -> String {
"REDIS_URL".to_string()
}
fn default_wait_timeout() -> u32 {
600
}
fn default_wait_interval() -> u32 {
5
}
pub struct QuickDatabaseWorkflow;
impl Workflow for QuickDatabaseWorkflow {
fn name(&self) -> &str {
"quick-database"
}
fn description(&self) -> &str {
"Create or reuse a free Redis database and write its connection string to a file"
}
fn execute(
&self,
context: WorkflowContext,
args: WorkflowArgs,
) -> Pin<Box<dyn Future<Output = Result<WorkflowResult>> + Send>> {
Box::pin(async move {
let args: QuickDatabaseArgs = args
.get("args")
.ok_or_else(|| anyhow::anyhow!("Missing workflow arguments"))?;
run(context, args).await.map_err(anyhow::Error::new)
})
}
}
async fn run(
context: WorkflowContext,
args: QuickDatabaseArgs,
) -> std::result::Result<WorkflowResult, QuickDatabaseError> {
let quiet = context.output_format.is_json() || context.output_format.is_yaml();
let client = context
.conn_mgr
.create_cloud_client(context.profile_name.as_deref())
.await
.map_err(client_setup_error)?;
let pb = spinner(quiet, "Provisioning free database…");
let result = provision(&client, &args.to_params()).await;
finish(pb);
let report = result?;
let human = format!(
"{} database '{}' (id {}). Connection string written to {} as {}.",
if report.status == "reused" {
"Reused"
} else {
"Provisioned"
},
report.database.name,
report.database.id,
report.credentials_written_to,
report.credentials_variable,
);
let mut outputs = HashMap::new();
outputs.insert(
REPORT_KEY.to_string(),
serde_json::to_value(&report)
.map_err(|e| QuickDatabaseError::Other(format!("failed to serialize report: {e}")))?,
);
Ok(WorkflowResult {
success: true,
message: human,
outputs,
})
}
fn client_setup_error(err: RedisCtlError) -> QuickDatabaseError {
match err {
RedisCtlError::MissingCredentials { .. }
| RedisCtlError::NoProfileConfigured { .. }
| RedisCtlError::ProfileNotFound { .. }
| RedisCtlError::AuthenticationFailed { .. } => QuickDatabaseError::NotAuthenticated(
format!("{err}. Run `redisctl cloud auth login` first."),
),
other => QuickDatabaseError::Other(other.to_string()),
}
}
fn spinner(quiet: bool, message: &str) -> Option<ProgressBar> {
if quiet {
return None;
}
let pb = ProgressBar::new_spinner();
pb.set_style(
ProgressStyle::with_template("{spinner:.green} {msg}")
.unwrap()
.tick_strings(&["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]),
);
pb.set_message(message.to_string());
pb.enable_steady_tick(Duration::from_millis(100));
Some(pb)
}
fn finish(pb: Option<ProgressBar>) {
if let Some(pb) = pb {
pb.finish_and_clear();
}
}