at 24.11-pre 61 kB view raw
1{ config, lib, options, pkgs, utils, ... }: 2 3with lib; 4 5let 6 cfg = config.services.gitlab; 7 opt = options.services.gitlab; 8 9 toml = pkgs.formats.toml {}; 10 yaml = pkgs.formats.yaml {}; 11 12 postgresqlPackage = if config.services.postgresql.enable then 13 config.services.postgresql.package 14 else 15 pkgs.postgresql_13; 16 17 gitlabSocket = "${cfg.statePath}/tmp/sockets/gitlab.socket"; 18 gitalySocket = "${cfg.statePath}/tmp/sockets/gitaly.socket"; 19 pathUrlQuote = url: replaceStrings ["/"] ["%2F"] url; 20 21 gitlabVersionAtLeast = version: lib.versionAtLeast (lib.getVersion cfg.packages.gitlab) version; 22 23 databaseConfig = let 24 val = { 25 adapter = "postgresql"; 26 database = cfg.databaseName; 27 host = cfg.databaseHost; 28 username = cfg.databaseUsername; 29 encoding = "utf8"; 30 pool = cfg.databasePool; 31 } // cfg.extraDatabaseConfig; 32 in { 33 production = ( 34 if (gitlabVersionAtLeast "15.0") 35 then { main = val; } 36 else val 37 ) // lib.optionalAttrs (gitlabVersionAtLeast "15.9") { 38 ci = val // { 39 database_tasks = false; 40 }; 41 }; 42 }; 43 44 # We only want to create a database if we're actually going to connect to it. 45 databaseActuallyCreateLocally = cfg.databaseCreateLocally && cfg.databaseHost == ""; 46 47 gitalyToml = pkgs.writeText "gitaly.toml" '' 48 socket_path = "${lib.escape ["\""] gitalySocket}" 49 runtime_dir = "/run/gitaly" 50 bin_dir = "${cfg.packages.gitaly}/bin" 51 prometheus_listen_addr = "localhost:9236" 52 53 [git] 54 bin_path = "${pkgs.git}/bin/git" 55 56 [gitlab-shell] 57 dir = "${cfg.packages.gitlab-shell}" 58 59 [hooks] 60 custom_hooks_dir = "${cfg.statePath}/custom_hooks" 61 62 [gitlab] 63 secret_file = "${cfg.statePath}/gitlab_shell_secret" 64 url = "http+unix://${pathUrlQuote gitlabSocket}" 65 66 [gitlab.http-settings] 67 self_signed_cert = false 68 69 ${concatStringsSep "\n" (attrValues (mapAttrs (k: v: '' 70 [[storage]] 71 name = "${lib.escape ["\""] k}" 72 path = "${lib.escape ["\""] v.path}" 73 '') gitlabConfig.production.repositories.storages))} 74 ''; 75 76 gitlabShellConfig = flip recursiveUpdate cfg.extraShellConfig { 77 user = cfg.user; 78 gitlab_url = "http+unix://${pathUrlQuote gitlabSocket}"; 79 http_settings.self_signed_cert = false; 80 repos_path = "${cfg.statePath}/repositories"; 81 secret_file = "${cfg.statePath}/gitlab_shell_secret"; 82 log_file = "${cfg.statePath}/log/gitlab-shell.log"; 83 }; 84 85 redisConfig.production.url = cfg.redisUrl; 86 87 cableYml = yaml.generate "cable.yml" { 88 production = { 89 adapter = "redis"; 90 url = cfg.redisUrl; 91 channel_prefix = "gitlab_production"; 92 }; 93 }; 94 95 # Redis configuration file 96 resqueYml = pkgs.writeText "resque.yml" (builtins.toJSON redisConfig); 97 98 gitlabConfig = { 99 # These are the default settings from config/gitlab.example.yml 100 production = flip recursiveUpdate cfg.extraConfig { 101 gitlab = { 102 host = cfg.host; 103 port = cfg.port; 104 https = cfg.https; 105 user = cfg.user; 106 email_enabled = true; 107 email_display_name = "GitLab"; 108 email_reply_to = "noreply@localhost"; 109 default_theme = 2; 110 default_projects_features = { 111 issues = true; 112 merge_requests = true; 113 wiki = true; 114 snippets = true; 115 builds = true; 116 container_registry = true; 117 }; 118 }; 119 repositories.storages.default.path = "${cfg.statePath}/repositories"; 120 repositories.storages.default.gitaly_address = "unix:${gitalySocket}"; 121 artifacts.enabled = true; 122 lfs.enabled = true; 123 gravatar.enabled = true; 124 cron_jobs = { }; 125 gitlab_ci.builds_path = "${cfg.statePath}/builds"; 126 ldap.enabled = false; 127 omniauth.enabled = false; 128 shared.path = "${cfg.statePath}/shared"; 129 gitaly.client_path = "${cfg.packages.gitaly}/bin"; 130 backup = { 131 gitaly_backup_path = "${cfg.packages.gitaly}/bin/gitaly-backup"; 132 path = cfg.backup.path; 133 keep_time = cfg.backup.keepTime; 134 } // (optionalAttrs (cfg.backup.uploadOptions != {}) { 135 upload = cfg.backup.uploadOptions; 136 }); 137 gitlab_shell = { 138 path = "${cfg.packages.gitlab-shell}"; 139 hooks_path = "${cfg.statePath}/shell/hooks"; 140 secret_file = "${cfg.statePath}/gitlab_shell_secret"; 141 upload_pack = true; 142 receive_pack = true; 143 }; 144 workhorse.secret_file = "${cfg.statePath}/.gitlab_workhorse_secret"; 145 gitlab_kas.secret_file = "${cfg.statePath}/.gitlab_kas_secret"; 146 git.bin_path = "git"; 147 monitoring = { 148 ip_whitelist = [ "127.0.0.0/8" "::1/128" ]; 149 sidekiq_exporter = { 150 enable = true; 151 address = "localhost"; 152 port = 3807; 153 }; 154 }; 155 registry = lib.optionalAttrs cfg.registry.enable { 156 enabled = true; 157 host = cfg.registry.externalAddress; 158 port = cfg.registry.externalPort; 159 key = cfg.registry.keyFile; 160 api_url = "http://${config.services.dockerRegistry.listenAddress}:${toString config.services.dockerRegistry.port}/"; 161 issuer = cfg.registry.issuer; 162 }; 163 elasticsearch.indexer_path = "${pkgs.gitlab-elasticsearch-indexer}/bin/gitlab-elasticsearch-indexer"; 164 extra = {}; 165 uploads.storage_path = cfg.statePath; 166 pages = optionalAttrs cfg.pages.enable { 167 enabled = cfg.pages.enable; 168 port = 8090; 169 host = cfg.pages.settings.pages-domain; 170 secret_file = cfg.pages.settings.api-secret-key; 171 }; 172 }; 173 }; 174 175 gitlabEnv = cfg.packages.gitlab.gitlabEnv // { 176 HOME = "${cfg.statePath}/home"; 177 PUMA_PATH = "${cfg.statePath}/"; 178 GITLAB_PATH = "${cfg.packages.gitlab}/share/gitlab/"; 179 SCHEMA = "${cfg.statePath}/db/structure.sql"; 180 GITLAB_UPLOADS_PATH = "${cfg.statePath}/uploads"; 181 GITLAB_LOG_PATH = "${cfg.statePath}/log"; 182 prometheus_multiproc_dir = "/run/gitlab"; 183 RAILS_ENV = "production"; 184 MALLOC_ARENA_MAX = "2"; 185 } // cfg.extraEnv; 186 187 runtimeDeps = with pkgs; [ 188 nodejs 189 gzip 190 git 191 gnutar 192 postgresqlPackage 193 coreutils 194 procps 195 findutils # Needed for gitlab:cleanup:orphan_job_artifact_files 196 ]; 197 198 gitlab-rake = pkgs.stdenv.mkDerivation { 199 name = "gitlab-rake"; 200 nativeBuildInputs = [ pkgs.makeWrapper ]; 201 dontBuild = true; 202 dontUnpack = true; 203 installPhase = '' 204 mkdir -p $out/bin 205 makeWrapper ${cfg.packages.gitlab.rubyEnv}/bin/rake $out/bin/gitlab-rake \ 206 ${concatStrings (mapAttrsToList (name: value: "--set ${name} '${value}' ") gitlabEnv)} \ 207 --set PATH '${lib.makeBinPath runtimeDeps}:$PATH' \ 208 --set RAKEOPT '-f ${cfg.packages.gitlab}/share/gitlab/Rakefile' \ 209 --chdir '${cfg.packages.gitlab}/share/gitlab' 210 ''; 211 }; 212 213 gitlab-rails = pkgs.stdenv.mkDerivation { 214 name = "gitlab-rails"; 215 nativeBuildInputs = [ pkgs.makeWrapper ]; 216 dontBuild = true; 217 dontUnpack = true; 218 installPhase = '' 219 mkdir -p $out/bin 220 makeWrapper ${cfg.packages.gitlab.rubyEnv}/bin/rails $out/bin/gitlab-rails \ 221 ${concatStrings (mapAttrsToList (name: value: "--set ${name} '${value}' ") gitlabEnv)} \ 222 --set PATH '${lib.makeBinPath runtimeDeps}:$PATH' \ 223 --chdir '${cfg.packages.gitlab}/share/gitlab' 224 ''; 225 }; 226 227 extraGitlabRb = pkgs.writeText "extra-gitlab.rb" cfg.extraGitlabRb; 228 229 smtpSettings = pkgs.writeText "gitlab-smtp-settings.rb" '' 230 if Rails.env.production? 231 Rails.application.config.action_mailer.delivery_method = :smtp 232 233 ActionMailer::Base.delivery_method = :smtp 234 ActionMailer::Base.smtp_settings = { 235 address: "${cfg.smtp.address}", 236 port: ${toString cfg.smtp.port}, 237 ${optionalString (cfg.smtp.username != null) ''user_name: "${cfg.smtp.username}",''} 238 ${optionalString (cfg.smtp.passwordFile != null) ''password: "@smtpPassword@",''} 239 domain: "${cfg.smtp.domain}", 240 ${optionalString (cfg.smtp.authentication != null) "authentication: :${cfg.smtp.authentication},"} 241 enable_starttls_auto: ${boolToString cfg.smtp.enableStartTLSAuto}, 242 tls: ${boolToString cfg.smtp.tls}, 243 ca_file: "/etc/ssl/certs/ca-certificates.crt", 244 openssl_verify_mode: '${cfg.smtp.opensslVerifyMode}' 245 } 246 end 247 ''; 248 249in { 250 251 imports = [ 252 (mkRenamedOptionModule [ "services" "gitlab" "stateDir" ] [ "services" "gitlab" "statePath" ]) 253 (mkRenamedOptionModule [ "services" "gitlab" "backupPath" ] [ "services" "gitlab" "backup" "path" ]) 254 (mkRemovedOptionModule [ "services" "gitlab" "satelliteDir" ] "") 255 (mkRemovedOptionModule [ "services" "gitlab" "logrotate" "extraConfig" ] "Modify services.logrotate.settings.gitlab directly instead") 256 (mkRemovedOptionModule [ "services" "gitlab" "pagesExtraArgs" ] "Use services.gitlab.pages.settings instead") 257 ]; 258 259 options = { 260 services.gitlab = { 261 enable = mkOption { 262 type = types.bool; 263 default = false; 264 description = '' 265 Enable the gitlab service. 266 ''; 267 }; 268 269 packages.gitlab = mkPackageOption pkgs "gitlab" { 270 example = "gitlab-ee"; 271 }; 272 273 packages.gitlab-shell = mkPackageOption pkgs "gitlab-shell" { }; 274 275 packages.gitlab-workhorse = mkPackageOption pkgs "gitlab-workhorse" { }; 276 277 packages.gitaly = mkPackageOption pkgs "gitaly" { }; 278 279 packages.pages = mkPackageOption pkgs "gitlab-pages" { }; 280 281 statePath = mkOption { 282 type = types.str; 283 default = "/var/gitlab/state"; 284 description = '' 285 GitLab state directory. Configuration, repositories and 286 logs, among other things, are stored here. 287 288 The directory will be created automatically if it doesn't 289 exist already. Its parent directories must be owned by 290 either `root` or the user set in 291 {option}`services.gitlab.user`. 292 ''; 293 }; 294 295 extraEnv = mkOption { 296 type = types.attrsOf types.str; 297 default = {}; 298 description = '' 299 Additional environment variables for the GitLab environment. 300 ''; 301 }; 302 303 backup.startAt = mkOption { 304 type = with types; either str (listOf str); 305 default = []; 306 example = "03:00"; 307 description = '' 308 The time(s) to run automatic backup of GitLab 309 state. Specified in systemd's time format; see 310 {manpage}`systemd.time(7)`. 311 ''; 312 }; 313 314 backup.path = mkOption { 315 type = types.str; 316 default = cfg.statePath + "/backup"; 317 defaultText = literalExpression ''config.${opt.statePath} + "/backup"''; 318 description = "GitLab path for backups."; 319 }; 320 321 backup.keepTime = mkOption { 322 type = types.int; 323 default = 0; 324 example = 48; 325 apply = x: x * 60 * 60; 326 description = '' 327 How long to keep the backups around, in 328 hours. `0` means keep forever. 329 ''; 330 }; 331 332 backup.skip = mkOption { 333 type = with types; 334 let value = enum [ 335 "db" 336 "uploads" 337 "builds" 338 "artifacts" 339 "lfs" 340 "registry" 341 "pages" 342 "repositories" 343 "tar" 344 ]; 345 in 346 either value (listOf value); 347 default = []; 348 example = [ "artifacts" "lfs" ]; 349 apply = x: if isString x then x else concatStringsSep "," x; 350 description = '' 351 Directories to exclude from the backup. The example excludes 352 CI artifacts and LFS objects from the backups. The 353 `tar` option skips the creation of a tar 354 file. 355 356 Refer to <https://docs.gitlab.com/ee/raketasks/backup_restore.html#excluding-specific-directories-from-the-backup> 357 for more information. 358 ''; 359 }; 360 361 backup.uploadOptions = mkOption { 362 type = types.attrs; 363 default = {}; 364 example = literalExpression '' 365 { 366 # Fog storage connection settings, see http://fog.io/storage/ 367 connection = { 368 provider = "AWS"; 369 region = "eu-north-1"; 370 aws_access_key_id = "AKIAXXXXXXXXXXXXXXXX"; 371 aws_secret_access_key = { _secret = config.deployment.keys.aws_access_key.path; }; 372 }; 373 374 # The remote 'directory' to store your backups in. 375 # For S3, this would be the bucket name. 376 remote_directory = "my-gitlab-backups"; 377 378 # Use multipart uploads when file size reaches 100MB, see 379 # http://docs.aws.amazon.com/AmazonS3/latest/dev/uploadobjusingmpu.html 380 multipart_chunk_size = 104857600; 381 382 # Turns on AWS Server-Side Encryption with Amazon S3-Managed Keys for backups, this is optional 383 encryption = "AES256"; 384 385 # Specifies Amazon S3 storage class to use for backups, this is optional 386 storage_class = "STANDARD"; 387 }; 388 ''; 389 description = '' 390 GitLab automatic upload specification. Tells GitLab to 391 upload the backup to a remote location when done. 392 393 Attributes specified here are added under 394 `production -> backup -> upload` in 395 {file}`config/gitlab.yml`. 396 ''; 397 }; 398 399 databaseHost = mkOption { 400 type = types.str; 401 default = ""; 402 description = '' 403 GitLab database hostname. An empty string means 404 use local unix socket connection. 405 ''; 406 }; 407 408 databasePasswordFile = mkOption { 409 type = with types; nullOr path; 410 default = null; 411 description = '' 412 File containing the GitLab database user password. 413 414 This should be a string, not a nix path, since nix paths are 415 copied into the world-readable nix store. 416 ''; 417 }; 418 419 databaseCreateLocally = mkOption { 420 type = types.bool; 421 default = true; 422 description = '' 423 Whether a database should be automatically created on the 424 local host. Set this to `false` if you plan 425 on provisioning a local database yourself. This has no effect 426 if {option}`services.gitlab.databaseHost` is customized. 427 ''; 428 }; 429 430 databaseName = mkOption { 431 type = types.str; 432 default = "gitlab"; 433 description = "GitLab database name."; 434 }; 435 436 databaseUsername = mkOption { 437 type = types.str; 438 default = "gitlab"; 439 description = "GitLab database user."; 440 }; 441 442 databasePool = mkOption { 443 type = types.int; 444 default = 5; 445 description = "Database connection pool size."; 446 }; 447 448 extraDatabaseConfig = mkOption { 449 type = types.attrs; 450 default = {}; 451 description = "Extra configuration in config/database.yml."; 452 }; 453 454 redisUrl = mkOption { 455 type = types.str; 456 default = "unix:/run/gitlab/redis.sock"; 457 example = "redis://localhost:6379/"; 458 description = "Redis URL for all GitLab services."; 459 }; 460 461 extraGitlabRb = mkOption { 462 type = types.str; 463 default = ""; 464 example = '' 465 if Rails.env.production? 466 Rails.application.config.action_mailer.delivery_method = :sendmail 467 ActionMailer::Base.delivery_method = :sendmail 468 ActionMailer::Base.sendmail_settings = { 469 location: "/run/wrappers/bin/sendmail", 470 arguments: "-i -t" 471 } 472 end 473 ''; 474 description = '' 475 Extra configuration to be placed in config/extra-gitlab.rb. This can 476 be used to add configuration not otherwise exposed through this module's 477 options. 478 ''; 479 }; 480 481 host = mkOption { 482 type = types.str; 483 default = config.networking.hostName; 484 defaultText = literalExpression "config.networking.hostName"; 485 description = "GitLab host name. Used e.g. for copy-paste URLs."; 486 }; 487 488 port = mkOption { 489 type = types.port; 490 default = 8080; 491 description = '' 492 GitLab server port for copy-paste URLs, e.g. 80 or 443 if you're 493 service over https. 494 ''; 495 }; 496 497 https = mkOption { 498 type = types.bool; 499 default = false; 500 description = "Whether gitlab prints URLs with https as scheme."; 501 }; 502 503 user = mkOption { 504 type = types.str; 505 default = "gitlab"; 506 description = "User to run gitlab and all related services."; 507 }; 508 509 group = mkOption { 510 type = types.str; 511 default = "gitlab"; 512 description = "Group to run gitlab and all related services."; 513 }; 514 515 initialRootEmail = mkOption { 516 type = types.str; 517 default = "admin@local.host"; 518 description = '' 519 Initial email address of the root account if this is a new install. 520 ''; 521 }; 522 523 initialRootPasswordFile = mkOption { 524 type = with types; nullOr path; 525 default = null; 526 description = '' 527 File containing the initial password of the root account if 528 this is a new install. 529 530 This should be a string, not a nix path, since nix paths are 531 copied into the world-readable nix store. 532 ''; 533 }; 534 535 registry = { 536 enable = mkOption { 537 type = types.bool; 538 default = false; 539 description = "Enable GitLab container registry."; 540 }; 541 package = mkOption { 542 type = types.package; 543 default = 544 if versionAtLeast config.system.stateVersion "23.11" 545 then pkgs.gitlab-container-registry 546 else pkgs.docker-distribution; 547 defaultText = literalExpression "pkgs.docker-distribution"; 548 description = '' 549 Container registry package to use. 550 551 External container registries such as `pkgs.docker-distribution` are not supported 552 anymore since GitLab 16.0.0. 553 ''; 554 }; 555 host = mkOption { 556 type = types.str; 557 default = config.services.gitlab.host; 558 defaultText = literalExpression "config.services.gitlab.host"; 559 description = "GitLab container registry host name."; 560 }; 561 port = mkOption { 562 type = types.port; 563 default = 4567; 564 description = "GitLab container registry port."; 565 }; 566 certFile = mkOption { 567 type = types.path; 568 description = "Path to GitLab container registry certificate."; 569 }; 570 keyFile = mkOption { 571 type = types.path; 572 description = "Path to GitLab container registry certificate-key."; 573 }; 574 defaultForProjects = mkOption { 575 type = types.bool; 576 default = cfg.registry.enable; 577 defaultText = literalExpression "config.${opt.registry.enable}"; 578 description = "If GitLab container registry should be enabled by default for projects."; 579 }; 580 issuer = mkOption { 581 type = types.str; 582 default = "gitlab-issuer"; 583 description = "GitLab container registry issuer."; 584 }; 585 serviceName = mkOption { 586 type = types.str; 587 default = "container_registry"; 588 description = "GitLab container registry service name."; 589 }; 590 externalAddress = mkOption { 591 type = types.str; 592 default = ""; 593 description = "External address used to access registry from the internet"; 594 }; 595 externalPort = mkOption { 596 type = types.int; 597 description = "External port used to access registry from the internet"; 598 }; 599 }; 600 601 smtp = { 602 enable = mkOption { 603 type = types.bool; 604 default = false; 605 description = "Enable gitlab mail delivery over SMTP."; 606 }; 607 608 address = mkOption { 609 type = types.str; 610 default = "localhost"; 611 description = "Address of the SMTP server for GitLab."; 612 }; 613 614 port = mkOption { 615 type = types.port; 616 default = 25; 617 description = "Port of the SMTP server for GitLab."; 618 }; 619 620 username = mkOption { 621 type = with types; nullOr str; 622 default = null; 623 description = "Username of the SMTP server for GitLab."; 624 }; 625 626 passwordFile = mkOption { 627 type = types.nullOr types.path; 628 default = null; 629 description = '' 630 File containing the password of the SMTP server for GitLab. 631 632 This should be a string, not a nix path, since nix paths 633 are copied into the world-readable nix store. 634 ''; 635 }; 636 637 domain = mkOption { 638 type = types.str; 639 default = "localhost"; 640 description = "HELO domain to use for outgoing mail."; 641 }; 642 643 authentication = mkOption { 644 type = with types; nullOr str; 645 default = null; 646 description = "Authentication type to use, see http://api.rubyonrails.org/classes/ActionMailer/Base.html"; 647 }; 648 649 enableStartTLSAuto = mkOption { 650 type = types.bool; 651 default = true; 652 description = "Whether to try to use StartTLS."; 653 }; 654 655 tls = mkOption { 656 type = types.bool; 657 default = false; 658 description = "Whether to use TLS wrapper-mode."; 659 }; 660 661 opensslVerifyMode = mkOption { 662 type = types.str; 663 default = "peer"; 664 description = "How OpenSSL checks the certificate, see http://api.rubyonrails.org/classes/ActionMailer/Base.html"; 665 }; 666 }; 667 668 pages.enable = mkEnableOption "the GitLab Pages service"; 669 670 pages.settings = mkOption { 671 example = literalExpression '' 672 { 673 pages-domain = "example.com"; 674 auth-client-id = "generated-id-xxxxxxx"; 675 auth-client-secret = { _secret = "/var/keys/auth-client-secret"; }; 676 auth-redirect-uri = "https://projects.example.com/auth"; 677 auth-secret = { _secret = "/var/keys/auth-secret"; }; 678 auth-server = "https://gitlab.example.com"; 679 } 680 ''; 681 682 description = '' 683 Configuration options to set in the GitLab Pages config 684 file. 685 686 Options containing secret data should be set to an attribute 687 set containing the attribute `_secret` - a string pointing 688 to a file containing the value the option should be set 689 to. See the example to get a better picture of this: in the 690 resulting configuration file, the `auth-client-secret` and 691 `auth-secret` keys will be set to the contents of the 692 {file}`/var/keys/auth-client-secret` and 693 {file}`/var/keys/auth-secret` files respectively. 694 ''; 695 696 type = types.submodule { 697 freeformType = with types; attrsOf (nullOr (oneOf [ str int bool attrs ])); 698 699 options = { 700 listen-http = mkOption { 701 type = with types; listOf str; 702 apply = x: if x == [] then null else lib.concatStringsSep "," x; 703 default = []; 704 description = '' 705 The address(es) to listen on for HTTP requests. 706 ''; 707 }; 708 709 listen-https = mkOption { 710 type = with types; listOf str; 711 apply = x: if x == [] then null else lib.concatStringsSep "," x; 712 default = []; 713 description = '' 714 The address(es) to listen on for HTTPS requests. 715 ''; 716 }; 717 718 listen-proxy = mkOption { 719 type = with types; listOf str; 720 apply = x: if x == [] then null else lib.concatStringsSep "," x; 721 default = [ "127.0.0.1:8090" ]; 722 description = '' 723 The address(es) to listen on for proxy requests. 724 ''; 725 }; 726 727 artifacts-server = mkOption { 728 type = with types; nullOr str; 729 default = "http${optionalString cfg.https "s"}://${cfg.host}/api/v4"; 730 defaultText = "http(s)://<services.gitlab.host>/api/v4"; 731 example = "https://gitlab.example.com/api/v4"; 732 description = '' 733 API URL to proxy artifact requests to. 734 ''; 735 }; 736 737 gitlab-server = mkOption { 738 type = with types; nullOr str; 739 default = "http${optionalString cfg.https "s"}://${cfg.host}"; 740 defaultText = "http(s)://<services.gitlab.host>"; 741 example = "https://gitlab.example.com"; 742 description = '' 743 Public GitLab server URL. 744 ''; 745 }; 746 747 internal-gitlab-server = mkOption { 748 type = with types; nullOr str; 749 default = null; 750 defaultText = "http(s)://<services.gitlab.host>"; 751 example = "https://gitlab.example.internal"; 752 description = '' 753 Internal GitLab server used for API requests, useful 754 if you want to send that traffic over an internal load 755 balancer. By default, the value of 756 `services.gitlab.pages.settings.gitlab-server` is 757 used. 758 ''; 759 }; 760 761 api-secret-key = mkOption { 762 type = with types; nullOr str; 763 default = "${cfg.statePath}/gitlab_pages_secret"; 764 internal = true; 765 description = '' 766 File with secret key used to authenticate with the 767 GitLab API. 768 ''; 769 }; 770 771 pages-domain = mkOption { 772 type = with types; nullOr str; 773 example = "example.com"; 774 description = '' 775 The domain to serve static pages on. 776 ''; 777 }; 778 779 pages-root = mkOption { 780 type = types.str; 781 default = "${gitlabConfig.production.shared.path}/pages"; 782 defaultText = literalExpression ''config.${opt.extraConfig}.production.shared.path + "/pages"''; 783 description = '' 784 The directory where pages are stored. 785 ''; 786 }; 787 }; 788 }; 789 }; 790 791 secrets.secretFile = mkOption { 792 type = with types; nullOr path; 793 default = null; 794 description = '' 795 A file containing the secret used to encrypt variables in 796 the DB. If you change or lose this key you will be unable to 797 access variables stored in database. 798 799 Make sure the secret is at least 32 characters and all random, 800 no regular words or you'll be exposed to dictionary attacks. 801 802 This should be a string, not a nix path, since nix paths are 803 copied into the world-readable nix store. 804 ''; 805 }; 806 807 secrets.dbFile = mkOption { 808 type = with types; nullOr path; 809 default = null; 810 description = '' 811 A file containing the secret used to encrypt variables in 812 the DB. If you change or lose this key you will be unable to 813 access variables stored in database. 814 815 Make sure the secret is at least 32 characters and all random, 816 no regular words or you'll be exposed to dictionary attacks. 817 818 This should be a string, not a nix path, since nix paths are 819 copied into the world-readable nix store. 820 ''; 821 }; 822 823 secrets.otpFile = mkOption { 824 type = with types; nullOr path; 825 default = null; 826 description = '' 827 A file containing the secret used to encrypt secrets for OTP 828 tokens. If you change or lose this key, users which have 2FA 829 enabled for login won't be able to login anymore. 830 831 Make sure the secret is at least 32 characters and all random, 832 no regular words or you'll be exposed to dictionary attacks. 833 834 This should be a string, not a nix path, since nix paths are 835 copied into the world-readable nix store. 836 ''; 837 }; 838 839 secrets.jwsFile = mkOption { 840 type = with types; nullOr path; 841 default = null; 842 description = '' 843 A file containing the secret used to encrypt session 844 keys. If you change or lose this key, users will be 845 disconnected. 846 847 Make sure the secret is an RSA private key in PEM format. You can 848 generate one with 849 850 openssl genrsa 2048 851 852 This should be a string, not a nix path, since nix paths are 853 copied into the world-readable nix store. 854 ''; 855 }; 856 857 extraShellConfig = mkOption { 858 type = types.attrs; 859 default = {}; 860 description = "Extra configuration to merge into shell-config.yml"; 861 }; 862 863 puma.workers = mkOption { 864 type = types.int; 865 default = 2; 866 apply = x: builtins.toString x; 867 description = '' 868 The number of worker processes Puma should spawn. This 869 controls the amount of parallel Ruby code can be 870 executed. GitLab recommends `Number of CPU cores - 1`, but at least two. 871 872 ::: {.note} 873 Each worker consumes quite a bit of memory, so 874 be careful when increasing this. 875 ::: 876 ''; 877 }; 878 879 puma.threadsMin = mkOption { 880 type = types.int; 881 default = 0; 882 apply = x: builtins.toString x; 883 description = '' 884 The minimum number of threads Puma should use per 885 worker. 886 887 ::: {.note} 888 Each thread consumes memory and contributes to Global VM 889 Lock contention, so be careful when increasing this. 890 ::: 891 ''; 892 }; 893 894 puma.threadsMax = mkOption { 895 type = types.int; 896 default = 4; 897 apply = x: builtins.toString x; 898 description = '' 899 The maximum number of threads Puma should use per 900 worker. This limits how many threads Puma will automatically 901 spawn in response to requests. In contrast to workers, 902 threads will never be able to run Ruby code in parallel, but 903 give higher IO parallelism. 904 905 ::: {.note} 906 Each thread consumes memory and contributes to Global VM 907 Lock contention, so be careful when increasing this. 908 ::: 909 ''; 910 }; 911 912 sidekiq.concurrency = mkOption { 913 type = with types; nullOr int; 914 default = null; 915 description = '' 916 How many processor threads to use for processing sidekiq background job queues. When null, the GitLab default is used. 917 918 See <https://docs.gitlab.com/ee/administration/sidekiq/extra_sidekiq_processes.html#manage-thread-counts-explicitly> for details. 919 ''; 920 }; 921 922 sidekiq.memoryKiller.enable = mkOption { 923 type = types.bool; 924 default = true; 925 description = '' 926 Whether the Sidekiq MemoryKiller should be turned 927 on. MemoryKiller kills Sidekiq when its memory consumption 928 exceeds a certain limit. 929 930 See <https://docs.gitlab.com/ee/administration/operations/sidekiq_memory_killer.html> 931 for details. 932 ''; 933 }; 934 935 sidekiq.memoryKiller.maxMemory = mkOption { 936 type = types.int; 937 default = 2000; 938 apply = x: builtins.toString (x * 1024); 939 description = '' 940 The maximum amount of memory, in MiB, a Sidekiq worker is 941 allowed to consume before being killed. 942 ''; 943 }; 944 945 sidekiq.memoryKiller.graceTime = mkOption { 946 type = types.int; 947 default = 900; 948 apply = x: builtins.toString x; 949 description = '' 950 The time MemoryKiller waits after noticing excessive memory 951 consumption before killing Sidekiq. 952 ''; 953 }; 954 955 sidekiq.memoryKiller.shutdownWait = mkOption { 956 type = types.int; 957 default = 30; 958 apply = x: builtins.toString x; 959 description = '' 960 The time allowed for all jobs to finish before Sidekiq is 961 killed forcefully. 962 ''; 963 }; 964 965 logrotate = { 966 enable = mkOption { 967 type = types.bool; 968 default = true; 969 description = '' 970 Enable rotation of log files. 971 ''; 972 }; 973 974 frequency = mkOption { 975 type = types.str; 976 default = "daily"; 977 description = "How often to rotate the logs."; 978 }; 979 980 keep = mkOption { 981 type = types.int; 982 default = 30; 983 description = "How many rotations to keep."; 984 }; 985 }; 986 987 workhorse.config = mkOption { 988 type = toml.type; 989 default = {}; 990 example = literalExpression '' 991 { 992 object_storage.provider = "AWS"; 993 object_storage.s3 = { 994 aws_access_key_id = "AKIAXXXXXXXXXXXXXXXX"; 995 aws_secret_access_key = { _secret = "/var/keys/aws_secret_access_key"; }; 996 }; 997 }; 998 ''; 999 description = '' 1000 Configuration options to add to Workhorse's configuration 1001 file. 1002 1003 See 1004 <https://gitlab.com/gitlab-org/gitlab/-/blob/master/workhorse/config.toml.example> 1005 and 1006 <https://docs.gitlab.com/ee/development/workhorse/configuration.html> 1007 for examples and option documentation. 1008 1009 Options containing secret data should be set to an attribute 1010 set containing the attribute `_secret` - a string pointing 1011 to a file containing the value the option should be set 1012 to. See the example to get a better picture of this: in the 1013 resulting configuration file, the 1014 `object_storage.s3.aws_secret_access_key` key will be set to 1015 the contents of the {file}`/var/keys/aws_secret_access_key` 1016 file. 1017 ''; 1018 }; 1019 1020 extraConfig = mkOption { 1021 type = yaml.type; 1022 default = {}; 1023 example = literalExpression '' 1024 { 1025 gitlab = { 1026 default_projects_features = { 1027 builds = false; 1028 }; 1029 }; 1030 omniauth = { 1031 enabled = true; 1032 auto_sign_in_with_provider = "openid_connect"; 1033 allow_single_sign_on = ["openid_connect"]; 1034 block_auto_created_users = false; 1035 providers = [ 1036 { 1037 name = "openid_connect"; 1038 label = "OpenID Connect"; 1039 args = { 1040 name = "openid_connect"; 1041 scope = ["openid" "profile"]; 1042 response_type = "code"; 1043 issuer = "https://keycloak.example.com/auth/realms/My%20Realm"; 1044 discovery = true; 1045 client_auth_method = "query"; 1046 uid_field = "preferred_username"; 1047 client_options = { 1048 identifier = "gitlab"; 1049 secret = { _secret = "/var/keys/gitlab_oidc_secret"; }; 1050 redirect_uri = "https://git.example.com/users/auth/openid_connect/callback"; 1051 }; 1052 }; 1053 } 1054 ]; 1055 }; 1056 }; 1057 ''; 1058 description = '' 1059 Extra options to be added under 1060 `production` in 1061 {file}`config/gitlab.yml`, as a nix attribute 1062 set. 1063 1064 Options containing secret data should be set to an attribute 1065 set containing the attribute `_secret` - a 1066 string pointing to a file containing the value the option 1067 should be set to. See the example to get a better picture of 1068 this: in the resulting 1069 {file}`config/gitlab.yml` file, the 1070 `production.omniauth.providers[0].args.client_options.secret` 1071 key will be set to the contents of the 1072 {file}`/var/keys/gitlab_oidc_secret` file. 1073 ''; 1074 }; 1075 }; 1076 }; 1077 1078 config = mkIf cfg.enable { 1079 warnings = [ 1080 (mkIf 1081 (cfg.registry.enable && versionAtLeast (getVersion cfg.packages.gitlab) "16.0.0" && cfg.registry.package == pkgs.docker-distribution) 1082 ''Support for container registries other than gitlab-container-registry has ended since GitLab 16.0.0 and is scheduled for removal in a future release. 1083 Please back up your data and migrate to the gitlab-container-registry package.'' 1084 ) 1085 (mkIf 1086 (versionAtLeast (getVersion cfg.packages.gitlab) "16.2.0" && versionOlder (getVersion cfg.packages.gitlab) "16.5.0") 1087 ''GitLab instances created or updated between versions [15.11.0, 15.11.2] have an incorrect database schema. 1088 Check the upstream documentation for a workaround: https://docs.gitlab.com/ee/update/versions/gitlab_16_changes.html#undefined-column-error-upgrading-to-162-or-later'' 1089 ) 1090 ]; 1091 1092 assertions = [ 1093 { 1094 assertion = databaseActuallyCreateLocally -> (cfg.user == cfg.databaseUsername); 1095 message = ''For local automatic database provisioning (services.gitlab.databaseCreateLocally == true) with peer authentication (services.gitlab.databaseHost == "") to work services.gitlab.user and services.gitlab.databaseUsername must be identical.''; 1096 } 1097 { 1098 assertion = (cfg.databaseHost != "") -> (cfg.databasePasswordFile != null); 1099 message = "When services.gitlab.databaseHost is customized, services.gitlab.databasePasswordFile must be set!"; 1100 } 1101 { 1102 assertion = cfg.initialRootPasswordFile != null; 1103 message = "services.gitlab.initialRootPasswordFile must be set!"; 1104 } 1105 { 1106 assertion = cfg.secrets.secretFile != null; 1107 message = "services.gitlab.secrets.secretFile must be set!"; 1108 } 1109 { 1110 assertion = cfg.secrets.dbFile != null; 1111 message = "services.gitlab.secrets.dbFile must be set!"; 1112 } 1113 { 1114 assertion = cfg.secrets.otpFile != null; 1115 message = "services.gitlab.secrets.otpFile must be set!"; 1116 } 1117 { 1118 assertion = cfg.secrets.jwsFile != null; 1119 message = "services.gitlab.secrets.jwsFile must be set!"; 1120 } 1121 { 1122 assertion = versionAtLeast postgresqlPackage.version "13.6.0"; 1123 message = "PostgreSQL >=13.6 is required to run GitLab 16. Follow the instructions in the manual section for upgrading PostgreSQL here: https://nixos.org/manual/nixos/stable/index.html#module-services-postgres-upgrading"; 1124 } 1125 ]; 1126 1127 environment.systemPackages = [ pkgs.git gitlab-rake gitlab-rails cfg.packages.gitlab-shell ]; 1128 1129 systemd.targets.gitlab = { 1130 description = "Common target for all GitLab services."; 1131 wantedBy = [ "multi-user.target" ]; 1132 }; 1133 1134 # Redis is required for the sidekiq queue runner. 1135 services.redis.servers.gitlab = { 1136 enable = mkDefault true; 1137 user = mkDefault cfg.user; 1138 unixSocket = mkDefault "/run/gitlab/redis.sock"; 1139 unixSocketPerm = mkDefault 770; 1140 }; 1141 1142 # We use postgres as the main data store. 1143 services.postgresql = optionalAttrs databaseActuallyCreateLocally { 1144 enable = true; 1145 ensureUsers = singleton { name = cfg.databaseUsername; }; 1146 }; 1147 1148 # Enable rotation of log files 1149 services.logrotate = { 1150 enable = cfg.logrotate.enable; 1151 settings = { 1152 gitlab = { 1153 files = "${cfg.statePath}/log/*.log"; 1154 su = "${cfg.user} ${cfg.group}"; 1155 frequency = cfg.logrotate.frequency; 1156 rotate = cfg.logrotate.keep; 1157 copytruncate = true; 1158 compress = true; 1159 }; 1160 }; 1161 }; 1162 1163 # The postgresql module doesn't currently support concepts like 1164 # objects owners and extensions; for now we tack on what's needed 1165 # here. 1166 systemd.services.gitlab-postgresql = let pgsql = config.services.postgresql; in mkIf databaseActuallyCreateLocally { 1167 after = [ "postgresql.service" ]; 1168 bindsTo = [ "postgresql.service" ]; 1169 wantedBy = [ "gitlab.target" ]; 1170 partOf = [ "gitlab.target" ]; 1171 path = [ 1172 pgsql.package 1173 pkgs.util-linux 1174 ]; 1175 script = '' 1176 set -eu 1177 1178 PSQL() { 1179 psql --port=${toString pgsql.settings.port} "$@" 1180 } 1181 1182 PSQL -tAc "SELECT 1 FROM pg_database WHERE datname = '${cfg.databaseName}'" | grep -q 1 || PSQL -tAc 'CREATE DATABASE "${cfg.databaseName}" OWNER "${cfg.databaseUsername}"' 1183 current_owner=$(PSQL -tAc "SELECT pg_catalog.pg_get_userbyid(datdba) FROM pg_catalog.pg_database WHERE datname = '${cfg.databaseName}'") 1184 if [[ "$current_owner" != "${cfg.databaseUsername}" ]]; then 1185 PSQL -tAc 'ALTER DATABASE "${cfg.databaseName}" OWNER TO "${cfg.databaseUsername}"' 1186 if [[ -e "${config.services.postgresql.dataDir}/.reassigning_${cfg.databaseName}" ]]; then 1187 echo "Reassigning ownership of database ${cfg.databaseName} to user ${cfg.databaseUsername} failed on last boot. Failing..." 1188 exit 1 1189 fi 1190 touch "${config.services.postgresql.dataDir}/.reassigning_${cfg.databaseName}" 1191 PSQL "${cfg.databaseName}" -tAc "REASSIGN OWNED BY \"$current_owner\" TO \"${cfg.databaseUsername}\"" 1192 rm "${config.services.postgresql.dataDir}/.reassigning_${cfg.databaseName}" 1193 fi 1194 PSQL '${cfg.databaseName}' -tAc "CREATE EXTENSION IF NOT EXISTS pg_trgm" 1195 PSQL '${cfg.databaseName}' -tAc "CREATE EXTENSION IF NOT EXISTS btree_gist;" 1196 ''; 1197 1198 serviceConfig = { 1199 User = pgsql.superUser; 1200 Type = "oneshot"; 1201 RemainAfterExit = true; 1202 }; 1203 }; 1204 1205 systemd.services.gitlab-registry-cert = optionalAttrs cfg.registry.enable { 1206 path = with pkgs; [ openssl ]; 1207 1208 script = '' 1209 mkdir -p $(dirname ${cfg.registry.keyFile}) 1210 mkdir -p $(dirname ${cfg.registry.certFile}) 1211 openssl req -nodes -newkey rsa:4096 -keyout ${cfg.registry.keyFile} -out /tmp/registry-auth.csr -subj "/CN=${cfg.registry.issuer}" 1212 openssl x509 -in /tmp/registry-auth.csr -out ${cfg.registry.certFile} -req -signkey ${cfg.registry.keyFile} -days 3650 1213 chown ${cfg.user}:${cfg.group} $(dirname ${cfg.registry.keyFile}) 1214 chown ${cfg.user}:${cfg.group} $(dirname ${cfg.registry.certFile}) 1215 chown ${cfg.user}:${cfg.group} ${cfg.registry.keyFile} 1216 chown ${cfg.user}:${cfg.group} ${cfg.registry.certFile} 1217 ''; 1218 1219 unitConfig = { 1220 ConditionPathExists = "!${cfg.registry.certFile}"; 1221 }; 1222 }; 1223 1224 # Ensure Docker Registry launches after the certificate generation job 1225 systemd.services.docker-registry = optionalAttrs cfg.registry.enable { 1226 wants = [ "gitlab-registry-cert.service" ]; 1227 after = [ "gitlab-registry-cert.service" ]; 1228 }; 1229 1230 # Enable Docker Registry, if GitLab-Container Registry is enabled 1231 services.dockerRegistry = optionalAttrs cfg.registry.enable { 1232 enable = true; 1233 enableDelete = true; # This must be true, otherwise GitLab won't manage it correctly 1234 package = cfg.registry.package; 1235 extraConfig = { 1236 auth.token = { 1237 realm = "http${optionalString (cfg.https == true) "s"}://${cfg.host}/jwt/auth"; 1238 service = cfg.registry.serviceName; 1239 issuer = cfg.registry.issuer; 1240 rootcertbundle = cfg.registry.certFile; 1241 }; 1242 }; 1243 }; 1244 1245 # Use postfix to send out mails. 1246 services.postfix.enable = mkDefault (cfg.smtp.enable && cfg.smtp.address == "localhost"); 1247 1248 users.users.${cfg.user} = 1249 { group = cfg.group; 1250 home = "${cfg.statePath}/home"; 1251 shell = "${pkgs.bash}/bin/bash"; 1252 uid = config.ids.uids.gitlab; 1253 }; 1254 1255 users.groups.${cfg.group}.gid = config.ids.gids.gitlab; 1256 1257 systemd.tmpfiles.rules = [ 1258 "d /run/gitlab 0755 ${cfg.user} ${cfg.group} -" 1259 "d ${gitlabEnv.HOME} 0750 ${cfg.user} ${cfg.group} -" 1260 "z ${gitlabEnv.HOME}/.ssh/authorized_keys 0600 ${cfg.user} ${cfg.group} -" 1261 "d ${cfg.backup.path} 0750 ${cfg.user} ${cfg.group} -" 1262 "d ${cfg.statePath} 0750 ${cfg.user} ${cfg.group} -" 1263 "d ${cfg.statePath}/builds 0750 ${cfg.user} ${cfg.group} -" 1264 "d ${cfg.statePath}/config 0750 ${cfg.user} ${cfg.group} -" 1265 "d ${cfg.statePath}/db 0750 ${cfg.user} ${cfg.group} -" 1266 "d ${cfg.statePath}/log 0750 ${cfg.user} ${cfg.group} -" 1267 "d ${cfg.statePath}/repositories 2770 ${cfg.user} ${cfg.group} -" 1268 "d ${cfg.statePath}/shell 0750 ${cfg.user} ${cfg.group} -" 1269 "d ${cfg.statePath}/tmp 0750 ${cfg.user} ${cfg.group} -" 1270 "d ${cfg.statePath}/tmp/pids 0750 ${cfg.user} ${cfg.group} -" 1271 "d ${cfg.statePath}/tmp/sockets 0750 ${cfg.user} ${cfg.group} -" 1272 "d ${cfg.statePath}/uploads 0700 ${cfg.user} ${cfg.group} -" 1273 "d ${cfg.statePath}/custom_hooks 0700 ${cfg.user} ${cfg.group} -" 1274 "d ${cfg.statePath}/custom_hooks/pre-receive.d 0700 ${cfg.user} ${cfg.group} -" 1275 "d ${cfg.statePath}/custom_hooks/post-receive.d 0700 ${cfg.user} ${cfg.group} -" 1276 "d ${cfg.statePath}/custom_hooks/update.d 0700 ${cfg.user} ${cfg.group} -" 1277 "d ${gitlabConfig.production.shared.path} 0750 ${cfg.user} ${cfg.group} -" 1278 "d ${gitlabConfig.production.shared.path}/artifacts 0750 ${cfg.user} ${cfg.group} -" 1279 "d ${gitlabConfig.production.shared.path}/lfs-objects 0750 ${cfg.user} ${cfg.group} -" 1280 "d ${gitlabConfig.production.shared.path}/packages 0750 ${cfg.user} ${cfg.group} -" 1281 "d ${gitlabConfig.production.shared.path}/pages 0750 ${cfg.user} ${cfg.group} -" 1282 "d ${gitlabConfig.production.shared.path}/registry 0750 ${cfg.user} ${cfg.group} -" 1283 "d ${gitlabConfig.production.shared.path}/terraform_state 0750 ${cfg.user} ${cfg.group} -" 1284 "d ${gitlabConfig.production.shared.path}/ci_secure_files 0750 ${cfg.user} ${cfg.group} -" 1285 "L+ /run/gitlab/config - - - - ${cfg.statePath}/config" 1286 "L+ /run/gitlab/log - - - - ${cfg.statePath}/log" 1287 "L+ /run/gitlab/tmp - - - - ${cfg.statePath}/tmp" 1288 "L+ /run/gitlab/uploads - - - - ${cfg.statePath}/uploads" 1289 1290 "L+ /run/gitlab/shell-config.yml - - - - ${pkgs.writeText "config.yml" (builtins.toJSON gitlabShellConfig)}" 1291 ]; 1292 1293 1294 systemd.services.gitlab-config = { 1295 wantedBy = [ "gitlab.target" ]; 1296 partOf = [ "gitlab.target" ]; 1297 path = with pkgs; [ 1298 jq 1299 openssl 1300 replace-secret 1301 git 1302 ]; 1303 serviceConfig = { 1304 Type = "oneshot"; 1305 User = cfg.user; 1306 Group = cfg.group; 1307 TimeoutSec = "infinity"; 1308 Restart = "on-failure"; 1309 WorkingDirectory = "${cfg.packages.gitlab}/share/gitlab"; 1310 RemainAfterExit = true; 1311 1312 ExecStartPre = let 1313 preStartFullPrivileges = '' 1314 set -o errexit -o pipefail -o nounset 1315 shopt -s dotglob nullglob inherit_errexit 1316 1317 chown --no-dereference '${cfg.user}':'${cfg.group}' '${cfg.statePath}'/* 1318 if [[ -n "$(ls -A '${cfg.statePath}'/config/)" ]]; then 1319 chown --no-dereference '${cfg.user}':'${cfg.group}' '${cfg.statePath}'/config/* 1320 fi 1321 ''; 1322 in "+${pkgs.writeShellScript "gitlab-pre-start-full-privileges" preStartFullPrivileges}"; 1323 1324 ExecStart = pkgs.writeShellScript "gitlab-config" '' 1325 set -o errexit -o pipefail -o nounset 1326 shopt -s inherit_errexit 1327 1328 umask u=rwx,g=rx,o= 1329 1330 cp -f ${cfg.packages.gitlab}/share/gitlab/VERSION ${cfg.statePath}/VERSION 1331 rm -rf ${cfg.statePath}/db/* 1332 rm -f ${cfg.statePath}/lib 1333 find '${cfg.statePath}/config/' -maxdepth 1 -mindepth 1 -type d -execdir rm -rf {} \; 1334 cp -rf --no-preserve=mode ${cfg.packages.gitlab}/share/gitlab/config.dist/* ${cfg.statePath}/config 1335 cp -rf --no-preserve=mode ${cfg.packages.gitlab}/share/gitlab/db/* ${cfg.statePath}/db 1336 ln -sf ${extraGitlabRb} ${cfg.statePath}/config/initializers/extra-gitlab.rb 1337 ln -sf ${cableYml} ${cfg.statePath}/config/cable.yml 1338 ln -sf ${resqueYml} ${cfg.statePath}/config/resque.yml 1339 1340 ${cfg.packages.gitlab-shell}/bin/install 1341 1342 ${optionalString cfg.smtp.enable '' 1343 install -m u=rw ${smtpSettings} ${cfg.statePath}/config/initializers/smtp_settings.rb 1344 ${optionalString (cfg.smtp.passwordFile != null) '' 1345 replace-secret '@smtpPassword@' '${cfg.smtp.passwordFile}' '${cfg.statePath}/config/initializers/smtp_settings.rb' 1346 ''} 1347 ''} 1348 1349 ( 1350 umask u=rwx,g=,o= 1351 1352 openssl rand -hex 32 > ${cfg.statePath}/gitlab_shell_secret 1353 ${optionalString cfg.pages.enable '' 1354 openssl rand -base64 32 > ${cfg.pages.settings.api-secret-key} 1355 ''} 1356 1357 rm -f '${cfg.statePath}/config/database.yml' 1358 1359 ${lib.optionalString (cfg.databasePasswordFile != null) '' 1360 db_password="$(<'${cfg.databasePasswordFile}')" 1361 export db_password 1362 1363 if [[ -z "$db_password" ]]; then 1364 >&2 echo "Database password was an empty string!" 1365 exit 1 1366 fi 1367 ''} 1368 1369 # GitLab expects the `production.main` section to be the first entry in the file. 1370 jq <${pkgs.writeText "database.yml" (builtins.toJSON databaseConfig)} '{ 1371 production: [ 1372 ${lib.optionalString (cfg.databasePasswordFile != null) ( 1373 builtins.concatStringsSep "\n " ( 1374 [ ".production${lib.optionalString (gitlabVersionAtLeast "15.0") ".main"}.password = $ENV.db_password" ] 1375 ++ lib.optional (gitlabVersionAtLeast "15.9") "| .production.ci.password = $ENV.db_password" 1376 ++ [ "|" ] 1377 ) 1378 )} .production 1379 | to_entries[] 1380 ] 1381 | sort_by(.key) 1382 | reverse 1383 | from_entries 1384 }' >'${cfg.statePath}/config/database.yml' 1385 1386 ${utils.genJqSecretsReplacementSnippet 1387 gitlabConfig 1388 "${cfg.statePath}/config/gitlab.yml" 1389 } 1390 1391 rm -f '${cfg.statePath}/config/secrets.yml' 1392 1393 secret="$(<'${cfg.secrets.secretFile}')" 1394 db="$(<'${cfg.secrets.dbFile}')" 1395 otp="$(<'${cfg.secrets.otpFile}')" 1396 jws="$(<'${cfg.secrets.jwsFile}')" 1397 export secret db otp jws 1398 jq -n '{production: {secret_key_base: $ENV.secret, 1399 otp_key_base: $ENV.otp, 1400 db_key_base: $ENV.db, 1401 openid_connect_signing_key: $ENV.jws}}' \ 1402 > '${cfg.statePath}/config/secrets.yml' 1403 ) 1404 1405 # We remove potentially broken links to old gitlab-shell versions 1406 rm -Rf ${cfg.statePath}/repositories/**/*.git/hooks 1407 1408 git config --global core.autocrlf "input" 1409 ''; 1410 }; 1411 }; 1412 1413 systemd.services.gitlab-db-config = { 1414 after = [ "gitlab-config.service" "gitlab-postgresql.service" "postgresql.service" ]; 1415 wants = optional (cfg.databaseHost == "") "postgresql.service" ++ optional databaseActuallyCreateLocally "gitlab-postgresql.service"; 1416 bindsTo = [ "gitlab-config.service" ]; 1417 wantedBy = [ "gitlab.target" ]; 1418 partOf = [ "gitlab.target" ]; 1419 serviceConfig = { 1420 Type = "oneshot"; 1421 User = cfg.user; 1422 Group = cfg.group; 1423 TimeoutSec = "infinity"; 1424 Restart = "on-failure"; 1425 WorkingDirectory = "${cfg.packages.gitlab}/share/gitlab"; 1426 RemainAfterExit = true; 1427 1428 ExecStart = pkgs.writeShellScript "gitlab-db-config" '' 1429 set -o errexit -o pipefail -o nounset 1430 shopt -s inherit_errexit 1431 umask u=rwx,g=rx,o= 1432 1433 initial_root_password="$(<'${cfg.initialRootPasswordFile}')" 1434 ${gitlab-rake}/bin/gitlab-rake gitlab:db:configure GITLAB_ROOT_PASSWORD="$initial_root_password" \ 1435 GITLAB_ROOT_EMAIL='${cfg.initialRootEmail}' > /dev/null 1436 ''; 1437 }; 1438 }; 1439 1440 systemd.services.gitlab-sidekiq = { 1441 after = [ 1442 "network.target" 1443 "redis-gitlab.service" 1444 "postgresql.service" 1445 "gitlab-config.service" 1446 "gitlab-db-config.service" 1447 ]; 1448 bindsTo = [ 1449 "gitlab-config.service" 1450 "gitlab-db-config.service" 1451 ]; 1452 wants = [ "redis-gitlab.service" ] ++ optional (cfg.databaseHost == "") "postgresql.service"; 1453 wantedBy = [ "gitlab.target" ]; 1454 partOf = [ "gitlab.target" ]; 1455 environment = gitlabEnv // (optionalAttrs cfg.sidekiq.memoryKiller.enable { 1456 SIDEKIQ_MEMORY_KILLER_MAX_RSS = cfg.sidekiq.memoryKiller.maxMemory; 1457 SIDEKIQ_MEMORY_KILLER_GRACE_TIME = cfg.sidekiq.memoryKiller.graceTime; 1458 SIDEKIQ_MEMORY_KILLER_SHUTDOWN_WAIT = cfg.sidekiq.memoryKiller.shutdownWait; 1459 }); 1460 path = with pkgs; [ 1461 postgresqlPackage 1462 git 1463 ruby 1464 openssh 1465 nodejs 1466 gnupg 1467 1468 "${cfg.packages.gitlab}/share/gitlab/vendor/gems/sidekiq-${cfg.packages.gitlab.rubyEnv.gems.sidekiq.version}" 1469 1470 # Needed for GitLab project imports 1471 gnutar 1472 gzip 1473 1474 procps # Sidekiq MemoryKiller 1475 ]; 1476 serviceConfig = { 1477 Type = "simple"; 1478 User = cfg.user; 1479 Group = cfg.group; 1480 TimeoutSec = "infinity"; 1481 Restart = "always"; 1482 WorkingDirectory = "${cfg.packages.gitlab}/share/gitlab"; 1483 ExecStart = utils.escapeSystemdExecArgs ( 1484 [ 1485 "${cfg.packages.gitlab}/share/gitlab/bin/sidekiq-cluster" 1486 "*" # all queue groups 1487 ] ++ lib.optionals (cfg.sidekiq.concurrency != null) [ 1488 "--concurrency" (toString cfg.sidekiq.concurrency) 1489 ] ++ [ 1490 "--environment" "production" 1491 "--require" "." 1492 ] 1493 ); 1494 }; 1495 }; 1496 1497 systemd.services.gitaly = { 1498 after = [ "network.target" "gitlab-config.service" ]; 1499 bindsTo = [ "gitlab-config.service" ]; 1500 wantedBy = [ "gitlab.target" ]; 1501 partOf = [ "gitlab.target" ]; 1502 path = with pkgs; [ 1503 openssh 1504 git 1505 gzip 1506 bzip2 1507 ]; 1508 serviceConfig = { 1509 Type = "simple"; 1510 User = cfg.user; 1511 Group = cfg.group; 1512 TimeoutSec = "infinity"; 1513 Restart = "on-failure"; 1514 WorkingDirectory = gitlabEnv.HOME; 1515 RuntimeDirectory = "gitaly"; 1516 ExecStart = "${cfg.packages.gitaly}/bin/gitaly ${gitalyToml}"; 1517 }; 1518 }; 1519 1520 services.gitlab.pages.settings = { 1521 api-secret-key = "${cfg.statePath}/gitlab_pages_secret"; 1522 }; 1523 1524 systemd.services.gitlab-pages = 1525 let 1526 filteredConfig = filterAttrs (_: v: v != null) cfg.pages.settings; 1527 isSecret = v: isAttrs v && v ? _secret && isString v._secret; 1528 mkPagesKeyValue = lib.generators.toKeyValue { 1529 mkKeyValue = lib.flip lib.generators.mkKeyValueDefault "=" rec { 1530 mkValueString = v: 1531 if isInt v then toString v 1532 else if isString v then v 1533 else if true == v then "true" 1534 else if false == v then "false" 1535 else if isSecret v then builtins.hashString "sha256" v._secret 1536 else throw "unsupported type ${builtins.typeOf v}: ${(lib.generators.toPretty {}) v}"; 1537 }; 1538 }; 1539 secretPaths = lib.catAttrs "_secret" (lib.collect isSecret filteredConfig); 1540 mkSecretReplacement = file: '' 1541 replace-secret ${lib.escapeShellArgs [ (builtins.hashString "sha256" file) file "/run/gitlab-pages/gitlab-pages.conf" ]} 1542 ''; 1543 secretReplacements = lib.concatMapStrings mkSecretReplacement secretPaths; 1544 configFile = pkgs.writeText "gitlab-pages.conf" (mkPagesKeyValue filteredConfig); 1545 in 1546 mkIf cfg.pages.enable { 1547 description = "GitLab static pages daemon"; 1548 after = [ "network.target" "gitlab-config.service" "gitlab.service" ]; 1549 bindsTo = [ "gitlab-config.service" "gitlab.service" ]; 1550 wantedBy = [ "gitlab.target" ]; 1551 partOf = [ "gitlab.target" ]; 1552 1553 path = with pkgs; [ 1554 unzip 1555 replace-secret 1556 ]; 1557 1558 serviceConfig = { 1559 Type = "simple"; 1560 TimeoutSec = "infinity"; 1561 Restart = "on-failure"; 1562 1563 User = cfg.user; 1564 Group = cfg.group; 1565 1566 ExecStartPre = pkgs.writeShellScript "gitlab-pages-pre-start" '' 1567 set -o errexit -o pipefail -o nounset 1568 shopt -s dotglob nullglob inherit_errexit 1569 1570 install -m u=rw ${configFile} /run/gitlab-pages/gitlab-pages.conf 1571 ${secretReplacements} 1572 ''; 1573 ExecStart = "${cfg.packages.pages}/bin/gitlab-pages -config=/run/gitlab-pages/gitlab-pages.conf"; 1574 WorkingDirectory = gitlabEnv.HOME; 1575 RuntimeDirectory = "gitlab-pages"; 1576 RuntimeDirectoryMode = "0700"; 1577 }; 1578 }; 1579 1580 systemd.services.gitlab-workhorse = { 1581 after = [ "network.target" ]; 1582 wantedBy = [ "gitlab.target" ]; 1583 partOf = [ "gitlab.target" ]; 1584 path = with pkgs; [ 1585 remarshal 1586 exiftool 1587 git 1588 gnutar 1589 gzip 1590 openssh 1591 cfg.packages.gitlab-workhorse 1592 ]; 1593 serviceConfig = { 1594 Type = "simple"; 1595 User = cfg.user; 1596 Group = cfg.group; 1597 TimeoutSec = "infinity"; 1598 Restart = "on-failure"; 1599 WorkingDirectory = gitlabEnv.HOME; 1600 ExecStartPre = pkgs.writeShellScript "gitlab-workhorse-pre-start" '' 1601 set -o errexit -o pipefail -o nounset 1602 shopt -s dotglob nullglob inherit_errexit 1603 1604 ${utils.genJqSecretsReplacementSnippet 1605 cfg.workhorse.config 1606 "${cfg.statePath}/config/gitlab-workhorse.json"} 1607 1608 json2toml "${cfg.statePath}/config/gitlab-workhorse.json" "${cfg.statePath}/config/gitlab-workhorse.toml" 1609 rm "${cfg.statePath}/config/gitlab-workhorse.json" 1610 ''; 1611 ExecStart = 1612 "${cfg.packages.gitlab-workhorse}/bin/${ 1613 optionalString (lib.versionAtLeast (lib.getVersion cfg.packages.gitlab-workhorse) "16.10") "gitlab-" 1614 }workhorse " 1615 + "-listenUmask 0 " 1616 + "-listenNetwork unix " 1617 + "-listenAddr /run/gitlab/gitlab-workhorse.socket " 1618 + "-authSocket ${gitlabSocket} " 1619 + "-documentRoot ${cfg.packages.gitlab}/share/gitlab/public " 1620 + "-config ${cfg.statePath}/config/gitlab-workhorse.toml " 1621 + "-secretPath ${cfg.statePath}/.gitlab_workhorse_secret"; 1622 }; 1623 }; 1624 1625 systemd.services.gitlab-mailroom = mkIf (gitlabConfig.production.incoming_email.enabled or false) { 1626 description = "GitLab incoming mail daemon"; 1627 after = [ "network.target" "redis-gitlab.service" "gitlab-config.service" ]; 1628 bindsTo = [ "gitlab-config.service" ]; 1629 wantedBy = [ "gitlab.target" ]; 1630 partOf = [ "gitlab.target" ]; 1631 environment = gitlabEnv; 1632 serviceConfig = { 1633 Type = "simple"; 1634 TimeoutSec = "infinity"; 1635 Restart = "on-failure"; 1636 1637 User = cfg.user; 1638 Group = cfg.group; 1639 ExecStart = "${cfg.packages.gitlab.rubyEnv}/bin/bundle exec mail_room -c ${cfg.statePath}/config/mail_room.yml"; 1640 WorkingDirectory = gitlabEnv.HOME; 1641 }; 1642 }; 1643 1644 systemd.services.gitlab = { 1645 after = [ 1646 "gitlab-workhorse.service" 1647 "network.target" 1648 "redis-gitlab.service" 1649 "gitlab-config.service" 1650 "gitlab-db-config.service" 1651 ]; 1652 bindsTo = [ 1653 "gitlab-config.service" 1654 "gitlab-db-config.service" 1655 ]; 1656 wants = [ "redis-gitlab.service" ] ++ optional (cfg.databaseHost == "") "postgresql.service"; 1657 requiredBy = [ "gitlab.target" ]; 1658 partOf = [ "gitlab.target" ]; 1659 environment = gitlabEnv; 1660 path = with pkgs; [ 1661 postgresqlPackage 1662 git 1663 openssh 1664 nodejs 1665 procps 1666 gnupg 1667 gzip 1668 ]; 1669 serviceConfig = { 1670 Type = "notify"; 1671 User = cfg.user; 1672 Group = cfg.group; 1673 TimeoutSec = "infinity"; 1674 Restart = "on-failure"; 1675 WorkingDirectory = "${cfg.packages.gitlab}/share/gitlab"; 1676 ExecStart = concatStringsSep " " [ 1677 "${cfg.packages.gitlab.rubyEnv}/bin/bundle" "exec" "puma" 1678 "-e production" 1679 "-C ${cfg.statePath}/config/puma.rb" 1680 "-w ${cfg.puma.workers}" 1681 "-t ${cfg.puma.threadsMin}:${cfg.puma.threadsMax}" 1682 ]; 1683 }; 1684 1685 }; 1686 1687 systemd.services.gitlab-backup = { 1688 after = [ "gitlab.service" ]; 1689 bindsTo = [ "gitlab.service" ]; 1690 startAt = cfg.backup.startAt; 1691 environment = { 1692 RAILS_ENV = "production"; 1693 CRON = "1"; 1694 } // optionalAttrs (stringLength cfg.backup.skip > 0) { 1695 SKIP = cfg.backup.skip; 1696 }; 1697 serviceConfig = { 1698 User = cfg.user; 1699 Group = cfg.group; 1700 ExecStart = "${gitlab-rake}/bin/gitlab-rake gitlab:backup:create"; 1701 }; 1702 }; 1703 1704 }; 1705 1706 meta.doc = ./gitlab.md; 1707 meta.maintainers = teams.gitlab.members; 1708}