From 2f7747526cc80844a506c4aa14706429324be157 Mon Sep 17 00:00:00 2001 From: adisbladis Date: Mon, 20 Apr 2020 12:31:07 +0100 Subject: nixos/docker-containers: Rename to virtualisation.oci-containers.containers. And allow the runtime to be configurable via the `virtualisation.oci-containers.backend` option. Valid choices are "podman" and "docker". --- nixos/doc/manual/release-notes/rl-2009.xml | 6 + nixos/modules/module-list.nix | 2 +- nixos/modules/virtualisation/docker-containers.nix | 281 ------------------ nixos/modules/virtualisation/oci-containers.nix | 323 +++++++++++++++++++++ nixos/modules/virtualisation/podman.nix | 12 +- nixos/tests/all-tests.nix | 2 +- nixos/tests/docker-containers.nix | 27 -- nixos/tests/oci-containers.nix | 43 +++ 8 files changed, 385 insertions(+), 311 deletions(-) delete mode 100644 nixos/modules/virtualisation/docker-containers.nix create mode 100644 nixos/modules/virtualisation/oci-containers.nix delete mode 100644 nixos/tests/docker-containers.nix create mode 100644 nixos/tests/oci-containers.nix diff --git a/nixos/doc/manual/release-notes/rl-2009.xml b/nixos/doc/manual/release-notes/rl-2009.xml index 78b8eee47ef..c6a766cc045 100644 --- a/nixos/doc/manual/release-notes/rl-2009.xml +++ b/nixos/doc/manual/release-notes/rl-2009.xml @@ -55,6 +55,12 @@ The new virtualisation.containers module manages configuration shared by the CRI-O and Podman modules. + + + Declarative Docker containers are renamed from docker-containers to virtualisation.oci-containers.containers. + This is to make it possible to use podman instead of docker. + + diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix index 0cd17775e51..28f536056bf 100644 --- a/nixos/modules/module-list.nix +++ b/nixos/modules/module-list.nix @@ -984,9 +984,9 @@ ./virtualisation/container-config.nix ./virtualisation/containers.nix ./virtualisation/nixos-containers.nix + ./virtualisation/oci-containers.nix ./virtualisation/cri-o.nix ./virtualisation/docker.nix - ./virtualisation/docker-containers.nix ./virtualisation/ecs-agent.nix ./virtualisation/libvirtd.nix ./virtualisation/lxc.nix diff --git a/nixos/modules/virtualisation/docker-containers.nix b/nixos/modules/virtualisation/docker-containers.nix deleted file mode 100644 index 5ab990a3d7c..00000000000 --- a/nixos/modules/virtualisation/docker-containers.nix +++ /dev/null @@ -1,281 +0,0 @@ -{ config, lib, pkgs, ... }: - -with lib; -let - cfg = config.docker-containers; - - dockerContainer = - { ... }: { - - options = { - - image = mkOption { - type = with types; str; - description = "Docker image to run."; - example = "library/hello-world"; - }; - - imageFile = mkOption { - type = with types; nullOr package; - default = null; - description = '' - Path to an image file to load instead of pulling from a registry. - If defined, do not pull from registry. - - You still need to set the image attribute, as it - will be used as the image name for docker to start a container. - ''; - example = literalExample "pkgs.dockerTools.buildDockerImage {...};"; - }; - - cmd = mkOption { - type = with types; listOf str; - default = []; - description = "Commandline arguments to pass to the image's entrypoint."; - example = literalExample '' - ["--port=9000"] - ''; - }; - - entrypoint = mkOption { - type = with types; nullOr str; - description = "Override the default entrypoint of the image."; - default = null; - example = "/bin/my-app"; - }; - - environment = mkOption { - type = with types; attrsOf str; - default = {}; - description = "Environment variables to set for this container."; - example = literalExample '' - { - DATABASE_HOST = "db.example.com"; - DATABASE_PORT = "3306"; - } - ''; - }; - - log-driver = mkOption { - type = types.str; - default = "none"; - description = '' - Logging driver for the container. The default of - "none" means that the container's logs will be - handled as part of the systemd unit. Setting this to - "journald" will result in duplicate logging, but - the container's logs will be visible to the docker - logs command. - - For more details and a full list of logging drivers, refer to the - - Docker engine documentation - ''; - }; - - ports = mkOption { - type = with types; listOf str; - default = []; - description = '' - Network ports to publish from the container to the outer host. - - Valid formats: - - - - - <ip>:<hostPort>:<containerPort> - - - - - <ip>::<containerPort> - - - - - <hostPort>:<containerPort> - - - - - <containerPort> - - - - - Both hostPort and - containerPort can be specified as a range of - ports. When specifying ranges for both, the number of container - ports in the range must match the number of host ports in the - range. Example: 1234-1236:1234-1236/tcp - - When specifying a range for hostPort only, the - containerPort must not be a - range. In this case, the container port is published somewhere - within the specified hostPort range. Example: - 1234-1236:1234/tcp - - Refer to the - - Docker engine documentation for full details. - ''; - example = literalExample '' - [ - "8080:9000" - ] - ''; - }; - - user = mkOption { - type = with types; nullOr str; - default = null; - description = '' - Override the username or UID (and optionally groupname or GID) used - in the container. - ''; - example = "nobody:nogroup"; - }; - - volumes = mkOption { - type = with types; listOf str; - default = []; - description = '' - List of volumes to attach to this container. - - Note that this is a list of "src:dst" strings to - allow for src to refer to - /nix/store paths, which would be difficult with an - attribute set. There are also a variety of mount options available - as a third field; please refer to the - - docker engine documentation for details. - ''; - example = literalExample '' - [ - "volume_name:/path/inside/container" - "/path/on/host:/path/inside/container" - ] - ''; - }; - - workdir = mkOption { - type = with types; nullOr str; - default = null; - description = "Override the default working directory for the container."; - example = "/var/lib/hello_world"; - }; - - dependsOn = mkOption { - type = with types; listOf str; - default = []; - description = '' - Define which other containers this one depends on. They will be added to both After and Requires for the unit. - - Use the same name as the attribute under services.docker-containers. - ''; - example = literalExample '' - services.docker-containers = { - node1 = {}; - node2 = { - dependsOn = [ "node1" ]; - } - } - ''; - }; - - extraDockerOptions = mkOption { - type = with types; listOf str; - default = []; - description = "Extra options for docker run."; - example = literalExample '' - ["--network=host"] - ''; - }; - - autoStart = mkOption { - type = types.bool; - default = true; - description = '' - When enabled, the container is automatically started on boot. - If this option is set to false, the container has to be started on-demand via its service. - ''; - }; - }; - }; - - mkService = name: container: let - mkAfter = map (x: "docker-${x}.service") container.dependsOn; - in rec { - wantedBy = [] ++ optional (container.autoStart) "multi-user.target"; - after = [ "docker.service" "docker.socket" ] ++ mkAfter; - requires = after; - path = [ pkgs.docker ]; - - preStart = '' - docker rm -f ${name} || true - ${optionalString (container.imageFile != null) '' - docker load -i ${container.imageFile} - ''} - ''; - postStop = "docker rm -f ${name} || true"; - - serviceConfig = { - ExecStart = concatStringsSep " \\\n " ([ - "${pkgs.docker}/bin/docker run" - "--rm" - "--name=${name}" - "--log-driver=${container.log-driver}" - ] ++ optional (container.entrypoint != null) - "--entrypoint=${escapeShellArg container.entrypoint}" - ++ (mapAttrsToList (k: v: "-e ${escapeShellArg k}=${escapeShellArg v}") container.environment) - ++ map (p: "-p ${escapeShellArg p}") container.ports - ++ optional (container.user != null) "-u ${escapeShellArg container.user}" - ++ map (v: "-v ${escapeShellArg v}") container.volumes - ++ optional (container.workdir != null) "-w ${escapeShellArg container.workdir}" - ++ map escapeShellArg container.extraDockerOptions - ++ [container.image] - ++ map escapeShellArg container.cmd - ); - - ExecStop = ''${pkgs.bash}/bin/sh -c "[ $SERVICE_RESULT = success ] || docker stop ${name}"''; - - ### There is no generalized way of supporting `reload` for docker - ### containers. Some containers may respond well to SIGHUP sent to their - ### init process, but it is not guaranteed; some apps have other reload - ### mechanisms, some don't have a reload signal at all, and some docker - ### images just have broken signal handling. The best compromise in this - ### case is probably to leave ExecReload undefined, so `systemctl reload` - ### will at least result in an error instead of potentially undefined - ### behaviour. - ### - ### Advanced users can still override this part of the unit to implement - ### a custom reload handler, since the result of all this is a normal - ### systemd service from the perspective of the NixOS module system. - ### - # ExecReload = ...; - ### - - TimeoutStartSec = 0; - TimeoutStopSec = 120; - Restart = "always"; - }; - }; - -in { - - options.docker-containers = mkOption { - default = {}; - type = types.attrsOf (types.submodule dockerContainer); - description = "Docker containers to run as systemd services."; - }; - - config = mkIf (cfg != {}) { - - systemd.services = mapAttrs' (n: v: nameValuePair "docker-${n}" (mkService n v)) cfg; - - virtualisation.docker.enable = true; - - }; - -} diff --git a/nixos/modules/virtualisation/oci-containers.nix b/nixos/modules/virtualisation/oci-containers.nix new file mode 100644 index 00000000000..a46dd65eb49 --- /dev/null +++ b/nixos/modules/virtualisation/oci-containers.nix @@ -0,0 +1,323 @@ +{ config, options, lib, pkgs, ... }: + +with lib; +let + cfg = config.virtualisation.oci-containers; + proxy_env = config.networking.proxy.envVars; + + defaultBackend = options.virtualisation.oci-containers.backend.default; + + containerOptions = + { ... }: { + + options = { + + image = mkOption { + type = with types; str; + description = "OCI image to run."; + example = "library/hello-world"; + }; + + imageFile = mkOption { + type = with types; nullOr package; + default = null; + description = '' + Path to an image file to load instead of pulling from a registry. + If defined, do not pull from registry. + + You still need to set the image attribute, as it + will be used as the image name for docker to start a container. + ''; + example = literalExample "pkgs.dockerTools.buildDockerImage {...};"; + }; + + cmd = mkOption { + type = with types; listOf str; + default = []; + description = "Commandline arguments to pass to the image's entrypoint."; + example = literalExample '' + ["--port=9000"] + ''; + }; + + entrypoint = mkOption { + type = with types; nullOr str; + description = "Override the default entrypoint of the image."; + default = null; + example = "/bin/my-app"; + }; + + environment = mkOption { + type = with types; attrsOf str; + default = {}; + description = "Environment variables to set for this container."; + example = literalExample '' + { + DATABASE_HOST = "db.example.com"; + DATABASE_PORT = "3306"; + } + ''; + }; + + log-driver = mkOption { + type = types.str; + default = "journald"; + description = '' + Logging driver for the container. The default of + "journald" means that the container's logs will be + handled as part of the systemd unit. + + For more details and a full list of logging drivers, refer to respective backends documentation. + + For Docker: + Docker engine documentation + + For Podman: + Refer to the docker-run(1) man page. + ''; + }; + + ports = mkOption { + type = with types; listOf str; + default = []; + description = '' + Network ports to publish from the container to the outer host. + + Valid formats: + + + + + <ip>:<hostPort>:<containerPort> + + + + + <ip>::<containerPort> + + + + + <hostPort>:<containerPort> + + + + + <containerPort> + + + + + Both hostPort and + containerPort can be specified as a range of + ports. When specifying ranges for both, the number of container + ports in the range must match the number of host ports in the + range. Example: 1234-1236:1234-1236/tcp + + When specifying a range for hostPort only, the + containerPort must not be a + range. In this case, the container port is published somewhere + within the specified hostPort range. Example: + 1234-1236:1234/tcp + + Refer to the + + Docker engine documentation for full details. + ''; + example = literalExample '' + [ + "8080:9000" + ] + ''; + }; + + user = mkOption { + type = with types; nullOr str; + default = null; + description = '' + Override the username or UID (and optionally groupname or GID) used + in the container. + ''; + example = "nobody:nogroup"; + }; + + volumes = mkOption { + type = with types; listOf str; + default = []; + description = '' + List of volumes to attach to this container. + + Note that this is a list of "src:dst" strings to + allow for src to refer to + /nix/store paths, which would be difficult with an + attribute set. There are also a variety of mount options available + as a third field; please refer to the + + docker engine documentation for details. + ''; + example = literalExample '' + [ + "volume_name:/path/inside/container" + "/path/on/host:/path/inside/container" + ] + ''; + }; + + workdir = mkOption { + type = with types; nullOr str; + default = null; + description = "Override the default working directory for the container."; + example = "/var/lib/hello_world"; + }; + + dependsOn = mkOption { + type = with types; listOf str; + default = []; + description = '' + Define which other containers this one depends on. They will be added to both After and Requires for the unit. + + Use the same name as the attribute under virtualisation.oci-containers. + ''; + example = literalExample '' + virtualisation.oci-containers = { + node1 = {}; + node2 = { + dependsOn = [ "node1" ]; + } + } + ''; + }; + + extraOptions = mkOption { + type = with types; listOf str; + default = []; + description = "Extra options for ${defaultBackend} run."; + example = literalExample '' + ["--network=host"] + ''; + }; + + autoStart = mkOption { + type = types.bool; + default = true; + description = '' + When enabled, the container is automatically started on boot. + If this option is set to false, the container has to be started on-demand via its service. + ''; + }; + }; + }; + + mkService = name: container: let + dependsOn = map (x: "${cfg.backend}-${x}.service") container.dependsOn; + in { + wantedBy = [] ++ optional (container.autoStart) "multi-user.target"; + after = lib.optionals (cfg.backend == "docker") [ "docker.service" "docker.socket" ] ++ dependsOn; + requires = dependsOn; + environment = proxy_env; + + path = + if cfg.backend == "docker" then [ pkgs.docker ] + else if cfg.backend == "podman" then [ config.virtualisation.podman.package ] + else throw "Unhandled backend: ${cfg.backend}"; + + preStart = '' + ${cfg.backend} rm -f ${name} || true + ${optionalString (container.imageFile != null) '' + ${cfg.backend} load -i ${container.imageFile} + ''} + ''; + postStop = "${cfg.backend} rm -f ${name} || true"; + + serviceConfig = { + StandardOutput = "null"; + StandardError = "null"; + ExecStart = concatStringsSep " \\\n " ([ + "${config.system.path}/bin/${cfg.backend} run" + "--rm" + "--name=${name}" + "--log-driver=${container.log-driver}" + ] ++ optional (container.entrypoint != null) + "--entrypoint=${escapeShellArg container.entrypoint}" + ++ (mapAttrsToList (k: v: "-e ${escapeShellArg k}=${escapeShellArg v}") container.environment) + ++ map (p: "-p ${escapeShellArg p}") container.ports + ++ optional (container.user != null) "-u ${escapeShellArg container.user}" + ++ map (v: "-v ${escapeShellArg v}") container.volumes + ++ optional (container.workdir != null) "-w ${escapeShellArg container.workdir}" + ++ map escapeShellArg container.extraOptions + ++ [container.image] + ++ map escapeShellArg container.cmd + ); + + ExecStop = ''${pkgs.bash}/bin/sh -c "[ $SERVICE_RESULT = success ] || ${cfg.backend} stop ${name}"''; + + ### There is no generalized way of supporting `reload` for docker + ### containers. Some containers may respond well to SIGHUP sent to their + ### init process, but it is not guaranteed; some apps have other reload + ### mechanisms, some don't have a reload signal at all, and some docker + ### images just have broken signal handling. The best compromise in this + ### case is probably to leave ExecReload undefined, so `systemctl reload` + ### will at least result in an error instead of potentially undefined + ### behaviour. + ### + ### Advanced users can still override this part of the unit to implement + ### a custom reload handler, since the result of all this is a normal + ### systemd service from the perspective of the NixOS module system. + ### + # ExecReload = ...; + ### + + TimeoutStartSec = 0; + TimeoutStopSec = 120; + Restart = "always"; + }; + }; + +in { + imports = [ + ( + lib.mkChangedOptionModule + [ "docker-containers" ] + [ "virtualisation" "oci-containers" ] + (oldcfg: { + backend = "docker"; + containers = lib.mapAttrs (n: v: builtins.removeAttrs (v // { + extraOptions = v.extraDockerOptions or []; + }) [ "extraDockerOptions" ]) oldcfg.docker-containers; + }) + ) + ]; + + options.virtualisation.oci-containers = { + + backend = mkOption { + type = types.enum [ "podman" "docker" ]; + default = + # TODO: Once https://github.com/NixOS/nixpkgs/issues/77925 is resolved default to podman + # if versionAtLeast config.system.stateVersion "20.09" then "podman" + # else "docker"; + "docker"; + description = "The underlying Docker implementation to use."; + }; + + containers = mkOption { + default = {}; + type = types.attrsOf (types.submodule containerOptions); + description = "OCI (Docker) containers to run as systemd services."; + }; + + }; + + config = lib.mkIf (cfg.containers != {}) (lib.mkMerge [ + { + systemd.services = mapAttrs' (n: v: nameValuePair "${cfg.backend}-${n}" (mkService n v)) cfg.containers; + } + (lib.mkIf (cfg.backend == "podman") { + virtualisation.podman.enable = true; + }) + (lib.mkIf (cfg.backend == "docker") { + virtualisation.docker.enable = true; + }) + ]); + +} diff --git a/nixos/modules/virtualisation/podman.nix b/nixos/modules/virtualisation/podman.nix index 1dc79272ccb..9f98c2086d1 100644 --- a/nixos/modules/virtualisation/podman.nix +++ b/nixos/modules/virtualisation/podman.nix @@ -88,11 +88,21 @@ in }; }; + package = lib.mkOption { + type = types.package; + default = podmanPackage; + internal = true; + description = '' + The final Podman package (including extra packages). + ''; + }; + + }; config = lib.mkIf cfg.enable { - environment.systemPackages = [ podmanPackage ] + environment.systemPackages = [ cfg.package ] ++ lib.optional cfg.dockerCompat dockerCompat; environment.etc."containers/libpod.conf".text = '' diff --git a/nixos/tests/all-tests.nix b/nixos/tests/all-tests.nix index eff1752bbbf..ebb0dfef15a 100644 --- a/nixos/tests/all-tests.nix +++ b/nixos/tests/all-tests.nix @@ -70,7 +70,7 @@ in dhparams = handleTest ./dhparams.nix {}; dnscrypt-proxy2 = handleTestOn ["x86_64-linux"] ./dnscrypt-proxy2.nix {}; docker = handleTestOn ["x86_64-linux"] ./docker.nix {}; - docker-containers = handleTestOn ["x86_64-linux"] ./docker-containers.nix {}; + oci-containers = handleTestOn ["x86_64-linux"] ./oci-containers.nix {}; docker-edge = handleTestOn ["x86_64-linux"] ./docker-edge.nix {}; docker-preloader = handleTestOn ["x86_64-linux"] ./docker-preloader.nix {}; docker-registry = handleTest ./docker-registry.nix {}; diff --git a/nixos/tests/docker-containers.nix b/nixos/tests/docker-containers.nix deleted file mode 100644 index 0e318a52d9f..00000000000 --- a/nixos/tests/docker-containers.nix +++ /dev/null @@ -1,27 +0,0 @@ -# Test Docker containers as systemd units - -import ./make-test-python.nix ({ pkgs, lib, ... }: { - name = "docker-containers"; - meta = { - maintainers = with lib.maintainers; [ benley mkaito ]; - }; - - nodes = { - docker = { pkgs, ... }: { - virtualisation.docker.enable = true; - - docker-containers.nginx = { - image = "nginx-container"; - imageFile = pkgs.dockerTools.examples.nginx; - ports = ["8181:80"]; - }; - }; - }; - - testScript = '' - start_all() - docker.wait_for_unit("docker-nginx.service") - docker.wait_for_open_port(8181) - docker.wait_until_succeeds("curl http://localhost:8181 | grep Hello") - ''; -}) diff --git a/nixos/tests/oci-containers.nix b/nixos/tests/oci-containers.nix new file mode 100644 index 00000000000..bb6c019f07c --- /dev/null +++ b/nixos/tests/oci-containers.nix @@ -0,0 +1,43 @@ +{ system ? builtins.currentSystem +, config ? {} +, pkgs ? import ../.. { inherit system config; } +, lib ? pkgs.lib +}: + +let + + inherit (import ../lib/testing-python.nix { inherit system pkgs; }) makeTest; + + mkOCITest = backend: makeTest { + name = "oci-containers-${backend}"; + + meta = { + maintainers = with lib.maintainers; [ adisbladis benley mkaito ]; + }; + + nodes = { + ${backend} = { pkgs, ... }: { + virtualisation.oci-containers = { + inherit backend; + containers.nginx = { + image = "nginx-container"; + imageFile = pkgs.dockerTools.examples.nginx; + ports = ["8181:80"]; + }; + }; + }; + }; + + testScript = '' + start_all() + ${backend}.wait_for_unit("${backend}-nginx.service") + ${backend}.wait_for_open_port(8181) + ${backend}.wait_until_succeeds("curl http://localhost:8181 | grep Hello") + ''; + }; + +in +lib.foldl' (attrs: backend: attrs // { ${backend} = mkOCITest backend; }) {} [ + "docker" + "podman" +] -- cgit 1.4.1