Merge pull request #282886 from WxNzEMof/docker-tools-uid

Allow streaming layered containers with non-root Nix store

Changed files
+76 -46
doc
build-helpers
nixos
pkgs
build-support
+10
doc/build-helpers/images/dockertools.section.md
···
_Default value:_ `"1970-01-01T00:00:01Z"`.
+
`uid` (Number; _optional_) []{#dockerTools-buildLayeredImage-arg-uid}
+
`gid` (Number; _optional_) []{#dockerTools-buildLayeredImage-arg-gid}
+
`uname` (String; _optional_) []{#dockerTools-buildLayeredImage-arg-uname}
+
`gname` (String; _optional_) []{#dockerTools-buildLayeredImage-arg-gname}
+
+
: Credentials for Nix store ownership.
+
Can be overridden to e.g. `1000` / `1000` / `"user"` / `"user"` to enable building a container where Nix can be used as an unprivileged user in single-user mode.
+
+
_Default value:_ `0` / `0` / `"root"` / `"root"`
+
`maxLayers` (Number; _optional_) []{#dockerTools-buildLayeredImage-arg-maxLayers}
: The maximum number of layers that will be used by the generated image.
+21 -1
nixos/tests/docker-tools.nix
···
'';
config.Cmd = [ "${pkgs.coreutils}/bin/stat" "-c" "%u:%g" "/testfile" ];
};
+
+
nonRootTestImage =
+
pkgs.dockerTools.streamLayeredImage rec {
+
name = "non-root-test";
+
tag = "latest";
+
uid = 1000;
+
gid = 1000;
+
uname = "user";
+
gname = "user";
+
config = {
+
User = "user";
+
Cmd = [ "${pkgs.coreutils}/bin/stat" "-c" "%u:%g" "${pkgs.coreutils}/bin/stat" ];
+
};
+
};
in {
name = "docker-tools";
meta = with pkgs.lib.maintainers; {
···
):
docker.succeed(
"docker load --input='${examples.bashLayeredWithUser}'",
-
"docker run -u somebody --rm ${examples.bashLayeredWithUser.imageName} ${pkgs.bash}/bin/bash -c 'test 555 == $(stat --format=%a /nix) && test 555 == $(stat --format=%a /nix/store)'",
+
"docker run -u somebody --rm ${examples.bashLayeredWithUser.imageName} ${pkgs.bash}/bin/bash -c 'test 755 == $(stat --format=%a /nix) && test 755 == $(stat --format=%a /nix/store)'",
"docker rmi ${examples.bashLayeredWithUser.imageName}",
)
···
with subtest("streamLayeredImage: chown is persistent in fakeRootCommands"):
docker.succeed(
"${chownTestImage} | docker load",
+
"docker run --rm ${chownTestImage.imageName} | diff /dev/stdin <(echo 12345:12345)"
+
)
+
+
with subtest("streamLayeredImage: with non-root user"):
+
docker.succeed(
+
"${nonRootTestImage} | docker load",
"docker run --rm ${chownTestImage.imageName} | diff /dev/stdin <(echo 12345:12345)"
)
'';
+28 -35
pkgs/build-support/docker/default.nix
···
})
);
+
# Arguments are documented in ../../../doc/build-helpers/images/dockertools.section.md
streamLayeredImage = lib.makeOverridable (
{
-
# Image Name
name
-
, # Image tag, the Nix's output hash will be used if null
-
tag ? null
-
, # Parent image, to append to.
-
fromImage ? null
-
, # Files to put on the image (a nix store path or list of paths).
-
contents ? [ ]
-
, # Docker config; e.g. what command to run on the container.
-
config ? { }
-
, # Image architecture, defaults to the architecture of the `hostPlatform` when unset
-
architecture ? defaultArchitecture
-
, # Time of creation of the image. Passing "now" will make the
-
# created date be the time of building.
-
created ? "1970-01-01T00:00:01Z"
-
, # Optional bash script to run on the files prior to fixturizing the layer.
-
extraCommands ? ""
-
, # Optional bash script to run inside fakeroot environment.
-
# Could be used for changing ownership of files in customisation layer.
-
fakeRootCommands ? ""
-
, # Whether to run fakeRootCommands in fakechroot as well, so that they
-
# appear to run inside the image, but have access to the normal Nix store.
-
# Perhaps this could be enabled on by default on pkgs.stdenv.buildPlatform.isLinux
-
enableFakechroot ? false
-
, # We pick 100 to ensure there is plenty of room for extension. I
-
# believe the actual maximum is 128.
-
maxLayers ? 100
-
, # Whether to include store paths in the image. You generally want to leave
-
# this on, but tooling may disable this to insert the store paths more
-
# efficiently via other means, such as bind mounting the host store.
-
includeStorePaths ? true
-
, # Passthru arguments for the underlying derivation.
-
passthru ? {}
+
, tag ? null
+
, fromImage ? null
+
, contents ? [ ]
+
, config ? { }
+
, architecture ? defaultArchitecture
+
, created ? "1970-01-01T00:00:01Z"
+
, uid ? 0
+
, gid ? 0
+
, uname ? "root"
+
, gname ? "root"
+
, maxLayers ? 100
+
, extraCommands ? ""
+
, fakeRootCommands ? ""
+
, enableFakechroot ? false
+
, includeStorePaths ? true
+
, passthru ? {}
,
}:
assert
···
conf = runCommand "${baseName}-conf.json"
{
-
inherit fromImage maxLayers created;
+
inherit fromImage maxLayers created uid gid uname gname;
imageName = lib.toLower name;
preferLocalBuild = true;
passthru.imageTag =
···
"store_layers": $store_layers[0],
"customisation_layer", $customisation_layer,
"repo_tag": $repo_tag,
-
"created": $created
+
"created": $created,
+
"uid": $uid,
+
"gid": $gid,
+
"uname": $uname,
+
"gname": $gname
' --arg store_dir "${storeDir}" \
--argjson from_image ${if fromImage == null then "null" else "'\"${fromImage}\"'"} \
--slurpfile store_layers store_layers.json \
--arg customisation_layer ${customisationLayer} \
--arg repo_tag "$imageName:$imageTag" \
-
--arg created "$created" |
+
--arg created "$created" \
+
--arg uid "$uid" \
+
--arg gid "$gid" \
+
--arg uname "$uname" \
+
--arg gname "$gname" |
tee $out
'';
+17 -10
pkgs/build-support/docker/stream_layered_image.py
···
the fields with the same name on the image spec [2].
* "created" can be "now".
* "created" is also used as mtime for files added to the image.
+
* "uid", "gid", "uname", "gname" is the file ownership, for example,
+
0, 0, "root", "root".
* "store_layers" is a list of layers in ascending order, where each
layer is the list of store paths to include in that layer.
···
from collections import namedtuple
-
def archive_paths_to(obj, paths, mtime):
+
def archive_paths_to(obj, paths, mtime, uid, gid, uname, gname):
"""
Writes the given store paths as a tar file to the given stream.
···
def apply_filters(ti):
ti.mtime = mtime
-
ti.uid = 0
-
ti.gid = 0
-
ti.uname = "root"
-
ti.gname = "root"
+
ti.uid = uid
+
ti.gid = gid
+
ti.uname = uname
+
ti.gname = gname
return ti
def nix_root(ti):
-
ti.mode = 0o0555 # r-xr-xr-x
+
ti.mode = 0o0755 # rwxr-xr-x
return ti
def dir(path):
···
return final_config
-
def add_layer_dir(tar, paths, store_dir, mtime):
+
def add_layer_dir(tar, paths, store_dir, mtime, uid, gid, uname, gname):
"""
Appends given store paths to a TarFile object as a new layer.
···
archive_paths_to(
extract_checksum,
paths,
-
mtime=mtime,
+
mtime, uid, gid, uname, gname
)
(checksum, size) = extract_checksum.extract()
···
archive_paths_to(
write,
paths,
-
mtime=mtime,
+
mtime, uid, gid, uname, gname
)
write.close()
···
else datetime.fromisoformat(conf["created"])
)
mtime = int(created.timestamp())
+
uid = int(conf["uid"])
+
gid = int(conf["gid"])
+
uname = conf["uname"]
+
gname = conf["gname"]
store_dir = conf["store_dir"]
from_image = load_from_image(conf["from_image"])
···
for num, store_layer in enumerate(conf["store_layers"], start=start):
print("Creating layer", num, "from paths:", store_layer,
file=sys.stderr)
-
info = add_layer_dir(tar, store_layer, store_dir, mtime=mtime)
+
info = add_layer_dir(tar, store_layer, store_dir,
+
mtime, uid, gid, uname, gname)
layers.append(info)
print("Creating layer", len(layers) + 1, "with customisation...",