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