|
| 1 | +{ |
| 2 | + config, |
| 3 | + lib, |
| 4 | + pkgs, |
| 5 | + utils, |
| 6 | + ... |
| 7 | +}: |
| 8 | + |
| 9 | +let |
| 10 | + inherit (lib) |
| 11 | + any |
| 12 | + attrValues |
| 13 | + concatStringsSep |
| 14 | + escapeShellArg |
| 15 | + hasInfix |
| 16 | + hasSuffix |
| 17 | + literalExpression |
| 18 | + mapAttrs' |
| 19 | + mkEnableOption |
| 20 | + mkIf |
| 21 | + mkOption |
| 22 | + mkPackageOption |
| 23 | + mkRenamedOptionModule |
| 24 | + nameValuePair |
| 25 | + optionalAttrs |
| 26 | + optionals |
| 27 | + teams |
| 28 | + types |
| 29 | + ; |
| 30 | + |
| 31 | + inherit (utils) |
| 32 | + escapeSystemdPath |
| 33 | + ; |
| 34 | + |
| 35 | + cfg = config.services.forgejo.runner; |
| 36 | + |
| 37 | + settingsFormat = pkgs.formats.yaml { }; |
| 38 | + |
| 39 | + # Empty label strings result in upstream default labels, which require docker. |
| 40 | + hasDockerScheme = |
| 41 | + instance: instance.labels == [ ] || any (label: hasInfix ":docker:" label) instance.labels; |
| 42 | + wantsContainerRuntime = any hasDockerScheme (attrValues cfg.instances); |
| 43 | + |
| 44 | + hasHostScheme = instance: any (label: hasSuffix ":host" label) instance.labels; |
| 45 | + |
| 46 | + hasDocker = config.virtualisation.docker.enable; |
| 47 | + hasPodman = config.virtualisation.podman.enable; |
| 48 | +in |
| 49 | +{ |
| 50 | + meta.maintainers = teams.forgejo.members; |
| 51 | + |
| 52 | + imports = [ |
| 53 | + (mkRenamedOptionModule [ "services" "forgejo-runner" ] [ "services" "forgejo" "runner" ]) |
| 54 | + ]; |
| 55 | + |
| 56 | + options.services.forgejo.runner = with types; { |
| 57 | + package = mkPackageOption pkgs "forgejo-runner" { }; |
| 58 | + |
| 59 | + instances = mkOption { |
| 60 | + default = { }; |
| 61 | + description = '' |
| 62 | + Forgejo Actions Runner instances. |
| 63 | + ''; |
| 64 | + type = attrsOf ( |
| 65 | + submodule ( |
| 66 | + { name, ... }: |
| 67 | + { |
| 68 | + options = { |
| 69 | + enable = mkEnableOption "Forgejo Actions Runner instance"; |
| 70 | + |
| 71 | + name = mkOption { |
| 72 | + type = str; |
| 73 | + example = literalExpression "config.networking.hostName"; |
| 74 | + description = '' |
| 75 | + The name identifying the runner instance towards the Forgejo instance. |
| 76 | + ''; |
| 77 | + default = name; |
| 78 | + }; |
| 79 | + |
| 80 | + url = mkOption { |
| 81 | + type = str; |
| 82 | + example = "https://forge.example.com"; |
| 83 | + description = '' |
| 84 | + Base URL of your Forgejo instance. |
| 85 | + ''; |
| 86 | + }; |
| 87 | + |
| 88 | + tokenFile = mkOption { |
| 89 | + type = nullOr (either str path); |
| 90 | + default = null; |
| 91 | + description = '' |
| 92 | + Path to a file containing only the token that will be used to register |
| 93 | + with the the configured Forgejo instance. |
| 94 | + ''; |
| 95 | + }; |
| 96 | + |
| 97 | + labels = mkOption { |
| 98 | + type = listOf str; |
| 99 | + example = literalExpression '' |
| 100 | + [ |
| 101 | + # provide a debian base with nodejs for actions |
| 102 | + "debian-latest:docker://node:18-bullseye" |
| 103 | + # fake the ubuntu name, because node provides no ubuntu builds |
| 104 | + "ubuntu-latest:docker://node:18-bullseye" |
| 105 | + # provide native execution on the host |
| 106 | + #"native:host" |
| 107 | + ] |
| 108 | + ''; |
| 109 | + description = '' |
| 110 | + Labels used to map jobs to their runtime environment. Changing these |
| 111 | + labels currently requires a new registration token. |
| 112 | +
|
| 113 | + Many common actions require bash, git and nodejs, as well as a filesystem |
| 114 | + that follows the filesystem hierarchy standard. |
| 115 | + ''; |
| 116 | + }; |
| 117 | + |
| 118 | + settings = mkOption { |
| 119 | + description = '' |
| 120 | + Configuration for `forgejo-runner daemon`. |
| 121 | + See <https://code.forgejo.org/forgejo/runner/src/branch/main/internal/pkg/config/config.example.yaml> for an example configuration. |
| 122 | + ''; |
| 123 | + |
| 124 | + type = types.submodule { |
| 125 | + freeformType = settingsFormat.type; |
| 126 | + }; |
| 127 | + |
| 128 | + default = { }; |
| 129 | + }; |
| 130 | + |
| 131 | + hostPackages = mkOption { |
| 132 | + type = listOf package; |
| 133 | + default = with pkgs; [ |
| 134 | + bash |
| 135 | + coreutils |
| 136 | + curl |
| 137 | + gawk |
| 138 | + gitMinimal |
| 139 | + gnused |
| 140 | + nodejs |
| 141 | + wget |
| 142 | + ]; |
| 143 | + defaultText = literalExpression '' |
| 144 | + with pkgs; [ |
| 145 | + bash |
| 146 | + coreutils |
| 147 | + curl |
| 148 | + gawk |
| 149 | + gitMinimal |
| 150 | + gnused |
| 151 | + nodejs |
| 152 | + wget |
| 153 | + ] |
| 154 | + ''; |
| 155 | + description = '' |
| 156 | + List of packages that are available to actions, when the runner is configured |
| 157 | + with a host execution label. |
| 158 | + ''; |
| 159 | + }; |
| 160 | + }; |
| 161 | + } |
| 162 | + ) |
| 163 | + ); |
| 164 | + }; |
| 165 | + }; |
| 166 | + |
| 167 | + config = mkIf (cfg.instances != { }) { |
| 168 | + assertions = [ |
| 169 | + { |
| 170 | + assertion = wantsContainerRuntime -> hasDocker || hasPodman; |
| 171 | + message = "Label configuration on forgejo.runner instance requires either docker or podman."; |
| 172 | + } |
| 173 | + ]; |
| 174 | + |
| 175 | + systemd.services = |
| 176 | + let |
| 177 | + mkRunnerInstance = |
| 178 | + _: instance: |
| 179 | + let |
| 180 | + escapedName = escapeSystemdPath instance.name; |
| 181 | + wantsContainer = hasDockerScheme instance; |
| 182 | + wantsHost = hasHostScheme instance; |
| 183 | + wantsDocker = wantsContainer && hasDocker; |
| 184 | + wantsPodman = wantsContainer && hasPodman; |
| 185 | + configFile = settingsFormat.generate "forgejo-runner-${escapedName}.yaml" instance.settings; |
| 186 | + in |
| 187 | + nameValuePair "forgejo-runner@${escapedName}" { |
| 188 | + overrideStrategy = "asDropin"; |
| 189 | + inherit (instance) enable; |
| 190 | + wants = [ |
| 191 | + "network-online.target" |
| 192 | + ] |
| 193 | + ++ optionals wantsDocker [ "docker.service" ] |
| 194 | + ++ optionals wantsPodman [ "podman.service" ]; |
| 195 | + after = [ |
| 196 | + "network-online.target" |
| 197 | + ] |
| 198 | + ++ optionals wantsDocker [ "docker.service" ] |
| 199 | + ++ optionals wantsPodman [ "podman.service" ]; |
| 200 | + wantedBy = [ "multi-user.target" ]; |
| 201 | + |
| 202 | + environment = optionalAttrs wantsPodman { |
| 203 | + DOCKER_HOST = "unix:///run/podman/podman.sock"; |
| 204 | + }; |
| 205 | + |
| 206 | + path = [ pkgs.coreutils ] ++ lib.optionals wantsHost instance.hostPackages; |
| 207 | + |
| 208 | + serviceConfig = { |
| 209 | + MemoryDenyWriteExecute = !wantsHost; |
| 210 | + |
| 211 | + LoadCredential = [ "TOKEN:${instance.tokenFile}" ]; |
| 212 | + |
| 213 | + ExecStartPre = [ |
| 214 | + (lib.getExe ( |
| 215 | + pkgs.writeShellApplication { |
| 216 | + name = "forgejo-register-runner-${escapedName}"; |
| 217 | + text = '' |
| 218 | + INSTANCE_DIR="$STATE_DIRECTORY" |
| 219 | + mkdir -vp "$INSTANCE_DIR" |
| 220 | + cd "$INSTANCE_DIR" |
| 221 | +
|
| 222 | + LABELS_FILE="$INSTANCE_DIR/.labels.sha256" |
| 223 | + LABELS_WANTED="$(echo ${escapeShellArg (concatStringsSep "\n" instance.labels)} | sort)" |
| 224 | + LABELS_WANTED_HASH="$(printf '%s' "$LABELS_WANTED" | sha256sum | cut -d' ' -f1)" |
| 225 | + LABELS_CURRENT_HASH="$(cat "$LABELS_FILE" 2>/dev/null || true)" |
| 226 | +
|
| 227 | + if [ ! -e "$INSTANCE_DIR/.runner" ] || [ "$LABELS_WANTED_HASH" != "$LABELS_CURRENT_HASH" ]; then |
| 228 | + rm -vf "$INSTANCE_DIR/.runner" || true |
| 229 | +
|
| 230 | + ${cfg.package}/bin/forgejo-runner register \ |
| 231 | + --no-interactive \ |
| 232 | + --instance ${escapeShellArg instance.url} \ |
| 233 | + --token "$(cat "$CREDENTIALS_DIRECTORY/TOKEN")" \ |
| 234 | + --name ${escapeShellArg instance.name} \ |
| 235 | + --labels ${escapeShellArg (concatStringsSep "," instance.labels)} \ |
| 236 | + --config ${configFile} |
| 237 | +
|
| 238 | + printf '%s' "$LABELS_WANTED_HASH" > "$LABELS_FILE" |
| 239 | + fi |
| 240 | + ''; |
| 241 | + } |
| 242 | + )) |
| 243 | + ]; |
| 244 | + ExecStart = lib.mkForce "${cfg.package}/bin/forgejo-runner daemon --config ${configFile}"; |
| 245 | + SupplementaryGroups = optionals wantsDocker [ "docker" ] ++ optionals wantsPodman [ "podman" ]; |
| 246 | + ExecPaths = lib.optionals wantsHost [ "/var/lib/forgejo-runner/${escapedName}" ]; |
| 247 | + }; |
| 248 | + }; |
| 249 | + in |
| 250 | + { |
| 251 | + "forgejo-runner@" = { |
| 252 | + description = "Forgejo Actions Runner (%I)"; |
| 253 | + |
| 254 | + environment = { |
| 255 | + HOME = "/var/lib/forgejo-runner/%i"; |
| 256 | + }; |
| 257 | + |
| 258 | + serviceConfig = { |
| 259 | + DynamicUser = true; |
| 260 | + User = "forgejo-runner-%i"; |
| 261 | + StateDirectory = "forgejo-runner/%i"; |
| 262 | + WorkingDirectory = "/var/lib/forgejo-runner/%i"; |
| 263 | + |
| 264 | + Restart = "on-failure"; |
| 265 | + RestartSec = 2; |
| 266 | + |
| 267 | + AmbientCapabilities = ""; |
| 268 | + CapabilityBoundingSet = ""; |
| 269 | + LockPersonality = true; |
| 270 | + NoNewPrivileges = true; |
| 271 | + PrivateDevices = true; |
| 272 | + PrivateTmp = true; |
| 273 | + ProcSubset = "pid"; |
| 274 | + ProtectClock = true; |
| 275 | + ProtectControlGroups = true; |
| 276 | + ProtectHome = true; |
| 277 | + ProtectHostname = true; |
| 278 | + ProtectKernelLogs = true; |
| 279 | + ProtectKernelModules = true; |
| 280 | + ProtectKernelTunables = true; |
| 281 | + ProtectProc = "invisible"; |
| 282 | + ProtectSystem = "strict"; |
| 283 | + RemoveIPC = true; |
| 284 | + RestrictAddressFamilies = [ |
| 285 | + "AF_INET" |
| 286 | + "AF_INET6" |
| 287 | + "AF_UNIX" |
| 288 | + ]; |
| 289 | + RestrictNamespaces = true; |
| 290 | + RestrictRealtime = true; |
| 291 | + RestrictSUIDSGID = true; |
| 292 | + SystemCallArchitectures = "native"; |
| 293 | + SystemCallFilter = [ "@system-service" ]; |
| 294 | + UMask = "0077"; |
| 295 | + }; |
| 296 | + }; |
| 297 | + } |
| 298 | + // mapAttrs' mkRunnerInstance cfg.instances; |
| 299 | + }; |
| 300 | +} |
0 commit comments