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:
@@ -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
|
||||
})
|
||||
]
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user