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