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