wasmer_cli/config/
mod.rs

1mod env;
2pub use env::*;
3
4use serde::{Deserialize, Serialize};
5use std::path::{Path, PathBuf};
6use std::sync::LazyLock;
7use url::Url;
8use wasmer_backend_api::WasmerClient;
9
10pub static GLOBAL_CONFIG_FILE_NAME: &str = "wasmer.toml";
11pub static DEFAULT_PROD_REGISTRY: &str = "https://registry.wasmer.io/graphql";
12
13/// The default value for `$WASMER_DIR`.
14pub static DEFAULT_WASMER_DIR: LazyLock<PathBuf> =
15    LazyLock::new(|| match WasmerConfig::get_wasmer_dir() {
16        Ok(path) => path,
17        Err(e) => {
18            if let Some(install_prefix) = option_env!("WASMER_INSTALL_PREFIX") {
19                return PathBuf::from(install_prefix);
20            }
21
22            panic!("Unable to determine the wasmer dir: {e}");
23        }
24    });
25
26/// The default value for `$WASMER_DIR`.
27pub static DEFAULT_WASMER_CACHE_DIR: LazyLock<PathBuf> =
28    LazyLock::new(|| DEFAULT_WASMER_DIR.join("cache"));
29
30#[derive(Deserialize, Serialize, Debug, PartialEq, Eq)]
31pub struct WasmerConfig {
32    /// Whether or not telemetry is enabled.
33    #[serde(default)]
34    pub telemetry_enabled: bool,
35
36    /// Whether or not updated notifications are enabled.
37    #[serde(default)]
38    pub update_notifications_enabled: bool,
39
40    /// The registry that wasmer will connect to.
41    pub registry: MultiRegistry,
42
43    /// The proxy to use when connecting to the Internet.
44    #[serde(default)]
45    pub proxy: Proxy,
46}
47
48impl Default for WasmerConfig {
49    fn default() -> Self {
50        Self {
51            telemetry_enabled: true,
52            update_notifications_enabled: true,
53            registry: Default::default(),
54            proxy: Default::default(),
55        }
56    }
57}
58
59#[derive(Deserialize, Serialize, Debug, PartialEq, Eq, Default)]
60pub struct Proxy {
61    pub url: Option<String>,
62}
63
64/// Struct to store login tokens for multiple registry URLs
65/// inside of the wasmer.toml configuration file
66#[derive(Deserialize, Serialize, Debug, PartialEq, Eq, Clone)]
67pub struct MultiRegistry {
68    /// Currently active registry
69    pub active_registry: String,
70    /// Map from "RegistryUrl" to "LoginToken", in order to
71    /// be able to be able to easily switch between registries
72    pub tokens: Vec<RegistryLogin>,
73}
74
75impl Default for MultiRegistry {
76    fn default() -> Self {
77        MultiRegistry {
78            active_registry: format_graphql(DEFAULT_PROD_REGISTRY),
79            tokens: Vec::new(),
80        }
81    }
82}
83
84#[derive(Deserialize, Serialize, Debug, PartialEq, Eq, Clone)]
85pub struct Registry {
86    pub url: String,
87    pub token: Option<String>,
88}
89
90pub fn format_graphql(registry: &str) -> String {
91    if let Ok(mut url) = Url::parse(registry) {
92        // Looks like we've got a valid URL. Let's try to use it as-is.
93        if url.has_host() {
94            if url.path() == "/" {
95                // make sure we convert http://registry.wasmer.io/ to
96                // http://registry.wasmer.io/graphql
97                url.set_path("/graphql");
98            }
99
100            return url.to_string();
101        }
102    }
103
104    if !registry.contains("://") && !registry.contains('/') {
105        return endpoint_from_domain_name(registry);
106    }
107
108    // looks like we've received something we can't deal with. Just pass it
109    // through as-is and hopefully it'll either work or the end user can figure
110    // it out
111    registry.to_string()
112}
113
114/// By convention, something like `"wasmer.io"` should be converted to
115/// `"https://registry.wasmer.io/graphql"`.
116fn endpoint_from_domain_name(domain_name: &str) -> String {
117    if domain_name.contains("localhost") {
118        return format!("http://{domain_name}/graphql");
119    }
120
121    format!("https://registry.{domain_name}/graphql")
122}
123
124async fn test_if_registry_present(registry: &str) -> anyhow::Result<()> {
125    let client = WasmerClient::new(url::Url::parse(registry)?, &DEFAULT_WASMER_CLI_USER_AGENT)?;
126
127    wasmer_backend_api::query::current_user(&client)
128        .await
129        .map(|_| ())
130}
131
132#[derive(PartialEq, Eq, Copy, Clone)]
133pub enum UpdateRegistry {
134    Update,
135    #[allow(unused)]
136    LeaveAsIs,
137}
138
139impl MultiRegistry {
140    /// Gets the current (active) registry URL
141    pub fn remove_registry(&mut self, registry: &str) {
142        let MultiRegistry { tokens, .. } = self;
143        tokens.retain(|i| i.registry != registry);
144        tokens.retain(|i| i.registry != format_graphql(registry));
145    }
146
147    #[allow(unused)]
148    pub fn get_graphql_url(&self) -> String {
149        self.get_current_registry()
150    }
151
152    /// Gets the current (active) registry URL
153    pub fn get_current_registry(&self) -> String {
154        format_graphql(&self.active_registry)
155    }
156
157    /// Checks if the current registry equals `registry`.
158    pub fn is_current_registry(&self, registry: &str) -> bool {
159        format_graphql(&self.active_registry) == format_graphql(registry)
160    }
161
162    #[allow(unused)]
163    pub fn current_login(&self) -> Option<&RegistryLogin> {
164        self.tokens
165            .iter()
166            .find(|login| login.registry == self.active_registry)
167    }
168
169    /// Sets the current (active) registry URL
170    pub async fn set_current_registry(&mut self, registry: &str) {
171        let registry = format_graphql(registry);
172        if let Err(e) = test_if_registry_present(&registry).await {
173            println!("Error when trying to ping registry {registry:?}: {e}");
174            println!("WARNING: Registry {registry:?} will be used, but commands may not succeed.");
175        }
176        self.active_registry = registry;
177    }
178
179    /// Returns the login token for the registry
180    pub fn get_login_token_for_registry(&self, registry: &str) -> Option<String> {
181        let registry_formatted = format_graphql(registry);
182        self.tokens
183            .iter()
184            .rev()
185            .find(|login| login.registry == registry || login.registry == registry_formatted)
186            .map(|login| login.token.clone())
187    }
188
189    /// Sets the login token for the registry URL
190    pub fn set_login_token_for_registry(
191        &mut self,
192        registry: &str,
193        token: &str,
194        update_current_registry: UpdateRegistry,
195    ) {
196        let registry_formatted = format_graphql(registry);
197        self.tokens
198            .retain(|login| !(login.registry == registry || login.registry == registry_formatted));
199        self.tokens.push(RegistryLogin {
200            registry: format_graphql(registry),
201            token: token.to_string(),
202        });
203        if update_current_registry == UpdateRegistry::Update {
204            self.active_registry = format_graphql(registry);
205        }
206    }
207}
208
209impl WasmerConfig {
210    /// Save the config to a file
211    pub fn save<P: AsRef<Path>>(&self, to: P) -> anyhow::Result<()> {
212        use std::{fs::File, io::Write};
213        let config_serialized = toml::to_string(&self)?;
214        let mut file = File::create(to)?;
215        file.write_all(config_serialized.as_bytes())?;
216        Ok(())
217    }
218
219    pub fn from_file(wasmer_dir: &Path) -> Result<Self, String> {
220        let path = Self::get_file_location(wasmer_dir);
221        match std::fs::read_to_string(path) {
222            Ok(config_toml) => Ok(toml::from_str(&config_toml).unwrap_or_else(|_| Self::default())),
223            Err(_e) => Ok(Self::default()),
224        }
225    }
226
227    /// Creates and returns the `WASMER_DIR` directory (or $HOME/.wasmer as a fallback)
228    pub fn get_wasmer_dir() -> Result<PathBuf, String> {
229        Ok(
230            if let Some(folder_str) = std::env::var("WASMER_DIR").ok().filter(|s| !s.is_empty()) {
231                let folder = PathBuf::from(folder_str);
232                std::fs::create_dir_all(folder.clone())
233                    .map_err(|e| format!("cannot create config directory: {e}"))?;
234                folder
235            } else {
236                let home_dir =
237                    dirs::home_dir().ok_or_else(|| "cannot find home directory".to_string())?;
238                let mut folder = home_dir;
239                folder.push(".wasmer");
240                std::fs::create_dir_all(folder.clone())
241                    .map_err(|e| format!("cannot create config directory: {e}"))?;
242                folder
243            },
244        )
245    }
246
247    #[allow(unused)]
248    /// Load the config based on environment variables and default config file locations.
249    pub fn from_env() -> Result<Self, anyhow::Error> {
250        let dir = Self::get_wasmer_dir()
251            .map_err(|err| anyhow::anyhow!("Could not determine wasmer dir: {err}"))?;
252        let file_path = Self::get_file_location(&dir);
253        Self::from_file(&file_path).map_err(|err| {
254            anyhow::anyhow!(
255                "Could not load config file at '{}': {}",
256                file_path.display(),
257                err
258            )
259        })
260    }
261
262    pub fn get_file_location(wasmer_dir: &Path) -> PathBuf {
263        wasmer_dir.join(GLOBAL_CONFIG_FILE_NAME)
264    }
265}
266
267#[derive(Deserialize, Serialize, Debug, PartialEq, Eq, Clone)]
268pub struct RegistryLogin {
269    /// Registry URL to login to
270    pub registry: String,
271    /// Login token for the registry
272    pub token: String,
273}
274
275#[cfg(test)]
276mod tests {
277    use super::*;
278
279    #[tokio::test]
280    async fn test_registries_switch_token() {
281        let mut registries = MultiRegistry::default();
282
283        registries
284            .set_current_registry("https://registry.wasmer.wtf")
285            .await;
286        assert_eq!(
287            registries.get_current_registry(),
288            "https://registry.wasmer.wtf/graphql".to_string()
289        );
290        registries.set_login_token_for_registry(
291            "https://registry.wasmer.io",
292            "token1",
293            UpdateRegistry::LeaveAsIs,
294        );
295        assert_eq!(
296            registries.get_current_registry(),
297            "https://registry.wasmer.wtf/graphql".to_string()
298        );
299        assert_eq!(
300            registries.get_login_token_for_registry(&registries.get_current_registry()),
301            None
302        );
303        registries
304            .set_current_registry("https://registry.wasmer.io")
305            .await;
306        assert_eq!(
307            registries.get_login_token_for_registry(&registries.get_current_registry()),
308            Some("token1".to_string())
309        );
310        registries.remove_registry("https://registry.wasmer.io");
311        assert_eq!(
312            registries.get_login_token_for_registry(&registries.get_current_registry()),
313            None
314        );
315    }
316
317    #[test]
318    fn format_registry_urls() {
319        let inputs = [
320            // Domain names work
321            ("wasmer.io", "https://registry.wasmer.io/graphql"),
322            ("wasmer.wtf", "https://registry.wasmer.wtf/graphql"),
323            // Plain URLs
324            (
325                "https://registry.wasmer.wtf/graphql",
326                "https://registry.wasmer.wtf/graphql",
327            ),
328            (
329                "https://registry.wasmer.wtf/something/else",
330                "https://registry.wasmer.wtf/something/else",
331            ),
332            // We don't automatically prepend the domain name with
333            // "registry", but we will make sure "/" gets turned into "/graphql"
334            ("https://wasmer.wtf/", "https://wasmer.wtf/graphql"),
335            ("https://wasmer.wtf", "https://wasmer.wtf/graphql"),
336            // local development
337            (
338                "http://localhost:8000/graphql",
339                "http://localhost:8000/graphql",
340            ),
341            ("localhost:8000", "http://localhost:8000/graphql"),
342        ];
343
344        for (input, expected) in inputs {
345            let url = format_graphql(input);
346            assert_eq!(url, expected);
347        }
348    }
349}