Skip to content

Add composefs-ostree and some basic CLI tools #144

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 6 commits into
base: main
Choose a base branch
from

Conversation

alexlarsson
Copy link

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 blob containing all the
remaining ostree objects. From the blob we can create an image.

When pulling a commit, a base blob (i.e. "the previous version" can be
specified. Any objects in that base blob will not be downloaded. If a
name is given for the pulled commit, then pre-existing blobs with the
same name will automatically be used as a base blob.

This is an initial version and there are several things missing:

  • Pull operations are completely serial
  • There is no support for ostree summary files
  • There is no support for ostree delta files
  • There is no caching of local file availability (other than base blob)
  • Local ostree repos only support archive mode

@alexlarsson alexlarsson force-pushed the ostree-support branch 2 times, most recently from e0e827f to 9c5b086 Compare June 17, 2025 06:54
Copy link
Collaborator

@allisonkarlitskaya allisonkarlitskaya left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I love this! Thanks for working on it!

I made some comments on the first round of commits. Feel free to adjust those and PR them separately: we can merge those now without further discussion.

The blobs thing is going to need a call.

I didn't review the crate addition in any detail at all. That's probably also going to need a call :)

@@ -432,7 +433,30 @@ impl<ObjectID: FsVerityHashValue> Repository<ObjectID> {
relative.push(target_component);
}

symlinkat(relative, &self.repository, name)
// Atomically replace existing symlink
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for doing this. flatpak-rs currently has a workaround for exactly this reason, which I'd love to rip out: https://github.com/allisonkarlitskaya/flatpak-rs/blob/8e1741d06b18a450536297a334fc347b010639ba/src/install.rs#L19

Can't we get some sort of kernel love here? I've generally tried to avoid this sort of nonsense (by way of O_TMPFILE)...

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Atomic rename over symlinks is the canonical way to do this atm. Don't think there is anything better.

Comment on lines 438 to 439
let mut count = 0;
let tmp_name = loop {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Feels a bit like this could be better handled with a for loop. I like the let tmp_name = loop { } thing, of course (which you can't do with for) but after you break the loop, the only thing you do is a renameat()... so that could probably be brought into the loop body.

You could also make a utility function that was responsible for creating the symlink and used return to escape the for loop, returning the value. Having this inline already feels like "a bit too much" here...

}
};

renameat(&self.repository, &tmp_name, &self.repository, name)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No cleanup of the temporary here in case the rename fails...

@@ -332,9 +332,11 @@ impl<ObjectID: FsVerityHashValue> Repository<ObjectID> {
let filename = format!("streams/{name}");

let file = File::from(if let Some(verity_hash) = verity {
self.open_with_verity(&filename, verity_hash)?
self.open_with_verity(&filename, verity_hash)
.with_context(|| format!("Opening ref 'streams/{name}'"))?
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We'll never escape anyhow 🤣

Agreed that having this is better than not, though.

@@ -513,15 +513,30 @@ impl<ObjectID: FsVerityHashValue> Repository<ObjectID> {
fn gc_category(&self, category: &str) -> Result<HashSet<ObjectID>> {
let mut objects = HashSet::new();

let category_fd = self.openat(category, OFlags::RDONLY | OFlags::DIRECTORY)?;
let category_fd = match self.openat(category, OFlags::RDONLY | OFlags::DIRECTORY) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe consider lifting this from flatpak-rs into our own utils module?

https://github.com/allisonkarlitskaya/flatpak-rs/blob/8e1741d06b18a450536297a334fc347b010639ba/src/sandbox/util.rs#L34

I'm sure we could find a couple more uses around the code for this...

@@ -1,3 +1,4 @@
pub mod blob;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not keen on this addition. We store OCI config json in splitstreams as well (as a single inline block) specifically because I wanted to avoid creating a separate file format. If there is a missing feature there, I'd rather extend splitstreams a bit rather than adding yet another object type to the repository. We'll need to talk about this...

@@ -11,9 +11,10 @@ rust-version.workspace = true
version.workspace = true

[features]
default = ['pre-6.15', 'oci']
default = ['pre-6.15', 'oci','ostree']
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

missing space. surprised some sort of linter didn't pick on that...

@@ -366,10 +366,7 @@ impl<ObjectID: FsVerityHashValue> Repository<ObjectID> {
pub fn has_named_blob(&self, name: &str) -> bool {
let blob_path = format!("blobs/refs/{}", name);

match readlinkat(&self.repository, &blob_path, []) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This goes in a previous commit, obviously...

@alexlarsson
Copy link
Author

Hmmm, thinking more about this. We probably want a "content type" magic thing in the splitstream header as well, so we can error out if the wrapped thing is of the wrong type.

alexlarsson and others added 6 commits June 19, 2025 11:02
For example, if there has been no streams created, we don't want to
fail with some random ENOENT.

Signed-off-by: Alexander Larsson <[email protected]>
Naming a new version of an image the same as the old version is pretty
standard, and this is currently giving EEXIST errors. We fix this by
doing an atomic symlink + rename operation to replace the symlink if it
doesn't already have the expected value.

Drop our existing Repository::ensure_symlink() function and use the new
function for that as well.

Co-authored-by: Alexander Larsson <[email protected]>
Signed-off-by: Alexander Larsson <[email protected]>
Signed-off-by: Allison Karlitskaya <[email protected]>
This adds error context when opening images and streams. Without
this it is kinda hard to figure out what ENOENT really means.

Signed-off-by: Alexander Larsson <[email protected]>
This changes the splitstream format a bit. The primary differences are:

 * The header is not compressed
 * All referenced fs-verity objects are stored in the header, including
   external chunks, mapped splitstreams and (a new feature) references
   that are not used in chunks.
 * The mapping table is separate from the reference table (and generally
   smaller), and indexes into it.
 * There is a magic value to detect the file format.
 * We store a tag for what ObjectID format is used
 * The total size of the stream is stored in the header.

The ability to reference file objects in the repo even if they are not
part of the splitstream "content" will be useful for the ostree
support to reference file content objects.

This change also allows More efficient GC enumeration, because we
don't have to parse the entire splitstream to find the referenced
objects.
This is an extra check to avoid confusion.
Based on ideas from containers#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 blob containing all the
remaining ostree objects. From the blob we can create an image.

When pulling a commit, a base blob (i.e. "the previous version" can be
specified. Any objects in that base blob will not be downloaded. If a
name is given for the pulled commit, then pre-existing blobs with the
same name will automatically be used as a base blob.

This is an initial version and there are several things missing:
 * Pull operations are completely serial
 * There is no support for ostree summary files
 * There is no support for ostree delta files
 * There is no caching of local file availability (other than base blob)
 * Local ostree repos only support archive mode
 * There is no GPG validation on ostree pull

Signed-off-by: Alexander Larsson <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants