1{ config, lib, pkgs, ... }:
2with builtins;
3with lib;
4let
5 cfg = config.services.gitlab-runner;
6 hasDocker = config.virtualisation.docker.enable;
7
8 /* The whole logic of this module is to diff the hashes of the desired vs existing runners
9 The hash is recorded in the runner's name because we can't do better yet
10 See https://gitlab.com/gitlab-org/gitlab-runner/-/issues/29350 for more details
11 */
12 genRunnerName = name: service: let
13 hash = substring 0 12 (hashString "md5" (unsafeDiscardStringContext (toJSON service)));
14 in if service ? description && service.description != null
15 then "${hash} ${service.description}"
16 else "${name}_${config.networking.hostName}_${hash}";
17
18 hashedServices = mapAttrs'
19 (name: service: nameValuePair (genRunnerName name service) service) cfg.services;
20 configPath = ''"$HOME"/.gitlab-runner/config.toml'';
21 configureScript = pkgs.writeShellApplication {
22 name = "gitlab-runner-configure";
23 runtimeInputs = with pkgs; [
24 bash
25 gawk
26 jq
27 moreutils
28 remarshal
29 util-linux
30 cfg.package
31 perl
32 python3
33 ];
34 text = if (cfg.configFile != null) then ''
35 cp ${cfg.configFile} ${configPath}
36 # make config file readable by service
37 chown -R --reference="$HOME" "$(dirname ${configPath})"
38 '' else ''
39 export CONFIG_FILE=${configPath}
40
41 mkdir -p "$(dirname ${configPath})"
42 touch ${configPath}
43
44 # update global options
45 remarshal --if toml --of json ${configPath} \
46 | jq -cM 'with_entries(select([.key] | inside(["runners"])))' \
47 | jq -scM '.[0] + .[1]' - <(echo ${escapeShellArg (toJSON cfg.settings)}) \
48 | remarshal --if json --of toml \
49 | sponge ${configPath}
50
51 # remove no longer existing services
52 gitlab-runner verify --delete
53
54 ${toShellVar "NEEDED_SERVICES" (lib.mapAttrs (name: value: 1) hashedServices)}
55
56 declare -A REGISTERED_SERVICES
57
58 while IFS="," read -r name token;
59 do
60 REGISTERED_SERVICES["$name"]="$token"
61 done < <(gitlab-runner --log-format json list 2>&1 | grep Token | jq -r '.msg +"," + .Token')
62
63 echo "NEEDED_SERVICES: " "''${!NEEDED_SERVICES[@]}"
64 echo "REGISTERED_SERVICES:" "''${!REGISTERED_SERVICES[@]}"
65
66 # difference between current and desired state
67 declare -A NEW_SERVICES
68 for name in "''${!NEEDED_SERVICES[@]}"; do
69 if [ ! -v 'REGISTERED_SERVICES[$name]' ]; then
70 NEW_SERVICES[$name]=1
71 fi
72 done
73
74 declare -A OLD_SERVICES
75 # shellcheck disable=SC2034
76 for name in "''${!REGISTERED_SERVICES[@]}"; do
77 if [ ! -v 'NEEDED_SERVICES[$name]' ]; then
78 OLD_SERVICES[$name]=1
79 fi
80 done
81
82 # register new services
83 ${concatStringsSep "\n" (mapAttrsToList (name: service: ''
84 # TODO so here we should mention NEW_SERVICES
85 if [ -v 'NEW_SERVICES["${name}"]' ] ; then
86 bash -c ${escapeShellArg (concatStringsSep " \\\n " ([
87 "set -a && source ${service.registrationConfigFile} &&"
88 "gitlab-runner register"
89 "--non-interactive"
90 "--name '${name}'"
91 "--executor ${service.executor}"
92 "--limit ${toString service.limit}"
93 "--request-concurrency ${toString service.requestConcurrency}"
94 "--maximum-timeout ${toString service.maximumTimeout}"
95 ] ++ service.registrationFlags
96 ++ optional (service.buildsDir != null)
97 "--builds-dir ${service.buildsDir}"
98 ++ optional (service.cloneUrl != null)
99 "--clone-url ${service.cloneUrl}"
100 ++ optional (service.preCloneScript != null)
101 "--pre-clone-script ${service.preCloneScript}"
102 ++ optional (service.preBuildScript != null)
103 "--pre-build-script ${service.preBuildScript}"
104 ++ optional (service.postBuildScript != null)
105 "--post-build-script ${service.postBuildScript}"
106 ++ optional (service.tagList != [ ])
107 "--tag-list ${concatStringsSep "," service.tagList}"
108 ++ optional service.runUntagged
109 "--run-untagged"
110 ++ optional service.protected
111 "--access-level ref_protected"
112 ++ optional service.debugTraceDisabled
113 "--debug-trace-disabled"
114 ++ map (e: "--env ${escapeShellArg e}") (mapAttrsToList (name: value: "${name}=${value}") service.environmentVariables)
115 ++ optionals (hasPrefix "docker" service.executor) (
116 assert (
117 assertMsg (service.dockerImage != null)
118 "dockerImage option is required for ${service.executor} executor (${name})");
119 [ "--docker-image ${service.dockerImage}" ]
120 ++ optional service.dockerDisableCache
121 "--docker-disable-cache"
122 ++ optional service.dockerPrivileged
123 "--docker-privileged"
124 ++ map (v: "--docker-volumes ${escapeShellArg v}") service.dockerVolumes
125 ++ map (v: "--docker-extra-hosts ${escapeShellArg v}") service.dockerExtraHosts
126 ++ map (v: "--docker-allowed-images ${escapeShellArg v}") service.dockerAllowedImages
127 ++ map (v: "--docker-allowed-services ${escapeShellArg v}") service.dockerAllowedServices
128 )
129 ))} && sleep 1 || exit 1
130 fi
131 '') hashedServices)}
132
133 # check key is in array https://stackoverflow.com/questions/30353951/how-to-check-if-dictionary-contains-a-key-in-bash
134
135 echo "NEW_SERVICES: ''${NEW_SERVICES[*]}"
136 echo "OLD_SERVICES: ''${OLD_SERVICES[*]}"
137 # unregister old services
138 for NAME in "''${!OLD_SERVICES[@]}"
139 do
140 [ -n "$NAME" ] && gitlab-runner unregister \
141 --name "$NAME" && sleep 1
142 done
143
144 # make config file readable by service
145 chown -R --reference="$HOME" "$(dirname ${configPath})"
146 '';
147 };
148 startScript = pkgs.writeShellScriptBin "gitlab-runner-start" ''
149 export CONFIG_FILE=${configPath}
150 exec gitlab-runner run --working-directory $HOME
151 '';
152in {
153 options.services.gitlab-runner = {
154 enable = mkEnableOption "Gitlab Runner";
155 configFile = mkOption {
156 type = types.nullOr types.path;
157 default = null;
158 description = ''
159 Configuration file for gitlab-runner.
160
161 {option}`configFile` takes precedence over {option}`services`.
162 {option}`checkInterval` and {option}`concurrent` will be ignored too.
163
164 This option is deprecated, please use {option}`services` instead.
165 You can use {option}`registrationConfigFile` and
166 {option}`registrationFlags`
167 for settings not covered by this module.
168 '';
169 };
170 settings = mkOption {
171 type = types.submodule {
172 freeformType = (pkgs.formats.json { }).type;
173 };
174 default = { };
175 description = ''
176 Global gitlab-runner configuration. See
177 <https://docs.gitlab.com/runner/configuration/advanced-configuration.html#the-global-section>
178 for supported values.
179 '';
180 };
181 gracefulTermination = mkOption {
182 type = types.bool;
183 default = false;
184 description = ''
185 Finish all remaining jobs before stopping.
186 If not set gitlab-runner will stop immediately without waiting
187 for jobs to finish, which will lead to failed builds.
188 '';
189 };
190 gracefulTimeout = mkOption {
191 type = types.str;
192 default = "infinity";
193 example = "5min 20s";
194 description = ''
195 Time to wait until a graceful shutdown is turned into a forceful one.
196 '';
197 };
198 package = mkPackageOption pkgs "gitlab-runner" {
199 example = "gitlab-runner_1_11";
200 };
201 extraPackages = mkOption {
202 type = types.listOf types.package;
203 default = [ ];
204 description = ''
205 Extra packages to add to PATH for the gitlab-runner process.
206 '';
207 };
208 services = mkOption {
209 description = "GitLab Runner services.";
210 default = { };
211 example = literalExpression ''
212 {
213 # runner for building in docker via host's nix-daemon
214 # nix store will be readable in runner, might be insecure
215 nix = {
216 # File should contain at least these two variables:
217 # `CI_SERVER_URL`
218 # `REGISTRATION_TOKEN`
219 registrationConfigFile = "/run/secrets/gitlab-runner-registration";
220 dockerImage = "alpine";
221 dockerVolumes = [
222 "/nix/store:/nix/store:ro"
223 "/nix/var/nix/db:/nix/var/nix/db:ro"
224 "/nix/var/nix/daemon-socket:/nix/var/nix/daemon-socket:ro"
225 ];
226 dockerDisableCache = true;
227 preBuildScript = pkgs.writeScript "setup-container" '''
228 mkdir -p -m 0755 /nix/var/log/nix/drvs
229 mkdir -p -m 0755 /nix/var/nix/gcroots
230 mkdir -p -m 0755 /nix/var/nix/profiles
231 mkdir -p -m 0755 /nix/var/nix/temproots
232 mkdir -p -m 0755 /nix/var/nix/userpool
233 mkdir -p -m 1777 /nix/var/nix/gcroots/per-user
234 mkdir -p -m 1777 /nix/var/nix/profiles/per-user
235 mkdir -p -m 0755 /nix/var/nix/profiles/per-user/root
236 mkdir -p -m 0700 "$HOME/.nix-defexpr"
237
238 . ''${pkgs.nix}/etc/profile.d/nix.sh
239
240 ''${pkgs.nix}/bin/nix-env -i ''${concatStringsSep " " (with pkgs; [ nix cacert git openssh ])}
241
242 ''${pkgs.nix}/bin/nix-channel --add https://nixos.org/channels/nixpkgs-unstable
243 ''${pkgs.nix}/bin/nix-channel --update nixpkgs
244 ''';
245 environmentVariables = {
246 ENV = "/etc/profile";
247 USER = "root";
248 NIX_REMOTE = "daemon";
249 PATH = "/nix/var/nix/profiles/default/bin:/nix/var/nix/profiles/default/sbin:/bin:/sbin:/usr/bin:/usr/sbin";
250 NIX_SSL_CERT_FILE = "/nix/var/nix/profiles/default/etc/ssl/certs/ca-bundle.crt";
251 };
252 tagList = [ "nix" ];
253 };
254 # runner for building docker images
255 docker-images = {
256 # File should contain at least these two variables:
257 # `CI_SERVER_URL`
258 # `REGISTRATION_TOKEN`
259 registrationConfigFile = "/run/secrets/gitlab-runner-registration";
260 dockerImage = "docker:stable";
261 dockerVolumes = [
262 "/var/run/docker.sock:/var/run/docker.sock"
263 ];
264 tagList = [ "docker-images" ];
265 };
266 # runner for executing stuff on host system (very insecure!)
267 # make sure to add required packages (including git!)
268 # to `environment.systemPackages`
269 shell = {
270 # File should contain at least these two variables:
271 # `CI_SERVER_URL`
272 # `REGISTRATION_TOKEN`
273 registrationConfigFile = "/run/secrets/gitlab-runner-registration";
274 executor = "shell";
275 tagList = [ "shell" ];
276 };
277 # runner for everything else
278 default = {
279 # File should contain at least these two variables:
280 # `CI_SERVER_URL`
281 # `REGISTRATION_TOKEN`
282 registrationConfigFile = "/run/secrets/gitlab-runner-registration";
283 dockerImage = "debian:stable";
284 };
285 }
286 '';
287 type = types.attrsOf (types.submodule {
288 options = {
289 registrationConfigFile = mkOption {
290 type = types.path;
291 description = ''
292 Absolute path to a file with environment variables
293 used for gitlab-runner registration.
294 A list of all supported environment variables can be found in
295 `gitlab-runner register --help`.
296
297 Ones that you probably want to set is
298
299 `CI_SERVER_URL=<CI server URL>`
300
301 `REGISTRATION_TOKEN=<registration secret>`
302
303 WARNING: make sure to use quoted absolute path,
304 or it is going to be copied to Nix Store.
305 '';
306 };
307 registrationFlags = mkOption {
308 type = types.listOf types.str;
309 default = [ ];
310 example = [ "--docker-helper-image my/gitlab-runner-helper" ];
311 description = ''
312 Extra command-line flags passed to
313 `gitlab-runner register`.
314 Execute `gitlab-runner register --help`
315 for a list of supported flags.
316 '';
317 };
318 environmentVariables = mkOption {
319 type = types.attrsOf types.str;
320 default = { };
321 example = { NAME = "value"; };
322 description = ''
323 Custom environment variables injected to build environment.
324 For secrets you can use {option}`registrationConfigFile`
325 with `RUNNER_ENV` variable set.
326 '';
327 };
328 description = mkOption {
329 type = types.nullOr types.str;
330 default = null;
331 description = ''
332 Name/description of the runner.
333 '';
334 };
335 executor = mkOption {
336 type = types.str;
337 default = "docker";
338 description = ''
339 Select executor, eg. shell, docker, etc.
340 See [runner documentation](https://docs.gitlab.com/runner/executors/README.html) for more information.
341 '';
342 };
343 buildsDir = mkOption {
344 type = types.nullOr types.path;
345 default = null;
346 example = "/var/lib/gitlab-runner/builds";
347 description = ''
348 Absolute path to a directory where builds will be stored
349 in context of selected executor (Locally, Docker, SSH).
350 '';
351 };
352 cloneUrl = mkOption {
353 type = types.nullOr types.str;
354 default = null;
355 example = "http://gitlab.example.local";
356 description = ''
357 Overwrite the URL for the GitLab instance. Used if the Runner can’t connect to GitLab on the URL GitLab exposes itself.
358 '';
359 };
360 dockerImage = mkOption {
361 type = types.nullOr types.str;
362 default = null;
363 description = ''
364 Docker image to be used.
365 '';
366 };
367 dockerVolumes = mkOption {
368 type = types.listOf types.str;
369 default = [ ];
370 example = [ "/var/run/docker.sock:/var/run/docker.sock" ];
371 description = ''
372 Bind-mount a volume and create it
373 if it doesn't exist prior to mounting.
374 '';
375 };
376 dockerDisableCache = mkOption {
377 type = types.bool;
378 default = false;
379 description = ''
380 Disable all container caching.
381 '';
382 };
383 dockerPrivileged = mkOption {
384 type = types.bool;
385 default = false;
386 description = ''
387 Give extended privileges to container.
388 '';
389 };
390 dockerExtraHosts = mkOption {
391 type = types.listOf types.str;
392 default = [ ];
393 example = [ "other-host:127.0.0.1" ];
394 description = ''
395 Add a custom host-to-IP mapping.
396 '';
397 };
398 dockerAllowedImages = mkOption {
399 type = types.listOf types.str;
400 default = [ ];
401 example = [ "ruby:*" "python:*" "php:*" "my.registry.tld:5000/*:*" ];
402 description = ''
403 Whitelist allowed images.
404 '';
405 };
406 dockerAllowedServices = mkOption {
407 type = types.listOf types.str;
408 default = [ ];
409 example = [ "postgres:9" "redis:*" "mysql:*" ];
410 description = ''
411 Whitelist allowed services.
412 '';
413 };
414 preCloneScript = mkOption {
415 type = types.nullOr types.path;
416 default = null;
417 description = ''
418 Runner-specific command script executed before code is pulled.
419 '';
420 };
421 preBuildScript = mkOption {
422 type = types.nullOr types.path;
423 default = null;
424 description = ''
425 Runner-specific command script executed after code is pulled,
426 just before build executes.
427 '';
428 };
429 postBuildScript = mkOption {
430 type = types.nullOr types.path;
431 default = null;
432 description = ''
433 Runner-specific command script executed after code is pulled
434 and just after build executes.
435 '';
436 };
437 tagList = mkOption {
438 type = types.listOf types.str;
439 default = [ ];
440 description = ''
441 Tag list.
442 '';
443 };
444 runUntagged = mkOption {
445 type = types.bool;
446 default = false;
447 description = ''
448 Register to run untagged builds; defaults to
449 `true` when {option}`tagList` is empty.
450 '';
451 };
452 limit = mkOption {
453 type = types.int;
454 default = 0;
455 description = ''
456 Limit how many jobs can be handled concurrently by this service.
457 0 (default) simply means don't limit.
458 '';
459 };
460 requestConcurrency = mkOption {
461 type = types.int;
462 default = 0;
463 description = ''
464 Limit number of concurrent requests for new jobs from GitLab.
465 '';
466 };
467 maximumTimeout = mkOption {
468 type = types.int;
469 default = 0;
470 description = ''
471 What is the maximum timeout (in seconds) that will be set for
472 job when using this Runner. 0 (default) simply means don't limit.
473 '';
474 };
475 protected = mkOption {
476 type = types.bool;
477 default = false;
478 description = ''
479 When set to true Runner will only run on pipelines
480 triggered on protected branches.
481 '';
482 };
483 debugTraceDisabled = mkOption {
484 type = types.bool;
485 default = false;
486 description = ''
487 When set to true Runner will disable the possibility of
488 using the `CI_DEBUG_TRACE` feature.
489 '';
490 };
491 };
492 });
493 };
494 clear-docker-cache = {
495 enable = mkOption {
496 type = types.bool;
497 default = false;
498 description = ''
499 Whether to periodically prune gitlab runner's Docker resources. If
500 enabled, a systemd timer will run {command}`clear-docker-cache` as
501 specified by the `dates` option.
502 '';
503 };
504
505 flags = mkOption {
506 type = types.listOf types.str;
507 default = [ ];
508 example = [ "prune" ];
509 description = ''
510 Any additional flags passed to {command}`clear-docker-cache`.
511 '';
512 };
513
514 dates = mkOption {
515 default = "weekly";
516 type = types.str;
517 description = ''
518 Specification (in the format described by
519 {manpage}`systemd.time(7)`) of the time at
520 which the prune will occur.
521 '';
522 };
523
524 package = mkOption {
525 default = config.virtualisation.docker.package;
526 defaultText = literalExpression "config.virtualisation.docker.package";
527 example = literalExpression "pkgs.docker";
528 description = "Docker package to use for clearing up docker cache.";
529 };
530 };
531 };
532 config = mkIf cfg.enable {
533 warnings = mapAttrsToList
534 (n: v: "services.gitlab-runner.services.${n}.`registrationConfigFile` points to a file in Nix Store. You should use quoted absolute path to prevent this.")
535 (filterAttrs (n: v: isStorePath v.registrationConfigFile) cfg.services);
536
537 environment.systemPackages = [ cfg.package ];
538 systemd.services.gitlab-runner = {
539 description = "Gitlab Runner";
540 documentation = [ "https://docs.gitlab.com/runner/" ];
541 after = [ "network.target" ]
542 ++ optional hasDocker "docker.service";
543 requires = optional hasDocker "docker.service";
544 wantedBy = [ "multi-user.target" ];
545 environment = config.networking.proxy.envVars // {
546 HOME = "/var/lib/gitlab-runner";
547 };
548 path = with pkgs; [
549 bash
550 gawk
551 jq
552 moreutils
553 remarshal
554 util-linux
555 cfg.package
556 ] ++ cfg.extraPackages;
557 reloadIfChanged = true;
558 serviceConfig = {
559 # Set `DynamicUser` under `systemd.services.gitlab-runner.serviceConfig`
560 # to `lib.mkForce false` in your configuration to run this service as root.
561 # You can also set `User` and `Group` options to run this service as desired user.
562 # Make sure to restart service or changes won't apply.
563 DynamicUser = true;
564 StateDirectory = "gitlab-runner";
565 SupplementaryGroups = optional hasDocker "docker";
566 ExecStartPre = "!${configureScript}/bin/gitlab-runner-configure";
567 ExecStart = "${startScript}/bin/gitlab-runner-start";
568 ExecReload = "!${configureScript}/bin/gitlab-runner-configure";
569 } // optionalAttrs cfg.gracefulTermination {
570 TimeoutStopSec = "${cfg.gracefulTimeout}";
571 KillSignal = "SIGQUIT";
572 KillMode = "process";
573 };
574 };
575 # Enable periodic clear-docker-cache script
576 systemd.services.gitlab-runner-clear-docker-cache = mkIf (cfg.clear-docker-cache.enable && (any (s: s.executor == "docker") (attrValues cfg.services))) {
577 description = "Prune gitlab-runner docker resources";
578 restartIfChanged = false;
579 unitConfig.X-StopOnRemoval = false;
580
581 serviceConfig.Type = "oneshot";
582
583 path = [ cfg.clear-docker-cache.package pkgs.gawk ];
584
585 script = ''
586 ${pkgs.gitlab-runner}/bin/clear-docker-cache ${toString cfg.clear-docker-cache.flags}
587 '';
588
589 startAt = cfg.clear-docker-cache.dates;
590 };
591 # Enable docker if `docker` executor is used in any service
592 virtualisation.docker.enable = mkIf (
593 any (s: s.executor == "docker") (attrValues cfg.services)
594 ) (mkDefault true);
595 };
596 imports = [
597 (mkRenamedOptionModule [ "services" "gitlab-runner" "packages" ] [ "services" "gitlab-runner" "extraPackages" ] )
598 (mkRemovedOptionModule [ "services" "gitlab-runner" "configOptions" ] "Use services.gitlab-runner.services option instead" )
599 (mkRemovedOptionModule [ "services" "gitlab-runner" "workDir" ] "You should move contents of workDir (if any) to /var/lib/gitlab-runner" )
600
601 (mkRenamedOptionModule [ "services" "gitlab-runner" "checkInterval" ] [ "services" "gitlab-runner" "settings" "check_interval" ] )
602 (mkRenamedOptionModule [ "services" "gitlab-runner" "concurrent" ] [ "services" "gitlab-runner" "settings" "concurrent" ] )
603 (mkRenamedOptionModule [ "services" "gitlab-runner" "sentryDSN" ] [ "services" "gitlab-runner" "settings" "sentry_dsn" ] )
604 (mkRenamedOptionModule [ "services" "gitlab-runner" "prometheusListenAddress" ] [ "services" "gitlab-runner" "settings" "listen_address" ] )
605
606 (mkRenamedOptionModule [ "services" "gitlab-runner" "sessionServer" "listenAddress" ] [ "services" "gitlab-runner" "settings" "session_server" "listen_address" ] )
607 (mkRenamedOptionModule [ "services" "gitlab-runner" "sessionServer" "advertiseAddress" ] [ "services" "gitlab-runner" "settings" "session_server" "advertise_address" ] )
608 (mkRenamedOptionModule [ "services" "gitlab-runner" "sessionServer" "sessionTimeout" ] [ "services" "gitlab-runner" "settings" "session_server" "session_timeout" ] )
609 ];
610
611 meta.maintainers = teams.gitlab.members;
612}