use std::collections::BTreeMap;
use std::convert::TryFrom;
use std::path::PathBuf;
use chrono::prelude::*;
use serde::{Deserialize, Serialize};
use serde_json::Value;
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ImageSpecification {
#[serde(skip_serializing_if = "Option::is_none")]
pub created: Option<DateTime<Utc>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub author: Option<String>,
pub architecture: Architecture,
pub os: OperatingSystem,
#[serde(skip_serializing_if = "Option::is_none")]
pub config: Option<ImageConfig>,
#[serde(skip_serializing_if = "Option::is_none")]
pub rootfs: Option<ImageRootfs>,
#[serde(skip_serializing_if = "Option::is_none")]
pub history: Option<Vec<LayerHistoryItem>>,
}
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum Architecture {
Amd64,
I386,
ARM,
ARM64,
PPC64le,
PPC64,
Mips64le,
Mips64,
Mipsle,
Mips,
S390x,
}
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum OperatingSystem {
Darwin,
Dragonfly,
Freebsd,
Linux,
Netbsd,
Openbsd,
Plan9,
Solaris,
Windows,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(from = "RawImageConfig")]
#[serde(into = "RawImageConfig")]
pub struct ImageConfig {
pub user: Option<String>,
pub exposed_ports: Option<Vec<ExposedPort>>,
pub env: Option<BTreeMap<String, String>>,
pub entrypoint: Option<Vec<String>>,
pub cmd: Option<Vec<String>>,
pub volumes: Option<Vec<PathBuf>>,
pub working_dir: Option<PathBuf>,
pub labels: Option<BTreeMap<String, String>>,
pub stop_signal: Option<Signal>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "PascalCase")]
struct RawImageConfig {
#[serde(skip_serializing_if = "Option::is_none")]
user: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
exposed_ports: Option<BTreeMap<ExposedPort, Value>>,
#[serde(skip_serializing_if = "Option::is_none")]
env: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
entrypoint: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
cmd: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
volumes: Option<BTreeMap<PathBuf, Value>>,
#[serde(skip_serializing_if = "Option::is_none")]
working_dir: Option<PathBuf>,
#[serde(skip_serializing_if = "Option::is_none")]
labels: Option<BTreeMap<String, String>>,
#[serde(skip_serializing_if = "Option::is_none")]
stop_signal: Option<Signal>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ImageRootfs {
#[serde(rename = "type")]
pub diff_type: RootfsType,
pub diff_ids: Vec<String>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct LayerHistoryItem {
#[serde(skip_serializing_if = "Option::is_none")]
pub created: Option<DateTime<Utc>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub author: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub created_by: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub comment: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub empty_layer: Option<bool>,
}
#[derive(Debug, Clone, Copy, PartialEq, PartialOrd, Eq, Ord, Serialize, Deserialize)]
#[serde(try_from = "String")]
#[serde(into = "String")]
pub enum ExposedPort {
Tcp(u16),
Udp(u16),
}
impl TryFrom<String> for ExposedPort {
type Error = std::num::ParseIntError;
fn try_from(value: String) -> Result<Self, Self::Error> {
let postfix_len = value.len() - 4;
match &value[postfix_len..] {
"/tcp" => Ok(ExposedPort::Tcp(value[..postfix_len].parse()?)),
"/udp" => Ok(ExposedPort::Udp(value[..postfix_len].parse()?)),
_ => Ok(ExposedPort::Tcp(value.parse()?)),
}
}
}
impl Into<String> for ExposedPort {
fn into(self) -> String {
match self {
ExposedPort::Tcp(port) => format!("{}/tcp", port),
ExposedPort::Udp(port) => format!("{}/udp", port),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum RootfsType {
Layers,
}
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
pub enum Signal {
SIGHUP,
SIGINT,
SIGQUIT,
SIGILL,
SIGTRAP,
SIGABRT,
SIGBUS,
SIGFPE,
SIGKILL,
SIGUSR1,
SIGSEGV,
SIGUSR2,
SIGPIPE,
SIGALRM,
SIGTERM,
SIGSTKFLT,
SIGCHLD,
SIGCONT,
SIGSTOP,
SIGTSTP,
SIGTTIN,
SIGTTOU,
SIGURG,
SIGXCPU,
SIGXFSZ,
SIGVTALRM,
SIGPROF,
SIGWINCH,
SIGIO,
SIGPWR,
SIGSYS,
SIGEMT,
SIGINFO,
}
impl From<RawImageConfig> for ImageConfig {
fn from(raw: RawImageConfig) -> Self {
Self {
user: raw.user,
entrypoint: raw.entrypoint,
cmd: raw.cmd,
working_dir: raw.working_dir,
labels: raw.labels,
stop_signal: raw.stop_signal,
env: raw.env.map(|inner| {
inner
.into_iter()
.map(|mut pair| match pair.find('=') {
Some(pos) => {
let value = pair.split_off(pos + 1);
let mut name = pair;
name.pop();
(name, value)
}
None => (pair, String::with_capacity(0)),
})
.collect()
}),
exposed_ports: raw
.exposed_ports
.map(|inner| inner.into_iter().map(|(port, _)| port).collect()),
volumes: raw
.volumes
.map(|inner| inner.into_iter().map(|(volume, _)| volume).collect()),
}
}
}
impl Into<RawImageConfig> for ImageConfig {
fn into(self) -> RawImageConfig {
RawImageConfig {
user: self.user,
entrypoint: self.entrypoint,
cmd: self.cmd,
working_dir: self.working_dir,
labels: self.labels,
stop_signal: self.stop_signal,
env: self.env.map(|inner| {
inner
.into_iter()
.map(|(key, value)| format!("{}={}", key, value))
.collect()
}),
exposed_ports: self.exposed_ports.map(|inner| {
inner
.into_iter()
.map(|port| (port, Value::Object(Default::default())))
.collect()
}),
volumes: self.volumes.map(|inner| {
inner
.into_iter()
.map(|volume| (volume, Value::Object(Default::default())))
.collect()
}),
}
}
}
#[test]
fn serialization() {
use pretty_assertions::assert_eq;
let ref_json = include_str!("../tests/oci-image-spec.json");
let ref_spec = ImageSpecification {
created: Some("2015-10-31T22:22:56.015925234Z".parse().unwrap()),
author: Some("Alyssa P. Hacker <alyspdev@example.com>".into()),
architecture: Architecture::Amd64,
os: OperatingSystem::Linux,
rootfs: Some(ImageRootfs {
diff_type: RootfsType::Layers,
diff_ids: vec![
"sha256:c6f988f4874bb0add23a778f753c65efe992244e148a1d2ec2a8b664fb66bbd1".into(),
"sha256:5f70bf18a086007016e948b04aed3b82103a36bea41755b6cddfaf10ace3c6ef".into(),
],
}),
history: Some(vec![
LayerHistoryItem {
created: Some("2015-10-31T22:22:54.690851953Z".parse().unwrap()),
created_by: Some("/bin/sh -c #(nop) ADD file in /".into()),
author: None,
comment: None,
empty_layer: None,
},
LayerHistoryItem {
created: Some("2015-10-31T22:22:55.613815829Z".parse().unwrap()),
created_by: Some("/bin/sh -c #(nop) CMD [\"sh\"]".into()),
author: None,
comment: None,
empty_layer: Some(true),
},
]),
config: Some(ImageConfig {
user: Some("alice".into()),
exposed_ports: Some(vec![ExposedPort::Tcp(8080), ExposedPort::Udp(8081)]),
env: Some(
vec![(
String::from("PATH"),
String::from("/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"),
)]
.into_iter()
.collect(),
),
entrypoint: Some(vec!["/bin/my-app-binary".into()]),
cmd: Some(vec![
"--foreground".into(),
"--config".into(),
"/etc/my-app.d/default.cfg".into(),
]),
volumes: Some(vec![
"/var/job-result-data".into(),
"/var/log/my-app-logs".into(),
]),
working_dir: Some("/home/alice".into()),
labels: Some(
vec![(
String::from("com.example.project.git.url"),
String::from("https://example.com/project.git"),
)]
.into_iter()
.collect(),
),
stop_signal: Some(Signal::SIGKILL),
}),
};
assert_eq!(serde_json::to_string_pretty(&ref_spec).unwrap(), ref_json);
assert_eq!(
serde_json::from_str::<ImageSpecification>(ref_json).unwrap(),
ref_spec
);
}
#[test]
fn min_serialization() {
use pretty_assertions::assert_eq;
let ref_json = include_str!("../tests/oci-image-spec-min.json");
let ref_spec = ImageSpecification {
created: None,
author: None,
architecture: Architecture::Amd64,
os: OperatingSystem::Linux,
rootfs: Some(ImageRootfs {
diff_type: RootfsType::Layers,
diff_ids: vec![
"sha256:c6f988f4874bb0add23a778f753c65efe992244e148a1d2ec2a8b664fb66bbd1".into(),
"sha256:5f70bf18a086007016e948b04aed3b82103a36bea41755b6cddfaf10ace3c6ef".into(),
],
}),
history: None,
config: None,
};
assert_eq!(serde_json::to_string_pretty(&ref_spec).unwrap(), ref_json);
assert_eq!(
serde_json::from_str::<ImageSpecification>(ref_json).unwrap(),
ref_spec
);
}