at 22.05-pre 23 kB view raw
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 (&lt;host&gt;:&lt;port&gt;) 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=&lt;CI server URL&gt;</literal> 340 341 <literal>REGISTRATION_TOKEN=&lt;registration secret&gt;</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 cant 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}