mirror of https://github.com/buster-so/buster.git
270 lines
9.1 KiB
Rust
270 lines
9.1 KiB
Rust
use anyhow::Result;
|
|
use colored::*;
|
|
use inquire::{Select, Text, Password, validator::Validation, Confirm};
|
|
use regex::Regex;
|
|
use serde::{Deserialize, Serialize};
|
|
use std::error::Error;
|
|
use indicatif::{ProgressBar, ProgressStyle};
|
|
use std::time::Duration;
|
|
|
|
use crate::utils::{
|
|
buster_credentials::get_and_validate_buster_credentials,
|
|
profiles::{Credential, PostgresCredentials},
|
|
BusterClient, PostDataSourcesRequest,
|
|
};
|
|
|
|
#[derive(Debug, Clone)]
|
|
enum DatabaseType {
|
|
Redshift,
|
|
Postgres,
|
|
BigQuery,
|
|
Snowflake,
|
|
}
|
|
|
|
impl std::fmt::Display for DatabaseType {
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
match self {
|
|
DatabaseType::Redshift => write!(f, "Redshift"),
|
|
DatabaseType::Postgres => write!(f, "Postgres"),
|
|
DatabaseType::BigQuery => write!(f, "BigQuery"),
|
|
DatabaseType::Snowflake => write!(f, "Snowflake"),
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Serialize, Deserialize, Clone)]
|
|
struct RedshiftCredentials {
|
|
pub host: String,
|
|
pub port: u16,
|
|
pub username: String,
|
|
pub password: String,
|
|
pub database: Option<String>,
|
|
pub schemas: Option<Vec<String>>,
|
|
}
|
|
|
|
pub async fn init() -> Result<()> {
|
|
println!("{}", "Initializing Buster...".bold().green());
|
|
|
|
// Check for Buster credentials with progress indicator
|
|
let spinner = ProgressBar::new_spinner();
|
|
spinner.set_style(
|
|
ProgressStyle::default_spinner()
|
|
.tick_chars("⠁⠂⠄⡀⢀⠠⠐⠈ ")
|
|
.template("{spinner:.green} {msg}")
|
|
.unwrap(),
|
|
);
|
|
spinner.set_message("Checking for Buster credentials...");
|
|
spinner.enable_steady_tick(Duration::from_millis(100));
|
|
|
|
let buster_creds = match get_and_validate_buster_credentials().await {
|
|
Ok(creds) => {
|
|
spinner.finish_with_message("✓ Buster credentials found".green().to_string());
|
|
creds
|
|
},
|
|
Err(_) => {
|
|
spinner.finish_with_message("✗ No valid Buster credentials found".red().to_string());
|
|
println!("Please run {} first.", "buster auth".cyan());
|
|
return Err(anyhow::anyhow!("No valid Buster credentials found"));
|
|
}
|
|
};
|
|
|
|
// Select database type
|
|
let db_types = vec![
|
|
DatabaseType::Redshift,
|
|
DatabaseType::Postgres,
|
|
DatabaseType::BigQuery,
|
|
DatabaseType::Snowflake,
|
|
];
|
|
|
|
let db_type = Select::new(
|
|
"Select your database type:",
|
|
db_types,
|
|
)
|
|
.prompt()?;
|
|
|
|
println!("You selected: {}", db_type.to_string().cyan());
|
|
|
|
match db_type {
|
|
DatabaseType::Redshift => setup_redshift(buster_creds.url, buster_creds.api_key).await,
|
|
_ => {
|
|
println!("{}", format!("{} support is coming soon!", db_type).yellow());
|
|
println!("Currently, only Redshift is supported.");
|
|
Err(anyhow::anyhow!("Database type not yet implemented"))
|
|
}
|
|
}
|
|
}
|
|
|
|
async fn setup_redshift(buster_url: String, buster_api_key: String) -> Result<()> {
|
|
println!("{}", "Setting up Redshift connection...".bold().green());
|
|
|
|
// Collect name (with validation)
|
|
let name_regex = Regex::new(r"^[a-zA-Z0-9_-]+$")?;
|
|
let name = Text::new("Enter a unique name for this data source:")
|
|
.with_help_message("Only alphanumeric characters, dash (-) and underscore (_) allowed")
|
|
.with_validator(move |input: &str| {
|
|
if input.trim().is_empty() {
|
|
return Ok(Validation::Invalid("Name cannot be empty".into()));
|
|
}
|
|
if name_regex.is_match(input) {
|
|
Ok(Validation::Valid)
|
|
} else {
|
|
Ok(Validation::Invalid("Name must contain only alphanumeric characters, dash (-) or underscore (_)".into()))
|
|
}
|
|
})
|
|
.prompt()?;
|
|
|
|
// Collect host
|
|
let host = Text::new("Enter the Redshift host:")
|
|
.with_help_message("Example: my-cluster.abc123xyz789.us-west-2.redshift.amazonaws.com")
|
|
.with_validator(|input: &str| {
|
|
if input.trim().is_empty() {
|
|
return Ok(Validation::Invalid("Host cannot be empty".into()));
|
|
}
|
|
Ok(Validation::Valid)
|
|
})
|
|
.prompt()?;
|
|
|
|
// Collect port (with validation)
|
|
let port_str = Text::new("Enter the Redshift port:")
|
|
.with_default("5439")
|
|
.with_help_message("Default Redshift port is 5439")
|
|
.with_validator(|input: &str| {
|
|
match input.parse::<u16>() {
|
|
Ok(_) => Ok(Validation::Valid),
|
|
Err(_) => Ok(Validation::Invalid("Port must be a valid number between 1 and 65535".into())),
|
|
}
|
|
})
|
|
.prompt()?;
|
|
let port = port_str.parse::<u16>()?;
|
|
|
|
// Collect username
|
|
let username = Text::new("Enter the Redshift username:")
|
|
.with_validator(|input: &str| {
|
|
if input.trim().is_empty() {
|
|
return Ok(Validation::Invalid("Username cannot be empty".into()));
|
|
}
|
|
Ok(Validation::Valid)
|
|
})
|
|
.prompt()?;
|
|
|
|
// Collect password (masked)
|
|
let password = Password::new("Enter the Redshift password:")
|
|
.with_validator(|input: &str| {
|
|
if input.trim().is_empty() {
|
|
return Ok(Validation::Invalid("Password cannot be empty".into()));
|
|
}
|
|
Ok(Validation::Valid)
|
|
})
|
|
.without_confirmation()
|
|
.prompt()?;
|
|
|
|
// Collect database (optional)
|
|
let database = Text::new("Enter the Redshift database (optional):")
|
|
.with_help_message("Leave blank to access all available databases")
|
|
.prompt()?;
|
|
let database = if database.trim().is_empty() {
|
|
None
|
|
} else {
|
|
Some(database)
|
|
};
|
|
|
|
// Collect schema (optional)
|
|
let schema = Text::new("Enter the Redshift schema (optional):")
|
|
.with_help_message("Leave blank to access all available schemas")
|
|
.prompt()?;
|
|
let schema = if schema.trim().is_empty() {
|
|
None
|
|
} else {
|
|
Some(schema)
|
|
};
|
|
|
|
// Show summary and confirm
|
|
println!("\n{}", "Connection Summary:".bold());
|
|
println!("Name: {}", name.cyan());
|
|
println!("Host: {}", host.cyan());
|
|
println!("Port: {}", port.to_string().cyan());
|
|
println!("Username: {}", username.cyan());
|
|
println!("Password: {}", "********".cyan());
|
|
|
|
// Display database and schema with clear indication if they're empty
|
|
if let Some(db) = &database {
|
|
println!("Database: {}", db.cyan());
|
|
} else {
|
|
println!("Database: {}", "All databases (null)".cyan());
|
|
}
|
|
|
|
if let Some(sch) = &schema {
|
|
println!("Schema: {}", sch.cyan());
|
|
} else {
|
|
println!("Schema: {}", "All schemas (null)".cyan());
|
|
}
|
|
|
|
let confirm = Confirm::new("Do you want to create this data source?")
|
|
.with_default(true)
|
|
.prompt()?;
|
|
|
|
if !confirm {
|
|
println!("{}", "Data source creation cancelled.".yellow());
|
|
return Ok(());
|
|
}
|
|
|
|
// Create credentials
|
|
let redshift_creds = RedshiftCredentials {
|
|
host,
|
|
port,
|
|
username,
|
|
password,
|
|
database,
|
|
schemas: schema.map(|s| vec![s]),
|
|
};
|
|
|
|
// Create API request
|
|
// Note: PostgresCredentials requires String for database and schema, not Option<String>
|
|
// We use empty strings to represent null/all databases or schemas
|
|
let request = PostDataSourcesRequest {
|
|
name: name.clone(),
|
|
env: "dev".to_string(), // Default to dev environment
|
|
credential: Credential::Redshift(
|
|
PostgresCredentials {
|
|
host: redshift_creds.host,
|
|
port: redshift_creds.port,
|
|
username: redshift_creds.username,
|
|
password: redshift_creds.password,
|
|
database: redshift_creds.database.clone().unwrap_or_default(),
|
|
schema: redshift_creds.schemas.clone().and_then(|s| s.first().cloned()).unwrap_or_default(),
|
|
jump_host: None,
|
|
ssh_username: None,
|
|
ssh_private_key: None,
|
|
}
|
|
),
|
|
};
|
|
|
|
// Send to API with progress indicator
|
|
let spinner = ProgressBar::new_spinner();
|
|
spinner.set_style(
|
|
ProgressStyle::default_spinner()
|
|
.tick_chars("⠁⠂⠄⡀⢀⠠⠐⠈ ")
|
|
.template("{spinner:.green} {msg}")
|
|
.unwrap(),
|
|
);
|
|
spinner.set_message("Sending credentials to Buster API...");
|
|
spinner.enable_steady_tick(Duration::from_millis(100));
|
|
|
|
let client = BusterClient::new(buster_url, buster_api_key)?;
|
|
|
|
match client.post_data_sources(vec![request]).await {
|
|
Ok(_) => {
|
|
spinner.finish_with_message("✓ Data source created successfully!".green().bold().to_string());
|
|
println!("\nData source '{}' is now available for use with Buster.", name.cyan());
|
|
println!("You can now use this data source with other Buster commands.");
|
|
Ok(())
|
|
},
|
|
Err(e) => {
|
|
spinner.finish_with_message("✗ Failed to create data source".red().bold().to_string());
|
|
println!("\nError: {}", e);
|
|
println!("Please check your credentials and try again.");
|
|
Err(anyhow::anyhow!("Failed to create data source: {}", e))
|
|
}
|
|
}
|
|
}
|