1
0
Fork 0
mirror of https://github.com/nix-community/home-manager.git synced 2025-11-30 22:21:02 +01:00
home-manager/modules/services/podman-linux/containers.nix
Tarow 27a72d9913
podman: include systemd in quadlet service path
Podman uses systemd-run to setup transient systemd timers, e.g. for healthchecks.

On systems where systemd is not present in /run/current-system/sw/bin or ~/.nix-profile/bin (like one of my Ubuntu hosts), setting up the transient timers will fail. For containers with healthchecks configured, this results in the container being stuck in starting state.

Relevant issue here: containers/podman#25034
2025-03-19 14:09:57 -05:00

361 lines
11 KiB
Nix

{ config, lib, pkgs, ... }:
with lib;
let
cfg = config.services.podman;
podman-lib = import ./podman-lib.nix { inherit pkgs lib config; };
createQuadletSource = name: containerDef:
let
extractQuadletReference = type: value:
let
regex = "([a-zA-Z0-9_-]+\\." + type + ").*";
parts = builtins.match regex value;
in if parts == null then value else builtins.elemAt parts 0;
dependencyBySuffix = type: value:
if (hasInfix ".${type}" value) then
let name = extractQuadletReference type value;
in if (hasAttr name cfg.internal.builtQuadlets) then
[ (cfg.internal.builtQuadlets.${name}) ]
else
[ ]
else
[ ];
withResolverFor = type: value:
let resolve = v: dependencyBySuffix type v;
in if builtins.isList value then
builtins.concatLists (map resolve value) # Flatten list of lists
else
resolve value;
dependencyServices = (withResolverFor "image" containerDef.image)
++ (withResolverFor "build" containerDef.image)
++ (withResolverFor "network" containerDef.network)
++ (withResolverFor "volume" containerDef.volumes);
checkQuadletReference = types: value:
if builtins.isList value then
builtins.concatLists (map (checkQuadletReference types) value)
else
let type = findFirst (t: hasInfix ".${t}" value) null types;
in if (type != null) then
let
quadletName = extractQuadletReference type value;
quadletsOfType =
filterAttrs (n: v: v.quadletData.resourceType == type)
cfg.internal.builtQuadlets;
in if (hasAttr quadletName quadletsOfType) then
[
(replaceStrings [ quadletName ] [ "podman-${quadletName}" ]
value)
]
else
[ value ]
else if ((hasInfix "/nix/store" value) == false
&& hasAttr value cfg.internal.builtQuadlets) then
lib.warn ''
A value for Podman container '${name}' might use a reference to another quadlet: ${value}.
Append the type '.${
cfg.internal.builtQuadlets.${value}.quadletData.resourceType
}' to '${baseName value}' if this is intended.
'' [ value ]
else
[ value ];
quadlet = (podman-lib.deepMerge {
Container = {
AddCapability = containerDef.addCapabilities;
AddDevice = containerDef.devices;
AutoUpdate = containerDef.autoUpdate;
ContainerName = name;
DropCapability = containerDef.dropCapabilities;
Entrypoint = containerDef.entrypoint;
Environment = containerDef.environment;
EnvironmentFile = containerDef.environmentFile;
Exec = containerDef.exec;
Group = containerDef.group;
Image = checkQuadletReference [ "build" "image" ] containerDef.image;
IP = containerDef.ip4;
IP6 = containerDef.ip6;
Label =
(containerDef.labels // { "nix.home-manager.managed" = true; });
Network = checkQuadletReference [ "network" ] containerDef.network;
NetworkAlias = containerDef.networkAlias;
PodmanArgs = containerDef.extraPodmanArgs;
PublishPort = containerDef.ports;
UserNS = containerDef.userNS;
User = containerDef.user;
Volume = checkQuadletReference [ "volume" ] containerDef.volumes;
};
Install = {
WantedBy = optionals containerDef.autoStart [
"default.target"
"multi-user.target"
];
};
Service = {
Environment = {
PATH = (builtins.concatStringsSep ":" [
"/run/wrappers/bin"
"/run/current-system/sw/bin"
"${config.home.homeDirectory}/.nix-profile/bin"
"${pkgs.systemd}/bin"
]);
};
Restart = "always";
TimeoutStopSec = 30;
};
Unit = {
Description = (if (builtins.isString containerDef.description) then
containerDef.description
else
"Service for container ${name}");
};
} containerDef.extraConfig);
in {
dependencies = dependencyServices;
attrs = quadlet;
text = ''
# Automatically generated by home-manager podman container configuration
# DO NOT EDIT THIS FILE DIRECTLY
#
# ${name}.container
${podman-lib.toQuadletIni quadlet}
'';
};
toQuadletInternal = name: containerDef:
let src = createQuadletSource name containerDef;
in {
assertions = podman-lib.buildConfigAsserts name containerDef.extraConfig;
dependencies = src.dependencies;
resourceType = "container";
serviceName =
"podman-${src.attrs.Container.ContainerName}"; # generated service name: 'podman-<name>.service'
source = podman-lib.removeBlankLines src.text;
};
# Define the container user type as the user interface
containerDefinitionType = types.submodule {
options = {
addCapabilities = mkOption {
type = with types; listOf str;
default = [ ];
example = [ "CAP_DAC_OVERRIDE" "CAP_IPC_OWNER" ];
description = "The capabilities to add to the container.";
};
autoStart = mkOption {
type = types.bool;
default = true;
description = ''
Whether to start the container on boot (requires user lingering).
'';
};
autoUpdate = mkOption {
type = types.enum [ null "registry" "local" ];
default = null;
example = "registry";
description = "The autoupdate policy for the container.";
};
description = mkOption {
type = with types; nullOr str;
default = null;
example = "My Container";
description = "The description of the container.";
};
devices = mkOption {
type = with types; listOf str;
default = [ ];
example = [ "/dev/<host>:/dev/<container>" ];
description = "The devices to mount into the container";
};
dropCapabilities = mkOption {
type = with types; listOf str;
default = [ ];
example = [ "CAP_DAC_OVERRIDE" "CAP_IPC_OWNER" ];
description = "The capabilities to drop from the container.";
};
entrypoint = mkOption {
type = with types; nullOr str;
default = null;
example = "/foo.sh";
description = "The container entrypoint.";
};
environment = mkOption {
type = podman-lib.primitiveAttrs;
default = { };
example = literalExpression ''
{
VAR1 = "0:100";
VAR2 = true;
VAR3 = 5;
}
'';
description = "Environment variables to set in the container.";
};
environmentFile = mkOption {
type = with types; listOf str;
default = [ ];
example = [ "/etc/environment" "/etc/other-env" ];
description = ''
Paths to files containing container environment variables.
'';
};
exec = mkOption {
type = with types; nullOr str;
default = null;
example = "sleep inf";
description = "The command to run after the container start.";
};
extraPodmanArgs = mkOption {
type = types.listOf types.str;
default = [ ];
example = [
"--security-opt=no-new-privileges"
"--security-opt=seccomp=unconfined"
];
description = "Extra arguments to pass to the podman run command.";
};
extraConfig = mkOption {
type = podman-lib.extraConfigType;
default = { };
example = literalExpression ''
{
Container = {
User = 1000;
};
Service = {
TimeoutStartSec = 15;
};
}
'';
description = ''
INI sections and values to populate the Container Quadlet.
'';
};
group = mkOption {
type = with types; nullOr (either int str);
default = null;
description = "The group ID inside the container.";
};
image = mkOption {
type = types.str;
example = "registry.access.redhat.com/ubi9-minimal:latest";
description = "The container image.";
};
ip4 = mkOption {
type = with types; nullOr str;
default = null;
description = "Set an IPv4 address for the container.";
};
ip6 = mkOption {
type = with types; nullOr str;
default = null;
description = "Set an IPv6 address for the container.";
};
labels = mkOption {
type = with types; attrsOf str;
default = { };
example = {
app = "myapp";
some-label = "somelabel";
};
description = "The labels to apply to the container.";
};
network = mkOption {
type = with types; either str (listOf str);
default = [ ];
apply = value: if isString value then [ value ] else value;
example = literalMD ''
`"host"`
or
`"bridge_network_1"`
or
`[ "bridge_network_1" "bridge_network_2" ]`
'';
description = ''
The network mode or network/s to connect the container to. Equivalent
to `podman run --network=<option>`.
'';
};
networkAlias = mkOption {
type = with types; listOf str;
default = [ ];
example = [ "mycontainer" "web" ];
description = "Network aliases for the container.";
};
ports = mkOption {
type = with types; listOf str;
default = [ ];
example = [ "8080:80" "8443:443" ];
description = "A mapping of ports between host and container";
};
userNS = mkOption {
type = with types; nullOr str;
default = null;
description = "Use a user namespace for the container.";
};
user = mkOption {
type = with types; nullOr (either int str);
default = null;
description = "The user ID inside the container.";
};
volumes = mkOption {
type = with types; listOf str;
default = [ ];
example = [ "/tmp:/tmp" "/var/run/test.secret:/etc/secret:ro" ];
description = "The volumes to mount into the container.";
};
};
};
in {
imports = [ ./options.nix ];
options.services.podman.containers = mkOption {
type = types.attrsOf containerDefinitionType;
default = { };
description = "Defines Podman container quadlet configurations.";
};
config =
let containerQuadlets = mapAttrsToList toQuadletInternal cfg.containers;
in mkIf cfg.enable {
services.podman.internal.quadletDefinitions = containerQuadlets;
assertions =
flatten (map (container: container.assertions) containerQuadlets);
# manifest file
xdg.configFile."podman/containers.manifest".text =
podman-lib.generateManifestText containerQuadlets;
};
}