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    /// The registry to fetch packages from (inferred from the environment by
26    /// default)
27    #[clap(long, env = "WASMER_REGISTRY")]
28    pub(crate) registry: Option<UserRegistry>,
29
30    /// The API token to use when communicating with the registry (inferred from
31    /// the environment by default)
32    #[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    /// 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 {
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/// A registry as specified by the user.
202#[derive(Debug, Clone, PartialEq, Eq)]
203pub struct UserRegistry(String);
204
205impl UserRegistry {
206    /// Get the [`Registry`]'s string representation.
207    pub fn as_str(&self) -> &str {
208        self.0.as_str()
209    }
210
211    /// Get the GraphQL endpoint for this [`Registry`].
212    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        // Prod
279        {
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        // Dev
291        {
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}