oci_tar_builder/
lib.rs

1use std::collections::HashMap;
2use std::fs::metadata;
3use std::io::Write;
4use std::path::PathBuf;
5
6use anyhow::{Context, Error, Result};
7use indexmap::IndexMap;
8use log::{debug, warn};
9use oci_spec::image::{
10    DescriptorBuilder, ImageConfiguration, ImageIndexBuilder, ImageManifestBuilder, MediaType,
11    PlatformBuilder, SCHEMA_VERSION,
12};
13use serde::Serialize;
14use sha256::{digest, try_digest};
15#[derive(Debug, Default)]
16pub struct Builder {
17    configs: Vec<(ImageConfiguration, String)>,
18    layers: Vec<(PathBuf, String)>,
19}
20
21#[derive(Serialize, Debug)]
22struct OciLayout {
23    #[serde(rename = "imageLayoutVersion")]
24    image_layout_version: String,
25}
26
27impl Default for OciLayout {
28    fn default() -> Self {
29        Self {
30            image_layout_version: "1.0.0".to_string(),
31        }
32    }
33}
34
35#[derive(Serialize, Debug)]
36struct DockerManifest {
37    #[serde(rename = "Config")]
38    config: String,
39    #[serde(rename = "RepoTags")]
40    repo_tags: Vec<String>,
41    #[serde(rename = "Layers")]
42    layers: Vec<String>,
43}
44
45pub const WASM_LAYER_MEDIA_TYPE: &str =
46    "application/vnd.bytecodealliance.wasm.component.layer.v0+wasm";
47
48impl Builder {
49    pub fn add_config(&mut self, config: ImageConfiguration, name: String) -> &mut Self {
50        self.configs.push((config, name));
51        self
52    }
53
54    pub fn add_layer(&mut self, layer: &PathBuf) -> &mut Self {
55        self.layers.push((layer.to_owned(), "".to_string()));
56        self
57    }
58
59    pub fn add_layer_with_media_type(&mut self, layer: &PathBuf, media_type: String) -> &mut Self {
60        self.layers.push((layer.to_owned(), media_type));
61        self
62    }
63
64    pub fn build<W: Write>(&mut self, w: W) -> Result<(), Error> {
65        let mut tb = tar::Builder::new(w);
66        let mut manifests = Vec::new();
67        // use IndexMap in order to keep layers in order they were added.
68        let mut layer_digests = IndexMap::new();
69
70        if self.configs.len() > 1 {
71            anyhow::bail!("only one config is supported");
72        }
73
74        let mut mfst = DockerManifest {
75            config: "".to_string(),
76            repo_tags: Vec::new(),
77            layers: Vec::new(),
78        };
79
80        for layer in self.layers.iter() {
81            let dgst = try_digest(layer.0.as_path()).context("failed to digest layer")?;
82            let meta = metadata(layer.0.clone()).context("could not get layer metadata")?;
83            let oci_digest = "sha256:".to_owned() + &dgst;
84
85            let mut media_type = MediaType::ImageLayer;
86            if !layer.1.is_empty() {
87                media_type = MediaType::Other(layer.1.clone());
88            }
89            let desc = DescriptorBuilder::default()
90                // TODO: check file headers to determine mediatype? Could also just require it to be passed in on add_layer
91                .media_type(media_type)
92                .digest(&oci_digest)
93                .size(meta.len() as i64)
94                .build()
95                .context("failed to build descriptor")?;
96            layer_digests.insert(oci_digest, desc);
97
98            let mut th = tar::Header::new_gnu();
99            th.set_mode(0o444);
100            th.set_size(meta.len());
101            let p = "blobs/sha256/".to_owned() + &dgst;
102            th.set_path(&p).context("could not set path for layer")?;
103            th.set_cksum();
104            let f = std::fs::File::open(layer.0.clone()).context("could not open layer")?;
105            tb.append(&th, f)?;
106
107            mfst.layers.push(p.to_string());
108        }
109
110        for config in self.configs.iter() {
111            let s = config.0.to_string().context("failed to serialize config")?;
112            let b = s.as_bytes();
113            let dgst = digest(b);
114            let mut th = tar::Header::new_gnu();
115            th.set_mode(0o444);
116            th.set_size(b.len() as u64);
117            let p = "blobs/sha256/".to_owned() + &dgst;
118            th.set_path(&p).context("could not set path for config")?;
119            th.set_cksum();
120            tb.append(&th, b)?;
121
122            mfst.config = p.to_string();
123
124            let desc = DescriptorBuilder::default()
125                .media_type(MediaType::ImageConfig)
126                .size(b.len() as i64)
127                .digest("sha256:".to_owned() + &dgst)
128                .build()
129                .context("failed to build descriptor")?;
130
131            // add all layer_digests including any OCI WASM types that are may not be in the rootfs
132            let mut layers = Vec::new();
133            for (_k, v) in layer_digests.iter() {
134                layers.push(v.clone());
135            }
136
137            for id in config.0.rootfs().diff_ids().iter() {
138                debug!("id: {}", id);
139                if layer_digests.get(id).is_none() {
140                    warn!("rootfs diff with id {} not found in layers", id);
141                }
142            }
143
144            let mut annotations = HashMap::new();
145            if config.1.contains(':') {
146                let split = config.1.split(':').collect::<Vec<&str>>()[1];
147                annotations.insert(
148                    "org.opencontainers.image.ref.name".to_string(),
149                    split.to_string(),
150                );
151            }
152            mfst.repo_tags.push(config.1.clone());
153            annotations.insert("io.containerd.image.name".to_string(), config.1.clone());
154
155            let manifest = ImageManifestBuilder::default()
156                .schema_version(SCHEMA_VERSION)
157                .media_type(MediaType::ImageManifest);
158
159            let manifest = manifest
160                .layers(layers)
161                .config(desc)
162                .annotations(annotations.clone())
163                .build()
164                .context("failed to build manifest")?
165                .to_string()
166                .context("failed to serialize manifest")?;
167            let b = manifest.as_bytes();
168            let dgst = digest(b);
169
170            let mut th = tar::Header::new_gnu();
171            th.set_mode(0o444);
172            th.set_size(b.len() as u64);
173            th.set_path("blobs/sha256/".to_owned() + &dgst)
174                .context("could not set path for manifest")?;
175            th.set_cksum();
176            tb.append(&th, b)?;
177
178            let platform = PlatformBuilder::default()
179                .os(config.0.os().clone())
180                .architecture(config.0.architecture().clone())
181                .build()
182                .context("failed to build platform")?;
183
184            let desc = DescriptorBuilder::default()
185                .media_type(MediaType::ImageManifest)
186                .size(b.len() as i64)
187                .platform(platform)
188                .annotations(annotations)
189                .digest("sha256:".to_owned() + &dgst)
190                .build()
191                .context("failed to build descriptor")?;
192
193            manifests.push(desc);
194        }
195
196        let idx = ImageIndexBuilder::default()
197            .schema_version(SCHEMA_VERSION)
198            .media_type(MediaType::ImageIndex)
199            .manifests(manifests)
200            .build()
201            .context("failed to build index")?;
202
203        let s = idx.to_string().context("failed to serialize index")?;
204        let b = s.as_bytes();
205
206        let mut th = tar::Header::new_gnu();
207        th.set_path("index.json")
208            .context("could not set path to index.json")?;
209        th.set_size(b.len() as u64);
210        th.set_mode(0o644);
211        th.set_cksum();
212
213        tb.append(&th, b)?;
214
215        let layout = serde_json::to_string(&OciLayout::default())
216            .context("failed to serialize oci-layout")?;
217        let b = layout.as_bytes();
218
219        let mut th = tar::Header::new_gnu();
220        th.set_path("oci-layout")
221            .context("could not set path for oci-layout file")?;
222        th.set_size(b.len() as u64);
223        th.set_mode(0o644);
224        th.set_cksum();
225        tb.append(&th, b)?;
226
227        let mfst_data =
228            serde_json::to_string(&vec![&mfst]).context("failed to serialize manifest")?;
229        let mut th = tar::Header::new_gnu();
230        th.set_path("manifest.json")?;
231        th.set_mode(0o644);
232        th.set_size(mfst_data.as_bytes().len() as u64);
233        th.set_cksum();
234        tb.append(&th, mfst_data.as_bytes())?;
235
236        tb.finish()?;
237
238        Ok(())
239    }
240}