use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use crate::image::oci::digest::Digest;
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)]
pub struct Empty {}
/// Struct representing a 'descriptor'.
///
/// This structure implements a 'descriptor' defined in OCI Image Spec v1 [description][desc].
/// The original [Go definitions][godefs] are defined here.
///
/// [desc]: https://github.com/opencontainers/image-spec/blob/master/descriptor.md
/// [godefs]: https://github.com/opencontainers/image-spec/blob/master/specs-go/v1/descriptor.go
#[derive(PartialEq, Eq, Serialize, Deserialize, Debug, Clone)]
pub struct Descriptor {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub mediatype: Option<String>,
pub digest: Digest,
pub size: i64,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub urls: Option<Vec<String>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub platform: Option<Platform>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub annotations: Option<HashMap<String, String>>,
}
/// Platform Struct used by the 'descriptor' struct above
/// [Go definition]
/// (https://github.com/opencontainers/image-spec/blob/master/specs-go/v1/descriptor.go#L45)
#[derive(PartialEq, Eq, Serialize, Deserialize, Debug, Clone)]
pub struct Platform {
pub architecture: String,
pub os: String,
#[serde(
default,
rename = "os.version",
skip_serializing_if = "Option::is_none"
)]
pub os_version: Option<String>,
#[serde(
default,
rename = "os.features",
skip_serializing_if = "Option::is_none"
)]
pub os_features: Option<Vec<String>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub variant: Option<String>,
}
/// Image Index defined in OCI Spec v1
///
/// [Reference](https://github.com/opencontainers/image-spec/blob/master/image-index.md)
/// [Go Definition](https://github.com/opencontainers/image-spec/blob/master/specs-go/v1/index.go)
#[derive(PartialEq, Eq, Serialize, Deserialize, Debug, Clone)]
pub struct Index {
#[serde(rename = "schemaVersion")]
pub version: u8,
pub manifests: Vec<Descriptor>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub annotations: Option<HashMap<String, String>>,
}
impl Default for Index {
fn default() -> Self {
Index {
version: 2,
manifests: vec![],
annotations: None,
}
}
}
/// Image Layout structure. See [Reference][ref].
///
/// [ref]: https://github.com/opencontainers/image-spec/blob/master/image-layout.md#oci-layout-file
#[derive(PartialEq, Eq, Serialize, Deserialize, Debug, Clone)]
pub struct ImageLayout {
#[serde(rename = "imageLayoutVersion")]
pub img_layout_version: String,
}
impl Default for ImageLayout {
fn default() -> Self {
ImageLayout {
img_layout_version: "1.0.0".to_string(),
}
}
}
/// Image Manifest
///
/// [Reference](https://github.com/opencontainers/image-spec/blob/master/manifest.md)
/// [Go Definition]
/// (https://github.com/opencontainers/image-spec/blob/master/specs-go/v1/manifest.go)
#[derive(PartialEq, Eq, Serialize, Deserialize, Debug, Clone)]
pub struct Manifest {
#[serde(rename = "schemaVersion")]
pub version: u8,
pub config: Descriptor,
pub layers: Vec<Descriptor>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub annotations: Option<HashMap<String, String>>,
}
pub const MEDIA_TYPE_DESCRIPTOR: &str = "application/vnd.oci.descriptor.v1+json";
pub const MEDIA_TYPE_LAYOUT_HEADER: &str = "application/vnd.oci.layout.header.v1+json";
pub const MEDIA_TYPE_IMAGE_MANIFEST: &str = "application/vnd.oci.image.manifest.v1+json";
pub const MEDIA_TYPE_IMAGE_INDEX: &str = "application/vnd.oci.image.index.v1+json";
pub const MEDIA_TYPE_IMAGE_LAYER: &str = "application/vnd.oci.image.layer.v1.tar";
pub const MEDIA_TYPE_IMAGE_LAYER_GZIP: &str = "application/vnd.oci.image.layer.v1.tar+gzip";
pub const MEDIA_TYPE_IMAGE_LAYER_ZSTD: &str = "application/vnd.oci.image.layer.v1.tar+zstd";
pub const MEDIA_TYPE_IMAGE_LAYER_NON_DISTRIBUTABLE: &str =
"application/vnd.oci.image.layer.nondistributable.v1.tar";
pub const MEDIA_TYPE_IMAGE_LAYER_NON_DISTRIBUTABLE_GZIP: &str =
"application/vnd.oci.image.layer.nondistributable.v1.tar+gzip";
pub const MEDIA_TYPE_IMAGE_LAYER_NON_DISTRIBUTABLE_ZSTD: &str =
"application/vnd.oci.image.layer.nondistributable.v1.tar+zstd";
pub const MEDIA_TYPE_IMAGE_CONFIG: &str = "application/vnd.oci.image.config.v1+json";
/// Image Config
///
/// [Reference](https://github.com/opencontainers/image-spec/blob/master/config.md)
/// [Go Definition](https://github.com/opencontainers/image-spec/blob/master/specs-go/v1/config.go)
#[derive(PartialEq, Eq, Serialize, Deserialize, Debug)]
pub struct ImageConfig {
#[serde(default, skip_serializing_if = "Option::is_none", rename = "User")]
pub user: Option<String>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
rename = "ExposedPorts"
)]
pub exposed_ports: Option<HashMap<String, Empty>>,
#[serde(default, skip_serializing_if = "Option::is_none", rename = "Env")]
pub env: Option<Vec<String>>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
rename = "EntryPoint"
)]
pub entry_point: Option<Vec<String>>,
#[serde(default, skip_serializing_if = "Option::is_none", rename = "Cmd")]
pub cmd: Option<Vec<String>>,
#[serde(default, skip_serializing_if = "Option::is_none", rename = "Volumes")]
pub volumes: Option<HashMap<String, Empty>>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
rename = "WorkingDir"
)]
pub working_dir: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none", rename = "Labels")]
pub labels: Option<HashMap<String, String>>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
rename = "StopSignal"
)]
pub stop_signal: Option<String>,
}
#[derive(PartialEq, Eq, Serialize, Deserialize, Debug)]
pub struct RootFS {
#[serde(default, rename = "type")]
pub type_: String,
pub diff_ids: Vec<Digest>,
}
#[derive(PartialEq, Eq, Serialize, Deserialize, Debug)]
pub struct History {
pub created: DateTime<Utc>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub created_by: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub author: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub comment: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub empty_layer: Option<bool>,
}
/// Image Struct
///
/// [Go
/// Definition](https://github.com/opencontainers/image-spec/blob/master/specs-go/v1/config.go#L82)
#[derive(PartialEq, Eq, Serialize, Deserialize, Debug)]
pub struct Image {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub created: Option<DateTime<Utc>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub author: Option<String>,
pub architecture: String,
pub os: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub config: Option<ImageConfig>,
pub rootfs: RootFS,
pub history: Option<Vec<History>>,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_image_index_ok() {
// Reference: https://github.com/opencontainers/image-spec/blob/master/image-index.md
let input = r##"{ "schemaVersion": 2, "manifests": [ { "mediaType": "application/vnd.oci.image.manifest.v1+json", "size": 7143, "digest": "sha256:e692418e4cbaf90ca69d05a66403747baa33ee08806650b51fab815ad7fc331f", "platform": { "architecture": "ppc64le", "os": "linux" } }, { "mediaType": "application/vnd.oci.image.manifest.v1+json", "size": 7682, "digest": "sha256:5b0bcabd1ed22e9fb1310cf6c2dec7cdef19f0ad69efa1f392e94a4333501270", "platform": { "architecture": "amd64", "os": "linux" } } ], "annotations": { "com.example.key1": "value1", "com.example.key2": "value2" } }"##;
let parsed = serde_json::from_str::<Index>(input);
assert!(parsed.is_ok(), "{}", parsed.err().unwrap());
}
#[test]
fn test_image_manifest_ok() {
// Reference: https://github.com/opencontainers/image-spec/blob/master/manifest.md
let input = r##"{ "schemaVersion": 2, "config": { "mediaType": "application/vnd.oci.image.config.v1+json", "size": 7023, "digest": "sha256:b5b2b2c507a0944348e0303114d8d93aaaa081732b86451d9bce1f432a537bc7" }, "layers": [ { "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip", "size": 32654, "digest": "sha256:9834876dcfb05cb167a5c24953eba58c4ac89b1adf57f28f2f9d09af107ee8f0" }, { "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip", "size": 16724, "digest": "sha256:3c3a4604a545cdc127456d94e421cd355bca5b528f4a9c1905b15da2eb4a4c6b" }, { "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip", "size": 73109, "digest": "sha256:ec4b8955958665577945c89419d1af06b5f7636b4ac3da7f12184802ad867736" } ], "annotations": { "com.example.key1": "value1", "com.example.key2": "value2" } }"##;
let parsed = serde_json::from_str::<Manifest>(input);
assert!(parsed.is_ok(), "{}", parsed.err().unwrap());
}
#[test]
fn test_image_config_ok() {
// Reference: https://github.com/opencontainers/image-spec/blob/master/config.md
let input = r##"{ "created": "2015-10-31T22:22:56.015925234Z", "author": "Alyssa P. Hacker <alyspdev@example.com>", "architecture": "amd64", "os": "linux", "config": { "User": "alice", "ExposedPorts": { "8080/tcp": {} }, "Env": [ "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", "FOO=oci_is_a", "BAR=well_written_spec" ], "Entrypoint": [ "/bin/my-app-binary" ], "Cmd": [ "--foreground", "--config", "/etc/my-app.d/default.cfg" ], "Volumes": { "/var/job-result-data": {}, "/var/log/my-app-logs": {} }, "WorkingDir": "/home/alice", "Labels": { "com.example.project.git.url": "https://example.com/project.git", "com.example.project.git.commit": "45a939b2999782a3f005621a8d0f29aa387e1d6b" } }, "rootfs": { "diff_ids": [ "sha256:c6f988f4874bb0add23a778f753c65efe992244e148a1d2ec2a8b664fb66bbd1", "sha256:5f70bf18a086007016e948b04aed3b82103a36bea41755b6cddfaf10ace3c6ef" ], "type": "layers" }, "history": [ { "created": "2015-10-31T22:22:54.690851953Z", "created_by": "/bin/sh -c #(nop) ADD file:a3bc1e842b69636f9df5256c49c5374fb4eef1e281fe3f282c65fb853ee171c5 in /" }, { "created": "2015-10-31T22:22:55.613815829Z", "created_by": "/bin/sh -c #(nop) CMD [\"sh\"]", "empty_layer": true } ] }"##;
let parsed = serde_json::from_str::<ImageConfig>(input);
assert!(parsed.is_ok(), "{}", parsed.err().unwrap());
}
}