Skip to main content

greentic_operator/
runtime_state.rs

1use std::path::{Path, PathBuf};
2
3use serde::de::DeserializeOwned;
4use serde::{Deserialize, Serialize};
5
6pub struct RuntimePaths {
7    state_dir: PathBuf,
8    log_root: PathBuf,
9    tenant: String,
10    team: String,
11}
12
13impl RuntimePaths {
14    pub fn new(
15        state_dir: impl Into<PathBuf>,
16        tenant: impl Into<String>,
17        team: impl Into<String>,
18    ) -> Self {
19        let state_dir = state_dir.into();
20        let log_root = state_dir
21            .parent()
22            .map(|parent| parent.to_path_buf())
23            .unwrap_or_else(|| PathBuf::from("."))
24            .join("logs");
25        Self {
26            state_dir,
27            log_root,
28            tenant: tenant.into(),
29            team: team.into(),
30        }
31    }
32
33    pub fn key(&self) -> String {
34        format!("{}.{}", self.tenant, self.team)
35    }
36
37    pub fn runtime_root(&self) -> PathBuf {
38        self.state_dir.join("runtime").join(self.key())
39    }
40
41    pub fn pids_dir(&self) -> PathBuf {
42        self.state_dir.join("pids").join(self.key())
43    }
44
45    pub fn logs_dir(&self) -> PathBuf {
46        self.log_root.join(self.key())
47    }
48
49    pub fn dlq_log_path(&self) -> PathBuf {
50        self.logs_dir().join("dlq.log")
51    }
52
53    pub fn resolved_dir(&self) -> PathBuf {
54        self.runtime_root().join("resolved")
55    }
56
57    pub fn pid_path(&self, service_id: &str) -> PathBuf {
58        self.pids_dir().join(format!("{service_id}.pid"))
59    }
60
61    pub fn log_path(&self, service_id: &str) -> PathBuf {
62        self.logs_dir().join(format!("{service_id}.log"))
63    }
64
65    pub fn resolved_path(&self, service_id: &str) -> PathBuf {
66        self.resolved_dir().join(format!("{service_id}.json"))
67    }
68
69    pub fn logs_root(&self) -> PathBuf {
70        self.log_root.clone()
71    }
72
73    pub fn service_manifest_path(&self) -> PathBuf {
74        self.runtime_root().join("services.json")
75    }
76}
77
78#[cfg(test)]
79mod tests {
80    use super::*;
81
82    #[test]
83    fn logs_dir_uses_bundle_logs() {
84        let paths = RuntimePaths::new("/tmp/bundle/state", "demo", "default");
85        assert_eq!(
86            paths.logs_dir(),
87            PathBuf::from("/tmp/bundle/logs").join("demo.default")
88        );
89    }
90}
91
92pub fn write_json<T: Serialize>(path: &Path, value: &T) -> anyhow::Result<()> {
93    let bytes = serde_json::to_vec_pretty(value)?;
94    atomic_write(path, &bytes)
95}
96
97pub fn read_json<T: DeserializeOwned>(path: &Path) -> anyhow::Result<Option<T>> {
98    if !path.exists() {
99        return Ok(None);
100    }
101    let data = std::fs::read(path)?;
102    let value = serde_json::from_slice(&data)?;
103    Ok(Some(value))
104}
105
106pub fn atomic_write(path: &Path, bytes: &[u8]) -> anyhow::Result<()> {
107    use std::io::Write;
108
109    if let Some(parent) = path.parent() {
110        std::fs::create_dir_all(parent)?;
111    }
112    let mut tmp = path.to_path_buf();
113    tmp.set_extension("tmp");
114    let mut file = std::fs::File::create(&tmp)?;
115    file.write_all(bytes)?;
116    file.sync_all()?;
117    std::fs::rename(&tmp, path)?;
118    if let Some(parent) = path.parent()
119        && let Ok(dir) = std::fs::File::open(parent)
120    {
121        let _ = dir.sync_all();
122    }
123    Ok(())
124}
125
126#[derive(Debug, Serialize, Deserialize, Default)]
127pub struct ServiceManifest {
128    #[serde(default)]
129    pub log_dir: Option<String>,
130    #[serde(default)]
131    pub services: Vec<ServiceEntry>,
132}
133
134#[derive(Debug, Serialize, Deserialize)]
135pub struct ServiceEntry {
136    pub id: String,
137    pub kind: String,
138    pub log_path: Option<String>,
139}
140
141impl ServiceEntry {
142    pub fn new(id: impl Into<String>, kind: impl Into<String>, log_path: Option<&Path>) -> Self {
143        Self {
144            id: id.into(),
145            kind: kind.into(),
146            log_path: log_path.map(|path| path.display().to_string()),
147        }
148    }
149}
150
151pub fn persist_service_manifest(
152    paths: &RuntimePaths,
153    manifest: &ServiceManifest,
154) -> anyhow::Result<()> {
155    write_json(&paths.service_manifest_path(), manifest)
156}
157
158pub fn read_service_manifest(paths: &RuntimePaths) -> anyhow::Result<Option<ServiceManifest>> {
159    read_json(&paths.service_manifest_path())
160}
161
162pub fn remove_service_manifest(paths: &RuntimePaths) -> anyhow::Result<()> {
163    let path = paths.service_manifest_path();
164    if path.exists() {
165        std::fs::remove_file(path)?;
166    }
167    Ok(())
168}