Unverified Commit 57c11082 authored by Robert Hensing's avatar Robert Hensing Committed by GitHub
Browse files

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

Allow streaming layered containers with non-root Nix store
parents f5622df4 4733b52c
Loading
Loading
Loading
Loading
+10 −0
Original line number Diff line number Diff line
@@ -507,6 +507,16 @@ This allows the function to produce reproducible images.

  _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
Original line number Diff line number Diff line
@@ -58,6 +58,20 @@ let
      '';
      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; {
@@ -181,7 +195,7 @@ in {
    ):
        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}",
        )

@@ -604,5 +618,11 @@ in {
            "${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
Original line number Diff line number Diff line
@@ -890,41 +890,26 @@ rec {
    })
  );

  # 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
@@ -1007,7 +992,7 @@ rec {

        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 =
@@ -1086,14 +1071,22 @@ rec {
              "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
Original line number Diff line number Diff line
@@ -9,6 +9,8 @@ image as an uncompressed tarball to stdout:
  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.

@@ -45,7 +47,7 @@ from datetime import datetime, timezone
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.

@@ -61,14 +63,14 @@ def archive_paths_to(obj, paths, mtime):

    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):
@@ -208,7 +210,7 @@ def overlay_base_config(from_image, final_config):
    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.

@@ -231,7 +233,7 @@ def add_layer_dir(tar, paths, store_dir, mtime):
    archive_paths_to(
        extract_checksum,
        paths,
        mtime=mtime,
        mtime, uid, gid, uname, gname
    )
    (checksum, size) = extract_checksum.extract()

@@ -247,7 +249,7 @@ def add_layer_dir(tar, paths, store_dir, mtime):
            archive_paths_to(
                write,
                paths,
                mtime=mtime,
                mtime, uid, gid, uname, gname
            )
            write.close()

@@ -324,6 +326,10 @@ def main():
      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"])
@@ -336,7 +342,8 @@ def main():
        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...",