From 7d443d378b07ad55686e9ba68faf16802c030025 Mon Sep 17 00:00:00 2001 From: Maximilian Bosch Date: Fri, 27 Dec 2024 13:07:24 +0100 Subject: [PATCH] nixos/oci-containers: support rootless containers & healthchecks Closes #259770 Closes #207050 The motivation for the former is to not execute the container as root, so you don't have to `sudo -i` to perform podman management tasks. The idea behind healthchecks is to be able to keep the unit in the activating state until the container is healthy, only then then unit is marked as active. The following changes were necessary: * Move the ctr-id into `/run/${containerName}` to make podman can actually write to it since it's now in its RuntimeDirectory. * Make `sdnotify` option configurable (`healthy` for healthchecks that must pass, default remains `conmon`). * Set Delegate=yes for `sdnotify=healthy` to make sure a rootless container can actually talk to sd_notify[1]. * Add a warning that lingering must be enabled to have a `systemd --user` instance running which is required for the cgroup support to work properly. * Added a testcase for rootless containers with both conmon and healthchecks. [1] https://github.com/containers/podman/discussions/20573#discussioncomment-7612481 --- .../manual/release-notes/rl-2505.section.md | 3 + .../modules/virtualisation/oci-containers.nix | 122 +++++++++++++++--- nixos/tests/oci-containers.nix | 65 +++++++++- pkgs/build-support/docker/examples.nix | 19 +++ 4 files changed, 185 insertions(+), 24 deletions(-) diff --git a/nixos/doc/manual/release-notes/rl-2505.section.md b/nixos/doc/manual/release-notes/rl-2505.section.md index 06345d9e2e16..351cba01c58a 100644 --- a/nixos/doc/manual/release-notes/rl-2505.section.md +++ b/nixos/doc/manual/release-notes/rl-2505.section.md @@ -470,6 +470,9 @@ +- `virtualisation.containers` with backend "podman" now supports rootless containers and `sd_notify(3)`-integration + based on container healthchecks. + - Cinnamon has been updated to 6.4, please check the [upstream announcement](https://www.linuxmint.com/rel_xia_whatsnew.php) for more details. - Following [changes in Mint 22](https://github.com/linuxmint/mintupgrade/commit/f239cde908288b8c250f938e7311c7ffbc16bd59) we are no longer overriding Qt application styles. You can still restore the previous default with `qt.style = "gtk2"` and `qt.platformTheme = "gtk2"`. - Following [changes in Mint 20](https://github.com/linuxmint/mintupgrade-legacy/commit/ce15d946ed9a8cb8444abd25088edd824bfb18f6) we are replacing xplayer with celluloid since xplayer is no longer maintained. diff --git a/nixos/modules/virtualisation/oci-containers.nix b/nixos/modules/virtualisation/oci-containers.nix index 642e3a5e6d76..8f24eee80a83 100644 --- a/nixos/modules/virtualisation/oci-containers.nix +++ b/nixos/modules/virtualisation/oci-containers.nix @@ -17,6 +17,10 @@ let { name, ... }: { + config = { + podman = mkIf (cfg.backend == "podman") { }; + }; + options = { image = mkOption { @@ -287,6 +291,43 @@ let ''; }; + podman = mkOption { + type = types.nullOr ( + types.submodule { + options = { + sdnotify = mkOption { + default = "conmon"; + type = types.enum [ + "conmon" + "healthy" + "container" + ]; + description = '' + Determines how `podman` should notify systemd that the unit is ready. There are + [three options](https://docs.podman.io/en/latest/markdown/podman-run.1.html#sdnotify-container-conmon-healthy-ignore): + + * `conmon`: marks the unit as ready when the container has started. + * `healthy`: marks the unit as ready when the [container's healthcheck](https://docs.podman.io/en/stable/markdown/podman-healthcheck-run.1.html) passes. + * `container`: `NOTIFY_SOCKET` is passed into the container and the process inside the container needs to indicate on its own that it's ready. + ''; + }; + user = mkOption { + default = "root"; + type = types.str; + description = '' + The user under which the container should run. + ''; + }; + }; + } + ); + default = null; + description = '' + Podman-specific settings in OCI containers. These must be null when using + the `docker` backend. + ''; + }; + pull = mkOption { type = with types; @@ -379,16 +420,20 @@ let ${container.imageStream} | ${cfg.backend} load ''} ${optionalString (cfg.backend == "podman") '' - rm -f /run/podman-${escapedName}.ctr-id + rm -f /run/${escapedName}/ctr-id ''} ''; }; + + effectiveUser = container.podman.user or "root"; + dependOnLingerService = + cfg.backend == "podman" && effectiveUser != "root" && config.users.users.${effectiveUser}.linger; in { wantedBy = [ ] ++ optional (container.autoStart) "multi-user.target"; - wants = lib.optional ( - container.imageFile == null && container.imageStream == null - ) "network-online.target"; + wants = + lib.optional (container.imageFile == null && container.imageStream == null) "network-online.target" + ++ lib.optional dependOnLingerService "linger-users.service"; after = lib.optionals (cfg.backend == "docker") [ "docker.service" @@ -398,9 +443,15 @@ let ++ lib.optionals (container.imageFile == null && container.imageStream == null) [ "network-online.target" ] - ++ dependsOn; + ++ dependsOn + ++ lib.optional dependOnLingerService "linger-users.service"; requires = dependsOn; - environment = proxy_env; + environment = lib.mkMerge [ + proxy_env + (mkIf (cfg.backend == "podman" && container.podman.user != "root") { + HOME = config.users.users.${container.podman.user}.home; + }) + ]; path = if cfg.backend == "docker" then @@ -424,9 +475,9 @@ let ++ optional (container.entrypoint != null) "--entrypoint=${escapeShellArg container.entrypoint}" ++ optional (container.hostname != null) "--hostname=${escapeShellArg container.hostname}" ++ lib.optionals (cfg.backend == "podman") [ - "--cidfile=/run/podman-${escapedName}.ctr-id" - "--cgroups=no-conmon" - "--sdnotify=conmon" + "--cidfile=/run/${escapedName}/ctr-id" + "--cgroups=enabled" + "--sdnotify=${container.podman.sdnotify}" "-d" "--replace" ] @@ -454,13 +505,13 @@ let preStop = if cfg.backend == "podman" then - "podman stop --ignore --cidfile=/run/podman-${escapedName}.ctr-id" + "podman stop --ignore --cidfile=/run/${escapedName}/ctr-id" else "${cfg.backend} stop ${name} || true"; postStop = if cfg.backend == "podman" then - "podman rm -f --ignore --cidfile=/run/podman-${escapedName}.ctr-id" + "podman rm -f --ignore --cidfile=/run/${escapedName}/ctr-id" else "${cfg.backend} rm -f ${name} || true"; @@ -490,6 +541,9 @@ let Environment = "PODMAN_SYSTEMD_UNIT=podman-${name}.service"; Type = "notify"; NotifyAccess = "all"; + Delegate = mkIf (container.podman.sdnotify == "healthy") true; + User = effectiveUser; + RuntimeDirectory = escapedName; }; }; @@ -536,17 +590,46 @@ in assertions = let - toAssertion = - _: - { imageFile, imageStream, ... }: + toAssertions = + name: { - assertion = imageFile == null || imageStream == null; - - message = "You can only define one of imageFile and imageStream"; - }; + imageFile, + imageStream, + podman, + ... + }: + [ + { + assertion = imageFile == null || imageStream == null; + message = "virtualisation.oci-containers.containers.${name}: You can only define one of imageFile and imageStream"; + } + { + assertion = cfg.backend == "docker" -> podman == null; + message = "virtualisation.oci-containers.containers.${name}: Cannot set `podman` option if backend is `docker`."; + } + ]; in - lib.mapAttrsToList toAssertion cfg.containers; + concatMap (name: toAssertions name cfg.containers.${name}) (lib.attrNames cfg.containers); + + warnings = mkIf (cfg.backend == "podman") ( + lib.foldlAttrs ( + warnings: name: + { podman, ... }: + let + inherit (config.users.users.${podman.user}) linger; + in + warnings + ++ lib.optional (podman.user != "root" && linger && podman.sdnotify == "conmon") '' + Podman container ${name} is configured as rootless (user ${podman.user}) + with `--sdnotify=conmon`, but lingering for this user is turned on. + '' + ++ lib.optional (podman.user != "root" && !linger && podman.sdnotify == "healthy") '' + Podman container ${name} is configured as rootless (user ${podman.user}) + with `--sdnotify=healthy`, but lingering for this user is turned off. + '' + ) [ ] cfg.containers + ); } (lib.mkIf (cfg.backend == "podman") { virtualisation.podman.enable = true; @@ -556,5 +639,4 @@ in }) ] ); - } diff --git a/nixos/tests/oci-containers.nix b/nixos/tests/oci-containers.nix index ed83446b44b9..073f62cf5155 100644 --- a/nixos/tests/oci-containers.nix +++ b/nixos/tests/oci-containers.nix @@ -63,8 +63,65 @@ let ''; }; + podmanRootlessTests = lib.genAttrs [ "conmon" "healthy" ] ( + type: + makeTest { + name = "oci-containers-podman-rootless-${type}"; + meta.maintainers = lib.teams.flyingcircus.members; + nodes = { + podman = + { pkgs, ... }: + { + environment.systemPackages = [ pkgs.redis ]; + users.groups.redis = { }; + users.users.redis = { + isSystemUser = true; + group = "redis"; + home = "/var/lib/redis"; + linger = type == "healthy"; + createHome = true; + subUidRanges = [ + { + count = 65536; + startUid = 2147483646; + } + ]; + subGidRanges = [ + { + count = 65536; + startGid = 2147483647; + } + ]; + }; + virtualisation.oci-containers = { + backend = "podman"; + containers.redis = { + image = "redis:latest"; + imageFile = pkgs.dockerTools.examples.redis; + ports = [ "6379:6379" ]; + podman = { + user = "redis"; + sdnotify = type; + }; + }; + }; + }; + }; + + testScript = '' + start_all() + podman.wait_for_unit("podman-redis.service") + ${lib.optionalString (type != "healthy") '' + podman.wait_for_open_port(6379) + ''} + podman.wait_until_succeeds("set -eo pipefail; echo 'keys *' | redis-cli") + ''; + } + ); in -lib.foldl' (attrs: backend: attrs // { ${backend} = mkOCITest backend; }) { } [ - "docker" - "podman" -] +{ + docker = mkOCITest "docker"; + podman = mkOCITest "podman"; + podman-rootless-conmon = podmanRootlessTests.conmon; + podman-rootless-healthy = podmanRootlessTests.healthy; +} diff --git a/pkgs/build-support/docker/examples.nix b/pkgs/build-support/docker/examples.nix index 683c8255ca05..5edc1db78ee8 100644 --- a/pkgs/build-support/docker/examples.nix +++ b/pkgs/build-support/docker/examples.nix @@ -110,6 +110,16 @@ rec { runAsRoot = '' mkdir -p /data + cat >/bin/healthcheck <<-'EOF' + set -x + probe="$(/bin/redis-cli ping)" + echo "$probe" + if [ "$probe" = 'PONG' ]; then + exit 0 + fi + exit 1 + EOF + chmod +x /bin/healthcheck ''; config = { @@ -118,6 +128,15 @@ rec { Volumes = { "/data" = { }; }; + Healthcheck = { + Test = [ + "CMD-SHELL" + "/bin/healthcheck" + ]; + Interval = 30000000000; + Timeout = 10000000000; + Retries = 3; + }; }; };