1use super::WasmerConfig;
2use anyhow::{Context, Error};
3use std::path::{Path, PathBuf};
4use std::sync::LazyLock;
5use url::Url;
6use wasmer_backend_api::WasmerClient;
7
8pub static DEFAULT_WASMER_CLI_USER_AGENT: LazyLock<String> =
9 LazyLock::new(|| format!("WasmerCLI-v{}", env!("CARGO_PKG_VERSION")));
10
11#[derive(Debug, Clone, PartialEq, clap::Parser)]
16pub struct WasmerEnv {
17 #[clap(long, env = "WASMER_DIR", default_value = super::DEFAULT_WASMER_DIR.as_os_str())]
19 wasmer_dir: PathBuf,
20
21 #[clap(long, env = "WASMER_CACHE_DIR", default_value = super::DEFAULT_WASMER_CACHE_DIR.as_os_str())]
23 pub(crate) cache_dir: PathBuf,
24
25 #[clap(long, env = "WASMER_REGISTRY")]
27 pub(crate) registry: Option<UserRegistry>,
28
29 #[clap(long, env = "WASMER_TOKEN")]
32 token: Option<String>,
33}
34
35impl WasmerEnv {
36 const APP_DOMAIN_PROD: &'static str = "wasmer.app";
37 const APP_DOMAIN_DEV: &'static str = "wasmer.dev";
38 const APP_DOMAIN_BUGT: &'static str = "wasmerfun.app";
39
40 pub fn new(
41 wasmer_dir: PathBuf,
42 cache_dir: PathBuf,
43 token: Option<String>,
44 registry: Option<UserRegistry>,
45 ) -> Self {
46 WasmerEnv {
47 wasmer_dir,
48 registry,
49 token,
50 cache_dir,
51 }
52 }
53
54 pub fn registry_public_url(&self) -> Result<Url, Error> {
57 let mut url = self.registry_endpoint()?;
58 url.set_path("");
59
60 let mut domain = url.host_str().context("url has no host")?.to_string();
61 if domain.starts_with("registry.") {
62 domain = domain.strip_prefix("registry.").unwrap().to_string();
63 }
64
65 url.set_host(Some(&domain))
66 .context("could not derive registry public url")?;
67
68 Ok(url)
69 }
70
71 pub fn registry_endpoint(&self) -> Result<Url, Error> {
73 if let Some(registry) = &self.registry {
74 return registry.graphql_endpoint();
75 }
76
77 let config = self.config()?;
78 let url = config.registry.get_current_registry().parse()?;
79
80 Ok(url)
81 }
82
83 pub fn config(&self) -> Result<WasmerConfig, Error> {
85 WasmerConfig::from_file(self.dir())
86 .map_err(Error::msg)
87 .with_context(|| {
88 format!(
89 "Unable to load the config from the \"{}\" directory",
90 self.dir().display()
91 )
92 })
93 }
94
95 pub fn proxy(&self) -> Result<Option<reqwest::Proxy>, Error> {
97 self.config()?
98 .proxy
99 .url
100 .as_ref()
101 .map(reqwest::Proxy::all)
102 .transpose()
103 .map_err(Into::into)
104 }
105
106 pub fn dir(&self) -> &Path {
108 &self.wasmer_dir
109 }
110
111 pub fn cache_dir(&self) -> &Path {
113 &self.cache_dir
114 }
115
116 #[allow(unused)]
121 pub fn get_token_opt(&self) -> Option<&str> {
122 self.token.as_deref()
123 }
124
125 pub fn token(&self) -> Option<String> {
127 if let Some(token) = &self.token {
128 return Some(token.clone());
129 }
130
131 let config = self.config().ok()?;
134 let registry_endpoint = self.registry_endpoint().ok()?;
135 config
136 .registry
137 .get_login_token_for_registry(registry_endpoint.as_str())
138 }
139
140 pub fn app_domain(&self) -> Result<String, Error> {
141 let registry_url = self.registry_public_url()?;
142 let domain = registry_url
143 .host_str()
144 .context("url has no host")?
145 .trim_end_matches('.');
146
147 if domain.ends_with("wasmer.io") {
148 Ok(Self::APP_DOMAIN_PROD.to_string())
149 } else if domain.ends_with("wasmer.wtf") {
150 Ok(Self::APP_DOMAIN_DEV.to_string())
151 } else if domain.ends_with("wasmer.fun") {
152 Ok(Self::APP_DOMAIN_BUGT.to_string())
153 } else {
154 anyhow::bail!(
155 "could not determine app domain for backend url '{domain}': unknown backend"
156 );
157 }
158 }
159
160 pub fn client_unauthennticated(&self) -> Result<WasmerClient, anyhow::Error> {
161 let registry_url = self.registry_endpoint()?;
162
163 let proxy = self.proxy()?;
164
165 let client = wasmer_backend_api::WasmerClient::new_with_proxy(
166 registry_url,
167 &DEFAULT_WASMER_CLI_USER_AGENT,
168 proxy,
169 )?;
170
171 let client = if let Some(token) = self.token() {
172 client.with_auth_token(token)
173 } else {
174 client
175 };
176
177 Ok(client)
178 }
179
180 pub fn client(&self) -> Result<WasmerClient, anyhow::Error> {
181 let client = self.client_unauthennticated()?;
182 if client.auth_token().is_none() {
183 anyhow::bail!(
184 "no token provided - run 'wasmer login', specify --token=XXX, or set the WASMER_TOKEN env var"
185 );
186 }
187
188 Ok(client)
189 }
190}
191
192impl Default for WasmerEnv {
193 fn default() -> Self {
194 Self {
195 wasmer_dir: super::DEFAULT_WASMER_DIR.clone(),
196 cache_dir: super::DEFAULT_WASMER_CACHE_DIR.clone(),
197 registry: None,
198 token: None,
199 }
200 }
201}
202
203#[derive(Debug, Clone, PartialEq, Eq)]
205pub struct UserRegistry(String);
206
207impl UserRegistry {
208 pub fn as_str(&self) -> &str {
210 self.0.as_str()
211 }
212
213 pub fn graphql_endpoint(&self) -> Result<Url, Error> {
215 let url = super::format_graphql(self.as_str()).parse()?;
216 Ok(url)
217 }
218}
219
220impl From<String> for UserRegistry {
221 fn from(value: String) -> Self {
222 UserRegistry(value)
223 }
224}
225
226impl From<&str> for UserRegistry {
227 fn from(value: &str) -> Self {
228 UserRegistry(value.to_string())
229 }
230}
231
232#[cfg(test)]
233mod tests {
234 use tempfile::TempDir;
235
236 use super::*;
237
238 const WASMER_TOML: &str = r#"
239 telemetry_enabled = false
240 update_notifications_enabled = false
241
242 [registry]
243 active_registry = "https://registry.wasmer.io/graphql"
244
245 [[registry.tokens]]
246 registry = "https://registry.wasmer.wtf/graphql"
247 token = "dev-token"
248
249 [[registry.tokens]]
250 registry = "https://registry.wasmer.io/graphql"
251 token = "prod-token"
252
253 [[registry.tokens]]
254 registry = "http://localhost:11/graphql"
255 token = "invalid"
256 "#;
257
258 #[test]
259 fn load_defaults_from_config() {
260 let temp = TempDir::new().unwrap();
261 std::fs::write(temp.path().join("wasmer.toml"), WASMER_TOML).unwrap();
262
263 let env = WasmerEnv {
264 wasmer_dir: temp.path().to_path_buf(),
265 registry: None,
266 cache_dir: temp.path().join("cache").to_path_buf(),
267 token: None,
268 };
269
270 assert_eq!(
271 env.registry_endpoint().unwrap().as_str(),
272 "https://registry.wasmer.io/graphql"
273 );
274 assert_eq!(env.token().unwrap(), "prod-token");
275 assert_eq!(env.cache_dir(), temp.path().join("cache"));
276 }
277
278 #[test]
279 fn env_app_domain() {
280 {
282 let env = WasmerEnv {
283 wasmer_dir: PathBuf::from("/tmp"),
284 registry: Some(UserRegistry::from("https://registry.wasmer.io/graphql")),
285 cache_dir: PathBuf::from("/tmp/cache"),
286 token: None,
287 };
288
289 assert_eq!(env.app_domain().unwrap(), "wasmer.app");
290 }
291
292 {
294 let env = WasmerEnv {
295 wasmer_dir: PathBuf::from("/tmp"),
296 registry: Some(UserRegistry::from("https://registry.wasmer.wtf/graphql")),
297 cache_dir: PathBuf::from("/tmp/cache"),
298 token: None,
299 };
300
301 assert_eq!(env.app_domain().unwrap(), "wasmer.dev");
302 }
303
304 {
306 let env = WasmerEnv {
307 wasmer_dir: PathBuf::from("/tmp"),
308 registry: Some(UserRegistry::from("https://registry.wasmer.fun/graphql")),
309 cache_dir: PathBuf::from("/tmp/cache"),
310 token: None,
311 };
312
313 assert_eq!(env.app_domain().unwrap(), "wasmerfun.app");
314 }
315 }
316
317 #[test]
318 fn override_token() {
319 let temp = TempDir::new().unwrap();
320 std::fs::write(temp.path().join("wasmer.toml"), WASMER_TOML).unwrap();
321
322 let env = WasmerEnv {
323 wasmer_dir: temp.path().to_path_buf(),
324 registry: None,
325 cache_dir: temp.path().join("cache").to_path_buf(),
326 token: Some("asdf".to_string()),
327 };
328
329 assert_eq!(
330 env.registry_endpoint().unwrap().as_str(),
331 "https://registry.wasmer.io/graphql"
332 );
333 assert_eq!(env.token().unwrap(), "asdf");
334 assert_eq!(env.cache_dir(), temp.path().join("cache"));
335 }
336
337 #[test]
338 fn override_registry() {
339 let temp = TempDir::new().unwrap();
340 std::fs::write(temp.path().join("wasmer.toml"), WASMER_TOML).unwrap();
341 let env = WasmerEnv {
342 wasmer_dir: temp.path().to_path_buf(),
343 registry: Some(UserRegistry::from("wasmer.wtf")),
344 cache_dir: temp.path().join("cache").to_path_buf(),
345 token: None,
346 };
347
348 assert_eq!(
349 env.registry_endpoint().unwrap().as_str(),
350 "https://registry.wasmer.wtf/graphql"
351 );
352 assert_eq!(env.token().unwrap(), "dev-token");
353 assert_eq!(env.cache_dir(), temp.path().join("cache"));
354 }
355
356 #[test]
357 fn override_registry_and_token() {
358 let temp = TempDir::new().unwrap();
359 std::fs::write(temp.path().join("wasmer.toml"), WASMER_TOML).unwrap();
360
361 let env = WasmerEnv {
362 wasmer_dir: temp.path().to_path_buf(),
363 registry: Some(UserRegistry::from("wasmer.wtf")),
364 cache_dir: temp.path().join("cache").to_path_buf(),
365 token: Some("asdf".to_string()),
366 };
367
368 assert_eq!(
369 env.registry_endpoint().unwrap().as_str(),
370 "https://registry.wasmer.wtf/graphql"
371 );
372 assert_eq!(env.token().unwrap(), "asdf");
373 assert_eq!(env.cache_dir(), temp.path().join("cache"));
374 }
375
376 #[test]
377 fn override_cache_dir() {
378 let temp = TempDir::new().unwrap();
379 std::fs::write(temp.path().join("wasmer.toml"), WASMER_TOML).unwrap();
380 let expected_cache_dir = temp.path().join("some-other-cache");
381
382 let env = WasmerEnv {
383 wasmer_dir: temp.path().to_path_buf(),
384 registry: None,
385 cache_dir: expected_cache_dir.clone(),
386 token: None,
387 };
388
389 assert_eq!(
390 env.registry_endpoint().unwrap().as_str(),
391 "https://registry.wasmer.io/graphql"
392 );
393 assert_eq!(env.token().unwrap(), "prod-token");
394 assert_eq!(env.cache_dir(), expected_cache_dir);
395 }
396
397 #[test]
398 fn registries_have_public_url() {
399 let temp = TempDir::new().unwrap();
400 std::fs::write(temp.path().join("wasmer.toml"), WASMER_TOML).unwrap();
401
402 let inputs = [
403 ("https://wasmer.io/", "https://registry.wasmer.io/graphql"),
404 ("https://wasmer.wtf/", "https://registry.wasmer.wtf/graphql"),
405 ("https://wasmer.wtf/", "https://registry.wasmer.wtf/graphql"),
406 (
407 "https://wasmer.wtf/",
408 "https://registry.wasmer.wtf/something/else",
409 ),
410 ("https://wasmer.wtf/", "https://wasmer.wtf/graphql"),
411 ("https://wasmer.wtf/", "https://wasmer.wtf/graphql"),
412 ("http://localhost:8000/", "http://localhost:8000/graphql"),
413 ("http://localhost:8000/", "http://localhost:8000/graphql"),
414 ];
415
416 for (want, input) in inputs {
417 let env = WasmerEnv {
418 wasmer_dir: temp.path().to_path_buf(),
419 registry: Some(UserRegistry::from(input)),
420 cache_dir: temp.path().join("cache").to_path_buf(),
421 token: None,
422 };
423
424 assert_eq!(want, &env.registry_public_url().unwrap().to_string())
425 }
426 }
427}