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
This commit is contained in:
Maximilian Bosch
2024-12-27 13:07:24 +01:00
parent 02a75dfbd0
commit 7d443d378b
4 changed files with 185 additions and 24 deletions

View File

@@ -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
})
]
);
}