From 50619b3cce419c37b911e0e255d12f1a82a917dc Mon Sep 17 00:00:00 2001 From: Kai Norman Clasen Date: Mon, 23 Sep 2024 18:25:38 +0200 Subject: [PATCH 1/2] internal: apply nixfmt --- container.nix | 77 +++++++++++++++++++++++++++----------------- flake.nix | 14 ++++---- network.nix | 75 ++++++++++++++++++++++++++---------------- nixos-module.nix | 84 +++++++++++++++++++++++++++--------------------- utils.nix | 33 ++++++++++++------- 5 files changed, 172 insertions(+), 111 deletions(-) diff --git a/container.nix b/container.nix index 30d7e0c..e76d933 100644 --- a/container.nix +++ b/container.nix @@ -1,5 +1,10 @@ { quadletUtils }: -{ config, name, lib, ... }: +{ + config, + name, + lib, + ... +}: with lib; @@ -30,7 +35,12 @@ let }; autoUpdate = quadletUtils.mkOption { - type = types.nullOr (types.enum [ "registry" "local" ]); + type = types.nullOr ( + types.enum [ + "registry" + "local" + ] + ); default = null; example = "registry"; description = "--label \"io.containers.autoupdate=...\""; @@ -56,7 +66,9 @@ let environments = quadletUtils.mkOption { type = types.attrs; default = { }; - example = { foo = "bar"; }; + example = { + foo = "bar"; + }; description = "--env"; property = "Environment"; }; @@ -91,7 +103,7 @@ let description = "--expose"; property = "ExposeHostPort"; }; - + group = quadletUtils.mkOption { type = types.nullOr types.str; default = null; @@ -210,7 +222,7 @@ let description = "--ip"; property = "IP"; }; - + ip6 = quadletUtils.mkOption { type = types.nullOr types.str; default = null; @@ -242,7 +254,7 @@ let description = "--mount"; property = "Mount"; }; - + networks = quadletUtils.mkOption { type = types.listOf types.str; default = [ ]; @@ -376,7 +388,9 @@ let sysctl = quadletUtils.mkOption { type = types.attrs; default = { }; - example = { name = "value"; }; + example = { + name = "value"; + }; description = "--sysctl"; property = "Sysctl"; }; @@ -441,7 +455,8 @@ let Restart = "always"; TimeoutStartSec = 900; }; -in { +in +{ options = { autoStart = mkOption { type = types.bool; @@ -452,7 +467,7 @@ in { containerConfig = containerOpts; unitConfig = mkOption { type = types.attrs; - default = {}; + default = { }; }; serviceConfig = mkOption { type = types.attrs; @@ -464,26 +479,28 @@ in { _configText = mkOption { internal = true; }; }; - config = let - configRelPath = "containers/systemd/${name}.container"; - containerName = if config.containerConfig.name != null - then config.containerConfig.name - else name; - containerConfig = config.containerConfig // { name = containerName; }; - unitConfig = { - Unit = { - Description = "Podman container ${name}"; - } // config.unitConfig; - Install = { - WantedBy = if config.autoStart then [ "default.target" ] else []; + config = + let + configRelPath = "containers/systemd/${name}.container"; + containerName = if config.containerConfig.name != null then config.containerConfig.name else name; + containerConfig = config.containerConfig // { + name = containerName; }; - Container = quadletUtils.configToProperties containerConfig containerOpts; - Service = serviceConfigDefault // config.serviceConfig; - }; - unitConfigText = quadletUtils.unitConfigToText unitConfig; - in { - _configName = "${name}.container"; - _unitName = "${name}.service"; - _configText = quadletUtils.unitConfigToText unitConfig; - }; + unitConfig = { + Unit = { + Description = "Podman container ${name}"; + } // config.unitConfig; + Install = { + WantedBy = if config.autoStart then [ "default.target" ] else [ ]; + }; + Container = quadletUtils.configToProperties containerConfig containerOpts; + Service = serviceConfigDefault // config.serviceConfig; + }; + unitConfigText = quadletUtils.unitConfigToText unitConfig; + in + { + _configName = "${name}.container"; + _unitName = "${name}.service"; + _configText = quadletUtils.unitConfigToText unitConfig; + }; } diff --git a/flake.nix b/flake.nix index 617091d..d0dd655 100644 --- a/flake.nix +++ b/flake.nix @@ -5,10 +5,12 @@ nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; }; - outputs = { nixpkgs, ... }: - let - libUtils = import "${nixpkgs}/nixos/lib/utils.nix"; - in { - nixosModules.quadlet = import ./nixos-module.nix { inherit libUtils; }; - }; + outputs = + { nixpkgs, ... }: + let + libUtils = import "${nixpkgs}/nixos/lib/utils.nix"; + in + { + nixosModules.quadlet = import ./nixos-module.nix { inherit libUtils; }; + }; } diff --git a/network.nix b/network.nix index 893e6c6..c6c1392 100644 --- a/network.nix +++ b/network.nix @@ -1,5 +1,10 @@ { quadletUtils, pkgs }: -{ config, name, lib, ... }: +{ + config, + name, + lib, + ... +}: with lib; @@ -13,7 +18,13 @@ let }; driver = quadletUtils.mkOption { - type = types.nullOr (types.enum [ "bridge" "macvlan" "ipvlan" ]); + type = types.nullOr ( + types.enum [ + "bridge" + "macvlan" + "ipvlan" + ] + ); default = null; example = "bridge"; description = "--driver"; @@ -36,7 +47,13 @@ let }; ipamDriver = quadletUtils.mkOption { - type = types.nullOr (types.enum [ "host-local" "dhcp" "none" ]); + type = types.nullOr ( + types.enum [ + "host-local" + "dhcp" + "none" + ] + ); default = null; example = "dhcp"; description = "--ipam-driver"; @@ -98,7 +115,8 @@ let property = "Subnet"; }; }; -in { +in +{ options = { autoStart = mkOption { type = types.bool; @@ -109,11 +127,11 @@ in { networkConfig = networkOpts; unitConfig = mkOption { type = types.attrs; - default = {}; + default = { }; }; serviceConfig = mkOption { type = types.attrs; - default = {}; + default = { }; }; _configName = mkOption { internal = true; }; @@ -121,27 +139,28 @@ in { _configText = mkOption { internal = true; }; }; - config = let - configRelPath = "containers/systemd/${name}.network"; - networkName = if config.networkConfig.name != null - then config.networkConfig.name - else "systemd-${name}"; - networkConfig = config.networkConfig; - unitConfig = { - Unit = { - Description = "Podman network ${name}"; - } // config.unitConfig; - Install = { - WantedBy = if config.autoStart then [ "default.target" ] else []; + config = + let + configRelPath = "containers/systemd/${name}.network"; + networkName = + if config.networkConfig.name != null then config.networkConfig.name else "systemd-${name}"; + networkConfig = config.networkConfig; + unitConfig = { + Unit = { + Description = "Podman network ${name}"; + } // config.unitConfig; + Install = { + WantedBy = if config.autoStart then [ "default.target" ] else [ ]; + }; + Network = quadletUtils.configToProperties networkConfig networkOpts; + Service = { + ExecStop = "${pkgs.podman}/bin/podman network rm ${networkName}"; + } // config.serviceConfig; }; - Network = quadletUtils.configToProperties networkConfig networkOpts; - Service = { - ExecStop = "${pkgs.podman}/bin/podman network rm ${networkName}"; - } // config.serviceConfig; - }; - in { - _configName = "${name}.network"; - _unitName = "${name}-network.service"; - _configText = quadletUtils.unitConfigToText unitConfig; - }; + in + { + _configName = "${name}.network"; + _unitName = "${name}-network.service"; + _configText = quadletUtils.unitConfigToText unitConfig; + }; } diff --git a/nixos-module.nix b/nixos-module.nix index 09e1621..7e9807a 100644 --- a/nixos-module.nix +++ b/nixos-module.nix @@ -1,5 +1,10 @@ { libUtils }: -{ config, lib, pkgs, ... }@attrs: +{ + config, + lib, + pkgs, + ... +}@attrs: with lib; @@ -7,16 +12,18 @@ let cfg = config.virtualisation.quadlet; quadletUtils = import ./utils.nix { inherit lib; - systemdLib = (libUtils { - inherit lib config pkgs; - }).systemdUtils.lib; + systemdLib = + (libUtils { + inherit lib config pkgs; + }).systemdUtils.lib; }; # TODO: replace with lib.mergeAttrsList once stable. - mergeAttrsList = foldl mergeAttrs {}; + mergeAttrsList = foldl mergeAttrs { }; - containerOpts = types.submodule (import ./container.nix { inherit quadletUtils; } ); - networkOpts = types.submodule (import ./network.nix { inherit quadletUtils pkgs; } ); -in { + containerOpts = types.submodule (import ./container.nix { inherit quadletUtils; }); + networkOpts = types.submodule (import ./network.nix { inherit quadletUtils pkgs; }); +in +{ options = { virtualisation.quadlet = { containers = mkOption { @@ -31,33 +38,38 @@ in { }; }; - config = let - allObjects = (attrValues cfg.containers) ++ (attrValues cfg.networks); - in { - virtualisation.podman.enable = true; - environment.etc = mergeAttrsList ( - map (p: { - "containers/systemd/${p._configName}" = { - text = p._configText; - mode = "0600"; - }; - }) allObjects); - # The symlinks are not necessary for the services to be honored by systemd, - # but necessary for NixOS activation process to pick them up for updates. - systemd.packages = [ - (pkgs.linkFarm "quadlet-service-symlinks" ( + config = + let + allObjects = (attrValues cfg.containers) ++ (attrValues cfg.networks); + in + { + virtualisation.podman.enable = true; + environment.etc = mergeAttrsList ( map (p: { - name = "etc/systemd/system/${p._unitName}"; - path = "/run/systemd/generator/${p._unitName}"; - }) allObjects)) - ]; - # Inject X-RestartIfChanged=${hash} for NixOS to detect changes. - systemd.units = mergeAttrsList ( - map (p: { - ${p._unitName} = { - overrideStrategy = "asDropin"; - text = "[Unit]\nX-RestartIfChanged=${builtins.hashString "sha256" p._configText}"; - }; - }) allObjects); - }; + "containers/systemd/${p._configName}" = { + text = p._configText; + mode = "0600"; + }; + }) allObjects + ); + # The symlinks are not necessary for the services to be honored by systemd, + # but necessary for NixOS activation process to pick them up for updates. + systemd.packages = [ + (pkgs.linkFarm "quadlet-service-symlinks" ( + map (p: { + name = "etc/systemd/system/${p._unitName}"; + path = "/run/systemd/generator/${p._unitName}"; + }) allObjects + )) + ]; + # Inject X-RestartIfChanged=${hash} for NixOS to detect changes. + systemd.units = mergeAttrsList ( + map (p: { + ${p._unitName} = { + overrideStrategy = "asDropin"; + text = "[Unit]\nX-RestartIfChanged=${builtins.hashString "sha256" p._configText}"; + }; + }) allObjects + ); + }; } diff --git a/utils.nix b/utils.nix index 687e756..37f2cac 100644 --- a/utils.nix +++ b/utils.nix @@ -1,19 +1,30 @@ { lib, systemdLib }: let - attrsToList = attrs: if builtins.isAttrs attrs - then lib.mapAttrsToList (name: value: "${name}=${toString value}") attrs - else attrs; -in { - mkOption = { property, ... }@attrs: - (lib.mkOption (lib.filterAttrs (name: _: name != "property") attrs)) // { + attrsToList = + attrs: + if builtins.isAttrs attrs then + lib.mapAttrsToList (name: value: "${name}=${toString value}") attrs + else + attrs; +in +{ + mkOption = + { property, ... }@attrs: + (lib.mkOption (lib.filterAttrs (name: _: name != "property") attrs)) + // { inherit property; }; - configToProperties = config: options: lib.mapAttrs' - (name: value: lib.nameValuePair options.${name}.property (attrsToList value)) - (lib.filterAttrs (_: value: value != null) config); + configToProperties = + config: options: + lib.mapAttrs' (name: value: lib.nameValuePair options.${name}.property (attrsToList value)) ( + lib.filterAttrs (_: value: value != null) config + ); - unitConfigToText = unitConfig: builtins.concatStringsSep "\n\n" ( - lib.mapAttrsToList (name: section: "[${name}]\n${systemdLib.attrsToSection section}") unitConfig); + unitConfigToText = + unitConfig: + builtins.concatStringsSep "\n\n" ( + lib.mapAttrsToList (name: section: "[${name}]\n${systemdLib.attrsToSection section}") unitConfig + ); } From 5970e7be88ec6d063a79c7669a68918c4827caa0 Mon Sep 17 00:00:00 2001 From: Kai Norman Clasen Date: Sat, 7 Sep 2024 09:05:16 +0200 Subject: [PATCH 2/2] Add `pod` support Include some basic tests to ensure that the container + pod configurations is valid. Add test that ensures that the names for containers/pods/networks is unique to avoid cryptic error messages. --- README.md | 6 +- container.nix | 15 ++-- network.nix | 10 ++- nixos-module.nix | 34 +++++++- pod.nix | 219 +++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 271 insertions(+), 13 deletions(-) create mode 100644 pod.nix diff --git a/README.md b/README.md index a7b184f..05a2313 100644 --- a/README.md +++ b/README.md @@ -46,13 +46,17 @@ Compared to alternatives like [`virtualisation.oci-containers`](https://github.c containers = { nginx.containerConfig.image = "docker.io/library/nginx:latest"; nginx.containerConfig.networks = [ "host" "internal.network" ]; + nginx.containerConfig.pod = "nginx-pod.pod"; nginx.serviceConfig.TimeoutStartSec = "60"; }; networks = { internal.networkConfig.subnets = [ "10.0.123.1/24" ]; }; + pods = { + nginx-pod = { }; + }; }; } ``` -See [`container.nix`](./container.nix) and [`network.nix`](./network.nix) for all options. +See [`container.nix`](./container.nix), [`network.nix`](./network.nix), and [`pod.nix`](./pod.nix) for all options. diff --git a/container.nix b/container.nix index e76d933..6135ba1 100644 --- a/container.nix +++ b/container.nix @@ -5,9 +5,7 @@ lib, ... }: - with lib; - let containerOpts = { addCapabilities = quadletUtils.mkOption { @@ -259,7 +257,7 @@ let type = types.listOf types.str; default = [ ]; example = [ "host" ]; - description = "--network"; + description = "--net"; property = "Network"; }; @@ -285,6 +283,13 @@ let property = "Notify"; }; + pod = quadletUtils.mkOption { + type = types.nullOr types.str; + default = null; + description = "The full name of the pod to link to."; + property = "Pod"; + }; + podmanArgs = quadletUtils.mkOption { type = types.listOf types.str; default = [ ]; @@ -474,6 +479,7 @@ in default = serviceConfigDefault; }; + _name = mkOption { internal = true; }; _configName = mkOption { internal = true; }; _unitName = mkOption { internal = true; }; _configText = mkOption { internal = true; }; @@ -481,7 +487,6 @@ in config = let - configRelPath = "containers/systemd/${name}.container"; containerName = if config.containerConfig.name != null then config.containerConfig.name else name; containerConfig = config.containerConfig // { name = containerName; @@ -496,9 +501,9 @@ in Container = quadletUtils.configToProperties containerConfig containerOpts; Service = serviceConfigDefault // config.serviceConfig; }; - unitConfigText = quadletUtils.unitConfigToText unitConfig; in { + _name = containerName; _configName = "${name}.container"; _unitName = "${name}.service"; _configText = quadletUtils.unitConfigToText unitConfig; diff --git a/network.nix b/network.nix index c6c1392..10997c7 100644 --- a/network.nix +++ b/network.nix @@ -1,13 +1,14 @@ -{ quadletUtils, pkgs }: +{ + quadletUtils, + pkgs, +}: { config, name, lib, ... }: - with lib; - let networkOpts = { disableDns = quadletUtils.mkOption { @@ -135,13 +136,13 @@ in }; _configName = mkOption { internal = true; }; + _name = mkOption { internal = true; }; _unitName = mkOption { internal = true; }; _configText = mkOption { internal = true; }; }; config = let - configRelPath = "containers/systemd/${name}.network"; networkName = if config.networkConfig.name != null then config.networkConfig.name else "systemd-${name}"; networkConfig = config.networkConfig; @@ -159,6 +160,7 @@ in }; in { + _name = networkName; _configName = "${name}.network"; _unitName = "${name}-network.service"; _configText = quadletUtils.unitConfigToText unitConfig; diff --git a/nixos-module.nix b/nixos-module.nix index 7e9807a..bf27e43 100644 --- a/nixos-module.nix +++ b/nixos-module.nix @@ -5,9 +5,7 @@ pkgs, ... }@attrs: - with lib; - let cfg = config.virtualisation.quadlet; quadletUtils = import ./utils.nix { @@ -22,6 +20,7 @@ let containerOpts = types.submodule (import ./container.nix { inherit quadletUtils; }); networkOpts = types.submodule (import ./network.nix { inherit quadletUtils pkgs; }); + podOpts = types.submodule (import ./pod.nix { inherit quadletUtils; }); in { options = { @@ -35,15 +34,44 @@ in type = types.attrsOf networkOpts; default = { }; }; + + pods = mkOption { + type = types.attrsOf podOpts; + default = { }; + }; }; }; config = let - allObjects = (attrValues cfg.containers) ++ (attrValues cfg.networks); + containerAndPodObjects = (attrValues cfg.containers) ++ (attrValues cfg.pods); + allObjects = (attrValues cfg.containers) ++ (attrValues cfg.networks) ++ (attrValues cfg.pods); in { virtualisation.podman.enable = true; + assertions = + let + count_occurances = + str_list: + lib.lists.foldl' ( + acc: el: if acc ? ${el} then acc // { ${el} = acc.${el} + 1; } else acc // { ${el} = 1; } + ) { } str_list; + find_duplicate_elements = + str_l: lib.attrsets.attrNames (lib.attrsets.filterAttrs (_: v: v > 1) (count_occurances str_l)); + # assuming that only `name` defines the final name without the suffix! + # Containers and pods cannot have the same name! + duplicate_elements = find_duplicate_elements (map (x: x._name) containerAndPodObjects); + in + [ + { + assertion = duplicate_elements == [ ]; + message = '' + The container/pod names should be unique! + See: https://docs.podman.io/en/latest/markdown/podman-systemd.unit.5.html#podname + The following names are not unique: ${lib.strings.concatStringsSep " " duplicate_elements} + ''; + } + ]; environment.etc = mergeAttrsList ( map (p: { "containers/systemd/${p._configName}" = { diff --git a/pod.nix b/pod.nix new file mode 100644 index 0000000..5d5f7fa --- /dev/null +++ b/pod.nix @@ -0,0 +1,219 @@ +{ quadletUtils }: +{ + config, + name, + lib, + ... +}: +with lib; +let + podOpts = { + name = quadletUtils.mkOption { + type = types.nullOr types.str; + default = null; + example = "name"; + description = "--name"; + property = "PodName"; + }; + + addHosts = quadletUtils.mkOption { + type = types.listOf types.str; + default = [ ]; + example = [ "hostname:192.168.10.11" ]; + description = "--add-host"; + property = "AddHost"; + }; + + dns = quadletUtils.mkOption { + type = types.listOf types.str; + default = [ ]; + example = [ "192.168.55.1" ]; + description = "--dns"; + property = "DNS"; + }; + + dnsOptions = quadletUtils.mkOption { + type = types.listOf types.str; + default = [ ]; + example = [ "ndots:1" ]; + description = "--dns-option"; + property = "DNSOption"; + }; + + dnsSearches = quadletUtils.mkOption { + type = types.listOf types.str; + default = [ ]; + example = [ "foo.com" ]; + description = "--dns-search"; + property = "DNSSearch"; + }; + + gidMaps = quadletUtils.mkOption { + type = types.listOf types.str; + default = [ ]; + example = [ "0:10000:10" ]; + description = "--gidmap"; + property = "GIDMap"; + }; + + # Not recommended to use by upstream: + # globalArgs = quadletUtils.mkOption { + # type = types.listOf types.str; + # default = [ ]; + # example = [ "--log-level=debug" ]; + # description = ""; + # property = "GlobalArgs"; + # }; + + ip = quadletUtils.mkOption { + type = types.nullOr types.str; + default = null; + example = "192.5.0.1"; + description = "--ip"; + property = "IP"; + }; + + ip6 = quadletUtils.mkOption { + type = types.nullOr types.str; + default = null; + example = "2001:db8::1"; + description = "--ip6"; + property = "IP6"; + }; + + networks = quadletUtils.mkOption { + type = types.listOf types.str; + default = [ ]; + example = [ "host" ]; + description = "--network"; + property = "Network"; + }; + + networkAliases = quadletUtils.mkOption { + type = types.listOf types.str; + default = [ ]; + example = [ "name" ]; + description = "--network-alias"; + property = "NetworkAlias"; + }; + + podmanArgs = quadletUtils.mkOption { + type = types.listOf types.str; + default = [ ]; + example = [ "--cpus=2" ]; + description = "Additional podman arguments"; + property = "PodmanArgs"; + }; + + publishPorts = quadletUtils.mkOption { + type = types.listOf types.str; + default = [ ]; + example = [ "50-59" ]; + description = "--publish"; + property = "PublishPort"; + }; + + serviceName = quadletUtils.mkOption { + type = types.nullOr types.str; + default = null; + example = "service-name"; + description = "Instructs Quadlet to use the provided name."; + property = "ServiceName"; + }; + + subGIDMap = quadletUtils.mkOption { + type = types.nullOr types.str; + default = null; + example = "gtest"; + description = "--subgidname"; + property = "SubGIDMap"; + }; + + subUIDMap = quadletUtils.mkOption { + type = types.nullOr types.str; + default = null; + example = "utest"; + description = "--subuidname"; + property = "SubUIDMap"; + }; + + uidMaps = quadletUtils.mkOption { + type = types.listOf types.str; + default = [ ]; + example = [ "0:10000:10" ]; + description = "--uidmap"; + property = "UIDMap"; + }; + + userns = quadletUtils.mkOption { + type = types.nullOr types.str; + default = null; + example = "keep-id:uid=200,gid=210"; + description = "--userns"; + property = "UserNS"; + }; + + volumes = quadletUtils.mkOption { + type = types.listOf types.str; + default = [ ]; + example = [ ]; + description = "--volume"; + property = "Volume"; + }; + }; +in +{ + options = { + podConfig = podOpts; + + autoStart = mkOption { + type = types.bool; + default = true; + example = true; + description = "When enabled, the pod is automatically started on boot."; + }; + + unitConfig = mkOption { + type = types.attrs; + default = { }; + }; + + serviceConfig = mkOption { + type = types.attrs; + default = { }; + }; + + _name = mkOption { internal = true; }; + _configName = mkOption { internal = true; }; + _unitName = mkOption { internal = true; }; + _configText = mkOption { internal = true; }; + }; + + config = + let + serviceConfigDefault = { + Restart = "always"; + TimeoutStartSec = 900; + }; + podName = if config.podConfig.name != null then config.podConfig.name else name; + podConfig = config.podConfig // { + name = podName; + }; + unitConfig = { + Unit = { + Description = "Podman pod ${name}"; + } // config.unitConfig; + Install = { + WantedBy = if config.autoStart then [ "default.target" ] else [ ]; + }; + Pod = quadletUtils.configToProperties podConfig podOpts; + Service = serviceConfigDefault // config.serviceConfig; + }; + in + { + _name = podName; + _configName = "${name}.pod"; + _unitName = "${name}-pod.service"; + _configText = quadletUtils.unitConfigToText unitConfig; + }; +}