1use crate::label;
5
6#[derive(Clone, Debug, Eq, PartialEq)]
19pub struct Name {
20 name: String,
21 container_name: ociman::ContainerName,
22}
23
24impl Name {
25 pub const OCI_PREFIX: &'static str = "pg-ephemeral-session-";
28
29 #[must_use]
31 pub fn as_str(&self) -> &str {
32 &self.name
33 }
34
35 #[must_use]
37 pub fn container_name(&self) -> &ociman::ContainerName {
38 &self.container_name
39 }
40}
41
42impl std::str::FromStr for Name {
43 type Err = ociman::ContainerNameError;
44
45 fn from_str(value: &str) -> Result<Self, Self::Err> {
46 let _: ociman::ContainerName = value.parse()?;
50 let container_name: ociman::ContainerName =
51 format!("{}{value}", Self::OCI_PREFIX).parse()?;
52 Ok(Self {
53 name: value.to_owned(),
54 container_name,
55 })
56 }
57}
58
59impl std::fmt::Display for Name {
60 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
61 formatter.write_str(&self.name)
62 }
63}
64
65#[derive(Debug)]
70pub struct Session {
71 container: ociman::Container,
72 name: Name,
73}
74
75impl Session {
76 #[must_use]
78 pub fn name(&self) -> &Name {
79 &self.name
80 }
81
82 #[must_use]
84 pub fn container(&self) -> &ociman::Container {
85 &self.container
86 }
87
88 #[must_use]
90 pub fn into_ociman_container(self) -> ociman::Container {
91 self.container
92 }
93
94 pub async fn metadata(&self) -> Result<crate::label::Metadata, MetadataError> {
99 let labels = self.container.labels().await?;
100 Ok(crate::label::read_container(&labels)?)
101 }
102
103 pub async fn stop(mut self) -> Result<(), StopError> {
109 self.container
110 .remove_force()
111 .await
112 .map_err(StopError::Remove)
113 }
114
115 pub async fn list(backend: &ociman::Backend) -> Result<Vec<Self>, ListError> {
122 Self::list_filtered(backend, None).await
123 }
124
125 pub async fn find(backend: &ociman::Backend, name: &Name) -> Result<Option<Self>, FindError> {
132 let value: ociman::label::Value = name.as_str().to_string().try_into().unwrap();
136 let mut sessions = Self::list_filtered(backend, Some(&value))
137 .await
138 .map_err(FindError::List)?;
139 match sessions.len() {
140 0 => Ok(None),
141 1 => Ok(Some(sessions.pop().unwrap())),
142 count => Err(FindError::MultipleMatches { count }),
143 }
144 }
145
146 async fn list_filtered(
147 backend: &ociman::Backend,
148 value: Option<&ociman::label::Value>,
149 ) -> Result<Vec<Self>, ListError> {
150 let key = label::SESSION_KEY;
151 let filter = match value {
152 None => ociman::label::Filter::key_only(&key),
153 Some(value) => ociman::label::Filter::exact(&key, value),
154 };
155 let entries = backend
156 .container_list_with_name([filter])
157 .await
158 .map_err(ListError::ListWithName)?;
159
160 entries
161 .into_iter()
162 .map(|(container, container_name)| {
163 let raw = container_name
164 .as_str()
165 .strip_prefix(Name::OCI_PREFIX)
166 .ok_or(ListError::MissingOciPrefix {
167 container_name: container_name.clone(),
168 })?;
169 let name: Name = raw.parse().map_err(ListError::InvalidSessionName)?;
170 Ok(Self { container, name })
171 })
172 .collect()
173 }
174}
175
176#[derive(Debug, thiserror::Error)]
177pub enum StopError {
178 #[error("failed to remove session container")]
179 Remove(#[source] cmd_proc::CommandError),
180}
181
182#[derive(Debug, thiserror::Error)]
183pub enum FindError {
184 #[error(transparent)]
185 List(#[from] ListError),
186 #[error("multiple containers ({count}) carry the same session label value")]
192 MultipleMatches { count: usize },
193}
194
195#[derive(Debug, thiserror::Error)]
196pub enum MetadataError {
197 #[error("failed to read session container labels")]
198 ReadLabels(#[from] ociman::label::ContainerError),
199 #[error("failed to decode pg-ephemeral metadata from session labels")]
200 Decode(#[from] crate::label::ReadError),
201}
202
203#[derive(Clone, Debug, PartialEq)]
206pub enum SeedStatus {
207 Sync,
210 Diverged,
214}
215
216impl std::fmt::Display for SeedStatus {
217 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
218 match self {
219 Self::Sync => formatter.write_str("sync"),
220 Self::Diverged => formatter.write_str("diverged"),
221 }
222 }
223}
224
225#[must_use]
232pub fn compute_seed_status(
233 stored_image: &ociman::image::Reference,
234 stored_seeds: &[crate::label::SeedEntry],
235 current_image: &crate::image::Image,
236 current_seeds: &crate::seed::LoadedSeeds<'_>,
237) -> SeedStatus {
238 let current_image_reference = ociman::image::Reference::from(current_image);
239 if stored_image != ¤t_image_reference {
240 return SeedStatus::Diverged;
241 }
242 let current: Vec<_> = current_seeds.iter_seeds().collect();
243 if stored_seeds.len() != current.len() {
244 return SeedStatus::Diverged;
245 }
246 for (stored_entry, current_seed) in stored_seeds.iter().zip(current.iter()) {
247 if &stored_entry.name != current_seed.name() {
248 return SeedStatus::Diverged;
249 }
250 if stored_entry.hash.as_ref() != current_seed.cache_status().hash() {
251 return SeedStatus::Diverged;
252 }
253 }
254 SeedStatus::Sync
255}
256
257#[derive(Debug, thiserror::Error)]
258pub enum ListError {
259 #[error("failed to list session containers")]
260 ListWithName(#[source] ociman::backend::ContainerListWithNameError),
261 #[error(
266 "container {container_name} matched session label filter but name does not start with {:?}",
267 Name::OCI_PREFIX
268 )]
269 MissingOciPrefix {
270 container_name: ociman::ContainerName,
271 },
272 #[error("session container name suffix is not a valid session name")]
273 InvalidSessionName(#[source] ociman::ContainerNameError),
274}
275
276#[cfg(test)]
277mod tests {
278 use super::*;
279
280 #[test]
281 fn name_derives_prefixed_oci_name() {
282 let session: Name = "foo".parse().unwrap();
283 assert_eq!(session.as_str(), "foo");
284 assert_eq!(session.to_string(), "foo");
285 assert_eq!(
286 session.container_name().as_str(),
287 "pg-ephemeral-session-foo"
288 );
289 }
290
291 #[test]
292 fn name_rejects_invalid_user_facing() {
293 assert!("".parse::<Name>().is_err());
295 assert!("-foo".parse::<Name>().is_err());
297 }
298}