Skip to content

Commit

Permalink
Use image in ServiceConfig
Browse files Browse the repository at this point in the history
- Changed REST API to use "image" attribute only for specifying Docker images
- Make getters of ServiceConfig idiomatic according to [RFC #344](https://github.com/rust-lang/rfcs/blob/master/text/0344-conventions-galore.md#gettersetter-apis)
  • Loading branch information
schrieveslaach committed Feb 25, 2019
1 parent 3d73074 commit f01d6a1
Show file tree
Hide file tree
Showing 7 changed files with 144 additions and 159 deletions.
20 changes: 5 additions & 15 deletions api/res/openapi.yml
Expand Up @@ -194,22 +194,12 @@ components:
type: string type: string
description: Name of the service to start description: Name of the service to start
example: mariadb example: mariadb
registry: image:
type: string type: string
description: Hostname of the docker registry, containing the image of the service description: >-
example: docker.io The docker image with `<repo-name>/<hub-user>/<repo-name>:<tag>`. `<repo-name>`, `<hub-user>` and `<tag>`
imageUser: are optional values.
type: string example: mariadb:10.3
description: cf. `hub-user` from command `docker push <hub-user>/<repo-name>:<tag>` ([push docker images](https://docs.docker.com/docker-hub/repos/#pushing-a-repository-image-to-docker-hub))
example: library
imageRepository:
type: string
description: cf. `repo-name` from command `docker push <hub-user>/<repo-name>:<tag>` ([push docker images](https://docs.docker.com/docker-hub/repos/#pushing-a-repository-image-to-docker-hub))
example: mariadb
imageTag:
type: string
description: cf. `tag` from command `docker push <hub-user>/<repo-name>:<tag>` ([push docker images](https://docs.docker.com/docker-hub/repos/#pushing-a-repository-image-to-docker-hub))
example: 10.3
env: env:
type: array type: array
items: items:
Expand Down
137 changes: 66 additions & 71 deletions api/src/models/service.rs
Expand Up @@ -26,6 +26,7 @@


use regex::Regex; use regex::Regex;
use serde::ser::{Serialize, Serializer}; use serde::ser::{Serialize, Serializer};
use serde::{de, Deserialize, Deserializer};
use std::collections::BTreeMap; use std::collections::BTreeMap;
use std::str::FromStr; use std::str::FromStr;
use url::Url; use url::Url;
Expand All @@ -43,10 +44,8 @@ pub struct Service {
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct ServiceConfig { pub struct ServiceConfig {
service_name: String, service_name: String,
image_repository: String, #[serde(deserialize_with = "Image::parse_from_string")]
registry: Option<String>, image: Image,
image_user: Option<String>,
image_tag: Option<String>,
env: Option<Vec<String>>, env: Option<Vec<String>>,
volumes: Option<BTreeMap<String, String>>, volumes: Option<BTreeMap<String, String>>,
#[serde(skip)] #[serde(skip)]
Expand All @@ -57,6 +56,7 @@ pub struct ServiceConfig {
port: u16, port: u16,
} }


#[derive(Clone, Deserialize, Eq, Hash, PartialEq)]
pub enum Image { pub enum Image {
Named { Named {
image_repository: String, image_repository: String,
Expand All @@ -70,28 +70,10 @@ pub enum Image {
} }


impl ServiceConfig { impl ServiceConfig {
pub fn new(service_name: String, image: &Image) -> ServiceConfig { pub fn new(service_name: String, image: Image) -> ServiceConfig {
let (image_repository, registry, image_user, image_tag) = match image {
Image::Digest { hash } => (hash.clone(), None, None, None),
Image::Named {
image_repository,
registry,
image_user,
image_tag,
} => (
image_repository.clone(),
registry.clone(),
image_user.clone(),
image_tag.clone(),
),
};

ServiceConfig { ServiceConfig {
service_name, service_name,
image_repository, image,
registry,
image_user,
image_tag,
env: None, env: None,
volumes: None, volumes: None,
labels: None, labels: None,
Expand All @@ -104,47 +86,28 @@ impl ServiceConfig {
self.container_type = container_type; self.container_type = container_type;
} }


pub fn get_container_type(&self) -> &ContainerType { pub fn container_type(&self) -> &ContainerType {
&self.container_type &self.container_type
} }


fn refers_to_image_id(&self) -> bool {
let regex = Regex::new(r"^(sha256:)?(?P<id>[a-fA-F0-9]+)$").unwrap();
regex
.captures(&self.image_repository)
.map(|_| true)
.unwrap_or_else(|| false)
}

/// Returns a fully qualifying docker image /// Returns a fully qualifying docker image
pub fn get_image(&self) -> Image { pub fn image(&self) -> &Image {
if self.refers_to_image_id() { &self.image
return Image::Digest {
hash: self.image_repository.clone(),
};
}

Image::Named {
image_repository: self.image_repository.clone(),
registry: self.registry.clone(),
image_user: self.image_user.clone(),
image_tag: self.image_tag.clone(),
}
} }


pub fn set_service_name(&mut self, service_name: &String) { pub fn set_service_name(&mut self, service_name: &String) {
self.service_name = service_name.clone() self.service_name = service_name.clone()
} }


pub fn get_service_name(&self) -> &String { pub fn service_name(&self) -> &String {
&self.service_name &self.service_name
} }


pub fn set_env(&mut self, env: Option<Vec<String>>) { pub fn set_env(&mut self, env: Option<Vec<String>>) {
self.env = env; self.env = env;
} }


pub fn get_env<'a, 'b: 'a>(&'b self) -> Option<&'a Vec<String>> { pub fn env<'a, 'b: 'a>(&'b self) -> Option<&'a Vec<String>> {
match &self.env { match &self.env {
None => None, None => None,
Some(env) => Some(&env), Some(env) => Some(&env),
Expand All @@ -155,7 +118,7 @@ impl ServiceConfig {
self.labels = labels; self.labels = labels;
} }


pub fn get_labels<'a, 'b: 'a>(&'b self) -> Option<&'a BTreeMap<String, String>> { pub fn labels<'a, 'b: 'a>(&'b self) -> Option<&'a BTreeMap<String, String>> {
match &self.labels { match &self.labels {
None => None, None => None,
Some(labels) => Some(&labels), Some(labels) => Some(&labels),
Expand All @@ -166,7 +129,7 @@ impl ServiceConfig {
self.volumes = volumes; self.volumes = volumes;
} }


pub fn get_volumes<'a, 'b: 'a>(&'b self) -> Option<&'a BTreeMap<String, String>> { pub fn volumes<'a, 'b: 'a>(&'b self) -> Option<&'a BTreeMap<String, String>> {
match &self.volumes { match &self.volumes {
None => None, None => None,
Some(volumes) => Some(&volumes), Some(volumes) => Some(&volumes),
Expand All @@ -177,7 +140,7 @@ impl ServiceConfig {
self.port = port; self.port = port;
} }


pub fn get_port(&self) -> u16 { pub fn port(&self) -> u16 {
self.port self.port
} }
} }
Expand Down Expand Up @@ -210,22 +173,22 @@ impl Service {
self.container_type = container_type; self.container_type = container_type;
} }


fn get_service_url(&self) -> Option<Url> { fn service_url(&self) -> Option<Url> {
self.base_url.clone().map(|url| { self.base_url.clone().map(|url| {
url.join(&format!("/{}/{}/", &self.app_name, &self.service_name)) url.join(&format!("/{}/{}/", &self.app_name, &self.service_name))
.unwrap() .unwrap()
}) })
} }


pub fn get_service_name(&self) -> &String { pub fn service_name(&self) -> &String {
&self.service_name &self.service_name
} }


pub fn get_container_id(&self) -> &String { pub fn container_id(&self) -> &String {
&self.container_id &self.container_id
} }


pub fn get_container_type(&self) -> &ContainerType { pub fn container_type(&self) -> &ContainerType {
&self.container_type &self.container_type
} }
} }
Expand All @@ -247,7 +210,7 @@ impl Serialize for Service {


let version_url = match self.container_type { let version_url = match self.container_type {
ContainerType::Instance | ContainerType::Replica => self ContainerType::Instance | ContainerType::Replica => self
.get_service_url() .service_url()
// TODO: use Web Host Metadata (RFC 6415): .well-known/host-meta.json // TODO: use Web Host Metadata (RFC 6415): .well-known/host-meta.json
.map(|url| url.join("version").unwrap()) .map(|url| url.join("version").unwrap())
.map(|url| url.to_string()), .map(|url| url.to_string()),
Expand All @@ -256,7 +219,7 @@ impl Serialize for Service {


let s = Service { let s = Service {
name: &self.service_name, name: &self.service_name,
url: self.get_service_url().map(|url| url.to_string()), url: self.service_url().map(|url| url.to_string()),
service_type: self.container_type.to_string(), service_type: self.container_type.to_string(),
version_url, version_url,
}; };
Expand Down Expand Up @@ -324,7 +287,15 @@ pub enum ServiceError {
} }


impl Image { impl Image {
pub fn get_tag(&self) -> Option<String> { fn parse_from_string<'de, D>(deserializer: D) -> Result<Image, D::Error>
where
D: Deserializer<'de>,
{
let img = String::deserialize(deserializer)?;
Image::from_str(&img).map_err(de::Error::custom)
}

pub fn tag(&self) -> Option<String> {
match &self { match &self {
Image::Digest { .. } => None, Image::Digest { .. } => None,
Image::Named { Image::Named {
Expand All @@ -339,7 +310,7 @@ impl Image {
} }
} }


pub fn get_name(&self) -> Option<String> { pub fn name(&self) -> Option<String> {
match &self { match &self {
Image::Digest { .. } => None, Image::Digest { .. } => None,
Image::Named { Image::Named {
Expand All @@ -358,7 +329,7 @@ impl Image {
} }
} }


pub fn get_registry(&self) -> Option<String> { pub fn registry(&self) -> Option<String> {
match &self { match &self {
Image::Digest { .. } => None, Image::Digest { .. } => None,
Image::Named { Image::Named {
Expand Down Expand Up @@ -460,42 +431,42 @@ mod tests {
&image.to_string(), &image.to_string(),
"sha256:9895c9b90b58c9490471b877f6bb6a90e6bdc154da7fbb526a0322ea242fc913" "sha256:9895c9b90b58c9490471b877f6bb6a90e6bdc154da7fbb526a0322ea242fc913"
); );
assert_eq!(image.get_name(), None); assert_eq!(image.name(), None);
assert_eq!(image.get_tag(), None); assert_eq!(image.tag(), None);
} }


#[test] #[test]
fn should_parse_image_id() { fn should_parse_image_id() {
let image = Image::from_str("9895c9b90b58").unwrap(); let image = Image::from_str("9895c9b90b58").unwrap();


assert_eq!(&image.to_string(), "9895c9b90b58"); assert_eq!(&image.to_string(), "9895c9b90b58");
assert_eq!(image.get_name(), None); assert_eq!(image.name(), None);
assert_eq!(image.get_tag(), None); assert_eq!(image.tag(), None);
} }


#[test] #[test]
fn should_parse_image_with_repo_and_user() { fn should_parse_image_with_repo_and_user() {
let image = Image::from_str("zammad/zammad-docker-compose").unwrap(); let image = Image::from_str("zammad/zammad-docker-compose").unwrap();


assert_eq!(&image.get_name().unwrap(), "zammad/zammad-docker-compose"); assert_eq!(&image.name().unwrap(), "zammad/zammad-docker-compose");
assert_eq!(&image.get_tag().unwrap(), "latest"); assert_eq!(&image.tag().unwrap(), "latest");
} }


#[test] #[test]
fn should_parse_image_with_version() { fn should_parse_image_with_version() {
let image = Image::from_str("mariadb:10.3").unwrap(); let image = Image::from_str("mariadb:10.3").unwrap();


assert_eq!(&image.get_name().unwrap(), "library/mariadb"); assert_eq!(&image.name().unwrap(), "library/mariadb");
assert_eq!(&image.get_tag().unwrap(), "10.3"); assert_eq!(&image.tag().unwrap(), "10.3");
assert_eq!(&image.to_string(), "docker.io/library/mariadb:10.3"); assert_eq!(&image.to_string(), "docker.io/library/mariadb:10.3");
} }


#[test] #[test]
fn should_parse_image_with_latest_version() { fn should_parse_image_with_latest_version() {
let image = Image::from_str("nginx:latest").unwrap(); let image = Image::from_str("nginx:latest").unwrap();


assert_eq!(&image.get_name().unwrap(), "library/nginx"); assert_eq!(&image.name().unwrap(), "library/nginx");
assert_eq!(&image.get_tag().unwrap(), "latest"); assert_eq!(&image.tag().unwrap(), "latest");
assert_eq!(&image.to_string(), "docker.io/library/nginx:latest"); assert_eq!(&image.to_string(), "docker.io/library/nginx:latest");
} }


Expand All @@ -511,6 +482,30 @@ mod tests {
let image = Image::from_str("localhost:5000/library/nginx:latest").unwrap(); let image = Image::from_str("localhost:5000/library/nginx:latest").unwrap();


assert_eq!(&image.to_string(), "localhost:5000/library/nginx:latest"); assert_eq!(&image.to_string(), "localhost:5000/library/nginx:latest");
assert_eq!(&image.get_registry().unwrap(), "localhost:5000"); assert_eq!(&image.registry().unwrap(), "localhost:5000");
}

#[test]
fn should_parse_service_config_json() {
let json = r#"{
"serviceName": "mariadb",
"image": "mariadb:10.3",
"env": [
"MYSQL_USER=admin",
"MYSQL_DATABASE=dbname"
]
}"#;

let config = serde_json::from_str::<ServiceConfig>(json).unwrap();

assert_eq!(config.service_name(), "mariadb");
assert_eq!(config.image().to_string(), "docker.io/library/mariadb:10.3");
assert_eq!(
config.env(),
Some(&vec![
String::from("MYSQL_USER=admin"),
String::from("MYSQL_DATABASE=dbname")
])
);
} }
} }
8 changes: 4 additions & 4 deletions api/src/services/apps_service.rs
Expand Up @@ -79,7 +79,7 @@ impl<'a> AppsService<'a> {
.filter(|config| { .filter(|config| {
match service_configs match service_configs
.iter() .iter()
.find(|c| c.get_service_name() == config.get_service_name()) .find(|c| c.service_name() == config.service_name())
{ {
None => true, None => true,
Some(_) => false, Some(_) => false,
Expand Down Expand Up @@ -119,8 +119,8 @@ impl<'a> AppsService<'a> {
configs.extend(self.get_application_companion_configs(app_name, &configs)?); configs.extend(self.get_application_companion_configs(app_name, &configs)?);


configs.sort_unstable_by(|a, b| { configs.sort_unstable_by(|a, b| {
let index1 = AppsService::container_type_index(a.get_container_type()); let index1 = AppsService::container_type_index(a.container_type());
let index2 = AppsService::container_type_index(b.get_container_type()); let index2 = AppsService::container_type_index(b.container_type());
index1.cmp(&index2) index1.cmp(&index2)
}); });


Expand Down Expand Up @@ -148,7 +148,7 @@ impl<'a> AppsService<'a> {
.filter(|config| { .filter(|config| {
match service_configs match service_configs
.iter() .iter()
.find(|c| c.get_service_name() == config.get_service_name()) .find(|c| c.service_name() == config.service_name())
{ {
None => true, None => true,
Some(_) => false, Some(_) => false,
Expand Down

0 comments on commit f01d6a1

Please sign in to comment.