Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
85 changes: 85 additions & 0 deletions helios-oci/src/container.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
use std::collections::HashMap;

use bollard::{query_parameters::ListContainersOptions, secret::ContainerSummary};

use super::{Client, Error, Result};

#[derive(Debug, Clone)]
pub struct Container<'a>(&'a Client);

impl<'a> Container<'a> {
pub fn new(client: &'a Client) -> Self {
Self(client)
}
}

impl Container<'_> {
/// Returns an iterator on the list of images on the server.
///
/// Note that it uses a different, smaller representation of an image than
/// inspecting a single image.
pub async fn list_with_labels(&self, labels: Vec<&str>) -> Result<List> {
let mut filters = HashMap::new();
filters.insert(
"label".to_string(),
labels.into_iter().map(|s| s.to_owned()).collect(),
);

let opts = ListContainersOptions {
all: true,
filters: Some(filters),
..Default::default()
};

let res = self.0.inner().list_containers(Some(opts)).await;
let containers = res.map_err(Error::with_context("failed to list containers"))?;

Ok(List(containers))
}
}

// by ref in order to clone only what's necessary to build LocalImage.
impl<'a> From<&'a ContainerSummary> for LocalContainer {
fn from(value: &'a ContainerSummary) -> Self {
let id = value.id.clone().expect("container ID should not be nil");
let image_id = value
.image_id
.clone()
.expect("container image ID should not be nil");

let labels = value.labels.clone().unwrap_or_default();
Self {
id,
image_id,
labels,
}
}
}

#[derive(Debug, Clone)]
pub struct List(Vec<ContainerSummary>);

type ListItem = (String, LocalContainer);

impl List {
pub fn iter(&self) -> impl Iterator<Item = ListItem> + '_ {
self.0
.iter()
.flat_map(|container| container.names.iter().map(move |name| (name, container)))
.flat_map(|(names, container)| {
names.iter().map(|name| (name.clone(), container.into()))
})
}
}

#[derive(Debug, Clone)]
pub struct LocalContainer {
/// The engine id of the container
pub id: String,

/// The content-addressable image id
pub image_id: String,

/// User-defined key/value metadata.
pub labels: HashMap<String, String>,
}
11 changes: 10 additions & 1 deletion helios-oci/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,11 @@ pub use bollard::errors::Error as ConnectionError;
mod image;
pub use image::{Image, LocalImage};

mod container;
pub use container::{Container, LocalContainer};

mod models;
pub use models::{ImageUri, InvalidImageUriError};
pub use models::{ImageRef, ImageUri, InvalidImageUriError};

mod registry;
pub use registry::{RegistryAuth, RegistryAuthClient, RegistryAuthError};
Expand Down Expand Up @@ -49,6 +52,12 @@ impl Client {
pub fn image(&self) -> Image<'_> {
Image::new(self)
}

/// Exposes methods to work with containers
#[inline]
pub fn container(&self) -> Container<'_> {
Container::new(self)
}
}

#[derive(Debug, thiserror::Error)]
Expand Down
39 changes: 39 additions & 0 deletions helios-oci/src/models.rs
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,45 @@ impl<'de> Deserialize<'de> for ImageUri {
}
}

/// An image reference is either a image URI or a content addressable image ID
#[derive(Serialize, Debug, Clone, PartialEq, Eq)]
#[serde(untagged)]
pub enum ImageRef {
/// A URI reference
Uri(ImageUri),

/// A content addressable image id
Id(String),
}

impl ImageRef {
/// Convenience method to get the digest of an image ref
///
/// Returns None if the image is not a Uri or the Uri does not have a digest
pub fn digest(&self) -> &Option<String> {
if let Self::Uri(uri) = self {
uri.digest()
} else {
&None
}
}
}

impl<'de> Deserialize<'de> for ImageRef {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let s: String = String::deserialize(deserializer)?;
if s.starts_with("sha256:") {
return Ok(ImageRef::Id(s));
}

let uri: ImageUri = s.parse().map_err(serde::de::Error::custom)?;
Ok(ImageRef::Uri(uri))
}
}

#[cfg(test)]
mod tests {
use super::*;
Expand Down
1 change: 1 addition & 0 deletions helios-oci/src/registry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,7 @@ impl RegistryAuthClient {

/// Clear the client cache
pub fn clear(&mut self) {
debug!("clean up registry credentials");
self.cached.clear();
}
}
168 changes: 164 additions & 4 deletions helios-state/src/models/service.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,98 @@
use std::collections::BTreeMap;
use std::convert::Infallible;
use std::fmt::Display;
use std::str::FromStr;

use serde::{Deserialize, Serialize};

use crate::oci::ImageUri;
use crate::oci::ImageRef;
use crate::util::types::Uuid;

// We don't want to fail if the service is supervised but it doesn't have an app-uuid,
// this could mean the container was tampered with or it is leftover from an old version of the
// supervisor.
pub const UNKNOWN_APP_UUID: &str = "10c401";

// We don't want to fail if the service is supervised but it has the wrong
// container name, that just means that we need to rename it (or remove it)
// so we use a fake release uuid for this.
const UNKNOWN_RELEASE_UUID: &str = "10ca12e1ea5e";

pub struct ServiceContainerName {
pub service_name: String,
pub release_uuid: Uuid,
}

impl Display for ServiceContainerName {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}_{}", self.service_name, self.release_uuid)
}
}

impl From<(String, Uuid)> for ServiceContainerName {
fn from((service_name, release_uuid): (String, Uuid)) -> Self {
Self {
service_name,
release_uuid,
}
}
}

impl Serialize for ServiceContainerName {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
self.to_string().serialize(serializer)
}
}

impl<'de> Deserialize<'de> for ServiceContainerName {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let s: String = String::deserialize(deserializer)?;
let name: ServiceContainerName = s.parse().map_err(serde::de::Error::custom)?;
Ok(name)
}
}

impl FromStr for ServiceContainerName {
type Err = Infallible;

fn from_str(s: &str) -> Result<Self, Self::Err> {
let s = s.trim_start_matches('/');

assert!(!s.is_empty(), "container name cannot be empty");
let parts: Vec<&str> = s.split('_').collect();

if parts.len() < 2 {
return Ok(ServiceContainerName {
service_name: s.to_owned(),
release_uuid: UNKNOWN_RELEASE_UUID.into(),
});
}

let mut service_name = parts[0].to_owned();
let mut release_uuid = parts[parts.len() - 1].to_owned();

if service_name.is_empty() || release_uuid.is_empty() {
service_name = s.to_owned();
release_uuid = UNKNOWN_RELEASE_UUID.to_owned()
}

Ok(ServiceContainerName {
service_name,
release_uuid: release_uuid.into(),
})
}
}

#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct Service {
pub id: u32,
pub image: ImageUri,
pub image: ImageRef,
}

#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
Expand All @@ -16,8 +101,8 @@ pub struct TargetService {
#[serde(default)]
pub id: u32,

/// Service image URI
pub image: ImageUri,
/// Service image
pub image: ImageRef,
}

impl From<Service> for TargetService {
Expand All @@ -29,3 +114,78 @@ impl From<Service> for TargetService {

pub type ServiceMap = BTreeMap<String, Service>;
pub type TargetServiceMap = BTreeMap<String, TargetService>;

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn test_service_container_name_basic_parsing() {
let name: ServiceContainerName = "myservice_abc123".parse().unwrap();
assert_eq!(name.service_name, "myservice");
assert_eq!(name.release_uuid, "abc123".into());
}

#[test]
fn test_service_container_name_with_variables() {
let name: ServiceContainerName = "myservice_var1_var2_abc123".parse().unwrap();
assert_eq!(name.service_name, "myservice");
assert_eq!(name.release_uuid, "abc123".into());
}

#[test]
fn test_service_container_name_with_many_variables() {
let name: ServiceContainerName =
"myservice_var1_var2_var3_var4_var5_abc123".parse().unwrap();
assert_eq!(name.service_name, "myservice");
assert_eq!(name.release_uuid, "abc123".into());
}

#[test]
fn test_service_container_name_empty_service_name() {
let name: ServiceContainerName = "_abc123".parse().unwrap();
assert_eq!(name.service_name, "_abc123");
assert_eq!(name.release_uuid, UNKNOWN_RELEASE_UUID.into());
}

#[test]
fn test_service_container_name_empty_release_uuid() {
let name: ServiceContainerName = "myservice_".parse().unwrap();
assert_eq!(name.service_name, "myservice_");
assert_eq!(name.release_uuid, UNKNOWN_RELEASE_UUID.into());
}

#[test]
fn test_service_container_name_no_underscore() {
let name: ServiceContainerName = "myservice".parse().unwrap();
assert_eq!(name.service_name, "myservice");
assert_eq!(name.release_uuid, UNKNOWN_RELEASE_UUID.into());
}

#[test]
#[should_panic(expected = "container name cannot be empty")]
fn test_service_container_name_empty_string() {
let _: ServiceContainerName = "".parse().unwrap();
}

#[test]
fn test_service_container_name_single_underscore() {
let name: ServiceContainerName = "_".parse().unwrap();
assert_eq!(name.service_name, "_");
assert_eq!(name.release_uuid, UNKNOWN_RELEASE_UUID.into());
}

#[test]
fn test_service_container_name_with_leading_slash() {
let name: ServiceContainerName = "/myservice_abc123".parse().unwrap();
assert_eq!(name.service_name, "myservice");
assert_eq!(name.release_uuid, "abc123".into());
}

#[test]
fn test_service_container_name_with_leading_slash_fallback() {
let name: ServiceContainerName = "/myservice".parse().unwrap();
assert_eq!(name.service_name, "myservice");
assert_eq!(name.release_uuid, UNKNOWN_RELEASE_UUID.into());
}
}
Loading
Loading