podman: add module

Adds a new Podman module for creating user containers and networks as
systemd services. These are installed to the user's
`$XDG_CONFIG/systemd/user` directory.
This commit is contained in:
Nicholas Hassan 2023-12-22 10:54:18 +10:30 committed by Robert Helgesson
parent 8ca921e5a8
commit 1743615b61
No known key found for this signature in database
GPG key ID: 96E745BD17AA17ED
21 changed files with 1327 additions and 0 deletions

View file

@ -43,6 +43,12 @@
github = "Avimitin";
githubId = 30021675;
};
bamhm182 = {
name = "bamhm182";
email = "bamhm182@gmail.com";
github = "bamhm182";
githubId = 920269;
};
blmhemu = {
name = "blmhemu";
email = "19410501+blmhemu@users.noreply.github.com";
@ -288,6 +294,16 @@
github = "NitroSniper";
githubId = 44097331;
};
n-hass = {
name = "Nicholas Hassan";
email = "nick@hassan.host";
github = "n-hass";
githubId = 72363381;
keys = [{
longkeyid = "rsa4096/0xFC95AB946A781EE7";
fingerprint = "FDEE 6116 DBA7 8840 7323 4466 A371 5973 2728 A6A6";
}];
};
seylerius = {
email = "sable@seyleri.us";
name = "Sable Seyler";

View file

@ -1813,6 +1813,20 @@ in {
systems" section in the Home Manager mantual for more.
'';
}
{
time = "2024-11-01T19:44:59+00:00";
condition = hostPlatform.isLinux;
message = ''
A new module is available: 'services.podman'.
Podman is a daemonless container engine that lets you manage
containers, pods, and images.
This Home Manager module allows you to define containers that will run
as systemd services.
'';
}
];
};
}

View file

@ -352,6 +352,7 @@ let
./services/plan9port.nix
./services/playerctld.nix
./services/plex-mpv-shim.nix
./services/podman-linux
./services/polybar.nix
./services/poweralertd.nix
./services/psd.nix

View file

@ -0,0 +1,99 @@
{ config, podman-lib, ... }:
{
cleanup = ''
PATH=$PATH:${podman-lib.newuidmapPaths}
export VERBOSE=true
DRYRUN_ENABLED() {
return $([ -n "''${DRY_RUN:-}" ] && echo 0 || echo 1)
}
VERBOSE_ENABLED() {
return $([ -n "''${VERBOSE:-}" ] && echo 0 || echo 1)
}
cleanup() {
local resourceType=$1
local manifestFile="${config.xdg.configHome}/podman/$2"
local extraListCommands="''${3:-}"
[[ $resourceType = "container" ]] && extraListCommands+=" -a"
[ ! -f "$manifestFile" ] && VERBOSE_ENABLED && echo "Manifest does not exist: $manifestFile" && return 0
VERBOSE_ENABLED && echo "Cleaning up ''${resourceType}s not in manifest..." || true
loadManifest "$manifestFile"
formatString="{{.Name}}"
[[ $resourceType = "container" ]] && formatString="{{.Names}}"
local listOutput=$(${config.services.podman.package}/bin/podman $resourceType ls $extraListCommands --filter 'label=nix.home-manager.managed=true' --format "$formatString")
IFS=$'\n' read -r -d "" -a podmanResources <<< "$listOutput" || true
if [ ''${#podmanResources[@]} -eq 0 ]; then
VERBOSE_ENABLED && echo "No ''${resourceType}s available to process." || true
else
for resource in "''${podmanResources[@]}"; do
if ! isResourceInManifest "$resource"; then
removeResource "$resourceType" "$resource"
else
VERBOSE_ENABLED && echo "Keeping managed $resourceType: $resource" || true
fi
done
fi
}
isResourceInManifest() {
local resource="$1"
for manifestEntry in "''${resourceManifest[@]}"; do
if [ "$resource" = "$manifestEntry" ]; then
return 0 # Resource found in manifest
fi
done
return 1 # Resource not found in manifest
}
# Function to fill resourceManifest from the manifest file
loadManifest() {
local manifestFile="$1"
VERBOSE_ENABLED && echo "Loading manifest from $manifestFile..." || true
IFS=$'\n' read -r -d "" -a resourceManifest <<< "$(cat "$manifestFile")" || true
}
removeResource() {
local resourceType="$1"
local resource="$2"
echo "Removing orphaned $resourceType: $resource"
commands=()
case "$resourceType" in
"container")
commands+="${config.services.podman.package}/bin/podman $resourceType stop $resource"
commands+="${config.services.podman.package}/bin/podman $resourceType rm -f $resource"
;;
"network")
commands+="${config.services.podman.package}/bin/podman $resourceType rm $resource"
;;
esac
for command in "''${commands[@]}"; do
command=$(echo $command | tr -d ';&|`')
DRYRUN_ENABLED && echo "Would run: $command" && continue || true
VERBOSE_ENABLED && echo "Running: $command" || true
if [[ "$(eval "$command")" != "$resource" ]]; then
echo -e "\tCommand failed: ''${command}"
usedByContainers=$(${config.services.podman.package}/bin/podman container ls -a --filter "$resourceType=$resource" --format "{{.Names}}")
echo -e "\t$resource in use by containers: $usedByContainers"
fi
done
}
resourceManifest=()
[[ "$@" == *"--verbose"* ]] && VERBOSE="true"
[[ "$@" == *"--dry-run"* ]] && DRY_RUN="true"
for type in "container" "network"; do
cleanup "$type" "''${type}s.manifest"
done
'';
}

View file

@ -0,0 +1,313 @@
{ config, lib, pkgs, ... }:
with lib;
let
cfg = config.services.podman;
podman-lib = import ./podman-lib.nix { inherit lib config; };
createQuadletSource = name: containerDef:
let
mapHmNetworks = network:
if builtins.hasAttr network cfg.networks then
"podman-${network}-network.service"
else
null;
finalConfig = let
managedNetworks = if lib.isList containerDef.network then
map mapHmNetworks containerDef.network
else if containerDef.network != null then
map mapHmNetworks [ containerDef.network ]
else
[ ];
in (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 = containerDef.image;
IP = containerDef.ip4;
IP6 = containerDef.ip6;
Label =
(containerDef.labels // { "nix.home-manager.managed" = true; });
Network = containerDef.network;
NetworkAlias = containerDef.networkAlias;
PodmanArgs = containerDef.extraPodmanArgs;
PublishPort = containerDef.ports;
UserNS = containerDef.userNS;
User = containerDef.user;
Volume = containerDef.volumes;
};
Install = {
WantedBy = (if containerDef.autoStart then [
"default.target"
"multi-user.target"
] else
[ ]);
};
Service = {
Environment = {
PATH = (builtins.concatStringsSep ":" [
"/run/wrappers/bin"
"/run/current-system/sw/bin"
"${config.home.homeDirectory}/.nix-profile/bin"
]);
};
Restart = "always";
TimeoutStopSec = 30;
};
Unit = {
After = [ "network.target" ] ++ managedNetworks;
Requires = managedNetworks;
Description = (if (builtins.isString containerDef.description) then
containerDef.description
else
"Service for container ${name}");
};
} containerDef.extraConfig);
in ''
# Automatically generated by home-manager podman container configuration
# DO NOT EDIT THIS FILE DIRECTLY
#
# ${name}.container
${podman-lib.toQuadletIni finalConfig}
'';
toQuadletInternal = name: containerDef: {
assertions = podman-lib.buildConfigAsserts name containerDef.extraConfig;
resourceType = "container";
serviceName =
"podman-${name}"; # quadlet service name: 'podman-<name>.service'
source =
podman-lib.removeBlankLines (createQuadletSource name containerDef);
};
# 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
home.file."${config.xdg.configHome}/podman/containers.manifest".text =
podman-lib.generateManifestText containerQuadlets;
};
}

View file

@ -0,0 +1,17 @@
{ config, pkgs, lib, ... }:
{
meta.maintainers = with lib.hm.maintainers; [ bamhm182 n-hass ];
imports =
[ ./containers.nix ./install-quadlet.nix ./networks.nix ./services.nix ];
options.services.podman = {
enable = lib.mkEnableOption "Podman, a daemonless container engine";
};
config = lib.mkIf config.services.podman.enable {
assertions =
[ (lib.hm.assertions.assertPlatform "podman" pkgs lib.platforms.linux) ];
};
}

View file

@ -0,0 +1,88 @@
{ config, lib, pkgs, ... }:
with lib;
let
cfg = config.services.podman;
podman-lib = import ./podman-lib.nix { inherit lib config; };
activation = import ./activation.nix { inherit config podman-lib; };
activationCleanupScript = activation.cleanup;
# derivation to build a single Podman quadlet, outputting its systemd unit files
buildPodmanQuadlet = quadlet:
pkgs.stdenv.mkDerivation {
name = "home-${quadlet.resourceType}-${quadlet.serviceName}";
buildInputs = [ cfg.package ];
dontUnpack = true;
installPhase = ''
mkdir $out
# Directory for the quadlet file
mkdir -p $out/quadlets
# Directory for systemd unit files
mkdir -p $out/units
# Write the quadlet file
echo -n "${quadlet.source}" > $out/quadlets/${quadlet.serviceName}.${quadlet.resourceType}
# Generate systemd unit file/s from the quadlet file
export QUADLET_UNIT_DIRS=$out/quadlets
${cfg.package}/lib/systemd/user-generators/podman-user-generator $out/units
'';
passthru = {
outPath = self.out;
quadletData = quadlet;
};
};
# Create a derivation for each quadlet spec
builtQuadlets = map buildPodmanQuadlet cfg.internal.quadletDefinitions;
accumulateUnitFiles = prefix: path: quadlet:
let
entries = builtins.readDir path;
processEntry = name: type:
let
newPath = "${path}/${name}";
newPrefix = prefix + (if prefix == "" then "" else "/") + name;
in if type == "directory" then
accumulateUnitFiles newPrefix newPath quadlet
else [{
key = newPrefix;
value = {
path = newPath;
parentQuadlet = quadlet;
};
}];
in flatten
(map (name: processEntry name (getAttr name entries)) (attrNames entries));
allUnitFiles = concatMap (builtQuadlet:
accumulateUnitFiles "" "${builtQuadlet.outPath}/units"
builtQuadlet.quadletData) builtQuadlets;
# we're doing this because the home-manager recursive file linking implementation can't
# merge from multiple sources. so we link each file explicitly, which is fine for all unique files
generateSystemdFileLinks = files:
listToAttrs (map (unitFile: {
name = "${config.xdg.configHome}/systemd/user/${unitFile.key}";
value = { source = unitFile.value.path; };
}) files);
in {
imports = [ ./options.nix ];
config = mkIf cfg.enable {
home.file = generateSystemdFileLinks allUnitFiles;
# if the length of builtQuadlets is 0, then we don't need register the activation script
home.activation.podmanQuadletCleanup =
lib.mkIf (lib.length builtQuadlets >= 1)
(lib.hm.dag.entryAfter [ "reloadSystemd" ] activationCleanupScript);
};
}

View file

@ -0,0 +1,168 @@
{ config, lib, pkgs, ... }:
with lib;
let
cfg = config.services.podman;
podman-lib = import ./podman-lib.nix { inherit lib config; };
awaitPodmanUnshare = pkgs.writeShellScript "await-podman-unshare" ''
until ${cfg.package}/bin/podman unshare ${pkgs.coreutils}/bin/true; do
sleep 1;
done
'';
createQuadletSource = name: networkDef:
let
cfg = (podman-lib.deepMerge {
Install = {
WantedBy = (if networkDef.autoStart then [
"default.target"
"multi-user.target"
] else
[ ]);
};
Network = {
Driver = networkDef.driver;
Gateway = networkDef.gateway;
Internal = networkDef.internal;
NetworkName = name;
Label = networkDef.labels // { "nix.home-manager.managed" = true; };
PodmanArgs = networkDef.extraPodmanArgs;
Subnet = networkDef.subnet;
};
Service = {
Environment = {
PATH = (builtins.concatStringsSep ":" [
"${podman-lib.newuidmapPaths}"
"${makeBinPath [ pkgs.su pkgs.coreutils ]}"
]);
};
ExecStartPre = [ "${awaitPodmanUnshare}" ];
TimeoutStartSec = 15;
RemainAfterExit = "yes";
};
Unit = {
After = [ "network.target" ];
Description = (if (builtins.isString networkDef.description) then
networkDef.description
else
"Service for network ${name}");
};
} networkDef.extraConfig);
in ''
# Automatically generated by home-manager for podman network configuration
# DO NOT EDIT THIS FILE DIRECTLY
#
# ${name}.network
${podman-lib.toQuadletIni cfg}
'';
toQuadletInternal = name: networkDef: {
assertions = podman-lib.buildConfigAsserts name networkDef.extraConfig;
serviceName =
"podman-${name}"; # quadlet service name: 'podman-<name>-network.service'
source = podman-lib.removeBlankLines (createQuadletSource name networkDef);
resourceType = "network";
};
in let
networkDefinitionType = types.submodule {
options = {
autoStart = mkOption {
type = types.bool;
default = true;
description = ''
Whether to start the network on boot (requires user lingering).
'';
};
description = mkOption {
type = with types; nullOr str;
default = null;
example = "My Network";
description = "The description of the network.";
};
driver = mkOption {
type = with types; nullOr str;
default = null;
example = "bridge";
description = "The network driver to use.";
};
extraConfig = mkOption {
type = podman-lib.extraConfigType;
default = { };
example = literalExpression ''
{
Network = {
ContainerConfModule = "/etc/nvd.conf";
};
Service = {
TimeoutStartSec = 30;
};
}
'';
description = "INI sections and values to populate the Network Quadlet";
};
extraPodmanArgs = mkOption {
type = with types; listOf str;
default = [ ];
example = [ "--dns=192.168.55.1" "--ipam-driver" ];
description = ''
Extra arguments to pass to the podman network create command.
'';
};
gateway = mkOption {
type = with types; nullOr str;
default = null;
example = "192.168.20.1";
description = "The gateway IP to use for the network.";
};
internal = mkOption {
type = with types; nullOr bool;
default = null;
description = "Whether the network should be internal";
};
labels = mkOption {
type = with types; attrsOf str;
default = { };
example = {
app = "myapp";
some-label = "somelabel";
};
description = "The labels to apply to the network.";
};
subnet = mkOption {
type = with types; nullOr str;
default = null;
example = "192.168.20.0/24";
description = "The subnet to use for the network.";
};
};
};
in {
options.services.podman.networks = mkOption {
type = types.attrsOf networkDefinitionType;
default = { };
description = "Defines Podman network quadlet configurations.";
};
config = let networkQuadlets = mapAttrsToList toQuadletInternal cfg.networks;
in mkIf cfg.enable {
services.podman.internal.quadletDefinitions = networkQuadlets;
assertions = flatten (map (network: network.assertions) networkQuadlets);
home.file."${config.xdg.configHome}/podman/networks.manifest".text =
podman-lib.generateManifestText networkQuadlets;
};
}

View file

@ -0,0 +1,52 @@
{ lib, pkgs, ... }:
let
# Define the systemd service type
quadletInternalType = lib.types.submodule {
options = {
assertions = lib.mkOption {
type = with lib.types; listOf unspecified;
default = [ ];
internal = true;
description = "List of Nix type assertions.";
};
resourceType = lib.mkOption {
type = lib.types.str;
default = "";
internal = true;
description = "The type of the podman Quadlet resource.";
};
serviceName = lib.mkOption {
type = lib.types.str;
internal = true;
description = "The name of the systemd service.";
};
source = lib.mkOption {
type = lib.types.str;
internal = true;
description = "The quadlet source file content.";
};
};
};
in {
options.services.podman = {
internal.quadletDefinitions = lib.mkOption {
type = lib.types.listOf quadletInternalType;
default = { };
internal = true;
description = "List of quadlet source file content and service names.";
};
package = lib.mkOption {
type = lib.types.package;
default = pkgs.podman;
defaultText = lib.literalExpression "pkgs.podman";
description = "The podman package to use.";
};
enableTypeChecks = lib.mkEnableOption "type checks for podman quadlets";
};
}

View file

@ -0,0 +1,136 @@
{ lib, config, ... }:
with lib;
let
normalizeKeyValue = k: v:
let
v' = (if builtins.isBool v then
(if v then "true" else "false")
else if builtins.isAttrs v then
(concatStringsSep ''
${k}='' (mapAttrsToList normalizeKeyValue v))
else
builtins.toString v);
in if builtins.isNull v then "" else "${k}=${v'}";
primitiveAttrs = with types; attrsOf (either primitive (listOf primitive));
primitiveList = with types; listOf primitive;
primitive = with types; nullOr (oneOf [ bool int str path ]);
toQuadletIni = generators.toINI {
listsAsDuplicateKeys = true;
mkKeyValue = normalizeKeyValue;
};
# meant for ini. favours b when two values are unmergeable
deepMerge = a: b:
foldl' (result: key:
let
aVal = if builtins.hasAttr key a then a.${key} else null;
bVal = if builtins.hasAttr key b then b.${key} else null;
# check if the types inside a list match the type of a primitive
listMatchesType = (list: val:
isList list && builtins.length list > 0
&& builtins.typeOf (builtins.head list) == builtins.typeOf val);
in if isAttrs aVal && isAttrs bVal then
result // { ${key} = deepMerge aVal bVal; }
else if isList aVal && isList bVal then
result // { ${key} = aVal ++ bVal; }
else if aVal == bVal then
result // { ${key} = aVal; }
else if aVal == null then
result // { ${key} = bVal; }
else if bVal == null then
result // { ${key} = aVal; }
else if isList aVal && listMatchesType aVal bVal then
result // { ${key} = aVal ++ [ bVal ]; }
else if isList bVal && listMatchesType bVal aVal then
result // { ${key} = [ aVal ] ++ bVal; }
else if builtins.typeOf aVal == builtins.typeOf bVal then
result // { ${key} = bVal; }
else
result // { ${key} = bVal; }) a (builtins.attrNames b);
in {
inherit primitiveAttrs;
inherit primitiveList;
inherit primitive;
inherit toQuadletIni;
inherit deepMerge;
buildConfigAsserts = quadletName: extraConfig:
let
configRules = {
Container = { ContainerName = types.enum [ quadletName ]; };
Network = { NetworkName = types.enum [ quadletName ]; };
};
# Function to build assertions for a specific section and its attributes.
buildSectionAsserts = section: attrs:
if builtins.hasAttr section configRules then
flatten (mapAttrsToList (attrName: attrValue:
if builtins.hasAttr attrName configRules.${section} then [{
assertion = configRules.${section}.${attrName}.check attrValue;
message = "In '${quadletName}' config. ${section}.${attrName}: '${
toString attrValue
}' does not match expected type: ${
configRules.${section}.${attrName}.description
}";
}] else
[ ]) attrs)
else
[ ];
# Flatten assertions from all sections in `extraConfig`.
in flatten (mapAttrsToList buildSectionAsserts extraConfig);
extraConfigType = with types;
attrsOf (attrsOf (oneOf [ primitiveAttrs primitiveList primitive ]));
# input expects a list of quadletInternalType with all the same resourceType
generateManifestText = quadlets:
let
# create a list of all unique quadlet.resourceType in quadlets
quadletTypes = unique (map (quadlet: quadlet.resourceType) quadlets);
# if quadletTypes is > 1, then all quadlets are not the same type
allQuadletsSameType = length quadletTypes <= 1;
# ensures the service name is formatted correctly to be easily read
# by the activation script and matches `podman <resource> ls` output
formatServiceName = quadlet:
let
# remove the podman- prefix from the service name string
strippedName =
builtins.replaceStrings [ "podman-" ] [ "" ] quadlet.serviceName;
# specific logic for writing the unit name goes here. It should be
# identical to what `podman <resource> ls` shows
in {
"container" = strippedName;
"network" = strippedName;
}."${quadlet.resourceType}";
in if allQuadletsSameType then ''
${concatStringsSep "\n"
(map (quadlet: formatServiceName quadlet) quadlets)}
'' else
abort ''
All quadlets must be of the same type.
Quadlet types in this manifest: ${concatStringsSep ", " quadletTypes}
'';
# podman requires setuid on newuidmad, so it cannot be provided by pkgs.shadow
# Including all possible locations in PATH for newuidmap is a workaround.
# NixOS provides a 'wrapped' variant at /run/wrappers/bin/newuidmap.
# Other distros must install the 'uidmap' package, ie for ubuntu: apt install uidmap.
# Extra paths are added to handle where distro package managers may put the uidmap binaries.
#
# Tracking for a potential solution: https://github.com/NixOS/nixpkgs/issues/138423
newuidmapPaths = "/run/wrappers/bin:/usr/bin:/bin:/usr/sbin:/sbin";
removeBlankLines = text:
let
lines = splitString "\n" text;
nonEmptyLines = filter (line: line != "") lines;
in concatStringsSep "\n" nonEmptyLines;
}

View file

@ -0,0 +1,65 @@
{ config, lib, pkgs, ... }:
with lib;
let cfg = config.services.podman;
in {
options.services.podman = {
autoUpdate = {
enable = mkOption {
type = types.bool;
default = false;
description = "Automatically update the podman images.";
};
onCalendar = mkOption {
type = types.str;
default = "Sun *-*-* 00:00";
description = ''
The systemd `OnCalendar` expression for the update. See
{manpage}`systemd.time(7)` for a description of the format.
'';
};
};
};
config = mkIf cfg.enable (mkMerge [
(mkIf cfg.autoUpdate.enable {
systemd.user.services."podman-auto-update" = {
Unit = {
Description = "Podman auto-update service";
Documentation = "man:podman-auto-update(1)";
Wants = [ "network-online.target" ];
After = [ "network-online.target" ];
};
Service = {
Type = "oneshot";
Environment = "PATH=${
builtins.concatStringsSep ":" [
"/run/wrappers/bin"
"/run/current-system/sw/bin"
"${config.home.homeDirectory}/.nix-profile/bin"
]
}";
ExecStart = "${pkgs.podman}/bin/podman auto-update";
ExecStartPost = "${pkgs.podman}/bin/podman image prune -f";
TimeoutStartSec = "300s";
TimeoutStopSec = "10s";
};
};
systemd.user.timers."podman-auto-update" = {
Unit = { Description = "Podman auto-update timer"; };
Timer = {
OnCalendar = cfg.autoUpdate.onCalendar;
RandomizedDelaySec = 300;
Persistent = true;
};
Install = { WantedBy = [ "timers.target" ]; };
};
})
]);
}

View file

@ -269,6 +269,7 @@ in import nmtSrc {
./modules/services/pbgopy
./modules/services/picom
./modules/services/playerctld
./modules/services/podman-linux
./modules/services/polybar
./modules/services/recoll
./modules/services/redshift-gammastep

View file

@ -0,0 +1,51 @@
# Automatically generated by /nix/store/00000000000000000000000000000000-podman/lib/systemd/user-generators/podman-user-generator
#
# Automatically generated by home-manager podman container configuration
# DO NOT EDIT THIS FILE DIRECTLY
#
# my-container.container
[X-Container]
AddDevice=/dev/null:/dev/null
AutoUpdate=registry
ContainerName=my-container
Entrypoint=/sleep.sh
Environment=VAL_A=A
Environment=VAL_B=2
Environment=VAL_C=false
Image=docker.io/alpine:latest
Label=nix.home-manager.managed=true
Network=mynet
NetworkAlias=test-alias-1
NetworkAlias=test-alias-2
PodmanArgs=--security-opt=no-new-privileges
PublishPort=8080:80
ReadOnlyTmpfs=true
Volume=/tmp:/tmp
[Install]
WantedBy=default.target
WantedBy=multi-user.target
[Service]
Environment=PATH=/run/wrappers/bin:/run/current-system/sw/bin:/home/hm-user/.nix-profile/bin
Restart=on-failure
TimeoutStopSec=30
Environment=PODMAN_SYSTEMD_UNIT=%n
KillMode=mixed
ExecStop=/nix/store/00000000000000000000000000000000-podman/bin/podman rm -v -f -i --cidfile=%t/%N.cid
ExecStopPost=-/nix/store/00000000000000000000000000000000-podman/bin/podman rm -v -f -i --cidfile=%t/%N.cid
Delegate=yes
Type=notify
NotifyAccess=all
SyslogIdentifier=%N
ExecStart=/nix/store/00000000000000000000000000000000-podman/bin/podman run --name=my-container --cidfile=%t/%N.cid --replace --rm --cgroups=split --network=mynet --network-alias test-alias-1 --network-alias test-alias-2 --sdnotify=conmon -d --device=/dev/null:/dev/null --entrypoint=/sleep.sh --read-only-tmpfs -v /tmp:/tmp --label io.containers.autoupdate=registry --publish 8080:80 --env VAL_A=A --env VAL_B=2 --env VAL_C=false --label nix.home-manager.managed=true --security-opt=no-new-privileges docker.io/alpine:latest
[Unit]
Wants=network-online.target
After=network-online.target
After=network.target
Before=fake.target
Description=home-manager test
SourcePath=/nix/store/00000000000000000000000000000000-home-container-podman-my-container/quadlets/podman-my-container.container
RequiresMountsFor=%t/containers
RequiresMountsFor=/tmp

View file

@ -0,0 +1,60 @@
{ ... }:
{
services.podman = {
enable = true;
containers = {
"my-container" = {
description = "home-manager test";
autoStart = true;
autoUpdate = "registry";
devices = [ "/dev/null:/dev/null" ];
entrypoint = "/sleep.sh";
environment = {
"VAL_A" = "A";
"VAL_B" = 2;
"VAL_C" = false;
};
extraPodmanArgs = [ "--security-opt=no-new-privileges" ];
extraConfig = {
Container = {
ReadOnlyTmpfs = true;
NetworkAlias = "test-alias-2";
};
Service.Restart = "on-failure";
Unit.Before = "fake.target";
};
image = "docker.io/alpine:latest";
# Should not generate Requires/After for network because there is no
# services.podman.networks.mynet.
network = "mynet";
networkAlias = [ "test-alias-1" ];
ports = [ "8080:80" ];
volumes = [ "/tmp:/tmp" ];
};
"my-container-2" = {
image = "docker.io/alpine:latest";
extraConfig = {
Container.ContainerName = "some-other-container-name";
};
};
};
};
test.asserts.assertions.expected = [
''
In 'my-container-2' config. Container.ContainerName: 'some-other-container-name' does not match expected type: value "my-container-2" (singular enum)''
];
nmt.script = ''
configPath=home-files/.config/systemd/user
containerFile=$configPath/podman-my-container.service
assertFileExists $containerFile
containerFile=$(normalizeStorePaths $containerFile)
assertFileContent $containerFile ${./container-expected.service}
'';
}

View file

@ -0,0 +1,6 @@
{
podman-container = ./container.nix;
podman-integration = ./integration.nix;
podman-manifest = ./manifest.nix;
podman-network = ./network.nix;
}

View file

@ -0,0 +1,41 @@
# Automatically generated by /nix/store/00000000000000000000000000000000-podman/lib/systemd/user-generators/podman-user-generator
#
# Automatically generated by home-manager podman container configuration
# DO NOT EDIT THIS FILE DIRECTLY
#
# my-container.container
[X-Container]
ContainerName=my-container
Environment=
Image=docker.io/alpine:latest
Label=nix.home-manager.managed=true
Network=my-net
Network=externalnet
[Install]
WantedBy=default.target
WantedBy=multi-user.target
[Service]
Environment=PATH=/run/wrappers/bin:/run/current-system/sw/bin:/home/hm-user/.nix-profile/bin
Restart=always
TimeoutStopSec=30
Environment=PODMAN_SYSTEMD_UNIT=%n
KillMode=mixed
ExecStop=/nix/store/00000000000000000000000000000000-podman/bin/podman rm -v -f -i --cidfile=%t/%N.cid
ExecStopPost=-/nix/store/00000000000000000000000000000000-podman/bin/podman rm -v -f -i --cidfile=%t/%N.cid
Delegate=yes
Type=notify
NotifyAccess=all
SyslogIdentifier=%N
ExecStart=/nix/store/00000000000000000000000000000000-podman/bin/podman run --name=my-container --cidfile=%t/%N.cid --replace --rm --cgroups=split --network=my-net --network=externalnet --sdnotify=conmon -d --label nix.home-manager.managed=true docker.io/alpine:latest
[Unit]
Wants=network-online.target
After=network-online.target
After=network.target
After=podman-my-net-network.service
Description=Service for container my-container
Requires=podman-my-net-network.service
SourcePath=/nix/store/00000000000000000000000000000000-home-container-podman-my-container/quadlets/podman-my-container.container
RequiresMountsFor=%t/containers

View file

@ -0,0 +1,29 @@
# Automatically generated by /nix/store/00000000000000000000000000000000-podman/lib/systemd/user-generators/podman-user-generator
#
# Automatically generated by home-manager for podman network configuration
# DO NOT EDIT THIS FILE DIRECTLY
#
# my-net.network
[Install]
WantedBy=default.target
WantedBy=multi-user.target
[X-Network]
Gateway=192.168.123.1
Label=nix.home-manager.managed=true
NetworkName=my-net
Subnet=192.168.123.0/24
[Service]
Environment=PATH=/run/wrappers/bin:/usr/bin:/bin:/usr/sbin:/sbin:/nix/store/00000000000000000000000000000000-shadow/bin:/nix/store/00000000000000000000000000000000-coreutils/bin
ExecStartPre=/nix/store/00000000000000000000000000000000-await-podman-unshare
RemainAfterExit=yes
TimeoutStartSec=15
ExecStart=/nix/store/00000000000000000000000000000000-podman/bin/podman network create --ignore --subnet=192.168.123.0/24 --gateway=192.168.123.1 --label nix.home-manager.managed=true my-net
Type=oneshot
SyslogIdentifier=%N
[Unit]
After=network.target
Description=Service for network my-net
RequiresMountsFor=%t/containers

View file

@ -0,0 +1,29 @@
{ ... }:
{
services.podman = {
enable = true;
containers."my-container" = {
image = "docker.io/alpine:latest";
network = [ "my-net" "externalnet" ];
};
networks."my-net" = {
gateway = "192.168.123.1";
subnet = "192.168.123.0/24";
};
};
nmt.script = ''
configPath=home-files/.config/systemd/user
containerFile=$configPath/podman-my-container.service
networkFile=$configPath/podman-my-net-network.service
assertFileExists $containerFile
assertFileExists $networkFile
containerFile=$(normalizeStorePaths $containerFile)
networkFile=$(normalizeStorePaths $networkFile)
assertFileContent $containerFile ${./integration-container-expected.service}
assertFileContent $networkFile ${./integration-network-expected.service}
'';
}

View file

@ -0,0 +1,64 @@
{ ... }:
{
services.podman = {
enable = true;
containers."my-container-1" = {
description = "home-manager test";
autoUpdate = "registry";
autoStart = true;
image = "docker.io/alpine:latest";
entrypoint = "sleep 1000";
environment = {
"VAL_A" = "A";
"VAL_B" = 2;
"VAL_C" = false;
};
};
};
services.podman.containers."my-container-2" = {
description = "home-manager test";
autoUpdate = "registry";
autoStart = true;
image = "docker.io/alpine:latest";
entrypoint = "sleep 1000";
environment = {
"VAL_A" = "B";
"VAL_B" = 3;
"VAL_C" = true;
};
};
services.podman.networks."mynet-1" = {
subnet = "192.168.1.0/24";
gateway = "192.168.1.1";
};
services.podman.networks."mynet-2" = {
subnet = "192.168.2.0/24";
gateway = "192.168.2.1";
};
nmt.script = ''
configPath=home-files/.config/podman
containerManifest=$configPath/containers.manifest
networkManifest=$configPath/networks.manifest
assertFileExists $containerManifest
assertFileExists $networkManifest
assertFileContent $containerManifest ${
builtins.toFile "containers.expected" ''
my-container-1
my-container-2
''
}
assertFileContent $networkManifest ${
builtins.toFile "networks.expected" ''
mynet-1
mynet-2
''
}
'';
}

View file

@ -0,0 +1,33 @@
# Automatically generated by /nix/store/00000000000000000000000000000000-podman/lib/systemd/user-generators/podman-user-generator
#
# Automatically generated by home-manager for podman network configuration
# DO NOT EDIT THIS FILE DIRECTLY
#
# my-net.network
[Install]
WantedBy=default.target
WantedBy=multi-user.target
[X-Network]
Gateway=192.168.1.1
Label=nix.home-manager.managed=true
NetworkName=my-net
Options=isolate=true
PodmanArgs=--ipam-driver dhcp
PodmanArgs=--dns=192.168.55.1
PodmanArgs=--log-level=debug
Subnet=192.168.1.0/24
[Service]
Environment=PATH=/run/wrappers/bin:/usr/bin:/bin:/usr/sbin:/sbin:/nix/store/00000000000000000000000000000000-shadow/bin:/nix/store/00000000000000000000000000000000-coreutils/bin
ExecStartPre=/nix/store/00000000000000000000000000000000-await-podman-unshare
RemainAfterExit=yes
TimeoutStartSec=15
ExecStart=/nix/store/00000000000000000000000000000000-podman/bin/podman network create --ignore --subnet=192.168.1.0/24 --gateway=192.168.1.1 --opt isolate=true --label nix.home-manager.managed=true --ipam-driver dhcp --dns=192.168.55.1 --log-level=debug my-net
Type=oneshot
SyslogIdentifier=%N
[Unit]
After=network.target
Description=Service for network my-net
RequiresMountsFor=%t/containers

View file

@ -0,0 +1,44 @@
{ ... }:
{
services.podman = {
enable = true;
networks = {
"my-net" = {
subnet = "192.168.1.0/24";
gateway = "192.168.1.1";
extraPodmanArgs = [ "--ipam-driver dhcp" ];
extraConfig = {
Network = {
NetworkName = "my-net";
Options = { isolate = "true"; };
PodmanArgs = [ "--dns=192.168.55.1" "--log-level=debug" ];
};
};
};
"my-net-2" = {
subnet = "192.168.2.0/24";
gateway = "192.168.2.1";
extraConfig = {
Network = { NetworkName = "some-other-network-name"; };
};
};
};
};
test.asserts.assertions.expected = [
''
In 'my-net-2' config. Network.NetworkName: 'some-other-network-name' does not match expected type: value "my-net-2" (singular enum)''
];
nmt.script = ''
configPath=home-files/.config/systemd/user
networkFile=$configPath/podman-my-net-network.service
assertFileExists $networkFile
networkFile=$(normalizeStorePaths $networkFile)
assertFileContent $networkFile ${./network-expected.service}
'';
}