Skip to content
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

nix: create efficient oci images with reusable layers #294

Merged
merged 1 commit into from
Apr 16, 2024
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
1 change: 1 addition & 0 deletions packages/by-name/ociImageConfig/README.md
36 changes: 36 additions & 0 deletions packages/by-name/ociImageConfig/package.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# application/vnd.oci.image.config.v1+json
{ lib, runCommand, writers, nix }:
{
# layers is a list of ociLayerTar
layers ? [ ]
# extraConfig is a set of extra configuration options
, extraConfig ? { }
}:
let
diffIDs = lib.lists.map (layer: builtins.readFile (layer + "/DiffID")) layers;
config = {
architecture = "amd64";
os = "linux";
} // extraConfig // {
rootfs = { type = "layers"; diff_ids = diffIDs; };
};
configJSON = writers.writeJSON "image-config.json" config;
in
runCommand "oci-image-config"
{
buildInputs = [ nix ];
platformJSON = builtins.toJSON { inherit (config) architecture; inherit (config) os; };
inherit configJSON;
} ''
# write the config to a file under blobs/sha256
mkdir -p $out/blobs/sha256
sha256=$(nix-hash --type sha256 --flat $configJSON)
cp $configJSON "$out/blobs/sha256/$sha256"

# create a symlink to the image config
ln -s "$out/blobs/sha256/$sha256" "$out/image-config.json"
# write the platform.json
echo "$platformJSON" > "$out/platform.json"
# write the media descriptor
echo -n "{\"mediaType\": \"application/vnd.oci.image.config.v1+json\", \"size\": $(stat -c %s $configJSON), \"digest\": \"sha256:$sha256\"}" > $out/media-descriptor.json
''
55 changes: 55 additions & 0 deletions packages/by-name/ociImageLayout/README.md
malt3 marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# OCI image tools

This is a set of nix functions for creating reproducible and cachable multi-layer OCI images.

It uses the following functions:

- `ociImageLayout`: Top level function for creating an [OCI image directory layout](https://github.com/opencontainers/image-spec/blob/v1.1.0/image-layout.md). This can be used directly (by podman) or uploaded to a registry (`crane push path/to/directory registry/image/name:tag`).
- `ociImageManifest`: An [OCI image manifest](https://github.com/opencontainers/image-spec/blob/v1.1.0/manifest.md#image-manifest) that can be added to the top-level layout. A manifest contains a configuration and layers.
- `ociImageConfig`: An [OCI image configuration](https://github.com/opencontainers/image-spec/blob/v1.1.0/config.md) that can be included in a manifest. The configuration describes the image including layers, entrypoint, arguments, architecture, os and more.
- `ociLayerTar`: An [OCI image layer filesystem changeset (layer tar)](https://github.com/opencontainers/image-spec/blob/v1.1.0/layer.md). Contains an individual container image layer. Can be freely remixed with other layers. Takes a list of store paths and their target destinations in the image.

## Example

The following example creates an image containing two layers:

- one layer for nginx
- one layer with `bash`, `jq`, a script and configuration

```nix
ociImageLayout {
manifests = [
ociImageManifest
{
layers = [
ociLayerTar {
files = [ { source = nginx; } ];
}
ociLayerTar {
files = [
{ source = bash; destination = "/bin/bash"; }
{ source = jq; destination = "/bin/jq"; }
{ source = writeShellScript "entrypoint.sh" '' jq $CONFIG_PATH ; nginx -g 'daemon off;' ''; destination = "/entrypoint.sh"; }
{ source = writers.writeJSON "conf.json" { a = 1; b = 2; }; destination = "/etc/configuration.json"; }
];
}
];
extraConfig = {
"config" = {
"Env" = [
"PATH=/bin:/usr/bin"
"CONFIG_PATH=/config"
];
"Entrypoint" = [ "/entrypoint.sh" ];
};
};
extraManifest = {
"annotations" = {
"org.opencontainers.image.title" = "example-image";
"org.opencontainers.image.description" = "Example image for ociImageLayout";
};
};
}
];
}
```
40 changes: 40 additions & 0 deletions packages/by-name/ociImageLayout/package.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# OCI image layout. Can be pushed to a registry or used as a local image.
{ lib
, runCommand
, writers
, nix
}:
{
# manifests is a list of ociImageManifest
manifests ? [ ]
# extraIndex is a set of additional fields to add to the index.json
, extraIndex ? { }
}:
let
manifestDescriptors = lib.lists.map (manifest: builtins.fromJSON (builtins.readFile (manifest + "/media-descriptor.json"))) manifests;
index = writers.writeJSON "index.json" (
{
schemaVersion = 2;
mediaType = "application/vnd.oci.image.index.v1+json";
} // extraIndex // {
manifests = manifestDescriptors;
}
);
in
runCommand "oci-image-layout"
{
buildInputs = [ nix ];
blobDirs = lib.lists.map (manifest: manifest + "/blobs/sha256") manifests;
inherit index;
} ''
# add the index.json, image-layout file and all blobs to the output
srcs=($blobDirs)
mkdir -p $out/blobs/sha256
cp $index $out/index.json
echo '{"imageLayoutVersion": "1.0.0"}' > $out/image-layout
for src in $srcs; do
for blob in $(ls $src); do
ln -s "$(realpath $src/$blob)" "$out/blobs/sha256/$blob"
done
done
''
1 change: 1 addition & 0 deletions packages/by-name/ociImageManifest/README.md
46 changes: 46 additions & 0 deletions packages/by-name/ociImageManifest/package.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# application/vnd.oci.image.manifest.v1+json
{ lib
, ociImageConfig
, runCommand
, writers
, nix
}:
{
# layers is a list of ociLayerTar
layers ? [ ]
# extraConfig is a set of extra configuration options
, extraConfig ? { }
# extraManifest is a set of extra manifest options
, extraManifest ? { }
}:
let
config = ociImageConfig { inherit layers extraConfig; };
configDescriptor = builtins.fromJSON (builtins.readFile (config + "/media-descriptor.json"));
configPlatform = builtins.fromJSON (builtins.readFile (config + "/platform.json"));
layerDescriptors = lib.lists.map (layer: builtins.fromJSON (builtins.readFile (layer + "/media-descriptor.json"))) layers;
manifest = writers.writeJSON "image-manifest.json" ({
schemaVersion = 2;
mediaType = "application/vnd.oci.image.manifest.v1+json";
} // extraManifest // {
config = configDescriptor;
layers = layerDescriptors;
});
in
runCommand "oci-image-manifest"
{
blobDirs = lib.lists.map (layer: layer + "/blobs/sha256") (layers ++ [ config ]);
platformJSON = builtins.toJSON configPlatform;
buildInputs = [ nix ];
inherit manifest;
} ''
mkdir -p $out/blobs/sha256
sha256=$(nix-hash --type sha256 --flat $manifest)
cp $manifest "$out/blobs/sha256/$sha256"
ln -s "$out/blobs/sha256/$sha256" "$out/image-manifest.json"
echo -n "{\"mediaType\": \"application/vnd.oci.image.manifest.v1+json\", \"size\": $(stat -c %s $manifest), \"digest\": \"sha256:$sha256\", \"platform\": $platformJSON}" > $out/media-descriptor.json
for src in $blobDirs; do
for blob in $(ls $src); do
ln -s "$src/$blob" "$out/blobs/sha256/$blob"
done
done
''
1 change: 1 addition & 0 deletions packages/by-name/ociLayerTar/README.md
63 changes: 63 additions & 0 deletions packages/by-name/ociLayerTar/package.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
# application/vnd.oci.image.layer.v1.tar
# application/vnd.oci.image.layer.v1.tar+gzip
# application/vnd.oci.image.layer.v1.tar+zstd
{ lib, runCommandLocal, nix, gzip, zstd }:
{
# files is a list of objects with the following attributes:
# source: the path to the file or directory to include in the layer
# destination: the path to place the file or directory in the layer
files ? [ ]
# compression is the compression algorithm to use, either "gzip" or "zstd"
, compression ? "gzip"
}:
runCommandLocal "ociLayer"
{
fileSources = lib.lists.map (file: file.source) files;
fileDestinations = lib.lists.map (file: file.destination or file.source) files;
outPath = "layer" + (
if compression == "gzip" then ".tar.gz"
else if compression == "zstd" then ".tar.zst"
else ".tar"
);
mediaType = "application/vnd.oci.image.layer.v1.tar" + (if compression == "" then "" else "+" + compression);
nativeBuildInputs = [ nix ]
++ lib.optional (compression == "gzip") gzip
++ lib.optional (compression == "zstd") zstd;
inherit compression;
} ''
malt3 marked this conversation as resolved.
Show resolved Hide resolved
set -o pipefail
srcs=($fileSources)
dests=($fileDestinations)
mkdir -p ./root $out

# Copy files into the tree (./root/)
for i in ''${!srcs[@]}; do
mkdir -p "./root/$(dirname ''${dests[$i]})"
cp -rT "''${srcs[i]}" "./root/''${dests[$i]}"
done

# Create the layer tarball
tar --sort=name --owner=root:0 --group=root:0 --mode=544 --mtime='UTC 1970-01-01' -cC ./root -f $out/layer.tar .
# Calculate the layer tarball's diffID (hash of the uncompressed tarball)
diffID=$(nix-hash --type sha256 --flat $out/layer.tar)
# Compress the layer tarball
if [[ "$compression" = "gzip" ]]; then
gzip -c $out/layer.tar > $out/$outPath
elif [[ "$compression" = "zstd" ]]; then
zstd -T0 -q -c $out/layer.tar > $out/$outPath
else
mv $out/layer.tar $out/$outPath
fi
rm -f $out/layer.tar

# Calculate the blob's sha256 hash and write the media descriptor
sha256=$(nix-hash --type sha256 --flat $out/$outPath)
echo -n "{\"mediaType\": \"$mediaType\", \"size\": $(stat -c %s $out/$outPath), \"digest\": \"sha256:$sha256\"}" > $out/media-descriptor.json
echo -n "sha256:$diffID" > $out/DiffID

# Move the compressed layer tarball to the blobs directory and create a symlink
mkdir -p $out/blobs/sha256
mv $out/$outPath $out/blobs/sha256/$sha256
ln -s $out/blobs/sha256/$sha256 $out/$outPath
rm -rf ./root
''
3 changes: 3 additions & 0 deletions tools/vale/styles/config/vocabularies/edgeless/accept.txt
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@ backport
Bazel
bootloader
Bootstrapper
cachable
cachix
changeset
cloud
cmdline
config
Expand Down Expand Up @@ -66,6 +68,7 @@ Nginx
paravisor
PCR
plaintext
podman
protobuf
proxied
QEMU
Expand Down