use std::fs;
use std::path::{Path, PathBuf};
use prost::Message;
use crate::daemon_registration::protocol::{BrokerIsolation, ServiceDefinition};
use crate::daemon_registration::validation::{validate_service_name, validate_version};
pub use crate::daemon_registration_common::service_definition::{
ensure_service_definition_dir, service_definition_dir, ServiceDefinitionError,
SERVICE_DEF_DIR_ENV,
};
pub const SERVICE_DEF_EXTENSION: &str = "servicedef";
#[derive(Clone, Debug)]
pub struct ServiceDefinitionLoader {
root: PathBuf,
}
impl ServiceDefinitionLoader {
pub fn new(root: impl Into<PathBuf>) -> Self {
Self { root: root.into() }
}
pub fn default_root() -> Self {
Self::new(service_definition_dir())
}
pub fn root(&self) -> &Path {
&self.root
}
pub fn load(&self, service_name: &str) -> Result<ServiceDefinition, ServiceDefinitionError> {
ensure_loadable_service_definition_dir(&self.root)?;
let path = service_definition_path(&self.root, service_name)?;
let bytes = fs::read(&path)?;
let definition = ServiceDefinition::decode(bytes.as_slice())?;
validate_service_definition_for_service(&definition, service_name)?;
Ok(definition)
}
pub fn reload(&self, service_name: &str) -> Result<ServiceDefinition, ServiceDefinitionError> {
self.load(service_name)
}
pub fn lookup_or_reload(
&self,
service_name: &str,
) -> Result<ServiceDefinition, ServiceDefinitionError> {
self.load(service_name)
}
}
pub fn write_service_definition(
root: &Path,
definition: &ServiceDefinition,
) -> Result<PathBuf, ServiceDefinitionError> {
ensure_service_definition_dir(root)?;
validate_service_definition_for_service(definition, &definition.service_name)?;
let path = service_definition_path(root, &definition.service_name)?;
fs::write(&path, definition.encode_to_vec())?;
Ok(path)
}
pub fn service_definition_path(
root: &Path,
service_name: &str,
) -> Result<PathBuf, ServiceDefinitionError> {
validate_service_name(service_name)?;
Ok(root.join(format!("{service_name}.{SERVICE_DEF_EXTENSION}")))
}
pub fn validate_service_definition_for_service(
definition: &ServiceDefinition,
expected_service: &str,
) -> Result<(), ServiceDefinitionError> {
validate_service_name(expected_service)?;
validate_service_name(&definition.service_name)?;
if definition.service_name != expected_service {
return Err(ServiceDefinitionError::ServiceNameMismatch {
requested: expected_service.into(),
actual: definition.service_name.clone(),
});
}
validate_absolute_path("binary_path", &definition.binary_path)?;
if !definition.per_version_binary_dir.is_empty() {
validate_absolute_path("per_version_binary_dir", &definition.per_version_binary_dir)?;
}
if !definition.min_version.is_empty() {
validate_version(&definition.min_version)?;
}
for version in &definition.version_allow_list {
validate_version(version)?;
}
match BrokerIsolation::try_from(definition.isolation) {
Ok(BrokerIsolation::PrivateBroker) | Ok(BrokerIsolation::SharedBroker) => {
if !definition.explicit_instance.is_empty() {
return Err(ServiceDefinitionError::InvalidIsolation {
reason: "explicit_instance must be empty unless isolation is EXPLICIT_INSTANCE",
});
}
}
Ok(BrokerIsolation::ExplicitInstance) => {
if definition.explicit_instance.is_empty() {
return Err(ServiceDefinitionError::InvalidIsolation {
reason: "EXPLICIT_INSTANCE requires explicit_instance",
});
}
validate_service_name(&definition.explicit_instance)?;
}
Err(_) => {
return Err(ServiceDefinitionError::InvalidIsolation {
reason: "unknown BrokerIsolation value",
});
}
}
Ok(())
}
fn ensure_loadable_service_definition_dir(path: &Path) -> Result<(), ServiceDefinitionError> {
if !crate::daemon_registration_common::secure_dir::private_dir_permissions_are_private(path)? {
return Err(ServiceDefinitionError::InsecureDirectory(
path.to_path_buf(),
));
}
Ok(())
}
fn validate_absolute_path(field: &'static str, value: &str) -> Result<(), ServiceDefinitionError> {
if value.is_empty() {
return Err(ServiceDefinitionError::InvalidPath {
field,
path: value.into(),
reason: "must not be empty",
});
}
if !Path::new(value).is_absolute() {
return Err(ServiceDefinitionError::InvalidPath {
field,
path: value.into(),
reason: "must be absolute",
});
}
Ok(())
}