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")]
28 pub(crate) registry: Option<UserRegistry>,
29
30 #[clap(long, env = "WASMER_TOKEN")]
33 token: Option<String>,
34}
35
36impl WasmerEnv {
37 const APP_DOMAIN_PROD: &'static str = "wasmer.app";
38 const APP_DOMAIN_DEV: &'static str = "wasmer.dev";
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 {
152 anyhow::bail!(
153 "could not determine app domain for backend url '{domain}': unknown backend"
154 );
155 }
156 }
157
158 pub fn client_unauthennticated(&self) -> Result<WasmerClient, anyhow::Error> {
159 let registry_url = self.registry_endpoint()?;
160
161 let proxy = self.proxy()?;
162
163 let client = wasmer_backend_api::WasmerClient::new_with_proxy(
164 registry_url,
165 &DEFAULT_WASMER_CLI_USER_AGENT,
166 proxy,
167 )?;
168
169 let client = if let Some(token) = self.token() {
170 client.with_auth_token(token)
171 } else {
172 client
173 };
174
175 Ok(client)
176 }
177
178 pub fn client(&self) -> Result<WasmerClient, anyhow::Error> {
179 let client = self.client_unauthennticated()?;
180 if client.auth_token().is_none() {
181 anyhow::bail!(
182 "no token provided - run 'wasmer login', specify --token=XXX, or set the WASMER_TOKEN env var"
183 );
184 }
185
186 Ok(client)
187 }
188}
189
190impl Default for WasmerEnv {
191 fn default() -> Self {
192 Self {
193 wasmer_dir: super::DEFAULT_WASMER_DIR.clone(),
194 cache_dir: super::DEFAULT_WASMER_CACHE_DIR.clone(),
195 registry: None,
196 token: None,
197 }
198 }
199}
200
201#[derive(Debug, Clone, PartialEq, Eq)]
203pub struct UserRegistry(String);
204
205impl UserRegistry {
206 pub fn as_str(&self) -> &str {
208 self.0.as_str()
209 }
210
211 pub fn graphql_endpoint(&self) -> Result<Url, Error> {
213 let url = super::format_graphql(self.as_str()).parse()?;
214 Ok(url)
215 }
216}
217
218impl From<String> for UserRegistry {
219 fn from(value: String) -> Self {
220 UserRegistry(value)
221 }
222}
223
224impl From<&str> for UserRegistry {
225 fn from(value: &str) -> Self {
226 UserRegistry(value.to_string())
227 }
228}
229
230#[cfg(test)]
231mod tests {
232 use tempfile::TempDir;
233
234 use super::*;
235
236 const WASMER_TOML: &str = r#"
237 telemetry_enabled = false
238 update_notifications_enabled = false
239
240 [registry]
241 active_registry = "https://registry.wasmer.io/graphql"
242
243 [[registry.tokens]]
244 registry = "https://registry.wasmer.wtf/graphql"
245 token = "dev-token"
246
247 [[registry.tokens]]
248 registry = "https://registry.wasmer.io/graphql"
249 token = "prod-token"
250
251 [[registry.tokens]]
252 registry = "http://localhost:11/graphql"
253 token = "invalid"
254 "#;
255
256 #[test]
257 fn load_defaults_from_config() {
258 let temp = TempDir::new().unwrap();
259 std::fs::write(temp.path().join("wasmer.toml"), WASMER_TOML).unwrap();
260
261 let env = WasmerEnv {
262 wasmer_dir: temp.path().to_path_buf(),
263 registry: None,
264 cache_dir: temp.path().join("cache").to_path_buf(),
265 token: None,
266 };
267
268 assert_eq!(
269 env.registry_endpoint().unwrap().as_str(),
270 "https://registry.wasmer.io/graphql"
271 );
272 assert_eq!(env.token().unwrap(), "prod-token");
273 assert_eq!(env.cache_dir(), temp.path().join("cache"));
274 }
275
276 #[test]
277 fn env_app_domain() {
278 {
280 let env = WasmerEnv {
281 wasmer_dir: PathBuf::from("/tmp"),
282 registry: Some(UserRegistry::from("https://registry.wasmer.io/graphql")),
283 cache_dir: PathBuf::from("/tmp/cache"),
284 token: None,
285 };
286
287 assert_eq!(env.app_domain().unwrap(), "wasmer.app");
288 }
289
290 {
292 let env = WasmerEnv {
293 wasmer_dir: PathBuf::from("/tmp"),
294 registry: Some(UserRegistry::from("https://registry.wasmer.wtf/graphql")),
295 cache_dir: PathBuf::from("/tmp/cache"),
296 token: None,
297 };
298
299 assert_eq!(env.app_domain().unwrap(), "wasmer.dev");
300 }
301 }
302
303 #[test]
304 fn override_token() {
305 let temp = TempDir::new().unwrap();
306 std::fs::write(temp.path().join("wasmer.toml"), WASMER_TOML).unwrap();
307
308 let env = WasmerEnv {
309 wasmer_dir: temp.path().to_path_buf(),
310 registry: None,
311 cache_dir: temp.path().join("cache").to_path_buf(),
312 token: Some("asdf".to_string()),
313 };
314
315 assert_eq!(
316 env.registry_endpoint().unwrap().as_str(),
317 "https://registry.wasmer.io/graphql"
318 );
319 assert_eq!(env.token().unwrap(), "asdf");
320 assert_eq!(env.cache_dir(), temp.path().join("cache"));
321 }
322
323 #[test]
324 fn override_registry() {
325 let temp = TempDir::new().unwrap();
326 std::fs::write(temp.path().join("wasmer.toml"), WASMER_TOML).unwrap();
327 let env = WasmerEnv {
328 wasmer_dir: temp.path().to_path_buf(),
329 registry: Some(UserRegistry::from("wasmer.wtf")),
330 cache_dir: temp.path().join("cache").to_path_buf(),
331 token: None,
332 };
333
334 assert_eq!(
335 env.registry_endpoint().unwrap().as_str(),
336 "https://registry.wasmer.wtf/graphql"
337 );
338 assert_eq!(env.token().unwrap(), "dev-token");
339 assert_eq!(env.cache_dir(), temp.path().join("cache"));
340 }
341
342 #[test]
343 fn override_registry_and_token() {
344 let temp = TempDir::new().unwrap();
345 std::fs::write(temp.path().join("wasmer.toml"), WASMER_TOML).unwrap();
346
347 let env = WasmerEnv {
348 wasmer_dir: temp.path().to_path_buf(),
349 registry: Some(UserRegistry::from("wasmer.wtf")),
350 cache_dir: temp.path().join("cache").to_path_buf(),
351 token: Some("asdf".to_string()),
352 };
353
354 assert_eq!(
355 env.registry_endpoint().unwrap().as_str(),
356 "https://registry.wasmer.wtf/graphql"
357 );
358 assert_eq!(env.token().unwrap(), "asdf");
359 assert_eq!(env.cache_dir(), temp.path().join("cache"));
360 }
361
362 #[test]
363 fn override_cache_dir() {
364 let temp = TempDir::new().unwrap();
365 std::fs::write(temp.path().join("wasmer.toml"), WASMER_TOML).unwrap();
366 let expected_cache_dir = temp.path().join("some-other-cache");
367
368 let env = WasmerEnv {
369 wasmer_dir: temp.path().to_path_buf(),
370 registry: None,
371 cache_dir: expected_cache_dir.clone(),
372 token: None,
373 };
374
375 assert_eq!(
376 env.registry_endpoint().unwrap().as_str(),
377 "https://registry.wasmer.io/graphql"
378 );
379 assert_eq!(env.token().unwrap(), "prod-token");
380 assert_eq!(env.cache_dir(), expected_cache_dir);
381 }
382
383 #[test]
384 fn registries_have_public_url() {
385 let temp = TempDir::new().unwrap();
386 std::fs::write(temp.path().join("wasmer.toml"), WASMER_TOML).unwrap();
387
388 let inputs = [
389 ("https://wasmer.io/", "https://registry.wasmer.io/graphql"),
390 ("https://wasmer.wtf/", "https://registry.wasmer.wtf/graphql"),
391 ("https://wasmer.wtf/", "https://registry.wasmer.wtf/graphql"),
392 (
393 "https://wasmer.wtf/",
394 "https://registry.wasmer.wtf/something/else",
395 ),
396 ("https://wasmer.wtf/", "https://wasmer.wtf/graphql"),
397 ("https://wasmer.wtf/", "https://wasmer.wtf/graphql"),
398 ("http://localhost:8000/", "http://localhost:8000/graphql"),
399 ("http://localhost:8000/", "http://localhost:8000/graphql"),
400 ];
401
402 for (want, input) in inputs {
403 let env = WasmerEnv {
404 wasmer_dir: temp.path().to_path_buf(),
405 registry: Some(UserRegistry::from(input)),
406 cache_dir: temp.path().join("cache").to_path_buf(),
407 token: None,
408 };
409
410 assert_eq!(want, &env.registry_public_url().unwrap().to_string())
411 }
412 }
413}