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