1use std::collections::{HashMap, HashSet};
2use std::sync::atomic::AtomicBool;
3use std::sync::{Arc, Mutex};
4
5pub struct VaultState {
7 pub(in crate::app) cert_cache: HashMap<
10 String,
11 (
12 std::time::Instant,
13 crate::vault_ssh::CertStatus,
14 Option<std::time::SystemTime>,
15 ),
16 >,
17 pub(in crate::app) cert_checks_in_flight: HashSet<String>,
19 pub(in crate::app) cleanup_warning: Option<String>,
21 pub(in crate::app) signing_cancel: Option<Arc<AtomicBool>>,
23 pub(in crate::app) sign_thread: Option<std::thread::JoinHandle<()>>,
25 pub(in crate::app) sign_in_flight: Arc<Mutex<HashSet<String>>>,
27 pub(in crate::app) pending_config_write: bool,
29}
30
31impl Default for VaultState {
32 fn default() -> Self {
33 Self {
34 cert_cache: HashMap::new(),
35 cert_checks_in_flight: HashSet::new(),
36 cleanup_warning: None,
37 signing_cancel: None,
38 sign_thread: None,
39 sign_in_flight: Arc::new(Mutex::new(HashSet::new())),
40 pending_config_write: false,
41 }
42 }
43}
44
45type CertCacheEntry = (
46 std::time::Instant,
47 crate::vault_ssh::CertStatus,
48 Option<std::time::SystemTime>,
49);
50
51impl VaultState {
52 pub fn cert_cache(&self) -> &HashMap<String, CertCacheEntry> {
53 &self.cert_cache
54 }
55
56 pub fn cert_entry(&self, alias: &str) -> Option<&CertCacheEntry> {
57 self.cert_cache.get(alias)
58 }
59
60 pub fn has_cert(&self, alias: &str) -> bool {
61 self.cert_cache.contains_key(alias)
62 }
63
64 pub fn insert_cert(&mut self, alias: String, entry: CertCacheEntry) {
65 self.cert_cache.insert(alias, entry);
66 }
67
68 pub fn remove_cert(&mut self, alias: &str) {
69 self.cert_cache.remove(alias);
70 }
71
72 pub fn clear_cert_cache(&mut self) {
73 self.cert_cache.clear();
74 }
75
76 pub fn is_cert_check_in_flight(&self, alias: &str) -> bool {
77 self.cert_checks_in_flight.contains(alias)
78 }
79
80 pub fn take_cleanup_warning(&mut self) -> Option<String> {
81 self.cleanup_warning.take()
82 }
83
84 pub fn signing_cancel(&self) -> Option<&Arc<AtomicBool>> {
85 self.signing_cancel.as_ref()
86 }
87
88 pub fn is_signing(&self) -> bool {
89 self.signing_cancel.is_some()
90 }
91
92 pub fn set_signing_cancel(&mut self, cancel: Arc<AtomicBool>) {
93 self.signing_cancel = Some(cancel);
94 }
95
96 pub fn clear_signing_cancel(&mut self) {
97 self.signing_cancel = None;
98 }
99
100 pub fn set_sign_thread(&mut self, handle: std::thread::JoinHandle<()>) {
101 self.sign_thread = Some(handle);
102 }
103
104 pub fn sign_in_flight(&self) -> &Arc<Mutex<HashSet<String>>> {
105 &self.sign_in_flight
106 }
107
108 pub fn pending_config_write(&self) -> bool {
109 self.pending_config_write
110 }
111
112 pub fn set_pending_config_write(&mut self, value: bool) {
113 self.pending_config_write = value;
114 }
115
116 pub(crate) fn mark_cert_check_started(&mut self, alias: String) {
120 self.cert_checks_in_flight.insert(alias);
121 }
122
123 pub(crate) fn record_cert_check(
128 &mut self,
129 alias: String,
130 status: crate::vault_ssh::CertStatus,
131 mtime: Option<std::time::SystemTime>,
132 ) {
133 self.cert_checks_in_flight.remove(&alias);
134 self.cert_cache
135 .insert(alias, (std::time::Instant::now(), status, mtime));
136 }
137
138 pub(crate) fn cancel_signing_run(&mut self) -> Option<std::thread::JoinHandle<()>> {
143 if let Some(ref cancel) = self.signing_cancel {
144 cancel.store(true, std::sync::atomic::Ordering::Relaxed);
145 }
146 self.signing_cancel = None;
147 self.sign_thread.take()
148 }
149
150 pub(crate) fn finalize_signing_run(&mut self) -> Option<std::thread::JoinHandle<()>> {
157 self.signing_cancel = None;
158 self.sign_thread.take()
159 }
160}
161
162#[cfg(test)]
163mod tests {
164 use super::*;
165 use std::sync::atomic::Ordering;
166
167 #[test]
168 fn mark_cert_check_started_inserts_alias() {
169 let mut v = VaultState::default();
170 v.mark_cert_check_started("web".to_string());
171 assert!(v.cert_checks_in_flight.contains("web"));
172 }
173
174 #[test]
175 fn mark_cert_check_started_is_idempotent() {
176 let mut v = VaultState::default();
179 v.mark_cert_check_started("web".to_string());
180 v.mark_cert_check_started("web".to_string());
181 assert_eq!(v.cert_checks_in_flight.len(), 1);
182 assert!(v.cert_checks_in_flight.contains("web"));
183 }
184
185 #[test]
186 fn record_cert_check_clears_in_flight_and_writes_cache() {
187 let mut v = VaultState::default();
188 v.mark_cert_check_started("web".to_string());
189 v.record_cert_check(
190 "web".to_string(),
191 crate::vault_ssh::CertStatus::Missing,
192 None,
193 );
194 assert!(!v.cert_checks_in_flight.contains("web"));
195 assert!(v.cert_cache.contains_key("web"));
196 let (_, status, mtime) = v.cert_cache.get("web").unwrap();
197 assert!(matches!(status, crate::vault_ssh::CertStatus::Missing));
198 assert!(mtime.is_none());
199 }
200
201 #[test]
202 fn record_cert_check_caches_even_without_prior_start() {
203 let mut v = VaultState::default();
208 v.record_cert_check(
209 "web".to_string(),
210 crate::vault_ssh::CertStatus::Invalid("nope".to_string()),
211 None,
212 );
213 assert!(v.cert_cache.contains_key("web"));
214 assert!(v.cert_checks_in_flight.is_empty());
215 }
216
217 #[test]
218 fn cancel_signing_run_with_no_active_run_returns_none() {
219 let mut v = VaultState::default();
220 let handle = v.cancel_signing_run();
221 assert!(handle.is_none());
222 assert!(v.signing_cancel.is_none());
223 assert!(v.sign_thread.is_none());
224 }
225
226 #[test]
227 fn cancel_signing_run_signals_cancel_and_clears_handle() {
228 let mut v = VaultState::default();
232 let cancel = Arc::new(AtomicBool::new(false));
233 v.signing_cancel = Some(cancel.clone());
234 v.sign_thread = Some(std::thread::spawn(|| {}));
235
236 let handle = v
237 .cancel_signing_run()
238 .expect("returned thread handle for joining");
239 let _ = handle.join();
240
241 assert!(
242 cancel.load(Ordering::Relaxed),
243 "cancel must be signalled so a long-running worker exits"
244 );
245 assert!(v.signing_cancel.is_none());
246 assert!(v.sign_thread.is_none());
247 }
248
249 #[test]
250 fn finalize_signing_run_does_not_signal_cancel() {
251 let mut v = VaultState::default();
257 let cancel = Arc::new(AtomicBool::new(false));
258 v.signing_cancel = Some(cancel.clone());
259 v.sign_thread = Some(std::thread::spawn(|| {}));
260
261 let handle = v
262 .finalize_signing_run()
263 .expect("returned thread handle for joining");
264 let _ = handle.join();
265
266 assert!(
267 !cancel.load(Ordering::Relaxed),
268 "finalize must not signal cancel: a racing newer run's Arc could be hit"
269 );
270 assert!(v.signing_cancel.is_none());
271 assert!(v.sign_thread.is_none());
272 }
273
274 #[test]
275 fn finalize_signing_run_with_cancel_but_no_thread_clears_cancel() {
276 let mut v = VaultState::default();
282 let cancel = Arc::new(AtomicBool::new(false));
283 v.signing_cancel = Some(cancel.clone());
284
285 let handle = v.finalize_signing_run();
286 assert!(handle.is_none());
287 assert!(v.signing_cancel.is_none());
288 assert!(!cancel.load(Ordering::Relaxed));
289 }
290}