Skip to content

Commit 47443c4

Browse files
author
Tom Fay
authored
use ocidir (#137)
* Use containers/ocidir for OCI image maniupulation * update to ocidir 0.2.1 * add straightforward fixes from #147 * avoid suppression of walkdir entry errors * use gnu headers consistently
1 parent a6a219c commit 47443c4

12 files changed

Lines changed: 637 additions & 796 deletions

File tree

Cargo.lock

Lines changed: 363 additions & 240 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@ anyhow = "1.0.75"
1515
chrono = { version = "0.4.26", features = ["clock"], default-features = false }
1616
clap = { version = "4.5.6", features = ["derive"] }
1717
clap-verbosity-flag = "2.0.0"
18-
dirs = "5.0.1"
1918
env_logger = "0.11.3"
2019
filetime = "0.2.22"
2120
flate2 = { version = "1.0.24", features = ["zlib"], default-features = false }
@@ -26,8 +25,6 @@ nix = { version = "0.29.0", features = [
2625
"signal",
2726
"user",
2827
], default-features = false }
29-
oci-spec = { version = "0.6.3", features = ["image"], default-features = false }
30-
openssl = "0.10.63"
3128
pathdiff = "0.2.1"
3229
pyo3 = { version = "0.22.1", features = ["auto-initialize"] }
3330
rpm = { version = "0.15.0", default-features = false }
@@ -41,6 +38,7 @@ toml = { version = "0.8.8" }
4138
url = { version = "2.2.2", features = ["serde"] }
4239
walkdir = "2.3.2"
4340
xattr = "1.0.1"
41+
ocidir = "0.2.1"
4442

4543
[dev-dependencies]
4644
test-temp-dir = "0.2.2"

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,11 @@ rpmoci features:
88
- **unprivileged** rpmoci can build images in environments without access to a container runtime, and without root access (this relies on the user being able to create [user namespaces](https://www.man7.org/linux/man-pages/man7/user_namespaces.7.html))
99
- **small** rpmoci images are built solely from the RPMs you request and their dependencies, so don't contain unnecessary dependencies.
1010

11+
rpmoci is a good fit for containerizing applications - you package your application as an RPM, and then use rpmoci to build a minimal container image from that RPM.
12+
1113
The design of rpmoci is influenced by [apko](https://github.com/chainguard-dev/apko) and [distroless](https://github.com/GoogleContainerTools/distroless) tooling.
14+
rpmoci is also similar to a smaller [`rpm-ostree compose image`](https://coreos.github.io/rpm-ostree/container/#creating-base-images), with a focus on building microservices.
15+
1216

1317
## Installing
1418

src/archive.rs

Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
//! Copyright (C) Microsoft Corporation.
2+
//!
3+
//! This program is free software: you can redistribute it and/or modify
4+
//! it under the terms of the GNU General Public License as published by
5+
//! the Free Software Foundation, either version 3 of the License, or
6+
//! (at your option) any later version.
7+
//!
8+
//! This program is distributed in the hope that it will be useful,
9+
//! but WITHOUT ANY WARRANTY; without even the implied warranty of
10+
//! MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11+
//! GNU General Public License for more details.
12+
//!
13+
//! You should have received a copy of the GNU General Public License
14+
//! along with this program. If not, see <https://www.gnu.org/licenses/>.
15+
use anyhow::{Context, Result};
16+
use std::{
17+
collections::{hash_map::Entry, HashMap},
18+
io::Write,
19+
os::unix::{
20+
fs::MetadataExt,
21+
prelude::{FileTypeExt, OsStrExt},
22+
},
23+
path::{Path, PathBuf},
24+
};
25+
use walkdir::WalkDir;
26+
27+
// https://mgorny.pl/articles/portability-of-tar-features.html#id25
28+
const PAX_SCHILY_XATTR: &[u8; 13] = b"SCHILY.xattr.";
29+
30+
/// custom implementation of tar-rs's append_dir_all that:
31+
/// - works around https://github.com/alexcrichton/tar-rs/issues/102 so that security capabilities are preserved
32+
/// - emulates tar's `--clamp-mtime` option so that any file/dir/symlink mtimes are no later than a specific value
33+
/// - supports hardlinks
34+
pub(super) fn append_dir_all_with_xattrs(
35+
builder: &mut tar::Builder<impl Write>,
36+
src_path: impl AsRef<Path>,
37+
clamp_mtime: i64,
38+
) -> Result<()> {
39+
let src_path = src_path.as_ref();
40+
// Map (dev, inode) -> path for hardlinks
41+
let mut hardlinks: HashMap<(u64, u64), PathBuf> = HashMap::new();
42+
43+
for entry in WalkDir::new(src_path)
44+
.follow_links(false)
45+
.sort_by_file_name()
46+
.into_iter()
47+
{
48+
let entry = entry?;
49+
let meta = entry.metadata()?;
50+
// skip sockets as tar-rs errors when trying to archive them.
51+
// For comparison, umoci also errors, whereas docker skips them
52+
if meta.file_type().is_socket() {
53+
continue;
54+
}
55+
56+
let rel_path = pathdiff::diff_paths(entry.path(), src_path)
57+
.expect("walkdir returns path inside of search root");
58+
if rel_path == Path::new("") {
59+
continue;
60+
}
61+
62+
if entry.file_type().is_symlink() {
63+
if meta.mtime() > clamp_mtime {
64+
// Setting the mtime on a symlink is fiddly with tar-rs, so we use filetime to change
65+
// the mtime before adding the symlink to the tar archive
66+
let mtime = filetime::FileTime::from_unix_time(clamp_mtime, 0);
67+
filetime::set_symlink_file_times(entry.path(), mtime, mtime)?;
68+
}
69+
add_pax_extension_header(entry.path(), builder)?;
70+
builder.append_path_with_name(entry.path(), rel_path)?;
71+
} else if entry.file_type().is_file() || entry.file_type().is_dir() {
72+
add_pax_extension_header(entry.path(), builder)?;
73+
74+
// If this is a hardlink, add a link header instead of the file
75+
// if this isn't the first time we've seen this inode
76+
if meta.nlink() > 1 {
77+
match hardlinks.entry((meta.dev(), meta.ino())) {
78+
Entry::Occupied(e) => {
79+
// Add link header and continue to next entry
80+
let mut header = tar::Header::new_gnu();
81+
header.set_metadata(&meta);
82+
if meta.mtime() > clamp_mtime {
83+
header.set_mtime(clamp_mtime as u64);
84+
}
85+
header.set_entry_type(tar::EntryType::Link);
86+
header.set_cksum();
87+
builder.append_link(&mut header, &rel_path, e.get())?;
88+
continue;
89+
}
90+
Entry::Vacant(e) => {
91+
// This is the first time we've seen this inode
92+
e.insert(rel_path.clone());
93+
}
94+
}
95+
}
96+
97+
let mut header = tar::Header::new_gnu();
98+
header.set_size(meta.len());
99+
header.set_metadata(&meta);
100+
if meta.mtime() > clamp_mtime {
101+
header.set_mtime(clamp_mtime as u64);
102+
}
103+
if entry.file_type().is_file() {
104+
builder.append_data(
105+
&mut header,
106+
rel_path,
107+
&mut std::fs::File::open(entry.path())?,
108+
)?;
109+
} else {
110+
builder.append_data(&mut header, rel_path, &mut std::io::empty())?;
111+
};
112+
}
113+
}
114+
115+
Ok(())
116+
}
117+
118+
// Convert any extended attributes on the specified path to a tar PAX extension header, and add it to the tar archive
119+
fn add_pax_extension_header(
120+
path: impl AsRef<Path>,
121+
builder: &mut tar::Builder<impl Write>,
122+
) -> Result<(), anyhow::Error> {
123+
let path = path.as_ref();
124+
let xattrs = xattr::list(path)
125+
.with_context(|| format!("Failed to list xattrs from `{}`", path.display()))?;
126+
let mut pax_header = tar::Header::new_gnu();
127+
let mut pax_data = Vec::new();
128+
for key in xattrs {
129+
let value = xattr::get(path, &key)
130+
.with_context(|| {
131+
format!(
132+
"Failed to get xattr `{}` from `{}`",
133+
key.to_string_lossy(),
134+
path.display()
135+
)
136+
})?
137+
.unwrap_or_default();
138+
139+
// each entry is "<len> <key>=<value>\n": https://www.ibm.com/docs/en/zos/2.3.0?topic=SSLTBW_2.3.0/com.ibm.zos.v2r3.bpxa500/paxex.html
140+
let data_len = PAX_SCHILY_XATTR.len() + key.as_bytes().len() + value.len() + 3;
141+
// Calculate the total length, including the length of the length field
142+
let mut len_len = 1;
143+
while data_len + len_len >= 10usize.pow(len_len.try_into().unwrap()) {
144+
len_len += 1;
145+
}
146+
write!(pax_data, "{} ", data_len + len_len)?;
147+
pax_data.write_all(PAX_SCHILY_XATTR)?;
148+
pax_data.write_all(key.as_bytes())?;
149+
pax_data.write_all("=".as_bytes())?;
150+
pax_data.write_all(&value)?;
151+
pax_data.write_all("\n".as_bytes())?;
152+
}
153+
if !pax_data.is_empty() {
154+
pax_header.set_size(pax_data.len() as u64);
155+
pax_header.set_entry_type(tar::EntryType::XHeader);
156+
pax_header.set_cksum();
157+
builder.append(&pax_header, &*pax_data)?;
158+
}
159+
Ok(())
160+
}

src/config.rs

Lines changed: 14 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
//! You should have received a copy of the GNU General Public License
1616
//! along with this program. If not, see <https://www.gnu.org/licenses/>.
1717
use anyhow::Result;
18-
use oci_spec::{
18+
use ocidir::oci_spec::{
1919
image::{Arch, ConfigBuilder, ImageConfiguration, ImageConfigurationBuilder, Os},
2020
OciSpecError,
2121
};
@@ -204,11 +204,10 @@ impl ImageConfig {
204204

205205
#[cfg(test)]
206206
mod tests {
207-
use std::collections::HashMap;
208-
209-
use crate::config::ImageConfig;
210-
211207
use super::Config;
208+
use crate::config::ImageConfig;
209+
use ocidir::oci_spec::image::ImageConfiguration;
210+
use std::collections::HashMap;
212211

213212
#[test]
214213
fn parse_basic() {
@@ -287,23 +286,21 @@ mod tests {
287286
let config_with_path = r#"
288287
envs = { PATH = "/usr/bin"}
289288
"#;
290-
let config: oci_spec::image::ImageConfiguration =
291-
toml::from_str::<ImageConfig>(config_with_path)
292-
.unwrap()
293-
.to_oci_image_configuration(HashMap::new(), chrono::Utc::now())
294-
.unwrap();
289+
let config: ImageConfiguration = toml::from_str::<ImageConfig>(config_with_path)
290+
.unwrap()
291+
.to_oci_image_configuration(HashMap::new(), chrono::Utc::now())
292+
.unwrap();
295293
let envs = config.config().as_ref().unwrap().env().as_ref().unwrap();
296294
assert!(envs.iter().any(|e| e == "PATH=/usr/bin"));
297295
assert_eq!(envs.len(), 1);
298296

299297
let config_without_path = r#"
300298
envs = { FOO = "bar"}
301299
"#;
302-
let config: oci_spec::image::ImageConfiguration =
303-
toml::from_str::<ImageConfig>(config_without_path)
304-
.unwrap()
305-
.to_oci_image_configuration(HashMap::new(), chrono::Utc::now())
306-
.unwrap();
300+
let config: ImageConfiguration = toml::from_str::<ImageConfig>(config_without_path)
301+
.unwrap()
302+
.to_oci_image_configuration(HashMap::new(), chrono::Utc::now())
303+
.unwrap();
307304
let envs = config.config().as_ref().unwrap().env().as_ref().unwrap();
308305
assert!(envs
309306
.iter()
@@ -317,7 +314,7 @@ mod tests {
317314
labels = { "foo.bar" = "baz"}
318315
"#;
319316
// No additional labels
320-
let config: oci_spec::image::ImageConfiguration = toml::from_str::<ImageConfig>(config_str)
317+
let config: ImageConfiguration = toml::from_str::<ImageConfig>(config_str)
321318
.unwrap()
322319
.to_oci_image_configuration(HashMap::new(), chrono::Utc::now())
323320
.unwrap();
@@ -331,7 +328,7 @@ mod tests {
331328
]
332329
.into_iter()
333330
.collect();
334-
let config: oci_spec::image::ImageConfiguration = toml::from_str::<ImageConfig>(config_str)
331+
let config: ImageConfiguration = toml::from_str::<ImageConfig>(config_str)
335332
.unwrap()
336333
.to_oci_image_configuration(extra_labels, chrono::Utc::now())
337334
.unwrap();

src/lib.rs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,10 @@ use std::{
2222
};
2323

2424
use anyhow::{bail, Context};
25+
mod archive;
2526
pub mod cli;
2627
pub mod config;
2728
pub mod lockfile;
28-
mod oci;
29-
mod sha256_writer;
3029
pub mod subid;
3130
pub mod write;
3231
use anyhow::Result;

0 commit comments

Comments
 (0)