grafana-alloy: don't build the frontend twice
[NixPkgs.git] / nixos / modules / services / misc / gitlab.nix
blob9fd6014f2c71c872ac95f53ada93da250b9558a3
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.targets.gitlab = {
1131       description = "Common target for all GitLab services.";
1132       wantedBy = [ "multi-user.target" ];
1133     };
1135     # Redis is required for the sidekiq queue runner.
1136     services.redis.servers.gitlab = {
1137       enable = mkDefault true;
1138       user = mkDefault cfg.user;
1139       unixSocket = mkDefault "/run/gitlab/redis.sock";
1140       unixSocketPerm = mkDefault 770;
1141     };
1143     # We use postgres as the main data store.
1144     services.postgresql = optionalAttrs databaseActuallyCreateLocally {
1145       enable = true;
1146       ensureUsers = singleton { name = cfg.databaseUsername; };
1147     };
1149     # Enable rotation of log files
1150     services.logrotate = {
1151       enable = cfg.logrotate.enable;
1152       settings = {
1153         gitlab = {
1154           files = "${cfg.statePath}/log/*.log";
1155           su = "${cfg.user} ${cfg.group}";
1156           frequency = cfg.logrotate.frequency;
1157           rotate = cfg.logrotate.keep;
1158           copytruncate = true;
1159           compress = true;
1160         };
1161       };
1162     };
1164     # The postgresql module doesn't currently support concepts like
1165     # objects owners and extensions; for now we tack on what's needed
1166     # here.
1167     systemd.services.gitlab-postgresql = let pgsql = config.services.postgresql; in mkIf databaseActuallyCreateLocally {
1168       after = [ "postgresql.service" ];
1169       bindsTo = [ "postgresql.service" ];
1170       wantedBy = [ "gitlab.target" ];
1171       partOf = [ "gitlab.target" ];
1172       path = [
1173         pgsql.package
1174         pkgs.util-linux
1175       ];
1176       script = ''
1177         set -eu
1179         PSQL() {
1180             psql --port=${toString pgsql.settings.port} "$@"
1181         }
1183         PSQL -tAc "SELECT 1 FROM pg_database WHERE datname = '${cfg.databaseName}'" | grep -q 1 || PSQL -tAc 'CREATE DATABASE "${cfg.databaseName}" OWNER "${cfg.databaseUsername}"'
1184         current_owner=$(PSQL -tAc "SELECT pg_catalog.pg_get_userbyid(datdba) FROM pg_catalog.pg_database WHERE datname = '${cfg.databaseName}'")
1185         if [[ "$current_owner" != "${cfg.databaseUsername}" ]]; then
1186             PSQL -tAc 'ALTER DATABASE "${cfg.databaseName}" OWNER TO "${cfg.databaseUsername}"'
1187             if [[ -e "${config.services.postgresql.dataDir}/.reassigning_${cfg.databaseName}" ]]; then
1188                 echo "Reassigning ownership of database ${cfg.databaseName} to user ${cfg.databaseUsername} failed on last boot. Failing..."
1189                 exit 1
1190             fi
1191             touch "${config.services.postgresql.dataDir}/.reassigning_${cfg.databaseName}"
1192             PSQL "${cfg.databaseName}" -tAc "REASSIGN OWNED BY \"$current_owner\" TO \"${cfg.databaseUsername}\""
1193             rm "${config.services.postgresql.dataDir}/.reassigning_${cfg.databaseName}"
1194         fi
1195         PSQL '${cfg.databaseName}' -tAc "CREATE EXTENSION IF NOT EXISTS pg_trgm"
1196         PSQL '${cfg.databaseName}' -tAc "CREATE EXTENSION IF NOT EXISTS btree_gist;"
1197       '';
1199       serviceConfig = {
1200         User = pgsql.superUser;
1201         Type = "oneshot";
1202         RemainAfterExit = true;
1203       };
1204     };
1206     systemd.services.gitlab-registry-cert = optionalAttrs cfg.registry.enable {
1207       path = with pkgs; [ openssl ];
1209       script = ''
1210         mkdir -p $(dirname ${cfg.registry.keyFile})
1211         mkdir -p $(dirname ${cfg.registry.certFile})
1212         openssl req -nodes -newkey rsa:4096 -keyout ${cfg.registry.keyFile} -out /tmp/registry-auth.csr -subj "/CN=${cfg.registry.issuer}"
1213         openssl x509 -in /tmp/registry-auth.csr -out ${cfg.registry.certFile} -req -signkey ${cfg.registry.keyFile} -days 3650
1214         chown ${cfg.user}:${cfg.group} $(dirname ${cfg.registry.keyFile})
1215         chown ${cfg.user}:${cfg.group} $(dirname ${cfg.registry.certFile})
1216         chown ${cfg.user}:${cfg.group} ${cfg.registry.keyFile}
1217         chown ${cfg.user}:${cfg.group} ${cfg.registry.certFile}
1218       '';
1220       unitConfig = {
1221         ConditionPathExists = "!${cfg.registry.certFile}";
1222       };
1223     };
1225     # Ensure Docker Registry launches after the certificate generation job
1226     systemd.services.docker-registry = optionalAttrs cfg.registry.enable {
1227       wants = [ "gitlab-registry-cert.service" ];
1228       after = [ "gitlab-registry-cert.service" ];
1229     };
1231     # Enable Docker Registry, if GitLab-Container Registry is enabled
1232     services.dockerRegistry = optionalAttrs cfg.registry.enable {
1233       enable = true;
1234       enableDelete = true; # This must be true, otherwise GitLab won't manage it correctly
1235       package = cfg.registry.package;
1236       extraConfig = {
1237         auth.token = {
1238           realm = "http${optionalString (cfg.https == true) "s"}://${cfg.host}/jwt/auth";
1239           service = cfg.registry.serviceName;
1240           issuer = cfg.registry.issuer;
1241           rootcertbundle = cfg.registry.certFile;
1242         };
1243       };
1244     };
1246     # Use postfix to send out mails.
1247     services.postfix.enable = mkDefault (cfg.smtp.enable && cfg.smtp.address == "localhost");
1249     users.users.${cfg.user} =
1250       { group = cfg.group;
1251         home = "${cfg.statePath}/home";
1252         shell = "${pkgs.bash}/bin/bash";
1253         uid = config.ids.uids.gitlab;
1254       };
1256     users.groups.${cfg.group}.gid = config.ids.gids.gitlab;
1258     systemd.tmpfiles.rules = [
1259       "d /run/gitlab 0755 ${cfg.user} ${cfg.group} -"
1260       "d ${gitlabEnv.HOME} 0750 ${cfg.user} ${cfg.group} -"
1261       "z ${gitlabEnv.HOME}/.ssh/authorized_keys 0600 ${cfg.user} ${cfg.group} -"
1262       "d ${cfg.backup.path} 0750 ${cfg.user} ${cfg.group} -"
1263       "d ${cfg.statePath} 0750 ${cfg.user} ${cfg.group} -"
1264       "d ${cfg.statePath}/builds 0750 ${cfg.user} ${cfg.group} -"
1265       "d ${cfg.statePath}/config 0750 ${cfg.user} ${cfg.group} -"
1266       "d ${cfg.statePath}/db 0750 ${cfg.user} ${cfg.group} -"
1267       "d ${cfg.statePath}/log 0750 ${cfg.user} ${cfg.group} -"
1268       "d ${cfg.statePath}/repositories 2770 ${cfg.user} ${cfg.group} -"
1269       "d ${cfg.statePath}/shell 0750 ${cfg.user} ${cfg.group} -"
1270       "d ${cfg.statePath}/tmp 0750 ${cfg.user} ${cfg.group} -"
1271       "d ${cfg.statePath}/tmp/pids 0750 ${cfg.user} ${cfg.group} -"
1272       "d ${cfg.statePath}/tmp/sockets 0750 ${cfg.user} ${cfg.group} -"
1273       "d ${cfg.statePath}/uploads 0700 ${cfg.user} ${cfg.group} -"
1274       "d ${cfg.statePath}/custom_hooks 0700 ${cfg.user} ${cfg.group} -"
1275       "d ${cfg.statePath}/custom_hooks/pre-receive.d 0700 ${cfg.user} ${cfg.group} -"
1276       "d ${cfg.statePath}/custom_hooks/post-receive.d 0700 ${cfg.user} ${cfg.group} -"
1277       "d ${cfg.statePath}/custom_hooks/update.d 0700 ${cfg.user} ${cfg.group} -"
1278       "d ${gitlabConfig.production.shared.path} 0750 ${cfg.user} ${cfg.group} -"
1279       "d ${gitlabConfig.production.shared.path}/artifacts 0750 ${cfg.user} ${cfg.group} -"
1280       "d ${gitlabConfig.production.shared.path}/lfs-objects 0750 ${cfg.user} ${cfg.group} -"
1281       "d ${gitlabConfig.production.shared.path}/packages 0750 ${cfg.user} ${cfg.group} -"
1282       "d ${gitlabConfig.production.shared.path}/pages 0750 ${cfg.user} ${cfg.group} -"
1283       "d ${gitlabConfig.production.shared.path}/registry 0750 ${cfg.user} ${cfg.group} -"
1284       "d ${gitlabConfig.production.shared.path}/terraform_state 0750 ${cfg.user} ${cfg.group} -"
1285       "d ${gitlabConfig.production.shared.path}/ci_secure_files 0750 ${cfg.user} ${cfg.group} -"
1286       "d ${gitlabConfig.production.shared.path}/external-diffs 0750 ${cfg.user} ${cfg.group} -"
1287       "L+ /run/gitlab/config - - - - ${cfg.statePath}/config"
1288       "L+ /run/gitlab/log - - - - ${cfg.statePath}/log"
1289       "L+ /run/gitlab/tmp - - - - ${cfg.statePath}/tmp"
1290       "L+ /run/gitlab/uploads - - - - ${cfg.statePath}/uploads"
1292       "L+ /run/gitlab/shell-config.yml - - - - ${pkgs.writeText "config.yml" (builtins.toJSON gitlabShellConfig)}"
1293     ];
1296     systemd.services.gitlab-config = {
1297       wantedBy = [ "gitlab.target" ];
1298       partOf = [ "gitlab.target" ];
1299       path = [ git ] ++ (with pkgs; [
1300         jq
1301         openssl
1302         replace-secret
1303       ]);
1304       serviceConfig = {
1305         Type = "oneshot";
1306         User = cfg.user;
1307         Group = cfg.group;
1308         TimeoutSec = "infinity";
1309         Restart = "on-failure";
1310         WorkingDirectory = "${cfg.packages.gitlab}/share/gitlab";
1311         RemainAfterExit = true;
1313         ExecStartPre = let
1314           preStartFullPrivileges = ''
1315             set -o errexit -o pipefail -o nounset
1316             shopt -s dotglob nullglob inherit_errexit
1318             chown --no-dereference '${cfg.user}':'${cfg.group}' '${cfg.statePath}'/*
1319             if [[ -n "$(ls -A '${cfg.statePath}'/config/)" ]]; then
1320               chown --no-dereference '${cfg.user}':'${cfg.group}' '${cfg.statePath}'/config/*
1321             fi
1322           '';
1323         in "+${pkgs.writeShellScript "gitlab-pre-start-full-privileges" preStartFullPrivileges}";
1325         ExecStart = pkgs.writeShellScript "gitlab-config" ''
1326           set -o errexit -o pipefail -o nounset
1327           shopt -s inherit_errexit
1329           umask u=rwx,g=rx,o=
1331           cp -f ${cfg.packages.gitlab}/share/gitlab/VERSION ${cfg.statePath}/VERSION
1332           rm -rf ${cfg.statePath}/db/*
1333           rm -f ${cfg.statePath}/lib
1334           find '${cfg.statePath}/config/' -maxdepth 1 -mindepth 1 -type d -execdir rm -rf {} \;
1335           cp -rf --no-preserve=mode ${cfg.packages.gitlab}/share/gitlab/config.dist/* ${cfg.statePath}/config
1336           cp -rf --no-preserve=mode ${cfg.packages.gitlab}/share/gitlab/db/* ${cfg.statePath}/db
1337           ln -sf ${extraGitlabRb} ${cfg.statePath}/config/initializers/extra-gitlab.rb
1338           ln -sf ${cableYml} ${cfg.statePath}/config/cable.yml
1339           ln -sf ${resqueYml} ${cfg.statePath}/config/resque.yml
1341           ${cfg.packages.gitlab-shell}/bin/gitlab-shell-install
1343           ${optionalString cfg.smtp.enable ''
1344               install -m u=rw ${smtpSettings} ${cfg.statePath}/config/initializers/smtp_settings.rb
1345               ${optionalString (cfg.smtp.passwordFile != null) ''
1346                   replace-secret '@smtpPassword@' '${cfg.smtp.passwordFile}' '${cfg.statePath}/config/initializers/smtp_settings.rb'
1347               ''}
1348           ''}
1350           (
1351             umask u=rwx,g=,o=
1353             openssl rand -hex 32 > ${cfg.statePath}/gitlab_shell_secret
1354             ${optionalString cfg.pages.enable ''
1355                 openssl rand -base64 32 > ${cfg.pages.settings.api-secret-key}
1356             ''}
1358             rm -f '${cfg.statePath}/config/database.yml'
1360             ${lib.optionalString (cfg.databasePasswordFile != null) ''
1361                 db_password="$(<'${cfg.databasePasswordFile}')"
1362                 export db_password
1364                 if [[ -z "$db_password" ]]; then
1365                   >&2 echo "Database password was an empty string!"
1366                   exit 1
1367                 fi
1368             ''}
1370             # GitLab expects the `production.main` section to be the first entry in the file.
1371             jq <${pkgs.writeText "database.yml" (builtins.toJSON databaseConfig)} '{
1372               production: [
1373                 ${lib.optionalString (cfg.databasePasswordFile != null) (
1374                   builtins.concatStringsSep "\n      " (
1375                     [ ".production${lib.optionalString (gitlabVersionAtLeast "15.0") ".main"}.password = $ENV.db_password" ]
1376                     ++ lib.optional (gitlabVersionAtLeast "15.9") "| .production.ci.password = $ENV.db_password"
1377                     ++ [ "|" ]
1378                   )
1379                 )} .production
1380                 | to_entries[]
1381               ]
1382               | sort_by(.key)
1383               | reverse
1384               | from_entries
1385             }' >'${cfg.statePath}/config/database.yml'
1387             ${utils.genJqSecretsReplacementSnippet
1388                 gitlabConfig
1389                 "${cfg.statePath}/config/gitlab.yml"
1390             }
1392             rm -f '${cfg.statePath}/config/secrets.yml'
1394             secret="$(<'${cfg.secrets.secretFile}')"
1395             db="$(<'${cfg.secrets.dbFile}')"
1396             otp="$(<'${cfg.secrets.otpFile}')"
1397             jws="$(<'${cfg.secrets.jwsFile}')"
1398             export secret db otp jws
1399             jq -n '{production: {secret_key_base: $ENV.secret,
1400                     otp_key_base: $ENV.otp,
1401                     db_key_base: $ENV.db,
1402                     openid_connect_signing_key: $ENV.jws}}' \
1403                > '${cfg.statePath}/config/secrets.yml'
1404           )
1406           # We remove potentially broken links to old gitlab-shell versions
1407           rm -Rf ${cfg.statePath}/repositories/**/*.git/hooks
1409           git config --global core.autocrlf "input"
1410         '';
1411       };
1412     };
1414     systemd.services.gitlab-db-config = {
1415       after = [ "gitlab-config.service" "gitlab-postgresql.service" "postgresql.service" ];
1416       wants = optional (cfg.databaseHost == "") "postgresql.service" ++ optional databaseActuallyCreateLocally "gitlab-postgresql.service";
1417       bindsTo = [ "gitlab-config.service" ];
1418       wantedBy = [ "gitlab.target" ];
1419       partOf = [ "gitlab.target" ];
1420       serviceConfig = {
1421         Type = "oneshot";
1422         User = cfg.user;
1423         Group = cfg.group;
1424         TimeoutSec = "infinity";
1425         Restart = "on-failure";
1426         WorkingDirectory = "${cfg.packages.gitlab}/share/gitlab";
1427         RemainAfterExit = true;
1429         ExecStart = pkgs.writeShellScript "gitlab-db-config" ''
1430           set -o errexit -o pipefail -o nounset
1431           shopt -s inherit_errexit
1432           umask u=rwx,g=rx,o=
1434           initial_root_password="$(<'${cfg.initialRootPasswordFile}')"
1435           ${gitlab-rake}/bin/gitlab-rake gitlab:db:configure GITLAB_ROOT_PASSWORD="$initial_root_password" \
1436                                                              GITLAB_ROOT_EMAIL='${cfg.initialRootEmail}' > /dev/null
1437         '';
1438       };
1439     };
1441     systemd.services.gitlab-sidekiq = {
1442       after = [
1443         "network.target"
1444         "redis-gitlab.service"
1445         "postgresql.service"
1446         "gitlab-config.service"
1447         "gitlab-db-config.service"
1448       ];
1449       bindsTo = [
1450         "gitlab-config.service"
1451         "gitlab-db-config.service"
1452       ];
1453       wants = [ "redis-gitlab.service" ] ++ optional (cfg.databaseHost == "") "postgresql.service";
1454       wantedBy = [ "gitlab.target" ];
1455       partOf = [ "gitlab.target" ];
1456       environment = gitlabEnv // (optionalAttrs cfg.sidekiq.memoryKiller.enable {
1457         SIDEKIQ_MEMORY_KILLER_MAX_RSS = cfg.sidekiq.memoryKiller.maxMemory;
1458         SIDEKIQ_MEMORY_KILLER_GRACE_TIME = cfg.sidekiq.memoryKiller.graceTime;
1459         SIDEKIQ_MEMORY_KILLER_SHUTDOWN_WAIT = cfg.sidekiq.memoryKiller.shutdownWait;
1460       });
1461       path = [ git ] ++ (with pkgs; [
1462         postgresqlPackage
1463         ruby
1464         openssh
1465         nodejs
1466         gnupg
1468         "${cfg.packages.gitlab}/share/gitlab/vendor/gems/sidekiq-${cfg.packages.gitlab.rubyEnv.gems.sidekiq.version}"
1470         # Needed for GitLab project imports
1471         gnutar
1472         gzip
1474         procps # Sidekiq MemoryKiller
1475       ]);
1476       serviceConfig = {
1477         Type = "simple";
1478         User = cfg.user;
1479         Group = cfg.group;
1480         TimeoutSec = "infinity";
1481         Restart = "always";
1482         WorkingDirectory = "${cfg.packages.gitlab}/share/gitlab";
1483         ExecStart = utils.escapeSystemdExecArgs (
1484           [
1485             "${cfg.packages.gitlab}/share/gitlab/bin/sidekiq-cluster"
1486             "*" # all queue groups
1487           ] ++ lib.optionals (cfg.sidekiq.concurrency != null) [
1488             "--concurrency" (toString cfg.sidekiq.concurrency)
1489           ] ++ [
1490             "--environment" "production"
1491             "--require" "."
1492           ]
1493         );
1494       };
1495     };
1497     systemd.services.gitaly = {
1498       after = [ "network.target" "gitlab-config.service" ];
1499       bindsTo = [ "gitlab-config.service" ];
1500       wantedBy = [ "gitlab.target" ];
1501       partOf = [ "gitlab.target" ];
1502       path = [ git ] ++ (with pkgs; [
1503         openssh
1504         gzip
1505         bzip2
1506       ]);
1507       serviceConfig = {
1508         Type = "simple";
1509         User = cfg.user;
1510         Group = cfg.group;
1511         TimeoutSec = "infinity";
1512         Restart = "on-failure";
1513         WorkingDirectory = gitlabEnv.HOME;
1514         RuntimeDirectory = "gitaly";
1515         ExecStart = "${cfg.packages.gitaly}/bin/gitaly ${gitalyToml}";
1516       };
1517     };
1519     services.gitlab.pages.settings = {
1520       api-secret-key = "${cfg.statePath}/gitlab_pages_secret";
1521     };
1523     systemd.services.gitlab-pages =
1524       let
1525         filteredConfig = filterAttrs (_: v: v != null) cfg.pages.settings;
1526         isSecret = v: isAttrs v && v ? _secret && isString v._secret;
1527         mkPagesKeyValue = lib.generators.toKeyValue {
1528           mkKeyValue = lib.flip lib.generators.mkKeyValueDefault "=" rec {
1529             mkValueString = v:
1530               if isInt           v then toString v
1531               else if isString   v then v
1532               else if true  ==   v then "true"
1533               else if false ==   v then "false"
1534               else if isSecret   v then builtins.hashString "sha256" v._secret
1535               else throw "unsupported type ${builtins.typeOf v}: ${(lib.generators.toPretty {}) v}";
1536           };
1537         };
1538         secretPaths = lib.catAttrs "_secret" (lib.collect isSecret filteredConfig);
1539         mkSecretReplacement = file: ''
1540           replace-secret ${lib.escapeShellArgs [ (builtins.hashString "sha256" file) file "/run/gitlab-pages/gitlab-pages.conf" ]}
1541         '';
1542         secretReplacements = lib.concatMapStrings mkSecretReplacement secretPaths;
1543         configFile = pkgs.writeText "gitlab-pages.conf" (mkPagesKeyValue filteredConfig);
1544       in
1545         mkIf cfg.pages.enable {
1546           description = "GitLab static pages daemon";
1547           after = [ "network.target" "gitlab-config.service" "gitlab.service" ];
1548           bindsTo = [ "gitlab-config.service" "gitlab.service" ];
1549           wantedBy = [ "gitlab.target" ];
1550           partOf = [ "gitlab.target" ];
1552           path = with pkgs; [
1553             unzip
1554             replace-secret
1555           ];
1557           serviceConfig = {
1558             Type = "simple";
1559             TimeoutSec = "infinity";
1560             Restart = "on-failure";
1562             User = cfg.user;
1563             Group = cfg.group;
1565             ExecStartPre = pkgs.writeShellScript "gitlab-pages-pre-start" ''
1566               set -o errexit -o pipefail -o nounset
1567               shopt -s dotglob nullglob inherit_errexit
1569               install -m u=rw ${configFile} /run/gitlab-pages/gitlab-pages.conf
1570               ${secretReplacements}
1571             '';
1572             ExecStart = "${cfg.packages.pages}/bin/gitlab-pages -config=/run/gitlab-pages/gitlab-pages.conf";
1573             WorkingDirectory = gitlabEnv.HOME;
1574             RuntimeDirectory = "gitlab-pages";
1575             RuntimeDirectoryMode = "0700";
1576           };
1577         };
1579     systemd.services.gitlab-workhorse = {
1580       after = [ "network.target" ];
1581       wantedBy = [ "gitlab.target" ];
1582       partOf = [ "gitlab.target" ];
1583       path = [ git ] ++ (with pkgs; [
1584         remarshal
1585         exiftool
1586         git
1587         gnutar
1588         gzip
1589         openssh
1590         cfg.packages.gitlab-workhorse
1591       ]);
1592       serviceConfig = {
1593         Type = "simple";
1594         User = cfg.user;
1595         Group = cfg.group;
1596         TimeoutSec = "infinity";
1597         Restart = "on-failure";
1598         WorkingDirectory = gitlabEnv.HOME;
1599         ExecStartPre = pkgs.writeShellScript "gitlab-workhorse-pre-start" ''
1600           set -o errexit -o pipefail -o nounset
1601           shopt -s dotglob nullglob inherit_errexit
1603           ${utils.genJqSecretsReplacementSnippet
1604               cfg.workhorse.config
1605               "${cfg.statePath}/config/gitlab-workhorse.json"}
1607           json2toml "${cfg.statePath}/config/gitlab-workhorse.json" "${cfg.statePath}/config/gitlab-workhorse.toml"
1608           rm "${cfg.statePath}/config/gitlab-workhorse.json"
1609         '';
1610         ExecStart =
1611           "${cfg.packages.gitlab-workhorse}/bin/${
1612               optionalString (lib.versionAtLeast (lib.getVersion cfg.packages.gitlab-workhorse) "16.10") "gitlab-"
1613             }workhorse "
1614           + "-listenUmask 0 "
1615           + "-listenNetwork unix "
1616           + "-listenAddr /run/gitlab/gitlab-workhorse.socket "
1617           + "-authSocket ${gitlabSocket} "
1618           + "-documentRoot ${cfg.packages.gitlab}/share/gitlab/public "
1619           + "-config ${cfg.statePath}/config/gitlab-workhorse.toml "
1620           + "-secretPath ${cfg.statePath}/.gitlab_workhorse_secret";
1621       };
1622     };
1624     systemd.services.gitlab-mailroom = mkIf (gitlabConfig.production.incoming_email.enabled or false) {
1625       description = "GitLab incoming mail daemon";
1626       after = [ "network.target" "redis-gitlab.service" "gitlab-config.service" ];
1627       bindsTo = [ "gitlab-config.service" ];
1628       wantedBy = [ "gitlab.target" ];
1629       partOf = [ "gitlab.target" ];
1630       environment = gitlabEnv;
1631       serviceConfig = {
1632         Type = "simple";
1633         TimeoutSec = "infinity";
1634         Restart = "on-failure";
1636         User = cfg.user;
1637         Group = cfg.group;
1638         ExecStart = "${cfg.packages.gitlab.rubyEnv}/bin/bundle exec mail_room -c ${cfg.statePath}/config/mail_room.yml";
1639         WorkingDirectory = gitlabEnv.HOME;
1640       };
1641     };
1643     systemd.services.gitlab = {
1644       after = [
1645         "gitlab-workhorse.service"
1646         "network.target"
1647         "redis-gitlab.service"
1648         "gitlab-config.service"
1649         "gitlab-db-config.service"
1650       ];
1651       bindsTo = [
1652         "gitlab-config.service"
1653         "gitlab-db-config.service"
1654       ];
1655       wants = [ "redis-gitlab.service" ] ++ optional (cfg.databaseHost == "") "postgresql.service";
1656       requiredBy = [ "gitlab.target" ];
1657       partOf = [ "gitlab.target" ];
1658       environment = gitlabEnv;
1659       path = [ git ] ++ (with pkgs; [
1660         postgresqlPackage
1661         openssh
1662         nodejs
1663         procps
1664         gnupg
1665         gzip
1666       ]);
1667       serviceConfig = {
1668         Type = "notify";
1669         User = cfg.user;
1670         Group = cfg.group;
1671         TimeoutSec = "infinity";
1672         Restart = "on-failure";
1673         WorkingDirectory = "${cfg.packages.gitlab}/share/gitlab";
1674         ExecStart = concatStringsSep " " [
1675           "${cfg.packages.gitlab.rubyEnv}/bin/bundle" "exec" "puma"
1676           "-e production"
1677           "-C ${cfg.statePath}/config/puma.rb"
1678           "-w ${cfg.puma.workers}"
1679           "-t ${cfg.puma.threadsMin}:${cfg.puma.threadsMax}"
1680         ];
1681       };
1683     };
1685     systemd.services.gitlab-backup = {
1686       after = [ "gitlab.service" ];
1687       bindsTo = [ "gitlab.service" ];
1688       startAt = cfg.backup.startAt;
1689       environment = {
1690         RAILS_ENV = "production";
1691         CRON = "1";
1692       } // optionalAttrs (stringLength cfg.backup.skip > 0) {
1693         SKIP = cfg.backup.skip;
1694       };
1695       serviceConfig = {
1696         User = cfg.user;
1697         Group = cfg.group;
1698         ExecStart = "${gitlab-rake}/bin/gitlab-rake gitlab:backup:create";
1699       };
1700     };
1702   };
1704   meta.doc = ./gitlab.md;
1705   meta.maintainers = teams.gitlab.members;