diff --git a/doc/manual/default.nix b/doc/manual/default.nix index 59f62ab..ff2096c 100644 --- a/doc/manual/default.nix +++ b/doc/manual/default.nix @@ -20,7 +20,15 @@ let } '' export NIX_LOG_DIR=$PWD export NIX_STATE_DIR=$PWD - nix-instantiate --option sandbox false --readonly-mode --eval --expr "$optionsExpr" --xml --strict >$out + nix-instantiate \ + --option sandbox false \ + --readonly-mode \ + --eval \ + --expr "$optionsExpr" \ + --xml \ + --strict \ + --show-trace \ + >$out ''; optionsDocBook = runCommand "options-db.xml" {} '' @@ -61,7 +69,7 @@ let declarations = map (d: "src/nix" + (lib.strings.removePrefix (toString ${src}) (toString d))) opt.declarations; }; inherit (pkgs) lib; - composition = pkgs.callPackage ${src}/eval-service.nix {} { modules = []; host = {}; }; + composition = pkgs.callPackage ${src}/eval-service.nix {} { modules = []; host = {}; name = abort "The manual's service options section must not depend on the service name."; }; in map fixPaths (lib.filter (opt: opt.visible && !opt.internal) (lib.optionAttrSetToDocList composition.options)) ''; }; diff --git a/src/arion b/src/arion index 15eab98..32ec65d 100755 --- a/src/arion +++ b/src/arion @@ -163,6 +163,32 @@ do_build() { --show-trace \ --attr 'config.build.dockerComposeYaml' \ >/dev/null ; + + echo 1>&2 "Ensuring required images are loaded..." + jq -r <"$docker_compose_yaml" \ + '.["x-arion"].images | map(" - " + .imageName + ":" + .imageTag) | join("\n")' + eval "$( + jq -r '.["docker-compose"]["x-arion"].images as $images + | .["existing-images"] as $loaded + | $images + | map( + if $loaded[.imageName + ":" + .imageTag] + then "" + else "docker load <" + .image + ";" end + ) + | join("\n") + ' <${text}''; dockerComposeRef = fragment: - ''See Docker Compose#${fragment}''; + ''See Docker Compose#${fragment}''; dockerComposeKitchenSink = '' Analogous to the docker run counterpart. ${dockerComposeRef "domainname-hostname-ipc-mac_address-privileged-read_only-shm_size-stdin_open-tty-user-working_dir"} ''; + + cap_add = lib.attrNames (lib.filterAttrs (name: value: value == true) config.service.capabilities); + cap_drop = lib.attrNames (lib.filterAttrs (name: value: value == false) config.service.capabilities); + in { options = { @@ -33,6 +39,14 @@ in ''; }; + service.name = mkOption { + type = str; + description = '' + The name of the service - <name> in the composition-level docker-compose.services.<name> + ''; + readOnly = true; + }; + service.volumes = mkOption { type = listOf types.unspecified; default = []; @@ -81,6 +95,16 @@ in default = []; description = dockerComposeRef "depends_on"; }; + service.devices = mkOption { + type = listOf str; + default = []; + description = '' + See ${link "https://docs.docker.com/engine/reference/run/#runtime-privilege-and-linux-capabilities" + "docker run --device documentation"} + + ${dockerComposeRef "devices"} + ''; + }; service.links = mkOption { type = listOf str; default = []; @@ -145,6 +169,24 @@ in default = null; description = dockerComposeRef "stop_signal"; }; + service.capabilities = mkOption { + type = attrsOf (nullOr bool); + default = {}; + example = { ALL = true; SYS_ADMIN = false; NET_ADMIN = false; }; + description = '' + Enable/disable linux capabilities, or pick Docker's default. + + Setting a capability to true means that it will be + "added". Setting it to false means that it will be "dropped". + ${dockerComposeRef "cap_add-cap_drop"} + + Omitted and null capabilities will therefore be set + according to Docker's ${ + link "https://docs.docker.com/engine/reference/run/#runtime-privilege-and-linux-capabilities" + "default list of capabilities." + } + ''; + }; }; config.build.service = { @@ -155,10 +197,16 @@ in ; } // lib.optionalAttrs (config.service.build.context != null) { inherit (config.service) build; + } // lib.optionalAttrs (cap_add != []) { + inherit cap_add; + } // lib.optionalAttrs (cap_drop != []) { + inherit cap_drop; } // lib.optionalAttrs (config.service.command != null) { inherit (config.service) command; } // lib.optionalAttrs (config.service.depends_on != []) { inherit (config.service) depends_on; + } // lib.optionalAttrs (config.service.devices != []) { + inherit (config.service) devices; } // lib.optionalAttrs (config.service.entrypoint != null) { inherit (config.service) entrypoint; } // lib.optionalAttrs (config.service.env_file != []) { diff --git a/src/nix/modules/service/host-store.nix b/src/nix/modules/service/host-store.nix index 2060517..3ec3a9f 100644 --- a/src/nix/modules/service/host-store.nix +++ b/src/nix/modules/service/host-store.nix @@ -8,6 +8,7 @@ let inherit (lib) mkOption types mkIf; + escape = s: lib.replaceStrings ["$"] ["$$"] s; in { options = { @@ -23,11 +24,13 @@ in }; }; config = mkIf config.service.useHostStore { + image.nixBuild = false; # no need to build and load service.image = "arion-base"; service.build.context = "${../../../arion-image}"; service.volumes = [ "${config.host.nixStorePrefix}/nix/store:/nix/store" "${config.host.nixStorePrefix}${pkgs.buildEnv { name = "container-system-env"; paths = [ pkgs.bashInteractive pkgs.coreutils ]; }}:/run/system" ] ++ lib.optional config.service.useHostNixDaemon "/nix/var/nix/daemon-socket:/nix/var/nix/daemon-socket"; + service.command = lib.mkDefault (map escape (config.image.rawConfig.Cmd or [])); }; } diff --git a/src/nix/modules/service/image.nix b/src/nix/modules/service/image.nix new file mode 100644 index 0000000..f530165 --- /dev/null +++ b/src/nix/modules/service/image.nix @@ -0,0 +1,115 @@ +{ pkgs, lib, config, ... }: +let + inherit (lib) types mkOption; + inherit (types) attrsOf listOf nullOr package str unspecified bool; + + # TODO: dummy-config is a useless layer. Nix 2.3 will let us inspect + # the string context instead, so we can avoid this. + contentsList = config.image.contents ++ [ + (pkgs.writeText "dummy-config.json" (builtins.toJSON config.image.rawConfig)) + ]; + + builtImage = pkgs.dockerTools.buildLayeredImage { + inherit (config.image) + name + contents + ; + config = config.image.rawConfig; + maxLayers = 100; + + # TODO: allow use of image's Nix package instead + # TODO: option to disable db generation + extraCommands = '' + echo "Generating the nix database..." + echo "Warning: only the database of the deepest Nix layer is loaded." + echo " If you want to use nix commands in the container, it would" + echo " be better to only have one layer that contains a nix store." + export NIX_REMOTE=local?root=$PWD + ${pkgs.nix}/bin/nix-store --load-db < ${pkgs.closureInfo {rootPaths = contentsList;}}/registration + mkdir -p nix/var/nix/gcroots/docker/ + for i in ${lib.concatStringsSep " " contentsList}; do + ln -s $i nix/var/nix/gcroots/docker/$(basename $i) + done; + ''; + }; +in +{ + options = { + build.image = mkOption { + type = nullOr package; + description = '' + Docker image derivation to be docker loaded. + ''; + internal = true; + }; + build.imageName = mkOption { + type = str; + description = "Derived from build.image"; + internal = true; + }; + build.imageTag = mkOption { + type = str; + description = "Derived from build.image"; + internal = true; + }; + image.nixBuild = mkOption { + type = bool; + description = '' + Whether to build this image with Nixpkgs' + dockerTools.buildLayeredImage + and then load it with docker load. + ''; + default = true; + }; + image.name = mkOption { + type = str; + default = config.service.name; + defaultText = lib.literalExample "config.service.name"; + description = '' + A human readable name for the docker image. + + Shows up in the docker ps output in the + IMAGE column, among other places. + ''; + }; + image.contents = mkOption { + type = listOf package; + default = []; + description = '' + Top level paths in the container. + ''; + }; + image.rawConfig = mkOption { + type = attrsOf unspecified; + default = {}; + description = '' + This is a low-level fallback for when a container option has not + been modeled in the Arion module system. + + This attribute set does not have an appropriate merge function. + Please use the specific image options instead. + + Run-time configuration of the container. A full list of the + options are available at in the Docker Image Specification + v1.2.0. + ''; + }; + image.command = mkOption { + type = listOf str; + default = []; + description = '' + ''; + }; + }; + config = { + build.image = builtImage; + build.imageName = config.build.image.imageName; + build.imageTag = + if config.build.image.imageTag != "" + then config.build.image.imageTag + else lib.head (lib.strings.splitString "-" (baseNameOf config.build.image.outPath)); + + service.image = lib.mkDefault "${config.build.imageName}:${config.build.imageTag}"; + image.rawConfig.Cmd = config.image.command; + }; +} diff --git a/src/nix/modules/service/nixos-init.nix b/src/nix/modules/service/nixos-init.nix index 2b37db8..0dab936 100644 --- a/src/nix/modules/service/nixos-init.nix +++ b/src/nix/modules/service/nixos-init.nix @@ -23,15 +23,15 @@ in ../nixos/container-systemd.nix (pkgs.path + "/nixos/modules/profiles/minimal.nix") ]; - service.command = [ "${config.nixos.build.toplevel}/init" ]; + image.command = [ "${config.nixos.build.toplevel}/init" ]; service.environment.container = "docker"; service.volumes = [ "/sys/fs/cgroup:/sys/fs/cgroup:ro" ]; service.tmpfs = [ - "/tmp" - "/run" - "/run/wrappers" + "/tmp:exec,mode=777" + "/run" # noexec is fine because exes should be symlinked from elsewhere anyway + "/run/wrappers" # noexec breaks this intentionally ]; service.stop_signal = "SIGRTMIN+3"; service.tty = true;