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