wasmer_cli/config/
env.rs

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/// Command-line flags for determining the local "Wasmer Environment".
12///
13/// This is where you access `$WASMER_DIR`, the `$WASMER_DIR/wasmer.toml` config
14/// file, and specify the current registry.
15#[derive(Debug, Clone, PartialEq, clap::Parser)]
16pub struct WasmerEnv {
17    /// Set Wasmer's home directory
18    #[clap(long, env = "WASMER_DIR", default_value = super::DEFAULT_WASMER_DIR.as_os_str())]
19    wasmer_dir: PathBuf,
20
21    /// The directory cached artefacts are saved to.
22    #[clap(long, env = "WASMER_CACHE_DIR", default_value = super::DEFAULT_WASMER_CACHE_DIR.as_os_str())]
23    pub(crate) cache_dir: PathBuf,
24
25    /// Change the current registry
26    #[clap(long, env = "WASMER_REGISTRY")]
27    pub(crate) registry: Option<UserRegistry>,
28
29    /// The API token to use when communicating with the registry (inferred from
30    /// the environment by default)
31    #[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    /// Get the "public" url of the current registry (e.g. "https://wasmer.io" instead of
55    /// "https://registry.wasmer.io/graphql").
56    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    /// Get the GraphQL endpoint used to query the registry.
72    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    /// Load the current Wasmer config.
84    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    /// Returns the proxy specified in wasmer config if present
96    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    /// The directory all Wasmer artifacts are stored in.
107    pub fn dir(&self) -> &Path {
108        &self.wasmer_dir
109    }
110
111    /// The directory all cached artifacts should be saved to.
112    pub fn cache_dir(&self) -> &Path {
113        &self.cache_dir
114    }
115
116    /// Retrieve the specified token.
117    ///
118    /// NOTE: In contrast to [`Self::token`], this will not fall back to loading
119    /// the token from the confg file.
120    #[allow(unused)]
121    pub fn get_token_opt(&self) -> Option<&str> {
122        self.token.as_deref()
123    }
124
125    /// The API token for the active registry.
126    pub fn token(&self) -> Option<String> {
127        if let Some(token) = &self.token {
128            return Some(token.clone());
129        }
130
131        // Fall back to the config file
132
133        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/// A registry as specified by the user.
204#[derive(Debug, Clone, PartialEq, Eq)]
205pub struct UserRegistry(String);
206
207impl UserRegistry {
208    /// Get the [`Registry`]'s string representation.
209    pub fn as_str(&self) -> &str {
210        self.0.as_str()
211    }
212
213    /// Get the GraphQL endpoint for this [`Registry`].
214    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        // Prod
281        {
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        // Dev
293        {
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        // Bugtopia
305        {
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}