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
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    /// Get the "public" url of the current registry (e.g. "https://wasmer.io" instead of
54    /// "https://registry.wasmer.io/graphql").
55    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    /// Get the GraphQL endpoint used to query the registry.
71    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    /// Load the current Wasmer config.
83    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    /// Returns the proxy specified in wasmer config if present
95    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    /// The directory all Wasmer artifacts are stored in.
106    pub fn dir(&self) -> &Path {
107        &self.wasmer_dir
108    }
109
110    /// The directory all cached artifacts should be saved to.
111    pub fn cache_dir(&self) -> &Path {
112        &self.cache_dir
113    }
114
115    /// Retrieve the specified token.
116    ///
117    /// NOTE: In contrast to [`Self::token`], this will not fall back to loading
118    /// the token from the confg file.
119    #[allow(unused)]
120    pub fn get_token_opt(&self) -> Option<&str> {
121        self.token.as_deref()
122    }
123
124    /// The API token for the active registry.
125    pub fn token(&self) -> Option<String> {
126        if let Some(token) = &self.token {
127            return Some(token.clone());
128        }
129
130        // Fall back to the config file
131
132        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/// A registry as specified by the user.
201#[derive(Debug, Clone, PartialEq, Eq)]
202pub struct UserRegistry(String);
203
204impl UserRegistry {
205    /// Get the [`Registry`]'s string representation.
206    pub fn as_str(&self) -> &str {
207        self.0.as_str()
208    }
209
210    /// Get the GraphQL endpoint for this [`Registry`].
211    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        // Prod
278        {
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        // Dev
290        {
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}