diff --git a/modules/lib/maintainers.nix b/modules/lib/maintainers.nix index 6b7dc8b42..91ecb47be 100644 --- a/modules/lib/maintainers.nix +++ b/modules/lib/maintainers.nix @@ -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"; diff --git a/modules/misc/news.nix b/modules/misc/news.nix index 801391cc2..aeec8f9a0 100644 --- a/modules/misc/news.nix +++ b/modules/misc/news.nix @@ -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. + ''; + } ]; }; } diff --git a/modules/modules.nix b/modules/modules.nix index 3b4985083..75f8ac461 100644 --- a/modules/modules.nix +++ b/modules/modules.nix @@ -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 diff --git a/modules/services/podman-linux/activation.nix b/modules/services/podman-linux/activation.nix new file mode 100644 index 000000000..5791f19b0 --- /dev/null +++ b/modules/services/podman-linux/activation.nix @@ -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 + ''; +} diff --git a/modules/services/podman-linux/containers.nix b/modules/services/podman-linux/containers.nix new file mode 100644 index 000000000..41ab29130 --- /dev/null +++ b/modules/services/podman-linux/containers.nix @@ -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-.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/:/dev/" ]; + 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=