intermodal-rs 0.0.4

Container handling in Rust.
Documentation
//! Structs Describing Docker Schema2 Manifest etc.
//!
//! Note: We are supporting only the 'list' and 'schema2' descriptors from docker/distribution

use std::collections::HashMap;
use std::time::Duration as StdDuration;

use chrono::{DateTime, Utc};
use serde::{Deserialize, Deserializer, Serialize};

use crate::image::oci::digest::Digest;

#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)]
pub struct Empty {}

/// A Descriptor in docker/distribution Schema 2
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)]
pub struct Schema2Descriptor {
    #[serde(rename = "mediaType")]
    pub media_type: String,

    pub size: i64,

    pub digest: Digest,

    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub urls: Option<Vec<String>>,
}

/// A Manifest in a docker/distribution Schema 2
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)]
pub struct Schema2 {
    #[serde(rename = "schemaVersion")]
    pub schema_version: i8,

    #[serde(rename = "mediaType")]
    pub media_type: String,

    pub config: Schema2Descriptor,

    pub layers: Vec<Schema2Descriptor>,
}

/// Configuration Settings for the HEALTHCHECKER features.
///
/// From docker/api/types/container
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)]
pub struct Schema2HealthConfig {
    #[serde(rename = "Test", skip_serializing_if = "Option::is_none")]
    pub test: Option<Vec<String>>,

    #[serde(rename = "StartPeriod", skip_serializing_if = "Option::is_none")]
    pub start_period: Option<StdDuration>,

    #[serde(rename = "Interval", skip_serializing_if = "Option::is_none")]
    pub interval: Option<StdDuration>,

    #[serde(rename = "Timeout", skip_serializing_if = "Option::is_none")]
    pub timeout: Option<StdDuration>,

    #[serde(rename = "Retries", skip_serializing_if = "Option::is_none")]
    pub retrie: Option<i16>,
}

/// Schema2Config from docker/api/types/container
#[derive(Debug, Default, Serialize, Deserialize, PartialEq, Eq)]
pub struct Schema2Config {
    #[serde(rename = "Hostname")]
    pub hostname: String,

    #[serde(rename = "Domainname")]
    pub domainname: String,

    #[serde(rename = "User")]
    pub user: String,

    #[serde(rename = "AttachStdin")]
    pub attach_stdin: bool,

    #[serde(rename = "AttachStdout")]
    pub attach_stdout: bool,

    #[serde(rename = "AttachStderr")]
    pub attach_stderr: bool,

    #[serde(rename = "ExposedPorts", skip_serializing_if = "Option::is_none")]
    pub exposed_ports: Option<HashMap<String, Empty>>,

    #[serde(rename = "Tty")]
    pub tty: bool,

    #[serde(rename = "OpenStdin")]
    pub open_stdin: bool,

    #[serde(rename = "StdinOnce")]
    pub stdin_once: bool,

    #[serde(default, rename = "Env", deserialize_with = "deserialize_null_default")]
    pub env: Vec<String>,

    #[serde(default, rename = "Cmd", deserialize_with = "deserialize_null_default")]
    pub cmd: Vec<String>,

    #[serde(rename = "HealthCheck", skip_serializing_if = "Option::is_none")]
    pub health_check: Option<Schema2HealthConfig>,

    #[serde(rename = "ArgsEscaped", skip_serializing_if = "Option::is_none")]
    pub args_escaped: Option<bool>,

    #[serde(rename = "Image")]
    pub image: String,

    #[serde(rename = "Volumes", skip_serializing_if = "Option::is_none")]
    pub volumes: Option<Vec<String>>,

    #[serde(rename = "WorkingDir")]
    pub working_dir: String,

    #[serde(rename = "EntryPoint", skip_serializing_if = "Option::is_none")]
    pub entry_point: Option<String>,

    #[serde(rename = "NetworkDisabled", skip_serializing_if = "Option::is_none")]
    pub network_disabled: Option<bool>,

    #[serde(rename = "MacAddress", skip_serializing_if = "Option::is_none")]
    pub mac_address: Option<String>,

    #[serde(rename = "OnBuild", skip_serializing_if = "Option::is_none")]
    pub on_build: Option<Vec<String>>,

    #[serde(rename = "Labels", deserialize_with = "deserialize_null_default")]
    pub labels: HashMap<String, String>,

    #[serde(rename = "StopSignal", skip_serializing_if = "Option::is_none")]
    pub stop_signal: Option<String>,

    #[serde(rename = "StopTimeout", skip_serializing_if = "Option::is_none")]
    pub stop_timeout: Option<i16>,

    #[serde(rename = "Shell", skip_serializing_if = "Option::is_none")]
    pub shell: Option<String>, // FIXME: &'static str?
}

/// Schema2Image struct from docker/docker/image
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)]
pub struct Schema2Image {
    #[serde(skip_serializing_if = "Option::is_none")]
    pub id: Option<String>,

    #[serde(skip_serializing_if = "Option::is_none")]
    pub parent: Option<String>,

    #[serde(skip_serializing_if = "Option::is_none")]
    pub comment: Option<String>,

    pub created: DateTime<Utc>,

    #[serde(skip_serializing_if = "Option::is_none")]
    pub container: Option<String>,

    #[serde(skip_serializing_if = "Option::is_none")]
    pub container_config: Option<Schema2Config>,

    #[serde(skip_serializing_if = "Option::is_none")]
    pub docker_version: Option<String>,

    #[serde(skip_serializing_if = "Option::is_none")]
    pub author: Option<String>,

    #[serde(skip_serializing_if = "Option::is_none")]
    pub config: Option<Schema2Config>,

    #[serde(skip_serializing_if = "Option::is_none")]
    pub architecture: Option<String>,

    #[serde(skip_serializing_if = "Option::is_none")]
    pub variant: Option<String>,

    #[serde(skip_serializing_if = "Option::is_none")]
    pub os: Option<String>,

    #[serde(skip_serializing_if = "Option::is_none")]
    pub size: Option<i64>,

    #[serde(skip_serializing_if = "Option::is_none")]
    rootfs: Option<Schema2RootFS>,

    #[serde(skip_serializing_if = "Option::is_none")]
    history: Option<Vec<Schema2History>>,

    #[serde(rename = "os.version", skip_serializing_if = "Option::is_none")]
    pub os_version: Option<String>,

    #[serde(rename = "os.features", skip_serializing_if = "Option::is_none")]
    pub os_features: Option<String>,
}

/// RootFS Struct
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)]
pub struct Schema2RootFS {
    #[serde(rename = "type")]
    pub type_: String,

    pub diff_ids: Vec<Digest>,
}

/// Schema2History Struct
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)]
pub struct Schema2History {
    pub created: 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>,
}

/// Schema2PlatformSpec Struct
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)]
pub struct Schema2PlatformSpec {
    #[serde(skip_serializing_if = "Option::is_none")]
    pub architecture: Option<String>,

    pub os: String,

    #[serde(rename = "os.version", skip_serializing_if = "Option::is_none")]
    pub os_version: Option<String>,

    #[serde(rename = "os.features", skip_serializing_if = "Option::is_none")]
    pub os_features: Option<String>,

    #[serde(skip_serializing_if = "Option::is_none")]
    pub variant: Option<String>,

    #[serde(skip_serializing_if = "Option::is_none")]
    pub features: Option<String>,
}

/// Schema2Manifest Descriptor
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)]
pub struct Schema2ManifestDescriptor {
    #[serde(rename = "mediaType")]
    pub media_type: String,

    pub size: i64,

    pub digest: Digest,

    pub platform: Schema2PlatformSpec,
}

/// Schema2List Structure
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)]
pub struct Schema2List {
    #[serde(rename = "schemaVersion")]
    pub schema_version: i8,

    #[serde(rename = "mediaType")]
    pub media_type: String,

    pub manifests: Vec<Schema2ManifestDescriptor>,
}

fn deserialize_null_default<'de, D, T>(deserializer: D) -> Result<T, D::Error>
where
    T: Default + Deserialize<'de>,
    D: Deserializer<'de>,
{
    let opt = Option::deserialize(deserializer)?;
    Ok(opt.unwrap_or_default())
}

#[cfg(test)]
mod tests {

    use super::*;

    #[test]
    fn test_sample_fedoraproject_schema2image() {
        let input = r#"
{"architecture": "amd64", "comment": "Created by Image Factory", "config": {"AttachStderr": false, "AttachStdin": false, "AttachStdout": false, "Cmd": ["/bin/bash"], "Domainname": "", "Entrypoint": null, "Env": ["DISTTAG=f33container", "FGC=f33", "container=oci"], "ExposedPorts": null, "Hostname": "", "Image": "", "Labels": {"name": "fedora", "license": "MIT", "vendor": "Fedora Project", "version": "33"}, "MacAddress": "", "NetworkDisabled": false, "OnBuild": null, "OpenStdin": false, "StdinOnce": false, "Systemd": false, "Tty": false, "User": "", "VolumeDriver": "", "Volumes": null, "WorkingDir": ""}, "container_config": {"AttachStderr": false, "AttachStdin": false, "AttachStdout": false, "Cmd": null, "Domainname": "", "Entrypoint": null, "Env": null, "ExposedPorts": null, "Hostname": "", "Image": "", "Labels": null, "MacAddress": "", "NetworkDisabled": false, "OnBuild": null, "OpenStdin": false, "StdinOnce": false, "Systemd": false, "Tty": false, "User": "", "VolumeDriver": "", "Volumes": null, "WorkingDir": ""}, "created": "2020-10-27T07:49:11Z", "docker_version": "1.10.1", "os": "linux", "history": [{"comment": "Created by Image Factory", "created": "2020-10-27T07:49:11Z"}], "rootfs": {"diff_ids": ["sha256:b4fa6ff1346dec95ce4454464201fdadfd816e10eb7322048829c551ce032d08"], "type": "layers"}}
    "#;
        let parsed = serde_json::from_str::<Schema2Image>(input);
        assert!(parsed.is_ok(), "{}", parsed.err().unwrap())
    }

    #[test]
    fn test_sample_quay_io_schema2image() {
        let input = r#" {"architecture": "amd64", "comment": "Created by Image Factory", "config": {"AttachStderr": false, "AttachStdin": false, "AttachStdout": false, "Cmd": ["/bin/bash"], "Domainname": "", "Entrypoint": null, "Env": ["DISTTAG=f33container", "FGC=f33", "container=oci"], "ExposedPorts": null, "Hostname": "", "Image": "", "Labels": {"name": "fedora", "license": "MIT", "vendor": "Fedora Project", "version": "33"}, "MacAddress": "", "NetworkDisabled": false, "OnBuild": null, "OpenStdin": false, "StdinOnce": false, "Systemd": false, "Tty": false, "User": "", "VolumeDriver": "", "Volumes": null, "WorkingDir": ""}, "container_config": {"AttachStderr": false, "AttachStdin": false, "AttachStdout": false, "Cmd": null, "Domainname": "", "Entrypoint": null, "Env": null, "ExposedPorts": null, "Hostname": "", "Image": "", "Labels": null, "MacAddress": "", "NetworkDisabled": false, "OnBuild": null, "OpenStdin": false, "StdinOnce": false, "Systemd": false, "Tty": false, "User": "", "VolumeDriver": "", "Volumes": null, "WorkingDir": ""}, "created": "2020-10-27T07:49:11Z", "docker_version": "1.10.1", "os": "linux", "history": [{"comment": "Created by Image Factory", "created": "2020-10-27T07:49:11Z"}], "rootfs": {"diff_ids": ["sha256:b4fa6ff1346dec95ce4454464201fdadfd816e10eb7322048829c551ce032d08"], "type": "layers"}}
            "#;
        let parsed = serde_json::from_str::<Schema2Image>(input);
        assert!(parsed.is_ok(), "{}", parsed.err().unwrap())
    }

    #[test]
    fn test_sample_docker_io_schema2image() {
        let input = r##"
        {"architecture":"amd64","config":{"Hostname":"","Domainname":"","User":"","AttachStdin":false,"AttachStdout":false,"AttachStderr":false,"Tty":false,"OpenStdin":false,"StdinOnce":false,"Env":["PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin","DISTTAG=f33container","FGC=f33","FBR=f33"],"Cmd":["/bin/bash"],"Image":"sha256:3b1b0c55a47e10ea93d904fc20c39d253f9e1ad770922e8fb4af93dcec6691ce","Volumes":null,"WorkingDir":"","Entrypoint":null,"OnBuild":null,"Labels":{"maintainer":"Clement Verna \u003ccverna@fedoraproject.org\u003e"}},"container":"50cf73b69958473ab2f9a10d3249df073c99b7767ec7f1ff5ffd56da4f35397b","container_config":{"Hostname":"50cf73b69958","Domainname":"","User":"","AttachStdin":false,"AttachStdout":false,"AttachStderr":false,"Tty":false,"OpenStdin":false,"StdinOnce":false,"Env":["PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin","DISTTAG=f33container","FGC=f33","FBR=f33"],"Cmd":["/bin/sh","-c","#(nop) ","CMD [\"/bin/bash\"]"],"Image":"sha256:3b1b0c55a47e10ea93d904fc20c39d253f9e1ad770922e8fb4af93dcec6691ce","Volumes":null,"WorkingDir":"","Entrypoint":null,"OnBuild":null,"Labels":{"maintainer":"Clement Verna \u003ccverna@fedoraproject.org\u003e"}},"created":"2020-11-12T00:25:31.334712859Z","docker_version":"19.03.12","history":[{"created":"2019-01-16T21:21:55.569693599Z","created_by":"/bin/sh -c #(nop)  LABEL maintainer=Clement Verna \u003ccverna@fedoraproject.org\u003e","empty_layer":true},{"created":"2020-04-30T23:21:44.324893962Z","created_by":"/bin/sh -c #(nop)  ENV DISTTAG=f33container FGC=f33 FBR=f33","empty_layer":true},{"created":"2020-11-12T00:25:30.976066436Z","created_by":"/bin/sh -c #(nop) ADD file:240dde03c4d9f0ad759f8d1291fb45ab2745b6a108c6164d746766239d3420ab in / "},{"created":"2020-11-12T00:25:31.334712859Z","created_by":"/bin/sh -c #(nop)  CMD [\"/bin/bash\"]","empty_layer":true}],"os":"linux","rootfs":{"type":"layers","diff_ids":["sha256:ed0c36ccfcbe08498869bb435711b2657b593806792e29582fa90f43d87b2dfb"]}}
            "##;
        let parsed = serde_json::from_str::<Schema2Image>(input);
        assert!(parsed.is_ok(), "{}", parsed.err().unwrap())
    }
}