Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

CLI: Volumes commands #5013

Merged
merged 3 commits into from
Aug 13, 2024
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion lib/backend-api/Cargo.toml
Original file line number Diff line number Diff line change
@@ -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"] }
1 change: 1 addition & 0 deletions lib/backend-api/schema.graphql
Original file line number Diff line number Diff line change
@@ -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
84 changes: 84 additions & 0 deletions lib/backend-api/src/query.rs
Original file line number Diff line number Diff line change
@@ -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<String>,
name: impl Into<String>,
) -> Result<Vec<types::AppVersionVolume>, 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<String>,
) -> Result<AppS3Credentials, anyhow::Error> {
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.
37 changes: 37 additions & 0 deletions lib/backend-api/src/types.rs
Original file line number Diff line number Diff line change
@@ -775,6 +775,38 @@ mod queries {
pub get_deploy_app_version: Option<DeployAppVersion>,
}

#[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<AppVolumes>,
}

#[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<Vec<Option<AppVersionVolume>>>,
}

#[derive(serde::Serialize, cynic::QueryFragment, Debug)]
pub struct AppVersionVolume {
pub name: String,
pub size: Option<i32>,
pub used_size: Option<i32>,
}

#[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<Url>,
}

#[derive(cynic::QueryFragment, Serialize, Debug, Clone)]
@@ -1991,6 +2024,10 @@ mod queries {
pub purge_cache_for_app_version: Option<PurgeCacheForAppVersionPayload>,
}

#[derive(cynic::Scalar, Debug, Clone)]
#[cynic(graphql_type = "URL")]
pub struct Url(pub String);

#[derive(cynic::Scalar, Debug, Clone)]
pub struct BigInt(pub i64);

4 changes: 4 additions & 0 deletions lib/cli/src/commands/app/mod.rs
Original file line number Diff line number Diff line change
@@ -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,
}
}
}
38 changes: 38 additions & 0 deletions lib/cli/src/commands/app/volumes/list.rs
Original file line number Diff line number Diff line change
@@ -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(())
}
}
29 changes: 29 additions & 0 deletions lib/cli/src/commands/app/volumes/mod.rs
Original file line number Diff line number Diff line change
@@ -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<Self::Output, anyhow::Error> {
match self {
Self::S3Credentials(c) => {
c.run_async().await?;
Ok(())
}
Self::List(c) => {
c.run_async().await?;
Ok(())
}
}
}
}
42 changes: 42 additions & 0 deletions lib/cli/src/commands/app/volumes/s3_credentials.rs
Original file line number Diff line number Diff line change
@@ -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<DeployApp, anyhow::Error> {
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)
}
}
33 changes: 33 additions & 0 deletions lib/cli/src/types.rs
Original file line number Diff line number Diff line change
@@ -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<i32>) -> String {
if let Some(v) = value {
format!("{}Mb", v)
} else {
"n/a".to_string()
}
}