wasmer_cli/commands/app/volumes/
mod.rs1use comfy_table::Table;
2use wasmer_backend_api::WasmerClient;
3
4use crate::commands::AsyncCliCommand;
5use crate::utils::render::CliRender;
6
7pub mod credentials;
8pub mod enable_s3;
9pub mod list;
10
11#[derive(Debug, clap::Parser)]
13pub enum CmdAppVolumes {
14 Credentials(credentials::CmdAppVolumesCredentials),
15 List(list::CmdAppVolumesList),
16 RotateSecrets(credentials::rotate_secrets::CmdAppVolumesRotateSecrets),
17 EnableS3(enable_s3::CmdAppVolumesEnableS3),
18}
19
20#[async_trait::async_trait]
21impl AsyncCliCommand for CmdAppVolumes {
22 type Output = ();
23
24 async fn run_async(self) -> Result<Self::Output, anyhow::Error> {
25 match self {
26 Self::Credentials(c) => {
27 c.run_async().await?;
28 Ok(())
29 }
30 Self::RotateSecrets(c) => {
31 c.run_async().await?;
32 Ok(())
33 }
34 Self::List(c) => {
35 c.run_async().await?;
36 Ok(())
37 }
38 Self::EnableS3(c) => {
39 c.run_async().await?;
40 Ok(())
41 }
42 }
43 }
44}
45
46pub(crate) struct Volume {
55 pub id: wasmer_backend_api::types::Id,
56 pub volume_id: String,
57 pub mount_path: String,
58 pub s3_enabled: bool,
59 pub s3: Option<wasmer_backend_api::types::S3>,
60 pub name: Option<String>,
61 pub used_size: Option<i64>,
63}
64
65impl Volume {
66 pub fn label(&self) -> String {
68 match &self.name {
69 Some(name) => format!("{name} ({})", self.mount_path),
70 None => self.mount_path.clone(),
71 }
72 }
73}
74
75pub(crate) async fn list_volumes(
79 client: &WasmerClient,
80 owner: &str,
81 app_name: &str,
82) -> Result<Vec<Volume>, anyhow::Error> {
83 let persistent =
84 wasmer_backend_api::query::get_deploy_app_volumes(client, owner, app_name).await?;
85
86 let version = wasmer_backend_api::query::get_app_volumes(client, owner, app_name)
89 .await
90 .unwrap_or_default();
91
92 let mut meta_by_mount: std::collections::HashMap<String, (String, Option<i64>)> =
93 std::collections::HashMap::new();
94 for descriptor in version {
95 let used_size = descriptor.used_size.map(|b| b.0);
96 for mount in descriptor.mount_paths {
97 meta_by_mount
98 .entry(mount.path.trim_end_matches('/').to_string())
99 .or_insert_with(|| (descriptor.name.clone(), used_size));
100 }
101 }
102
103 Ok(persistent
104 .into_iter()
105 .map(|volume| {
106 let meta = meta_by_mount.get(volume.mount_path.trim_end_matches('/'));
107 Volume {
108 id: volume.id,
109 volume_id: volume.volume_id,
110 mount_path: volume.mount_path,
111 s3_enabled: volume.s3_enabled,
112 s3: volume.s3,
113 name: meta.map(|(name, _)| name.clone()),
114 used_size: meta.and_then(|(_, used)| *used),
115 }
116 })
117 .collect())
118}
119
120pub(crate) fn select_volume_by_selector<'a>(
129 app_name: &str,
130 volumes: &'a [Volume],
131 selector: &str,
132) -> Result<&'a Volume, anyhow::Error> {
133 if let Some(volume) = volumes.iter().find(|v| v.volume_id == selector) {
134 return Ok(volume);
135 }
136
137 let matched: Vec<&Volume> = volumes
138 .iter()
139 .filter(|v| v.name.as_deref() == Some(selector) || v.mount_path == selector)
140 .collect();
141
142 match matched.as_slice() {
143 [] => {
144 let available = volumes
145 .iter()
146 .map(Volume::label)
147 .collect::<Vec<_>>()
148 .join(", ");
149 if available.is_empty() {
150 anyhow::bail!("App {app_name} has no volumes.");
151 }
152 anyhow::bail!(
153 "App {app_name} has no volume matching '{selector}'. Available volumes: {available}"
154 );
155 }
156 [volume] => Ok(volume),
157 multiple => {
158 let colliding = multiple
159 .iter()
160 .map(|v| format!("{} [id {}]", v.label(), v.volume_id))
161 .collect::<Vec<_>>()
162 .join(", ");
163 anyhow::bail!(
164 "'{selector}' matches multiple volumes of app {app_name}: {colliding}. \
165 Pass the volume id to select one."
166 )
167 }
168 }
169}
170
171#[derive(serde::Serialize)]
173pub(crate) struct VolumeListItem {
174 pub name: Option<String>,
175 pub mount_path: String,
176 pub volume_id: String,
177 pub used_size: Option<i64>,
178 pub s3_enabled: bool,
179}
180
181impl From<&Volume> for VolumeListItem {
182 fn from(volume: &Volume) -> Self {
183 Self {
184 name: volume.name.clone(),
185 mount_path: volume.mount_path.clone(),
186 volume_id: volume.volume_id.clone(),
187 used_size: volume.used_size,
188 s3_enabled: volume.s3_enabled,
189 }
190 }
191}
192
193impl VolumeListItem {
194 fn row(&self) -> Vec<String> {
195 vec![
196 self.name.clone().unwrap_or_else(|| "-".to_string()),
197 self.mount_path.clone(),
198 crate::types::format_disk_size_opt(
199 self.used_size.map(wasmer_backend_api::types::BigInt),
200 ),
201 if self.s3_enabled {
202 "enabled"
203 } else {
204 "disabled"
205 }
206 .to_string(),
207 ]
208 }
209}
210
211impl CliRender for VolumeListItem {
212 fn render_item_table(&self) -> String {
213 let row = self.row();
214 let mut table = Table::new();
215 table.add_rows([
216 vec!["Name".to_string(), row[0].clone()],
217 vec!["Mount path".to_string(), row[1].clone()],
218 vec!["Used size".to_string(), row[2].clone()],
219 vec!["S3".to_string(), row[3].clone()],
220 ]);
221 table.to_string()
222 }
223
224 fn render_list_table(items: &[Self]) -> String {
225 let mut table = Table::new();
226 table.set_header(vec!["Name", "Mount path", "Used size", "S3"]);
227 table.add_rows(items.iter().map(|item| item.row()));
228 table.to_string()
229 }
230}