diff --git a/Cargo.lock b/Cargo.lock index 6b5916d..b00aa1b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -442,7 +442,7 @@ dependencies = [ [[package]] name = "cloud-openapi" version = "0.1.0" -source = "git+https://github.com/fermyon/cloud-openapi?rev=dbf4657ef4e70433aea17210a539202443e2ce96#dbf4657ef4e70433aea17210a539202443e2ce96" +source = "git+https://github.com/fermyon/cloud-openapi?rev=c37c0f28c06a206ebe05a811c2b886290a5bab40#c37c0f28c06a206ebe05a811c2b886290a5bab40" dependencies = [ "reqwest", "serde", diff --git a/Cargo.toml b/Cargo.toml index 14cacc8..b2bf6c7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,12 +14,12 @@ edition = "2021" [dependencies] anyhow = "1.0" bindle = { git = "https://github.com/fermyon/bindle", tag = "v0.8.2", default-features = false, features = [ - "client", + "client", ] } chrono = "0.4" clap = { version = "3.2.24", features = ["derive", "env"] } cloud = { path = "crates/cloud" } -cloud-openapi = { git = "https://github.com/fermyon/cloud-openapi", rev = "dbf4657ef4e70433aea17210a539202443e2ce96" } +cloud-openapi = { workspace = true } dirs = "5.0" dialoguer = "0.10" tokio = { version = "1.23", features = ["full"] } @@ -48,6 +48,7 @@ openssl = { version = "0.10" } [workspace.dependencies] tracing = { version = "0.1", features = ["log"] } +cloud-openapi = { git = "https://github.com/fermyon/cloud-openapi", rev = "c37c0f28c06a206ebe05a811c2b886290a5bab40" } [build-dependencies] vergen = { version = "^8.2.1", default-features = false, features = [ diff --git a/crates/cloud/Cargo.toml b/crates/cloud/Cargo.toml index b0d3193..d15f1ea 100644 --- a/crates/cloud/Cargo.toml +++ b/crates/cloud/Cargo.toml @@ -6,11 +6,11 @@ edition = { workspace = true } [dependencies] anyhow = "1.0" -cloud-openapi = { git = "https://github.com/fermyon/cloud-openapi", rev = "dbf4657ef4e70433aea17210a539202443e2ce96" } +cloud-openapi = { workspace = true } mime_guess = { version = "2.0" } reqwest = { version = "0.11", features = ["stream"] } semver = "1.0" -serde = {version = "1.0", features = ["derive"]} +serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" tokio = { version = "1.17", features = ["full"] } tokio-util = { version = "0.7.3", features = ["codec"] } diff --git a/crates/cloud/src/client.rs b/crates/cloud/src/client.rs index 3924e84..d3f085a 100644 --- a/crates/cloud/src/client.rs +++ b/crates/cloud/src/client.rs @@ -13,7 +13,8 @@ use cloud_openapi::{ key_value_pairs_api::api_key_value_pairs_post, revisions_api::{api_revisions_get, api_revisions_post}, sql_databases_api::{ - api_sql_databases_delete, api_sql_databases_get, api_sql_databases_post, + api_sql_databases_create_post, api_sql_databases_delete, + api_sql_databases_execute_post, api_sql_databases_get, }, variable_pairs_api::{ api_variable_pairs_delete, api_variable_pairs_get, api_variable_pairs_post, @@ -24,9 +25,9 @@ use cloud_openapi::{ AppItemPage, ChannelItem, ChannelItemPage, ChannelRevisionSelectionStrategy, CreateAppCommand, CreateChannelCommand, CreateDeviceCodeCommand, CreateKeyValuePairCommand, CreateSqlDatabaseCommand, CreateVariablePairCommand, Database, DeleteSqlDatabaseCommand, - DeleteVariablePairCommand, DeviceCodeItem, EnvironmentVariableItem, GetChannelLogsVm, - GetSqlDatabasesQuery, GetVariablesQuery, RefreshTokenCommand, RegisterRevisionCommand, - RevisionItemPage, TokenInfo, + DeleteVariablePairCommand, DeviceCodeItem, EnvironmentVariableItem, + ExecuteSqlStatementCommand, GetChannelLogsVm, GetSqlDatabasesQuery, GetVariablesQuery, + RefreshTokenCommand, RegisterRevisionCommand, RevisionItemPage, TokenInfo, }, }; use reqwest::header; @@ -386,7 +387,7 @@ impl Client { } pub async fn create_database(&self, app_id: Option, name: String) -> anyhow::Result<()> { - api_sql_databases_post( + api_sql_databases_create_post( &self.configuration, CreateSqlDatabaseCommand { name, @@ -398,6 +399,21 @@ impl Client { .map_err(format_response_error) } + pub async fn execute_sql(&self, database: String, statement: String) -> anyhow::Result<()> { + api_sql_databases_execute_post( + &self.configuration, + ExecuteSqlStatementCommand { + database, + statement, + default: false, + }, + None, + ) + .await + .map_err(format_response_error)?; + Ok(()) + } + pub async fn delete_database(&self, name: String) -> anyhow::Result<()> { api_sql_databases_delete(&self.configuration, DeleteSqlDatabaseCommand { name }, None) .await diff --git a/src/commands/deploy.rs b/src/commands/deploy.rs index 678948f..6c6b116 100644 --- a/src/commands/deploy.rs +++ b/src/commands/deploy.rs @@ -33,7 +33,10 @@ use std::{ use url::Url; use uuid::Uuid; -use crate::commands::variables::{get_variables, set_variables, Variable}; +use crate::commands::{ + get_app_id_cloud, + variables::{get_variables, set_variables, Variable}, +}; use crate::{ commands::login::{LoginCommand, LoginConnection}, @@ -183,7 +186,7 @@ impl DeployCommand { // Create or update app // TODO: this process involves many calls to Cloud. Should be able to update the channel // via only `add_revision` if bindle naming schema is updated so bindles can be deterministically ordered by Cloud. - let channel_id = match self.get_app_id_cloud(&client, name.clone()).await { + let channel_id = match get_app_id_cloud(&client, &name).await { Ok(app_id) => { if uses_default_db(&cfg) { create_default_database_if_does_not_exist(&name, app_id, &client).await?; @@ -396,15 +399,6 @@ impl DeployCommand { Err(anyhow!("The application requires values for the following variable(s) which have not been set: {list_text}. Use the --variable flag to provide values.")) } - async fn get_app_id_cloud(&self, cloud_client: &CloudClient, name: String) -> Result { - let apps_vm = CloudClient::list_apps(cloud_client).await?; - let app = apps_vm.items.iter().find(|&x| x.name == name.clone()); - match app { - Some(a) => Ok(a.id), - None => bail!("No app with name: {}", name), - } - } - async fn try_get_app_id_cloud( &self, cloud_client: &CloudClient, @@ -845,15 +839,6 @@ pub async fn login_connection(deployment_env_id: Option<&str>) -> Result Result { - let apps_vm = CloudClient::list_apps(cloud_client).await?; - let app = apps_vm.items.iter().find(|&x| x.name == name); - match app { - Some(a) => Ok(a.id), - None => bail!("No app with name: {}", name), - } -} - // TODO: unify with login pub fn config_file_path(deployment_env_id: Option<&str>) -> Result { let root = dirs::config_dir() diff --git a/src/commands/mod.rs b/src/commands/mod.rs index 8e7f942..00e2b1b 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -4,8 +4,9 @@ pub mod sqlite; pub mod variables; use crate::commands::deploy::login_connection; -use anyhow::Result; +use anyhow::{bail, Result}; use cloud::client::{Client as CloudClient, ConnectionConfig}; +use uuid::Uuid; pub(crate) async fn create_cloud_client(deployment_env_id: Option<&str>) -> Result { let login_connection = login_connection(deployment_env_id).await?; @@ -16,3 +17,12 @@ pub(crate) async fn create_cloud_client(deployment_env_id: Option<&str>) -> Resu }; Ok(CloudClient::new(connection_config)) } + +pub(crate) async fn get_app_id_cloud(cloud_client: &CloudClient, name: &str) -> Result { + let apps_vm = CloudClient::list_apps(cloud_client).await?; + let app = apps_vm.items.iter().find(|&x| x.name == name); + match app { + Some(a) => Ok(a.id), + None => bail!("No app with name: {}", name), + } +} diff --git a/src/commands/sqlite.rs b/src/commands/sqlite.rs index 52e159e..ef3dfb7 100644 --- a/src/commands/sqlite.rs +++ b/src/commands/sqlite.rs @@ -13,6 +13,8 @@ use crate::opts::*; pub enum SqliteCommand { /// Delete a SQLite database Delete(DeleteCommand), + /// Execute SQL against a SQLite database + Execute(ExecuteCommand), /// List all SQLite databases of a user List(ListCommand), } @@ -30,6 +32,27 @@ pub struct DeleteCommand { common: CommonArgs, } +#[derive(Parser, Debug)] +pub struct ExecuteCommand { + /// Name of database to execute against + #[clap(value_parser = clap::builder::ValueParser::new(disallow_empty))] + name: String, + + ///Statement to execute + #[clap(value_parser = clap::builder::ValueParser::new(disallow_empty))] + statement: String, + + #[clap(flatten)] + common: CommonArgs, +} + +fn disallow_empty(statement: &str) -> anyhow::Result { + if statement.trim().is_empty() { + anyhow::bail!("cannot be empty"); + } + return Ok(statement.trim().to_owned()); +} + #[derive(Parser, Debug)] pub struct ListCommand { #[clap(flatten)] @@ -67,6 +90,18 @@ impl SqliteCommand { println!("Database \"{}\" deleted", cmd.name); } } + Self::Execute(cmd) => { + let client = create_cloud_client(cmd.common.deployment_env_id.as_deref()).await?; + let list = CloudClient::get_databases(&client, None) + .await + .context("Problem fetching databases")?; + if !list.iter().any(|d| d.name == cmd.name) { + anyhow::bail!("No database found with name \"{}\"", cmd.name); + } + CloudClient::execute_sql(&client, cmd.name, cmd.statement) + .await + .context("Problem executing SQL")?; + } Self::List(cmd) => { let client = create_cloud_client(cmd.common.deployment_env_id.as_deref()).await?; let list = CloudClient::get_databases(&client, None) diff --git a/src/commands/variables.rs b/src/commands/variables.rs index eb8e6a2..a038a76 100644 --- a/src/commands/variables.rs +++ b/src/commands/variables.rs @@ -8,7 +8,7 @@ use uuid::Uuid; use crate::opts::*; -use crate::commands::{create_cloud_client, deploy::get_app_id_cloud}; +use crate::commands::{create_cloud_client, get_app_id_cloud}; #[derive(Deserialize)] pub(crate) struct Variable {