use std::error::Error;
use std::fmt;
use std::path::{Path, PathBuf};
use running_process::daemon_registration_v2 as backend;
#[derive(Debug)]
pub enum DaemonRegistrationV2Error {
Io(std::io::Error),
InvalidName {
detail: String,
},
InsecureDirectory {
path: PathBuf,
},
InvalidDefinition {
detail: String,
},
}
impl fmt::Display for DaemonRegistrationV2Error {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Io(error) => write!(formatter, "daemon registration v2 I/O failed: {error}"),
Self::InvalidName { detail } => {
write!(formatter, "invalid frozen v2 service name: {detail}")
}
Self::InsecureDirectory { path } => write!(
formatter,
"v2 service-definition directory is not owner-private: {}",
path.display()
),
Self::InvalidDefinition { detail } => {
write!(formatter, "invalid frozen v2 service definition: {detail}")
}
}
}
}
impl Error for DaemonRegistrationV2Error {
fn source(&self) -> Option<&(dyn Error + 'static)> {
match self {
Self::Io(error) => Some(error),
_ => None,
}
}
}
fn service_error(error: backend::ServiceDefinitionError) -> DaemonRegistrationV2Error {
match error {
backend::ServiceDefinitionError::Io(error) => DaemonRegistrationV2Error::Io(error),
backend::ServiceDefinitionError::InvalidName(error) => {
DaemonRegistrationV2Error::InvalidName {
detail: error.to_string(),
}
}
backend::ServiceDefinitionError::InsecureDirectory(path) => {
DaemonRegistrationV2Error::InsecureDirectory { path }
}
other => DaemonRegistrationV2Error::InvalidDefinition {
detail: other.to_string(),
},
}
}
#[must_use]
pub fn service_definition_directory() -> PathBuf {
backend::service_definition_dir_v2()
}
pub fn service_definition_path(
root: impl AsRef<Path>,
service_name: impl AsRef<str>,
) -> Result<PathBuf, DaemonRegistrationV2Error> {
backend::service_definition_path_v2(root.as_ref(), service_name.as_ref()).map_err(service_error)
}
pub fn write_service_definition(
root: impl AsRef<Path>,
definition: &ServiceDefinition,
) -> Result<PathBuf, DaemonRegistrationV2Error> {
backend::write_service_definition_v2(root.as_ref(), &definition.inner).map_err(service_error)
}
#[derive(Clone, Debug)]
pub struct ServiceDefinitionBuilder {
service_name: String,
binary_path: String,
per_version_binary_dir: Option<String>,
min_version: Option<String>,
allowed_versions: Vec<String>,
labels: Vec<(String, String)>,
}
impl ServiceDefinitionBuilder {
#[must_use]
pub fn shared_broker(service_name: impl Into<String>, binary_path: impl Into<String>) -> Self {
Self {
service_name: service_name.into(),
binary_path: binary_path.into(),
per_version_binary_dir: None,
min_version: None,
allowed_versions: Vec::new(),
labels: Vec::new(),
}
}
#[must_use]
pub fn per_version_binary_dir(mut self, directory: impl Into<String>) -> Self {
self.per_version_binary_dir = Some(directory.into());
self
}
#[must_use]
pub fn min_version(mut self, version: impl Into<String>) -> Self {
self.min_version = Some(version.into());
self
}
#[must_use]
pub fn allow_version(mut self, version: impl Into<String>) -> Self {
self.allowed_versions.push(version.into());
self
}
#[must_use]
pub fn label(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.labels.push((key.into(), value.into()));
self
}
#[must_use]
pub fn build(self) -> ServiceDefinition {
let mut inner =
backend::ServiceDefinitionBuilder::shared_broker(self.service_name, self.binary_path);
if let Some(directory) = self.per_version_binary_dir {
inner = inner.per_version_binary_dir(directory);
}
if let Some(version) = self.min_version {
inner = inner.min_version(version);
}
inner = inner.version_allow_list(self.allowed_versions);
for (key, value) in self.labels {
inner = inner.label(key, value);
}
ServiceDefinition {
inner: inner.build(),
}
}
pub fn install(self) -> Result<PathBuf, DaemonRegistrationV2Error> {
let root = service_definition_directory();
self.install_in(root)
}
pub fn install_in(self, root: impl AsRef<Path>) -> Result<PathBuf, DaemonRegistrationV2Error> {
write_service_definition(root, &self.build())
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct ServiceDefinition {
inner: backend::ServiceDefinition,
}
impl ServiceDefinition {
#[must_use]
pub fn service_name(&self) -> &str {
&self.inner.service_name
}
#[must_use]
pub fn binary_path(&self) -> &str {
&self.inner.binary_path
}
#[must_use]
pub fn is_shared_broker(&self) -> bool {
self.inner.isolation == backend::BrokerIsolation::SharedBroker as i32
}
#[must_use]
pub fn per_version_binary_dir(&self) -> &str {
&self.inner.per_version_binary_dir
}
#[must_use]
pub fn min_version(&self) -> &str {
&self.inner.min_version
}
pub fn allowed_versions(&self) -> impl ExactSizeIterator<Item = &str> {
self.inner.version_allow_list.iter().map(String::as_str)
}
#[must_use]
pub fn label(&self, key: &str) -> Option<&str> {
self.inner.labels.get(key).map(String::as_str)
}
pub fn labels(&self) -> impl ExactSizeIterator<Item = (&str, &str)> {
self.inner
.labels
.iter()
.map(|(key, value)| (key.as_str(), value.as_str()))
}
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::tempdir;
#[test]
fn facade_and_backend_keep_same_object_write_bytes_in_one_process() {
let definition = ServiceDefinitionBuilder::shared_broker("service", "/bin/service")
.per_version_binary_dir("/bin")
.min_version("1.2.3")
.allow_version("1.2.3")
.allow_version("1.2.4")
.label("vendor", "zackees")
.label("package", "fixture")
.build();
let facade_root = tempdir().expect("facade root");
let backend_root = tempdir().expect("backend root");
let facade_path =
write_service_definition(facade_root.path(), &definition).expect("facade write");
let backend_path =
backend::write_service_definition_v2(backend_root.path(), &definition.inner)
.expect("backend write");
assert_eq!(
std::fs::read(facade_path).expect("facade bytes"),
std::fs::read(backend_path).expect("backend bytes"),
"the comparison intentionally uses the same object in one process; labels remain a HashMap and are not a canonical byte promise"
);
}
}