use std::{collections::BTreeMap, error::Error, fmt};
use boxferry_model::{Identifier, ProvenanceKind, ResourceOwnership, Sourced};
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
#[non_exhaustive]
pub enum RuntimeResourceKind {
Network,
Volume,
ServiceGroup,
}
impl fmt::Display for RuntimeResourceKind {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(match self {
Self::Network => "network",
Self::Volume => "volume",
Self::ServiceGroup => "service group",
})
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
#[non_exhaustive]
pub enum RuntimeResolutionError {
UnsupportedOwnership {
kind: RuntimeResourceKind,
name: String,
},
MissingUserOverride {
kind: RuntimeResourceKind,
name: String,
},
Duplicate {
kind: RuntimeResourceKind,
name: String,
},
}
impl fmt::Display for RuntimeResolutionError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::UnsupportedOwnership { kind, name } => write!(
formatter,
"runtime {kind} `{name}` must be resolved as application-owned or external"
),
Self::MissingUserOverride { kind, name } => write!(
formatter,
"runtime {kind} `{name}` resolution requires user-override provenance"
),
Self::Duplicate { kind, name } => {
write!(formatter, "runtime {kind} `{name}` already has a lifecycle resolution")
}
}
}
}
impl Error for RuntimeResolutionError {}
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct RuntimeResolutions {
networks: BTreeMap<Identifier, Sourced<ResourceOwnership>>,
volumes: BTreeMap<Identifier, Sourced<ResourceOwnership>>,
service_groups: BTreeMap<Identifier, Sourced<ResourceOwnership>>,
}
impl RuntimeResolutions {
#[must_use]
pub const fn new() -> Self {
Self {
networks: BTreeMap::new(),
volumes: BTreeMap::new(),
service_groups: BTreeMap::new(),
}
}
pub fn set_network_ownership(
&mut self,
name: Identifier,
ownership: Sourced<ResourceOwnership>,
) -> Result<(), RuntimeResolutionError> {
insert_resolution(&mut self.networks, RuntimeResourceKind::Network, name, ownership)
}
pub fn set_volume_ownership(
&mut self,
name: Identifier,
ownership: Sourced<ResourceOwnership>,
) -> Result<(), RuntimeResolutionError> {
insert_resolution(&mut self.volumes, RuntimeResourceKind::Volume, name, ownership)
}
pub fn set_service_group_ownership(
&mut self,
name: Identifier,
ownership: Sourced<ResourceOwnership>,
) -> Result<(), RuntimeResolutionError> {
insert_resolution(
&mut self.service_groups,
RuntimeResourceKind::ServiceGroup,
name,
ownership,
)
}
#[must_use]
pub fn network_ownership(&self, name: &Identifier) -> Option<&Sourced<ResourceOwnership>> {
self.networks.get(name)
}
#[must_use]
pub fn volume_ownership(&self, name: &Identifier) -> Option<&Sourced<ResourceOwnership>> {
self.volumes.get(name)
}
#[must_use]
pub fn service_group_ownership(&self, name: &Identifier) -> Option<&Sourced<ResourceOwnership>> {
self.service_groups.get(name)
}
}
fn insert_resolution(
resolutions: &mut BTreeMap<Identifier, Sourced<ResourceOwnership>>,
kind: RuntimeResourceKind,
name: Identifier,
ownership: Sourced<ResourceOwnership>,
) -> Result<(), RuntimeResolutionError> {
let error_name = name.as_str().to_owned();
if !matches!(
ownership.value(),
ResourceOwnership::Application | ResourceOwnership::External
) {
return Err(RuntimeResolutionError::UnsupportedOwnership { kind, name: error_name });
}
if !ownership
.origins()
.iter()
.any(|origin| origin.kind() == ProvenanceKind::UserOverride)
{
return Err(RuntimeResolutionError::MissingUserOverride { kind, name: error_name });
}
if resolutions.contains_key(&name) {
return Err(RuntimeResolutionError::Duplicate { kind, name: error_name });
}
resolutions.insert(name, ownership);
Ok(())
}
#[cfg(test)]
mod tests {
use boxferry_model::{Identifier, Provenance, ResourceOwnership, SourceId, Sourced};
use super::{RuntimeResolutionError, RuntimeResolutions, RuntimeResourceKind};
#[test]
fn requires_supported_ownership_and_explicit_user_provenance() -> Result<(), String> {
let name = Identifier::new("data").map_err(|error| error.to_string())?;
let source = SourceId::new("decision:runtime-lifecycle").map_err(|error| error.to_string())?;
let mut resolutions = RuntimeResolutions::new();
assert_eq!(
resolutions.set_volume_ownership(
name.clone(),
Sourced::from_source(ResourceOwnership::Uncertain, Provenance::user_override(source.clone())),
),
Err(RuntimeResolutionError::UnsupportedOwnership {
kind: RuntimeResourceKind::Volume,
name: "data".to_owned(),
})
);
assert_eq!(
resolutions.set_volume_ownership(
name.clone(),
Sourced::from_source(ResourceOwnership::Application, Provenance::runtime_observation(source)),
),
Err(RuntimeResolutionError::MissingUserOverride {
kind: RuntimeResourceKind::Volume,
name: "data".to_owned(),
})
);
Ok(())
}
#[test]
fn retains_one_resolution_per_exact_resource_name() -> Result<(), String> {
let name = Identifier::new("frontend").map_err(|error| error.to_string())?;
let source = SourceId::new("decision:runtime-lifecycle").map_err(|error| error.to_string())?;
let ownership = || Sourced::from_source(ResourceOwnership::External, Provenance::user_override(source.clone()));
let mut resolutions = RuntimeResolutions::new();
resolutions
.set_network_ownership(name.clone(), ownership())
.map_err(|error| error.to_string())?;
assert_eq!(
resolutions.set_network_ownership(name.clone(), ownership()),
Err(RuntimeResolutionError::Duplicate {
kind: RuntimeResourceKind::Network,
name: "frontend".to_owned(),
})
);
assert_eq!(
resolutions.network_ownership(&name).map(Sourced::value),
Some(&ResourceOwnership::External)
);
Ok(())
}
}