running_process/broker/protocol_v2/
manifest_io.rs1use std::fs;
24use std::path::{Path, PathBuf};
25use std::time::{SystemTime, UNIX_EPOCH};
26
27use prost::Message as _;
28
29use super::super::manifest::ManifestError;
30use super::super::secure_dir;
31use super::{CacheManifest, CacheRoot, CacheRootKind};
32
33pub const ROOT_MANIFEST_FILE_V2: &str = ".running-process-manifest.v2.pb";
37
38pub const CENTRAL_MANIFEST_EXTENSION_V2: &str = "v2.pb";
41
42pub const BROKER_ENVELOPE_VERSION_V2: &str = "v2";
46
47#[must_use]
57pub fn central_registry_dir_v2() -> PathBuf {
58 super::super::manifest::central_registry_dir()
59}
60
61#[derive(Debug, Clone)]
65pub struct CacheManifestBuilder {
66 manifest: CacheManifest,
67}
68
69impl CacheManifestBuilder {
70 #[must_use]
76 pub fn new(service_name: impl Into<String>, service_version: impl Into<String>) -> Self {
77 let now = now_unix_ms();
78 Self {
79 manifest: CacheManifest {
80 service_name: service_name.into(),
81 service_version: service_version.into(),
82 broker_envelope_version: BROKER_ENVELOPE_VERSION_V2.to_owned(),
83 created_at_unix_ms: now,
84 last_active_unix_ms: now,
85 ..Default::default()
86 },
87 }
88 }
89
90 #[must_use]
92 pub fn root(mut self, kind: CacheRootKind, path: impl Into<String>) -> Self {
93 self.manifest.roots.push(CacheRoot {
94 kind: kind as i32,
95 path: path.into(),
96 });
97 self
98 }
99
100 #[must_use]
103 pub fn broker_instance(mut self, instance: impl Into<String>) -> Self {
104 self.manifest.broker_instance = instance.into();
105 self
106 }
107
108 #[must_use]
110 pub fn bundle_id(mut self, bundle_id: impl Into<String>) -> Self {
111 self.manifest.bundle_id = bundle_id.into();
112 self
113 }
114
115 #[must_use]
117 pub fn build(self) -> CacheManifest {
118 self.manifest
119 }
120
121 pub fn publish(self) -> Result<PathBuf, ManifestError> {
128 let manifest = self.build();
129 write_to_central_v2(&manifest.service_name, &manifest.service_version, &manifest)
130 }
131
132 pub fn publish_in(self, registry_dir: &Path) -> Result<PathBuf, ManifestError> {
139 let manifest = self.build();
140 write_to_central_in_dir_v2(
141 registry_dir,
142 &manifest.service_name,
143 &manifest.service_version,
144 &manifest,
145 )
146 }
147}
148
149pub fn write_to_root_v2(cache_root: &Path, manifest: &CacheManifest) -> Result<(), ManifestError> {
157 fs::create_dir_all(cache_root)?;
158 secure_dir::ensure_private_dir(cache_root)?;
159 let target = cache_root.join(ROOT_MANIFEST_FILE_V2);
160 write_manifest_file_v2(&target, manifest)
161}
162
163pub fn write_to_central_v2(
171 service_name: &str,
172 version: &str,
173 manifest: &CacheManifest,
174) -> Result<PathBuf, ManifestError> {
175 let dir = central_registry_dir_v2();
176 write_to_central_in_dir_v2(&dir, service_name, version, manifest)
177}
178
179pub fn write_to_central_in_dir_v2(
186 registry_dir: &Path,
187 service_name: &str,
188 version: &str,
189 manifest: &CacheManifest,
190) -> Result<PathBuf, ManifestError> {
191 super::super::manifest::ensure_central_registry_dir(registry_dir)?;
192 let target = central_manifest_path_v2(registry_dir, service_name, version)?;
193 write_manifest_file_v2(&target, manifest)?;
194 Ok(target)
195}
196
197pub fn central_manifest_path_v2(
206 registry_dir: &Path,
207 service_name: &str,
208 version: &str,
209) -> Result<PathBuf, ManifestError> {
210 super::super::manifest::central_manifest_path(registry_dir, service_name, version).map(
212 |v1_path| {
213 let stem = v1_path.file_stem().and_then(|s| s.to_str()).unwrap_or("");
216 registry_dir.join(format!("{stem}.{CENTRAL_MANIFEST_EXTENSION_V2}"))
217 },
218 )
219}
220
221fn write_manifest_file_v2(target: &Path, manifest: &CacheManifest) -> Result<(), ManifestError> {
222 let bytes = manifest.encode_to_vec();
223 super::super::manifest::write_atomic(target, &bytes)
224}
225
226fn now_unix_ms() -> u64 {
227 SystemTime::now()
228 .duration_since(UNIX_EPOCH)
229 .map(|d| d.as_millis() as u64)
230 .unwrap_or(0)
231}
232
233#[cfg(test)]
234mod tests {
235 use super::*;
236 use tempfile::tempdir;
237
238 #[test]
239 fn root_manifest_filename_is_v2() {
240 assert_eq!(ROOT_MANIFEST_FILE_V2, ".running-process-manifest.v2.pb");
241 }
242
243 #[test]
244 fn central_extension_is_v2_pb() {
245 assert_eq!(CENTRAL_MANIFEST_EXTENSION_V2, "v2.pb");
246 }
247
248 #[test]
249 fn envelope_version_is_v2() {
250 assert_eq!(BROKER_ENVELOPE_VERSION_V2, "v2");
251 }
252
253 #[test]
254 fn builder_new_populates_required_fields() {
255 let manifest = CacheManifestBuilder::new("svc", "1.0.0").build();
256 assert_eq!(manifest.service_name, "svc");
257 assert_eq!(manifest.service_version, "1.0.0");
258 assert_eq!(manifest.broker_envelope_version, "v2");
259 assert!(manifest.created_at_unix_ms > 0);
260 assert_eq!(manifest.created_at_unix_ms, manifest.last_active_unix_ms);
261 assert!(manifest.roots.is_empty());
262 }
263
264 #[test]
265 fn builder_root_appends_in_order() {
266 let manifest = CacheManifestBuilder::new("svc", "1.0.0")
267 .root(CacheRootKind::CacheData, "/var/cache/svc")
268 .root(CacheRootKind::CacheIndex, "/var/cache/svc/index")
269 .root(CacheRootKind::CacheLogs, "/var/log/svc")
270 .root(CacheRootKind::CacheLocks, "/var/cache/svc/locks")
271 .build();
272 assert_eq!(manifest.roots.len(), 4);
273 assert_eq!(manifest.roots[0].kind, CacheRootKind::CacheData as i32);
274 assert_eq!(manifest.roots[0].path, "/var/cache/svc");
275 assert_eq!(manifest.roots[1].kind, CacheRootKind::CacheIndex as i32);
276 assert_eq!(manifest.roots[2].kind, CacheRootKind::CacheLogs as i32);
277 assert_eq!(manifest.roots[3].kind, CacheRootKind::CacheLocks as i32);
278 }
279
280 #[test]
286 fn cache_root_kind_wire_values_mirror_v1() {
287 assert_eq!(CacheRootKind::Unspecified as i32, 0);
288 assert_eq!(CacheRootKind::CacheData as i32, 1);
289 assert_eq!(CacheRootKind::CacheLogs as i32, 2);
290 assert_eq!(CacheRootKind::CacheLocks as i32, 3);
291 assert_eq!(CacheRootKind::CacheRuntime as i32, 4);
292 assert_eq!(CacheRootKind::CacheTmp as i32, 5);
293 assert_eq!(CacheRootKind::CacheConfig as i32, 6);
294 assert_eq!(CacheRootKind::CacheIndex as i32, 7);
295 assert_eq!(CacheRootKind::CacheJournal as i32, 8);
296 assert_eq!(CacheRootKind::CacheSecrets as i32, 9);
297 }
298
299 #[test]
300 fn builder_broker_instance_and_bundle_id_round_trip() {
301 let manifest = CacheManifestBuilder::new("svc", "1.0.0")
302 .broker_instance("ci-trusted")
303 .bundle_id("bundle-42")
304 .build();
305 assert_eq!(manifest.broker_instance, "ci-trusted");
306 assert_eq!(manifest.bundle_id, "bundle-42");
307 }
308
309 #[test]
310 fn write_to_root_v2_writes_to_canonical_filename() {
311 let dir = tempdir().expect("tempdir");
312 let manifest = CacheManifestBuilder::new("svc", "1.0.0")
313 .root(CacheRootKind::CacheData, "/path/to/data")
314 .build();
315 write_to_root_v2(dir.path(), &manifest).expect("write_to_root_v2");
316
317 let written = dir.path().join(ROOT_MANIFEST_FILE_V2);
318 assert!(written.exists(), "v2 manifest file must exist");
319
320 let bytes = fs::read(&written).expect("read");
321 let decoded = CacheManifest::decode(bytes.as_slice()).expect("decode");
322 assert_eq!(decoded.service_name, "svc");
323 assert_eq!(decoded.roots.len(), 1);
324 assert_eq!(decoded.roots[0].path, "/path/to/data");
325 }
326
327 #[test]
328 fn publish_in_writes_to_central_with_v2_extension() {
329 let dir = tempdir().expect("tempdir");
330 let path = CacheManifestBuilder::new("svc", "1.2.3")
331 .root(CacheRootKind::CacheData, "/path")
332 .publish_in(dir.path())
333 .expect("publish_in");
334
335 assert_eq!(
336 path.file_name().and_then(|s| s.to_str()),
337 Some("svc-1.2.3.v2.pb")
338 );
339 let bytes = fs::read(&path).expect("read");
340 let decoded = CacheManifest::decode(bytes.as_slice()).expect("decode");
341 assert_eq!(decoded.service_name, "svc");
342 assert_eq!(decoded.service_version, "1.2.3");
343 }
344
345 #[test]
346 fn publish_in_rejects_invalid_service_name() {
347 let dir = tempdir().expect("tempdir");
348 let manifest = CacheManifest {
349 service_name: "BAD-Caps".to_owned(),
350 service_version: "1.0.0".to_owned(),
351 ..Default::default()
352 };
353 let err = write_to_central_in_dir_v2(dir.path(), "BAD-Caps", "1.0.0", &manifest)
354 .expect_err("must reject");
355 let _ = err;
356 }
357
358 #[test]
362 fn builder_publish_round_trip_preserves_every_field() {
363 let dir = tempdir().expect("tempdir");
364 let path = CacheManifestBuilder::new("zccache", "1.12.9")
365 .root(CacheRootKind::CacheData, "/var/cache/zccache/data")
366 .root(CacheRootKind::CacheIndex, "/var/cache/zccache/index")
367 .root(CacheRootKind::CacheLogs, "/var/log/zccache")
368 .broker_instance("shared")
369 .bundle_id("zccache-bundle-v1")
370 .publish_in(dir.path())
371 .expect("publish_in");
372
373 let bytes = fs::read(&path).expect("read");
374 let decoded = CacheManifest::decode(bytes.as_slice()).expect("decode");
375 assert_eq!(decoded.service_name, "zccache");
376 assert_eq!(decoded.service_version, "1.12.9");
377 assert_eq!(decoded.broker_envelope_version, "v2");
378 assert_eq!(decoded.roots.len(), 3);
379 assert_eq!(decoded.broker_instance, "shared");
380 assert_eq!(decoded.bundle_id, "zccache-bundle-v1");
381 assert!(decoded.created_at_unix_ms > 0);
382 }
383
384 #[test]
388 fn v2_central_filename_does_not_collide_with_v1() {
389 let dir = tempdir().expect("tempdir");
390 let v1_path = dir.path().join("zccache-1.12.9.pb");
391 let v2_path = central_manifest_path_v2(dir.path(), "zccache", "1.12.9").unwrap();
392 assert_ne!(v1_path, v2_path);
393 assert_eq!(
394 v2_path.file_name().and_then(|s| s.to_str()),
395 Some("zccache-1.12.9.v2.pb")
396 );
397 }
398}