Skip to content

Commit

Permalink
Integrate the PuzzleFS image into the OCI image specification
Browse files Browse the repository at this point in the history
Previously, the OCI Image Index contained a list of manifests which were
referencing the PuzzleFS rootfs image, i.e. the metadata of the PuzzleFS
image in Capnproto format. Now the Image Index [1] references an Image
Manifest [2] and the PuzzleFS image (the PuzzleFS rootfs image together
with the file chunks) is embedded into the layers field of the Image
Manifest.

Where PuzzleFS diverges from the Image Manifest spec is in the layers
definition: our layers are not self contained images and thus they do
not stack. Instead, we have a rootfs layer which stores the PuzzleFS
image rootfs and multiple file chunks which contain the actual data of
the filesystem. No extraction step is performed. Instead, when mounting
a PuzzleFS image, the filesystem is reconstructed from the PuzzleFS
metadata and the file chunks, not unlike how squashfs/erofs archives are
mounted directly.
See the "Inspecting a puzzlefs image" section from the README for more
details about the format.

The image config is an empty descriptor [3] for now, but we don't store
it in blobs/sha256, which causes `skopeo copy` to fail because it
doesn't find the blob referenced by the empty descriptor in the data
store. This will be addressed in a subsequent commit.

See project-machine#55 for more context.

[1] https://github.com/opencontainers/image-spec/blob/main/image-index.md
[2] https://github.com/opencontainers/image-spec/blob/main/manifest.md
[3] https://github.com/opencontainers/image-spec/blob/main/manifest.md#guidance-for-an-empty-descriptor

Signed-off-by: Ariel Miculas-Trif <[email protected]>
  • Loading branch information
ariel-miculas committed Sep 15, 2024
1 parent ab7a74f commit 4f4866d
Show file tree
Hide file tree
Showing 17 changed files with 1,298 additions and 530 deletions.
999 changes: 857 additions & 142 deletions Cargo.lock

Large diffs are not rendered by default.

175 changes: 111 additions & 64 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -272,80 +272,127 @@ Otherwise, run `fusermount -u /tmp/mounted-image`. You will need to have `fuse`
### Inspecting a puzzlefs image
```
$ cd /tmp/puzzlefs-image
$ cat index.json | jq .
$ cat index.json | jq
{
"schemaVersion": -1,
"manifests": [
{
"digest": "sha256:0efa2a4b490abb02a5b9b5f2d43c8262643dba48c67f14b236df0a6f1ea745d8",
"size": 272,
"media_type": "application/vnd.puzzlefs.image.rootfs.v1",
"annotations": {
"org.opencontainers.image.ref.name": "puzzlefs_example"
}
},
"digest": "sha256:c9106994f5e18833e45164e2028431e9c822b4697172f8a997a0d9a3b0d26c9e",
"mediaType": "application/vnd.oci.image.manifest.v1+json",
"platform": {
"architecture": "amd64",
"os": "linux"
},
"size": 619
}
],
"annotations": {}
"schemaVersion": 2
}
```
The `digest` specifies the puzzlefs image manifest, which needs to be decoded using the `capnp tool` and the manifest schema
(assuming you've cloned puzzlefs in `~/puzzlefs`):
```
$ capnp convert binary:json ~/puzzlefs/format/manifest.capnp Rootfs < blobs/sha256/0efa2a4b490abb02a5b9b5f2d43c8262643dba48c67f14b236df0a6f1ea745d8
{ "metadatas": [{ "digest": [102, 197, 227, 96, 136, 156, 147, 144, 139, 154, 248, 228, 29, 161, 252, 228, 118, 222, 21, 44, 132, 0, 214, 164, 80, 74, 121, 156, 26, 85, 123, 57],
"offset": "0",
"compressed": false }],
"fsVerityData": [
{ "digest": [102, 197, 227, 96, 136, 156, 147, 144, 139, 154, 248, 228, 29, 161, 252, 228, 118, 222, 21, 44, 132, 0, 214, 164, 80, 74, 121, 156, 26, 85, 123, 57],
"verity": [224, 180, 63, 193, 142, 198, 24, 175, 78, 42, 126, 227, 253, 187, 102, 162, 31, 77, 85, 252, 205, 137, 198, 216, 26, 213, 113, 238, 144, 79, 93, 244] },
{ "digest": [239, 32, 68, 39, 210, 105, 37, 83, 131, 158, 224, 24, 162, 25, 96, 90, 140, 95, 158, 194, 97, 2, 153, 175, 54, 197, 216, 193, 115, 121, 62, 22],
"verity": [196, 54, 71, 79, 3, 104, 3, 253, 163, 243, 85, 213, 67, 235, 144, 210, 20, 206, 160, 209, 75, 164, 93, 22, 79, 84, 41, 119, 20, 84, 64, 164] } ],
"manifestVersion": "1" }
```
`metadatas` contains a list of layers (in this case only one) which can be further decoded (the sha of the blob is obtained by a decimal to hexadecimal conversion):
```
$ capnp convert binary:json ~/puzzlefs/format/metadata.capnp InodeVector < blobs/sha256/66c5e360889c93908b9af8e41da1fce476de152c8400d6a4504a799c1a557b39
{"inodes": [
{ "ino": "1",
"mode": {"dir": {
"entries": [
{ "ino": "2",
"name": [97, 108, 103, 111, 114, 105, 116, 104, 109, 115] },
{ "ino": "3",
"name": [108, 111, 114, 101, 109, 95, 105, 112, 115, 117, 109, 46, 116, 120, 116] } ],
"lookBelow": false }},
"uid": 1000,
"gid": 1000,
"permissions": 493 },
{ "ino": "2",
"mode": {"dir": {
"entries": [{ "ino": "4",
"name": [98, 105, 110, 97, 114, 121, 45, 115, 101, 97, 114, 99, 104, 46, 116, 120, 116] }],
"lookBelow": false }},
"uid": 1000,
"gid": 1000,
"permissions": 493 },
{ "ino": "3",
"mode": {"file": {"chunks": [{ "blob": {
"digest": [239, 32, 68, 39, 210, 105, 37, 83, 131, 158, 224, 24, 162, 25, 96, 90, 140, 95, 158, 194, 97, 2, 153, 175, 54, 197, 216, 193, 115, 121, 62, 22],
"offset": "0",
"compressed": false },
"len": "865" }]}},
"uid": 1000,
"gid": 1000,
"permissions": 420 },
{ "ino": "4",
"mode": {"file": {"chunks": [{ "blob": {
"digest": [239, 32, 68, 39, 210, 105, 37, 83, 131, 158, 224, 24, 162, 25, 96, 90, 140, 95, 158, 194, 97, 2, 153, 175, 54, 197, 216, 193, 115, 121, 62, 22],
"offset": "865",
"compressed": false },
"len": "278" }]}},
"uid": 1000,
"gid": 1000,
"permissions": 420 } ]}
`index.json` follows the [OCI Image Index Specification](https://github.com/opencontainers/image-spec/blob/main/image-index.md).

The digest tagged with the `puzzlefs_example` tag is an [OCI Image
Manifest](https://github.com/opencontainers/image-spec/blob/main/manifest.md)
with the caveat that `layers` are not applied in the usual way (i.e. by
stacking each one on top of one another). See below for details about the
PuzzleFS `layer` descriptors.

The Image Manifest looks like this:
```
$ cat blobs/sha256/c9106994f5e18833e45164e2028431e9c822b4697172f8a997a0d9a3b0d26c9e | jq
{
"config": {
"data": "e30=",
"digest": "sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a",
"mediaType": "application/vnd.oci.empty.v1+json",
"size": 2
},
"layers": [
{
"digest": "sha256:b7f1ee9373416a49835747455ec4d287bcccc5a4bf8c38156483d46b35ce4dbd",
"mediaType": "application/vnd.puzzlefs.image.filedata.v1",
"size": 27
},
{
"annotations": {
"io.puzzlefsoci.puzzlefs.puzzlefs_verity_root_hash": "7b22d0210c16134159be75d8239d100817b451591d39af2031d94ae84ac4f8c7"
},
"digest": "sha256:9e2edc6917b65606b1112ac8663665dfd2d945cfea960ca595accf790922b910",
"mediaType": "application/vnd.puzzlefs.image.rootfs.v1",
"size": 552
}
],
"schemaVersion": 2
}
```

There are two types of layer descriptors:
* `application/vnd.puzzlefs.image.rootfs.v1`: the PuzzleFS image rootfs which
contains metadata in Capnproto format and must appear only once in the
`layers` array
* `application/vnd.puzzlefs.image.filedata.v1`: a PuzzleFS data chunk generated
by the FastCDC algorithm; usually there are multiple chunks in an image and
they contain all the filesystem data

There is no extraction step for these layers, PuzzleFS mounts the filesystem by
reading the PuzzleFS image rootfs and using this metadata to combine the data
chunks back into the original files. In fact, the data chunks are part of the
OCI Image Manifest so that the other tools copy the image correctly. For
example, with skopeo:
```
$ skopeo --version
skopeo version 1.15.2
$ skopeo copy oci:/tmp/puzzlefs-image:puzzlefs_example oci:/tmp/copy-puzzlefs-image:puzzlefs_example
```
The information about the data chunks is also stored in the PuzzleFS image rootfs,
so that PuzzleFS could mount the filesystem efficiently and that the PuzzleFS
image could also be decoded in the kernel.

The `digest` of the PuzzleFS iamge rootfs contains the filesystem metadata and
it can be decoded using the `capnp tool` and the capnp metadata schema (the
following snippet assumes that you've cloned puzzlefs in `~/puzzlefs`):
```
$ capnp convert binary:json ~/puzzlefs/puzzlefs-lib/src/format/metadata.capnp Rootfs < blobs/sha256/9e2edc6917b65606b1112ac8663665dfd2d945cfea960ca595accf790922b910
{ "metadatas": [{"inodes": [
{ "ino": "1",
"mode": {"dir": {
"entries": [
{ "ino": "2",
"name": [97, 108, 103, 111, 114, 105, 116, 104, 109, 115] },
{ "ino": "3",
"name": [108, 111, 114, 101, 109, 95, 105, 112, 115, 117, 109, 46, 116, 120, 116] } ],
"lookBelow": false }},
"uid": 1000,
"gid": 1000,
"permissions": 493 },
{ "ino": "2",
"mode": {"dir": {
"entries": [{ "ino": "4",
"name": [98, 105, 110, 97, 114, 121, 45, 115, 101, 97, 114, 99, 104, 46, 116, 120, 116] }],
"lookBelow": false }},
"uid": 1000,
"gid": 1000,
"permissions": 509 },
{ "ino": "3",
"mode": {"file": [{ "blob": {
"digest": [183, 241, 238, 147, 115, 65, 106, 73, 131, 87, 71, 69, 94, 196, 210, 135, 188, 204, 197, 164, 191, 140, 56, 21, 100, 131, 212, 107, 53, 206, 77, 189],
"offset": "0",
"compressed": false },
"len": "27" }]},
"uid": 1000,
"gid": 1000,
"permissions": 436 },
{"ino": "4", "mode": {"file": []}, "uid": 1000, "gid": 1000, "permissions": 436} ]}],
"fsVerityData": [{ "digest": [183, 241, 238, 147, 115, 65, 106, 73, 131, 87, 71, 69, 94, 196, 210, 135, 188, 204, 197, 164, 191, 140, 56, 21, 100, 131, 212, 107, 53, 206, 77, 189],
"verity": [91, 20, 52, 173, 44, 8, 31, 244, 53, 178, 16, 121, 46, 144, 14, 39, 2, 30, 196, 43, 104, 230, 143, 98, 219, 173, 82, 223, 224, 201, 247, 164] }],
"manifestVersion": "3" }
```

`metadatas` contains a list of PuzzleFS layers, each layer consisting of a
vector of Inodes. See the [capnp
schema](./puzzlefs-lib/src/format/metadata.capnp) for details.

## Implementation

Expand Down
14 changes: 6 additions & 8 deletions exe/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -157,21 +157,19 @@ fn main() -> anyhow::Result<()> {
let image = Image::new(oci_dir)?;
let new_image = match b.base_layer {
Some(base_layer) => {
let (desc, image) = if b.compression {
add_rootfs_delta::<Zstd>(rootfs, image, &base_layer)?
let (_desc, image) = if b.compression {
add_rootfs_delta::<Zstd>(rootfs, image, &b.tag, &base_layer)?
} else {
add_rootfs_delta::<Noop>(rootfs, image, &base_layer)?
add_rootfs_delta::<Noop>(rootfs, image, &b.tag, &base_layer)?
};
image.add_tag(&b.tag, desc)?;
image
}
None => {
let desc = if b.compression {
build_initial_rootfs::<Zstd>(rootfs, &image)?
if b.compression {
build_initial_rootfs::<Zstd>(rootfs, &image, &b.tag)?
} else {
build_initial_rootfs::<Noop>(rootfs, &image)?
build_initial_rootfs::<Noop>(rootfs, &image, &b.tag)?
};
image.add_tag(&b.tag, desc)?;
Arc::new(image)
}
};
Expand Down
2 changes: 1 addition & 1 deletion exe/tests/verity.rs
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ fn test_fs_verity() -> anyhow::Result<()> {
assert!(mount_output
.unwrap_err()
.to_string()
.contains("Error: fs error: invalid fs_verity data: fsverity mismatch"));
.contains("invalid fs_verity data: fsverity mismatch"));

// test that we can mount with the right digest
puzzlefs([
Expand Down
2 changes: 2 additions & 0 deletions puzzlefs-lib/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ os_pipe = "1.1.2"
tempfile = "3.10"
openat = "0.1.21"
zstd-seekable = "0.1.23"
ocidir = "0.3.0"
cap-std = "3.2.0"


[dev-dependencies]
Expand Down
Loading

0 comments on commit 4f4866d

Please sign in to comment.