main
  1{
  2  den,
  3  lib,
  4  ...
  5}: {
  6  den.aspects.services.forgejo.runner = {
  7    includes = [den.aspects.services.podman];
  8    settings.host = {
  9      instances = lib.mkOption {
 10        type = lib.types.attrsOf (lib.types.submodule ({name, ...}: {
 11          options = {
 12            name = lib.mkOption {
 13              type = lib.types.str;
 14              default = name;
 15            };
 16            servers = lib.mkOption {
 17              type = lib.types.attrsOf (lib.types.submodule {
 18                options = {
 19                  url = lib.mkOption {
 20                    type = lib.types.str;
 21                  };
 22                  uuid = lib.mkOption {
 23                    type = lib.types.str;
 24                  };
 25                  tokenFileAged = lib.mkOption {
 26                    type = lib.types.path;
 27                  };
 28                  labels = lib.mkOption {
 29                    type = lib.types.listOf lib.types.str;
 30                    description = "Extra labels used for this server.";
 31                    default = [];
 32                  };
 33                };
 34              });
 35              default = {};
 36            };
 37            labels = lib.mkOption {
 38              type = lib.types.listOf lib.types.str;
 39              default = [];
 40            };
 41            cacheProxyPort = lib.mkOption {
 42              type = lib.types.port;
 43              default = 4000;
 44              description = "Port of the internal cache proxy. Must be unique per instance, as each runner binds it on the same host.";
 45            };
 46            resourcesLimit = {
 47              capacity = lib.mkOption {
 48                type = lib.types.ints.positive;
 49                default = 1;
 50                description = "Execute how many tasks concurrently at the same time.";
 51              };
 52              cpus = lib.mkOption {
 53                type = lib.types.ints.positive;
 54                default = 1;
 55                description = "CPU cores usage limit per container";
 56              };
 57              memory = lib.mkOption {
 58                type = lib.types.str;
 59                default = "1g";
 60                description = "Memory usage limit per container";
 61              };
 62            };
 63            extraEnvironments = lib.mkOption {
 64              type = lib.types.attrsOf lib.types.str;
 65              default = {};
 66            };
 67            extraSettings = lib.mkOption {
 68              type = lib.types.attrsOf lib.types.anything;
 69              default = {};
 70            };
 71          };
 72        }));
 73        default = {};
 74      };
 75    };
 76
 77    persist = {
 78      directoies = [
 79        {
 80          directory = "/var/lib/private/forgejo-runner";
 81          user = "nobody";
 82          group = "nogroup";
 83          mode = "0700";
 84        }
 85      ];
 86    };
 87
 88    nixos = {
 89      host,
 90      config,
 91      ...
 92    }: let
 93      cfg = host.settings.services.forgejo.runner;
 94      mkServerTokenSecretName = instance: server: "forgejo-runner-${instance}-${server}-token";
 95      cacheProxyPorts = lib.mapAttrsToList (_: instanceCfg: instanceCfg.cacheProxyPort) cfg.instances;
 96
 97      matchPodmanNetwork =
 98        if config.networking.nftables.enable
 99        then "podman*"
100        else "podman+";
101    in {
102      # Docker runners with cache actions need their "br-*" bridges trusted.
103      # See https://forgejo.org/docs/next/admin/actions/runner-installation/#nixos
104      networking.firewall.trustedInterfaces =
105        if (config.networking.nftables.enable)
106        then ["br-*"]
107        else ["br-+"];
108
109      # The cache proxy must be reachable from job containers on podman bridges.
110      networking.firewall.interfaces."${matchPodmanNetwork}".allowedTCPPorts = cacheProxyPorts;
111
112      services.forgejo-runner.instances =
113        lib.mapAttrs (instance: instanceCfg: {
114          enable = true;
115
116          settings = lib.mkMerge [
117            {
118              runner = {
119                labels = lib.unique (
120                  instanceCfg.labels
121                  ++ (
122                    lib.concatLists
123                    (lib.mapAttrsToList
124                      (_: serverCfg: serverCfg.labels)
125                      instanceCfg.servers)
126                  )
127                );
128                capacity = instanceCfg.resourcesLimit.capacity;
129              };
130              server.connections =
131                lib.mapAttrs (server: serverCfg: {
132                  inherit (serverCfg) url uuid;
133                })
134                instanceCfg.servers;
135              cache = {
136                enabled = true;
137                # See https://forgejo.org/docs/latest/user/actions/advanced-features/#cache
138                # ONLY for podman backend
139                proxy_port = instanceCfg.cacheProxyPort;
140                actions_cache_url_override = lib.mkDefault "http://host.containers.internal:${toString instanceCfg.cacheProxyPort}";
141              };
142              container = {
143                enable_ipv6 = true;
144                options = lib.concatStringsSep " " [
145                  "--cpus=${toString instanceCfg.resourcesLimit.cpus}"
146                  "--memory=${toString instanceCfg.resourcesLimit.memory}"
147                  "--cap-add sys_admin"
148                  "--cap-add mknod"
149                  "--device /dev/fuse"
150                ];
151              };
152            }
153            instanceCfg.extraSettings
154          ];
155
156          secrets = {
157            server.connections =
158              lib.mapAttrs (server: _: {
159                token_url = config.vaultix.secrets.${mkServerTokenSecretName instance server}.path;
160              })
161              instanceCfg.servers;
162          };
163        })
164        cfg.instances;
165
166      vaultix.secrets = lib.mergeAttrsList (
167        lib.mapAttrsToList (instance: instanceCfg:
168          lib.mapAttrs' (
169            server: serverCfg:
170              lib.nameValuePair (mkServerTokenSecretName instance server) {
171                file = serverCfg.tokenFileAged;
172              }
173          )
174          instanceCfg.servers)
175        cfg.instances
176      );
177    };
178  };
179}