use crate::label;
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct Name {
name: String,
container_name: ociman::ContainerName,
}
impl Name {
pub const OCI_PREFIX: &'static str = "pg-ephemeral-session-";
#[must_use]
pub fn as_str(&self) -> &str {
&self.name
}
#[must_use]
pub fn container_name(&self) -> &ociman::ContainerName {
&self.container_name
}
}
impl std::str::FromStr for Name {
type Err = ociman::ContainerNameError;
fn from_str(value: &str) -> Result<Self, Self::Err> {
let _: ociman::ContainerName = value.parse()?;
let container_name: ociman::ContainerName =
format!("{}{value}", Self::OCI_PREFIX).parse()?;
Ok(Self {
name: value.to_owned(),
container_name,
})
}
}
impl std::fmt::Display for Name {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter.write_str(&self.name)
}
}
#[derive(Debug)]
pub struct Session {
container: ociman::Container,
name: Name,
}
impl Session {
#[must_use]
pub fn name(&self) -> &Name {
&self.name
}
#[must_use]
pub fn container(&self) -> &ociman::Container {
&self.container
}
#[must_use]
pub fn into_ociman_container(self) -> ociman::Container {
self.container
}
pub async fn metadata(&self) -> Result<crate::label::Metadata, MetadataError> {
let labels = self.container.labels().await?;
Ok(crate::label::read_container(&labels)?)
}
pub async fn stop(mut self) -> Result<(), StopError> {
self.container
.remove_force()
.await
.map_err(StopError::Remove)
}
pub async fn list(backend: &ociman::Backend) -> Result<Vec<Self>, ListError> {
Self::list_filtered(backend, None).await
}
pub async fn find(backend: &ociman::Backend, name: &Name) -> Result<Option<Self>, FindError> {
let value: ociman::label::Value = name.as_str().to_string().try_into().unwrap();
let mut sessions = Self::list_filtered(backend, Some(&value))
.await
.map_err(FindError::List)?;
match sessions.len() {
0 => Ok(None),
1 => Ok(Some(sessions.pop().unwrap())),
count => Err(FindError::MultipleMatches { count }),
}
}
async fn list_filtered(
backend: &ociman::Backend,
value: Option<&ociman::label::Value>,
) -> Result<Vec<Self>, ListError> {
let key = label::SESSION_KEY;
let filter = match value {
None => ociman::label::Filter::key_only(&key),
Some(value) => ociman::label::Filter::exact(&key, value),
};
let entries = backend
.container_list_with_name([filter])
.await
.map_err(ListError::ListWithName)?;
entries
.into_iter()
.map(|(container, container_name)| {
let raw = container_name
.as_str()
.strip_prefix(Name::OCI_PREFIX)
.ok_or(ListError::MissingOciPrefix {
container_name: container_name.clone(),
})?;
let name: Name = raw.parse().map_err(ListError::InvalidSessionName)?;
Ok(Self { container, name })
})
.collect()
}
}
#[derive(Debug, thiserror::Error)]
pub enum StopError {
#[error("failed to remove session container")]
Remove(#[source] cmd_proc::CommandError),
}
#[derive(Debug, thiserror::Error)]
pub enum FindError {
#[error(transparent)]
List(#[from] ListError),
#[error("multiple containers ({count}) carry the same session label value")]
MultipleMatches { count: usize },
}
#[derive(Debug, thiserror::Error)]
pub enum MetadataError {
#[error("failed to read session container labels")]
ReadLabels(#[from] ociman::label::ContainerError),
#[error("failed to decode pg-ephemeral metadata from session labels")]
Decode(#[from] crate::label::ReadError),
}
#[derive(Clone, Debug, PartialEq)]
pub enum SeedStatus {
Sync,
Diverged,
}
impl std::fmt::Display for SeedStatus {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Sync => formatter.write_str("sync"),
Self::Diverged => formatter.write_str("diverged"),
}
}
}
#[must_use]
pub fn compute_seed_status(
stored_image: &ociman::image::Reference,
stored_seeds: &[crate::label::SeedEntry],
current_image: &crate::image::Image,
current_seeds: &crate::seed::LoadedSeeds<'_>,
) -> SeedStatus {
let current_image_reference = ociman::image::Reference::from(current_image);
if stored_image != ¤t_image_reference {
return SeedStatus::Diverged;
}
let current: Vec<_> = current_seeds.iter_seeds().collect();
if stored_seeds.len() != current.len() {
return SeedStatus::Diverged;
}
for (stored_entry, current_seed) in stored_seeds.iter().zip(current.iter()) {
if &stored_entry.name != current_seed.name() {
return SeedStatus::Diverged;
}
if stored_entry.hash.as_ref() != current_seed.cache_status().hash() {
return SeedStatus::Diverged;
}
}
SeedStatus::Sync
}
#[derive(Debug, thiserror::Error)]
pub enum ListError {
#[error("failed to list session containers")]
ListWithName(#[source] ociman::backend::ContainerListWithNameError),
#[error(
"container {container_name} matched session label filter but name does not start with {:?}",
Name::OCI_PREFIX
)]
MissingOciPrefix {
container_name: ociman::ContainerName,
},
#[error("session container name suffix is not a valid session name")]
InvalidSessionName(#[source] ociman::ContainerNameError),
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn name_derives_prefixed_oci_name() {
let session: Name = "foo".parse().unwrap();
assert_eq!(session.as_str(), "foo");
assert_eq!(session.to_string(), "foo");
assert_eq!(
session.container_name().as_str(),
"pg-ephemeral-session-foo"
);
}
#[test]
fn name_rejects_invalid_user_facing() {
assert!("".parse::<Name>().is_err());
assert!("-foo".parse::<Name>().is_err());
}
}