vuls: init at 0.27.0
[NixPkgs.git] / nixos / modules / services / continuous-integration / gitea-actions-runner.nix
blob30be56f8eeabefe7041d598e49b63269340694b4
1 { config
2 , lib
3 , pkgs
4 , utils
5 , ...
6 }:
8 let
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   ;
28   inherit (utils)
29     escapeSystemdPath
30   ;
32   cfg = config.services.gitea-actions-runner;
34   settingsFormat = pkgs.formats.yaml { };
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);
43   hasHostScheme = instance: any (label: hasSuffix ":host" label) instance.labels;
45   # provide shorthands for whether container runtimes are enabled
46   hasDocker = config.virtualisation.docker.enable;
47   hasPodman = config.virtualisation.podman.enable;
49   tokenXorTokenFile = instance:
50     (instance.token == null && instance.tokenFile != null) ||
51     (instance.token != null && instance.tokenFile == null);
54   meta.maintainers = with lib.maintainers; [
55     hexa
56   ];
58   options.services.gitea-actions-runner = with types; {
59     package = mkPackageOption pkgs "gitea-actions-runner" { };
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";
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           };
78           url = mkOption {
79             type = str;
80             example = "https://forge.example.com";
81             description = ''
82               Base URL of your Gitea/Forgejo instance.
83             '';
84           };
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           };
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           };
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.
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             '';
130             type = types.submodule {
131               freeformType = settingsFormat.type;
132             };
134             default = { };
135           };
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   };
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     } ];
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}";
218             # gitea-runner might fail when gitea is restarted during upgrade.
219             Restart = "on-failure";
220             RestartSec = 2;
222             ExecStartPre = [(pkgs.writeShellScript "gitea-register-runner-${name}" ''
223               export INSTANCE_DIR="$STATE_DIRECTORY/${name}"
224               mkdir -vp "$INSTANCE_DIR"
225               cd "$INSTANCE_DIR"
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)"
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
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}
244                 # and write back the configured labels
245                 echo "$LABELS_WANTED" > "$LABELS_FILE"
246               fi
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   };