runner_manager_platform/
coordinator.rs1use std::path::PathBuf;
16
17use chrono::{DateTime, Utc};
18use serde::{Deserialize, Serialize};
19
20use crate::paths::AppPaths;
21use crate::service::{ServiceError, read_state_record, remove_state_record, write_state_record};
22
23pub const CONFIG_FILE: &str = "coordinator.toml";
25
26pub const LINK_FILE: &str = "coordinator-link.toml";
28
29const CONFIG_SCHEMA_VERSION: u32 = 1;
30const LINK_SCHEMA_VERSION: u32 = 1;
31
32#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
34pub struct CoordinatorConfig {
35 pub schema_version: u32,
36 pub url: String,
38 pub host_id: String,
40 pub name: String,
42 pub joined_at: DateTime<Utc>,
43}
44
45impl CoordinatorConfig {
46 #[must_use]
47 pub const fn new(url: String, host_id: String, name: String, joined_at: DateTime<Utc>) -> Self {
48 Self {
49 schema_version: CONFIG_SCHEMA_VERSION,
50 url,
51 host_id,
52 name,
53 joined_at,
54 }
55 }
56}
57
58fn config_path(paths: &AppPaths) -> PathBuf {
59 paths.config_dir().join(CONFIG_FILE)
60}
61
62fn link_path(paths: &AppPaths) -> PathBuf {
63 paths.state_dir().join(LINK_FILE)
64}
65
66pub fn configured(paths: &AppPaths) -> Result<Option<CoordinatorConfig>, ServiceError> {
71 read_state_record(&config_path(paths))
72}
73
74pub fn save(paths: &AppPaths, config: &CoordinatorConfig) -> Result<(), ServiceError> {
79 write_state_record(&config_path(paths), config)
80}
81
82pub fn forget(paths: &AppPaths) -> Result<(), ServiceError> {
87 remove_state_record(&config_path(paths))?;
88 remove_state_record(&link_path(paths))
89}
90
91#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
95#[serde(default)]
96pub struct LinkRecord {
97 pub schema_version: u32,
98 pub written_at: DateTime<Utc>,
99 pub connected: bool,
100 pub since: Option<DateTime<Utc>>,
102 pub reason: Option<String>,
105 pub last_wake_at: Option<DateTime<Utc>>,
106 pub wakes_received: u64,
108 pub reconcile_interval_secs: Option<u32>,
111 pub covered: Vec<String>,
114}
115
116impl LinkRecord {
117 #[must_use]
118 pub fn new(written_at: DateTime<Utc>) -> Self {
119 Self {
120 schema_version: LINK_SCHEMA_VERSION,
121 written_at,
122 ..Self::default()
123 }
124 }
125}
126
127pub fn record_link(paths: &AppPaths, link: &LinkRecord) -> Result<(), ServiceError> {
132 write_state_record(&link_path(paths), link)
133}
134
135pub fn last_link(paths: &AppPaths) -> Result<Option<LinkRecord>, ServiceError> {
140 read_state_record(&link_path(paths))
141}
142
143#[cfg(test)]
144mod tests {
145 use super::*;
146
147 #[test]
148 fn the_setting_and_the_link_round_trip_and_leave_removes_both() {
149 let dir = tempfile::tempdir().expect("a temporary directory");
150 let paths = AppPaths::rooted_at(dir.path());
151 assert_eq!(configured(&paths).expect("absent"), None);
152 let config = CoordinatorConfig::new(
153 "https://runners.example.com".into(),
154 "h-1".into(),
155 "win-big".into(),
156 Utc::now(),
157 );
158 save(&paths, &config).expect("written");
159 assert_eq!(configured(&paths).expect("read"), Some(config));
160
161 let mut link = LinkRecord::new(Utc::now());
162 link.connected = true;
163 link.wakes_received = 3;
164 record_link(&paths, &link).expect("written");
165 assert_eq!(last_link(&paths).expect("read"), Some(link));
166
167 forget(&paths).expect("removed");
168 assert_eq!(configured(&paths).expect("absent"), None);
169 assert_eq!(last_link(&paths).expect("absent"), None);
170 forget(&paths).expect("removing nothing is fine");
171 }
172}