use comfy_table::Table;
use wasmer_backend_api::WasmerClient;
use crate::commands::AsyncCliCommand;
use crate::utils::render::CliRender;
pub mod credentials;
pub mod enable_s3;
pub mod list;
#[derive(Debug, clap::Parser)]
pub enum CmdAppVolumes {
Credentials(credentials::CmdAppVolumesCredentials),
List(list::CmdAppVolumesList),
RotateSecrets(credentials::rotate_secrets::CmdAppVolumesRotateSecrets),
EnableS3(enable_s3::CmdAppVolumesEnableS3),
}
#[async_trait::async_trait]
impl AsyncCliCommand for CmdAppVolumes {
type Output = ();
async fn run_async(self) -> Result<Self::Output, anyhow::Error> {
match self {
Self::Credentials(c) => {
c.run_async().await?;
Ok(())
}
Self::RotateSecrets(c) => {
c.run_async().await?;
Ok(())
}
Self::List(c) => {
c.run_async().await?;
Ok(())
}
Self::EnableS3(c) => {
c.run_async().await?;
Ok(())
}
}
}
}
pub(crate) struct Volume {
pub id: wasmer_backend_api::types::Id,
pub volume_id: String,
pub mount_path: String,
pub s3_enabled: bool,
pub s3: Option<wasmer_backend_api::types::S3>,
pub name: Option<String>,
pub used_size: Option<i64>,
}
impl Volume {
pub fn label(&self) -> String {
match &self.name {
Some(name) => format!("{name} ({})", self.mount_path),
None => self.mount_path.clone(),
}
}
}
pub(crate) async fn list_volumes(
client: &WasmerClient,
owner: &str,
app_name: &str,
) -> Result<Vec<Volume>, anyhow::Error> {
let persistent =
wasmer_backend_api::query::get_deploy_app_volumes(client, owner, app_name).await?;
let version = wasmer_backend_api::query::get_app_volumes(client, owner, app_name)
.await
.unwrap_or_default();
let mut meta_by_mount: std::collections::HashMap<String, (String, Option<i64>)> =
std::collections::HashMap::new();
for descriptor in version {
let used_size = descriptor.used_size.map(|b| b.0);
for mount in descriptor.mount_paths {
meta_by_mount
.entry(mount.path.trim_end_matches('/').to_string())
.or_insert_with(|| (descriptor.name.clone(), used_size));
}
}
Ok(persistent
.into_iter()
.map(|volume| {
let meta = meta_by_mount.get(volume.mount_path.trim_end_matches('/'));
Volume {
id: volume.id,
volume_id: volume.volume_id,
mount_path: volume.mount_path,
s3_enabled: volume.s3_enabled,
s3: volume.s3,
name: meta.map(|(name, _)| name.clone()),
used_size: meta.and_then(|(_, used)| *used),
}
})
.collect())
}
pub(crate) fn select_volume_by_selector<'a>(
app_name: &str,
volumes: &'a [Volume],
selector: &str,
) -> Result<&'a Volume, anyhow::Error> {
if let Some(volume) = volumes.iter().find(|v| v.volume_id == selector) {
return Ok(volume);
}
let matched: Vec<&Volume> = volumes
.iter()
.filter(|v| v.name.as_deref() == Some(selector) || v.mount_path == selector)
.collect();
match matched.as_slice() {
[] => {
let available = volumes
.iter()
.map(Volume::label)
.collect::<Vec<_>>()
.join(", ");
if available.is_empty() {
anyhow::bail!("App {app_name} has no volumes.");
}
anyhow::bail!(
"App {app_name} has no volume matching '{selector}'. Available volumes: {available}"
);
}
[volume] => Ok(volume),
multiple => {
let colliding = multiple
.iter()
.map(|v| format!("{} [id {}]", v.label(), v.volume_id))
.collect::<Vec<_>>()
.join(", ");
anyhow::bail!(
"'{selector}' matches multiple volumes of app {app_name}: {colliding}. \
Pass the volume id to select one."
)
}
}
}
#[derive(serde::Serialize)]
pub(crate) struct VolumeListItem {
pub name: Option<String>,
pub mount_path: String,
pub volume_id: String,
pub used_size: Option<i64>,
pub s3_enabled: bool,
}
impl From<&Volume> for VolumeListItem {
fn from(volume: &Volume) -> Self {
Self {
name: volume.name.clone(),
mount_path: volume.mount_path.clone(),
volume_id: volume.volume_id.clone(),
used_size: volume.used_size,
s3_enabled: volume.s3_enabled,
}
}
}
impl VolumeListItem {
fn row(&self) -> Vec<String> {
vec![
self.name.clone().unwrap_or_else(|| "-".to_string()),
self.mount_path.clone(),
crate::types::format_disk_size_opt(
self.used_size.map(wasmer_backend_api::types::BigInt),
),
if self.s3_enabled {
"enabled"
} else {
"disabled"
}
.to_string(),
]
}
}
impl CliRender for VolumeListItem {
fn render_item_table(&self) -> String {
let row = self.row();
let mut table = Table::new();
table.add_rows([
vec!["Name".to_string(), row[0].clone()],
vec!["Mount path".to_string(), row[1].clone()],
vec!["Used size".to_string(), row[2].clone()],
vec!["S3".to_string(), row[3].clone()],
]);
table.to_string()
}
fn render_list_table(items: &[Self]) -> String {
let mut table = Table::new();
table.set_header(vec!["Name", "Mount path", "Used size", "S3"]);
table.add_rows(items.iter().map(|item| item.row()));
table.to_string()
}
}