Skip to content

Commit 0dfb0ed

Browse files
committed
nixos/forgejo.runner: initialize module
1 parent 85793c2 commit 0dfb0ed

4 files changed

Lines changed: 314 additions & 1 deletion

File tree

nixos/doc/manual/release-notes/rl-2605.section.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,8 @@
6666

6767
- [Shoko](https://shokoanime.com), an anime management system. Available as [services.shoko](#opt-services.shoko.enable).
6868

69+
- [Forgejo Runner](https://code.forgejo.org/forgejo/runner), a dedicated Forgejo Actions runner module with hardened systemd template services for multi-instance deployments. Available as [services.forgejo.runner.instances](#opt-services.forgejo.runner.instances).
70+
6971
- [Drasl](https://github.com/unmojang/drasl), an alternative authentication server for Minecraft. Available as [services.drasl](#opt-services.drasl.enable).
7072

7173
## Backward Incompatibilities {#sec-release-26.05-incompatibilities}

nixos/modules/module-list.nix

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -503,6 +503,7 @@
503503
./services/continuous-integration/buildbot/master.nix
504504
./services/continuous-integration/buildbot/worker.nix
505505
./services/continuous-integration/buildkite-agents.nix
506+
./services/continuous-integration/forgejo-runner.nix
506507
./services/continuous-integration/gitea-actions-runner.nix
507508
./services/continuous-integration/github-runners.nix
508509
./services/continuous-integration/gitlab-runner/runner.nix
Lines changed: 289 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,289 @@
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 (submodule {
65+
options = {
66+
enable = mkEnableOption "Forgejo Actions Runner instance";
67+
68+
name = mkOption {
69+
type = str;
70+
example = literalExpression "config.networking.hostName";
71+
description = ''
72+
The name identifying the runner instance towards the Forgejo instance.
73+
'';
74+
};
75+
76+
url = mkOption {
77+
type = str;
78+
example = "https://forge.example.com";
79+
description = ''
80+
Base URL of your Forgejo instance.
81+
'';
82+
};
83+
84+
tokenFile = mkOption {
85+
type = nullOr (either str path);
86+
default = null;
87+
description = ''
88+
Path to an environment file, containing the `TOKEN` environment
89+
variable, that holds a token to register at the configured
90+
Forgejo instance.
91+
'';
92+
};
93+
94+
labels = mkOption {
95+
type = listOf str;
96+
example = literalExpression ''
97+
[
98+
# provide a debian base with nodejs for actions
99+
"debian-latest:docker://node:18-bullseye"
100+
# fake the ubuntu name, because node provides no ubuntu builds
101+
"ubuntu-latest:docker://node:18-bullseye"
102+
# provide native execution on the host
103+
#"native:host"
104+
]
105+
'';
106+
description = ''
107+
Labels used to map jobs to their runtime environment. Changing these
108+
labels currently requires a new registration token.
109+
110+
Many common actions require bash, git and nodejs, as well as a filesystem
111+
that follows the filesystem hierarchy standard.
112+
'';
113+
};
114+
115+
settings = mkOption {
116+
description = ''
117+
Configuration for `forgejo-runner daemon`.
118+
See <https://code.forgejo.org/forgejo/runner/src/branch/main/internal/pkg/config/config.example.yaml> for an example configuration.
119+
'';
120+
121+
type = types.submodule {
122+
freeformType = settingsFormat.type;
123+
};
124+
125+
default = { };
126+
};
127+
128+
hostPackages = mkOption {
129+
type = listOf package;
130+
default = with pkgs; [
131+
bash
132+
coreutils
133+
curl
134+
gawk
135+
gitMinimal
136+
gnused
137+
nodejs
138+
wget
139+
];
140+
defaultText = literalExpression ''
141+
with pkgs; [
142+
bash
143+
coreutils
144+
curl
145+
gawk
146+
gitMinimal
147+
gnused
148+
nodejs
149+
wget
150+
]
151+
'';
152+
description = ''
153+
List of packages that are available to actions, when the runner is configured
154+
with a host execution label.
155+
'';
156+
};
157+
};
158+
});
159+
};
160+
};
161+
162+
config = mkIf (cfg.instances != { }) {
163+
assertions = [
164+
{
165+
assertion = wantsContainerRuntime -> hasDocker || hasPodman;
166+
message = "Label configuration on forgejo.runner instance requires either docker or podman.";
167+
}
168+
];
169+
170+
systemd.services =
171+
let
172+
mkRunnerInstance =
173+
name: instance:
174+
let
175+
escapedName = escapeSystemdPath name;
176+
wantsContainer = hasDockerScheme instance;
177+
wantsHost = hasHostScheme instance;
178+
wantsDocker = wantsContainer && hasDocker;
179+
wantsPodman = wantsContainer && hasPodman;
180+
configFile = settingsFormat.generate "forgejo-runner-${escapedName}.yaml" instance.settings;
181+
in
182+
nameValuePair "forgejo-runner@${escapedName}" {
183+
overrideStrategy = "asDropin";
184+
inherit (instance) enable;
185+
wants = [
186+
"network-online.target"
187+
]
188+
++ optionals wantsDocker [ "docker.service" ]
189+
++ optionals wantsPodman [ "podman.service" ];
190+
after = [
191+
"network-online.target"
192+
]
193+
++ optionals wantsDocker [ "docker.service" ]
194+
++ optionals wantsPodman [ "podman.service" ];
195+
wantedBy = [ "multi-user.target" ];
196+
197+
environment = optionalAttrs wantsPodman {
198+
DOCKER_HOST = "unix:///run/podman/podman.sock";
199+
};
200+
201+
path = [ pkgs.coreutils ] ++ lib.optionals wantsHost instance.hostPackages;
202+
203+
serviceConfig = {
204+
MemoryDenyWriteExecute = !wantsHost;
205+
206+
ExecStartPre = [
207+
(pkgs.writeShellScript "forgejo-register-runner-${escapedName}" ''
208+
export INSTANCE_DIR="$STATE_DIRECTORY"
209+
mkdir -vp "$INSTANCE_DIR"
210+
cd "$INSTANCE_DIR"
211+
212+
export LABELS_FILE="$INSTANCE_DIR/.labels.sha256"
213+
export LABELS_WANTED="$(echo ${escapeShellArg (concatStringsSep "\n" instance.labels)} | sort)"
214+
export LABELS_WANTED_HASH="$(printf '%s' "$LABELS_WANTED" | sha256sum | cut -d' ' -f1)"
215+
export LABELS_CURRENT_HASH="$(cat "$LABELS_FILE" 2>/dev/null || true)"
216+
217+
if [ ! -e "$INSTANCE_DIR/.runner" ] || [ "$LABELS_WANTED_HASH" != "$LABELS_CURRENT_HASH" ]; then
218+
rm -vf "$INSTANCE_DIR/.runner" || true
219+
220+
${cfg.package}/bin/forgejo-runner register --no-interactive \
221+
--instance ${escapeShellArg instance.url} \
222+
--token "$TOKEN" \
223+
--name ${escapeShellArg instance.name} \
224+
--labels ${escapeShellArg (concatStringsSep "," instance.labels)} \
225+
--config ${configFile}
226+
227+
printf '%s' "$LABELS_WANTED_HASH" > "$LABELS_FILE"
228+
fi
229+
'')
230+
];
231+
ExecStart = lib.mkForce "${cfg.package}/bin/forgejo-runner daemon --config ${configFile}";
232+
SupplementaryGroups = optionals wantsDocker [ "docker" ] ++ optionals wantsPodman [ "podman" ];
233+
}
234+
// optionalAttrs (instance.tokenFile != null) {
235+
EnvironmentFile = instance.tokenFile;
236+
};
237+
};
238+
in
239+
{
240+
"forgejo-runner@" = {
241+
description = "Forgejo Actions Runner (%I)";
242+
243+
environment = {
244+
HOME = "/var/lib/forgejo-runner/%i";
245+
};
246+
247+
serviceConfig = {
248+
DynamicUser = true;
249+
User = "forgejo-runner-%i";
250+
StateDirectory = "forgejo-runner/%i";
251+
WorkingDirectory = "/var/lib/forgejo-runner/%i";
252+
253+
Restart = "on-failure";
254+
RestartSec = 2;
255+
256+
AmbientCapabilities = "";
257+
CapabilityBoundingSet = "";
258+
LockPersonality = true;
259+
NoNewPrivileges = true;
260+
PrivateDevices = true;
261+
PrivateTmp = true;
262+
ProcSubset = "pid";
263+
ProtectClock = true;
264+
ProtectControlGroups = true;
265+
ProtectHome = true;
266+
ProtectHostname = true;
267+
ProtectKernelLogs = true;
268+
ProtectKernelModules = true;
269+
ProtectKernelTunables = true;
270+
ProtectProc = "invisible";
271+
ProtectSystem = "strict";
272+
RemoveIPC = true;
273+
RestrictAddressFamilies = [
274+
"AF_INET"
275+
"AF_INET6"
276+
"AF_UNIX"
277+
];
278+
RestrictNamespaces = true;
279+
RestrictRealtime = true;
280+
RestrictSUIDSGID = true;
281+
SystemCallArchitectures = "native";
282+
SystemCallFilter = [ "@system-service" ];
283+
UMask = "0077";
284+
};
285+
};
286+
}
287+
// mapAttrs' mkRunnerInstance cfg.instances;
288+
};
289+
}

nixos/tests/forgejo.nix

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,22 @@ let
6161
services.openssh.enable = true;
6262

6363
specialisation.runner = {
64+
inheritParentConfig = true;
65+
configuration.services.forgejo.runner = {
66+
package = pkgs.forgejo-runner;
67+
instances."test" = {
68+
enable = true;
69+
name = "ci";
70+
url = "http://localhost:3000";
71+
labels = [
72+
# type ":host" does not depend on docker/podman/lxc
73+
"native:host"
74+
];
75+
tokenFile = "/var/lib/forgejo/runner_token";
76+
};
77+
};
78+
};
79+
specialisation.runnerLegacy = {
6480
inheritParentConfig = true;
6581
configuration.services.gitea-actions-runner = {
6682
package = pkgs.forgejo-runner;
@@ -221,10 +237,15 @@ let
221237
server.succeed(
222238
"su -l forgejo -c 'GITEA_WORK_DIR=/var/lib/forgejo forgejo actions generate-runner-token' | sed 's/^/TOKEN=/' | tee /var/lib/forgejo/runner_token"
223239
)
224-
server.succeed("${serverSystem}/specialisation/runner/bin/switch-to-configuration test")
240+
241+
server.succeed("${serverSystem}/specialisation/runnerLegacy/bin/switch-to-configuration test")
225242
server.wait_for_unit("gitea-runner-test.service")
226243
server.succeed("journalctl -o cat -u gitea-runner-test.service | grep -q 'Runner registered successfully'")
227244
245+
server.succeed("${serverSystem}/specialisation/runner/bin/switch-to-configuration test")
246+
server.wait_for_unit("forgejo-runner@test.service")
247+
server.succeed("journalctl -o cat -u forgejo-runner@test.service | grep -q 'Runner registered successfully'")
248+
228249
# enable actions feature for this repository, defaults to disabled
229250
server.succeed(
230251
"curl --fail -X PATCH http://localhost:3000/api/v1/repos/test/repo "

0 commit comments

Comments
 (0)