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; + }; }; };