|
38 | 38 | //! This module handles both transparently. Use `is_container_image()` to check. |
39 | 39 |
|
40 | 40 | use std::collections::{HashMap, HashSet}; |
| 41 | +use std::str::FromStr; |
41 | 42 | use std::sync::Arc; |
42 | 43 |
|
43 | 44 | use anyhow::{ensure, Context, Result}; |
44 | 45 | use containers_image_proxy::oci_spec::image::{ |
45 | | - Descriptor, ImageConfiguration, ImageManifest, MediaType, |
| 46 | + Descriptor, DescriptorBuilder, Digest as OciDigest, ImageConfiguration, ImageManifest, |
| 47 | + ImageManifestBuilder, MediaType, |
46 | 48 | }; |
47 | 49 | use rustix::fs::{openat, readlinkat, unlinkat, AtFlags, Dir, Mode, OFlags}; |
48 | 50 | use rustix::io::Errno; |
@@ -389,7 +391,15 @@ impl<ObjectID: FsVerityHashValue> OciImage<ObjectID> { |
389 | 391 |
|
390 | 392 | let referrers_value: Vec<serde_json::Value> = referrers |
391 | 393 | .iter() |
392 | | - .map(|(digest, _verity)| serde_json::json!({ "digest": digest })) |
| 394 | + .map(|(digest, verity)| { |
| 395 | + let mut entry = serde_json::json!({ "digest": digest }); |
| 396 | + if let Ok(referrer_img) = OciImage::open(repo, digest, Some(verity)) { |
| 397 | + if let Some(artifact_type) = referrer_img.manifest().artifact_type() { |
| 398 | + entry["artifactType"] = serde_json::json!(artifact_type.to_string()); |
| 399 | + } |
| 400 | + } |
| 401 | + entry |
| 402 | + }) |
393 | 403 | .collect(); |
394 | 404 |
|
395 | 405 | Ok(serde_json::json!({ |
@@ -599,6 +609,76 @@ pub fn write_manifest<ObjectID: FsVerityHashValue>( |
599 | 609 | Ok((manifest_digest.to_string(), id)) |
600 | 610 | } |
601 | 611 |
|
| 612 | +/// Seals an image by tag: creates a sealed config, a new manifest referencing it, |
| 613 | +/// and updates the tag to point to the new manifest. |
| 614 | +/// |
| 615 | +/// This is the complete seal workflow. It: |
| 616 | +/// 1. Opens the image by tag to get the original manifest and layer refs |
| 617 | +/// 2. Calls `seal()` to create a config with the fsverity label |
| 618 | +/// 3. Builds a new manifest referencing the sealed config (same layers) |
| 619 | +/// 4. Stores the new manifest and updates the tag |
| 620 | +/// |
| 621 | +/// Returns the new manifest digest. |
| 622 | +pub fn seal_image<ObjectID: FsVerityHashValue>( |
| 623 | + repo: &Arc<Repository<ObjectID>>, |
| 624 | + name: &str, |
| 625 | +) -> Result<String> { |
| 626 | + let img = OciImage::open_ref(repo, name)?; |
| 627 | + ensure!( |
| 628 | + img.is_container_image(), |
| 629 | + "Can only seal container images, not artifacts" |
| 630 | + ); |
| 631 | + |
| 632 | + let config_digest = img.config_digest().to_string(); |
| 633 | + let (sealed_config_digest, sealed_config_verity) = |
| 634 | + crate::seal(repo, &config_digest, None)?; |
| 635 | + |
| 636 | + // Build a new config descriptor for the sealed config |
| 637 | + let sealed_config_json = { |
| 638 | + let config_id = crate::config_identifier(&sealed_config_digest); |
| 639 | + let (data, _) = read_external_splitstream( |
| 640 | + repo, |
| 641 | + &config_id, |
| 642 | + Some(&sealed_config_verity), |
| 643 | + Some(OCI_CONFIG_CONTENT_TYPE), |
| 644 | + )?; |
| 645 | + data |
| 646 | + }; |
| 647 | + |
| 648 | + let new_config_descriptor = DescriptorBuilder::default() |
| 649 | + .media_type(MediaType::ImageConfig) |
| 650 | + .digest( |
| 651 | + OciDigest::from_str(&sealed_config_digest) |
| 652 | + .context("parsing sealed config digest")?, |
| 653 | + ) |
| 654 | + .size(sealed_config_json.len() as u64) |
| 655 | + .build() |
| 656 | + .context("building config descriptor")?; |
| 657 | + |
| 658 | + // Build new manifest with same layers but sealed config |
| 659 | + let new_manifest = ImageManifestBuilder::default() |
| 660 | + .schema_version(2u32) |
| 661 | + .media_type(MediaType::ImageManifest) |
| 662 | + .config(new_config_descriptor) |
| 663 | + .layers(img.manifest().layers().to_vec()) |
| 664 | + .build() |
| 665 | + .context("building sealed manifest")?; |
| 666 | + |
| 667 | + let new_manifest_json = new_manifest.to_string()?; |
| 668 | + let new_manifest_digest = hash(new_manifest_json.as_bytes()); |
| 669 | + |
| 670 | + write_manifest( |
| 671 | + repo, |
| 672 | + &new_manifest, |
| 673 | + &new_manifest_digest, |
| 674 | + &sealed_config_verity, |
| 675 | + img.layer_refs(), |
| 676 | + Some(name), |
| 677 | + )?; |
| 678 | + |
| 679 | + Ok(new_manifest_digest) |
| 680 | +} |
| 681 | + |
602 | 682 | /// Checks if a manifest exists. |
603 | 683 | pub fn has_manifest<ObjectID: FsVerityHashValue>( |
604 | 684 | repo: &Repository<ObjectID>, |
|
0 commit comments