1
0
Fork 0
mirror of https://github.com/nix-community/home-manager.git synced 2025-12-07 01:21:03 +01:00

podman: added volume, image, and build quadlets (#6137)

Added support for build, image, and volume quadlets
Resolved test failures due to podman 5.3.0 upgrade
Replaced several instances of pkgs.podman with services.podman.package
This commit is contained in:
bamhm182 2025-03-10 00:02:05 -04:00 committed by GitHub
parent f8bb0ba6de
commit ce9cb2496c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
24 changed files with 1000 additions and 54 deletions

View file

@ -18,6 +18,7 @@
local manifestFile="${config.xdg.configHome}/podman/$2"
local extraListCommands="''${3:-}"
[[ $resourceType = "container" ]] && extraListCommands+=" -a"
[[ $resourceType = "volume" ]] && extraListCommands+=" --filter label=nix.home-manager.preserve=false"
[ ! -f "$manifestFile" ] && VERBOSE_ENABLED && echo "Manifest does not exist: $manifestFile" && return 0
@ -27,6 +28,7 @@
formatString="{{.Name}}"
[[ $resourceType = "container" ]] && formatString="{{.Names}}"
[[ $resourceType = "image" ]] && formatString="{{.Repository}}"
local listOutput=$(${config.services.podman.package}/bin/podman $resourceType ls $extraListCommands --filter 'label=nix.home-manager.managed=true' --format "$formatString")
@ -36,12 +38,12 @@
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
if ! isResourceInManifest "$resource"; then
removeResource "$resourceType" "$resource"
else
VERBOSE_ENABLED && echo "Keeping managed $resourceType: $resource" || true
fi
done
fi
}
@ -69,19 +71,20 @@
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"
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"
"image" | "network" | "volume")
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
if [[ "$(eval "$command")" != *"$resource" ]]; then
echo -e "\tCommand failed: ''${command}"
[ "$resourceType" == "image" ] && resourceType="ancestor"
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
@ -92,7 +95,7 @@
[[ "$@" == *"--verbose"* ]] && VERBOSE="true"
[[ "$@" == *"--dry-run"* ]] && DRY_RUN="true"
for type in "container" "network"; do
for type in "container" "image" "network" "volume"; do
cleanup "$type" "''${type}s.manifest"
done
'';

View file

@ -0,0 +1,168 @@
{ config, lib, pkgs, ... }:
with lib;
let
cfg = config.services.podman;
podman-lib = import ./podman-lib.nix { inherit pkgs lib config; };
createQuadletSource = name: buildDef:
let
buildConfig = podman-lib.deepMerge {
Build = {
AuthFile = buildDef.authFile;
Environment = buildDef.environment;
File = buildDef.file;
ImageTag = [ "homemanager/${name}" ] ++ buildDef.tags;
Label = buildDef.labels // { "nix.home-manager.managed" = true; };
PodmanArgs = buildDef.extraPodmanArgs;
SetWorkingDirectory = buildDef.workingDirectory;
TLSVerify = buildDef.tlsVerify;
};
Install = {
WantedBy = optionals buildDef.autoStart [
"default.target"
"multi-user.target"
];
};
Service = {
TimeoutStartSec = 300;
RemainAfterExit = "yes";
};
Unit = { Description = buildDef.description; };
} buildDef.extraConfig;
in ''
# Automatically generated by home-manager for podman build configuration
# DO NOT EDIT THIS FILE DIRECTLY
#
# ${name}.build
${podman-lib.toQuadletIni buildConfig}
'';
toQuadletInternal = name: buildDef: {
assertions = podman-lib.buildConfigAsserts name buildDef.extraConfig;
serviceName =
"podman-${name}"; # quadlet service name: 'podman-<name>-build.service
source = podman-lib.removeBlankLines (createQuadletSource name buildDef);
resourceType = "build";
};
in let
buildDefinitionType = types.submodule ({ name, ... }: {
options = {
autoStart = mkOption {
type = types.bool;
default = true;
description =
"Whether to start the build on boot. Requires user lingering.";
};
authFile = mkOption {
type = with types; nullOr path;
default = null;
description = "Path of the authentication file.";
};
description = mkOption {
type = with types; nullOr str;
default = "Service for build ${name}";
defaultText = "Service for build \${name}";
example = "My Build";
description = "The description of the build.";
};
environment = mkOption {
type = podman-lib.primitiveAttrs;
default = { };
example = literalExpression ''
{
VAR1 = "0:100";
VAR2 = true;
VAR3 = 5;
}
'';
description = "Environment variables to set in the build.";
};
extraConfig = mkOption {
type = podman-lib.extraConfigType;
default = { };
example = literalExpression ''
{
Build = {
Arch = "aarch64";
};
Service = {
TimeoutStartSec = 15;
};
}
'';
description = "INI sections and values to populate the Build Quadlet.";
};
extraPodmanArgs = mkOption {
type = types.listOf types.str;
default = [ ];
example = [ "--retries 5" ];
description = "Extra arguments to pass to the podman build command.";
};
file = mkOption {
type = types.str;
example = literalExpression ''
`"xdg.configFile."containerfiles/my-img/Containerfile"`
or
`"https://github.com/.../my-img/Containerfile"`
'';
description =
"Path to a Containerfile which contains instructions to build the image.";
};
tags = mkOption {
type = with types; listOf str;
default = [ ];
description = ''
Name associated with the build.
First tag will always be "homemanager/<name>".
'';
};
labels = mkOption {
type = with types; attrsOf str;
default = { };
example = {
app = "myapp";
some-label = "somelabel";
};
description = "The labels to apply to the build.";
};
tlsVerify = mkOption {
type = types.bool;
default = true;
description =
"Require HTTPS and verification of certificates when contacting registries.";
};
workingDirectory = mkOption {
type = with types; nullOr path;
default = null;
description = "WorkingDirectory of the systemd unit file.";
};
};
});
in {
options.services.podman.builds = mkOption {
type = types.attrsOf buildDefinitionType;
default = { };
description = "Defines Podman build quadlet configurations.";
};
config = let buildQuadlets = mapAttrsToList toQuadletInternal cfg.builds;
in mkIf cfg.enable {
services.podman.internal.quadletDefinitions = buildQuadlets;
assertions = flatten (map (build: build.assertions) buildQuadlets);
xdg.configFile."podman/images.manifest".text =
podman-lib.generateManifestText buildQuadlets;
};
}

View file

@ -5,24 +5,60 @@ with lib;
let
cfg = config.services.podman;
podman-lib = import ./podman-lib.nix { inherit lib config; };
podman-lib = import ./podman-lib.nix { inherit pkgs lib config; };
createQuadletSource = name: containerDef:
let
mapHmNetworks = network:
if builtins.hasAttr network cfg.networks then
"podman-${network}-network.service"
else
null;
formatServiceNameForType = type: name:
{
image = "podman-${name}-image.service";
build = "podman-${name}-build.service";
network = "podman-${name}-network.service";
volume = "podman-${name}-volume.service";
}."${type}";
finalConfig = let
managedNetworks = if lib.isList containerDef.network then
map mapHmNetworks containerDef.network
else if containerDef.network != null then
map mapHmNetworks [ containerDef.network ]
dependencyByHomeManagerQuadlet = type: name:
let
definitionsOfType =
filter (q: q.resourceType == type) cfg.internal.quadletDefinitions;
matchingName =
filter (q: q.serviceName == "podman-${name}") definitionsOfType;
in if ((length matchingName) == 1) then
[ (formatServiceNameForType type name) ]
else
[ ];
in (podman-lib.deepMerge {
forEachValue = type: value:
let resolve = v: dependencyByHomeManagerQuadlet type v;
in if isList value then
concatLists (map resolve value)
else
resolve value;
withResolverFor = type: value:
{
"image" = forEachValue "image" value;
"build" = forEachValue "build" value;
"network" = forEachValue "network" value;
"volume" = let
a = if isList value then value else [ value ];
volumes = map (v: elemAt (splitString ":" v) 0) a;
in forEachValue "volume" volumes;
}.${type};
dependencyServices = (withResolverFor "image" containerDef.image)
++ (withResolverFor "build" containerDef.image)
++ (withResolverFor "network" containerDef.network)
++ (withResolverFor "volume" containerDef.volumes);
resolvedImage = if (builtins.hasAttr containerDef.image cfg.images) then
cfg.images."${containerDef.image}".image
else if (builtins.hasAttr containerDef.image cfg.builds) then
"localhost/homemanager/${containerDef.image}"
else
containerDef.image;
quadlet = (podman-lib.deepMerge {
Container = {
AddCapability = containerDef.addCapabilities;
AddDevice = containerDef.devices;
@ -34,7 +70,7 @@ let
EnvironmentFile = containerDef.environmentFile;
Exec = containerDef.exec;
Group = containerDef.group;
Image = containerDef.image;
Image = resolvedImage;
IP = containerDef.ip4;
IP6 = containerDef.ip6;
Label =
@ -48,11 +84,10 @@ let
Volume = containerDef.volumes;
};
Install = {
WantedBy = (if containerDef.autoStart then [
WantedBy = optionals containerDef.autoStart [
"default.target"
"multi-user.target"
] else
[ ]);
];
};
Service = {
Environment = {
@ -66,8 +101,8 @@ let
TimeoutStopSec = 30;
};
Unit = {
After = [ "network.target" ] ++ managedNetworks;
Requires = managedNetworks;
After = dependencyServices;
Requires = dependencyServices;
Description = (if (builtins.isString containerDef.description) then
containerDef.description
else
@ -79,7 +114,7 @@ let
# DO NOT EDIT THIS FILE DIRECTLY
#
# ${name}.container
${podman-lib.toQuadletIni finalConfig}
${podman-lib.toQuadletIni quadlet}
'';
toQuadletInternal = name: containerDef: {
@ -307,7 +342,7 @@ in {
flatten (map (container: container.assertions) containerQuadlets);
# manifest file
home.file."${config.xdg.configHome}/podman/containers.manifest".text =
xdg.configFile."podman/containers.manifest".text =
podman-lib.generateManifestText containerQuadlets;
};
}

View file

@ -5,8 +5,15 @@ let
in {
meta.maintainers = with lib.hm.maintainers; [ bamhm182 n-hass ];
imports =
[ ./containers.nix ./install-quadlet.nix ./networks.nix ./services.nix ];
imports = [
./builds.nix
./containers.nix
./images.nix
./install-quadlet.nix
./networks.nix
./services.nix
./volumes.nix
];
options.services.podman = {
enable = lib.mkEnableOption "Podman, a daemonless container engine";

View file

@ -0,0 +1,162 @@
{ config, lib, pkgs, ... }:
with lib;
let
cfg = config.services.podman;
podman-lib = import ./podman-lib.nix { inherit pkgs lib config; };
createQuadletSource = name: imageDef:
let
credsString =
(if imageDef.username != null then imageDef.username else "")
+ (if imageDef.password != null then ":${imageDef.password}" else "");
imageConfig = podman-lib.deepMerge {
Image = {
AuthFile = imageDef.authFile;
CertDir = imageDef.certDir;
Creds = (if credsString != "" then credsString else null);
DecryptionKey = imageDef.decryptionKeyFile;
Image = imageDef.image;
ImageTag = imageDef.tag;
PodmanArgs = imageDef.extraPodmanArgs;
TLSVerify = imageDef.tlsVerify;
};
Install = {
WantedBy = optionals imageDef.autoStart [
"default.target"
"multi-user.target"
];
};
Service = {
ExecStartPre = [ "${podman-lib.awaitPodmanUnshare}" ];
TimeoutStartSec = 300;
RemainAfterExit = "yes";
};
Unit = { Description = imageDef.description; };
} imageDef.extraConfig;
in ''
# Automatically generated by home-manager for podman image configuration
# DO NOT EDIT THIS FILE DIRECTLY
#
# ${name}.image
${podman-lib.toQuadletIni imageConfig}
'';
toQuadletInternal = name: imageDef: {
assertions = podman-lib.buildConfigAsserts name imageDef.extraConfig;
serviceName =
"podman-${name}"; # quadlet service name: 'podman-<name>-image.service
source = podman-lib.removeBlankLines (createQuadletSource name imageDef);
resourceType = "image";
};
in let
imageDefinitionType = types.submodule ({ name, ... }: {
options = {
autoStart = mkOption {
type = types.bool;
default = true;
description =
"Whether to pull the image on boot. Requires user lingering.";
};
authFile = mkOption {
type = with types; nullOr path;
default = null;
description =
"Path of the authentication file used to connect to registry.";
};
certDir = mkOption {
type = with types; nullOr path;
default = null;
description =
"Path of certificates (*.{crt,cert,key}) used to connect to registry.";
};
decryptionKeyFile = mkOption {
type = with types; nullOr path;
default = null;
description = "Path to key used for decrpytion of images.";
};
description = mkOption {
type = with types; nullOr str;
default = "Service for image ${name}";
defaultText = "Service for image \${name}";
example = "My Image";
description = "The description of the image.";
};
extraConfig = mkOption {
type = podman-lib.extraConfigType;
default = { };
example = literalExpression ''
{
Image = {
ContainersConfModule = "/etc/nvd.conf";
};
}
'';
description = "INI sections and values to populate the Image Quadlet.";
};
extraPodmanArgs = mkOption {
type = with types; listOf str;
default = [ ];
example = [ "--os=linux" ];
description =
"Extra arguments to pass to the podman image pull command.";
};
image = mkOption {
type = types.str;
example = "quay.io/centos/centos:latest";
description = "Image to pull.";
};
password = mkOption {
type = with types; nullOr str;
default = null;
example = "P@ssw0rd";
description =
"Password used to connect to registry. (Will be visible in nix store)";
};
tag = mkOption {
type = with types; nullOr str;
default = null;
example = "quay.io/centos/centos:latest";
description =
"FQIN of referenced Image when source is a file or directory archive.";
};
tlsVerify = mkOption {
type = types.bool;
default = true;
description =
"Require HTTPS and verification of certificates when contacting registries.";
};
username = mkOption {
type = with types; nullOr str;
default = null;
example = "bob";
description = "Username used to connect to registry.";
};
};
});
in {
options.services.podman.images = mkOption {
type = types.attrsOf imageDefinitionType;
default = { };
description = "Defines Podman image quadlet configurations.";
};
config = let imageQuadlets = mapAttrsToList toQuadletInternal cfg.images;
in mkIf cfg.enable {
services.podman.internal.quadletDefinitions = imageQuadlets;
assertions = flatten (map (image: image.assertions) imageQuadlets);
};
}

View file

@ -5,7 +5,7 @@ with lib;
let
cfg = config.services.podman;
podman-lib = import ./podman-lib.nix { inherit lib config; };
podman-lib = import ./podman-lib.nix { inherit pkgs lib config; };
activation = import ./activation.nix { inherit config podman-lib; };
activationCleanupScript = activation.cleanup;

View file

@ -5,13 +5,7 @@ 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
'';
podman-lib = import ./podman-lib.nix { inherit pkgs lib config; };
createQuadletSource = name: networkDef:
let
@ -39,7 +33,7 @@ let
"${makeBinPath [ pkgs.su pkgs.coreutils ]}"
]);
};
ExecStartPre = [ "${awaitPodmanUnshare}" ];
ExecStartPre = [ "${podman-lib.awaitPodmanUnshare}" ];
TimeoutStartSec = 15;
RemainAfterExit = "yes";
};
@ -162,7 +156,7 @@ in {
services.podman.internal.quadletDefinitions = networkQuadlets;
assertions = flatten (map (network: network.assertions) networkQuadlets);
home.file."${config.xdg.configHome}/podman/networks.manifest".text =
xdg.configFile."podman/networks.manifest".text =
podman-lib.generateManifestText networkQuadlets;
};
}

View file

@ -1,4 +1,4 @@
{ lib, config, ... }:
{ pkgs, lib, config, ... }:
with lib;
@ -63,8 +63,10 @@ in {
buildConfigAsserts = quadletName: extraConfig:
let
configRules = {
Build = { ImageTag = (with types; listOf str); };
Container = { ContainerName = types.enum [ quadletName ]; };
Network = { NetworkName = types.enum [ quadletName ]; };
Volume = { VolumeName = types.enum [ quadletName ]; };
};
# Function to build assertions for a specific section and its attributes.
@ -83,8 +85,23 @@ in {
else
[ ];
checkImageTag = extraConfig:
let
imageTags = (extraConfig.Build or { }).ImageTag or [ ];
containsRequiredTag =
builtins.elem "homemanager/${quadletName}" imageTags;
imageTagsStr = concatMapStringsSep ''" "'' toString imageTags;
in [{
assertion = imageTags == [ ] || containsRequiredTag;
message = ''
In '${quadletName}' config. Build.ImageTag: '[ "${imageTagsStr}" ]' does not contain 'homemanager/${quadletName}'.'';
}];
# Flatten assertions from all sections in `extraConfig`.
in flatten (mapAttrsToList buildSectionAsserts extraConfig);
in flatten (concatLists [
(mapAttrsToList buildSectionAsserts extraConfig)
(checkImageTag extraConfig)
]);
extraConfigType = with types;
attrsOf (attrsOf (oneOf [ primitiveAttrs primitiveList primitive ]));
@ -107,8 +124,10 @@ in {
# specific logic for writing the unit name goes here. It should be
# identical to what `podman <resource> ls` shows
in {
"build" = "localhost/homemanager/${strippedName}";
"container" = strippedName;
"network" = strippedName;
"volume" = strippedName;
}."${quadlet.resourceType}";
in if allQuadletsSameType then ''
${concatStringsSep "\n"
@ -133,4 +152,10 @@ in {
lines = splitString "\n" text;
nonEmptyLines = filter (line: line != "") lines;
in concatStringsSep "\n" nonEmptyLines;
awaitPodmanUnshare = pkgs.writeShellScript "await-podman-unshare" ''
until ${config.services.podman.package}/bin/podman unshare ${pkgs.coreutils}/bin/true; do
${pkgs.coreutils}/bin/sleep 1
done
'';
}

View file

@ -42,8 +42,8 @@ in {
"${config.home.homeDirectory}/.nix-profile/bin"
]
}";
ExecStart = "${pkgs.podman}/bin/podman auto-update";
ExecStartPost = "${pkgs.podman}/bin/podman image prune -f";
ExecStart = "${cfg.package}/bin/podman auto-update";
ExecStartPost = "${cfg.package}/bin/podman image prune -f";
TimeoutStartSec = "300s";
TimeoutStopSec = "10s";
};

View file

@ -0,0 +1,186 @@
{ config, lib, pkgs, ... }:
with lib;
let
cfg = config.services.podman;
podman-lib = import ./podman-lib.nix { inherit pkgs lib config; };
createQuadletSource = name: volumeDef:
let
volumeConfig = podman-lib.deepMerge {
Install = {
WantedBy = optionals volumeDef.autoStart [
"default.target"
"multi-user.target"
];
};
Service = {
Environment = {
PATH = (builtins.concatStringsSep ":" [
"${podman-lib.newuidmapPaths}"
"${makeBinPath [ pkgs.su pkgs.coreutils ]}"
]);
};
ExecStartPre = [ "${podman-lib.awaitPodmanUnshare}" ];
TimeoutStartSec = 15;
RemainAfterExit = "yes";
};
Unit = { Description = volumeDef.description; };
Volume = {
Copy = volumeDef.copy;
Device = volumeDef.device;
Driver = volumeDef.driver;
Group = volumeDef.group;
Image = volumeDef.image;
Label = volumeDef.labels // {
"nix.home-manager.managed" = true;
"nix.home-manager.preserve" = volumeDef.preserve;
};
PodmanArgs = volumeDef.extraPodmanArgs;
Type = volumeDef.type;
User = volumeDef.user;
VolumeName = name;
};
} volumeDef.extraConfig;
in ''
# Automatically generated by home-manager for podman volume configuration
# DO NOT EDIT THIS FILE DIRECTLY
#
# ${name}.volume
${podman-lib.toQuadletIni volumeConfig}
'';
toQuadletInternal = name: volumeDef: {
assertions = podman-lib.buildConfigAsserts name volumeDef.extraConfig;
serviceName =
"podman-${name}"; # quadlet service name: 'podman-<name>-volume.service'
source = podman-lib.removeBlankLines (createQuadletSource name volumeDef);
resourceType = "volume";
};
in let
volumeDefinitionType = types.submodule ({ name, ... }: {
options = {
autoStart = mkOption {
type = types.bool;
default = true;
description = "Whether to create the volume on boot.";
};
copy = mkOption {
type = types.bool;
default = true;
description =
"Copy content of the image located at the mountpoint of the volume on first run.";
};
description = mkOption {
type = with types; nullOr str;
default = "Service for volume ${name}";
defaultText = "Service for volume \${name}";
example = "My Volume";
description = "The description of the volume.";
};
device = mkOption {
type = with types; nullOr str;
default = null;
example = "tmpfs";
description = "The path of a device which is mounted for the volume.";
};
driver = mkOption {
type = with types; nullOr str;
default = null;
example = "image";
description = "The volume driver to use.";
};
extraConfig = mkOption {
type = podman-lib.extraConfigType;
default = { };
example = literalExpression ''
{
Volume = {
ContainerConfModule = "/etc/nvd.conf";
};
}
'';
description = "INI sections and values to populate the Volume Quadlet.";
};
extraPodmanArgs = mkOption {
type = with types; listOf str;
default = [ ];
example = [ "--opt copy" ];
description =
"Extra arguments to pass to the podman volume create command.";
};
group = mkOption {
type = with types; nullOr (either int str);
default = null;
description = "The group ID owning the volume inside the container.";
};
image = mkOption {
type = with types; nullOr str;
default = null;
example = "quay.io/centos/centos:latest";
description =
"Specifies the image the volume is based on when Driver is set to the image.";
};
labels = mkOption {
type = with types; attrsOf str;
default = { };
example = {
app = "myapp";
some-label = "somelabel";
};
description = "The labels to apply to the volume.";
};
preserve = mkOption {
type = types.bool;
default = true;
description = ''
Whether the volume should be preserved if it is removed from the configuration.
Setting this to false will cause the volume to be deleted if the volume is removed from the configuration
'';
};
type = mkOption {
type = with types; nullOr str;
default = null;
example = "tmpfs";
description =
"Filesystem type of Device. (used as -t in mount commands)";
};
user = mkOption {
type = with types; nullOr (either int str);
default = null;
description = "The user ID owning the volume inside the container.";
};
};
});
in {
options.services.podman.volumes = mkOption {
type = types.attrsOf volumeDefinitionType;
default = { };
description = "Defines Podman volume quadlet configurations.";
};
config = let volumeQuadlets = mapAttrsToList toQuadletInternal cfg.volumes;
in mkIf cfg.enable {
services.podman.internal.quadletDefinitions = volumeQuadlets;
assertions = flatten (map (volume: volume.assertions) volumeQuadlets);
xdg.configFile."podman/volumes.manifest".text =
podman-lib.generateManifestText volumeQuadlets;
};
}