diff --git a/cli/src/commands/init.rs b/cli/src/commands/init.rs index 86078edaf..47f749a68 100644 --- a/cli/src/commands/init.rs +++ b/cli/src/commands/init.rs @@ -117,12 +117,15 @@ pub async fn init(destination_path: Option<&str>) -> Result<()> { DatabaseType::Redshift => { setup_redshift(buster_creds.url, buster_creds.api_key, &config_path).await } + DatabaseType::Postgres => { + setup_postgres(buster_creds.url, buster_creds.api_key, &config_path).await + } _ => { println!( "{}", format!("{} support is coming soon!", db_type).yellow() ); - println!("Currently, only Redshift is supported."); + println!("Currently, only Redshift and Postgres are supported."); Err(anyhow::anyhow!("Database type not yet implemented")) } } @@ -332,6 +335,195 @@ async fn setup_redshift( } } +async fn setup_postgres( + buster_url: String, + buster_api_key: String, + config_path: &Path, +) -> Result<()> { + println!("{}", "Setting up PostgreSQL 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 PostgreSQL host:") + .with_help_message("Example: localhost or db.example.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 PostgreSQL port:") + .with_default("5432") // Default Postgres port is 5432 + .with_help_message("Default PostgreSQL port is 5432") + .with_validator(|input: &str| match input.parse::() { + 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::()?; + + // Collect username + let username = Text::new("Enter the PostgreSQL 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 PostgreSQL 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 PostgreSQL database name (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 PostgreSQL schema (optional):") + .with_help_message("Leave blank to access all available schemas") + .with_default("public") // Default Postgres schema is usually 'public' + .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 API request + let request = PostDataSourcesRequest { + name: name.clone(), + env: "dev".to_string(), // Default to dev environment + credential: Credential::Postgres(PostgresCredentials { + host, + port, + username, + password, + database: database.clone().unwrap_or_default(), + schema: schema.clone().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() + ); + + // Create a copy of the values we need for the config file + let db_copy = database.clone(); + let schema_copy = schema.clone(); + + // Create buster.yml file + create_buster_config_file( + config_path, + &name, + db_copy.as_deref(), + schema_copy.as_deref(), + )?; + + 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)) + } + } +} + // Helper function to create buster.yml file fn create_buster_config_file( path: &Path,