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
use std::path::{Path, PathBuf};

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

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

pub type DeployClientConfig = DeployClientConfigV1;

impl DeployClientConfigV1 {
    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 DeployClientConfigV1 {
    fn default() -> Self {
        Self {
            ssh_token: None,
            version: 1,
        }
    }
}

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

pub struct LoadedClientConfig {
    pub config: DeployClientConfig,
    pub path: PathBuf,
}

impl LoadedClientConfig {
    pub fn set_ssh_token(&mut self, token: String) -> Result<(), anyhow::Error> {
        self.config.ssh_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<LoadedClientConfig, 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(LoadedClientConfig { config, path });
                }
                Err(err) => {
                    eprintln!(
                        "WARNING: failed to load config file at '{}': {}",
                        path.display(),
                        err
                    );
                }
            }
        }
    }

    Ok(LoadedClientConfig {
        config: DeployClientConfig::default(),
        path: default_path,
    })
}

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