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