podman: add new module 'podman'
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:
parent
44677a1c96
commit
14bbb67c08
|
@ -243,6 +243,16 @@
|
||||||
github = "nilp0inter";
|
github = "nilp0inter";
|
||||||
githubId = 1224006;
|
githubId = 1224006;
|
||||||
};
|
};
|
||||||
|
n-hass = {
|
||||||
|
name = "Nicholas Hassan";
|
||||||
|
email = "nick@hassan.host";
|
||||||
|
github = "n-hass";
|
||||||
|
githubId = 72363381;
|
||||||
|
keys = [{
|
||||||
|
longkeyid = "ed25519/0xA37159732728A6A6";
|
||||||
|
fingerprint = "FDEE 6116 DBA7 8840 7323 4466 A371 5973 2728 A6A6";
|
||||||
|
}];
|
||||||
|
};
|
||||||
seylerius = {
|
seylerius = {
|
||||||
email = "sable@seyleri.us";
|
email = "sable@seyleri.us";
|
||||||
name = "Sable Seyler";
|
name = "Sable Seyler";
|
||||||
|
|
|
@ -343,6 +343,7 @@ let
|
||||||
./services/plan9port.nix
|
./services/plan9port.nix
|
||||||
./services/playerctld.nix
|
./services/playerctld.nix
|
||||||
./services/plex-mpv-shim.nix
|
./services/plex-mpv-shim.nix
|
||||||
|
./services/podman-linux/default.nix
|
||||||
./services/polybar.nix
|
./services/polybar.nix
|
||||||
./services/poweralertd.nix
|
./services/poweralertd.nix
|
||||||
./services/psd.nix
|
./services/psd.nix
|
||||||
|
|
343
modules/services/podman-linux/containers.nix
Normal file
343
modules/services/podman-linux/containers.nix
Normal file
|
@ -0,0 +1,343 @@
|
||||||
|
{ config, lib, ... }:
|
||||||
|
|
||||||
|
with lib;
|
||||||
|
|
||||||
|
let
|
||||||
|
podman-lib = import ./podman-lib.nix { inherit lib; };
|
||||||
|
|
||||||
|
createQuadletSource = name: containerDef:
|
||||||
|
let
|
||||||
|
### Definitions
|
||||||
|
serviceName = if containerDef.serviceName != null then containerDef.serviceName else name;
|
||||||
|
containerName = name; # Use the submodule name as the container name
|
||||||
|
mergedServiceConfig = podman-lib.serviceConfigDefaults // containerDef.serviceConfig;
|
||||||
|
mergedUnitConfig = podman-lib.unitConfigDefaults // containerDef.unitConfig;
|
||||||
|
###
|
||||||
|
|
||||||
|
### Helpers
|
||||||
|
ifNotNull = condition: text: if condition != null then text else "";
|
||||||
|
ifNotEmptyList = list: text: if list != [] then text else "";
|
||||||
|
ifNotEmptySet = set: text: if set != {} then text else "";
|
||||||
|
###
|
||||||
|
|
||||||
|
### Formatters
|
||||||
|
formatExtraConfig = podman-lib.formatExtraConfig;
|
||||||
|
formatPrimitiveValue = podman-lib.formatPrimitiveValue;
|
||||||
|
|
||||||
|
formatNetworkDependencies = networks:
|
||||||
|
let
|
||||||
|
formatElement = network: "podman-${network}-network.service";
|
||||||
|
in
|
||||||
|
concatStringsSep " " (map formatElement networks);
|
||||||
|
|
||||||
|
formatEnvironment = env:
|
||||||
|
if env != {} then
|
||||||
|
concatStringsSep " " (mapAttrsToList (k: v: "${k}=${formatPrimitiveValue v}") env)
|
||||||
|
else
|
||||||
|
"";
|
||||||
|
|
||||||
|
formatPorts = ports:
|
||||||
|
if ports != [] then
|
||||||
|
concatStringsSep "\n" (map (port: "PublishPort=${port}") ports)
|
||||||
|
else
|
||||||
|
"";
|
||||||
|
|
||||||
|
formatVolumes = volumes:
|
||||||
|
if volumes != [] then
|
||||||
|
concatStringsSep "\n" (map (volume: "Volume=${volume}") volumes)
|
||||||
|
else
|
||||||
|
"";
|
||||||
|
|
||||||
|
formatDevices = devices:
|
||||||
|
if devices != [] then
|
||||||
|
concatStringsSep "\n" (map (device: "AddDevice=${device}") devices)
|
||||||
|
else
|
||||||
|
"";
|
||||||
|
|
||||||
|
formatCapabilities = action: capabilities:
|
||||||
|
if capabilities != [] then
|
||||||
|
concatStringsSep "\n" (map (capability: "${action}Capability=${capability}") capabilities)
|
||||||
|
else
|
||||||
|
"";
|
||||||
|
|
||||||
|
formatLabels = labels:
|
||||||
|
if labels != [] then
|
||||||
|
concatStringsSep "\n" (map (label: "Label=${label}") labels)
|
||||||
|
else
|
||||||
|
"";
|
||||||
|
|
||||||
|
formatAutoUpdate = autoupdate:
|
||||||
|
if autoupdate == "registry" then
|
||||||
|
"AutoUpdate=registry"
|
||||||
|
else if autoupdate == "local" then
|
||||||
|
"AutoUpdate=local"
|
||||||
|
else
|
||||||
|
"";
|
||||||
|
|
||||||
|
# TODO: check that the user hasn't supplied both networkMode and networks
|
||||||
|
formatNetwork = containerDef:
|
||||||
|
if containerDef.networkMode != null then
|
||||||
|
"Network=${containerDef.networkMode}"
|
||||||
|
else if containerDef.networks != [] then
|
||||||
|
"Network=${concatStringsSep "," containerDef.networks}"
|
||||||
|
else
|
||||||
|
"";
|
||||||
|
|
||||||
|
formatPodmanArgs = containerDef:
|
||||||
|
let
|
||||||
|
networkAliasArg = if containerDef.networkAlias != null then "--network-alias ${containerDef.networkAlias}" else null;
|
||||||
|
entrypointArg = if containerDef.entrypoint != null then "--entrypoint ${containerDef.entrypoint}" else null;
|
||||||
|
allArgs = [networkAliasArg entrypointArg] ++ containerDef.extraOptions;
|
||||||
|
in
|
||||||
|
if allArgs != [] && allArgs != [""] then
|
||||||
|
"PodmanArgs=${concatStringsSep " " (filter (arg: arg != null && arg != "") allArgs)}"
|
||||||
|
else
|
||||||
|
"";
|
||||||
|
###
|
||||||
|
|
||||||
|
configText = ''
|
||||||
|
# Automatically generated by home-manager podman containers module
|
||||||
|
# DO NOT EDIT THIS FILE DIRECTLY
|
||||||
|
#
|
||||||
|
# ${serviceName}.container
|
||||||
|
[Unit]
|
||||||
|
Description=${if containerDef.description != null then containerDef.description else "Service for container ${containerName}"}
|
||||||
|
After=network.target
|
||||||
|
${ifNotEmptyList containerDef.networks "After=${formatNetworkDependencies containerDef.networks}"}
|
||||||
|
${formatExtraConfig mergedUnitConfig}
|
||||||
|
|
||||||
|
[Container]
|
||||||
|
ContainerName=${containerName}
|
||||||
|
Image=${containerDef.image}
|
||||||
|
Label=nix.home-manager.managed=true
|
||||||
|
${ifNotEmptySet containerDef.environment "Environment=${formatEnvironment containerDef.environment}"}
|
||||||
|
${ifNotNull containerDef.environmentFile "EnvironmentFile=${containerDef.environmentFile}"}
|
||||||
|
${ifNotNull containerDef.command "Exec=${containerDef.command}"}
|
||||||
|
${ifNotNull containerDef.user "User=${formatPrimitiveValue containerDef.user}"}
|
||||||
|
${ifNotNull containerDef.userNS "UserNS=${containerDef.userNS}"}
|
||||||
|
${ifNotNull containerDef.group "Group=${formatPrimitiveValue containerDef.group}"}
|
||||||
|
${ifNotEmptyList containerDef.ports (formatPorts containerDef.ports)}
|
||||||
|
${ifNotNull containerDef.networkMode "Network=${containerDef.networkMode}"}
|
||||||
|
${formatNetwork containerDef}
|
||||||
|
${ifNotNull containerDef.ip4 "IP=${containerDef.ip4}"}
|
||||||
|
${ifNotNull containerDef.ip6 "IP6=${containerDef.ip6}"}
|
||||||
|
${ifNotEmptyList containerDef.volumes (formatVolumes containerDef.volumes)}
|
||||||
|
${ifNotEmptyList containerDef.devices (formatDevices containerDef.devices)}
|
||||||
|
${formatAutoUpdate containerDef.autoupdate}
|
||||||
|
${ifNotEmptyList containerDef.addCapabilities (formatCapabilities "Add" containerDef.addCapabilities)}
|
||||||
|
${ifNotEmptyList containerDef.dropCapabilities (formatCapabilities "Drop" containerDef.dropCapabilities)}
|
||||||
|
${ifNotEmptyList containerDef.labels (formatLabels containerDef.labels)}
|
||||||
|
${formatPodmanArgs containerDef}
|
||||||
|
${formatExtraConfig containerDef.extraContainerConfig}
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Environment="PATH=/run/wrappers/bin:/run/current-system/sw/bin:${config.home.homeDirectory}/.nix-profile/bin"
|
||||||
|
${formatExtraConfig mergedServiceConfig}
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
${if containerDef.autostart then "WantedBy=multi-user.target default.target" else ""}
|
||||||
|
'';
|
||||||
|
|
||||||
|
removeBlankLines = text:
|
||||||
|
let
|
||||||
|
lines = splitString "\n" text;
|
||||||
|
nonEmptyLines = filter (line: line != "") lines;
|
||||||
|
in
|
||||||
|
concatStringsSep "\n" nonEmptyLines;
|
||||||
|
|
||||||
|
in
|
||||||
|
removeBlankLines configText;
|
||||||
|
|
||||||
|
toQuadletInternal = name: containerDef:
|
||||||
|
let
|
||||||
|
allAssertions = (podman-lib.assertConfigTypes podman-lib.serviceConfigTypeRules containerDef.serviceConfig name) ++
|
||||||
|
(podman-lib.assertConfigTypes podman-lib.unitConfigTypeRules containerDef.unitConfig name);
|
||||||
|
in
|
||||||
|
{
|
||||||
|
serviceName = if containerDef.serviceName != null then containerDef.serviceName else "podman-${name}";
|
||||||
|
source = createQuadletSource name containerDef;
|
||||||
|
unitType = "container";
|
||||||
|
assertions = allAssertions;
|
||||||
|
};
|
||||||
|
in
|
||||||
|
|
||||||
|
let
|
||||||
|
# Define the container user type as the user interface
|
||||||
|
containerDefinitionType = types.submodule {
|
||||||
|
options = {
|
||||||
|
serviceName = mkOption {
|
||||||
|
type = with types; nullOr str;
|
||||||
|
description = "The name of the systemd service to generate for the container.";
|
||||||
|
default = null;
|
||||||
|
};
|
||||||
|
|
||||||
|
description = mkOption {
|
||||||
|
type = with types; nullOr str;
|
||||||
|
description = "The description of the container.";
|
||||||
|
default = null;
|
||||||
|
};
|
||||||
|
|
||||||
|
image = mkOption {
|
||||||
|
type = types.str;
|
||||||
|
description = "The container image.";
|
||||||
|
};
|
||||||
|
|
||||||
|
entrypoint = mkOption {
|
||||||
|
type = with types; nullOr str;
|
||||||
|
description = "The container entrypoint.";
|
||||||
|
default = null;
|
||||||
|
};
|
||||||
|
|
||||||
|
command = mkOption {
|
||||||
|
type = with types; nullOr str;
|
||||||
|
description = "The command to run after the container specification.";
|
||||||
|
default = null;
|
||||||
|
};
|
||||||
|
|
||||||
|
environment = mkOption {
|
||||||
|
type = podman-lib.primitiveAttrs;
|
||||||
|
default = {};
|
||||||
|
};
|
||||||
|
|
||||||
|
environmentFile = mkOption {
|
||||||
|
type = with types; nullOr str;
|
||||||
|
default = null;
|
||||||
|
};
|
||||||
|
|
||||||
|
ports = mkOption {
|
||||||
|
type = with types; listOf str;
|
||||||
|
default = [];
|
||||||
|
};
|
||||||
|
|
||||||
|
user = mkOption {
|
||||||
|
type = with types; nullOr (oneOf [ str int ]);
|
||||||
|
default = null;
|
||||||
|
};
|
||||||
|
|
||||||
|
userNS = mkOption {
|
||||||
|
type = with types; nullOr str;
|
||||||
|
default = null;
|
||||||
|
};
|
||||||
|
|
||||||
|
group = mkOption {
|
||||||
|
type = with types; nullOr (oneOf [ str int ]);
|
||||||
|
default = null;
|
||||||
|
};
|
||||||
|
|
||||||
|
networkMode = mkOption {
|
||||||
|
type = with types; nullOr str;
|
||||||
|
default = null;
|
||||||
|
};
|
||||||
|
|
||||||
|
networks = mkOption {
|
||||||
|
type = with types; listOf str;
|
||||||
|
default = [];
|
||||||
|
};
|
||||||
|
|
||||||
|
ip4 = mkOption {
|
||||||
|
type = with types; nullOr str;
|
||||||
|
default = null;
|
||||||
|
};
|
||||||
|
|
||||||
|
ip6 = mkOption {
|
||||||
|
type = with types; nullOr str;
|
||||||
|
default = null;
|
||||||
|
};
|
||||||
|
|
||||||
|
networkAlias = mkOption {
|
||||||
|
type = with types; nullOr str;
|
||||||
|
default = null;
|
||||||
|
};
|
||||||
|
|
||||||
|
volumes = mkOption {
|
||||||
|
type = with types; listOf str;
|
||||||
|
default = [];
|
||||||
|
};
|
||||||
|
|
||||||
|
devices = mkOption {
|
||||||
|
type = types.listOf types.str;
|
||||||
|
default = [];
|
||||||
|
description = "The devices to mount into the container, in the format '/dev/<host>:/dev/<container>'.";
|
||||||
|
};
|
||||||
|
|
||||||
|
autoupdate = mkOption {
|
||||||
|
type = with types; enum [
|
||||||
|
""
|
||||||
|
"registry"
|
||||||
|
"local"
|
||||||
|
];
|
||||||
|
default = "";
|
||||||
|
};
|
||||||
|
|
||||||
|
autostart = mkOption {
|
||||||
|
type = types.bool;
|
||||||
|
default = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
addCapabilities = mkOption {
|
||||||
|
type = with types; listOf str;
|
||||||
|
default = [];
|
||||||
|
};
|
||||||
|
|
||||||
|
dropCapabilities = mkOption {
|
||||||
|
type = with types; listOf str;
|
||||||
|
default = [];
|
||||||
|
};
|
||||||
|
|
||||||
|
labels = mkOption {
|
||||||
|
type = with types; listOf str;
|
||||||
|
default = [];
|
||||||
|
};
|
||||||
|
|
||||||
|
extraOptions = mkOption {
|
||||||
|
type = with types; listOf str;
|
||||||
|
default = [];
|
||||||
|
};
|
||||||
|
|
||||||
|
extraContainerConfig = mkOption {
|
||||||
|
type = podman-lib.primitiveAttrs;
|
||||||
|
default = {};
|
||||||
|
example = literalExample ''
|
||||||
|
extraContainerConfig = {
|
||||||
|
UIDMap = "0:1000:1";
|
||||||
|
ReadOnlyTmpfs = true;
|
||||||
|
EnvironmentFile = [ /etc/environment /root/.env];
|
||||||
|
};
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
serviceConfig = mkOption {
|
||||||
|
type = podman-lib.serviceConfigType;
|
||||||
|
default = {};
|
||||||
|
};
|
||||||
|
|
||||||
|
unitConfig = mkOption {
|
||||||
|
type = podman-lib.unitConfigType;
|
||||||
|
default = {};
|
||||||
|
};
|
||||||
|
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
in {
|
||||||
|
|
||||||
|
imports = [
|
||||||
|
./options.nix
|
||||||
|
];
|
||||||
|
|
||||||
|
options.services.podman.containers = mkOption {
|
||||||
|
type = types.attrsOf containerDefinitionType;
|
||||||
|
default = {};
|
||||||
|
description = "Attribute set of container definitions.";
|
||||||
|
};
|
||||||
|
|
||||||
|
config = let
|
||||||
|
containerQuadlets = mapAttrsToList toQuadletInternal config.services.podman.containers;
|
||||||
|
in {
|
||||||
|
internal.podman-quadlet-definitions = containerQuadlets;
|
||||||
|
assertions = lib.flatten (map (container: container.assertions) config.internal.podman-quadlet-definitions);
|
||||||
|
|
||||||
|
# manifest file
|
||||||
|
home.file."${config.xdg.configHome}/podman/containers.manifest".text = podman-lib.generateManifestText containerQuadlets;
|
||||||
|
};
|
||||||
|
}
|
11
modules/services/podman-linux/default.nix
Normal file
11
modules/services/podman-linux/default.nix
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
{ pkgs, lib, ... }:
|
||||||
|
|
||||||
|
{
|
||||||
|
imports =
|
||||||
|
[ ./services.nix ./networks.nix ./containers.nix ./install-quadlet.nix ];
|
||||||
|
|
||||||
|
config = {
|
||||||
|
assertions =
|
||||||
|
[ (lib.hm.assertions.assertPlatform "podman" pkgs lib.platforms.linux) ];
|
||||||
|
};
|
||||||
|
}
|
169
modules/services/podman-linux/install-quadlet.nix
Normal file
169
modules/services/podman-linux/install-quadlet.nix
Normal file
|
@ -0,0 +1,169 @@
|
||||||
|
{ config, lib, pkgs, ... }:
|
||||||
|
|
||||||
|
with lib;
|
||||||
|
|
||||||
|
let
|
||||||
|
quadletActivationCleanupScript = ''
|
||||||
|
resourceManifest=()
|
||||||
|
# Define VERBOSE_ENABLED as a function
|
||||||
|
VERBOSE_ENABLED() {
|
||||||
|
if [[ -n "''${VERBOSE:-}" ]]; then
|
||||||
|
return 0
|
||||||
|
else
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Function to fill resourceManifest from the manifest file
|
||||||
|
function loadManifest {
|
||||||
|
local manifestFile="$1"
|
||||||
|
VERBOSE_ENABLED && echo "Loading manifest from $manifestFile..."
|
||||||
|
IFS=$'\n' read -r -d "" -a resourceManifest <<< "$(cat "$manifestFile")" || true
|
||||||
|
}
|
||||||
|
|
||||||
|
function 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 removeContainer {
|
||||||
|
echo "Removing orphaned container: $1"
|
||||||
|
if [[ -n "''${DRY_RUN:-}" ]]; then
|
||||||
|
echo "Would run podman stop $1"
|
||||||
|
echo "Would run podman $resourceType rm -f $1"
|
||||||
|
else
|
||||||
|
${config.services.podman.package}/bin/podman stop "$1"
|
||||||
|
${config.services.podman.package}/bin/podman $resourceType rm -f "$1"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeNetwork {
|
||||||
|
echo "Removing orphaned network: $1"
|
||||||
|
if [[ -n "''${DRY_RUN:-}" ]]; then
|
||||||
|
echo "Would run podman network rm $1"
|
||||||
|
else
|
||||||
|
if ! ${config.services.podman.package}/bin/podman network rm "$1"; then
|
||||||
|
echo "Failed to remove network $1. Is it still in use by a container?"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
function cleanup {
|
||||||
|
local resourceType=$1
|
||||||
|
local manifestFile="${config.xdg.configHome}/podman/$2"
|
||||||
|
local extraListCommands="''${3:-}"
|
||||||
|
[[ $resourceType = "container" ]] && extraListCommands+=" -a"
|
||||||
|
|
||||||
|
VERBOSE_ENABLED && echo "Cleaning up ''${resourceType}s not in manifest..."
|
||||||
|
|
||||||
|
loadManifest "$manifestFile"
|
||||||
|
|
||||||
|
formatString="{{.Name}}"
|
||||||
|
[[ $resourceType = "container" ]] && formatString="{{.Names}}"
|
||||||
|
|
||||||
|
# Capture the output of the podman command to a variable
|
||||||
|
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
|
||||||
|
|
||||||
|
# Check if the array is populated and iterate over it
|
||||||
|
if [ ''${#resourceManifest[@]} -eq 0 ]; then
|
||||||
|
VERBOSE_ENABLED && echo "No ''${resourceType}s available to process."
|
||||||
|
else
|
||||||
|
for resource in "''${podmanResources[@]}"; do
|
||||||
|
if ! isResourceInManifest "$resource"; then
|
||||||
|
|
||||||
|
[[ $resourceType = "container" ]] && removeContainer "$resource"
|
||||||
|
[[ $resourceType = "network" ]] && removeNetwork "$resource"
|
||||||
|
|
||||||
|
else
|
||||||
|
if VERBOSE_ENABLED; then
|
||||||
|
echo "Keeping managed $resourceType: $resource"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Cleanup containers
|
||||||
|
cleanup "container" "containers.manifest"
|
||||||
|
|
||||||
|
# Cleanup networks
|
||||||
|
cleanup "network" "networks.manifest"
|
||||||
|
'';
|
||||||
|
|
||||||
|
# derivation to build a single Podman quadlet, outputting its systemd unit files
|
||||||
|
buildPodmanQuadlet = quadlet: pkgs.stdenv.mkDerivation {
|
||||||
|
name = "home-${quadlet.unitType}-${quadlet.serviceName}";
|
||||||
|
|
||||||
|
buildInputs = [ config.services.podman.package ];
|
||||||
|
|
||||||
|
dontUnpack = true;
|
||||||
|
|
||||||
|
buildPhase = ''
|
||||||
|
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.unitType}
|
||||||
|
|
||||||
|
# Generate systemd unit file/s from the quadlet file
|
||||||
|
export QUADLET_UNIT_DIRS=$out/quadlets
|
||||||
|
${config.services.podman.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 config.internal.podman-quadlet-definitions;
|
||||||
|
|
||||||
|
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 = {
|
||||||
|
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"] quadletActivationCleanupScript);
|
||||||
|
};
|
||||||
|
}
|
57
modules/services/podman-linux/networks.nix
Normal file
57
modules/services/podman-linux/networks.nix
Normal file
|
@ -0,0 +1,57 @@
|
||||||
|
{ config, lib, pkgs, ... }:
|
||||||
|
|
||||||
|
with lib;
|
||||||
|
|
||||||
|
let
|
||||||
|
podman-lib = import ./podman-lib.nix { inherit lib; };
|
||||||
|
|
||||||
|
createQuadletSource = name: networkDef:
|
||||||
|
''
|
||||||
|
# Automatically generated by home-manager for podman network configuration
|
||||||
|
# DO NOT EDIT THIS FILE DIRECTLY
|
||||||
|
[Network]
|
||||||
|
Label=nix.home-manager.managed=true
|
||||||
|
${podman-lib.formatExtraConfig networkDef}
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target default.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Environment="PATH=${pkgs.shadow}"
|
||||||
|
'';
|
||||||
|
|
||||||
|
toQuadletInternal = name: networkDef:
|
||||||
|
{
|
||||||
|
serviceName = "podman-${name}"; # becomes podman-<netname>-network.service because of quadlet
|
||||||
|
source = createQuadletSource name networkDef;
|
||||||
|
unitType = "network";
|
||||||
|
};
|
||||||
|
|
||||||
|
in
|
||||||
|
{
|
||||||
|
options = {
|
||||||
|
services.podman.networks = mkOption {
|
||||||
|
type = types.attrsOf (podman-lib.primitiveAttrs);
|
||||||
|
default = {};
|
||||||
|
example = literalExample ''
|
||||||
|
{
|
||||||
|
mynetwork = {
|
||||||
|
Subnet = "192.168.1.0/24";
|
||||||
|
Gateway = "192.168.1.1";
|
||||||
|
NetworkName = "mynetwork";
|
||||||
|
};
|
||||||
|
}
|
||||||
|
'';
|
||||||
|
description = "Defines Podman network quadlet configurations.";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
config = let
|
||||||
|
networkQuadlets = mapAttrsToList toQuadletInternal config.services.podman.networks;
|
||||||
|
in {
|
||||||
|
internal.podman-quadlet-definitions = networkQuadlets;
|
||||||
|
|
||||||
|
# manifest file
|
||||||
|
home.file."${config.xdg.configHome}/podman/networks.manifest".text = podman-lib.generateManifestText networkQuadlets;
|
||||||
|
};
|
||||||
|
}
|
42
modules/services/podman-linux/options.nix
Normal file
42
modules/services/podman-linux/options.nix
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
{lib, pkgs, ...}:
|
||||||
|
|
||||||
|
let
|
||||||
|
# Define the systemd service type
|
||||||
|
quadletInternalType = lib.types.submodule {
|
||||||
|
options = {
|
||||||
|
serviceName = lib.mkOption {
|
||||||
|
type = lib.types.str;
|
||||||
|
description = "The name of the systemd service.";
|
||||||
|
};
|
||||||
|
|
||||||
|
unitType = lib.mkOption {
|
||||||
|
type = lib.types.str;
|
||||||
|
default = "";
|
||||||
|
description = "The type of the systemd unit.";
|
||||||
|
};
|
||||||
|
|
||||||
|
source = lib.mkOption {
|
||||||
|
type = lib.types.str;
|
||||||
|
description = "The quadlet source file content.";
|
||||||
|
};
|
||||||
|
|
||||||
|
assertions = lib.mkOption {
|
||||||
|
type = with lib.types; listOf unspecified;
|
||||||
|
default = [];
|
||||||
|
description = "List of Nix type assertions.";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
in {
|
||||||
|
options.internal.podman-quadlet-definitions = lib.mkOption {
|
||||||
|
type = lib.types.listOf quadletInternalType;
|
||||||
|
default = {};
|
||||||
|
description = "List of quadlet source file content and service names.";
|
||||||
|
};
|
||||||
|
|
||||||
|
options.services.podman.package = lib.mkOption {
|
||||||
|
type = lib.types.package;
|
||||||
|
default = pkgs.podman;
|
||||||
|
description = "The podman package to use.";
|
||||||
|
};
|
||||||
|
}
|
83
modules/services/podman-linux/podman-lib.nix
Normal file
83
modules/services/podman-linux/podman-lib.nix
Normal file
|
@ -0,0 +1,83 @@
|
||||||
|
{ lib, ... }:
|
||||||
|
|
||||||
|
with lib;
|
||||||
|
|
||||||
|
let
|
||||||
|
primitive = with types; nullOr (oneOf [ bool int str path ]);
|
||||||
|
primitiveAttrs = with types; attrsOf (either primitive (listOf primitive));
|
||||||
|
|
||||||
|
formatPrimitiveValue = value:
|
||||||
|
if isBool value then
|
||||||
|
(if value then "true" else "false")
|
||||||
|
else if isList value then
|
||||||
|
concatStringsSep " " (map toString value)
|
||||||
|
else
|
||||||
|
toString value;
|
||||||
|
in {
|
||||||
|
inherit primitive;
|
||||||
|
inherit primitiveAttrs;
|
||||||
|
inherit formatPrimitiveValue;
|
||||||
|
|
||||||
|
serviceConfigTypeRules = {
|
||||||
|
Restart = types.enum [ "no" "always" "on-failure" "unless-stopped" ];
|
||||||
|
TimeoutStopSec = types.int;
|
||||||
|
};
|
||||||
|
serviceConfigDefaults = {
|
||||||
|
Restart = "always";
|
||||||
|
TimeoutStopSec = 30;
|
||||||
|
ExecStartPre = null;
|
||||||
|
};
|
||||||
|
serviceConfigType = with types; attrsOf (either primitive (listOf primitive));
|
||||||
|
|
||||||
|
unitConfigTypeRules = {
|
||||||
|
After = with types; nullOr (listOf str);
|
||||||
|
};
|
||||||
|
unitConfigDefaults = {
|
||||||
|
After = null;
|
||||||
|
};
|
||||||
|
unitConfigType = with types; attrsOf (either primitive (listOf primitive));
|
||||||
|
|
||||||
|
assertConfigTypes = configTypeRules: config: containerName:
|
||||||
|
lib.flatten (lib.mapAttrsToList (name: value:
|
||||||
|
if lib.hasAttr name configTypeRules then
|
||||||
|
[{
|
||||||
|
assertion = configTypeRules.${name}.check value;
|
||||||
|
message = "in '${containerName}' config. ${name}: '${toString value}' does not match expected type: ${configTypeRules.${name}.description}";
|
||||||
|
}]
|
||||||
|
else []
|
||||||
|
) config);
|
||||||
|
|
||||||
|
formatExtraConfig = extraConfig:
|
||||||
|
let
|
||||||
|
nonNullConfig = lib.filterAttrs (name: value: value != null) extraConfig;
|
||||||
|
in
|
||||||
|
concatStringsSep "\n" (
|
||||||
|
mapAttrsToList (name: value: "${name}=${formatPrimitiveValue value}") nonNullConfig
|
||||||
|
);
|
||||||
|
|
||||||
|
# input is expecting a list of quadletInternalType with all the same unitType
|
||||||
|
generateManifestText = quadlets:
|
||||||
|
let
|
||||||
|
# create a list of all unique quadlet.unitTypes in quadlets
|
||||||
|
quadletTypes = unique (map (quadlet: quadlet.unitType) quadlets);
|
||||||
|
# if quadletTypes is not length 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;
|
||||||
|
in
|
||||||
|
# specific logic for writing the unit name goes here. It should be identical to what `podman <resource> ls` shows
|
||||||
|
{
|
||||||
|
"container" = strippedName;
|
||||||
|
"network" = strippedName;
|
||||||
|
}."${quadlet.unitType}";
|
||||||
|
in
|
||||||
|
if allQuadletsSameType then ''
|
||||||
|
${concatStringsSep "\n" (map (quadlet: formatServiceName quadlet) quadlets)}
|
||||||
|
''
|
||||||
|
else
|
||||||
|
abort "All quadlets must be of the same type.\nQuadlet types in this manifest: ${concatStringsSep ", " quadletTypes}";
|
||||||
|
}
|
60
modules/services/podman-linux/services.nix
Normal file
60
modules/services/podman-linux/services.nix
Normal file
|
@ -0,0 +1,60 @@
|
||||||
|
{ config, lib, pkgs, ... }:
|
||||||
|
|
||||||
|
with lib;
|
||||||
|
|
||||||
|
let
|
||||||
|
cfg = config.services.podman;
|
||||||
|
in {
|
||||||
|
options.services.podman = {
|
||||||
|
|
||||||
|
auto-update = {
|
||||||
|
enable = mkOption {
|
||||||
|
type = types.bool;
|
||||||
|
default = false;
|
||||||
|
description = "Automatically update the podman images.";
|
||||||
|
};
|
||||||
|
|
||||||
|
OnCalendar = mkOption {
|
||||||
|
type = types.str;
|
||||||
|
default = "Sun *-*-* 00:00";
|
||||||
|
description = "Systemd OnCalendar expression for the update";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
config = mkMerge [
|
||||||
|
( mkIf cfg.auto-update.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=/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.auto-update.OnCalendar;
|
||||||
|
RandomizedDelaySec = 300;
|
||||||
|
Persistent = true;
|
||||||
|
};
|
||||||
|
Install = {
|
||||||
|
WantedBy = [ "timers.target" ];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
})
|
||||||
|
];
|
||||||
|
}
|
Loading…
Reference in a new issue