about summary refs log tree commit diff
path: root/nixpkgs/nixos/modules/services/continuous-integration/gitea-actions-runner.nix
blob: fb70c48991260734864ca6d31918c7556f47cb7b (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
{ config
, lib
, pkgs
, utils
, ...
}:

let
  inherit (lib)
    any
    attrValues
    concatStringsSep
    escapeShellArg
    hasInfix
    hasSuffix
    optionalAttrs
    optionals
    literalExpression
    mapAttrs'
    mkEnableOption
    mkOption
    mkPackageOptionMD
    mkIf
    nameValuePair
    types
  ;

  inherit (utils)
    escapeSystemdPath
  ;

  cfg = config.services.gitea-actions-runner;

  # Check whether any runner instance label requires a container runtime
  # Empty label strings result in the upstream defined defaultLabels, which require docker
  # https://gitea.com/gitea/act_runner/src/tag/v0.1.5/internal/app/cmd/register.go#L93-L98
  hasDockerScheme = instance:
    instance.labels == [] || any (label: hasInfix ":docker:" label) instance.labels;
  wantsContainerRuntime = any hasDockerScheme (attrValues cfg.instances);

  hasHostScheme = instance: any (label: hasSuffix ":host" label) instance.labels;

  # provide shorthands for whether container runtimes are enabled
  hasDocker = config.virtualisation.docker.enable;
  hasPodman = config.virtualisation.podman.enable;

  tokenXorTokenFile = instance:
    (instance.token == null && instance.tokenFile != null) ||
    (instance.token != null && instance.tokenFile == null);
in
{
  meta.maintainers = with lib.maintainers; [
    hexa
  ];

  options.services.gitea-actions-runner = with types; {
    package = mkPackageOptionMD pkgs "gitea-actions-runner" { };

    instances = mkOption {
      default = {};
      description = lib.mdDoc ''
        Gitea Actions Runner instances.
      '';
      type = attrsOf (submodule {
        options = {
          enable = mkEnableOption (lib.mdDoc "Gitea Actions Runner instance");

          name = mkOption {
            type = str;
            example = literalExpression "config.networking.hostName";
            description = lib.mdDoc ''
              The name identifying the runner instance towards the Gitea/Forgejo instance.
            '';
          };

          url = mkOption {
            type = str;
            example = "https://forge.example.com";
            description = lib.mdDoc ''
              Base URL of your Gitea/Forgejo instance.
            '';
          };

          token = mkOption {
            type = nullOr str;
            default = null;
            description = lib.mdDoc ''
              Plain token to register at the configured Gitea/Forgejo instance.
            '';
          };

          tokenFile = mkOption {
            type = nullOr (either str path);
            default = null;
            description = lib.mdDoc ''
              Path to an environment file, containing the `TOKEN` environment
              variable, that holds a token to register at the configured
              Gitea/Forgejo instance.
            '';
          };

          labels = mkOption {
            type = listOf str;
            example = literalExpression ''
              [
                # provide a debian base with nodejs for actions
                "debian-latest:docker://node:18-bullseye"
                # fake the ubuntu name, because node provides no ubuntu builds
                "ubuntu-latest:docker://node:18-bullseye"
                # provide native execution on the host
                #"native:host"
              ]
            '';
            description = lib.mdDoc ''
              Labels used to map jobs to their runtime environment. Changing these
              labels currently requires a new registration token.

              Many common actions require bash, git and nodejs, as well as a filesystem
              that follows the filesystem hierarchy standard.
            '';
          };

          hostPackages = mkOption {
            type = listOf package;
            default = with pkgs; [
              bash
              coreutils
              curl
              gawk
              gitMinimal
              gnused
              nodejs
              wget
            ];
            defaultText = literalExpression ''
              with pkgs; [
                bash
                coreutils
                curl
                gawk
                gitMinimal
                gnused
                nodejs
                wget
              ]
            '';
            description = lib.mdDoc ''
              List of packages, that are available to actions, when the runner is configured
              with a host execution label.
            '';
          };
        };
      });
    };
  };

  config = mkIf (cfg.instances != {}) {
    assertions = [ {
      assertion = any tokenXorTokenFile (attrValues cfg.instances);
      message = "Instances of gitea-actions-runner can have `token` or `tokenFile`, not both.";
    } {
      assertion = wantsContainerRuntime -> hasDocker || hasPodman;
      message = "Label configuration on gitea-actions-runner instance requires either docker or podman.";
    } ];

    systemd.services = let
      mkRunnerService = name: instance: let
        wantsContainerRuntime = hasDockerScheme instance;
        wantsHost = hasHostScheme instance;
        wantsDocker = wantsContainerRuntime && config.virtualisation.docker.enable;
        wantsPodman = wantsContainerRuntime && config.virtualisation.podman.enable;
      in
        nameValuePair "gitea-runner-${escapeSystemdPath name}" {
          inherit (instance) enable;
          description = "Gitea Actions Runner";
          after = [
            "network-online.target"
          ] ++ optionals (wantsDocker) [
            "docker.service"
          ] ++ optionals (wantsPodman) [
            "podman.service"
          ];
          wantedBy = [
            "multi-user.target"
          ];
          environment = optionalAttrs (instance.token != null) {
            TOKEN = "${instance.token}";
          } // optionalAttrs (wantsPodman) {
            DOCKER_HOST = "unix:///run/podman/podman.sock";
          };
          path = with pkgs; [
            coreutils
          ] ++ lib.optionals wantsHost instance.hostPackages;
          serviceConfig = {
            DynamicUser = true;
            User = "gitea-runner";
            StateDirectory = "gitea-runner";
            WorkingDirectory = "-/var/lib/gitea-runner/${name}";
            ExecStartPre = pkgs.writeShellScript "gitea-register-runner-${name}" ''
              export INSTANCE_DIR="$STATE_DIRECTORY/${name}"
              mkdir -vp "$INSTANCE_DIR"
              cd "$INSTANCE_DIR"

              # force reregistration on changed labels
              export LABELS_FILE="$INSTANCE_DIR/.labels"
              export LABELS_WANTED="$(echo ${escapeShellArg (concatStringsSep "\n" instance.labels)} | sort)"
              export LABELS_CURRENT="$(cat $LABELS_FILE 2>/dev/null || echo 0)"

              if [ ! -e "$INSTANCE_DIR/.runner" ] || [ "$LABELS_WANTED" != "$LABELS_CURRENT" ]; then
                # remove existing registration file, so that changing the labels forces a re-registration
                rm -v "$INSTANCE_DIR/.runner" || true

                # perform the registration
                ${cfg.package}/bin/act_runner register --no-interactive \
                  --instance ${escapeShellArg instance.url} \
                  --token "$TOKEN" \
                  --name ${escapeShellArg instance.name} \
                  --labels ${escapeShellArg (concatStringsSep "," instance.labels)}

                # and write back the configured labels
                echo "$LABELS_WANTED" > "$LABELS_FILE"
              fi

            '';
            ExecStart = "${cfg.package}/bin/act_runner daemon";
            SupplementaryGroups = optionals (wantsDocker) [
              "docker"
            ] ++ optionals (wantsPodman) [
              "podman"
            ];
          } // optionalAttrs (instance.tokenFile != null) {
            EnvironmentFile = instance.tokenFile;
          };
        };
    in mapAttrs' mkRunnerService cfg.instances;
  };
}