1use std::{path::Path, str::FromStr};
2
3use anyhow::{Context, bail};
4use colored::Colorize;
5use dialoguer::{Confirm, theme::ColorfulTheme};
6use wasmer_backend_api::{
7 WasmerClient,
8 global_id::{GlobalId, NodeKind},
9 types::DeployApp,
10};
11use wasmer_config::app::AppConfigV1;
12
13use crate::{
14 commands::{AsyncCliCommand, Login},
15 config::WasmerEnv,
16};
17
18#[derive(Debug, PartialEq, Eq, Clone)]
22pub enum AppIdent {
23 AppId(String),
25 AppVersionId(String),
27 NamespacedName(String, String),
28 Name(String),
29}
30
31impl AppIdent {
32 pub async fn resolve(&self, client: &WasmerClient) -> Result<DeployApp, anyhow::Error> {
34 match self {
35 AppIdent::AppId(app_id) => {
36 wasmer_backend_api::query::get_app_by_id(client, app_id.clone())
37 .await
38 .with_context(|| format!("Could not find app with id '{app_id}'"))
39 }
40 AppIdent::AppVersionId(id) => {
41 let (app, _version) =
42 wasmer_backend_api::query::get_app_version_by_id_with_app(client, id.clone())
43 .await
44 .with_context(|| format!("Could not query for app version id '{id}'"))?;
45 Ok(app)
46 }
47 AppIdent::Name(name) => {
48 let user = wasmer_backend_api::query::current_user(client)
52 .await?
53 .context("not logged in")?;
54
55 wasmer_backend_api::query::get_app(client, user.username, name.clone())
56 .await?
57 .with_context(|| format!("Could not find app with name '{name}'"))
58 }
59 AppIdent::NamespacedName(owner, name) => {
60 wasmer_backend_api::query::get_app(client, owner.clone(), name.clone())
61 .await?
62 .with_context(|| format!("Could not find app '{owner}/{name}'"))
63 }
64 }
65 }
66}
67
68impl std::fmt::Display for AppIdent {
69 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
70 match self {
71 AppIdent::AppId(id) => write!(f, "{id}"),
72 AppIdent::AppVersionId(id) => write!(f, "{id}"),
73 AppIdent::NamespacedName(namespace, name) => write!(f, "{namespace}/{name}"),
74 AppIdent::Name(name) => write!(f, "{name}"),
75 }
76 }
77}
78
79impl std::str::FromStr for AppIdent {
80 type Err = anyhow::Error;
81
82 fn from_str(s: &str) -> Result<Self, Self::Err> {
83 if let Some((namespace, name)) = s.split_once('/') {
84 if namespace.is_empty() {
85 bail!("invalid app identifier '{s}': namespace can not be empty");
86 }
87 if name.is_empty() {
88 bail!("invalid app identifier '{s}': name can not be empty");
89 }
90
91 Ok(Self::NamespacedName(
92 namespace.to_string(),
93 name.to_string(),
94 ))
95 } else if let Ok(id) = GlobalId::parse_prefixed(s) {
96 match id.kind() {
97 NodeKind::DeployApp => Ok(Self::AppId(s.to_string())),
98 NodeKind::DeployAppVersion => Ok(Self::AppVersionId(s.to_string())),
99 _ => {
100 bail!(
101 "invalid app identifier '{s}': expected an app id, but id is of type {kind}",
102 kind = id.kind(),
103 );
104 }
105 }
106 } else {
107 Ok(Self::Name(s.to_string()))
108 }
109 }
110}
111
112#[derive(clap::Parser, Debug)]
120pub struct AppIdentOpts {
121 pub app: Option<AppIdent>,
131}
132
133#[allow(clippy::large_enum_variant)]
135pub enum ResolvedAppIdent {
136 Ident(AppIdent),
137 #[allow(dead_code)]
138 Config {
139 ident: AppIdent,
140 config: AppConfigV1,
141 path: std::path::PathBuf,
142 },
143}
144
145impl ResolvedAppIdent {
146 pub fn ident(&self) -> &AppIdent {
147 match self {
148 Self::Ident(ident) => ident,
149 Self::Config { ident, .. } => ident,
150 }
151 }
152}
153
154impl AppIdentOpts {
155 pub fn resolve_static_opt(&self) -> Result<Option<ResolvedAppIdent>, anyhow::Error> {
156 if let Some(id) = &self.app {
157 return Ok(Some(ResolvedAppIdent::Ident(id.clone())));
158 }
159
160 let Some((config, path)) = get_app_config_from_current_dir_opt()? else {
162 return Ok(None);
163 };
164
165 let ident = if let Some(id) = &config.app_id {
166 AppIdent::AppId(id.clone())
167 } else if let Some(owner) = &config.owner {
168 AppIdent::NamespacedName(
169 owner.clone(),
170 config.name.clone().context("App name was not specified")?,
171 )
172 } else {
173 AppIdent::Name(config.name.clone().context("App name was not specified")?)
174 };
175
176 Ok(Some(ResolvedAppIdent::Config {
177 ident,
178 config,
179 path,
180 }))
181 }
182
183 pub fn resolve_static(&self) -> Result<ResolvedAppIdent, anyhow::Error> {
184 if let Some(id) = &self.app {
185 return Ok(ResolvedAppIdent::Ident(id.clone()));
186 }
187
188 let (config, path) = get_app_config_from_current_dir()?;
190
191 let ident = if let Some(id) = &config.app_id {
192 AppIdent::AppId(id.clone())
193 } else if let Some(owner) = &config.owner {
194 AppIdent::NamespacedName(
195 owner.clone(),
196 config.name.clone().context("App name was not specified")?,
197 )
198 } else {
199 AppIdent::Name(config.name.clone().context("App name was not specified")?)
200 };
201
202 Ok(ResolvedAppIdent::Config {
203 ident,
204 config,
205 path,
206 })
207 }
208
209 pub async fn load_app(
211 &self,
212 client: &WasmerClient,
213 ) -> Result<(ResolvedAppIdent, DeployApp), anyhow::Error> {
214 let id = self.resolve_static()?;
215 let app = id.ident().resolve(client).await?;
216
217 Ok((id, app))
218 }
219
220 pub async fn load_app_opt(
221 &self,
222 client: &WasmerClient,
223 ) -> Result<Option<(ResolvedAppIdent, DeployApp)>, anyhow::Error> {
224 let id = match self.resolve_static_opt()? {
225 Some(id) => id,
226 None => return Ok(None),
227 };
228 let app = id.ident().resolve(client).await?;
229
230 Ok(Some((id, app)))
231 }
232}
233
234#[derive(clap::Parser, Debug, Clone)]
239pub struct AppIdentArgOpts {
240 #[clap(long, short)]
250 pub app: Option<AppIdent>,
251}
252
253impl AppIdentArgOpts {
254 pub fn to_opts(&self) -> AppIdentOpts {
257 AppIdentOpts {
258 app: self.app.clone(),
259 }
260 }
261}
262
263#[cfg(test)]
264mod tests {
265 use std::str::FromStr;
266
267 use super::*;
268
269 #[test]
270 fn test_app_ident() {
271 assert_eq!(
272 AppIdent::from_str("da_MRrWI0t5U582").unwrap(),
273 AppIdent::AppId("da_MRrWI0t5U582".to_string()),
274 );
275 assert_eq!(
276 AppIdent::from_str("lala").unwrap(),
277 AppIdent::Name("lala".to_string()),
278 );
279
280 assert_eq!(
281 AppIdent::from_str("alpha/beta").unwrap(),
282 AppIdent::NamespacedName("alpha".to_string(), "beta".to_string()),
283 );
284 }
285}
286
287#[derive(clap::Parser, Debug)]
292pub struct AppIdentFlag {
293 #[clap(long)]
300 pub app: Option<AppIdent>,
301}
302
303pub(super) async fn login_user(
304 env: &WasmerEnv,
305 interactive: bool,
306 msg: &str,
307) -> anyhow::Result<WasmerClient> {
308 if let Ok(client) = env.client() {
309 return Ok(client);
310 }
311
312 let theme = dialoguer::theme::ColorfulTheme::default();
313
314 if env.token().is_none() {
315 if interactive {
316 eprintln!(
317 "{}: You need to be logged in to {msg}.",
318 "WARN".yellow().bold()
319 );
320
321 if Confirm::with_theme(&theme)
322 .with_prompt("Do you want to login now?")
323 .interact()?
324 {
325 Login {
326 no_browser: false,
327 wasmer_dir: env.dir().to_path_buf(),
328 cache_dir: env.cache_dir().to_path_buf(),
329 token: None,
330 registry: env.registry.clone(),
331 }
332 .run_async()
333 .await?;
334 } else {
336 anyhow::bail!("Stopping the flow as the user is not logged in.")
337 }
338 } else {
339 let bin_name = match std::env::args().next() {
340 Some(n) => n,
341 None => String::from("wasmer"),
342 };
343 eprintln!(
344 "You are not logged in. Use the `--token` flag or log in (use `{bin_name} login`) to {msg}."
345 );
346
347 anyhow::bail!("Stopping execution as the user is not logged in.")
348 }
349 }
350
351 env.client()
352}
353
354pub fn get_app_config_from_dir_opt(
355 path: &Path,
356) -> Result<Option<(AppConfigV1, std::path::PathBuf)>, anyhow::Error> {
357 let app_config_path = path.join(AppConfigV1::CANONICAL_FILE_NAME);
358
359 if !app_config_path.exists() || !app_config_path.is_file() {
360 return Ok(None);
361 }
362 let raw_app_config = std::fs::read_to_string(&app_config_path)
364 .with_context(|| format!("Could not read file '{}'", app_config_path.display()))?;
365
366 let config = AppConfigV1::parse_yaml(&raw_app_config)
368 .map_err(|err| anyhow::anyhow!("Could not parse app.yaml: {err:?}"))?;
369
370 Ok(Some((config, app_config_path)))
371}
372
373pub fn get_app_config_from_current_dir_opt()
374-> Result<Option<(AppConfigV1, std::path::PathBuf)>, anyhow::Error> {
375 let current_dir = std::env::current_dir()?;
376 get_app_config_from_dir_opt(¤t_dir)
377}
378
379pub fn get_app_config_from_dir(
380 path: &Path,
381) -> Result<(AppConfigV1, std::path::PathBuf), anyhow::Error> {
382 get_app_config_from_dir_opt(path)?
383 .with_context(|| {
384 format!(
385 "Could not find app.yaml in directory: '{}'.\nPlease specify an app like 'wasmer app get <namespace>/<name>' or 'wasmer app get <name>`'",
386 path.display()
387 )
388 })
389}
390
391pub fn get_app_config_from_current_dir() -> Result<(AppConfigV1, std::path::PathBuf), anyhow::Error>
392{
393 let current_dir = std::env::current_dir()?;
394 get_app_config_from_dir(¤t_dir)
395}
396
397#[allow(dead_code)]
399pub(crate) fn prompt_app_ident(message: &str) -> Result<AppIdent, anyhow::Error> {
400 let theme = ColorfulTheme::default();
401 loop {
402 let ident: String = dialoguer::Input::with_theme(&theme)
403 .with_prompt(message)
404 .interact_text()?;
405 match AppIdent::from_str(&ident) {
406 Ok(id) => break Ok(id),
407 Err(e) => eprintln!("{e}"),
408 }
409 }
410}