Skip to content

Commit 824950e

Browse files
committed
ci: test get_blob_stream against old/new skopeo
Run tests in containers with skopeo 1.18 and >=1.19 to exercise both GetBlob fallback and GetRawBlob paths. Assisted-by: Codex CLI (GPT-5.2)
1 parent 98c7973 commit 824950e

File tree

2 files changed

+173
-1
lines changed

2 files changed

+173
-1
lines changed

.github/workflows/ci.yaml

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,3 +51,51 @@ jobs:
5151
fetch-depth: 20
5252
- name: Test ostree-rs-ext
5353
run: ./ci/test-ostree-rs-ext.sh
54+
55+
test-old-skopeo:
56+
name: Test (old skopeo)
57+
runs-on: ubuntu-latest
58+
steps:
59+
- name: Checkout repository
60+
uses: actions/checkout@v6
61+
with:
62+
ref: ${{ github.event.pull_request.head.sha }}
63+
fetch-depth: 20
64+
- name: Test in AlmaLinux bootc container (skopeo 1.18)
65+
run: |
66+
set -euxo pipefail
67+
rm -rf .ci-cargo-home
68+
mkdir -p .ci-cargo-home
69+
docker run --rm \
70+
-e CARGO_TARGET_DIR=/tmp/target \
71+
-e EXPECT_BLOB_STREAM_SOURCE=GetBlob \
72+
-v "$PWD:/src:ro" \
73+
-v "$PWD/.ci-cargo-home:/root/.cargo" \
74+
-w /src \
75+
quay.io/almalinuxorg/almalinux-bootc:10.0 \
76+
sh -lc 'dnf -y install cargo rustc >/dev/null && skopeo --version && cargo test --all-features -- --nocapture --quiet'
77+
sudo rm -rf .ci-cargo-home
78+
79+
test-new-skopeo:
80+
name: Test (new skopeo)
81+
runs-on: ubuntu-latest
82+
steps:
83+
- name: Checkout repository
84+
uses: actions/checkout@v6
85+
with:
86+
ref: ${{ github.event.pull_request.head.sha }}
87+
fetch-depth: 20
88+
- name: Test in Fedora container (skopeo >= 1.19)
89+
run: |
90+
set -euxo pipefail
91+
rm -rf .ci-cargo-home
92+
mkdir -p .ci-cargo-home
93+
docker run --rm \
94+
-e CARGO_TARGET_DIR=/tmp/target \
95+
-e EXPECT_BLOB_STREAM_SOURCE=GetRawBlob \
96+
-v "$PWD:/src:ro" \
97+
-v "$PWD/.ci-cargo-home:/root/.cargo" \
98+
-w /src \
99+
quay.io/fedora/fedora:41 \
100+
sh -lc 'dnf -y install cargo rustc skopeo >/dev/null && skopeo --version && cargo test --all-features -- --nocapture --quiet'
101+
sudo rm -rf .ci-cargo-home

src/imageproxy.rs

Lines changed: 125 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -950,7 +950,7 @@ impl ImageProxy {
950950
) -> Result<BlobStream<'a>> {
951951
let fallback_to_get_blob = || async move {
952952
let (reader, driver) = self.get_blob(img, digest, expected_size).await?;
953-
let driver = async move { driver.await }.boxed();
953+
let driver = driver.boxed();
954954
Ok(BlobStream {
955955
source: BlobStreamSource::GetBlob,
956956
expected_size,
@@ -1317,6 +1317,130 @@ mod tests {
13171317
Ok(())
13181318
}
13191319

1320+
#[tokio::test]
1321+
async fn test_get_blob_stream_oci_dir() -> Result<()> {
1322+
use std::str::FromStr;
1323+
1324+
if !check_skopeo() {
1325+
return Ok(());
1326+
}
1327+
1328+
fn sha256_digest(bytes: &[u8]) -> Digest {
1329+
let mut h = sha2::Sha256::new();
1330+
h.update(bytes);
1331+
Digest::from_str(&format!("sha256:{}", hex::encode(h.finalize()))).unwrap()
1332+
}
1333+
1334+
fn write_blob(root: &std::path::Path, bytes: &[u8]) -> Result<(Digest, u64)> {
1335+
let digest = sha256_digest(bytes);
1336+
let size = bytes.len() as u64;
1337+
let dir = root.join("blobs").join("sha256");
1338+
std::fs::create_dir_all(&dir)?;
1339+
std::fs::write(dir.join(digest.digest()), bytes)?;
1340+
Ok((digest, size))
1341+
}
1342+
1343+
let td = tempfile::tempdir()?;
1344+
std::fs::write(
1345+
td.path().join("oci-layout"),
1346+
serde_json::to_vec(&serde_json::json!({"imageLayoutVersion":"1.0.0"}))?,
1347+
)?;
1348+
1349+
let layer_bytes = b"layer bytes";
1350+
let (layer_digest, layer_size) = write_blob(td.path(), layer_bytes)?;
1351+
1352+
let config_bytes = serde_json::to_vec(&serde_json::json!({
1353+
"architecture": "amd64",
1354+
"os": "linux",
1355+
"rootfs": {
1356+
"type": "layers",
1357+
"diff_ids": [layer_digest.to_string()],
1358+
},
1359+
"config": {},
1360+
}))?;
1361+
let (config_digest, config_size) = write_blob(td.path(), &config_bytes)?;
1362+
1363+
let manifest_bytes = serde_json::to_vec(&serde_json::json!({
1364+
"schemaVersion": 2,
1365+
"mediaType": "application/vnd.oci.image.manifest.v1+json",
1366+
"config": {
1367+
"mediaType": "application/vnd.oci.image.config.v1+json",
1368+
"digest": config_digest.to_string(),
1369+
"size": config_size,
1370+
},
1371+
"layers": [{
1372+
"mediaType": "application/vnd.oci.image.layer.v1.tar",
1373+
"digest": layer_digest.to_string(),
1374+
"size": layer_size,
1375+
}],
1376+
}))?;
1377+
let (manifest_digest, manifest_size) = write_blob(td.path(), &manifest_bytes)?;
1378+
1379+
std::fs::write(
1380+
td.path().join("index.json"),
1381+
serde_json::to_vec(&serde_json::json!({
1382+
"schemaVersion": 2,
1383+
"mediaType": "application/vnd.oci.image.index.v1+json",
1384+
"manifests": [{
1385+
"mediaType": "application/vnd.oci.image.manifest.v1+json",
1386+
"digest": manifest_digest.to_string(),
1387+
"size": manifest_size,
1388+
"annotations": {
1389+
"org.opencontainers.image.ref.name": "test",
1390+
}
1391+
}]
1392+
}))?,
1393+
)?;
1394+
1395+
let proxy = ImageProxy::new().await?;
1396+
let imgref = format!("oci:{}:test", td.path().display());
1397+
let img = proxy.open_image(&imgref).await?;
1398+
1399+
let expected_source = match std::env::var("EXPECT_BLOB_STREAM_SOURCE").ok().as_deref() {
1400+
Some("GetRawBlob") => BlobStreamSource::GetRawBlob,
1401+
Some("GetBlob") => BlobStreamSource::GetBlob,
1402+
Some(v) => {
1403+
return Err(Error::Other(
1404+
format!(
1405+
"Invalid EXPECT_BLOB_STREAM_SOURCE={v}; expected GetRawBlob or GetBlob"
1406+
)
1407+
.into(),
1408+
));
1409+
}
1410+
None => {
1411+
if proxy.supports_get_raw_blob() {
1412+
BlobStreamSource::GetRawBlob
1413+
} else {
1414+
BlobStreamSource::GetBlob
1415+
}
1416+
}
1417+
};
1418+
1419+
let BlobStream {
1420+
source,
1421+
reader,
1422+
driver,
1423+
..
1424+
} = proxy
1425+
.get_blob_stream(&img, &layer_digest, layer_size)
1426+
.await?;
1427+
assert_eq!(source, expected_source);
1428+
1429+
let mut reader = reader;
1430+
let mut sink = tokio::io::sink();
1431+
let read = async move {
1432+
let n = tokio::io::copy(&mut *reader, &mut sink).await?;
1433+
Result::Ok(n)
1434+
};
1435+
let (n, driver) = tokio::join!(read, driver);
1436+
assert_eq!(n?, layer_size);
1437+
driver?;
1438+
1439+
proxy.close_image(&img).await?;
1440+
proxy.finalize().await?;
1441+
Ok(())
1442+
}
1443+
13201444
// Helper to create a dummy OwnedFd using memfd_create for testing.
13211445
fn create_dummy_fd() -> OwnedFd {
13221446
memfd_create(c"test-fd", MemfdFlags::CLOEXEC).unwrap()

0 commit comments

Comments
 (0)