at 24.11-pre 8.7 kB view raw
1{ config 2, lib 3, pkgs 4, utils 5, ... 6}: 7 8let 9 inherit (lib) 10 any 11 attrValues 12 concatStringsSep 13 escapeShellArg 14 hasInfix 15 hasSuffix 16 optionalAttrs 17 optionals 18 literalExpression 19 mapAttrs' 20 mkEnableOption 21 mkOption 22 mkPackageOption 23 mkIf 24 nameValuePair 25 types 26 ; 27 28 inherit (utils) 29 escapeSystemdPath 30 ; 31 32 cfg = config.services.gitea-actions-runner; 33 34 settingsFormat = pkgs.formats.yaml { }; 35 36 # Check whether any runner instance label requires a container runtime 37 # Empty label strings result in the upstream defined defaultLabels, which require docker 38 # https://gitea.com/gitea/act_runner/src/tag/v0.1.5/internal/app/cmd/register.go#L93-L98 39 hasDockerScheme = instance: 40 instance.labels == [] || any (label: hasInfix ":docker:" label) instance.labels; 41 wantsContainerRuntime = any hasDockerScheme (attrValues cfg.instances); 42 43 hasHostScheme = instance: any (label: hasSuffix ":host" label) instance.labels; 44 45 # provide shorthands for whether container runtimes are enabled 46 hasDocker = config.virtualisation.docker.enable; 47 hasPodman = config.virtualisation.podman.enable; 48 49 tokenXorTokenFile = instance: 50 (instance.token == null && instance.tokenFile != null) || 51 (instance.token != null && instance.tokenFile == null); 52in 53{ 54 meta.maintainers = with lib.maintainers; [ 55 hexa 56 ]; 57 58 options.services.gitea-actions-runner = with types; { 59 package = mkPackageOption pkgs "gitea-actions-runner" { }; 60 61 instances = mkOption { 62 default = {}; 63 description = '' 64 Gitea Actions Runner instances. 65 ''; 66 type = attrsOf (submodule { 67 options = { 68 enable = mkEnableOption "Gitea Actions Runner instance"; 69 70 name = mkOption { 71 type = str; 72 example = literalExpression "config.networking.hostName"; 73 description = '' 74 The name identifying the runner instance towards the Gitea/Forgejo instance. 75 ''; 76 }; 77 78 url = mkOption { 79 type = str; 80 example = "https://forge.example.com"; 81 description = '' 82 Base URL of your Gitea/Forgejo instance. 83 ''; 84 }; 85 86 token = mkOption { 87 type = nullOr str; 88 default = null; 89 description = '' 90 Plain token to register at the configured Gitea/Forgejo instance. 91 ''; 92 }; 93 94 tokenFile = mkOption { 95 type = nullOr (either str path); 96 default = null; 97 description = '' 98 Path to an environment file, containing the `TOKEN` environment 99 variable, that holds a token to register at the configured 100 Gitea/Forgejo instance. 101 ''; 102 }; 103 104 labels = mkOption { 105 type = listOf str; 106 example = literalExpression '' 107 [ 108 # provide a debian base with nodejs for actions 109 "debian-latest:docker://node:18-bullseye" 110 # fake the ubuntu name, because node provides no ubuntu builds 111 "ubuntu-latest:docker://node:18-bullseye" 112 # provide native execution on the host 113 #"native:host" 114 ] 115 ''; 116 description = '' 117 Labels used to map jobs to their runtime environment. Changing these 118 labels currently requires a new registration token. 119 120 Many common actions require bash, git and nodejs, as well as a filesystem 121 that follows the filesystem hierarchy standard. 122 ''; 123 }; 124 settings = mkOption { 125 description = '' 126 Configuration for `act_runner daemon`. 127 See https://gitea.com/gitea/act_runner/src/branch/main/internal/pkg/config/config.example.yaml for an example configuration 128 ''; 129 130 type = types.submodule { 131 freeformType = settingsFormat.type; 132 }; 133 134 default = { }; 135 }; 136 137 hostPackages = mkOption { 138 type = listOf package; 139 default = with pkgs; [ 140 bash 141 coreutils 142 curl 143 gawk 144 gitMinimal 145 gnused 146 nodejs 147 wget 148 ]; 149 defaultText = literalExpression '' 150 with pkgs; [ 151 bash 152 coreutils 153 curl 154 gawk 155 gitMinimal 156 gnused 157 nodejs 158 wget 159 ] 160 ''; 161 description = '' 162 List of packages, that are available to actions, when the runner is configured 163 with a host execution label. 164 ''; 165 }; 166 }; 167 }); 168 }; 169 }; 170 171 config = mkIf (cfg.instances != {}) { 172 assertions = [ { 173 assertion = any tokenXorTokenFile (attrValues cfg.instances); 174 message = "Instances of gitea-actions-runner can have `token` or `tokenFile`, not both."; 175 } { 176 assertion = wantsContainerRuntime -> hasDocker || hasPodman; 177 message = "Label configuration on gitea-actions-runner instance requires either docker or podman."; 178 } ]; 179 180 systemd.services = let 181 mkRunnerService = name: instance: let 182 wantsContainerRuntime = hasDockerScheme instance; 183 wantsHost = hasHostScheme instance; 184 wantsDocker = wantsContainerRuntime && config.virtualisation.docker.enable; 185 wantsPodman = wantsContainerRuntime && config.virtualisation.podman.enable; 186 configFile = settingsFormat.generate "config.yaml" instance.settings; 187 in 188 nameValuePair "gitea-runner-${escapeSystemdPath name}" { 189 inherit (instance) enable; 190 description = "Gitea Actions Runner"; 191 wants = [ "network-online.target" ]; 192 after = [ 193 "network-online.target" 194 ] ++ optionals (wantsDocker) [ 195 "docker.service" 196 ] ++ optionals (wantsPodman) [ 197 "podman.service" 198 ]; 199 wantedBy = [ 200 "multi-user.target" 201 ]; 202 environment = optionalAttrs (instance.token != null) { 203 TOKEN = "${instance.token}"; 204 } // optionalAttrs (wantsPodman) { 205 DOCKER_HOST = "unix:///run/podman/podman.sock"; 206 } // { 207 HOME = "/var/lib/gitea-runner/${name}"; 208 }; 209 path = with pkgs; [ 210 coreutils 211 ] ++ lib.optionals wantsHost instance.hostPackages; 212 serviceConfig = { 213 DynamicUser = true; 214 User = "gitea-runner"; 215 StateDirectory = "gitea-runner"; 216 WorkingDirectory = "-/var/lib/gitea-runner/${name}"; 217 218 # gitea-runner might fail when gitea is restarted during upgrade. 219 Restart = "on-failure"; 220 RestartSec = 2; 221 222 ExecStartPre = [(pkgs.writeShellScript "gitea-register-runner-${name}" '' 223 export INSTANCE_DIR="$STATE_DIRECTORY/${name}" 224 mkdir -vp "$INSTANCE_DIR" 225 cd "$INSTANCE_DIR" 226 227 # force reregistration on changed labels 228 export LABELS_FILE="$INSTANCE_DIR/.labels" 229 export LABELS_WANTED="$(echo ${escapeShellArg (concatStringsSep "\n" instance.labels)} | sort)" 230 export LABELS_CURRENT="$(cat $LABELS_FILE 2>/dev/null || echo 0)" 231 232 if [ ! -e "$INSTANCE_DIR/.runner" ] || [ "$LABELS_WANTED" != "$LABELS_CURRENT" ]; then 233 # remove existing registration file, so that changing the labels forces a re-registration 234 rm -v "$INSTANCE_DIR/.runner" || true 235 236 # perform the registration 237 ${cfg.package}/bin/act_runner register --no-interactive \ 238 --instance ${escapeShellArg instance.url} \ 239 --token "$TOKEN" \ 240 --name ${escapeShellArg instance.name} \ 241 --labels ${escapeShellArg (concatStringsSep "," instance.labels)} \ 242 --config ${configFile} 243 244 # and write back the configured labels 245 echo "$LABELS_WANTED" > "$LABELS_FILE" 246 fi 247 248 '')]; 249 ExecStart = "${cfg.package}/bin/act_runner daemon --config ${configFile}"; 250 SupplementaryGroups = optionals (wantsDocker) [ 251 "docker" 252 ] ++ optionals (wantsPodman) [ 253 "podman" 254 ]; 255 } // optionalAttrs (instance.tokenFile != null) { 256 EnvironmentFile = instance.tokenFile; 257 }; 258 }; 259 in mapAttrs' mkRunnerService cfg.instances; 260 }; 261}