spacetimedb/
config.rs

1use std::path::Path;
2use std::{fmt, io};
3
4use spacetimedb_lib::ConnectionId;
5use spacetimedb_paths::cli::{ConfigDir, PrivKeyPath, PubKeyPath};
6use spacetimedb_paths::server::{ConfigToml, MetadataTomlPath};
7
8/// Parse a TOML file at the given path, returning `None` if the file does not exist.
9///
10/// **WARNING**: Comments and formatting in the file will be lost.
11pub fn parse_config<T: serde::de::DeserializeOwned>(path: &Path) -> anyhow::Result<Option<T>> {
12    match std::fs::read_to_string(path) {
13        Ok(contents) => Ok(Some(toml::from_str(&contents)?)),
14        Err(e) if e.kind() == io::ErrorKind::NotFound => Ok(None),
15        Err(e) => Err(e.into()),
16    }
17}
18
19#[derive(serde::Serialize, serde::Deserialize, Debug)]
20pub struct MetadataFile {
21    pub version: semver::Version,
22    pub edition: String,
23
24    #[serde(skip_serializing_if = "Option::is_none")]
25    /// Unused and always `None` in SpacetimeDB-standalone,
26    /// but used by SpacetimeDB-cloud.
27    pub client_connection_id: Option<ConnectionId>,
28}
29
30impl MetadataFile {
31    pub fn new(edition: &str) -> Self {
32        let mut current_version: semver::Version = env!("CARGO_PKG_VERSION").parse().unwrap();
33        // set the patch version of newly-created metadata files to 0 -- v1.0.0
34        // set `cmp.patch = Some(file_version.patch)` when checking version
35        // compatibility, meaning it won't be forwards-compatible with a
36        // database claiming to be created on v1.0.1, even though that should
37        // work. This can be changed once we release v1.1.0, since we don't
38        // care about its DBs being backwards-compatible with v1.0.0 anyway.
39        if let semver::Version { major: 1, minor: 0, .. } = current_version {
40            current_version.patch = 0;
41        }
42        Self {
43            version: current_version,
44            edition: edition.to_owned(),
45            client_connection_id: None,
46        }
47    }
48
49    pub fn read(path: &MetadataTomlPath) -> anyhow::Result<Option<Self>> {
50        parse_config(path.as_ref())
51    }
52
53    pub fn write(&self, path: &MetadataTomlPath) -> io::Result<()> {
54        path.write(self.to_string())
55    }
56
57    /// Check if this meta file is compatible with the default meta
58    /// file of a just-started database, and if so return the metadata
59    /// to write back to the file.
60    ///
61    /// `self` is the metadata file read from a database, and current is
62    /// the default metadata file that the active database version would
63    /// right to a new database.
64    pub fn check_compatibility_and_update(mut self, current: Self) -> anyhow::Result<Self> {
65        anyhow::ensure!(
66            self.edition == current.edition,
67            "metadata.toml indicates that this database is from a different \
68             edition of SpacetimeDB (running {:?}, but this database is {:?})",
69            current.edition,
70            self.edition,
71        );
72        let cmp = semver::Comparator {
73            op: semver::Op::Caret,
74            major: self.version.major,
75            minor: Some(self.version.minor),
76            patch: None,
77            pre: self.version.pre.clone(),
78        };
79        anyhow::ensure!(
80            cmp.matches(&current.version),
81            "metadata.toml indicates that this database is from a newer, \
82             incompatible version of SpacetimeDB (running {:?}, but this \
83             database is from {:?})",
84            current.version,
85            self.version,
86        );
87        // bump the version in the file only if it's being run in a newer
88        // database -- this won't do anything until we release v1.1.0, since we
89        // set current.version.patch to 0 in Self::new() due to a bug in v1.0.0
90        self.version = std::cmp::max(self.version, current.version);
91        Ok(self)
92    }
93}
94
95impl fmt::Display for MetadataFile {
96    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
97        writeln!(f, "# THIS FILE IS GENERATED BY SPACETIMEDB, DO NOT MODIFY!")?;
98        writeln!(f)?;
99        f.write_str(&toml::to_string(self).unwrap())
100    }
101}
102
103#[derive(serde::Deserialize, Default)]
104#[serde(rename_all = "kebab-case")]
105pub struct ConfigFile {
106    #[serde(default)]
107    pub certificate_authority: Option<CertificateAuthority>,
108    #[serde(default)]
109    pub logs: LogConfig,
110}
111
112impl ConfigFile {
113    pub fn read(path: &ConfigToml) -> anyhow::Result<Option<Self>> {
114        parse_config(path.as_ref())
115    }
116}
117
118#[derive(serde::Deserialize)]
119#[serde(rename_all = "kebab-case")]
120pub struct CertificateAuthority {
121    pub jwt_priv_key_path: PrivKeyPath,
122    pub jwt_pub_key_path: PubKeyPath,
123}
124
125impl CertificateAuthority {
126    pub fn in_cli_config_dir(dir: &ConfigDir) -> Self {
127        Self {
128            jwt_priv_key_path: dir.jwt_priv_key(),
129            jwt_pub_key_path: dir.jwt_pub_key(),
130        }
131    }
132
133    pub fn get_or_create_keys(&self) -> anyhow::Result<crate::auth::JwtKeys> {
134        crate::auth::get_or_create_keys(self)
135    }
136}
137
138#[serde_with::serde_as]
139#[derive(Clone, serde::Deserialize, Default)]
140#[serde(rename_all = "kebab-case")]
141pub struct LogConfig {
142    #[serde_as(as = "Option<serde_with::DisplayFromStr>")]
143    pub level: Option<tracing_core::LevelFilter>,
144    #[serde(default)]
145    pub directives: Vec<String>,
146}
147
148#[cfg(test)]
149mod tests {
150    use super::*;
151
152    fn mkver(major: u64, minor: u64, patch: u64) -> semver::Version {
153        semver::Version::new(major, minor, patch)
154    }
155
156    fn mkmeta(major: u64, minor: u64, patch: u64) -> MetadataFile {
157        MetadataFile {
158            version: mkver(major, minor, patch),
159            edition: "standalone".to_owned(),
160            client_connection_id: None,
161        }
162    }
163
164    #[test]
165    fn check_metadata_compatibility_checking() {
166        assert_eq!(
167            mkmeta(1, 0, 0)
168                .check_compatibility_and_update(mkmeta(1, 0, 1))
169                .unwrap()
170                .version,
171            mkver(1, 0, 1)
172        );
173        assert_eq!(
174            mkmeta(1, 0, 1)
175                .check_compatibility_and_update(mkmeta(1, 0, 0))
176                .unwrap()
177                .version,
178            mkver(1, 0, 1)
179        );
180
181        mkmeta(1, 1, 0)
182            .check_compatibility_and_update(mkmeta(1, 0, 5))
183            .unwrap_err();
184        mkmeta(2, 0, 0)
185            .check_compatibility_and_update(mkmeta(1, 3, 5))
186            .unwrap_err();
187    }
188}