Skip to content

Commit 0b6424e

Browse files
committed
Add composefs-ostree and some basic CLI tools
Based on ideas from #141 This is an initial version of ostree support. This allows pulling from local and remote ostree repos, which will create a set of regular file content objects, as well as a commit splitstream containing all the remaining ostree objects and file data. From the splitstream we can create an image. When pulling a commit, base commits (i.e. "the previous version" can be specified, either manually and/or added automatically based on parent commit or previous commit for the pulled ref. Any objects in that base commit will not be downloaded. Commits are splitstreams named ostree-commit-xxxx, and refs that points to these are refs/ostree/$ref. erofs images are automatically created for pulled commits, and they can be mounted with "cfsctl ostree mount". There are also some other subcommands, that are simliar to those of oci: * dump * compute-id * inspect * tag * untag * images Signed-off-by: Alexander Larsson <alexl@redhat.com> Assisted-by: Claude Code (Opus 4.6)
1 parent ac5cf7c commit 0b6424e

15 files changed

Lines changed: 2845 additions & 5 deletions

File tree

.github/workflows/ci.yml

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ jobs:
3232
options: "--privileged --pid=host -v /var/tmp:/var/tmp --tmpfs /tmp:rw,exec,nosuid,nodev -v /:/run/host"
3333

3434
steps:
35-
- run: dnf -y install cargo clippy composefs-devel e2fsprogs just rustfmt gcc-c++
35+
- run: dnf -y install cargo clippy composefs-devel e2fsprogs just ostree rustfmt gcc-c++
3636
- name: Enable fs-verity on /
3737
run: tune2fs -O verity $(findmnt -vno SOURCE /run/host)
3838
- uses: actions/checkout@v7
@@ -53,6 +53,8 @@ jobs:
5353
- uses: dtolnay/rust-toolchain@stable
5454
- uses: taiki-e/install-action@nextest
5555
- uses: Swatinem/rust-cache@v2
56+
- name: Install ostree
57+
run: sudo apt-get update && sudo apt-get install -y ostree
5658
- run: just test-integration
5759

5860
# Fuzz smoke test — runs each fuzz target briefly to catch panics
@@ -123,6 +125,9 @@ jobs:
123125

124126
- uses: Swatinem/rust-cache@v2
125127

128+
- name: Install ostree
129+
run: sudo apt-get update && sudo apt-get install -y ostree
130+
126131
- name: Run integration tests (unprivileged + privileged via VM)
127132
run: just test-integration-vm
128133

Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ default-members = [
1111
"crates/composefs-http",
1212
"crates/composefs-ioctls",
1313
"crates/composefs-oci",
14+
"crates/composefs-ostree",
1415
"crates/composefs-setup-root",
1516
"crates/composefs-storage",
1617
"crates/composefs-erofs-debug",
@@ -38,6 +39,7 @@ composefs-ioctls = { version = "0.7.0", path = "crates/composefs-ioctls", defaul
3839
composefs-oci = { version = "0.7.0", path = "crates/composefs-oci", default-features = false }
3940
composefs-boot = { version = "0.7.0", path = "crates/composefs-boot", default-features = false }
4041
composefs-http = { version = "0.7.0", path = "crates/composefs-http", default-features = false }
42+
composefs-ostree = { version = "0.7.0", path = "crates/composefs-ostree", default-features = false }
4143
cap-std-ext = "5.1.2"
4244
ocidir = "0.7.2"
4345

contrib/packaging/install-test-deps.sh

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,12 @@ set -euo pipefail
1111

1212
case "${ID}" in
1313
centos|fedora|rhel)
14-
pkg_install composefs openssl podman skopeo xfsprogs
14+
pkg_install composefs openssl ostree podman skopeo xfsprogs
1515
;;
1616
debian|ubuntu)
1717
pkg_install \
1818
openssl e2fsprogs bubblewrap openssh-server \
19-
podman skopeo
19+
ostree podman skopeo
2020

2121
# OSTree symlink targets — /root, /home, /srv, etc. are symlinks
2222
# into /var on OSTree systems, so the target directories must exist.

crates/composefs-ctl/Cargo.toml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,11 @@ name = "cfsctl"
1717
path = "src/main.rs"
1818

1919
[features]
20-
default = ['pre-6.15', 'oci', 'containers-storage']
20+
default = ['pre-6.15', 'oci', 'containers-storage', 'ostree']
2121
http = ['composefs-http']
2222
oci = ['composefs-oci', 'composefs-oci/varlink']
2323
containers-storage = ['composefs-oci/containers-storage', 'cstorage']
24+
ostree = ['composefs-ostree']
2425
rhel9 = ['composefs/rhel9']
2526
'pre-6.15' = ['composefs/pre-6.15']
2627

@@ -34,6 +35,7 @@ composefs-boot = { workspace = true }
3435
composefs-oci = { workspace = true, optional = true, features = ["boot"] }
3536
composefs-http = { workspace = true, optional = true }
3637
cstorage = { package = "composefs-storage", path = "../composefs-storage", version = "0.7.0", features = ["userns-helper"], optional = true }
38+
composefs-ostree = { workspace = true, optional = true }
3739
env_logger = { version = "0.11.0", default-features = false }
3840
hex = { version = "0.4.0", default-features = false }
3941
indicatif = { version = "0.17.0", default-features = false }

crates/composefs-ctl/src/lib.rs

Lines changed: 181 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ use std::sync::Arc;
4242

4343
use anyhow::{Context as _, Result};
4444
use clap::{Parser, Subcommand, ValueEnum};
45-
#[cfg(feature = "oci")]
45+
#[cfg(any(feature = "oci", feature = "ostree"))]
4646
use comfy_table::{Table, presets::UTF8_FULL};
4747
#[cfg(any(feature = "oci", feature = "http"))]
4848
use indicatif::{MultiProgress, ProgressBar, ProgressStyle};
@@ -501,6 +501,76 @@ enum OciCommand {
501501
},
502502
}
503503

504+
#[cfg(feature = "ostree")]
505+
#[derive(Debug, Subcommand)]
506+
enum OstreeCommand {
507+
PullLocal {
508+
ostree_repo_path: PathBuf,
509+
/// Ostree ref name or commit ID (64-character hex)
510+
ostree_ref: String,
511+
#[clap(long)]
512+
base_name: Option<String>,
513+
},
514+
Pull {
515+
ostree_repo_url: String,
516+
/// Ostree ref name or commit ID (64-character hex)
517+
ostree_ref: String,
518+
#[clap(long)]
519+
base_name: Option<String>,
520+
},
521+
/// Mount an ostree commit's composefs EROFS at the given mountpoint
522+
Mount {
523+
/// Ostree commit ref or commit ID
524+
commit: String,
525+
/// Target mountpoint
526+
mountpoint: String,
527+
/// Writable upper layer directory for overlayfs
528+
#[arg(long, requires = "workdir")]
529+
upperdir: Option<PathBuf>,
530+
/// Work directory for overlayfs (required with --upperdir)
531+
#[arg(long, requires = "upperdir")]
532+
workdir: Option<PathBuf>,
533+
/// Mount read-write (requires --upperdir)
534+
#[arg(long, requires = "upperdir")]
535+
read_write: bool,
536+
},
537+
/// Dump the filesystem of an ostree commit as a composefs dumpfile to stdout
538+
Dump {
539+
/// Ostree commit ref name
540+
commit_name: String,
541+
},
542+
/// Compute the composefs image ID of an ostree commit
543+
ComputeId {
544+
/// Ostree commit ref name
545+
commit_name: String,
546+
},
547+
/// Show the contents of an ostree commit
548+
Inspect {
549+
/// Ostree ref name, commit ID, or commit ID prefix
550+
source: String,
551+
/// Print only the commit metadata key-value pairs
552+
#[clap(long)]
553+
metadata: bool,
554+
},
555+
/// Tag an ostree commit with a name
556+
///
557+
/// The source can be an ostree commit checksum or an existing ref name.
558+
Tag {
559+
/// Ostree commit checksum (hex) or existing ref name
560+
source: String,
561+
/// Tag name to assign
562+
name: String,
563+
},
564+
/// Remove a named ostree reference
565+
Untag {
566+
/// Tag name to remove
567+
name: String,
568+
},
569+
/// List all ostree commits in the repository
570+
#[clap(name = "images")]
571+
ListCommits,
572+
}
573+
504574
/// Common options for reading a filesystem from a path
505575
#[derive(Debug, Parser)]
506576
struct FsReadOptions {
@@ -570,6 +640,11 @@ enum Command {
570640
#[clap(subcommand)]
571641
cmd: OciCommand,
572642
},
643+
#[cfg(feature = "ostree")]
644+
Ostree {
645+
#[clap(subcommand)]
646+
cmd: OstreeCommand,
647+
},
573648
/// Mounts a composefs image, possibly enforcing fsverity of the image
574649
Mount {
575650
/// the name of the image to mount, either an fs-verity hash or prefixed with 'ref/'
@@ -1566,6 +1641,111 @@ where
15661641
unreachable!("oci varlink is handled before opening a repository");
15671642
}
15681643
},
1644+
#[cfg(feature = "ostree")]
1645+
Command::Ostree { cmd: ostree_cmd } => match ostree_cmd {
1646+
OstreeCommand::PullLocal {
1647+
ref ostree_repo_path,
1648+
ref ostree_ref,
1649+
base_name,
1650+
} => {
1651+
eprintln!("Fetching {ostree_ref}");
1652+
let (verity, stats) = composefs_ostree::pull_local(
1653+
&repo,
1654+
ostree_repo_path,
1655+
ostree_ref,
1656+
base_name.as_deref(),
1657+
)
1658+
.await?;
1659+
1660+
let image_id = composefs_ostree::get_image_ref(&repo, &stats.commit_id)?;
1661+
println!("commit {}", stats.commit_id);
1662+
println!("verity {}", verity.to_hex());
1663+
println!("image {}", image_id.to_hex());
1664+
if !composefs_ostree::is_commit_id(ostree_ref) {
1665+
println!("tagged {ostree_ref}");
1666+
}
1667+
println!(
1668+
"objects {} metadata + {} files fetched",
1669+
stats.metadata_fetched, stats.files_fetched
1670+
);
1671+
}
1672+
OstreeCommand::Pull {
1673+
ref ostree_repo_url,
1674+
ref ostree_ref,
1675+
base_name,
1676+
} => {
1677+
eprintln!("Fetching {ostree_ref}");
1678+
let (verity, stats) = composefs_ostree::pull(
1679+
&repo,
1680+
ostree_repo_url,
1681+
ostree_ref,
1682+
base_name.as_deref(),
1683+
)
1684+
.await?;
1685+
1686+
let image_id = composefs_ostree::get_image_ref(&repo, &stats.commit_id)?;
1687+
println!("commit {}", stats.commit_id);
1688+
println!("verity {}", verity.to_hex());
1689+
println!("image {}", image_id.to_hex());
1690+
if !composefs_ostree::is_commit_id(ostree_ref) {
1691+
println!("tagged {ostree_ref}");
1692+
}
1693+
println!(
1694+
"objects {} metadata + {} files fetched",
1695+
stats.metadata_fetched, stats.files_fetched
1696+
);
1697+
}
1698+
OstreeCommand::Mount {
1699+
ref commit,
1700+
ref mountpoint,
1701+
ref upperdir,
1702+
ref workdir,
1703+
read_write,
1704+
} => {
1705+
let mount_options =
1706+
get_mount_options(upperdir.as_deref(), workdir.as_deref(), read_write)?;
1707+
let image_id = composefs_ostree::get_image_ref(&repo, commit)?;
1708+
repo.mount_at(&image_id.to_hex(), mountpoint.as_str(), &mount_options)?;
1709+
}
1710+
OstreeCommand::Dump { ref commit_name } => {
1711+
let fs = composefs_ostree::create_filesystem(&repo, commit_name)?;
1712+
fs.print_dumpfile()?;
1713+
}
1714+
OstreeCommand::ComputeId { ref commit_name } => {
1715+
let image_id = composefs_ostree::ensure_ostree_erofs(&repo, commit_name)?;
1716+
println!("{}", image_id.to_hex());
1717+
}
1718+
OstreeCommand::Inspect {
1719+
ref source,
1720+
metadata,
1721+
} => {
1722+
composefs_ostree::inspect(&repo, source, metadata)?;
1723+
}
1724+
OstreeCommand::Tag {
1725+
ref source,
1726+
ref name,
1727+
} => {
1728+
composefs_ostree::tag(&repo, source, name)?;
1729+
println!("Tagged {source} as {name}");
1730+
}
1731+
OstreeCommand::Untag { ref name } => {
1732+
composefs_ostree::untag(&repo, name)?;
1733+
}
1734+
OstreeCommand::ListCommits => {
1735+
let commits = composefs_ostree::list_commits(&repo)?;
1736+
if commits.is_empty() {
1737+
println!("No ostree commits found");
1738+
} else {
1739+
let mut table = Table::new();
1740+
table.load_preset(UTF8_FULL);
1741+
table.set_header(["NAME", "COMMIT"]);
1742+
for c in commits {
1743+
table.add_row([c.name.as_str(), &c.commit_id]);
1744+
}
1745+
println!("{table}");
1746+
}
1747+
}
1748+
},
15691749
Command::CreateImage {
15701750
fs_opts,
15711751
ref image_name,

crates/composefs-integration-tests/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ composefs = { workspace = true }
3636
# Only the test_util module is used — for creating test OCI images.
3737
# All verification must go through the cfsctl CLI.
3838
composefs-oci = { workspace = true, features = ["test", "boot", "containers-storage"] }
39+
composefs-ostree = { workspace = true }
3940
# Used by the varlink tests to drive the service through zlink's native typed
4041
# proxy bindings (alongside the external `varlinkctl` CLI), and to assert on the
4142
# typed wire reply/error types.

crates/composefs-integration-tests/src/tests/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,6 @@ pub mod cstor;
55
pub mod digest_stability;
66
pub mod oci_compat;
77
pub mod old_format;
8+
pub mod ostree;
89
pub mod privileged;
910
pub mod varlink;

0 commit comments

Comments
 (0)