at 23.11-pre 24 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 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 (lib.mdDoc "Gitlab Runner"); 155 configFile = mkOption { 156 type = types.nullOr types.path; 157 default = null; 158 description = lib.mdDoc '' 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 = lib.mdDoc '' 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 = lib.mdDoc '' 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 = lib.mdDoc '' 195 Time to wait until a graceful shutdown is turned into a forceful one. 196 ''; 197 }; 198 package = mkOption { 199 type = types.package; 200 default = pkgs.gitlab-runner; 201 defaultText = literalExpression "pkgs.gitlab-runner"; 202 example = literalExpression "pkgs.gitlab-runner_1_11"; 203 description = lib.mdDoc "Gitlab Runner package to use."; 204 }; 205 extraPackages = mkOption { 206 type = types.listOf types.package; 207 default = [ ]; 208 description = lib.mdDoc '' 209 Extra packages to add to PATH for the gitlab-runner process. 210 ''; 211 }; 212 services = mkOption { 213 description = lib.mdDoc "GitLab Runner services."; 214 default = { }; 215 example = literalExpression '' 216 { 217 # runner for building in docker via host's nix-daemon 218 # nix store will be readable in runner, might be insecure 219 nix = { 220 # File should contain at least these two variables: 221 # `CI_SERVER_URL` 222 # `REGISTRATION_TOKEN` 223 registrationConfigFile = "/run/secrets/gitlab-runner-registration"; 224 dockerImage = "alpine"; 225 dockerVolumes = [ 226 "/nix/store:/nix/store:ro" 227 "/nix/var/nix/db:/nix/var/nix/db:ro" 228 "/nix/var/nix/daemon-socket:/nix/var/nix/daemon-socket:ro" 229 ]; 230 dockerDisableCache = true; 231 preBuildScript = pkgs.writeScript "setup-container" ''' 232 mkdir -p -m 0755 /nix/var/log/nix/drvs 233 mkdir -p -m 0755 /nix/var/nix/gcroots 234 mkdir -p -m 0755 /nix/var/nix/profiles 235 mkdir -p -m 0755 /nix/var/nix/temproots 236 mkdir -p -m 0755 /nix/var/nix/userpool 237 mkdir -p -m 1777 /nix/var/nix/gcroots/per-user 238 mkdir -p -m 1777 /nix/var/nix/profiles/per-user 239 mkdir -p -m 0755 /nix/var/nix/profiles/per-user/root 240 mkdir -p -m 0700 "$HOME/.nix-defexpr" 241 242 . ''${pkgs.nix}/etc/profile.d/nix.sh 243 244 ''${pkgs.nix}/bin/nix-env -i ''${concatStringsSep " " (with pkgs; [ nix cacert git openssh ])} 245 246 ''${pkgs.nix}/bin/nix-channel --add https://nixos.org/channels/nixpkgs-unstable 247 ''${pkgs.nix}/bin/nix-channel --update nixpkgs 248 '''; 249 environmentVariables = { 250 ENV = "/etc/profile"; 251 USER = "root"; 252 NIX_REMOTE = "daemon"; 253 PATH = "/nix/var/nix/profiles/default/bin:/nix/var/nix/profiles/default/sbin:/bin:/sbin:/usr/bin:/usr/sbin"; 254 NIX_SSL_CERT_FILE = "/nix/var/nix/profiles/default/etc/ssl/certs/ca-bundle.crt"; 255 }; 256 tagList = [ "nix" ]; 257 }; 258 # runner for building docker images 259 docker-images = { 260 # File should contain at least these two variables: 261 # `CI_SERVER_URL` 262 # `REGISTRATION_TOKEN` 263 registrationConfigFile = "/run/secrets/gitlab-runner-registration"; 264 dockerImage = "docker:stable"; 265 dockerVolumes = [ 266 "/var/run/docker.sock:/var/run/docker.sock" 267 ]; 268 tagList = [ "docker-images" ]; 269 }; 270 # runner for executing stuff on host system (very insecure!) 271 # make sure to add required packages (including git!) 272 # to `environment.systemPackages` 273 shell = { 274 # File should contain at least these two variables: 275 # `CI_SERVER_URL` 276 # `REGISTRATION_TOKEN` 277 registrationConfigFile = "/run/secrets/gitlab-runner-registration"; 278 executor = "shell"; 279 tagList = [ "shell" ]; 280 }; 281 # runner for everything else 282 default = { 283 # File should contain at least these two variables: 284 # `CI_SERVER_URL` 285 # `REGISTRATION_TOKEN` 286 registrationConfigFile = "/run/secrets/gitlab-runner-registration"; 287 dockerImage = "debian:stable"; 288 }; 289 } 290 ''; 291 type = types.attrsOf (types.submodule { 292 options = { 293 registrationConfigFile = mkOption { 294 type = types.path; 295 description = lib.mdDoc '' 296 Absolute path to a file with environment variables 297 used for gitlab-runner registration. 298 A list of all supported environment variables can be found in 299 `gitlab-runner register --help`. 300 301 Ones that you probably want to set is 302 303 `CI_SERVER_URL=<CI server URL>` 304 305 `REGISTRATION_TOKEN=<registration secret>` 306 307 WARNING: make sure to use quoted absolute path, 308 or it is going to be copied to Nix Store. 309 ''; 310 }; 311 registrationFlags = mkOption { 312 type = types.listOf types.str; 313 default = [ ]; 314 example = [ "--docker-helper-image my/gitlab-runner-helper" ]; 315 description = lib.mdDoc '' 316 Extra command-line flags passed to 317 `gitlab-runner register`. 318 Execute `gitlab-runner register --help` 319 for a list of supported flags. 320 ''; 321 }; 322 environmentVariables = mkOption { 323 type = types.attrsOf types.str; 324 default = { }; 325 example = { NAME = "value"; }; 326 description = lib.mdDoc '' 327 Custom environment variables injected to build environment. 328 For secrets you can use {option}`registrationConfigFile` 329 with `RUNNER_ENV` variable set. 330 ''; 331 }; 332 description = mkOption { 333 type = types.nullOr types.str; 334 default = null; 335 description = lib.mdDoc '' 336 Name/description of the runner. 337 ''; 338 }; 339 executor = mkOption { 340 type = types.str; 341 default = "docker"; 342 description = lib.mdDoc '' 343 Select executor, eg. shell, docker, etc. 344 See [runner documentation](https://docs.gitlab.com/runner/executors/README.html) for more information. 345 ''; 346 }; 347 buildsDir = mkOption { 348 type = types.nullOr types.path; 349 default = null; 350 example = "/var/lib/gitlab-runner/builds"; 351 description = lib.mdDoc '' 352 Absolute path to a directory where builds will be stored 353 in context of selected executor (Locally, Docker, SSH). 354 ''; 355 }; 356 cloneUrl = mkOption { 357 type = types.nullOr types.str; 358 default = null; 359 example = "http://gitlab.example.local"; 360 description = lib.mdDoc '' 361 Overwrite the URL for the GitLab instance. Used if the Runner cant connect to GitLab on the URL GitLab exposes itself. 362 ''; 363 }; 364 dockerImage = mkOption { 365 type = types.nullOr types.str; 366 default = null; 367 description = lib.mdDoc '' 368 Docker image to be used. 369 ''; 370 }; 371 dockerVolumes = mkOption { 372 type = types.listOf types.str; 373 default = [ ]; 374 example = [ "/var/run/docker.sock:/var/run/docker.sock" ]; 375 description = lib.mdDoc '' 376 Bind-mount a volume and create it 377 if it doesn't exist prior to mounting. 378 ''; 379 }; 380 dockerDisableCache = mkOption { 381 type = types.bool; 382 default = false; 383 description = lib.mdDoc '' 384 Disable all container caching. 385 ''; 386 }; 387 dockerPrivileged = mkOption { 388 type = types.bool; 389 default = false; 390 description = lib.mdDoc '' 391 Give extended privileges to container. 392 ''; 393 }; 394 dockerExtraHosts = mkOption { 395 type = types.listOf types.str; 396 default = [ ]; 397 example = [ "other-host:127.0.0.1" ]; 398 description = lib.mdDoc '' 399 Add a custom host-to-IP mapping. 400 ''; 401 }; 402 dockerAllowedImages = mkOption { 403 type = types.listOf types.str; 404 default = [ ]; 405 example = [ "ruby:*" "python:*" "php:*" "my.registry.tld:5000/*:*" ]; 406 description = lib.mdDoc '' 407 Whitelist allowed images. 408 ''; 409 }; 410 dockerAllowedServices = mkOption { 411 type = types.listOf types.str; 412 default = [ ]; 413 example = [ "postgres:9" "redis:*" "mysql:*" ]; 414 description = lib.mdDoc '' 415 Whitelist allowed services. 416 ''; 417 }; 418 preCloneScript = mkOption { 419 type = types.nullOr types.path; 420 default = null; 421 description = lib.mdDoc '' 422 Runner-specific command script executed before code is pulled. 423 ''; 424 }; 425 preBuildScript = mkOption { 426 type = types.nullOr types.path; 427 default = null; 428 description = lib.mdDoc '' 429 Runner-specific command script executed after code is pulled, 430 just before build executes. 431 ''; 432 }; 433 postBuildScript = mkOption { 434 type = types.nullOr types.path; 435 default = null; 436 description = lib.mdDoc '' 437 Runner-specific command script executed after code is pulled 438 and just after build executes. 439 ''; 440 }; 441 tagList = mkOption { 442 type = types.listOf types.str; 443 default = [ ]; 444 description = lib.mdDoc '' 445 Tag list. 446 ''; 447 }; 448 runUntagged = mkOption { 449 type = types.bool; 450 default = false; 451 description = lib.mdDoc '' 452 Register to run untagged builds; defaults to 453 `true` when {option}`tagList` is empty. 454 ''; 455 }; 456 limit = mkOption { 457 type = types.int; 458 default = 0; 459 description = lib.mdDoc '' 460 Limit how many jobs can be handled concurrently by this service. 461 0 (default) simply means don't limit. 462 ''; 463 }; 464 requestConcurrency = mkOption { 465 type = types.int; 466 default = 0; 467 description = lib.mdDoc '' 468 Limit number of concurrent requests for new jobs from GitLab. 469 ''; 470 }; 471 maximumTimeout = mkOption { 472 type = types.int; 473 default = 0; 474 description = lib.mdDoc '' 475 What is the maximum timeout (in seconds) that will be set for 476 job when using this Runner. 0 (default) simply means don't limit. 477 ''; 478 }; 479 protected = mkOption { 480 type = types.bool; 481 default = false; 482 description = lib.mdDoc '' 483 When set to true Runner will only run on pipelines 484 triggered on protected branches. 485 ''; 486 }; 487 debugTraceDisabled = mkOption { 488 type = types.bool; 489 default = false; 490 description = lib.mdDoc '' 491 When set to true Runner will disable the possibility of 492 using the `CI_DEBUG_TRACE` feature. 493 ''; 494 }; 495 }; 496 }); 497 }; 498 clear-docker-cache = { 499 enable = mkOption { 500 type = types.bool; 501 default = false; 502 description = lib.mdDoc '' 503 Whether to periodically prune gitlab runner's Docker resources. If 504 enabled, a systemd timer will run {command}`clear-docker-cache` as 505 specified by the `dates` option. 506 ''; 507 }; 508 509 flags = mkOption { 510 type = types.listOf types.str; 511 default = [ ]; 512 example = [ "prune" ]; 513 description = lib.mdDoc '' 514 Any additional flags passed to {command}`clear-docker-cache`. 515 ''; 516 }; 517 518 dates = mkOption { 519 default = "weekly"; 520 type = types.str; 521 description = lib.mdDoc '' 522 Specification (in the format described by 523 {manpage}`systemd.time(7)`) of the time at 524 which the prune will occur. 525 ''; 526 }; 527 528 package = mkOption { 529 default = config.virtualisation.docker.package; 530 defaultText = literalExpression "config.virtualisation.docker.package"; 531 example = literalExpression "pkgs.docker"; 532 description = lib.mdDoc "Docker package to use for clearing up docker cache."; 533 }; 534 }; 535 }; 536 config = mkIf cfg.enable { 537 warnings = mapAttrsToList 538 (n: v: "services.gitlab-runner.services.${n}.`registrationConfigFile` points to a file in Nix Store. You should use quoted absolute path to prevent this.") 539 (filterAttrs (n: v: isStorePath v.registrationConfigFile) cfg.services); 540 541 environment.systemPackages = [ cfg.package ]; 542 systemd.services.gitlab-runner = { 543 description = "Gitlab Runner"; 544 documentation = [ "https://docs.gitlab.com/runner/" ]; 545 after = [ "network.target" ] 546 ++ optional hasDocker "docker.service"; 547 requires = optional hasDocker "docker.service"; 548 wantedBy = [ "multi-user.target" ]; 549 environment = config.networking.proxy.envVars // { 550 HOME = "/var/lib/gitlab-runner"; 551 }; 552 path = with pkgs; [ 553 bash 554 gawk 555 jq 556 moreutils 557 remarshal 558 util-linux 559 cfg.package 560 ] ++ cfg.extraPackages; 561 reloadIfChanged = true; 562 serviceConfig = { 563 # Set `DynamicUser` under `systemd.services.gitlab-runner.serviceConfig` 564 # to `lib.mkForce false` in your configuration to run this service as root. 565 # You can also set `User` and `Group` options to run this service as desired user. 566 # Make sure to restart service or changes won't apply. 567 DynamicUser = true; 568 StateDirectory = "gitlab-runner"; 569 SupplementaryGroups = optional hasDocker "docker"; 570 ExecStartPre = "!${configureScript}/bin/gitlab-runner-configure"; 571 ExecStart = "${startScript}/bin/gitlab-runner-start"; 572 ExecReload = "!${configureScript}/bin/gitlab-runner-configure"; 573 } // optionalAttrs cfg.gracefulTermination { 574 TimeoutStopSec = "${cfg.gracefulTimeout}"; 575 KillSignal = "SIGQUIT"; 576 KillMode = "process"; 577 }; 578 }; 579 # Enable periodic clear-docker-cache script 580 systemd.services.gitlab-runner-clear-docker-cache = mkIf (cfg.clear-docker-cache.enable && (any (s: s.executor == "docker") (attrValues cfg.services))) { 581 description = "Prune gitlab-runner docker resources"; 582 restartIfChanged = false; 583 unitConfig.X-StopOnRemoval = false; 584 585 serviceConfig.Type = "oneshot"; 586 587 path = [ cfg.clear-docker-cache.package pkgs.gawk ]; 588 589 script = '' 590 ${pkgs.gitlab-runner}/bin/clear-docker-cache ${toString cfg.clear-docker-cache.flags} 591 ''; 592 593 startAt = cfg.clear-docker-cache.dates; 594 }; 595 # Enable docker if `docker` executor is used in any service 596 virtualisation.docker.enable = mkIf ( 597 any (s: s.executor == "docker") (attrValues cfg.services) 598 ) (mkDefault true); 599 }; 600 imports = [ 601 (mkRenamedOptionModule [ "services" "gitlab-runner" "packages" ] [ "services" "gitlab-runner" "extraPackages" ] ) 602 (mkRemovedOptionModule [ "services" "gitlab-runner" "configOptions" ] "Use services.gitlab-runner.services option instead" ) 603 (mkRemovedOptionModule [ "services" "gitlab-runner" "workDir" ] "You should move contents of workDir (if any) to /var/lib/gitlab-runner" ) 604 605 (mkRenamedOptionModule [ "services" "gitlab-runner" "checkInterval" ] [ "services" "gitlab-runner" "settings" "check_interval" ] ) 606 (mkRenamedOptionModule [ "services" "gitlab-runner" "concurrent" ] [ "services" "gitlab-runner" "settings" "concurrent" ] ) 607 (mkRenamedOptionModule [ "services" "gitlab-runner" "sentryDSN" ] [ "services" "gitlab-runner" "settings" "sentry_dsn" ] ) 608 (mkRenamedOptionModule [ "services" "gitlab-runner" "prometheusListenAddress" ] [ "services" "gitlab-runner" "settings" "listen_address" ] ) 609 610 (mkRenamedOptionModule [ "services" "gitlab-runner" "sessionServer" "listenAddress" ] [ "services" "gitlab-runner" "settings" "session_server" "listen_address" ] ) 611 (mkRenamedOptionModule [ "services" "gitlab-runner" "sessionServer" "advertiseAddress" ] [ "services" "gitlab-runner" "settings" "session_server" "advertise_address" ] ) 612 (mkRenamedOptionModule [ "services" "gitlab-runner" "sessionServer" "sessionTimeout" ] [ "services" "gitlab-runner" "settings" "session_server" "session_timeout" ] ) 613 ]; 614}