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