diff --git a/lib/backend-api/Cargo.toml b/lib/backend-api/Cargo.toml index 8d4120c1a7b..74faa5413bd 100644 --- a/lib/backend-api/Cargo.toml +++ b/lib/backend-api/Cargo.toml @@ -25,7 +25,7 @@ serde = { version = "1", features = ["derive"] } time = { version = "0.3", features = ["formatting", "parsing"] } tokio = { version = "1.23.0" } serde_json = "1" -url = "2" +url = { version = "2", features = ["serde"] } futures = "0.3" tracing = "0.1" cynic = { version = "3.7.2", features = ["http-reqwest"] } diff --git a/lib/backend-api/schema.graphql b/lib/backend-api/schema.graphql index 281826450c7..e972ff48fa5 100644 --- a/lib/backend-api/schema.graphql +++ b/lib/backend-api/schema.graphql @@ -949,6 +949,7 @@ type DeployApp implements Node & Owner { aliases(offset: Int, before: String, after: String, first: Int, last: Int): AppAliasConnection! secrets(offset: Int, before: String, after: String, first: Int, last: Int): SecretConnection! usageMetrics(forRange: MetricRange!, variant: MetricType!): [UsageMetric]! + s3Url: URL deleted: Boolean! favicon: URL screenshot(viewportSize: AppScreenshotViewportSize, appearance: AppScreenshotAppearance): URL diff --git a/lib/backend-api/src/query.rs b/lib/backend-api/src/query.rs index 3ee3852852f..4f784394ecb 100644 --- a/lib/backend-api/src/query.rs +++ b/lib/backend-api/src/query.rs @@ -184,6 +184,90 @@ pub async fn get_all_app_secrets_filtered( Ok(all_secrets) } +/// Retrieve volumes for an app. +pub async fn get_app_volumes( + client: &WasmerClient, + owner: impl Into, + name: impl Into, +) -> Result, anyhow::Error> { + let vars = types::GetAppVolumesVars { + owner: owner.into(), + name: name.into(), + }; + let res = client + .run_graphql_strict(types::GetAppVolumes::build(vars)) + .await?; + let volumes = res + .get_deploy_app + .context("app not found")? + .active_version + .volumes + .unwrap_or_default() + .into_iter() + .flatten() + .collect(); + Ok(volumes) +} + +/// S3 credentials for an app. +/// +/// Retrieved with [`get_app_s3_credentials`]. +#[derive(Clone)] +pub struct AppS3Credentials { + pub domain: String, + pub access_key: String, + pub secret_key: String, +} + +/// Load the S3 credentials. +/// +/// S3 can be used to get access to an apps volumes. +pub async fn get_app_s3_credentials( + client: &WasmerClient, + app_id: impl Into, +) -> Result { + const ACCESS_KEY_NAME: &str = "WASMER_APP_S3_ACCESS_KEY"; + const SECRET_KEY_NAME: &str = "WASMER_APP_S3_SECRET_KEY"; + + let app_id = app_id.into(); + + // Firt load the app to get the s3 url. + let app = get_app_by_id(client, app_id.clone()).await?; + let url = app.s3_url.context("app has no volumes")?; + + // Load the secrets. + let secrets = + get_all_app_secrets_filtered(client, app_id, [ACCESS_KEY_NAME, SECRET_KEY_NAME]).await?; + + let access_key_id = secrets + .iter() + .find(|s| s.name == ACCESS_KEY_NAME) + .context("missing access key")? + .id + .clone(); + + let secret_key_id = secrets + .iter() + .find(|s| s.name == SECRET_KEY_NAME) + .context("missing secret key")? + .id + .clone(); + + let access_key = get_app_secret_value_by_id(client, access_key_id.into_inner()) + .await? + .with_context(|| format!("No value found for secret with name '{}'", ACCESS_KEY_NAME))?; + + let secret_key = get_app_secret_value_by_id(client, secret_key_id.into_inner()) + .await? + .with_context(|| format!("No value found for secret with name '{}'", SECRET_KEY_NAME))?; + + Ok(AppS3Credentials { + domain: url.0, + access_key, + secret_key, + }) +} + /// Load all available regions. /// /// Will paginate through all versions and return them in a single list. diff --git a/lib/backend-api/src/types.rs b/lib/backend-api/src/types.rs index dcec89d7fae..8e669df0176 100644 --- a/lib/backend-api/src/types.rs +++ b/lib/backend-api/src/types.rs @@ -775,6 +775,38 @@ mod queries { pub get_deploy_app_version: Option, } + #[derive(cynic::QueryVariables, Debug)] + pub(crate) struct GetAppVolumesVars { + pub name: String, + pub owner: String, + } + + #[derive(cynic::QueryFragment, Debug)] + #[cynic(graphql_type = "Query", variables = "GetAppVolumesVars")] + pub(crate) struct GetAppVolumes { + #[arguments(owner: $owner, name: $name)] + pub get_deploy_app: Option, + } + + #[derive(cynic::QueryFragment, Debug)] + #[cynic(graphql_type = "DeployApp")] + pub(crate) struct AppVolumes { + pub active_version: AppVersionVolumes, + } + + #[derive(cynic::QueryFragment, Debug)] + #[cynic(graphql_type = "DeployAppVersion")] + pub(crate) struct AppVersionVolumes { + pub volumes: Option>>, + } + + #[derive(serde::Serialize, cynic::QueryFragment, Debug)] + pub struct AppVersionVolume { + pub name: String, + pub size: Option, + pub used_size: Option, + } + #[derive(cynic::QueryFragment, Debug)] pub struct RegisterDomainPayload { pub success: bool, @@ -871,6 +903,7 @@ mod queries { pub permalink: String, pub deleted: bool, pub aliases: AppAliasConnection, + pub s3_url: Option, } #[derive(cynic::QueryFragment, Serialize, Debug, Clone)] @@ -1991,6 +2024,10 @@ mod queries { pub purge_cache_for_app_version: Option, } + #[derive(cynic::Scalar, Debug, Clone)] + #[cynic(graphql_type = "URL")] + pub struct Url(pub String); + #[derive(cynic::Scalar, Debug, Clone)] pub struct BigInt(pub i64); diff --git a/lib/cli/src/commands/app/mod.rs b/lib/cli/src/commands/app/mod.rs index cc3ad250055..7a461a06c83 100644 --- a/lib/cli/src/commands/app/mod.rs +++ b/lib/cli/src/commands/app/mod.rs @@ -11,6 +11,7 @@ pub mod purge_cache; pub mod regions; pub mod secrets; pub mod version; +pub mod volumes; mod util; @@ -33,6 +34,8 @@ pub enum CmdApp { Secret(secrets::CmdAppSecrets), #[clap(subcommand, alias = "regions")] Region(regions::CmdAppRegions), + #[clap(subcommand, alias = "volumes")] + Volume(volumes::CmdAppVolumes), } #[async_trait::async_trait] @@ -61,6 +64,7 @@ impl AsyncCliCommand for CmdApp { Self::PurgeCache(cmd) => cmd.run_async().await, Self::Secret(cmd) => cmd.run_async().await, Self::Region(cmd) => cmd.run_async().await, + Self::Volume(cmd) => cmd.run_async().await, } } } diff --git a/lib/cli/src/commands/app/volumes/list.rs b/lib/cli/src/commands/app/volumes/list.rs new file mode 100644 index 00000000000..b3601209e0b --- /dev/null +++ b/lib/cli/src/commands/app/volumes/list.rs @@ -0,0 +1,38 @@ +//! List volumes tied to an edge app. + +use super::super::util::AppIdentOpts; +use crate::{commands::AsyncCliCommand, config::WasmerEnv, opts::ListFormatOpts}; + +/// List the volumes of an app. +#[derive(clap::Parser, Debug)] +pub struct CmdAppVolumesList { + #[clap(flatten)] + fmt: ListFormatOpts, + + #[clap(flatten)] + env: WasmerEnv, + + #[clap(flatten)] + ident: AppIdentOpts, +} + +#[async_trait::async_trait] +impl AsyncCliCommand for CmdAppVolumesList { + type Output = (); + + async fn run_async(self) -> Result<(), anyhow::Error> { + let client = self.env.client()?; + + let (_ident, app) = self.ident.load_app(&client).await?; + let volumes = + wasmer_api::query::get_app_volumes(&client, &app.owner.global_name, &app.name).await?; + + if volumes.is_empty() { + eprintln!("App {} has no volumes!", app.name); + } else { + println!("{}", self.fmt.format.render(volumes.as_slice())); + } + + Ok(()) + } +} diff --git a/lib/cli/src/commands/app/volumes/mod.rs b/lib/cli/src/commands/app/volumes/mod.rs new file mode 100644 index 00000000000..d41414da768 --- /dev/null +++ b/lib/cli/src/commands/app/volumes/mod.rs @@ -0,0 +1,29 @@ +use crate::commands::AsyncCliCommand; + +pub mod list; +pub mod s3_credentials; + +/// App volume management. +#[derive(Debug, clap::Parser)] +pub enum CmdAppVolumes { + S3Credentials(s3_credentials::CmdAppS3Credentials), + List(list::CmdAppVolumesList), +} + +#[async_trait::async_trait] +impl AsyncCliCommand for CmdAppVolumes { + type Output = (); + + async fn run_async(self) -> Result { + match self { + Self::S3Credentials(c) => { + c.run_async().await?; + Ok(()) + } + Self::List(c) => { + c.run_async().await?; + Ok(()) + } + } + } +} diff --git a/lib/cli/src/commands/app/volumes/s3_credentials.rs b/lib/cli/src/commands/app/volumes/s3_credentials.rs new file mode 100644 index 00000000000..0949902ddd9 --- /dev/null +++ b/lib/cli/src/commands/app/volumes/s3_credentials.rs @@ -0,0 +1,42 @@ +//! Get information about an edge app. + +use wasmer_api::types::DeployApp; + +use super::super::util::AppIdentOpts; + +use crate::{commands::AsyncCliCommand, config::WasmerEnv, opts::ItemFormatOpts}; + +/// Retrieve S3 access credentials for the volumes of an app. +#[derive(clap::Parser, Debug)] +pub struct CmdAppS3Credentials { + #[clap(flatten)] + pub env: WasmerEnv, + + #[clap(flatten)] + pub fmt: ItemFormatOpts, + + #[clap(flatten)] + pub ident: AppIdentOpts, +} + +#[async_trait::async_trait] +impl AsyncCliCommand for CmdAppS3Credentials { + type Output = DeployApp; + + async fn run_async(self) -> Result { + let client = self.env.client()?; + let (_ident, app) = self.ident.load_app(&client).await?; + + let creds = + wasmer_api::query::get_app_s3_credentials(&client, app.id.clone().into_inner()).await?; + + println!("S3 credentials for app {}:\n", app.name); + println!(" S3 URL: https://{}", creds.domain); + println!(" Access key: {}", creds.access_key); + println!(" Secret key: {}", creds.secret_key); + println!(); + println!("Consult the app volumes documentation for more information."); + + Ok(app) + } +} diff --git a/lib/cli/src/types.rs b/lib/cli/src/types.rs index 68897ca7468..27fc3349ff3 100644 --- a/lib/cli/src/types.rs +++ b/lib/cli/src/types.rs @@ -156,3 +156,36 @@ impl CliRender for DeployAppVersion { table.to_string() } } + +impl CliRender for wasmer_api::types::AppVersionVolume { + fn render_item_table(&self) -> String { + let mut table = Table::new(); + table.add_rows([ + vec!["Name".to_string(), self.name.clone()], + vec![ + "Used size".to_string(), + format_disk_size_opt(self.used_size), + ], + ]); + table.to_string() + } + + fn render_list_table(items: &[Self]) -> String { + let mut table = Table::new(); + table.set_header(vec!["Name".to_string(), "Used size".to_string()]); + table.add_rows( + items + .iter() + .map(|vol| vec![vol.name.clone(), format_disk_size_opt(vol.used_size)]), + ); + table.to_string() + } +} + +fn format_disk_size_opt(value: Option) -> String { + if let Some(v) = value { + format!("{}Mb", v) + } else { + "n/a".to_string() + } +}