nixpkgs/nixos/tests/oci-containers.nix
Maximilian Bosch 7d443d378b
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
2025-02-27 11:08:33 +01:00

127 lines
4 KiB
Nix

{
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 = lib.teams.serokell.members ++ (with lib.maintainers; [ benley ]);
nodes = {
${backend} =
{ pkgs, ... }:
{
virtualisation.oci-containers = {
inherit backend;
containers.nginx = {
image = "nginx-container";
imageStream = pkgs.dockerTools.examples.nginxStream;
ports = [ "8181:80" ];
capabilities = {
CAP_AUDIT_READ = true;
CAP_AUDIT_WRITE = false;
};
privileged = false;
devices = [
"/dev/random:/dev/random"
];
};
};
# Stop systemd from killing remaining processes if ExecStop script
# doesn't work, so that proper stopping can be tested.
systemd.services."${backend}-nginx".serviceConfig.KillSignal = "SIGCONT";
};
};
testScript = ''
import json
start_all()
${backend}.wait_for_unit("${backend}-nginx.service")
${backend}.wait_for_open_port(8181)
${backend}.wait_until_succeeds("curl -f http://localhost:8181 | grep Hello")
output = json.loads(${backend}.succeed("${backend} inspect nginx --format json").strip())[0]
${backend}.succeed("systemctl stop ${backend}-nginx.service", timeout=10)
assert output['HostConfig']['CapAdd'] == ["CAP_AUDIT_READ"]
assert output['HostConfig']['CapDrop'] == ${
if backend == "docker" then "[\"CAP_AUDIT_WRITE\"]" else "[]"
} # Rootless podman runs with no capabilities so it cannot drop them
assert output['HostConfig']['Privileged'] == False
assert output['HostConfig']['Devices'] == [{'PathOnHost': '/dev/random', 'PathInContainer': '/dev/random', 'CgroupPermissions': '${
if backend == "docker" then "rwm" else ""
}'}]
'';
};
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
{
docker = mkOCITest "docker";
podman = mkOCITest "podman";
podman-rootless-conmon = podmanRootlessTests.conmon;
podman-rootless-healthy = podmanRootlessTests.healthy;
}