1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519
//! OCI Manifest
use std::collections::BTreeMap;
use crate::{
client::{Config, ImageLayer},
sha256_digest,
};
/// The mediatype for WASM layers.
pub const WASM_LAYER_MEDIA_TYPE: &str = "application/vnd.wasm.content.layer.v1+wasm";
/// The mediatype for a WASM image config.
pub const WASM_CONFIG_MEDIA_TYPE: &str = "application/vnd.wasm.config.v1+json";
/// The mediatype for an docker v2 schema 2 manifest.
pub const IMAGE_MANIFEST_MEDIA_TYPE: &str = "application/vnd.docker.distribution.manifest.v2+json";
/// The mediatype for an docker v2 shema 2 manifest list.
pub const IMAGE_MANIFEST_LIST_MEDIA_TYPE: &str =
"application/vnd.docker.distribution.manifest.list.v2+json";
/// The mediatype for an OCI image index manifest.
pub const OCI_IMAGE_INDEX_MEDIA_TYPE: &str = "application/vnd.oci.image.index.v1+json";
/// The mediatype for an OCI image manifest.
pub const OCI_IMAGE_MEDIA_TYPE: &str = "application/vnd.oci.image.manifest.v1+json";
/// The mediatype for an image config (manifest).
pub const IMAGE_CONFIG_MEDIA_TYPE: &str = "application/vnd.oci.image.config.v1+json";
/// The mediatype that Docker uses for image configs.
pub const IMAGE_DOCKER_CONFIG_MEDIA_TYPE: &str = "application/vnd.docker.container.image.v1+json";
/// The mediatype for a layer.
pub const IMAGE_LAYER_MEDIA_TYPE: &str = "application/vnd.oci.image.layer.v1.tar";
/// The mediatype for a layer that is gzipped.
pub const IMAGE_LAYER_GZIP_MEDIA_TYPE: &str = "application/vnd.oci.image.layer.v1.tar+gzip";
/// The mediatype that Docker uses for a layer that is tarred.
pub const IMAGE_DOCKER_LAYER_TAR_MEDIA_TYPE: &str = "application/vnd.docker.image.rootfs.diff.tar";
/// The mediatype that Docker uses for a layer that is gzipped.
pub const IMAGE_DOCKER_LAYER_GZIP_MEDIA_TYPE: &str =
"application/vnd.docker.image.rootfs.diff.tar.gzip";
/// The mediatype for a layer that is nondistributable.
pub const IMAGE_LAYER_NONDISTRIBUTABLE_MEDIA_TYPE: &str =
"application/vnd.oci.image.layer.nondistributable.v1.tar";
/// The mediatype for a layer that is nondistributable and gzipped.
pub const IMAGE_LAYER_NONDISTRIBUTABLE_GZIP_MEDIA_TYPE: &str =
"application/vnd.oci.image.layer.nondistributable.v1.tar+gzip";
/// An image, or image index, OCI manifest
#[derive(serde::Serialize, serde::Deserialize, Clone, Debug)]
#[serde(untagged)]
pub enum OciManifest {
/// An OCI image manifest
Image(OciImageManifest),
/// An OCI image index manifest
ImageIndex(OciImageIndex),
}
impl OciManifest {
/// Returns the appropriate content-type for each variant.
pub fn content_type(&self) -> &str {
match self {
OciManifest::Image(image) => {
image.media_type.as_deref().unwrap_or(OCI_IMAGE_MEDIA_TYPE)
}
OciManifest::ImageIndex(image) => image
.media_type
.as_deref()
.unwrap_or(IMAGE_MANIFEST_LIST_MEDIA_TYPE),
}
}
}
/// The OCI image manifest describes an OCI image.
///
/// It is part of the OCI specification, and is defined [here](https://github.com/opencontainers/image-spec/blob/main/manifest.md)
#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)]
#[serde(rename_all = "camelCase")]
pub struct OciImageManifest {
/// This is a schema version.
///
/// The specification does not specify the width of this integer.
/// However, the only version allowed by the specification is `2`.
/// So we have made this a u8.
pub schema_version: u8,
/// This is an optional media type describing this manifest.
///
/// This property SHOULD be used and [remain compatible](https://github.com/opencontainers/image-spec/blob/main/media-types.md#compatibility-matrix)
/// with earlier versions of this specification and with other similar external formats.
#[serde(skip_serializing_if = "Option::is_none")]
pub media_type: Option<String>,
/// The image configuration.
///
/// This object is required.
pub config: OciDescriptor,
/// The OCI image layers
///
/// The specification is unclear whether this is required. We have left it
/// required, assuming an empty vector can be used if necessary.
pub layers: Vec<OciDescriptor>,
/// The OCI artifact type
///
/// This OPTIONAL property contains the type of an artifact when the manifest is used for an
/// artifact. This MUST be set when config.mediaType is set to the empty value. If defined,
/// the value MUST comply with RFC 6838, including the naming requirements in its section 4.2,
/// and MAY be registered with IANA. Implementations storing or copying image manifests
/// MUST NOT error on encountering an artifactType that is unknown to the implementation.
///
/// Introduced in OCI Image Format spec v1.1
#[serde(skip_serializing_if = "Option::is_none")]
pub artifact_type: Option<String>,
/// The annotations for this manifest
///
/// The specification says "If there are no annotations then this property
/// MUST either be absent or be an empty map."
/// TO accomodate either, this is optional.
#[serde(skip_serializing_if = "Option::is_none")]
pub annotations: Option<BTreeMap<String, String>>,
}
impl Default for OciImageManifest {
fn default() -> Self {
OciImageManifest {
schema_version: 2,
media_type: None,
config: OciDescriptor::default(),
layers: vec![],
artifact_type: None,
annotations: None,
}
}
}
impl OciImageManifest {
/// Create a new OciImageManifest using the given parameters
///
/// This can be useful to create an OCI Image Manifest with
/// custom annotations.
pub fn build(
layers: &[ImageLayer],
config: &Config,
annotations: Option<BTreeMap<String, String>>,
) -> Self {
let mut manifest = OciImageManifest::default();
manifest.config.media_type = config.media_type.to_string();
manifest.config.size = config.data.len() as i64;
manifest.config.digest = sha256_digest(&config.data);
manifest.annotations = annotations;
for layer in layers {
let digest = sha256_digest(&layer.data);
let descriptor = OciDescriptor {
size: layer.data.len() as i64,
digest,
media_type: layer.media_type.to_string(),
annotations: layer.annotations.clone(),
..Default::default()
};
manifest.layers.push(descriptor);
}
manifest
}
}
impl From<OciImageIndex> for OciManifest {
fn from(m: OciImageIndex) -> Self {
Self::ImageIndex(m)
}
}
impl From<OciImageManifest> for OciManifest {
fn from(m: OciImageManifest) -> Self {
Self::Image(m)
}
}
impl std::fmt::Display for OciManifest {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
OciManifest::Image(oci_image_manifest) => write!(f, "{}", oci_image_manifest),
OciManifest::ImageIndex(oci_image_index) => write!(f, "{}", oci_image_index),
}
}
}
impl std::fmt::Display for OciImageIndex {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let media_type = self
.media_type
.clone()
.unwrap_or_else(|| String::from("N/A"));
let manifests: Vec<String> = self.manifests.iter().map(|m| m.to_string()).collect();
write!(
f,
"OCI Image Index( schema-version: '{}', media-type: '{}', manifests: '{}' )",
self.schema_version,
media_type,
manifests.join(","),
)
}
}
impl std::fmt::Display for OciImageManifest {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let media_type = self
.media_type
.clone()
.unwrap_or_else(|| String::from("N/A"));
let annotations = self.annotations.clone().unwrap_or_default();
let layers: Vec<String> = self.layers.iter().map(|l| l.to_string()).collect();
write!(
f,
"OCI Image Manifest( schema-version: '{}', media-type: '{}', config: '{}', artifact-type: '{:?}', layers: '{:?}', annotations: '{:?}' )",
self.schema_version,
media_type,
self.config,
self.artifact_type,
layers,
annotations,
)
}
}
/// Versioned provides a struct with the manifest's schemaVersion and mediaType.
/// Incoming content with unknown schema versions can be decoded against this
/// struct to check the version.
#[derive(Clone, Debug, serde::Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Versioned {
/// schema_version is the image manifest schema that this image follows
pub schema_version: i32,
/// media_type is the media type of this schema.
#[serde(skip_serializing_if = "Option::is_none")]
pub media_type: Option<String>,
}
/// The OCI descriptor is a generic object used to describe other objects.
///
/// It is defined in the [OCI Image Specification](https://github.com/opencontainers/image-spec/blob/main/descriptor.md#properties):
#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)]
#[serde(rename_all = "camelCase")]
pub struct OciDescriptor {
/// The media type of this descriptor.
///
/// Layers, config, and manifests may all have descriptors. Each
/// is differentiated by its mediaType.
///
/// This REQUIRED property contains the media type of the referenced
/// content. Values MUST comply with RFC 6838, including the naming
/// requirements in its section 4.2.
pub media_type: String,
/// The SHA 256 or 512 digest of the object this describes.
///
/// This REQUIRED property is the digest of the targeted content, conforming
/// to the requirements outlined in Digests. Retrieved content SHOULD be
/// verified against this digest when consumed via untrusted sources.
pub digest: String,
/// The size, in bytes, of the object this describes.
///
/// This REQUIRED property specifies the size, in bytes, of the raw
/// content. This property exists so that a client will have an expected
/// size for the content before processing. If the length of the retrieved
/// content does not match the specified length, the content SHOULD NOT be
/// trusted.
pub size: i64,
/// This OPTIONAL property specifies a list of URIs from which this
/// object MAY be downloaded. Each entry MUST conform to RFC 3986.
/// Entries SHOULD use the http and https schemes, as defined in RFC 7230.
#[serde(skip_serializing_if = "Option::is_none")]
pub urls: Option<Vec<String>>,
/// This OPTIONAL property contains arbitrary metadata for this descriptor.
/// This OPTIONAL property MUST use the annotation rules.
/// <https://github.com/opencontainers/image-spec/blob/main/annotations.md#rules>
#[serde(skip_serializing_if = "Option::is_none")]
pub annotations: Option<BTreeMap<String, String>>,
}
impl std::fmt::Display for OciDescriptor {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let urls = self.urls.clone().unwrap_or_default();
let annotations = self.annotations.clone().unwrap_or_default();
write!(
f,
"( media-type: '{}', digest: '{}', size: '{}', urls: '{:?}', annotations: '{:?}' )",
self.media_type, self.digest, self.size, urls, annotations,
)
}
}
impl Default for OciDescriptor {
fn default() -> Self {
OciDescriptor {
media_type: IMAGE_CONFIG_MEDIA_TYPE.to_owned(),
digest: "".to_owned(),
size: 0,
urls: None,
annotations: None,
}
}
}
/// The image index is a higher-level manifest which points to specific image manifest.
///
/// It is part of the OCI specification, and is defined [here](https://github.com/opencontainers/image-spec/blob/main/image-index.md):
#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)]
#[serde(rename_all = "camelCase")]
pub struct OciImageIndex {
/// This is a schema version.
///
/// The specification does not specify the width of this integer.
/// However, the only version allowed by the specification is `2`.
/// So we have made this a u8.
pub schema_version: u8,
/// This is an optional media type describing this manifest.
///
/// It is reserved for compatibility, but the specification does not seem
/// to recommend setting it.
#[serde(skip_serializing_if = "Option::is_none")]
pub media_type: Option<String>,
/// This property contains a list of manifests for specific platforms.
/// The spec says this field must be present but the value may be an empty array.
pub manifests: Vec<ImageIndexEntry>,
/// The annotations for this manifest
///
/// The specification says "If there are no annotations then this property
/// MUST either be absent or be an empty map."
/// TO accomodate either, this is optional.
#[serde(skip_serializing_if = "Option::is_none")]
pub annotations: Option<BTreeMap<String, String>>,
}
/// The manifest entry of an `ImageIndex`.
///
/// It is part of the OCI specification, and is defined in the `manifests`
/// section [here](https://github.com/opencontainers/image-spec/blob/main/image-index.md#image-index-property-descriptions):
#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ImageIndexEntry {
/// The media type of this descriptor.
///
/// Layers, config, and manifests may all have descriptors. Each
/// is differentiated by its mediaType.
///
/// This REQUIRED property contains the media type of the referenced
/// content. Values MUST comply with RFC 6838, including the naming
/// requirements in its section 4.2.
pub media_type: String,
/// The SHA 256 or 512 digest of the object this describes.
///
/// This REQUIRED property is the digest of the targeted content, conforming
/// to the requirements outlined in Digests. Retrieved content SHOULD be
/// verified against this digest when consumed via untrusted sources.
pub digest: String,
/// The size, in bytes, of the object this describes.
///
/// This REQUIRED property specifies the size, in bytes, of the raw
/// content. This property exists so that a client will have an expected
/// size for the content before processing. If the length of the retrieved
/// content does not match the specified length, the content SHOULD NOT be
/// trusted.
pub size: i64,
/// This OPTIONAL property describes the minimum runtime requirements of the image.
/// This property SHOULD be present if its target is platform-specific.
#[serde(skip_serializing_if = "Option::is_none")]
pub platform: Option<Platform>,
/// This OPTIONAL property contains arbitrary metadata for the image index.
/// This OPTIONAL property MUST use the [annotation rules](https://github.com/opencontainers/image-spec/blob/main/annotations.md#rules).
#[serde(skip_serializing_if = "Option::is_none")]
pub annotations: Option<BTreeMap<String, String>>,
}
impl std::fmt::Display for ImageIndexEntry {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let platform = self
.platform
.clone()
.map(|p| p.to_string())
.unwrap_or_else(|| String::from("N/A"));
let annotations = self.annotations.clone().unwrap_or_default();
write!(
f,
"(media-type: '{}', digest: '{}', size: '{}', platform: '{}', annotations: {:?})",
self.media_type, self.digest, self.size, platform, annotations,
)
}
}
/// Platform specific fields of an Image Index manifest entry.
///
/// It is part of the OCI specification, and is in the `platform`
/// section [here](https://github.com/opencontainers/image-spec/blob/main/image-index.md#image-index-property-descriptions):
#[derive(Debug, Clone, serde::Deserialize, serde::Serialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct Platform {
/// This REQUIRED property specifies the CPU architecture.
/// Image indexes SHOULD use, and implementations SHOULD understand, values
/// listed in the Go Language document for [`GOARCH`](https://golang.org/doc/install/source#environment).
pub architecture: String,
/// This REQUIRED property specifies the operating system.
/// Image indexes SHOULD use, and implementations SHOULD understand, values
/// listed in the Go Language document for [`GOOS`](https://golang.org/doc/install/source#environment).
pub os: String,
/// This OPTIONAL property specifies the version of the operating system
/// targeted by the referenced blob.
/// Implementations MAY refuse to use manifests where `os.version` is not known
/// to work with the host OS version.
/// Valid values are implementation-defined. e.g. `10.0.14393.1066` on `windows`.
#[serde(rename = "os.version")]
#[serde(skip_serializing_if = "Option::is_none")]
pub os_version: Option<String>,
/// This OPTIONAL property specifies an array of strings, each specifying a mandatory OS feature.
/// When `os` is `windows`, image indexes SHOULD use, and implementations SHOULD understand the following values:
/// - `win32k`: image requires `win32k.sys` on the host (Note: `win32k.sys` is missing on Nano Server)
///
/// When `os` is not `windows`, values are implementation-defined and SHOULD be submitted to this specification for standardization.
#[serde(rename = "os.features")]
#[serde(skip_serializing_if = "Option::is_none")]
pub os_features: Option<Vec<String>>,
/// This OPTIONAL property specifies the variant of the CPU.
/// Image indexes SHOULD use, and implementations SHOULD understand, `variant` values listed in the [Platform Variants](#platform-variants) table.
#[serde(skip_serializing_if = "Option::is_none")]
pub variant: Option<String>,
/// This property is RESERVED for future versions of the specification.
#[serde(skip_serializing_if = "Option::is_none")]
pub features: Option<Vec<String>>,
}
impl std::fmt::Display for Platform {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let os_version = self
.os_version
.clone()
.unwrap_or_else(|| String::from("N/A"));
let os_features = self.os_features.clone().unwrap_or_default();
let variant = self.variant.clone().unwrap_or_else(|| String::from("N/A"));
let features = self.os_features.clone().unwrap_or_default();
write!(f, "( architecture: '{}', os: '{}', os-version: '{}', os-features: '{:?}', variant: '{}', features: '{:?}' )",
self.architecture,
self.os,
os_version,
os_features,
variant,
features,
)
}
}
#[cfg(test)]
mod test {
use super::*;
const TEST_MANIFEST: &str = r#"{
"schemaVersion": 2,
"mediaType": "application/vnd.docker.distribution.manifest.v2+json",
"config": {
"mediaType": "application/vnd.docker.container.image.v1+json",
"size": 2,
"digest": "sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a"
},
"artifactType": "application/vnd.wasm.component.v1+wasm",
"layers": [
{
"mediaType": "application/vnd.wasm.content.layer.v1+wasm",
"size": 1615998,
"digest": "sha256:f9c91f4c280ab92aff9eb03b279c4774a80b84428741ab20855d32004b2b983f",
"annotations": {
"org.opencontainers.image.title": "module.wasm"
}
}
]
}
"#;
#[test]
fn test_manifest() {
let manifest: OciImageManifest =
serde_json::from_str(TEST_MANIFEST).expect("parsed manifest");
assert_eq!(2, manifest.schema_version);
assert_eq!(
Some(IMAGE_MANIFEST_MEDIA_TYPE.to_owned()),
manifest.media_type
);
let config = manifest.config;
// Note that this is the Docker config media type, not the OCI one.
assert_eq!(IMAGE_DOCKER_CONFIG_MEDIA_TYPE.to_owned(), config.media_type);
assert_eq!(2, config.size);
assert_eq!(
"sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a".to_owned(),
config.digest
);
assert_eq!(
"application/vnd.wasm.component.v1+wasm".to_owned(),
manifest.artifact_type.unwrap()
);
assert_eq!(1, manifest.layers.len());
let wasm_layer = &manifest.layers[0];
assert_eq!(1_615_998, wasm_layer.size);
assert_eq!(WASM_LAYER_MEDIA_TYPE.to_owned(), wasm_layer.media_type);
assert_eq!(
1,
wasm_layer
.annotations
.as_ref()
.expect("annotations map")
.len()
);
}
}