diff --git a/tembo-cli/src/cli/auth_client.rs b/tembo-cli/src/cli/auth_client.rs deleted file mode 100644 index f0ffcbdb4..000000000 --- a/tembo-cli/src/cli/auth_client.rs +++ /dev/null @@ -1,234 +0,0 @@ -extern crate rpassword; - -use crate::cli::cloud_account::CloudAccount; -use crate::cli::config::Config; -use crate::Result; -use anyhow::{bail, Context}; -use chrono::prelude::*; -use clap::ArgMatches; -use reqwest::cookie::Jar; -use reqwest::header; -use reqwest::header::HeaderMap; -use reqwest::StatusCode; -use rpassword::read_password; -use serde_json::Value; -use simplelog::*; -use std::collections::HashMap; -use std::io; -use std::io::Write; -use std::sync::Arc; - -const CLERK_URL: &str = "https://clerk.tembo.io"; -const ORIGIN_URL: &str = "https://accounts.tembo.io"; -const CLERK_SIGN_IN_SLUG: &str = "/v1/client/sign_ins?_clerk_js_version=4.53.0"; - -pub struct AuthClient {} - -impl AuthClient { - pub fn authenticate(args: &ArgMatches) -> Result { - println!("Please enter the email address for a service user (https://tembo.io/docs/tembo-cloud/api):"); - let user = Self::get_input(); - - println!("Please enter the password for the Tembo service user:"); - std::io::stdout().flush().unwrap(); - let password = read_password().unwrap(); - - let clerk_url = CLERK_URL; - let client = Self::client(); - - match Self::create_sign_in(&client, clerk_url, &user, &password) { - Ok(token) => { - let sign_in_token = token; - // TODO: match in case this fails - let session_id = - Self::attempt_first_factor(&client, clerk_url, &sign_in_token, &password, args) - .unwrap(); - - let jwt = Self::get_expiring_api_token(&client, clerk_url, &session_id).unwrap(); - - Ok(jwt) - } - Err(e) => { - // TODO: remind users to use service accounts, not their every day user account - error!("there was an error signing in: {}", e); - Err(e) - } - } - } - - fn get_input() -> String { - let mut this_input = String::from(""); - - io::stdin() - .read_line(&mut this_input) - .expect("Failed to read line"); - this_input.trim().to_string() - } - - fn client() -> reqwest::blocking::Client { - let jar = Jar::default(); - - reqwest::blocking::Client::builder() - .cookie_store(true) - .cookie_provider(Arc::new(jar)) - .build() - .unwrap() - } - - fn headers() -> HeaderMap { - vec![ - ( - header::CONTENT_TYPE, - "application/x-www-form-urlencoded".parse().unwrap(), - ), - (header::ORIGIN, ORIGIN_URL.parse().unwrap()), - ] - .into_iter() - .collect() - } - - fn create_sign_in( - client: &reqwest::blocking::Client, - url: &str, - user: &str, - _pw: &str, - ) -> Result { - let request_url = format!("{}/{}", url, CLERK_SIGN_IN_SLUG); - - let mut map = HashMap::new(); - map.insert("identifier", user); - - let req = client.post(request_url).headers(Self::headers()).form(&map); - let res = req.send()?; - - match res.status() { - StatusCode::OK => { - let json: Value = res.json().unwrap_or("{}".into()); - let response_id = json["response"]["id"].as_str(); - - Ok(response_id.unwrap().to_string()) - } - status_code if status_code.is_client_error() => { - info!("{}", res.text()?); - bail!("Client error") - } - _ => bail!("Client error"), - } - } - - fn attempt_first_factor( - client: &reqwest::blocking::Client, - url: &str, - id: &str, - pw: &str, - args: &ArgMatches, - ) -> Result { - let request_url = format!( - "{}/v1/client/sign_ins/{}/attempt_first_factor?_clerk_js_version=4.53.0", - url, id - ); - - let mut map = HashMap::new(); - map.insert("strategy", "password"); - map.insert("password", pw); - - let req = client.post(request_url).headers(Self::headers()).form(&map); - let res = req.send()?; - - match res.status() { - StatusCode::OK => (), - status_code if status_code.is_client_error() => { - let error = res.text()?; - error!("client error:"); - error!("{error}"); - bail!("Client error: {error}"); - } - _ => (), - }; - - let json: &Value = &res.json()?; - let session_id = json["client"]["sessions"][0]["id"] - .as_str() - .with_context(|| "Failed to parse jwt")? - .to_string(); - - // set or update the user's org ids - let _ = Self::set_org_ids(json.clone(), args); - - Ok(session_id) - } - - fn get_expiring_api_token( - client: &reqwest::blocking::Client, - url: &str, - session_id: &str, - ) -> Result { - println!( - "Please enter the number of days you would like this token to valid for [1, 30, 365]:" - ); - let days = Self::get_input(); - - let request_url = format!( - "{}/v1/client/sessions/{}/tokens/api-token-{}-days?_clerk_js_version=4.53.0", - url, session_id, days - ); - - let req = client.post(request_url).headers(Self::headers()); - let res = req.send()?; - - match res.status() { - StatusCode::OK => (), - status_code if status_code.is_client_error() => { - error!("- client error:"); - error!("- {}", &res.text()?); - bail!("Client error"); - } - _ => (), - }; - - let json: &Value = &res.json()?; - let jwt = json["jwt"] - .as_str() - .with_context(|| "Failed to parse jwt")? - .to_string(); - - Ok(jwt) - } - - // stores the user's organization id(s) in the config - fn set_org_ids(json: Value, args: &ArgMatches) -> Result<()> { - let mut config = Config::new(args, &Config::full_path(args)); - let mut org_ids = vec![]; - - if let Some(organization_memberships) = - json["client"]["sessions"][0]["user"]["organization_memberships"].as_array() - { - for organization in organization_memberships { - let org_id: String = (organization["id"]).to_string(); - - org_ids.push(org_id); - } - } - - let user = &json["client"]["sessions"][0]["user"]; - let first_name = &user["first_name"]; - let last_name = &user["last_name"]; - let name = format!("{} {}", &first_name, &last_name); - let username = (user["username"]).to_string(); - let clerk_id = (user["id"]).to_string(); - - let created_at = Utc::now(); - let cloud_account = CloudAccount { - name: Some(name), - username: Some(username), - clerk_id: Some(clerk_id), - organizations: org_ids, // NOTE: we want to reset/update this with every login - created_at: Some(created_at), - }; - config.cloud_account = Some(cloud_account); - - config.write(&Config::full_path(args))?; - - Ok(()) - } -} diff --git a/tembo-cli/src/cli/cloud_account.rs b/tembo-cli/src/cli/cloud_account.rs deleted file mode 100644 index a66a2a0f3..000000000 --- a/tembo-cli/src/cli/cloud_account.rs +++ /dev/null @@ -1,13 +0,0 @@ -use chrono::prelude::*; -use serde::Deserialize; -use serde::Serialize; -use std::cmp::PartialEq; - -#[derive(Serialize, Deserialize, Debug, PartialEq)] -pub struct CloudAccount { - pub name: Option, - pub username: Option, - pub clerk_id: Option, - pub organizations: Vec, - pub created_at: Option>, -} diff --git a/tembo-cli/src/cli/config.rs b/tembo-cli/src/cli/config.rs deleted file mode 100644 index 4717edb5e..000000000 --- a/tembo-cli/src/cli/config.rs +++ /dev/null @@ -1,318 +0,0 @@ -#![allow(dead_code)] - -use crate::cli::cloud_account::CloudAccount; -use crate::cli::instance::Instance; -use crate::Result; -use chrono::prelude::*; -use clap::ArgMatches; -use serde::Deserialize; -use serde::Serialize; -use simplelog::*; -use std::env; -use std::fmt; -use std::fs; -use std::fs::File; -use std::io::{Read, Write}; -use std::path::Path; -use std::path::PathBuf; - -const CONFIG_FILE_NAME: &str = "configuration.toml"; -const CONFIG_FILE_PATH: &str = ".config/tembo/"; - -// TODO: look into swapping this out for https://crates.io/crates/config - -// NOTE: modifying the struct determines what gets persisted in the configuration file -#[derive(Serialize, Deserialize, Debug)] -pub struct Config { - pub created_at: DateTime, - pub cloud_account: Option, - pub jwt: Option, - pub instances: Vec, -} - -impl fmt::Display for Config { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let config_str = toml::to_string(&self).unwrap(); - - f.write_str(config_str.as_ref()) - } -} - -impl Config { - // Returns a default Rust object that will be persisted as serialized toml - pub fn new(_args: &ArgMatches, file_path: &PathBuf) -> Config { - let utc: DateTime = Utc::now(); - - match Self::read_to_string(file_path) { - Ok(contents) => Self::to_toml(&contents), - Err(_) => { - // NOTE: the defaults that get written to the configuration file - let config = Config { - created_at: utc, - cloud_account: None, - jwt: None, - instances: vec![], - }; - - let _init = Self::init(&config, file_path); - let _write = Self::write(&config, file_path); - - config - } - } - } - - // Reads the contents of an existing config file and returns contents as a string - pub fn read_to_string(file_path: &PathBuf) -> Result { - let mut file = File::open(file_path)?; - let mut contents = String::new(); - - file.read_to_string(&mut contents) - .expect("Unable to read stack config file"); - - Ok(contents) - } - - // Returns a Config object serialized to toml from a string - pub fn to_toml(str: &str) -> Config { - let config: Config = toml::from_str(str).unwrap(); - - config - } - - // Writes the current Config to the config file, overwriting anything else that was there - pub fn write(&self, file_path: &PathBuf) -> Result<()> { - let mut file = File::create(file_path)?; - let _delete = file.set_len(0); // this deletes all contents from the file - - let _result = file.write_all(self.to_string().as_bytes()); - - Ok(()) - } - - // Returns the full path to the config file - pub fn full_path(_args: &ArgMatches) -> PathBuf { - // NOTE: only supporting a file in the home directory for now - let home_dir = home::home_dir(); - - // if home directory can not be determined, use the current directory - match home_dir { - Some(mut path) => { - path.push(CONFIG_FILE_PATH); - path.push(CONFIG_FILE_NAME); - - path - } - None => env::current_dir().expect("Unable to determine the current directory"), - } - } - - // Creates the config directory - fn create_config_dir(dir_path: &str) -> Result<()> { - fs::create_dir_all(dir_path)?; - - Ok(()) - } - - // Creates the config file in the config directory - fn create_config_file(path: &str) -> Result<()> { - File::create_new(path)?; // don't overwrite existing file at path - - Ok(()) - } - - // Initializes the config file, creating the directories and files as needed - fn init(&self, file_path: &Path) -> Result<()> { - let mut dir_path = file_path.to_path_buf(); - dir_path.pop(); // removes any filename and extension - - match Config::create_config_dir(&dir_path.to_string_lossy()) { - Ok(()) => Config::create_config_file(&file_path.to_string_lossy()), - Err(e) => { - error!("Directory can not be created, {}", e); - - Err(e) - } - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::cli::config::Config; - use crate::cli::instance::{EnabledExtension, InstalledExtension, Instance}; - use clap::{Arg, ArgAction, Command}; - use std::env; - - fn test_path() -> PathBuf { - let mut path: PathBuf = env::current_dir().unwrap(); - path.push("tests"); - path.push(".config"); - path.push("tembo"); - - let _result = Config::create_config_dir(&path.to_string_lossy()); - - path.push("configuration.toml"); - - return path; - } - - fn setup() -> Config { - let matches = Command::new("myapp") - .arg( - Arg::new("file-path") - .value_parser(clap::value_parser!(std::path::PathBuf)) - .action(ArgAction::Set) - .required(false), - ) - .get_matches_from(vec!["myapp"]); - - let path: PathBuf = test_path(); - let config = Config::new(&matches, &path); // calls init and writes the file - - return config; - } - - fn cleanup() { - let path = test_path(); - let _ = std::fs::remove_file(&*path.to_string_lossy()); - } - - // NOTE: wrap tests that require a setup and cleanup step - #[test] - fn config_tests() { - setup(); - - init_test(); - read_to_string_test(); - to_toml_test(); - - cleanup(); - } - - fn init_test() { - // retrieves the full path, pops off the file_path, creates the directories if needed, and - // writes the file - let matches = Command::new("myapp") - .arg( - Arg::new("file-path") - .value_parser(clap::value_parser!(std::path::PathBuf)) - .action(ArgAction::Set) - .required(false), - ) - .get_matches_from(vec!["myapp"]); - - let path = test_path(); - let _config = Config::new(&matches, &path); // calls init and writes the file - // - let file = File::open(path); - let mut contents = String::new(); - - let _ = file.unwrap().read_to_string(&mut contents); - - assert_eq!(contents, Config::to_toml(&contents).to_string()); - } - - fn read_to_string_test() { - let _matches = Command::new("myapp") - .arg( - Arg::new("file-path") - .value_parser(clap::value_parser!(std::path::PathBuf)) - .action(ArgAction::Set) - .required(false), - ) - .get_matches_from(vec!["myapp"]); - - let path = test_path(); - let config = Config::read_to_string(&path); - - assert_eq!(config.is_ok(), true); - } - - fn to_toml_test() { - let mut config = setup(); - let toml = Config::to_toml(&config.to_string()); - - // with no instances - assert_eq!(toml.instances, vec![]); - - // wth instances - let instance = Instance { - name: Some(String::from("instance_name")), - r#type: Some(String::from("standard")), - port: Some(String::from("5432")), - version: Some(String::from("1.1")), - created_at: Some(Utc::now()), - installed_extensions: vec![InstalledExtension { - name: Some(String::from("pgmq")), - version: Some(String::from("1.0")), - created_at: Some(Utc::now()), - }], - enabled_extensions: vec![EnabledExtension { - name: Some(String::from("pgmq")), - version: Some(String::from("1.0")), - created_at: Some(Utc::now()), - locations: vec![], - }], - databases: vec![], - }; - config.instances = vec![instance]; - - let toml = Config::to_toml(&config.to_string()); - - //assert_eq!(toml.created_at.is_some(), true); - assert_eq!(toml.instances[0].name, Some(String::from("instance_name"))); - assert_eq!( - toml.instances[0].installed_extensions[0].name, - Some(String::from("pgmq")) - ); - } - /* - #[test] - fn full_path_test() { - let matches = Command::new("myapp") - .arg( - Arg::new("file-path") - .value_parser(clap::value_parser!(std::path::PathBuf)) - .action(ArgAction::Set) - .required(false), - ) - .get_matches_from(vec!["myapp"]); - - let binding = home::home_dir().unwrap(); - let home_dir = &binding.to_str().unwrap(); - - let result = Config::full_path(&matches); - - assert!(result.to_str().unwrap().contains(&*home_dir)); - } - - #[test] - fn create_config_dir_test() { - let mut path: PathBuf = test_path(); - path.pop(); - - let write = Config::create_config_dir(&path.to_string_lossy()); - assert_eq!(write.is_ok(), true); - - let overwrite = Config::create_config_file(&path.to_string_lossy()); - assert_eq!(overwrite.is_err(), true); - - cleanup(); - } - - #[test] - fn create_config_file_test() { - let path: PathBuf = test_path(); - - let write = Config::create_config_file(&path.to_string_lossy()); - assert_eq!(write.is_ok(), true); - - let overwrite = Config::create_config_file(&path.to_string_lossy()); - assert_eq!(overwrite.is_err(), true); - - cleanup(); - } - */ -} diff --git a/tembo-cli/src/cli/database.rs b/tembo-cli/src/cli/database.rs deleted file mode 100644 index eac68f59c..000000000 --- a/tembo-cli/src/cli/database.rs +++ /dev/null @@ -1,12 +0,0 @@ -// Objects representing a user created local database -use crate::cli::schema::Schema; -use chrono::prelude::*; -use serde::Deserialize; -use serde::Serialize; - -#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)] -pub struct Database { - pub name: String, - pub created_at: DateTime, - pub schemas: Vec, -} diff --git a/tembo-cli/src/cli/docker.rs b/tembo-cli/src/cli/docker.rs index 7c7c699a1..f399a3d03 100644 --- a/tembo-cli/src/cli/docker.rs +++ b/tembo-cli/src/cli/docker.rs @@ -1,4 +1,3 @@ -use crate::cli::instance::Instance; use crate::Result; use anyhow::bail; use simplelog::*; @@ -75,24 +74,6 @@ impl Docker { Ok(()) } - // start container if exists for name otherwise build container and start - pub fn start(name: &str, instance: &Instance) -> Result { - if Self::container_list_filtered(name) - .unwrap() - .contains("tembo-pg") - { - info!("existing container found"); - - instance.start(); - } else { - info!("building and then running container"); - - let _ = instance.init(); - }; - - Ok(()) - } - // stop & remove container for given name pub fn stop_remove(name: &str) -> Result { let mut sp = Spinner::new(Spinners::Line, "Stopping & Removing instance".into()); diff --git a/tembo-cli/src/cli/instance.rs b/tembo-cli/src/cli/instance.rs deleted file mode 100644 index 0fbba91b3..000000000 --- a/tembo-cli/src/cli/instance.rs +++ /dev/null @@ -1,236 +0,0 @@ -//! Objects representing a user created local instance of a stack -//! (a local container that runs with certain attributes and properties) - -use crate::cli::config::Config; -use crate::cli::database::Database; -use crate::cli::extension::Extension; -use crate::cli::stacks; -use crate::cli::stacks::{Stack, TrunkInstall}; -use crate::Result; -use anyhow::bail; -use chrono::prelude::*; -use clap::ArgMatches; -use serde::Deserialize; -use serde::Serialize; -use simplelog::*; -use spinners::{Spinner, Spinners}; -use std::cmp::PartialEq; -use std::process::Command as ShellCommand; - -#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)] -pub struct Instance { - pub name: Option, - pub r#type: Option, - pub port: Option, // TODO: persist as an - pub version: Option, - pub created_at: Option>, - pub installed_extensions: Vec, - pub enabled_extensions: Vec, - pub databases: Vec, -} - -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -pub struct InstalledExtension { - pub name: Option, - pub version: Option, - pub created_at: Option>, -} - -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -pub struct EnabledExtension { - pub name: Option, - pub version: Option, - pub created_at: Option>, - pub locations: Vec, -} - -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -pub struct ExtensionLocation { - pub database: String, - pub enabled: String, - pub version: String, -} - -#[derive(Debug)] -pub struct InstanceError { - pub name: String, -} - -impl Instance { - pub fn init(&self) -> Result<()> { - let stack = self.stack(); - - self.build(); - - for install in &stack.trunk_installs { - let _ = self.install_extension(install); - } - - for extension in &stack.extensions { - let _ = self.enable_extension(extension); - } - - Ok(()) - } - - // Returns the stack the instance is based on - // TODO: determine if there is a way to return a vector element in a better way - fn stack(&self) -> Stack { - let stacks = stacks::define_stacks(); - let stack_type = self.r#type.clone().unwrap().to_lowercase(); - - let stack_details: Vec<_> = stacks - .stacks - .into_iter() - .filter(|s| s.name.to_lowercase() == stack_type) - .collect(); - - let stack = stack_details.first().unwrap(); - - Stack { - name: stack.name.clone(), - description: stack.description.clone(), - version: stack.version.clone(), - trunk_installs: stack.trunk_installs.clone(), - extensions: stack.extensions.clone(), - } - } - - // builds (and starts) a new container - fn build(&self) { - let port_option = format!( - "--publish {}:{}", - self.port.clone().unwrap(), - self.port.clone().unwrap(), - ); - - let mut command = String::from("cd tembo "); - command.push_str("&& docker run -d --name "); - command.push_str(&self.name.clone().unwrap()); - command.push(' '); - command.push_str(&port_option); - command.push_str(" tembo-pg"); - - let _ = self.run_command(&command); - } - - // starts the existing container - pub fn start(&self) { - let mut command = String::from("cd tembo "); - command.push_str("&& docker start "); - command.push_str(&self.name.clone().unwrap()); - - let _ = self.run_command(&command); - } - - fn run_command(&self, command: &str) -> Result<()> { - let mut sp = Spinner::new(Spinners::Line, "Starting instance".into()); - - let output = ShellCommand::new("sh") - .arg("-c") - .arg(command) - .output() - .expect("failed to execute process"); - - let message = format!( - "- Tembo instance started on {}", - &self.port.clone().unwrap(), - ); - sp.stop_with_message(message); - - let stderr = String::from_utf8(output.stderr).unwrap(); - - if !stderr.is_empty() { - bail!("There was an issue starting the instance: {}", stderr) - } - - Ok(()) - } - - pub fn install_extension(&self, extension: &TrunkInstall) -> Result<()> { - let mut sp = Spinner::new(Spinners::Dots12, "Installing extension".into()); - - let mut command = String::from("cd tembo && docker exec "); - command.push_str(&self.name.clone().unwrap()); - command.push_str(" sh -c 'trunk install "); - command.push_str(&extension.name.clone().unwrap()); - command.push('\''); - - let output = ShellCommand::new("sh") - .arg("-c") - .arg(&command) - .output() - .expect("failed to execute process"); - - sp.stop_with_newline(); - - let stderr = String::from_utf8(output.stderr).unwrap(); - - if !stderr.is_empty() { - bail!("There was an issue installing the extension: {}", stderr) - } else { - let mut msg = String::from("- Stack extension installed: "); - msg.push_str(&extension.name.clone().unwrap()); - - println!("{}", msg); - - Ok(()) - } - } - - fn enable_extension(&self, extension: &Extension) -> Result<()> { - let mut sp = Spinner::new(Spinners::Dots12, "Enabling extension".into()); - - let locations = extension - .locations - .iter() - .map(|s| s.database.as_str()) - .collect::>() - .join(", "); - - let mut command = String::from("docker exec "); - command.push_str(&self.name.clone().unwrap()); - command.push_str(" sh -c 'psql -U postgres -c create extension if not exists \""); - command.push_str(&extension.name.clone().unwrap()); - command.push_str("\" schema "); - command.push_str(&locations); - command.push_str(" cascade;'"); - - let output = ShellCommand::new("sh") - .arg("-c") - .arg(&command) - .output() - .expect("failed to execute process"); - - let mut msg = String::from("- Stack extension enabled: "); - msg.push_str(&extension.name.clone().unwrap()); - - sp.stop_with_message(msg); - - let stderr = String::from_utf8(output.stderr).unwrap(); - - if !stderr.is_empty() { - bail!("There was an issue enabling the extension: {}", stderr) - } else { - Ok(()) - } - } - - pub fn find(args: &ArgMatches, name: &str) -> Result { - let config = Config::new(args, &Config::full_path(args)); - - info!("finding config for instance {}", name); - - for instance in &config.instances { - let i_name = instance.name.clone().unwrap(); - - if i_name.to_lowercase() == name.to_lowercase() { - let existing = Instance { ..instance.clone() }; - - return Ok(existing); - } - } - - bail!("Instance {name} not found"); - } -} diff --git a/tembo-cli/src/cli/mod.rs b/tembo-cli/src/cli/mod.rs index 05e9e14c1..7587cdee3 100644 --- a/tembo-cli/src/cli/mod.rs +++ b/tembo-cli/src/cli/mod.rs @@ -1,12 +1,5 @@ -pub mod auth_client; -pub mod cloud_account; -pub mod config; pub mod context; -pub mod database; pub mod docker; pub mod extension; pub mod file_utils; -pub mod instance; -pub mod schema; -pub mod stacks; pub mod tembo_config; diff --git a/tembo-cli/src/cli/stacks.rs b/tembo-cli/src/cli/stacks.rs deleted file mode 100644 index 0062feeae..000000000 --- a/tembo-cli/src/cli/stacks.rs +++ /dev/null @@ -1,107 +0,0 @@ -//! Stacks are defined templates provided by Tembo containing attributes and extensions -//! (templates contain configuration information tailored to a specific use case) - -use crate::cli::extension::Extension; -use crate::Result; -use crate::{Deserialize, Serialize}; -use anyhow::bail; -use chrono::{DateTime, Utc}; -use clap::ArgMatches; -use std::fs::File; -use std::io::Read; - -// object containing all of the defined stacks -#[derive(Debug, PartialEq, Serialize, Deserialize)] -pub struct Stacks { - pub stacks: Vec, -} - -// TODO: give a stack a unique id (name + version) -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -pub struct Stack { - pub name: String, - pub description: String, - pub version: String, - pub trunk_installs: Vec, - pub extensions: Vec, -} - -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -pub struct TrunkInstall { - pub name: Option, - pub version: Option, - pub created_at: Option>, -} - -// returns a result containing the stack name -pub fn define_stack(args: &ArgMatches) -> Result { - let stacks: Stacks = define_stacks(); - let names: Vec = stacks - .stacks - .clone() - .into_iter() - .map(|stack| stack.name.to_lowercase()) - .collect(); - - let passed = args.try_get_one::("stack"); - - if let Ok(Some(stack_option)) = passed { - let given_stack = stack_option.to_lowercase(); - - if !names.contains(&given_stack) { - bail!("- Given Stack type not valid"); - } - - Ok(given_stack) - } else { - // when no stack type is provided - Ok("standard".to_owned()) - } -} - -// returns a Stacks object containing attributes loaded from the stacks.yml file -pub fn define_stacks() -> Stacks { - let file = "./tembo/stacks.yaml"; // TODO: move to a constant - let mut file = File::open(file).expect("Unable to open stack config file"); - let mut contents = String::new(); - - file.read_to_string(&mut contents) - .expect("Unable to read stack config file"); - - serde_yaml::from_str(&contents).unwrap() -} - -#[cfg(test)] -mod tests { - use super::*; - use clap::{Arg, ArgAction, Command}; - - #[test] - fn define_stack_test() { - // given a stack name that matches - let app = Command::new("myapp").arg( - Arg::new("stack") - .value_parser(clap::value_parser!(String)) - .action(ArgAction::Set) - .required(false), - ); - - let matches = app.get_matches_from(vec!["myapp", "standard"]); - - assert_eq!(define_stack(&matches).unwrap(), "standard"); - - // given a stack name that does not match - let app = Command::new("myapp").arg( - Arg::new("stack") - .value_parser(clap::value_parser!(String)) - .action(ArgAction::Set) - .required(false), - ); - - let matches = app.get_matches_from(vec!["myapp", "unknown"]); - - let expected = anyhow::anyhow!("- Given Stack type not valid").to_string(); - let result = define_stack(&matches).err().unwrap().to_string(); - assert_eq!(expected, result); - } -} diff --git a/tembo-cli/src/cmd/auth.rs b/tembo-cli/src/cmd/auth.rs deleted file mode 100644 index 6d6daeb31..000000000 --- a/tembo-cli/src/cmd/auth.rs +++ /dev/null @@ -1,26 +0,0 @@ -use crate::Result; - -use clap::ArgMatches; -use simplelog::*; - -pub mod info; -pub mod login; - -// handles all instance command calls -pub fn execute(args: &ArgMatches) -> Result<()> { - // execute the instance subcommands - let res = match args.subcommand() { - Some(("login", sub_matches)) => login::execute(sub_matches), - Some(("info", sub_matches)) => info::execute(sub_matches), - _ => unreachable!(), - }; - - if res.is_err() { - error!("{}", res.err().unwrap()); - - // TODO: adding logging, log error - std::process::exit(101); - } - - Ok(()) -} diff --git a/tembo-cli/src/cmd/auth/info.rs b/tembo-cli/src/cmd/auth/info.rs deleted file mode 100644 index 69f8f48ab..000000000 --- a/tembo-cli/src/cmd/auth/info.rs +++ /dev/null @@ -1,44 +0,0 @@ -//! auth info command - -use crate::cli::config::Config; -use crate::Result; -use clap::{ArgMatches, Command}; -use dateparser::parse; -use jwt::Claims; -use jwt::Header; -use jwt::Token; -use simplelog::*; - -// example usage: tembo auth info -pub fn make_subcommand() -> Command { - Command::new("info").about("Command used to login/authenticate") -} - -pub fn execute(args: &ArgMatches) -> Result<()> { - let config = Config::new(args, &Config::full_path(args)); - let jwt = config.jwt.unwrap(); - - if jwt.is_empty() { - info!("No auth info, to authenticate, run tembo auth login"); - } else { - let _ = print_jwt_info(&jwt); - } - - Ok(()) -} - -// NOTE: uses println rather than logging intentionally -fn print_jwt_info(jwt: &str) -> Result<()> { - println!("Tembo auth information:"); - - let token: Token = Token::parse_unverified(jwt)?; - let claims = token.claims(); - let registered = &claims.registered; - println!("- issuer: {}", ®istered.issuer.clone().unwrap()); - - let expiration = ®istered.expiration.unwrap(); - let human_expire = parse(&expiration.to_string()); - println!("- expiration: {}", human_expire.unwrap().to_rfc2822()); - - Ok(()) -} diff --git a/tembo-cli/src/cmd/auth/login.rs b/tembo-cli/src/cmd/auth/login.rs deleted file mode 100644 index 23fa36584..000000000 --- a/tembo-cli/src/cmd/auth/login.rs +++ /dev/null @@ -1,28 +0,0 @@ -//! auth login command - -use crate::cli::{auth_client::AuthClient, config::Config}; -use crate::Result; -use clap::{ArgMatches, Command}; - -// example usage: tembo auth login -pub fn make_subcommand() -> Command { - Command::new("login").about("Command used to login/authenticate") -} - -pub fn execute(args: &ArgMatches) -> Result<()> { - match AuthClient::authenticate(args) { - Ok(jwt) => { - println!("- storing jwt in config file, it will be used in future requests"); - - let mut config = Config::new(args, &Config::full_path(args)); - config.jwt = Some(jwt); - let _ = config.write(&Config::full_path(args)); - - Ok(()) - } - Err(e) => { - println!("- there was an error authenticating"); - Err(e) - } - } -} diff --git a/tembo-cli/src/cmd/database.rs b/tembo-cli/src/cmd/database.rs deleted file mode 100644 index f82bf69b2..000000000 --- a/tembo-cli/src/cmd/database.rs +++ /dev/null @@ -1,22 +0,0 @@ -use crate::Result; -use clap::ArgMatches; -use simplelog::*; - -pub mod create; - -// handles all extension command calls -pub fn execute(args: &ArgMatches) -> Result<()> { - // execute the instance subcommands - let res = match args.subcommand() { - Some(("create", sub_matches)) => create::execute(sub_matches), - _ => unreachable!(), - }; - - if let Err(err) = res { - error!("{err}"); - - std::process::exit(101); - } - - Ok(()) -} diff --git a/tembo-cli/src/cmd/database/create.rs b/tembo-cli/src/cmd/database/create.rs deleted file mode 100644 index 84756a878..000000000 --- a/tembo-cli/src/cmd/database/create.rs +++ /dev/null @@ -1,117 +0,0 @@ -//! database create command - -use crate::cli::config::Config; -use crate::cli::database::Database; -use crate::cli::instance::Instance; -use crate::Result; -use anyhow::bail; -use chrono::Utc; -use clap::{Arg, ArgAction, ArgMatches, Command}; -use simplelog::*; -use spinners::{Spinner, Spinners}; -use std::process::Command as ShellCommand; - -// example usage: tembo db create -n my_database -i my_instance -pub fn make_subcommand() -> Command { - Command::new("create") - .about("Command used to create databases on instances") - .arg( - Arg::new("name") - .short('n') - .long("name") - .action(ArgAction::Set) - .required(true) - .help("The name of the database to create"), - ) - .arg( - Arg::new("instance") - .short('i') - .long("instance") - .action(ArgAction::Set) - .required(true) - .help("The name of the instance to create the database on"), - ) -} - -pub fn execute(args: &ArgMatches) -> Result<()> { - let config = Config::new(args, &Config::full_path(args)); - let name_arg = args.try_get_one::("name").unwrap(); - let instance_arg = args.try_get_one::("instance").unwrap(); - - info!( - "trying to create database '{}' on instance '{}'", - &name_arg.unwrap(), - &instance_arg.unwrap() - ); - - if config.instances.is_empty() { - warn!("- No instances have been configured"); - } else { - let instance = Instance::find(args, instance_arg.unwrap())?; - - create_database(instance, name_arg.unwrap(), args)?; - } - - Ok(()) -} - -fn create_database(instance: Instance, name: &str, args: &ArgMatches) -> Result<()> { - instance.start(); - - let mut sp = Spinner::new(Spinners::Dots12, "Creating database".into()); - - // psql -h localhost -U postgres -c 'create database test;' - let mut command = String::from("psql -h localhost -U postgres -p "); - command.push_str(&instance.port.clone().unwrap()); - command.push_str(" -c 'create database "); - command.push_str(name); - command.push_str(";'"); - - let output = ShellCommand::new("sh") - .arg("-c") - .arg(&command) - .output() - .expect("failed to execute process"); - - sp.stop_with_newline(); - - let stderr = String::from_utf8(output.stderr).unwrap(); - - if !stderr.is_empty() { - bail!("There was an issue creating the database: {}", stderr) - } else { - info!("database created"); - - let _ = persist_config(args, instance); - - Ok(()) - } -} - -fn persist_config(args: &ArgMatches, target_instance: Instance) -> Result<()> { - let mut config = Config::new(args, &Config::full_path(args)); - let name_arg = args.try_get_one::("name"); - - // TODO: push onto databases vector - let database = Database { - name: name_arg.unwrap().unwrap().to_string(), - created_at: Utc::now(), - schemas: vec![], - }; - - for instance in config.instances.iter_mut() { - if instance.name.clone().unwrap().to_lowercase() - == target_instance.name.clone().unwrap().to_lowercase() - { - instance.databases.push(database.clone()); - } - } - - match &config.write(&Config::full_path(args)) { - Ok(_) => Ok(()), - Err(e) => { - error!("there was an error: {}", e); - bail!("there was an error writing the config: {e}") - } - } -} diff --git a/tembo-cli/src/cmd/extension.rs b/tembo-cli/src/cmd/extension.rs deleted file mode 100644 index b70df6ad4..000000000 --- a/tembo-cli/src/cmd/extension.rs +++ /dev/null @@ -1,25 +0,0 @@ -use crate::Result; -use clap::ArgMatches; -use simplelog::*; - -pub mod install; -pub mod list; - -// handles all extension command calls -pub fn execute(args: &ArgMatches) -> Result<()> { - // execute the instance subcommands - let res = match args.subcommand() { - Some(("list", sub_matches)) => list::execute(sub_matches), - Some(("install", sub_matches)) => install::execute(sub_matches), - _ => unreachable!(), - }; - - if res.is_err() { - error!("{}", res.err().unwrap()); - - // TODO: adding logging, log error - std::process::exit(101); - } - - Ok(()) -} diff --git a/tembo-cli/src/cmd/extension/install.rs b/tembo-cli/src/cmd/extension/install.rs deleted file mode 100644 index ffef6409a..000000000 --- a/tembo-cli/src/cmd/extension/install.rs +++ /dev/null @@ -1,126 +0,0 @@ -//! extension install command - -use crate::cli::config::Config; -use crate::cli::instance::{InstalledExtension, Instance}; -use crate::cli::stacks::TrunkInstall; -use crate::Result; -use anyhow::bail; -use chrono::Utc; -use clap::{Arg, ArgAction, ArgMatches, Command}; -use simplelog::*; -use std::io; - -// example usage: tembo extension install -n pgmq -i my_instance -pub fn make_subcommand() -> Command { - Command::new("install") - .about("Command used to install extensions for instances") - .arg( - Arg::new("instance") - .short('i') - .long("instance") - .action(ArgAction::Set) - .required(true) - .help("The name of the instance to install the extension for"), - ) -} - -pub fn execute(args: &ArgMatches) -> Result<()> { - let config = Config::new(args, &Config::full_path(args)); - let instance_arg = args.try_get_one::("instance").unwrap(); - - println!("What extension would you like to install? Example: pgmq"); - let mut name_str = String::from(""); - - io::stdin() - .read_line(&mut name_str) - .expect("Failed to read line"); - let name_str = name_str.trim().to_string().replace('\n', ""); - - println!( - "trying to install extension '{}' on instance '{}'", - &name_str, - &instance_arg.unwrap() - ); - - if config.instances.is_empty() { - println!("- No instances have been configured"); - } else { - let instance = Instance::find(args, instance_arg.unwrap())?; - - install_extension(instance, &name_str, args)?; - } - - Ok(()) -} - -fn install_extension(instance: Instance, name: &str, args: &ArgMatches) -> Result<()> { - println!("What version would you like to install? Example: 2.1.0"); - let mut version_str = String::from(""); - - io::stdin() - .read_line(&mut version_str) - .expect("Failed to read line"); - let version_str = version_str.trim().to_string().replace('\n', ""); - - // TODO: decide if this should just prompt the user to start the instance first - instance.start(); - - for extension in &instance.installed_extensions { - // TODO: make sure the version is the same, what to do if it is not? - if extension.name.clone().unwrap().to_lowercase() == name.to_lowercase() { - warn!( - "extension {} is already installed for instance {}, remove first before upgrading version", - &name, - &instance.name.clone().unwrap() - ); - } else { - // try installing extension unless already installed - let trunk_install = TrunkInstall { - name: Some(name.to_string()), - version: Some(version_str.clone().to_string()), - created_at: Some(Utc::now()), - }; - - match instance.install_extension(&trunk_install) { - Ok(()) => { - info!("extension {} installed", name); - let _ = persist_config(args, trunk_install); - - // TODO: provide feedback on enabling the extension once enable action is in place - break; - } - Err(e) => error!("there was an error: {}", e), - } - } - } - - Ok(()) -} - -fn persist_config(args: &ArgMatches, trunk_install: TrunkInstall) -> Result<()> { - let mut config = Config::new(args, &Config::full_path(args)); - let target_instance = args.try_get_one::("instance"); - let installed_extension = InstalledExtension { - name: trunk_install.name, - version: trunk_install.version, - created_at: trunk_install.created_at, - }; - - for instance in config.instances.iter_mut() { - if instance.name.clone().unwrap().to_lowercase() - == target_instance.clone().unwrap().unwrap().to_lowercase() - { - instance - .installed_extensions - .push(installed_extension.clone()); - } - } - - match &config.write(&Config::full_path(args)) { - Ok(_) => Ok(()), - Err(e) => { - error!("there was an error: {}", e); - bail!("there was an error writing the config: {e}") - } - } -} diff --git a/tembo-cli/src/cmd/extension/list.rs b/tembo-cli/src/cmd/extension/list.rs deleted file mode 100644 index 16a3c954d..000000000 --- a/tembo-cli/src/cmd/extension/list.rs +++ /dev/null @@ -1,92 +0,0 @@ -//! extension list command - -use crate::cli::config::Config; -use crate::cli::instance::Instance; -use crate::Result; -use clap::{Arg, ArgAction, ArgMatches, Command}; -use simplelog::*; - -// example usage: tembo extension list -n test -pub fn make_subcommand() -> Command { - Command::new("list") - .about("Command used to list extensions for instances") - .arg( - Arg::new("name") - .short('n') - .long("name") - .action(ArgAction::Set) - .required(true) - .help("The name you want to use for this instance"), - ) -} - -pub fn execute(args: &ArgMatches) -> Result<()> { - let config = Config::new(args, &Config::full_path(args)); - let name = args.try_get_one::("name").unwrap(); - - if config.instances.is_empty() { - info!("No instances have been configured"); - } else { - let mut instances = vec![]; - - for instance in &config.instances { - if instance.name.clone().unwrap().to_lowercase() == name.unwrap().to_lowercase() { - instances.push(instance); - - installed_extensions(instance); - enabled_extensions(instance); - } - } - - if instances.is_empty() { - info!("No configuration found for {}", &name.unwrap()); - } - } - - Ok(()) -} - -// NOTE: uses println vs logging intentionally -fn installed_extensions(instance: &Instance) { - println!("- Installed extensions"); - - for extension in &instance.installed_extensions { - println!( - " {} - version: {}, installed: {}", - extension.name.clone().unwrap(), - extension.version.clone().unwrap(), - extension.created_at.unwrap().clone() - ); - } -} - -// NOTE: uses println vs logging intentionally -fn enabled_extensions(instance: &Instance) { - println!("- Enabled extensions (on databases)"); - - let mut extensions = vec![]; - - for extension in &instance.enabled_extensions { - let mut locations = vec![]; - - for location in &extension.locations { - if location.enabled == "true" { - locations.push(location.database.clone()); - } - } - - if !locations.is_empty() { - extensions.push(extension); - - println!( - " {} - locations: {}", - extension.name.clone().unwrap(), - locations.join(","), - ); - } - } - - if extensions.is_empty() { - println!(" none"); - } -} diff --git a/tembo-cli/src/cmd/instance.rs b/tembo-cli/src/cmd/instance.rs deleted file mode 100644 index bfe400c2c..000000000 --- a/tembo-cli/src/cmd/instance.rs +++ /dev/null @@ -1,27 +0,0 @@ -use crate::Result; -use clap::ArgMatches; -use simplelog::*; - -pub mod create; -pub mod list; -pub mod start; - -// handles all instance command calls -pub fn execute(args: &ArgMatches) -> Result<()> { - // execute the instance subcommands - let res = match args.subcommand() { - Some(("create", sub_matches)) => create::execute(sub_matches), - Some(("list", sub_matches)) => list::execute(sub_matches), - Some(("start", sub_matches)) => start::execute(sub_matches), - _ => unreachable!(), - }; - - if res.is_err() { - error!("{}", res.err().unwrap()); - - // TODO: adding logging, log error - std::process::exit(101); - } - - Ok(()) -} diff --git a/tembo-cli/src/cmd/instance/create.rs b/tembo-cli/src/cmd/instance/create.rs deleted file mode 100644 index 1edea2027..000000000 --- a/tembo-cli/src/cmd/instance/create.rs +++ /dev/null @@ -1,200 +0,0 @@ -//! instance create command - -use crate::cli::config::Config; -use crate::cli::docker::Docker; -use crate::cli::instance::{EnabledExtension, InstalledExtension, Instance}; -use crate::cli::stacks; -use crate::cli::stacks::Stacks; -use crate::Result; -use anyhow::bail; -use chrono::prelude::*; -use clap::{Arg, ArgAction, ArgMatches, Command}; -use simplelog::*; - -// example usage: tembo instance create -t oltp -n my_app_db -p 5432 -pub fn make_subcommand() -> Command { - Command::new("create") - .about("Command used to create a local instance") - .arg( - Arg::new("type") - .short('t') - .long("type") - .action(ArgAction::Set) - .required(false) - .default_value("standard") - .help("The name of a Tembo stack type to use"), - ) - .arg( - Arg::new("name") - .short('n') - .long("name") - .action(ArgAction::Set) - .required(true) - .help("The name you want to use for this instance"), - ) - .arg( - Arg::new("port") - .short('p') - .long("port") - .action(ArgAction::Set) - .required(false) - .default_value("5432") - .help("The port number you want to use for this instance (default is 5432)"), - ) -} - -pub fn execute(args: &ArgMatches) -> Result<()> { - let matches = args; - - // ensure the stack type provided is valid, if none given, default to the standard stack - if let Ok(_stack) = stacks::define_stack(matches) { - // make sure requirements are met - match check_requirements() { - Ok(_) => info!("Docker was found and appears running"), - Err(e) => { - error!("{}", e); - return Err(e); - } - } - - // TODO: make sure the instance name is valid - // unique and API will respond with 4xx when instance name starts - // or ends with non-alpha numeric character - - match persist_instance_config(matches) { - Ok(_) => info!("Instance config persisted in config file"), - Err(e) => { - error!("{}", e); - return Err(e); - } - } - - info!("Instance configuration created, you can start the instance using the command 'tembo start -i '"); - } else { - bail!("- Given Stack type is not valid"); - } - - Ok(()) -} - -fn check_requirements() -> Result<()> { - Docker::installed_and_running() -} - -fn persist_instance_config(matches: &ArgMatches) -> Result<()> { - let path = Config::full_path(matches); - let mut config = Config::new(matches, &path); // calls init and writes the file - - let r#type = matches.get_one::("type").unwrap(); - let name = matches.get_one::("name").unwrap(); - let port = matches.get_one::("port").unwrap(); - - let mut instance = Instance { - name: Some(name.to_string()), - r#type: Some(r#type.to_string()), - port: Some(port.to_string()), - created_at: Some(Utc::now()), - version: None, - installed_extensions: vec![], - enabled_extensions: vec![], - databases: vec![], - }; - - let stacks: Stacks = stacks::define_stacks(); - - for stack in stacks.stacks { - if stack.name.to_lowercase() == r#type.to_lowercase() { - // populate fields of instance - instance.version = Some(stack.version); - - for install in stack.trunk_installs { - let installed_extension = InstalledExtension { - name: install.name, - version: install.version, - created_at: install.created_at, - }; - - instance.installed_extensions.push(installed_extension); - } - - for extension in stack.extensions { - let enabled_extension = EnabledExtension { - name: extension.name, - version: extension.version, - created_at: extension.created_at, - locations: vec![], - }; - - instance.enabled_extensions.push(enabled_extension); - } - } - } - - config.instances.push(instance); - - let _ = config.write(&Config::full_path(matches)); - - Ok(()) -} - -/* -#[cfg(test)] -mod tests { - use super::*; - use clap::{Arg, ArgAction, ArgMatches, Command}; - - fn cleanup(matches: &ArgMatches) { - let path = Config::full_path(matches); - let mut config = Config::new(matches, &path); - - let _ = &config.instances.pop(); // remove last instance created from test - let _ = &config.write(&Config::full_path(&matches)); - } - - #[test] - fn valid_execute_test() { - // with a valid stack type - let stack_type = String::from("standard"); - - let m = Command::new("create") - .arg( - Arg::new("type") - .short('t') - .long("type") - .action(ArgAction::Set) - .required(true) - .help("The name of a Tembo stack type to create an instance of"), - ) - .arg( - Arg::new("name") - .short('n') - .long("name") - .action(ArgAction::Set) - .required(true) - .help("The name you want to give your instance"), - ) - .arg( - Arg::new("port") - .short('p') - .long("port") - .action(ArgAction::Set) - .required(true) - .help("The port number you want the instance to run on"), - ); - - let matches = &m.get_matches_from(vec![ - "create", - "-t", - &stack_type, - "-n", - "test_file", - "-p", - "5432", - ]); - let result = execute(&matches); - assert_eq!(result.is_ok(), true); - - cleanup(&matches); - } -} - */ diff --git a/tembo-cli/src/cmd/instance/list.rs b/tembo-cli/src/cmd/instance/list.rs deleted file mode 100644 index 5f489682b..000000000 --- a/tembo-cli/src/cmd/instance/list.rs +++ /dev/null @@ -1,35 +0,0 @@ -//! instance list command - -use crate::cli::config::Config; -use crate::Result; -use clap::{ArgMatches, Command}; -use simplelog::*; - -// example usage: tembo instance create -t oltp -n my_app_db -p 5432 -pub fn make_subcommand() -> Command { - Command::new("list").about("Command used to list local instances") -} - -pub fn execute(args: &ArgMatches) -> Result<()> { - let config = Config::new(args, &Config::full_path(args)); - - if config.instances.is_empty() { - info!("No instances have been configured"); - } else { - info!("Listing of configured instances"); - - for instance in &config.instances { - info!( - " {} - type: {}, port: {}", - instance.name.clone().unwrap(), - instance.r#type.clone().unwrap(), - instance.port.clone().unwrap() - ); - } - - info!("Start an instance using `tembo instance start -n `"); - info!("Coming soon: deploy an instance using `tembo instance deploy -n `"); - } - - Ok(()) -} diff --git a/tembo-cli/src/cmd/instance/start.rs b/tembo-cli/src/cmd/instance/start.rs deleted file mode 100644 index d02cdb996..000000000 --- a/tembo-cli/src/cmd/instance/start.rs +++ /dev/null @@ -1,45 +0,0 @@ -// instance start command -use crate::cli::config::Config; -use crate::cli::docker::Docker; -use crate::Result; -use anyhow::Context; -use clap::{Arg, ArgAction, ArgMatches, Command}; -use simplelog::*; - -// example usage: tembo instance start -n my_app_db -pub fn make_subcommand() -> Command { - Command::new("start") - .about("Command used to start local instances") - .arg( - Arg::new("name") - .short('n') - .long("name") - .action(ArgAction::Set) - .required(true) - .help("The name you want to use for this instance"), - ) -} - -pub fn execute(args: &ArgMatches) -> Result { - let config = Config::new(args, &Config::full_path(args)); - let name = args - .get_one::("name") - .with_context(|| "Name is missing.")?; - - if config.instances.is_empty() { - info!("No instances have been configured"); - } else { - info!("Finding config for {}", name); - - for instance in &config.instances { - if instance.name.clone().unwrap().to_lowercase() == name.to_lowercase() { - info!(" config has been found"); - info!(" starting via Docker"); - - Docker::start(name, instance)?; - } - } - } - - Ok(()) -} diff --git a/tembo-cli/src/cmd/mod.rs b/tembo-cli/src/cmd/mod.rs index 64fcd20ff..92f091132 100644 --- a/tembo-cli/src/cmd/mod.rs +++ b/tembo-cli/src/cmd/mod.rs @@ -1,9 +1,4 @@ pub mod apply; -pub mod auth; pub mod context; -pub mod database; pub mod delete; -pub mod extension; pub mod init; -pub mod instance; -pub mod schema; diff --git a/tembo-cli/src/cmd/schema.rs b/tembo-cli/src/cmd/schema.rs deleted file mode 100644 index fadcb0c52..000000000 --- a/tembo-cli/src/cmd/schema.rs +++ /dev/null @@ -1,21 +0,0 @@ -use crate::Result; -use clap::ArgMatches; -use simplelog::*; - -pub mod create; - -// handles all schema command calls -pub fn execute(args: &ArgMatches) -> Result<()> { - let res = match args.subcommand() { - Some(("create", sub_matches)) => create::execute(sub_matches), - _ => unreachable!(), - }; - - if let Err(err) = res { - error!("{err}"); - - std::process::exit(101); - } - - Ok(()) -} diff --git a/tembo-cli/src/cmd/schema/create.rs b/tembo-cli/src/cmd/schema/create.rs deleted file mode 100644 index a013c6ff7..000000000 --- a/tembo-cli/src/cmd/schema/create.rs +++ /dev/null @@ -1,133 +0,0 @@ -//! database create command - -use crate::cli::config::Config; -use crate::cli::instance::Instance; -use crate::cli::schema::Schema; -use crate::Result; -use anyhow::bail; -use chrono::Utc; -use clap::{Arg, ArgAction, ArgMatches, Command}; -use simplelog::*; -use spinners::{Spinner, Spinners}; -use std::process::Command as ShellCommand; - -// example usage: tembo schema create -n my_schema -d my_database -i my_instance -pub fn make_subcommand() -> Command { - Command::new("create") - .about("Command used to create databases on instances") - .arg( - Arg::new("name") - .short('n') - .long("name") - .action(ArgAction::Set) - .required(true) - .help("The name of the schema to create"), - ) - .arg( - Arg::new("database") - .short('d') - .long("database") - .action(ArgAction::Set) - .required(true) - .help("The name of the database to associate the schema with"), - ) - .arg( - Arg::new("instance") - .short('i') - .long("instance") - .action(ArgAction::Set) - .required(true) - .help("The name of the instance to create the schema on"), - ) -} - -pub fn execute(args: &ArgMatches) -> Result<()> { - let config = Config::new(args, &Config::full_path(args)); - let name_arg = args.try_get_one::("name").unwrap(); - let database_arg = args.try_get_one::("database").unwrap(); - let instance_arg = args.try_get_one::("instance").unwrap(); - - info!( - "trying to create schema '{}' for database '{}' on instance '{}'", - &name_arg.unwrap(), - &database_arg.unwrap(), - &instance_arg.unwrap() - ); - - if config.instances.is_empty() { - warn!("- No instances have been configured"); - } else { - let instance = Instance::find(args, instance_arg.unwrap())?; - create_schema(instance, name_arg.unwrap(), args)?; - } - - Ok(()) -} - -fn create_schema(instance: Instance, name: &str, args: &ArgMatches) -> Result<()> { - instance.start(); - - let mut sp = Spinner::new(Spinners::Dots12, "Creating schema".into()); - - // psql -h localhost -U postgres -c 'create schema if not exists test;' - let mut command = String::from("psql -h localhost -U postgres -p "); - command.push_str(&instance.port.clone().unwrap()); - command.push_str(" -c 'create schema if not exists "); - command.push_str(name); - command.push_str(";'"); - - let output = ShellCommand::new("sh") - .arg("-c") - .arg(&command) - .output() - .expect("failed to execute process"); - - sp.stop_with_newline(); - - let stderr = String::from_utf8(output.stderr).unwrap(); - - if !stderr.is_empty() { - bail!("There was an issue creating the schema: {stderr}"); - } else { - info!("schema created"); - - let _ = persist_config(args, instance); - - Ok(()) - } -} - -fn persist_config(args: &ArgMatches, target_instance: Instance) -> Result<()> { - let mut config = Config::new(args, &Config::full_path(args)); - let name_arg = args.try_get_one::("name"); - let database_arg = args.try_get_one::("database"); - - // loop through instances - for instance in config.instances.iter_mut() { - if instance.name.clone().unwrap().to_lowercase() - == target_instance.name.clone().unwrap().to_lowercase() - { - // loop through instance's databases - for database in instance.databases.iter_mut() { - if database.name.clone().to_lowercase() - == database_arg.clone().unwrap().unwrap().to_lowercase() - { - let schema = Schema { - name: name_arg.clone().unwrap().unwrap().to_string(), - created_at: Utc::now(), - }; - - database.schemas.push(schema.clone()); - } - } - } - } - - match &config.write(&Config::full_path(args)) { - Ok(_) => Ok(()), - Err(e) => { - error!("there was an error: {}", e); - bail!("there was an error writing the config: {e}") - } - } -} diff --git a/tembo-cli/src/main.rs b/tembo-cli/src/main.rs index 2b577bf02..998a9ae71 100644 --- a/tembo-cli/src/main.rs +++ b/tembo-cli/src/main.rs @@ -53,11 +53,6 @@ fn main() { Some(("context", sub_matches)) => cmd::context::execute(sub_matches), Some(("apply", sub_matches)) => cmd::apply::execute(sub_matches), Some(("delete", sub_matches)) => cmd::delete::execute(sub_matches), - Some(("instance", sub_matches)) => cmd::instance::execute(sub_matches), - Some(("db", sub_matches)) => cmd::database::execute(sub_matches), - Some(("schema", sub_matches)) => cmd::schema::execute(sub_matches), - Some(("extension", sub_matches)) => cmd::extension::execute(sub_matches), - Some(("auth", sub_matches)) => cmd::auth::execute(sub_matches), Some(("completions", sub_matches)) => (|| { let shell = sub_matches .get_one::("shell") @@ -97,41 +92,12 @@ fn create_clap_command() -> Command { .subcommand(cmd::init::make_subcommand()) .subcommand(cmd::apply::make_subcommand()) .subcommand(cmd::delete::make_subcommand()) - .subcommand( - Command::new("instance") - .about("Commands used to manage local and cloud instances") - .subcommand(cmd::instance::create::make_subcommand()) - .subcommand(cmd::instance::list::make_subcommand()) - .subcommand(cmd::instance::start::make_subcommand()), - ) .subcommand( Command::new("context") .about("Commands used to list/get/set context") .subcommand(cmd::context::list::make_subcommand()) .subcommand(cmd::context::set::make_subcommand()), ) - .subcommand( - Command::new("auth") - .about("Commands used to manage authentication") - .subcommand(cmd::auth::login::make_subcommand()) - .subcommand(cmd::auth::info::make_subcommand()), - ) - .subcommand( - Command::new("db") - .about("Commands used to manage local and cloud databases") - .subcommand(cmd::database::create::make_subcommand()), - ) - .subcommand( - Command::new("schema") - .about("Commands used to manage local and cloud schemas") - .subcommand(cmd::schema::create::make_subcommand()), - ) - .subcommand( - Command::new("extension") - .about("Commands used to manage local and cloud extensions") - .subcommand(cmd::extension::list::make_subcommand()) - .subcommand(cmd::extension::install::make_subcommand()), - ) .subcommand( Command::new("completions") .about("Generate shell completions for your shell to stdout") diff --git a/tembo-cli/tembo/postgresql.conf b/tembo-cli/tembo/postgresql.conf deleted file mode 100644 index 128ef1aa8..000000000 --- a/tembo-cli/tembo/postgresql.conf +++ /dev/null @@ -1 +0,0 @@ -listen_addresses = '*' diff --git a/tembo-cli/tembo/stacks.yaml b/tembo-cli/tembo/stacks.yaml deleted file mode 100644 index ad9eb0490..000000000 --- a/tembo-cli/tembo/stacks.yaml +++ /dev/null @@ -1,33 +0,0 @@ -stacks: - - name: Standard - description: A balanced Postgres instance optimized for OLTP workloads. - version: 0.1.0 - trunk_installs: - - name: pg_stat_statements - version: 1.10.0 - extensions: - - name: pg_stat_statements - locations: - - database: postgres - schema: public - enabled: true - version: 1.10.0 - - name: Data-Warehouse - description: A Postgres instance equipped with configuration and extensions for data warehouses. - version: 0.1.0 - trunk_installs: - - name: pg_stat_statements - version: 1.10.0 - - name: pg_later - version: 0.0.8 - extensions: - - name: pg_stat_statements - locations: - - database: postgres - enabled: true - version: 1.10.0 - - name: pg_later - locations: - - database: postgres - enabled: true - version: 0.0.8