greentic_operator/
runtime_state.rs1use 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}