use std::fs;
use std::path::{Path, PathBuf};
use std::time::{SystemTime, UNIX_EPOCH};
use prost::Message as _;
use super::super::manifest::ManifestError;
use super::super::secure_dir;
use super::{CacheManifest, CacheRoot, CacheRootKind};
pub const ROOT_MANIFEST_FILE_V2: &str = ".running-process-manifest.v2.pb";
pub const CENTRAL_MANIFEST_EXTENSION_V2: &str = "v2.pb";
pub const BROKER_ENVELOPE_VERSION_V2: &str = "v2";
#[must_use]
pub fn central_registry_dir_v2() -> PathBuf {
super::super::manifest::central_registry_dir()
}
#[derive(Debug, Clone)]
pub struct CacheManifestBuilder {
manifest: CacheManifest,
}
impl CacheManifestBuilder {
#[must_use]
pub fn new(service_name: impl Into<String>, service_version: impl Into<String>) -> Self {
let now = now_unix_ms();
Self {
manifest: CacheManifest {
service_name: service_name.into(),
service_version: service_version.into(),
broker_envelope_version: BROKER_ENVELOPE_VERSION_V2.to_owned(),
created_at_unix_ms: now,
last_active_unix_ms: now,
..Default::default()
},
}
}
#[must_use]
pub fn root(mut self, kind: CacheRootKind, path: impl Into<String>) -> Self {
self.manifest.roots.push(CacheRoot {
kind: kind as i32,
path: path.into(),
});
self
}
#[must_use]
pub fn broker_instance(mut self, instance: impl Into<String>) -> Self {
self.manifest.broker_instance = instance.into();
self
}
#[must_use]
pub fn bundle_id(mut self, bundle_id: impl Into<String>) -> Self {
self.manifest.bundle_id = bundle_id.into();
self
}
#[must_use]
pub fn build(self) -> CacheManifest {
self.manifest
}
pub fn publish(self) -> Result<PathBuf, ManifestError> {
let manifest = self.build();
write_to_central_v2(&manifest.service_name, &manifest.service_version, &manifest)
}
pub fn publish_in(self, registry_dir: &Path) -> Result<PathBuf, ManifestError> {
let manifest = self.build();
write_to_central_in_dir_v2(
registry_dir,
&manifest.service_name,
&manifest.service_version,
&manifest,
)
}
}
pub fn write_to_root_v2(cache_root: &Path, manifest: &CacheManifest) -> Result<(), ManifestError> {
fs::create_dir_all(cache_root)?;
secure_dir::ensure_private_dir(cache_root)?;
let target = cache_root.join(ROOT_MANIFEST_FILE_V2);
write_manifest_file_v2(&target, manifest)
}
pub fn write_to_central_v2(
service_name: &str,
version: &str,
manifest: &CacheManifest,
) -> Result<PathBuf, ManifestError> {
let dir = central_registry_dir_v2();
write_to_central_in_dir_v2(&dir, service_name, version, manifest)
}
pub fn write_to_central_in_dir_v2(
registry_dir: &Path,
service_name: &str,
version: &str,
manifest: &CacheManifest,
) -> Result<PathBuf, ManifestError> {
super::super::manifest::ensure_central_registry_dir(registry_dir)?;
let target = central_manifest_path_v2(registry_dir, service_name, version)?;
write_manifest_file_v2(&target, manifest)?;
Ok(target)
}
pub fn central_manifest_path_v2(
registry_dir: &Path,
service_name: &str,
version: &str,
) -> Result<PathBuf, ManifestError> {
super::super::manifest::central_manifest_path(registry_dir, service_name, version).map(
|v1_path| {
let stem = v1_path.file_stem().and_then(|s| s.to_str()).unwrap_or("");
registry_dir.join(format!("{stem}.{CENTRAL_MANIFEST_EXTENSION_V2}"))
},
)
}
fn write_manifest_file_v2(target: &Path, manifest: &CacheManifest) -> Result<(), ManifestError> {
let bytes = manifest.encode_to_vec();
super::super::manifest::write_atomic(target, &bytes)
}
fn now_unix_ms() -> u64 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_millis() as u64)
.unwrap_or(0)
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::tempdir;
#[test]
fn root_manifest_filename_is_v2() {
assert_eq!(ROOT_MANIFEST_FILE_V2, ".running-process-manifest.v2.pb");
}
#[test]
fn central_extension_is_v2_pb() {
assert_eq!(CENTRAL_MANIFEST_EXTENSION_V2, "v2.pb");
}
#[test]
fn envelope_version_is_v2() {
assert_eq!(BROKER_ENVELOPE_VERSION_V2, "v2");
}
#[test]
fn builder_new_populates_required_fields() {
let manifest = CacheManifestBuilder::new("svc", "1.0.0").build();
assert_eq!(manifest.service_name, "svc");
assert_eq!(manifest.service_version, "1.0.0");
assert_eq!(manifest.broker_envelope_version, "v2");
assert!(manifest.created_at_unix_ms > 0);
assert_eq!(manifest.created_at_unix_ms, manifest.last_active_unix_ms);
assert!(manifest.roots.is_empty());
}
#[test]
fn builder_root_appends_in_order() {
let manifest = CacheManifestBuilder::new("svc", "1.0.0")
.root(CacheRootKind::CacheData, "/var/cache/svc")
.root(CacheRootKind::CacheIndex, "/var/cache/svc/index")
.root(CacheRootKind::CacheLogs, "/var/log/svc")
.root(CacheRootKind::CacheLocks, "/var/cache/svc/locks")
.build();
assert_eq!(manifest.roots.len(), 4);
assert_eq!(manifest.roots[0].kind, CacheRootKind::CacheData as i32);
assert_eq!(manifest.roots[0].path, "/var/cache/svc");
assert_eq!(manifest.roots[1].kind, CacheRootKind::CacheIndex as i32);
assert_eq!(manifest.roots[2].kind, CacheRootKind::CacheLogs as i32);
assert_eq!(manifest.roots[3].kind, CacheRootKind::CacheLocks as i32);
}
#[test]
fn cache_root_kind_wire_values_mirror_v1() {
assert_eq!(CacheRootKind::Unspecified as i32, 0);
assert_eq!(CacheRootKind::CacheData as i32, 1);
assert_eq!(CacheRootKind::CacheLogs as i32, 2);
assert_eq!(CacheRootKind::CacheLocks as i32, 3);
assert_eq!(CacheRootKind::CacheRuntime as i32, 4);
assert_eq!(CacheRootKind::CacheTmp as i32, 5);
assert_eq!(CacheRootKind::CacheConfig as i32, 6);
assert_eq!(CacheRootKind::CacheIndex as i32, 7);
assert_eq!(CacheRootKind::CacheJournal as i32, 8);
assert_eq!(CacheRootKind::CacheSecrets as i32, 9);
}
#[test]
fn builder_broker_instance_and_bundle_id_round_trip() {
let manifest = CacheManifestBuilder::new("svc", "1.0.0")
.broker_instance("ci-trusted")
.bundle_id("bundle-42")
.build();
assert_eq!(manifest.broker_instance, "ci-trusted");
assert_eq!(manifest.bundle_id, "bundle-42");
}
#[test]
fn write_to_root_v2_writes_to_canonical_filename() {
let dir = tempdir().expect("tempdir");
let manifest = CacheManifestBuilder::new("svc", "1.0.0")
.root(CacheRootKind::CacheData, "/path/to/data")
.build();
write_to_root_v2(dir.path(), &manifest).expect("write_to_root_v2");
let written = dir.path().join(ROOT_MANIFEST_FILE_V2);
assert!(written.exists(), "v2 manifest file must exist");
let bytes = fs::read(&written).expect("read");
let decoded = CacheManifest::decode(bytes.as_slice()).expect("decode");
assert_eq!(decoded.service_name, "svc");
assert_eq!(decoded.roots.len(), 1);
assert_eq!(decoded.roots[0].path, "/path/to/data");
}
#[test]
fn publish_in_writes_to_central_with_v2_extension() {
let dir = tempdir().expect("tempdir");
let path = CacheManifestBuilder::new("svc", "1.2.3")
.root(CacheRootKind::CacheData, "/path")
.publish_in(dir.path())
.expect("publish_in");
assert_eq!(
path.file_name().and_then(|s| s.to_str()),
Some("svc-1.2.3.v2.pb")
);
let bytes = fs::read(&path).expect("read");
let decoded = CacheManifest::decode(bytes.as_slice()).expect("decode");
assert_eq!(decoded.service_name, "svc");
assert_eq!(decoded.service_version, "1.2.3");
}
#[test]
fn publish_in_rejects_invalid_service_name() {
let dir = tempdir().expect("tempdir");
let manifest = CacheManifest {
service_name: "BAD-Caps".to_owned(),
service_version: "1.0.0".to_owned(),
..Default::default()
};
let err = write_to_central_in_dir_v2(dir.path(), "BAD-Caps", "1.0.0", &manifest)
.expect_err("must reject");
let _ = err;
}
#[test]
fn builder_publish_round_trip_preserves_every_field() {
let dir = tempdir().expect("tempdir");
let path = CacheManifestBuilder::new("zccache", "1.12.9")
.root(CacheRootKind::CacheData, "/var/cache/zccache/data")
.root(CacheRootKind::CacheIndex, "/var/cache/zccache/index")
.root(CacheRootKind::CacheLogs, "/var/log/zccache")
.broker_instance("shared")
.bundle_id("zccache-bundle-v1")
.publish_in(dir.path())
.expect("publish_in");
let bytes = fs::read(&path).expect("read");
let decoded = CacheManifest::decode(bytes.as_slice()).expect("decode");
assert_eq!(decoded.service_name, "zccache");
assert_eq!(decoded.service_version, "1.12.9");
assert_eq!(decoded.broker_envelope_version, "v2");
assert_eq!(decoded.roots.len(), 3);
assert_eq!(decoded.broker_instance, "shared");
assert_eq!(decoded.bundle_id, "zccache-bundle-v1");
assert!(decoded.created_at_unix_ms > 0);
}
#[test]
fn v2_central_filename_does_not_collide_with_v1() {
let dir = tempdir().expect("tempdir");
let v1_path = dir.path().join("zccache-1.12.9.pb");
let v2_path = central_manifest_path_v2(dir.path(), "zccache", "1.12.9").unwrap();
assert_ne!(v1_path, v2_path);
assert_eq!(
v2_path.file_name().and_then(|s| s.to_str()),
Some("zccache-1.12.9.v2.pb")
);
}
}