diff --git a/flake.nix b/flake.nix index 212d2fc..a9bba2f 100644 --- a/flake.nix +++ b/flake.nix @@ -14,6 +14,8 @@ system = "x86_64-linux"; modules = [ + (import ./modules) + (import ./configuration) (import ./configuration/linode.nix) (import ./configuration/hardware-configuration.nix) @@ -25,6 +27,8 @@ system = "x86_64-linux"; modules = [ + (import ./modules) + (import ./configuration) ({ ... }: { users.users.tlater.password = "insecure"; diff --git a/modules/default.nix b/modules/default.nix new file mode 100644 index 0000000..0bc1f1c --- /dev/null +++ b/modules/default.nix @@ -0,0 +1,5 @@ +{ ... }: + +{ + imports = [ ./virtualisation/pods.nix ]; +} diff --git a/modules/virtualisation/pods.nix b/modules/virtualisation/pods.nix new file mode 100644 index 0000000..a5c9daa --- /dev/null +++ b/modules/virtualisation/pods.nix @@ -0,0 +1,220 @@ +{ lib, config, options, ... }: + +with lib; + +let + cfg = config.virtualisation.pods; + list-to-args = arg: list: + concatStringsSep " " (map (e: "--${arg}=${escapeShellArg e}") list); + possibly-unset-arg = arg: val: + (optionalString (val != null) "--${arg}=${escapeShellArg val}"); + + mkPod = name: pod: rec { + path = [ config.virtualisation.podman.package ]; + + wants = [ "network.target" ]; + after = [ "network-online.target" ]; + wantedBy = [ "multi-user.target" "default.target" ]; + + environment.PODMAN_SYSTEMD_UNIT = "%n"; + + preStart = concatStringsSep " " [ + "mkdir -p /run/podman/pods/ ;" + "podman pod create" + "--infra-conmon-pidfile=${escapeShellArg "/run/podman/pods/${name}.pid"}" + "--name=${escapeShellArg name}" + "--replace" + (list-to-args "add-host" pod.added-hosts) + (possibly-unset-arg "cgroup-parent" pod.cgroup-parent) + (list-to-args "dns" pod.dns) + (list-to-args "dns-opt" pod.dns-opt) + (list-to-args "dns-search" pod.dns-search) + (possibly-unset-arg "hostname" pod.hostname) + (possibly-unset-arg "infra" pod.infra) + (possibly-unset-arg "infra-command" pod.infra-command) + (possibly-unset-arg "infra-image" pod.infra-image) + (possibly-unset-arg "ip" pod.ip) + (possibly-unset-arg "mac-address" pod.mac-address) + (possibly-unset-arg "network" pod.network) + (possibly-unset-arg "network-alias" pod.network-alias) + (possibly-unset-arg "no-hosts" pod.no-hosts) + (list-to-args "publish" pod.publish) + (list-to-args "share" pod.share) + ]; + + script = "podman pod start ${escapeShellArg name}"; + preStop = "podman pod stop ${escapeShellArg name}"; + # `podman generate systemd` generates a second stop after the + # first; not sure why but clearly it's recommended. + postStop = preStop; + + serviceConfig = rec { + Type = "forking"; + TimeoutStopSec = 70; + Restart = "on-failure"; + PIDFile = "/run/podman/pods/${name}.pid"; + }; + }; + +in { + options.virtualisation.pods = mkOption { + type = with types; + attrsOf (submodule { + options = { + added-hosts = mkOption { + type = listOf str; + default = [ ]; + description = + "Additional hosts to add to /etc/hosts for each container."; + example = literalExample '' + [ "database:10.0.0.1" ] + ''; + }; + + cgroup-parent = mkOption { + type = nullOr str; + default = null; + description = + "The cgroups path under which the pod cgroup will be created."; + }; + + dns = mkOption { + type = listOf str; + default = [ ]; + description = "The dns servers to set in /etc/resolv.conf."; + }; + + dns-opt = mkOption { + type = listOf str; + default = [ ]; + description = "dns options to set in /etc/resolv.conf."; + }; + + dns-search = mkOption { + type = listOf str; + default = [ ]; + description = "Search domains to set in /etc/resolv.conf."; + }; + + hostname = mkOption { + type = nullOr str; + default = null; + description = "The pod hostname."; + }; + + infra = mkOption { + type = nullOr bool; + default = null; + description = "Whether to create the infra container for the pod."; + }; + + infra-command = mkOption { + type = nullOr str; + default = null; + description = "The command to run in the infra container."; + }; + + infra-image = mkOption { + type = nullOr str; + default = null; + description = "The image to use for the infra container."; + }; + + ip = mkOption { + type = nullOr str; + default = null; + description = "A static IP address for the pod network."; + }; + + # TODO: set up label file stuff. + # + # labels = mkOption {}; + + mac-address = mkOption { + type = nullOr str; + default = null; + description = "A static mac address for the pod network."; + }; + + network = mkOption { + type = nullOr str; + default = null; + description = "Network configuration for the pod."; + }; + + network-alias = mkOption { + type = nullOr str; + default = null; + description = "DNS alias for the pod."; + }; + + no-hosts = mkOption { + type = nullOr bool; + default = null; + description = "Whether to disable /etc/hosts creation for the pod."; + }; + + publish = mkOption { + type = listOf str; + default = [ ]; + description = "List of ports to publish from the pod."; + }; + + share = mkOption { + type = listOf str; + default = [ ]; + description = "List of kernel namespaces to share."; + }; + + containers = options.virtualisation.oci-containers.containers; + }; + }); + default = { }; + description = "Podman pods to run as systemd services."; + }; + + config = let + # Merge a list of attribute sets together + # + # TODO: See if there's a generic version for this somewhere in the + # pkgs lib? + mergeAttrs = attrList: foldr (a: b: a // b) { } attrList; + + # Create services for all defined pods + pod-services = mapAttrs' (n: v: nameValuePair "pod-${n}" (mkPod n v)) cfg; + + # Override the systemd-specific settings of containers defined in + # pods. + # + # I.e., make a systemd unit dependency on the pod service. + pod-container-services = mergeAttrs (mapAttrsToList (pname: pod: + mapAttrs' (cname: container: + nameValuePair "podman-${pname}-${cname}" rec { + after = [ "pod-${pname}.service" ]; + requires = after; + }) pod.containers) cfg); + + # Override the oci-container settings for containers defined in pods. + # + # I.e., set the --pod=podname setting, and update the dependsOn so + # it points to containers in the same pod. + podifyContainer = container: podname: + container // { + dependsOn = + map (dependency: "${podname}-${dependency}") container.dependsOn; + extraOptions = container.extraOptions ++ [ "--pod=${podname}" ]; + }; + + in lib.mkIf (cfg != { }) { + virtualisation.podman.enable = true; + virtualisation.oci-containers.backend = "podman"; + + systemd.services = pod-services // pod-container-services; + + virtualisation.oci-containers.containers = mergeAttrs (mapAttrsToList + (pname: pod: + mapAttrs' (cname: container: + nameValuePair "${pname}-${cname}" (podifyContainer container pname)) + pod.containers) cfg); + }; +}