Merge pull request #95409 from utdemir/stream_layered_image_fix

dockerTools.streamLayeredImage: Store the customisation layer as a tarball
This commit is contained in:
Anderson Torres 2020-09-14 11:05:48 -03:00 committed by GitHub
commit a5931fa6e3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 73 additions and 85 deletions

View File

@ -219,18 +219,11 @@ import ./make-test-python.nix ({ pkgs, ... }: {
) )
with subtest("Ensure correct behavior when no store is needed"): with subtest("Ensure correct behavior when no store is needed"):
# This check tests two requirements simultaneously # This check tests that buildLayeredImage can build images that don't need a store.
# 1. buildLayeredImage can build images that don't need a store.
# 2. Layers of symlinks are eliminated by the customization layer.
#
docker.succeed( docker.succeed(
"docker load --input='${pkgs.dockerTools.examples.no-store-paths}'" "docker load --input='${pkgs.dockerTools.examples.no-store-paths}'"
) )
# Busybox will not recognize argv[0] and print an error message with argv[0],
# but it confirms that the custom-true symlink is present.
docker.succeed("docker run --rm no-store-paths custom-true |& grep custom-true")
# This check may be loosened to allow an *empty* store rather than *no* store. # This check may be loosened to allow an *empty* store rather than *no* store.
docker.succeed("docker run --rm no-store-paths ls /") docker.succeed("docker run --rm no-store-paths ls /")
docker.fail("docker run --rm no-store-paths ls /nix/store") docker.fail("docker run --rm no-store-paths ls /nix/store")

View File

@ -718,28 +718,41 @@ rec {
architecture = buildPackages.go.GOARCH; architecture = buildPackages.go.GOARCH;
os = "linux"; os = "linux";
}); });
customisationLayer = runCommand "${name}-customisation-layer" { inherit extraCommands; } ''
cp -r ${contentsEnv}/ $out
if [[ -n $extraCommands ]]; then contentsList = if builtins.isList contents then contents else [ contents ];
chmod u+w $out
(cd $out; eval "$extraCommands") # We store the customisation layer as a tarball, to make sure that
fi # things like permissions set on 'extraCommands' are not overriden
''; # by Nix. Then we precompute the sha256 for performance.
contentsEnv = symlinkJoin { customisationLayer = symlinkJoin {
name = "${name}-bulk-layers"; name = "${name}-customisation-layer";
paths = if builtins.isList contents paths = contentsList;
then contents inherit extraCommands;
else [ contents ]; postBuild = ''
mv $out old_out
(cd old_out; eval "$extraCommands" )
mkdir $out
tar \
--owner 0 --group 0 --mtime "@$SOURCE_DATE_EPOCH" \
--hard-dereference \
-C old_out \
-cf $out/layer.tar .
sha256sum $out/layer.tar \
| cut -f 1 -d ' ' \
> $out/checksum
'';
}; };
# NOTE: the `closures` parameter is a list of closures to include. closureRoots = [ baseJson ] ++ contentsList;
# The TOP LEVEL store paths themselves will never be present in the overallClosure = writeText "closure" (lib.concatStringsSep " " closureRoots);
# resulting image. At this time (2020-06-18) none of these layers
# are appropriate to include, as they are all created as # These derivations are only created as implementation details of docker-tools,
# implementation details of dockerTools. # so they'll be excluded from the created images.
closures = [ baseJson contentsEnv ]; unnecessaryDrvs = [ baseJson overallClosure ];
overallClosure = writeText "closure" (lib.concatStringsSep " " closures);
conf = runCommand "${name}-conf.json" { conf = runCommand "${name}-conf.json" {
inherit maxLayers created; inherit maxLayers created;
imageName = lib.toLower name; imageName = lib.toLower name;
@ -751,9 +764,6 @@ rec {
paths = referencesByPopularity overallClosure; paths = referencesByPopularity overallClosure;
buildInputs = [ jq ]; buildInputs = [ jq ];
} '' } ''
paths() {
cat $paths ${lib.concatMapStringsSep " " (path: "| (grep -v ${path} || true)") (closures ++ [ overallClosure ])}
}
${if (tag == null) then '' ${if (tag == null) then ''
outName="$(basename "$out")" outName="$(basename "$out")"
outHash=$(echo "$outName" | cut -d - -f 1) outHash=$(echo "$outName" | cut -d - -f 1)
@ -768,6 +778,12 @@ rec {
created="$(date -Iseconds -d "$created")" created="$(date -Iseconds -d "$created")"
fi fi
paths() {
cat $paths ${lib.concatMapStringsSep " "
(path: "| (grep -v ${path} || true)")
unnecessaryDrvs}
}
# Create $maxLayers worth of Docker Layers, one layer per store path # Create $maxLayers worth of Docker Layers, one layer per store path
# unless there are more paths than $maxLayers. In that case, create # unless there are more paths than $maxLayers. In that case, create
# $maxLayers-1 for the most popular layers, and smush the remainaing # $maxLayers-1 for the most popular layers, and smush the remainaing

View File

@ -298,21 +298,10 @@ rec {
name = "no-store-paths"; name = "no-store-paths";
tag = "latest"; tag = "latest";
extraCommands = '' extraCommands = ''
chmod a+w bin
# This removes sharing of busybox and is not recommended. We do this # This removes sharing of busybox and is not recommended. We do this
# to make the example suitable as a test case with working binaries. # to make the example suitable as a test case with working binaries.
cp -r ${pkgs.pkgsStatic.busybox}/* . cp -r ${pkgs.pkgsStatic.busybox}/* .
''; '';
contents = [
# This layer has no dependencies and its symlinks will be dereferenced
# when creating the customization layer.
(pkgs.runCommand "layer-to-flatten" {} ''
mkdir -p $out/bin
ln -s /bin/true $out/bin/custom-true
''
)
];
}; };
nixLayered = pkgs.dockerTools.buildLayeredImageWithNixDb { nixLayered = pkgs.dockerTools.buildLayeredImageWithNixDb {
@ -415,7 +404,7 @@ rec {
pkgs.dockerTools.buildLayeredImage { pkgs.dockerTools.buildLayeredImage {
name = "bash-layered-with-user"; name = "bash-layered-with-user";
tag = "latest"; tag = "latest";
contents = [ pkgs.bash pkgs.coreutils (nonRootShadowSetup { uid = 999; user = "somebody"; }) ]; contents = [ pkgs.bash pkgs.coreutils ] ++ nonRootShadowSetup { uid = 999; user = "somebody"; };
}; };
} }

View File

@ -33,7 +33,6 @@ function does all this.
import io import io
import os import os
import re
import sys import sys
import json import json
import hashlib import hashlib
@ -45,21 +44,14 @@ from datetime import datetime, timezone
from collections import namedtuple from collections import namedtuple
def archive_paths_to(obj, paths, mtime, add_nix, filter=None): def archive_paths_to(obj, paths, mtime):
""" """
Writes the given store paths as a tar file to the given stream. Writes the given store paths as a tar file to the given stream.
obj: Stream to write to. Should have a 'write' method. obj: Stream to write to. Should have a 'write' method.
paths: List of store paths. paths: List of store paths.
add_nix: Whether /nix and /nix/store directories should be
prepended to the archive.
filter: An optional transformation to be applied to TarInfo
objects. Should take a single TarInfo object and return
another one. Defaults to identity.
""" """
filter = filter if filter else lambda i: i
# gettarinfo makes the paths relative, this makes them # gettarinfo makes the paths relative, this makes them
# absolute again # absolute again
def append_root(ti): def append_root(ti):
@ -72,7 +64,7 @@ def archive_paths_to(obj, paths, mtime, add_nix, filter=None):
ti.gid = 0 ti.gid = 0
ti.uname = "root" ti.uname = "root"
ti.gname = "root" ti.gname = "root"
return filter(ti) return ti
def nix_root(ti): def nix_root(ti):
ti.mode = 0o0555 # r-xr-xr-x ti.mode = 0o0555 # r-xr-xr-x
@ -85,11 +77,9 @@ def archive_paths_to(obj, paths, mtime, add_nix, filter=None):
with tarfile.open(fileobj=obj, mode="w|") as tar: with tarfile.open(fileobj=obj, mode="w|") as tar:
# To be consistent with the docker utilities, we need to have # To be consistent with the docker utilities, we need to have
# these directories first when building layer tarballs. But # these directories first when building layer tarballs.
# we don't need them on the customisation layer. tar.addfile(apply_filters(nix_root(dir("/nix"))))
if add_nix: tar.addfile(apply_filters(nix_root(dir("/nix/store"))))
tar.addfile(apply_filters(nix_root(dir("/nix"))))
tar.addfile(apply_filters(nix_root(dir("/nix/store"))))
for path in paths: for path in paths:
path = pathlib.Path(path) path = pathlib.Path(path)
@ -136,7 +126,7 @@ class ExtractChecksum:
LayerInfo = namedtuple("LayerInfo", ["size", "checksum", "path", "paths"]) LayerInfo = namedtuple("LayerInfo", ["size", "checksum", "path", "paths"])
def add_layer_dir(tar, paths, mtime, add_nix=True, filter=None): def add_layer_dir(tar, paths, mtime):
""" """
Appends given store paths to a TarFile object as a new layer. Appends given store paths to a TarFile object as a new layer.
@ -144,11 +134,6 @@ def add_layer_dir(tar, paths, mtime, add_nix=True, filter=None):
paths: List of store paths. paths: List of store paths.
mtime: 'mtime' of the added files and the layer tarball. mtime: 'mtime' of the added files and the layer tarball.
Should be an integer representing a POSIX time. Should be an integer representing a POSIX time.
add_nix: Whether /nix and /nix/store directories should be
added to a layer.
filter: An optional transformation to be applied to TarInfo
objects inside the layer. Should take a single TarInfo
object and return another one. Defaults to identity.
Returns: A 'LayerInfo' object containing some metadata of Returns: A 'LayerInfo' object containing some metadata of
the layer added. the layer added.
@ -164,8 +149,6 @@ def add_layer_dir(tar, paths, mtime, add_nix=True, filter=None):
extract_checksum, extract_checksum,
paths, paths,
mtime=mtime, mtime=mtime,
add_nix=add_nix,
filter=filter
) )
(checksum, size) = extract_checksum.extract() (checksum, size) = extract_checksum.extract()
@ -182,8 +165,6 @@ def add_layer_dir(tar, paths, mtime, add_nix=True, filter=None):
write, write,
paths, paths,
mtime=mtime, mtime=mtime,
add_nix=add_nix,
filter=filter
) )
write.close() write.close()
@ -199,29 +180,38 @@ def add_layer_dir(tar, paths, mtime, add_nix=True, filter=None):
return LayerInfo(size=size, checksum=checksum, path=path, paths=paths) return LayerInfo(size=size, checksum=checksum, path=path, paths=paths)
def add_customisation_layer(tar, path, mtime): def add_customisation_layer(target_tar, customisation_layer, mtime):
""" """
Adds the contents of the store path as a new layer. This is different Adds the customisation layer as a new layer. This is layer is structured
than the 'add_layer_dir' function defaults in the sense that the contents differently; given store path has the 'layer.tar' and corresponding
of a single store path will be added to the root of the layer. eg (without sha256sum ready.
the /nix/store prefix).
tar: 'tarfile.TarFile' object for the new layer to be added to. tar: 'tarfile.TarFile' object for the new layer to be added to.
path: A store path. customisation_layer: Path containing the layer archive.
mtime: 'mtime' of the added files and the layer tarball. Should be an mtime: 'mtime' of the added layer tarball.
integer representing a POSIX time.
""" """
def filter(ti): checksum_path = os.path.join(customisation_layer, "checksum")
ti.name = re.sub("^/nix/store/[^/]*", "", ti.name) with open(checksum_path) as f:
return ti checksum = f.read().strip()
return add_layer_dir( assert len(checksum) == 64, f"Invalid sha256 at ${checksum_path}."
tar,
[path], layer_path = os.path.join(customisation_layer, "layer.tar")
mtime=mtime,
add_nix=False, path = f"{checksum}/layer.tar"
filter=filter tarinfo = target_tar.gettarinfo(layer_path)
) tarinfo.name = path
tarinfo.mtime = mtime
with open(layer_path, "rb") as f:
target_tar.addfile(tarinfo, f)
return LayerInfo(
size=None,
checksum=checksum,
path=path,
paths=[customisation_layer]
)
def add_bytes(tar, path, content, mtime): def add_bytes(tar, path, content, mtime):