use std::path::{Path, PathBuf};
use prost::Message as _;
use crate::broker::lifecycle::names::validate_service_name;
use crate::broker::secure_dir;
use crate::broker::server::service_def_loader::{
ensure_service_definition_dir, service_definition_dir, ServiceDefinitionError,
};
use super::{BrokerIsolation, ServiceDefinition};
pub const SERVICE_DEF_V2_EXTENSION: &str = "servicedef.v2";
#[must_use]
pub fn service_definition_dir_v2() -> PathBuf {
service_definition_dir()
}
pub fn service_definition_path_v2(
root: &Path,
service_name: &str,
) -> Result<PathBuf, ServiceDefinitionError> {
validate_service_name(service_name)?;
Ok(root.join(format!("{service_name}.{SERVICE_DEF_V2_EXTENSION}")))
}
pub fn write_service_definition_v2(
root: &Path,
definition: &ServiceDefinition,
) -> Result<PathBuf, ServiceDefinitionError> {
ensure_service_definition_dir(root)?;
let path = service_definition_path_v2(root, &definition.service_name)?;
std::fs::write(&path, definition.encode_to_vec())?;
Ok(path)
}
#[derive(Debug, Clone)]
pub struct ServiceDefinitionBuilder {
definition: ServiceDefinition,
}
impl ServiceDefinitionBuilder {
#[must_use]
pub fn shared_broker(service_name: impl Into<String>, binary_path: impl Into<String>) -> Self {
Self {
definition: ServiceDefinition {
service_name: service_name.into(),
binary_path: binary_path.into(),
isolation: BrokerIsolation::SharedBroker as i32,
..Default::default()
},
}
}
#[must_use]
pub fn private_broker(service_name: impl Into<String>, binary_path: impl Into<String>) -> Self {
Self {
definition: ServiceDefinition {
service_name: service_name.into(),
binary_path: binary_path.into(),
isolation: BrokerIsolation::PrivateBroker as i32,
..Default::default()
},
}
}
#[must_use]
pub fn explicit_instance(
service_name: impl Into<String>,
binary_path: impl Into<String>,
instance: impl Into<String>,
) -> Self {
Self {
definition: ServiceDefinition {
service_name: service_name.into(),
binary_path: binary_path.into(),
isolation: BrokerIsolation::ExplicitInstance as i32,
explicit_instance: instance.into(),
..Default::default()
},
}
}
#[must_use]
pub fn per_version_binary_dir(mut self, dir: impl Into<String>) -> Self {
self.definition.per_version_binary_dir = dir.into();
self
}
#[must_use]
pub fn min_version(mut self, version: impl Into<String>) -> Self {
self.definition.min_version = version.into();
self
}
#[must_use]
pub fn version_allow_list<I, S>(mut self, versions: I) -> Self
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
self.definition.version_allow_list = versions.into_iter().map(Into::into).collect();
self
}
#[must_use]
pub fn label(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.definition.labels.insert(key.into(), value.into());
self
}
#[must_use]
pub fn build(self) -> ServiceDefinition {
self.definition
}
pub fn install_in(self, root: &Path) -> Result<PathBuf, ServiceDefinitionError> {
write_service_definition_v2(root, &self.build())
}
pub fn install(self) -> Result<PathBuf, ServiceDefinitionError> {
let root = service_definition_dir_v2();
secure_dir::ensure_private_dir(&root)?;
self.install_in(&root)
}
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::tempdir;
#[test]
fn extension_is_servicedef_v2() {
assert_eq!(SERVICE_DEF_V2_EXTENSION, "servicedef.v2");
}
#[test]
fn service_definition_path_v2_uses_v2_extension() {
let root = Path::new("/svc");
let path = service_definition_path_v2(root, "zccache").unwrap();
assert_eq!(
path.to_str().unwrap().replace('\\', "/"),
"/svc/zccache.servicedef.v2"
);
}
#[test]
fn service_definition_path_v2_rejects_invalid_name() {
let root = Path::new("/svc");
assert!(service_definition_path_v2(root, "ZCCACHE").is_err());
assert!(service_definition_path_v2(root, "").is_err());
assert!(service_definition_path_v2(root, "a/b").is_err());
}
#[test]
fn shared_broker_builder_sets_expected_fields() {
let def = ServiceDefinitionBuilder::shared_broker("zccache", "/usr/bin/zccache").build();
assert_eq!(def.service_name, "zccache");
assert_eq!(def.binary_path, "/usr/bin/zccache");
assert_eq!(def.isolation, BrokerIsolation::SharedBroker as i32);
assert!(def.explicit_instance.is_empty());
}
#[test]
fn private_broker_builder_sets_expected_fields() {
let def = ServiceDefinitionBuilder::private_broker("svc", "/bin/x").build();
assert_eq!(def.isolation, BrokerIsolation::PrivateBroker as i32);
}
#[test]
fn explicit_instance_builder_sets_expected_fields() {
let def =
ServiceDefinitionBuilder::explicit_instance("svc", "/bin/x", "ci-trusted").build();
assert_eq!(def.isolation, BrokerIsolation::ExplicitInstance as i32);
assert_eq!(def.explicit_instance, "ci-trusted");
}
#[test]
fn builder_chain_propagates_optional_fields() {
let def = ServiceDefinitionBuilder::shared_broker("svc", "/bin/x")
.per_version_binary_dir("/usr/local/bin")
.min_version("1.2.3")
.version_allow_list(["1.2.3", "1.3.0"])
.label("env", "prod")
.label("region", "us-west")
.build();
assert_eq!(def.per_version_binary_dir, "/usr/local/bin");
assert_eq!(def.min_version, "1.2.3");
assert_eq!(def.version_allow_list, vec!["1.2.3", "1.3.0"]);
assert_eq!(def.labels.get("env"), Some(&"prod".to_owned()));
assert_eq!(def.labels.get("region"), Some(&"us-west".to_owned()));
}
#[test]
fn install_in_writes_and_decodes_round_trip() {
let dir = tempdir().expect("tempdir");
let path = ServiceDefinitionBuilder::shared_broker("zccache", "/usr/bin/zccache")
.min_version("1.0.0")
.label("env", "prod")
.install_in(dir.path())
.expect("install_in");
assert_eq!(
path.file_name().and_then(|s| s.to_str()),
Some("zccache.servicedef.v2")
);
let bytes = std::fs::read(&path).expect("read file");
let decoded = ServiceDefinition::decode(bytes.as_slice()).expect("decode");
assert_eq!(decoded.service_name, "zccache");
assert_eq!(decoded.binary_path, "/usr/bin/zccache");
assert_eq!(decoded.isolation, BrokerIsolation::SharedBroker as i32);
assert_eq!(decoded.min_version, "1.0.0");
assert_eq!(decoded.labels.get("env"), Some(&"prod".to_owned()));
}
#[test]
fn write_service_definition_v2_rejects_invalid_name() {
let dir = tempdir().expect("tempdir");
let bad = ServiceDefinition {
service_name: "BAD-Caps".to_owned(),
..Default::default()
};
let err = write_service_definition_v2(dir.path(), &bad).expect_err("must reject");
let _ = err;
}
#[test]
fn write_service_definition_v2_creates_parent_dir() {
let dir = tempdir().expect("tempdir");
let nested = dir.path().join("nested");
let path = ServiceDefinitionBuilder::shared_broker("svc", "/bin/x")
.install_in(&nested)
.expect("install_in into nested");
assert!(path.exists());
assert!(nested.exists());
}
#[test]
fn builder_install_round_trip_preserves_every_field() {
let dir = tempdir().expect("tempdir");
let path = ServiceDefinitionBuilder::explicit_instance("svc", "/bin/x", "ci-trusted")
.per_version_binary_dir("/usr/local/bin")
.min_version("1.0.0")
.version_allow_list(["1.0.0", "1.1.0"])
.label("env", "prod")
.label("rollout", "blue")
.install_in(dir.path())
.expect("install_in");
let bytes = std::fs::read(&path).expect("read");
let decoded = ServiceDefinition::decode(bytes.as_slice()).expect("decode");
assert_eq!(decoded.service_name, "svc");
assert_eq!(decoded.binary_path, "/bin/x");
assert_eq!(decoded.isolation, BrokerIsolation::ExplicitInstance as i32);
assert_eq!(decoded.explicit_instance, "ci-trusted");
assert_eq!(decoded.per_version_binary_dir, "/usr/local/bin");
assert_eq!(decoded.min_version, "1.0.0");
assert_eq!(decoded.version_allow_list, vec!["1.0.0", "1.1.0"]);
assert_eq!(decoded.labels.len(), 2);
assert_eq!(decoded.labels.get("env"), Some(&"prod".to_owned()));
assert_eq!(decoded.labels.get("rollout"), Some(&"blue".to_owned()));
}
}