Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 13 additions & 3 deletions .config/nextest.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,11 @@ failure-output = "immediate"
success-output = "never"
status-level = "pass"

# Tests that pull OCI images need more time (especially on cold CI runners)
[[profile.default.overrides]]
filter = 'test(/digest_stability|oci_pull/)'
slow-timeout = { period = "300s", terminate-after = 2 }

# Profile for integration tests — run with limited parallelism due to QEMU/KVM resources
[profile.integration]
test-threads = 2
Expand All @@ -30,7 +35,12 @@ path = "junit.xml"
store-success-output = true
store-failure-output = true

# VM tests boot an ephemeral QEMU instance per test, limit parallelism
# Privileged tests boot an ephemeral QEMU instance per test — limit
# parallelism to avoid OOM kills on 16 GB CI runners.
# Requiring all 2 threads effectively serialises them.
#
# NOTE: use /regex/ syntax, not ~substring — the ~ operator treats ^ $ as
# literal characters, so ~^foo never matches anything.
[[profile.integration.overrides]]
filter = 'test(~^vm_)'
threads-required = 4
filter = 'test(/^privileged_/)'
threads-required = 2
2 changes: 2 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ jobs:
- uses: actions/checkout@v6
- uses: bootc-dev/actions/bootc-ubuntu-setup@main
- uses: dtolnay/rust-toolchain@stable
- uses: taiki-e/install-action@nextest
- uses: Swatinem/rust-cache@v2
- run: just test-integration

Expand Down Expand Up @@ -114,6 +115,7 @@ jobs:
libvirt: true

- uses: dtolnay/rust-toolchain@stable
- uses: taiki-e/install-action@nextest

- uses: Swatinem/rust-cache@v2

Expand Down
19 changes: 19 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,5 +1,21 @@
[workspace]
members = ["crates/*"]
# Exclude integration-tests from default `cargo test` — those require a
# built cfsctl binary and (for privileged tests) a VM. Run them via
# `just test-integration` or `just test-integration-vm` instead.
default-members = [
"crates/cfsctl",
"crates/composefs",
"crates/composefs-boot",
"crates/composefs-fuse",
"crates/composefs-http",
"crates/composefs-ioctls",
"crates/composefs-oci",
"crates/composefs-setup-root",
"crates/cstorage",
"crates/erofs-debug",
"crates/splitfdstream",
]
resolver = "2"

[workspace.package]
Expand All @@ -23,6 +39,9 @@ composefs-oci = { version = "0.3.0", path = "crates/composefs-oci", default-feat
composefs-boot = { version = "0.3.0", path = "crates/composefs-boot", default-features = false }
composefs-http = { version = "0.3.0", path = "crates/composefs-http", default-features = false }

# JSON-RPC with FD passing for userns helper
jsonrpc-fdpass = { version = "0.1.0", default-features = false }

[profile.dev.package.sha2]
# this is *really* slow otherwise
opt-level = 3
Expand Down
31 changes: 25 additions & 6 deletions Justfile
Original file line number Diff line number Diff line change
Expand Up @@ -58,18 +58,37 @@ cfsctl_features := env("COMPOSEFS_CFSCTL_FEATURES", "pre-6.15")
_test_image := if base_image =~ "debian" { "localhost/composefs-rs-test-debian:latest" } else if base_image =~ "stream9" { "localhost/composefs-rs-test-c9s:latest" } else { "localhost/composefs-rs-test:latest" }

# Run unprivileged integration tests against the cfsctl binary (no root, no VM)
test-integration: build
CFSCTL_PATH=$(pwd)/target/debug/cfsctl cargo run -p integration-tests -- --skip privileged_
# Prefers nextest for parallelism control and better UX; falls back to direct harness.
test-integration *ARGS: build
#!/usr/bin/env bash
set -euo pipefail
export CFSCTL_PATH=$(pwd)/target/debug/cfsctl
if command -v cargo-nextest &> /dev/null; then
cargo nextest run -p integration-tests -E 'not test(/^privileged_/)' {{ ARGS }}
else
cargo test -p integration-tests --test cfsctl-integration-tests -- --skip privileged_ {{ ARGS }}
fi

# Build the test container image for VM-based integration tests
_integration-container-build:
podman build --build-arg base_image={{base_image}} --build-arg cfsctl_features={{cfsctl_features}} -t {{_test_image}} .

# Run all integration tests including privileged VM tests (requires podman + libvirt)
test-integration-vm: build _integration-container-build
COMPOSEFS_TEST_IMAGE={{_test_image}} \
CFSCTL_PATH=$(pwd)/target/debug/cfsctl \
cargo run -p integration-tests
# Uses nextest with the integration profile for parallelism control of VM tests.
test-integration-vm *ARGS: build _integration-container-build
#!/usr/bin/env bash
set -euo pipefail
export COMPOSEFS_TEST_IMAGE={{_test_image}}
export CFSCTL_PATH=$(pwd)/target/debug/cfsctl
if command -v cargo-nextest &> /dev/null; then
cargo nextest run -P integration -p integration-tests {{ ARGS }}
else
cargo test -p integration-tests --test cfsctl-integration-tests -- {{ ARGS }}
fi

# Install cargo-nextest if not already installed
install-nextest:
@which cargo-nextest > /dev/null 2>&1 || cargo install cargo-nextest --locked

# Run everything: checks + full integration tests including VM
ci: check test-integration-vm
Expand Down
27 changes: 17 additions & 10 deletions bootc/Justfile
Original file line number Diff line number Diff line change
Expand Up @@ -45,20 +45,22 @@ clone:
# `rev` (done when the bootc branch is updated) busts the podman build
# cache. This recipe just adds a [patch] override so Cargo resolves from
# the bind-mounted local source instead of fetching from git.
#
# Errors if the composefs-rs tree has uncommitted changes.
patch: clone
#!/bin/bash
set -euo pipefail
cd "$COMPOSEFS_BOOTC_PATH"

cfs_path="$_COMPOSEFS_SRC/crates/cfsctl"

# Check if already patched (idempotent)
if grep -q 'Patched by composefs-rs' Cargo.toml 2>/dev/null; then
echo "bootc already patched for composefs-rs"
exit 0
# Require a clean composefs-rs working tree so we test a real commit
if ! git -C "$_COMPOSEFS_SRC" diff --quiet HEAD 2>/dev/null; then
echo "error: composefs-rs has uncommitted changes — commit or stash first" >&2
git -C "$_COMPOSEFS_SRC" status --short >&2
exit 1
fi

echo "Patching bootc Cargo.toml to use $_COMPOSEFS_SRC"
cfs_path="$_COMPOSEFS_SRC/crates/cfsctl"

cd "$COMPOSEFS_BOOTC_PATH"

# Add or update the [patch] section with a path override
patch_value="cfsctl = { path = \"${cfs_path}\" } # Patched by composefs-rs"
Expand All @@ -85,7 +87,12 @@ patch: clone
# We intentionally don't run `cargo update` here because it rewrites
# the workspace dependency line in Cargo.toml (replacing git+rev with path).

echo "bootc patched successfully"
# Update the rev comment in the [patch] section so Cargo.toml actually
# changes when composefs-rs moves to a new commit. Since the file is
# part of the podman build context this busts the layer cache.
_rev=$(git -C "$_COMPOSEFS_SRC" rev-parse HEAD)
sed -i "s/^# Patched by composefs-rs.*/# Patched by composefs-rs at ${_rev}/" Cargo.toml
echo "bootc patched for composefs-rs at ${_rev}"

# Build sealed bootc image using local composefs-rs
# The path dependency is auto-detected and bind-mounted by bootc's Justfile
Expand Down Expand Up @@ -158,6 +165,6 @@ config:

Example Usage:
just bootc/build # Clone main, patch, and build
COMPOSEFS_BOOTC_REF=v1.2.0 just bootc/build # Use specific tag
COMPOSEFS_BOOTC_REF=v1.0.0 just bootc/build # Use specific tag
COMPOSEFS_BOOTC_REF=refs/pull/1791/head just bootc/build # Use PR
EOF
5 changes: 3 additions & 2 deletions contrib/packaging/install-test-deps.sh
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,12 @@ set -euo pipefail

case "${ID}" in
centos|fedora|rhel)
pkg_install composefs openssl
pkg_install composefs openssl podman skopeo xfsprogs
;;
debian|ubuntu)
pkg_install \
openssl e2fsprogs bubblewrap openssh-server
openssl e2fsprogs bubblewrap openssh-server \
podman skopeo

# OSTree symlink targets — /root, /home, /srv, etc. are symlinks
# into /var on OSTree systems, so the target directories must exist.
Expand Down
5 changes: 4 additions & 1 deletion crates/cfsctl/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,10 @@ version.workspace = true
path = "src/lib.rs"

[features]
default = ['pre-6.15', 'oci']
default = ['pre-6.15', 'oci', 'containers-storage']
http = ['composefs-http']
oci = ['composefs-oci']
containers-storage = ['composefs-oci/containers-storage', 'cstorage']
rhel9 = ['composefs/rhel9']
'pre-6.15' = ['composefs/pre-6.15']

Expand All @@ -29,8 +30,10 @@ composefs = { workspace = true }
composefs-boot = { workspace = true }
composefs-oci = { workspace = true, optional = true, features = ["boot"] }
composefs-http = { workspace = true, optional = true }
cstorage = { path = "../cstorage", version = "0.3.0", features = ["userns-helper"], optional = true }
env_logger = { version = "0.11.0", default-features = false }
hex = { version = "0.4.0", default-features = false }
indicatif = { version = "0.17.0", default-features = false }
rustix = { version = "1.0.0", default-features = false, features = ["fs", "process"] }
serde = { version = "1.0", default-features = false, features = ["derive"] }
serde_json = { version = "1.0", default-features = false, features = ["std"] }
Expand Down
46 changes: 37 additions & 9 deletions crates/cfsctl/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,30 @@ impl std::fmt::Display for OciReference {
}
}

/// CLI representation of [`composefs_oci::LocalFetchOpt`].
#[cfg(feature = "oci")]
#[derive(Debug, Clone, Copy, Default, clap::ValueEnum)]
enum LocalFetchCli {
/// Do not use native containers-storage import; use skopeo.
#[default]
Disabled,
/// Use native import with reflink/hardlink/copy fallback.
Auto,
/// Use native import; error if zero-copy is not possible.
Zerocopy,
}

#[cfg(feature = "oci")]
impl From<LocalFetchCli> for composefs_oci::LocalFetchOpt {
fn from(cli: LocalFetchCli) -> Self {
match cli {
LocalFetchCli::Disabled => Self::Disabled,
LocalFetchCli::Auto => Self::IfPossible,
LocalFetchCli::Zerocopy => Self::ZeroCopy,
}
}
}

/// Common options for operations using OCI config manifest streams that may transform the image rootfs
#[cfg(feature = "oci")]
#[derive(Debug, Parser)]
Expand Down Expand Up @@ -226,6 +250,10 @@ enum OciCommand {
/// Also generate a bootable EROFS image from the pulled OCI image
#[arg(long)]
bootable: bool,
/// Controls whether containers-storage: references use the native
/// import path with zero-copy reflink/hardlink support.
#[arg(long, value_enum, default_value_t = LocalFetchCli::Disabled)]
local_fetch: LocalFetchCli,
},
/// List all tagged OCI images in the repository
#[clap(name = "images")]
Expand Down Expand Up @@ -925,23 +953,23 @@ where
ref image,
name,
bootable,
local_fetch,
} => {
// If no explicit name provided, use the image reference as the tag
let tag_name = name.as_deref().unwrap_or(image);
let (result, stats) =
composefs_oci::pull_image(&repo, image, Some(tag_name), None).await?;

let opts = composefs_oci::PullOptions {
local_fetch: local_fetch.into(),
..Default::default()
};

let result = composefs_oci::pull(&repo, image, Some(tag_name), opts).await?;

println!("manifest {}", result.manifest_digest);
println!("config {}", result.config_digest);
println!("verity {}", result.manifest_verity.to_hex());
println!("tagged {tag_name}");
println!(
"objects {} copied, {} already present, {} bytes copied, {} bytes inlined",
stats.objects_copied,
stats.objects_already_present,
stats.bytes_copied,
stats.bytes_inlined,
);
println!("objects {}", result.stats);

if bootable {
let image_verity =
Expand Down
16 changes: 14 additions & 2 deletions crates/cfsctl/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,20 @@ use cfsctl::App;
use anyhow::Result;
use clap::Parser;

#[tokio::main]
async fn main() -> Result<()> {
fn main() -> Result<()> {
// If we were spawned as a userns helper process, handle that and exit.
// This MUST be called before the tokio runtime is created.
#[cfg(feature = "containers-storage")]
cstorage::init_if_helper();

// Now we can create the tokio runtime for the main application
tokio::runtime::Builder::new_multi_thread()
.enable_all()
.build()?
.block_on(async_main())
}

async fn async_main() -> Result<()> {
env_logger::init();

let args = App::parse();
Expand Down
9 changes: 8 additions & 1 deletion crates/composefs-oci/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,34 +11,41 @@ rust-version.workspace = true
version.workspace = true

[features]
test = ["tar", "composefs/test"]
default = ["containers-storage"]
test = ["tar", "rand", "composefs/test"]
boot = ["composefs-boot"]
containers-storage = ["dep:cstorage", "dep:base64", "cstorage/userns-helper"]

[dependencies]
anyhow = { version = "1.0.87", default-features = false }
fn-error-context = "0.2"
async-compression = { version = "0.4.0", default-features = false, features = ["tokio", "zstd", "gzip"] }
base64 = { version = "0.22", default-features = false, features = ["std"], optional = true }
bytes = { version = "1", default-features = false }
composefs = { workspace = true }
composefs-boot = { workspace = true, optional = true }
containers-image-proxy = { version = "0.9.2", default-features = false }
cstorage = { path = "../cstorage", version = "0.3.0", optional = true }
hex = { version = "0.4.0", default-features = false }
indicatif = { version = "0.18.0", default-features = false, features = ["tokio"] }
rustix = { version = "1.0.0", features = ["fs"] }
serde = { version = "1.0", default-features = false, features = ["derive"] }
thiserror = { version = "2.0.0", default-features = false }
serde_json = { version = "1.0", default-features = false, features = ["std"] }
sha2 = { version = "0.11.0", default-features = false }
rand = { version = "0.10.0", default-features = false, optional = true }
tar = { version = "0.4.38", default-features = false, optional = true }
tar-core = "0.1.0"
tokio = { version = "1.24.2", features = ["macros", "rt-multi-thread"] }
tokio-util = { version = "0.7", default-features = false, features = ["io"] }
tracing = { version = "0.1", default-features = false }

[dev-dependencies]
cap-std = { version = "4.0.0", default-features = false }
cap-tempfile = { version = "4.0.0", default-features = false }
similar-asserts = "1.7.0"
tar = { version = "0.4.38", default-features = false }
rand = { version = "0.10.0", default-features = false }
composefs = { workspace = true, features = ["test"] }
composefs-boot = { workspace = true }
once_cell = "1.21.3"
Expand Down
Loading