1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
use std::path::{Path, PathBuf};

use anyhow::{bail, Context};
use serde::{Deserialize, Serialize};

#[derive(Serialize, Deserialize, PartialEq, Eq, Clone, Debug)]
pub struct EdgeConfig {
    pub version: u32,
    /// Token used for ssh access.
    pub ssh_token: Option<String>,
    /// Token used for network access.
    pub network_token: Option<String>,
}

impl EdgeConfig {
    pub const VERSION: u32 = 1;

    pub fn from_slice(data: &[u8]) -> Result<Self, anyhow::Error> {
        let data_str = std::str::from_utf8(data)?;
        let value: toml::Value = toml::from_str(data_str).context("failed to parse config TOML")?;

        let version = value
            .get("version")
            .and_then(|v| v.as_integer())
            .context("invalid client config: no 'version' key found")?;

        if version != Self::VERSION as i64 {
            bail!(
                "Invalid client config: unknown config version '{}'",
                version
            );
        }

        let config = toml::from_str(data_str)?;
        Ok(config)
    }
}

impl Default for EdgeConfig {
    fn default() -> Self {
        Self {
            ssh_token: None,
            network_token: None,
            version: 1,
        }
    }
}

const CONFIG_FILE_NAME: &str = "deploy_client.toml";
const CONFIG_PATH_ENV_VAR: &str = "DEPLOY_CLIENT_CONFIG_PATH";

pub struct LoadedEdgeConfig {
    pub config: EdgeConfig,
    pub path: PathBuf,
}

impl LoadedEdgeConfig {
    pub fn set_ssh_token(&mut self, token: String) -> Result<(), anyhow::Error> {
        self.config.ssh_token = Some(token);
        self.save()?;
        Ok(())
    }

    #[allow(dead_code)]
    pub fn set_network_token(&mut self, token: String) -> Result<(), anyhow::Error> {
        self.config.network_token = Some(token);
        self.save()?;
        Ok(())
    }

    pub fn save(&self) -> Result<(), anyhow::Error> {
        let data = toml::to_string(&self.config)?;
        std::fs::write(&self.path, data)
            .with_context(|| format!("failed to write config to '{}'", self.path.display()))?;
        Ok(())
    }
}

pub fn default_config_path() -> Result<PathBuf, anyhow::Error> {
    if let Some(var) = std::env::var_os(CONFIG_PATH_ENV_VAR) {
        Ok(var.into())
    } else {
        // TODO: use dirs crate to determine the correct path.
        // (this also depends on general wasmer config moving there.)

        #[allow(deprecated)]
        let home = std::env::home_dir().context("failed to get home directory")?;
        let path = home.join(".wasmer").join(CONFIG_FILE_NAME);
        Ok(path)
    }
}

pub fn load_config(custom_path: Option<PathBuf>) -> Result<LoadedEdgeConfig, anyhow::Error> {
    let default_path = default_config_path()?;

    let path = if let Some(p) = custom_path {
        Some(p)
    } else if default_path.is_file() {
        Some(default_path.clone())
    } else {
        None
    };

    if let Some(path) = path {
        if path.is_file() {
            match try_load_config(&path) {
                Ok(config) => {
                    return Ok(LoadedEdgeConfig { config, path });
                }
                Err(err) => {
                    eprintln!(
                        "WARNING: failed to load config file at '{}': {}",
                        path.display(),
                        err
                    );
                }
            }
        }
    }

    Ok(LoadedEdgeConfig {
        config: EdgeConfig::default(),
        path: default_path,
    })
}

fn try_load_config(path: &Path) -> Result<EdgeConfig, anyhow::Error> {
    let data = std::fs::read(path)
        .with_context(|| format!("failed to read config file at '{}'", path.display()))?;
    let config = EdgeConfig::from_slice(&data)
        .with_context(|| format!("failed to parse config file at '{}'", path.display()))?;
    Ok(config)
}