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