1
0
mirror of https://github.com/nix-community/home-manager synced 2024-06-14 02:33:38 +02:00

refactor + allow primitive types and 'escape hatch' config values with extensible validation rules

This commit is contained in:
Nicholas Hassan 2024-02-18 16:02:55 +10:30 committed by bndlfm
parent 5d8cdba815
commit 721f458d04
7 changed files with 202 additions and 131 deletions

View File

@ -333,7 +333,7 @@ let
./services/plan9port.nix
./services/playerctld.nix
./services/plex-mpv-shim.nix
./services/podman-linux/podman.nix
./services/podman-linux/default.nix
./services/polybar.nix
./services/poweralertd.nix
./services/pueue.nix

View File

@ -2,87 +2,104 @@
with lib;
let
createQuadletSource = name: containerDef:
let
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 "";
serviceName = if containerDef.serviceName != null then containerDef.serviceName else name;
containerName = name; # Use the submodule name as the container name
podman-lib = import ./podman-lib.nix { inherit lib; };
formatEnvironment = env:
if env != {} then
concatStringsSep " " (mapAttrsToList (k: v: "${k}=${v}") env)
else
"";
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;
###
formatPorts = ports:
if ports != [] then
concatStringsSep "\n" (map (port: "PublishPort=${port}") ports)
else
"";
### 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 "";
###
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
"";
### Formatters
formatExtraConfig = podman-lib.formatExtraConfig;
formatCapabilities = action: capabilities:
if capabilities != [] then
concatStringsSep "\n" (map (capability: "${action}Capability=${capability}") capabilities)
else
"";
formatNetworkDependencies = networks:
let
formatElement = network: "podman-${network}-network.service";
in
concatStringsSep " " (map formatElement networks);
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
"";
formatUnitTags = tagList:
if tagList != [] then
concatStringsSep " " (map (tag: "${tag}") tagList)
else
"";
formatExtraArgs = containerDef:
let
networkAliasArg = if containerDef.networkAlias != null then "--network-alias ${containerDef.networkAlias}" else "";
entrypointArg = if containerDef.entrypoint != null then "--entrypoint '${containerDef.entrypoint}'" else "";
allArgs = [networkAliasArg entrypointArg] ++ containerDef.extraOptions;
in
if allArgs != [] && allArgs != [""] then
"PodmanArgs=${concatStringsSep " " (filter (arg: arg != null && arg != "") allArgs)}"
formatEnvironment = env:
if env != {} then
concatStringsSep " " (mapAttrsToList (k: v: "${k}=${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
"";
formatUnitTags = tagList:
if tagList != [] then
concatStringsSep " " (map (tag: "${tag}") tagList)
else
"";
formatPodmanArgs = containerDef:
let
networkAliasArg = if containerDef.networkAlias != null then "--network-alias ${containerDef.networkAlias}" else "";
entrypointArg = if containerDef.entrypoint != null then "--entrypoint '${containerDef.entrypoint}'" else "";
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
@ -90,8 +107,9 @@ let
# ${serviceName}.container
[Unit]
Description=${if containerDef.description != null then containerDef.description else "Service for container ${containerName}"}
After=network.target ${formatUnitTags containerDef.unitConfig.After}
${ifNotEmptyList containerDef.networks "After=podman-networks-hm.service"}
After=network.target
${ifNotEmptyList containerDef.networks "After=${formatNetworkDependencies containerDef.networks}"}
${formatExtraConfig mergedUnitConfig}
[Container]
ContainerName=${containerName}
@ -112,13 +130,12 @@ let
${ifNotEmptyList containerDef.addCapabilities (formatCapabilities "Add" containerDef.addCapabilities)}
${ifNotEmptyList containerDef.dropCapabilities (formatCapabilities "Drop" containerDef.dropCapabilities)}
${ifNotEmptyList containerDef.labels (formatLabels containerDef.labels)}
${formatExtraArgs containerDef}
${formatPodmanArgs containerDef}
${formatExtraConfig containerDef.extraContainerConfig}
[Service]
Environment="PATH=/run/wrappers/bin:/run/current-system/sw/bin:${config.home.homeDirectory}/.nix-profile/bin"
Restart=${containerDef.serviceConfig.Restart}
TimeoutStopSec=${toString containerDef.serviceConfig.TimeoutStopSec}
${ifNotNull containerDef.serviceConfig.ExecStartPre "ExecStartPre=${containerDef.serviceConfig.ExecStartPre}"}
${formatExtraConfig mergedServiceConfig}
[Install]
${if containerDef.autostart then "WantedBy=multi-user.target default.target" else ""}
@ -135,11 +152,16 @@ let
removeBlankLines configText;
toQuadletInternal = name: containerDef:
{
serviceName = if containerDef.serviceName != null then containerDef.serviceName else "podman-${name}";
source = createQuadletSource name containerDef;
unitType = "container";
};
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
@ -264,41 +286,33 @@ let
default = [];
};
serviceConfig = {
TimeoutStopSec = mkOption {
type = types.int;
default = 30;
};
ExecStartPre = mkOption {
type = with types; nullOr str;
default = null;
};
Restart = mkOption {
type = types.enum [
"no"
"always"
"on-failure"
"unless-stopped"
];
default = "always";
description = "The restart policy of the container.";
};
};
unitConfig = {
After = mkOption {
type = with types; listOf str;
default = [];
};
};
extraOptions = mkOption {
type = types.listOf types.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 = {};
};
};
};
@ -316,5 +330,6 @@ in {
config = {
internal.podman-quadlet-definitions = mapAttrsToList toQuadletInternal config.services.podman.containers;
assertions = lib.flatten (map (container: container.assertions) config.internal.podman-quadlet-definitions);
};
}

View File

@ -1,15 +1,11 @@
{ pkgs, lib, ... }:
with lib;
{
meta.maintainers = [ maintainers.n-hass ];
imports =
imports =
[ ./services.nix ./networks.nix ./containers.nix ./install-quadlet.nix ];
config = {
assertions =
[ (lib.hm.assertions.assertPlatform "podman" pkgs lib.platforms.linux) ];
};
}
}

View File

@ -2,18 +2,15 @@
with lib;
# Helper function to safely escape strings
let
podman-lib = import ./podman-lib.nix { inherit lib; };
createQuadletSource = name: networkDef:
let
formatNetworkOption = k: v: "${k}=${v}";
networkOptions = mapAttrsToList formatNetworkOption networkDef;
in
''
# Automatically generated by home-manager for podman network configuration
# DO NOT EDIT THIS FILE DIRECTLY
[Network]
${concatStringsSep "\n" networkOptions}
${podman-lib.formatExtraConfig networkDef}
[Install]
WantedBy=multi-user.target default.target
@ -21,7 +18,7 @@ let
toQuadletInternal = name: networkDef:
{
serviceName = "podman-network-${name}";
serviceName = "podman-${name}"; # becomes podman-<netname>-network.service because of quadlet
source = createQuadletSource name networkDef;
unitType = "network";
};
@ -30,7 +27,7 @@ in
{
options = {
services.podman.networks = mkOption {
type = types.attrsOf (types.attrsOf types.str);
type = types.attrsOf (podman-lib.primitiveAttrs);
default = {};
example = literalExample ''
{

View File

@ -1,7 +1,7 @@
{lib, ...}:
let
# Define the type which the systemd services will be derived from
# Define the systemd service type
quadletInternalType = lib.types.submodule {
options = {
serviceName = lib.mkOption {
@ -19,8 +19,14 @@ let
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;

View File

@ -0,0 +1,58 @@
{ 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 {
primitive = primitive; # export
primitiveAttrs = primitiveAttrs; # export
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);
formatPrimitiveValue = formatPrimitiveValue; # export
formatExtraConfig = extraConfig:
let
nonNullConfig = lib.filterAttrs (name: value: value != null) extraConfig;
in
concatStringsSep "\n" (
mapAttrsToList (name: value: "${name}=${formatPrimitiveValue value}") nonNullConfig
);
}

View File

@ -18,7 +18,6 @@ in {
type = types.str;
default = "Sun *-*-* 00:00";
description = "Systemd OnCalendar expression for the update";
example = "Sun *-*-* 00:00";
};
};