From caec7074501d03cf24d4f5e6df98de05aad0c5a7 Mon Sep 17 00:00:00 2001 From: dal Date: Wed, 12 Feb 2025 13:00:29 -0700 Subject: [PATCH] Add macOS ARM64 support and enhance CLI authentication and version management --- .github/workflows/cli-release.yml | 6 + cli/Cargo.toml | 18 +- cli/build.rs | 21 ++ cli/src/commands/auth.rs | 166 ++++++++++---- cli/src/commands/mod.rs | 7 +- cli/src/commands/update.rs | 263 +++++++++++++++++++++++ cli/src/commands/version.rs | 119 ++++++++++ cli/src/main.rs | 89 +++++++- cli/src/utils/file/buster_credentials.rs | 2 +- warehouse/README.md | 2 +- 10 files changed, 631 insertions(+), 62 deletions(-) create mode 100644 cli/build.rs create mode 100644 cli/src/commands/update.rs create mode 100644 cli/src/commands/version.rs diff --git a/.github/workflows/cli-release.yml b/.github/workflows/cli-release.yml index c958cca1b..7522063db 100644 --- a/.github/workflows/cli-release.yml +++ b/.github/workflows/cli-release.yml @@ -27,6 +27,10 @@ jobs: target: x86_64-apple-darwin artifact_name: buster-cli-darwin-x86_64.tar.gz use_tar: true + - os: macos-latest + target: aarch64-apple-darwin + artifact_name: buster-cli-darwin-arm64.tar.gz + use_tar: true - os: windows-latest target: x86_64-pc-windows-msvc artifact_name: buster-cli-windows-x86_64.zip @@ -116,6 +120,8 @@ jobs: **/buster-cli-linux-x86_64.tar.gz.sha256 **/buster-cli-darwin-x86_64.tar.gz **/buster-cli-darwin-x86_64.tar.gz.sha256 + **/buster-cli-darwin-arm64.tar.gz + **/buster-cli-darwin-arm64.tar.gz.sha256 **/buster-cli-windows-x86_64.zip **/buster-cli-windows-x86_64.zip.sha256 draft: false diff --git a/cli/Cargo.toml b/cli/Cargo.toml index ef8ed35d1..1b94bf817 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -1,7 +1,8 @@ [package] name = "buster-cli" -version = "0.1.0" +version = "0.0.1" edition = "2021" +build = "build.rs" [lib] name = "buster_cli" @@ -11,24 +12,33 @@ path = "src/lib.rs" [dependencies] anyhow = "1.0.79" -clap = { version = "4.4.18", features = ["derive"] } +clap = { version = "4.4.18", features = ["derive", "env"] } confy = "0.6.0" dirs = "5.0.1" +futures = "0.3.29" indicatif = "0.17.8" inquire = "0.7.5" lazy_static = "1.4.0" ratatui = "0.29.0" regex = "1.10.3" -reqwest = { version = "0.12.9", features = ["json", "rustls-tls"] } +reqwest = { version = "0.12.9", features = ["json", "rustls-tls", "stream"] } rpassword = "7.3.1" serde = { version = "1.0.196", features = ["derive"] } serde_json = "1.0.113" serde_yaml = "0.9.34" +tempfile = "3.10.0" thiserror = "2.0.3" tokio = { version = "1.36.0", features = ["full"] } +tokio-util = { version = "0.7.10", features = ["io"] } +futures-util = "0.3.30" uuid = { version = "1.7.0", features = ["v4", "serde"] } colored = "3.0" -rustls = { version = "0.22", features = ["tls12"] } +rustls = { version = "0.23", features = ["tls12"] } +url = "2.5.0" +zip = "2.2.2" [dev-dependencies] tempfile = "3.16.0" + +[build-dependencies] +chrono = "0.4" diff --git a/cli/build.rs b/cli/build.rs new file mode 100644 index 000000000..2d125c0b2 --- /dev/null +++ b/cli/build.rs @@ -0,0 +1,21 @@ +use chrono::Utc; +use std::process::Command; + +fn main() { + // Set the build date + let build_date = Utc::now().format("%Y-%m-%d %H:%M:%S UTC").to_string(); + println!("cargo:rustc-env=BUILD_DATE={}", build_date); + + // Get the git hash if possible + let git_hash = Command::new("git") + .args(["rev-parse", "--short", "HEAD"]) + .output() + .ok() + .and_then(|output| String::from_utf8(output.stdout).ok()) + .unwrap_or_else(|| "unknown".to_string()); + println!("cargo:rustc-env=GIT_HASH={}", git_hash.trim()); + + // Tell cargo to rerun this if any git changes occur + println!("cargo:rerun-if-changed=.git/HEAD"); + println!("cargo:rerun-if-changed=.git/refs/heads/main"); +} \ No newline at end of file diff --git a/cli/src/commands/auth.rs b/cli/src/commands/auth.rs index 543aea10e..2947b08c6 100644 --- a/cli/src/commands/auth.rs +++ b/cli/src/commands/auth.rs @@ -1,60 +1,132 @@ -use anyhow::Result; +use anyhow::{Context, Result}; +use clap::Parser; use inquire::{Password, Text}; +use std::env; +use thiserror::Error; use crate::utils::{ buster_credentials::{get_buster_credentials, set_buster_credentials, BusterCredentials}, BusterClient, }; -pub async fn auth() -> Result<()> { - // Get the API key from the credentials file. - // If the file doesn't exist, set it to the default. - let mut buster_creds = match get_buster_credentials().await { - Ok(buster_creds) => buster_creds, - Err(_) => match set_buster_credentials(BusterCredentials::default()).await { - Ok(_) => get_buster_credentials().await?, - Err(e) => anyhow::bail!("Failed to set credentials: {}", e), - }, - }; +const DEFAULT_HOST: &str = "https://api2.buster.so"; - let url_input = Text::new("Enter the URL of your Buster API") - .with_default(&buster_creds.url) - .prompt()?; +#[derive(Error, Debug)] +pub enum AuthError { + #[error("URL is required")] + MissingUrl, + #[error("API key is required")] + MissingApiKey, + #[error("Invalid API key")] + InvalidApiKey, + #[error("Failed to validate credentials: {0}")] + ValidationError(String), + #[error("Failed to save credentials: {0}")] + StorageError(String), +} - // If no URL at all, error - if url_input.is_empty() { - anyhow::bail!("URL is required"); +#[derive(Parser, Debug)] +#[command(about = "Authenticate with Buster API")] +pub struct AuthArgs { + /// The Buster API host URL + #[arg(long, env = "BUSTER_HOST")] + pub host: Option, + + /// Your Buster API key + #[arg(long, env = "BUSTER_API_KEY")] + pub api_key: Option, + + /// Don't save credentials to disk + #[arg(long)] + pub no_save: bool, +} + +async fn validate_credentials(url: &str, api_key: &str) -> Result<(), AuthError> { + let buster_client = BusterClient::new(url.to_string(), api_key.to_string()) + .map_err(|e| AuthError::ValidationError(e.to_string()))?; + + if !buster_client.validate_api_key().await + .map_err(|e| AuthError::ValidationError(e.to_string()))? { + return Err(AuthError::InvalidApiKey); + } + + Ok(()) +} + +pub async fn auth() -> Result<()> { + let args = AuthArgs::parse(); + auth_with_args(args).await +} + +pub async fn auth_with_args(args: AuthArgs) -> Result<()> { + // Get existing credentials or create default + let mut buster_creds = match get_buster_credentials().await { + Ok(creds) => creds, + Err(_) => BusterCredentials { + url: DEFAULT_HOST.to_string(), + api_key: String::new(), + }, + }; + + // Apply host from args or use default + if let Some(host) = args.host { + buster_creds.url = host; + } + + // Apply API key from args or environment + if let Some(api_key) = args.api_key { + buster_creds.api_key = api_key; + } + + // Interactive mode for missing values + if buster_creds.url.is_empty() { + let url_input = Text::new("Enter the URL of your Buster API") + .with_default(DEFAULT_HOST) + .with_help_message("Press Enter to use the default URL") + .prompt() + .context("Failed to get URL input")?; + + if url_input.is_empty() { + buster_creds.url = DEFAULT_HOST.to_string(); + } else { + buster_creds.url = url_input; + } + } + + if buster_creds.api_key.is_empty() { + let obfuscated_api_key = if buster_creds.api_key.is_empty() { + String::from("None") + } else { + format!("{}****", &buster_creds.api_key[..4]) + }; + + let api_key_input = Password::new(&format!("Enter your API key [{obfuscated_api_key}]:")) + .without_confirmation() + .with_help_message("Your API key can be found in your Buster dashboard") + .prompt() + .context("Failed to get API key input")?; + + if api_key_input.is_empty() && buster_creds.api_key.is_empty() { + return Err(AuthError::MissingApiKey.into()); + } else if !api_key_input.is_empty() { + buster_creds.api_key = api_key_input; + } + } + + // Validate credentials + validate_credentials(&buster_creds.url, &buster_creds.api_key).await?; + + // Save credentials unless --no-save is specified + if !args.no_save { + set_buster_credentials(buster_creds.clone()).await + .context("Failed to save credentials")?; + println!("Credentials saved successfully!"); + } + + println!("Authentication successful!"); + if args.no_save { + println!("Note: Credentials were not saved due to --no-save flag"); } - buster_creds.url = url_input; - - // Obfuscate the API key for display - let obfuscated_api_key = if buster_creds.api_key.is_empty() { - String::from("None") - } else { - format!("{}****", &buster_creds.api_key[..4]) - }; - - let api_key_input = Password::new(&format!("Enter your API key [{obfuscated_api_key}]:")) - .without_confirmation() - .prompt()?; - - if !api_key_input.is_empty() { - buster_creds.api_key = api_key_input; - } else if buster_creds.api_key.is_empty() { - anyhow::bail!("API key is required"); - } - - // Validate the API key. - let buster_client = BusterClient::new(buster_creds.url.clone(), buster_creds.api_key.clone())?; - - if !buster_client.validate_api_key().await? { - anyhow::bail!("Invalid API key"); - } - - // Save the credentials. - set_buster_credentials(buster_creds).await?; - - println!("Authentication successful!"); Ok(()) } diff --git a/cli/src/commands/mod.rs b/cli/src/commands/mod.rs index fe1df86ff..06a8dcd48 100644 --- a/cli/src/commands/mod.rs +++ b/cli/src/commands/mod.rs @@ -1,13 +1,16 @@ -mod auth; +pub mod auth; mod deploy; mod deploy_v2; mod generate; mod import; mod init; +pub mod version; +pub mod update; -pub use auth::auth; +pub use auth::{auth, auth_with_args, AuthArgs}; pub use deploy::deploy; pub use deploy_v2::deploy_v2; pub use generate::GenerateCommand; pub use import::import; pub use init::init; +pub use update::UpdateCommand; diff --git a/cli/src/commands/update.rs b/cli/src/commands/update.rs new file mode 100644 index 000000000..8680b9826 --- /dev/null +++ b/cli/src/commands/update.rs @@ -0,0 +1,263 @@ +use anyhow::{Result, Context}; +use std::path::{Path, PathBuf}; +use std::process::Command; +use std::fs; +use std::env; +use reqwest::Client; +use indicatif::{ProgressBar, ProgressStyle}; +use colored::*; +use futures_util::StreamExt; +use inquire::Confirm; +use std::io::Write; +use zip::ZipArchive; + +const GITHUB_RELEASES_URL: &str = "https://github.com/buster-so/buster/releases/download"; + +pub struct UpdateCommand { + check_only: bool, + force: bool, + no_prompt: bool, +} + +impl UpdateCommand { + pub fn new(check_only: bool, force: bool, no_prompt: bool) -> Self { + Self { + check_only, + force, + no_prompt, + } + } + + pub async fn execute(&self) -> Result<()> { + // Check current version and latest version + let current_version = env!("CARGO_PKG_VERSION"); + let latest_version = super::version::check_latest_version() + .await? + .context("Failed to get latest version")?; + + println!("Current version: {}", current_version); + println!("Latest version: {}", latest_version); + + let update_available = super::version::is_update_available(current_version, &latest_version); + + if !update_available && !self.force { + println!("\n{}", "You are using the latest version".green()); + return Ok(()); + } + + if self.check_only { + if update_available { + println!("\n{}", "Update available!".yellow().bold()); + println!("Run {} to update", "buster update".cyan()); + } + return Ok(()); + } + + if !self.no_prompt { + let confirm = Confirm::new("Do you want to update to the latest version?") + .with_default(true) + .with_help_message("This will replace your current binary with the latest version") + .prompt(); + + match confirm { + Ok(true) => (), + Ok(false) => { + println!("Update cancelled"); + return Ok(()); + } + Err(_) => { + return Err(anyhow::anyhow!("Update cancelled due to input error")); + } + } + } + + self.perform_update(&latest_version).await + } + + async fn perform_update(&self, version: &str) -> Result<()> { + let package_name = self.get_package_name()?; + let download_url = format!("{}/{}/{}", GITHUB_RELEASES_URL, version, package_name); + let checksum_url = format!("{}.sha256", download_url); + + if env::var("BUSTER_DEBUG").is_ok() { + println!("Debug: Download URL: {}", download_url); + println!("Debug: Checksum URL: {}", checksum_url); + } + + println!("\nDownloading update..."); + + // Create temporary directory + let temp_dir = tempfile::Builder::new() + .prefix("buster-update-") + .tempdir()?; + + // Download package + let package_path = temp_dir.path().join(&package_name); + match self.download_file(&download_url, &package_path).await { + Ok(_) => println!("✓ Package downloaded successfully"), + Err(e) => return Err(anyhow::anyhow!("Failed to download package: {}\nURL: {}", e, download_url)), + } + + // Download and verify checksum + let checksum_path = temp_dir.path().join(format!("{}.sha256", package_name)); + match self.download_file(&checksum_url, &checksum_path).await { + Ok(_) => println!("✓ Checksum file downloaded"), + Err(e) => { + if env::var("BUSTER_DEV").is_ok() { + println!("Skipping checksum verification in development mode"); + } else { + return Err(anyhow::anyhow!("Failed to download checksum: {}\nURL: {}", e, checksum_url)); + } + } + } + + // Get current binary path + let current_binary = env::current_exe()?; + let backup_path = current_binary.with_extension("bak"); + + // Create backup + println!("Creating backup..."); + fs::copy(¤t_binary, &backup_path)?; + + // Extract and replace binary + println!("Installing update..."); + self.replace_binary(&package_path, ¤t_binary)?; + + // Verify new binary + if self.verify_new_binary(¤t_binary)? { + fs::remove_file(backup_path)?; + println!("\n{}", "✓ Successfully updated!".green().bold()); + println!("Version: {}", version); + } else { + println!("\n{}", "✗ Update verification failed, rolling back...".red()); + fs::copy(&backup_path, ¤t_binary)?; + return Err(anyhow::anyhow!("Failed to verify new binary")); + } + + Ok(()) + } + + fn get_package_name(&self) -> Result { + let os = env::consts::OS; + let arch = env::consts::ARCH; + + let package_name = match (os, arch) { + ("linux", "x86_64") => "buster-cli-linux-x86_64.tar.gz", + ("macos", "x86_64") => "buster-cli-darwin-x86_64.tar.gz", + ("macos", "aarch64") => "buster-cli-darwin-arm64.tar.gz", + ("windows", "x86_64") => "buster-cli-windows-x86_64.zip", + _ => return Err(anyhow::anyhow!("Unsupported platform: {}-{}", os, arch)), + }; + + Ok(package_name.to_string()) + } + + async fn download_file(&self, url: &str, path: &Path) -> Result<()> { + let client = Client::new(); + let response = client + .get(url) + .header("User-Agent", "buster-cli") + .send() + .await?; + + if !response.status().is_success() { + return Err(anyhow::anyhow!( + "Failed to download file: HTTP {}\nURL: {}", + response.status(), + url + )); + } + + let total_size = response.content_length().unwrap_or(0); + let pb = ProgressBar::new(total_size); + pb.set_style( + ProgressStyle::default_bar() + .template("[{elapsed_precise}] [{bar:40.cyan/blue}] {bytes}/{total_bytes} ({eta})") + .unwrap() + .progress_chars("#>-"), + ); + + let mut file = fs::File::create(path)?; + let mut stream = response.bytes_stream(); + + while let Some(chunk) = stream.next().await { + let chunk = chunk?; + pb.inc(chunk.len() as u64); + file.write_all(&chunk)?; + } + + pb.finish_and_clear(); + Ok(()) + } + + fn verify_checksum(&self, package_path: &Path, checksum_path: &Path) -> Result<()> { + // For local development, skip checksum verification if env var is set + if env::var("BUSTER_DEV").is_ok() { + println!("Skipping checksum verification in development mode"); + return Ok(()); + } + + let expected_checksum = fs::read_to_string(checksum_path)?; + let expected_checksum = expected_checksum.split_whitespace().next() + .context("Invalid checksum file format")?; + + let output = Command::new("shasum") + .arg("-a") + .arg("256") + .arg(package_path) + .output()?; + + let actual_checksum = String::from_utf8(output.stdout)?; + let actual_checksum = actual_checksum.split_whitespace().next() + .context("Failed to compute checksum")?; + + if actual_checksum != expected_checksum { + return Err(anyhow::anyhow!( + "Checksum verification failed\nExpected: {}\nGot: {}", + expected_checksum, + actual_checksum + )); + } + + Ok(()) + } + + fn replace_binary(&self, package_path: &Path, target_path: &Path) -> Result<()> { + // Extract the package based on its extension + if package_path.extension().and_then(|s| s.to_str()) == Some("zip") { + // Handle ZIP files (Windows) + let file = fs::File::open(package_path)?; + let mut archive = ZipArchive::new(file)?; + + // Extract the binary + let mut binary = archive.by_name("buster-cli.exe")?; + let mut temp_path = package_path.parent().unwrap().join("buster-cli.exe"); + let mut outfile = fs::File::create(&temp_path)?; + std::io::copy(&mut binary, &mut outfile)?; + + // Move to target location + fs::rename(temp_path, target_path)?; + } else { + // Handle tar.gz files (Unix) + Command::new("tar") + .arg("xzf") + .arg(package_path) + .current_dir(package_path.parent().unwrap()) + .output()?; + + // Move the extracted binary to the target location + let extracted_binary = package_path.parent().unwrap().join("buster-cli"); + fs::rename(extracted_binary, target_path)?; + } + + Ok(()) + } + + fn verify_new_binary(&self, binary_path: &Path) -> Result { + let output = Command::new(binary_path) + .arg("version") + .output()?; + + Ok(output.status.success()) + } +} \ No newline at end of file diff --git a/cli/src/commands/version.rs b/cli/src/commands/version.rs new file mode 100644 index 000000000..4986737f1 --- /dev/null +++ b/cli/src/commands/version.rs @@ -0,0 +1,119 @@ +use anyhow::Result; +use reqwest::Client; +use serde::{Deserialize, Serialize}; +use std::time::{Duration, SystemTime}; +use std::fs; +use std::path::PathBuf; +use dirs::cache_dir; + +const GITHUB_API_URL: &str = "https://api.github.com/repos/buster-so/buster/releases/latest"; +const CACHE_TTL: Duration = Duration::from_secs(3600); // 1 hour + +#[derive(Deserialize)] +struct GitHubRelease { + tag_name: String, + body: Option, +} + +#[derive(Deserialize, Serialize)] +struct VersionCache { + version: String, + timestamp: u64, +} + +pub async fn check_latest_version() -> Result> { + if let Some(cached_version) = get_cached_version()? { + return Ok(Some(cached_version)); + } + + let client = Client::new(); + let response = client + .get(GITHUB_API_URL) + .header("User-Agent", "buster-cli") + .send() + .await?; + + let release: GitHubRelease = response.json().await?; + cache_version(&release.tag_name)?; + + Ok(Some(release.tag_name)) +} + +fn get_cache_path() -> Option { + let mut cache_path = cache_dir()?; + cache_path.push("buster"); + cache_path.push("version_check.json"); + Some(cache_path) +} + +fn get_cached_version() -> Result> { + let cache_path = match get_cache_path() { + Some(path) => path, + None => return Ok(None), + }; + + if !cache_path.exists() { + return Ok(None); + } + + let cache_content = fs::read_to_string(cache_path)?; + let cached: VersionCache = serde_json::from_str(&cache_content)?; + + let now = SystemTime::now() + .duration_since(SystemTime::UNIX_EPOCH)? + .as_secs(); + + if now - cached.timestamp > CACHE_TTL.as_secs() { + return Ok(None); + } + + Ok(Some(cached.version)) +} + +fn cache_version(version: &str) -> Result<()> { + let cache_path = match get_cache_path() { + Some(path) => path, + None => return Ok(()), + }; + + if let Some(parent) = cache_path.parent() { + fs::create_dir_all(parent)?; + } + + let cache = VersionCache { + version: version.to_string(), + timestamp: SystemTime::now() + .duration_since(SystemTime::UNIX_EPOCH)? + .as_secs(), + }; + + let cache_content = serde_json::to_string(&cache)?; + fs::write(cache_path, cache_content)?; + + Ok(()) +} + +pub fn is_update_available(current: &str, latest: &str) -> bool { + // Strip 'v' prefix if present + let current = current.trim_start_matches('v'); + let latest = latest.trim_start_matches('v'); + + // Split into version components + let current_parts: Vec<&str> = current.split('.').collect(); + let latest_parts: Vec<&str> = latest.split('.').collect(); + + // Compare version components + for (c, l) in current_parts.iter().zip(latest_parts.iter()) { + let c_num: u32 = c.parse().unwrap_or(0); + let l_num: u32 = l.parse().unwrap_or(0); + if l_num > c_num { + return true; + } + if c_num > l_num { + return false; + } + } + + // If we get here and latest has more components, it's newer + latest_parts.len() > current_parts.len() +} \ No newline at end of file diff --git a/cli/src/main.rs b/cli/src/main.rs index 2973564a6..92cdce6a1 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -4,16 +4,47 @@ mod types; mod utils; use clap::{Parser, Subcommand}; -use commands::{auth, deploy, deploy_v2, GenerateCommand, import, init}; +use colored::*; +use commands::{auth::AuthArgs, deploy, deploy_v2, import, init, GenerateCommand}; use std::path::PathBuf; pub const APP_NAME: &str = "buster"; +pub const VERSION: &str = env!("CARGO_PKG_VERSION"); +pub const BUILD_DATE: &str = env!("BUILD_DATE"); +pub const GIT_HASH: &str = env!("GIT_HASH"); #[derive(Subcommand)] #[clap(rename_all = "kebab-case")] pub enum Commands { Init, - Auth, + /// Authenticate with Buster API + Auth { + /// The Buster API host URL + #[arg(long, env = "BUSTER_HOST")] + host: Option, + + /// Your Buster API key + #[arg(long, env = "BUSTER_API_KEY")] + api_key: Option, + + /// Don't save credentials to disk + #[arg(long)] + no_save: bool, + }, + /// Display version information + Version, + /// Update buster-cli to the latest version + Update { + /// Only check if an update is available + #[arg(long)] + check_only: bool, + /// Force update even if already on latest version + #[arg(long)] + force: bool, + /// Skip update confirmation prompt + #[arg(short = 'y')] + no_prompt: bool, + }, Generate { #[arg(long)] source_path: Option, @@ -48,19 +79,63 @@ async fn main() { // TODO: All commands should check for an update. let result = match args.cmd { Commands::Init => init().await, - Commands::Auth => auth().await, - Commands::Generate { + Commands::Auth { + host, + api_key, + no_save, + } => { + commands::auth::auth_with_args(AuthArgs { + host, + api_key, + no_save, + }) + .await + } + Commands::Version => { + println!("{} v{}", APP_NAME.bold(), VERSION); + println!("Build Date: {}", BUILD_DATE); + println!("Git Commit: {}", GIT_HASH); + + // Check for updates + match commands::version::check_latest_version().await { + Ok(Some(latest_version)) => { + if commands::version::is_update_available(VERSION, &latest_version) { + println!("\n{}", "Update available!".yellow().bold()); + println!("Latest version: {}", latest_version.green()); + println!("Run {} to update", "buster update".cyan()); + } else { + println!("\n{}", "You are using the latest version".green()); + } + } + Ok(None) => println!("\n{}", "Unable to check for updates".yellow()), + Err(e) => println!("\n{}: {}", "Error checking for updates".red(), e), + } + Ok(()) + } + Commands::Update { + check_only, + force, + no_prompt, + } => { + let cmd = commands::update::UpdateCommand::new(check_only, force, no_prompt); + cmd.execute().await + } + Commands::Generate { source_path, destination_path, data_source_name, schema, database, } => { - let source = source_path.map(PathBuf::from).unwrap_or_else(|| PathBuf::from(".")); - let dest = destination_path.map(PathBuf::from).unwrap_or_else(|| PathBuf::from(".")); + let source = source_path + .map(PathBuf::from) + .unwrap_or_else(|| PathBuf::from(".")); + let dest = destination_path + .map(PathBuf::from) + .unwrap_or_else(|| PathBuf::from(".")); let cmd = GenerateCommand::new(source, dest, data_source_name, schema, database); cmd.execute().await - }, + } Commands::Import => import().await, Commands::Deploy { path, dry_run } => deploy_v2(path.as_deref(), dry_run).await, }; diff --git a/cli/src/utils/file/buster_credentials.rs b/cli/src/utils/file/buster_credentials.rs index 93c406729..58b4a8695 100644 --- a/cli/src/utils/file/buster_credentials.rs +++ b/cli/src/utils/file/buster_credentials.rs @@ -5,7 +5,7 @@ use tokio::fs; use crate::{error::BusterError, utils::BusterClient}; -#[derive(Serialize, Deserialize)] +#[derive(Serialize, Deserialize, Clone)] pub struct BusterCredentials { pub url: String, pub api_key: String, diff --git a/warehouse/README.md b/warehouse/README.md index 6df96da0d..42123ec94 100644 --- a/warehouse/README.md +++ b/warehouse/README.md @@ -1,4 +1,4 @@ -## Buster Warehouse Overview +#p# Buster Warehouse Overview In working with our customers, we found that Snowflake, Bigquery, and other warehouse solutions were prohibitively expensive or slow in them being able to deploy AI-powered analytics at scale.