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 = literalExample "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 = literalExample "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 = literalExample ''
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 = "pkgs.gitlab-runner";
238 example = literalExample "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 = literalExample ''
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 };
344 registrationFlags = mkOption {
345 type = types.listOf types.str;
346 default = [ ];
347 example = [ "--docker-helper-image my/gitlab-runner-helper" ];
348 description = ''
349 Extra command-line flags passed to
350 <literal>gitlab-runner register</literal>.
351 Execute <literal>gitlab-runner register --help</literal>
352 for a list of supported flags.
353 '';
354 };
355 environmentVariables = mkOption {
356 type = types.attrsOf types.str;
357 default = { };
358 example = { NAME = "value"; };
359 description = ''
360 Custom environment variables injected to build environment.
361 For secrets you can use <option>registrationConfigFile</option>
362 with <literal>RUNNER_ENV</literal> variable set.
363 '';
364 };
365 executor = mkOption {
366 type = types.str;
367 default = "docker";
368 description = ''
369 Select executor, eg. shell, docker, etc.
370 See <link xlink:href="https://docs.gitlab.com/runner/executors/README.html">runner documentation</link> for more information.
371 '';
372 };
373 buildsDir = mkOption {
374 type = types.nullOr types.path;
375 default = null;
376 example = "/var/lib/gitlab-runner/builds";
377 description = ''
378 Absolute path to a directory where builds will be stored
379 in context of selected executor (Locally, Docker, SSH).
380 '';
381 };
382 cloneUrl = mkOption {
383 type = types.nullOr types.str;
384 default = null;
385 example = "http://gitlab.example.local";
386 description = ''
387 Overwrite the URL for the GitLab instance. Used if the Runner can’t connect to GitLab on the URL GitLab exposes itself.
388 '';
389 };
390 dockerImage = mkOption {
391 type = types.nullOr types.str;
392 default = null;
393 description = ''
394 Docker image to be used.
395 '';
396 };
397 dockerVolumes = mkOption {
398 type = types.listOf types.str;
399 default = [ ];
400 example = [ "/var/run/docker.sock:/var/run/docker.sock" ];
401 description = ''
402 Bind-mount a volume and create it
403 if it doesn't exist prior to mounting.
404 '';
405 };
406 dockerDisableCache = mkOption {
407 type = types.bool;
408 default = false;
409 description = ''
410 Disable all container caching.
411 '';
412 };
413 dockerPrivileged = mkOption {
414 type = types.bool;
415 default = false;
416 description = ''
417 Give extended privileges to container.
418 '';
419 };
420 dockerExtraHosts = mkOption {
421 type = types.listOf types.str;
422 default = [ ];
423 example = [ "other-host:127.0.0.1" ];
424 description = ''
425 Add a custom host-to-IP mapping.
426 '';
427 };
428 dockerAllowedImages = mkOption {
429 type = types.listOf types.str;
430 default = [ ];
431 example = [ "ruby:*" "python:*" "php:*" "my.registry.tld:5000/*:*" ];
432 description = ''
433 Whitelist allowed images.
434 '';
435 };
436 dockerAllowedServices = mkOption {
437 type = types.listOf types.str;
438 default = [ ];
439 example = [ "postgres:9" "redis:*" "mysql:*" ];
440 description = ''
441 Whitelist allowed services.
442 '';
443 };
444 preCloneScript = mkOption {
445 type = types.nullOr types.path;
446 default = null;
447 description = ''
448 Runner-specific command script executed before code is pulled.
449 '';
450 };
451 preBuildScript = mkOption {
452 type = types.nullOr types.path;
453 default = null;
454 description = ''
455 Runner-specific command script executed after code is pulled,
456 just before build executes.
457 '';
458 };
459 postBuildScript = mkOption {
460 type = types.nullOr types.path;
461 default = null;
462 description = ''
463 Runner-specific command script executed after code is pulled
464 and just after build executes.
465 '';
466 };
467 tagList = mkOption {
468 type = types.listOf types.str;
469 default = [ ];
470 description = ''
471 Tag list.
472 '';
473 };
474 runUntagged = mkOption {
475 type = types.bool;
476 default = false;
477 description = ''
478 Register to run untagged builds; defaults to
479 <literal>true</literal> when <option>tagList</option> is empty.
480 '';
481 };
482 limit = mkOption {
483 type = types.int;
484 default = 0;
485 description = ''
486 Limit how many jobs can be handled concurrently by this service.
487 0 (default) simply means don't limit.
488 '';
489 };
490 requestConcurrency = mkOption {
491 type = types.int;
492 default = 0;
493 description = ''
494 Limit number of concurrent requests for new jobs from GitLab.
495 '';
496 };
497 maximumTimeout = mkOption {
498 type = types.int;
499 default = 0;
500 description = ''
501 What is the maximum timeout (in seconds) that will be set for
502 job when using this Runner. 0 (default) simply means don't limit.
503 '';
504 };
505 protected = mkOption {
506 type = types.bool;
507 default = false;
508 description = ''
509 When set to true Runner will only run on pipelines
510 triggered on protected branches.
511 '';
512 };
513 debugTraceDisabled = mkOption {
514 type = types.bool;
515 default = false;
516 description = ''
517 When set to true Runner will disable the possibility of
518 using the <literal>CI_DEBUG_TRACE</literal> feature.
519 '';
520 };
521 };
522 });
523 };
524 };
525 config = mkIf cfg.enable {
526 warnings = optional (cfg.configFile != null) "services.gitlab-runner.`configFile` is deprecated, please use services.gitlab-runner.`services`.";
527 environment.systemPackages = [ cfg.package ];
528 systemd.services.gitlab-runner = {
529 description = "Gitlab Runner";
530 documentation = [ "https://docs.gitlab.com/runner/" ];
531 after = [ "network.target" ]
532 ++ optional hasDocker "docker.service";
533 requires = optional hasDocker "docker.service";
534 wantedBy = [ "multi-user.target" ];
535 environment = config.networking.proxy.envVars // {
536 HOME = "/var/lib/gitlab-runner";
537 };
538 path = with pkgs; [
539 bash
540 gawk
541 jq
542 moreutils
543 remarshal
544 util-linux
545 cfg.package
546 ] ++ cfg.extraPackages;
547 reloadIfChanged = true;
548 serviceConfig = {
549 # Set `DynamicUser` under `systemd.services.gitlab-runner.serviceConfig`
550 # to `lib.mkForce false` in your configuration to run this service as root.
551 # You can also set `User` and `Group` options to run this service as desired user.
552 # Make sure to restart service or changes won't apply.
553 DynamicUser = true;
554 StateDirectory = "gitlab-runner";
555 SupplementaryGroups = optional hasDocker "docker";
556 ExecStartPre = "!${configureScript}/bin/gitlab-runner-configure";
557 ExecStart = "${startScript}/bin/gitlab-runner-start";
558 ExecReload = "!${configureScript}/bin/gitlab-runner-configure";
559 } // optionalAttrs (cfg.gracefulTermination) {
560 TimeoutStopSec = "${cfg.gracefulTimeout}";
561 KillSignal = "SIGQUIT";
562 KillMode = "process";
563 };
564 };
565 # Enable docker if `docker` executor is used in any service
566 virtualisation.docker.enable = mkIf (
567 any (s: s.executor == "docker") (attrValues cfg.services)
568 ) (mkDefault true);
569 };
570 imports = [
571 (mkRenamedOptionModule [ "services" "gitlab-runner" "packages" ] [ "services" "gitlab-runner" "extraPackages" ] )
572 (mkRemovedOptionModule [ "services" "gitlab-runner" "configOptions" ] "Use services.gitlab-runner.services option instead" )
573 (mkRemovedOptionModule [ "services" "gitlab-runner" "workDir" ] "You should move contents of workDir (if any) to /var/lib/gitlab-runner" )
574 ];
575}