diff options
Diffstat (limited to 'nixos/modules/services/misc/gitlab.nix')
-rw-r--r-- | nixos/modules/services/misc/gitlab.nix | 776 |
1 files changed, 633 insertions, 143 deletions
diff --git a/nixos/modules/services/misc/gitlab.nix b/nixos/modules/services/misc/gitlab.nix index 425f35f37cb..1514cc0665d 100644 --- a/nixos/modules/services/misc/gitlab.nix +++ b/nixos/modules/services/misc/gitlab.nix @@ -10,7 +10,7 @@ let postgresqlPackage = if config.services.postgresql.enable then config.services.postgresql.package else - pkgs.postgresql; + pkgs.postgresql_12; gitlabSocket = "${cfg.statePath}/tmp/sockets/gitlab.socket"; gitalySocket = "${cfg.statePath}/tmp/sockets/gitaly.socket"; @@ -43,9 +43,16 @@ let [gitlab-shell] dir = "${cfg.packages.gitlab-shell}" + + [hooks] + custom_hooks_dir = "${cfg.statePath}/custom_hooks" + + [gitlab] secret_file = "${cfg.statePath}/gitlab_shell_secret" - gitlab_url = "http+unix://${pathUrlQuote gitlabSocket}" - http_settings = { self_signed_cert = false } + url = "http+unix://${pathUrlQuote gitlabSocket}" + + [gitlab.http-settings] + self_signed_cert = false ${concatStringsSep "\n" (attrValues (mapAttrs (k: v: '' [[storage]] @@ -61,7 +68,6 @@ let repos_path = "${cfg.statePath}/repositories"; secret_file = "${cfg.statePath}/gitlab_shell_secret"; log_file = "${cfg.statePath}/log/gitlab-shell.log"; - custom_hooks_dir = "${cfg.statePath}/custom_hooks"; redis = { bin = "${pkgs.redis}/bin/redis-cli"; host = "127.0.0.1"; @@ -73,6 +79,11 @@ let redisConfig.production.url = cfg.redisUrl; + pagesArgs = [ + "-pages-domain" gitlabConfig.production.pages.host + "-pages-root" "${gitlabConfig.production.shared.path}/pages" + ] ++ cfg.pagesExtraArgs; + gitlabConfig = { # These are the default settings from config/gitlab.example.yml production = flip recursiveUpdate cfg.extraConfig { @@ -105,7 +116,12 @@ let omniauth.enabled = false; shared.path = "${cfg.statePath}/shared"; gitaly.client_path = "${cfg.packages.gitaly}/bin"; - backup.path = "${cfg.backupPath}"; + backup = { + path = cfg.backup.path; + keep_time = cfg.backup.keepTime; + } // (optionalAttrs (cfg.backup.uploadOptions != {}) { + upload = cfg.backup.uploadOptions; + }); gitlab_shell = { path = "${cfg.packages.gitlab-shell}"; hooks_path = "${cfg.statePath}/shell/hooks"; @@ -114,6 +130,7 @@ let receive_pack = true; }; workhorse.secret_file = "${cfg.statePath}/.gitlab_workhorse_secret"; + gitlab_kas.secret_file = "${cfg.statePath}/.gitlab_kas_secret"; git.bin_path = "git"; monitoring = { ip_whitelist = [ "127.0.0.0/8" "::1/128" ]; @@ -123,14 +140,22 @@ let port = 3807; }; }; + registry = lib.optionalAttrs cfg.registry.enable { + enabled = true; + host = cfg.registry.externalAddress; + port = cfg.registry.externalPort; + key = cfg.registry.keyFile; + api_url = "http://${config.services.dockerRegistry.listenAddress}:${toString config.services.dockerRegistry.port}/"; + issuer = "gitlab-issuer"; + }; extra = {}; uploads.storage_path = cfg.statePath; }; }; - gitlabEnv = { + gitlabEnv = cfg.packages.gitlab.gitlabEnv // { HOME = "${cfg.statePath}/home"; - UNICORN_PATH = "${cfg.statePath}/"; + PUMA_PATH = "${cfg.statePath}/"; GITLAB_PATH = "${cfg.packages.gitlab}/share/gitlab/"; SCHEMA = "${cfg.statePath}/db/structure.sql"; GITLAB_UPLOADS_PATH = "${cfg.statePath}/uploads"; @@ -138,7 +163,8 @@ let GITLAB_REDIS_CONFIG_FILE = pkgs.writeText "redis.yml" (builtins.toJSON redisConfig); prometheus_multiproc_dir = "/run/gitlab"; RAILS_ENV = "production"; - }; + MALLOC_ARENA_MAX = "2"; + } // cfg.extraEnv; gitlab-rake = pkgs.stdenv.mkDerivation { name = "gitlab-rake"; @@ -184,6 +210,7 @@ let domain: "${cfg.smtp.domain}", ${optionalString (cfg.smtp.authentication != null) "authentication: :${cfg.smtp.authentication},"} enable_starttls_auto: ${boolToString cfg.smtp.enableStartTLSAuto}, + tls: ${boolToString cfg.smtp.tls}, ca_file: "/etc/ssl/certs/ca-certificates.crt", openssl_verify_mode: '${cfg.smtp.opensslVerifyMode}' } @@ -194,6 +221,7 @@ in { imports = [ (mkRenamedOptionModule [ "services" "gitlab" "stateDir" ] [ "services" "gitlab" "statePath" ]) + (mkRenamedOptionModule [ "services" "gitlab" "backupPath" ] [ "services" "gitlab" "backup" "path" ]) (mkRemovedOptionModule [ "services" "gitlab" "satelliteDir" ] "") ]; @@ -236,11 +264,18 @@ in { description = "Reference to the gitaly package"; }; + packages.pages = mkOption { + type = types.package; + default = pkgs.gitlab-pages; + defaultText = "pkgs.gitlab-pages"; + description = "Reference to the gitlab-pages package"; + }; + statePath = mkOption { type = types.str; default = "/var/gitlab/state"; description = '' - Gitlab state directory. Configuration, repositories and + GitLab state directory. Configuration, repositories and logs, among other things, are stored here. The directory will be created automatically if it doesn't @@ -250,17 +285,116 @@ in { ''; }; - backupPath = mkOption { + extraEnv = mkOption { + type = types.attrsOf types.str; + default = {}; + description = '' + Additional environment variables for the GitLab environment. + ''; + }; + + backup.startAt = mkOption { + type = with types; either str (listOf str); + default = []; + example = "03:00"; + description = '' + The time(s) to run automatic backup of GitLab + state. Specified in systemd's time format; see + <citerefentry><refentrytitle>systemd.time</refentrytitle> + <manvolnum>7</manvolnum></citerefentry>. + ''; + }; + + backup.path = mkOption { type = types.str; default = cfg.statePath + "/backup"; - description = "Gitlab path for backups."; + description = "GitLab path for backups."; + }; + + backup.keepTime = mkOption { + type = types.int; + default = 0; + example = 48; + apply = x: x * 60 * 60; + description = '' + How long to keep the backups around, in + hours. <literal>0</literal> means <quote>keep + forever</quote>. + ''; + }; + + backup.skip = mkOption { + type = with types; + let value = enum [ + "db" + "uploads" + "builds" + "artifacts" + "lfs" + "registry" + "pages" + "repositories" + "tar" + ]; + in + either value (listOf value); + default = []; + example = [ "artifacts" "lfs" ]; + apply = x: if isString x then x else concatStringsSep "," x; + description = '' + Directories to exclude from the backup. The example excludes + CI artifacts and LFS objects from the backups. The + <literal>tar</literal> option skips the creation of a tar + file. + + Refer to <link xlink:href="https://docs.gitlab.com/ee/raketasks/backup_restore.html#excluding-specific-directories-from-the-backup"/> + for more information. + ''; + }; + + backup.uploadOptions = mkOption { + type = types.attrs; + default = {}; + example = literalExample '' + { + # Fog storage connection settings, see http://fog.io/storage/ + connection = { + provider = "AWS"; + region = "eu-north-1"; + aws_access_key_id = "AKIAXXXXXXXXXXXXXXXX"; + aws_secret_access_key = { _secret = config.deployment.keys.aws_access_key.path; }; + }; + + # The remote 'directory' to store your backups in. + # For S3, this would be the bucket name. + remote_directory = "my-gitlab-backups"; + + # Use multipart uploads when file size reaches 100MB, see + # http://docs.aws.amazon.com/AmazonS3/latest/dev/uploadobjusingmpu.html + multipart_chunk_size = 104857600; + + # Turns on AWS Server-Side Encryption with Amazon S3-Managed Keys for backups, this is optional + encryption = "AES256"; + + # Specifies Amazon S3 storage class to use for backups, this is optional + storage_class = "STANDARD"; + }; + ''; + description = '' + GitLab automatic upload specification. Tells GitLab to + upload the backup to a remote location when done. + + Attributes specified here are added under + <literal>production -> backup -> upload</literal> in + <filename>config/gitlab.yml</filename>. + ''; }; databaseHost = mkOption { type = types.str; default = ""; description = '' - Gitlab database hostname. An empty string means <quote>use + GitLab database hostname. An empty string means <quote>use local unix socket connection</quote>. ''; }; @@ -269,7 +403,7 @@ in { type = with types; nullOr path; default = null; description = '' - File containing the Gitlab database user password. + File containing the GitLab database user password. This should be a string, not a nix path, since nix paths are copied into the world-readable nix store. @@ -290,13 +424,13 @@ in { databaseName = mkOption { type = types.str; default = "gitlab"; - description = "Gitlab database name."; + description = "GitLab database name."; }; databaseUsername = mkOption { type = types.str; default = "gitlab"; - description = "Gitlab database user."; + description = "GitLab database user."; }; databasePool = mkOption { @@ -340,14 +474,14 @@ in { host = mkOption { type = types.str; default = config.networking.hostName; - description = "Gitlab host name. Used e.g. for copy-paste URLs."; + description = "GitLab host name. Used e.g. for copy-paste URLs."; }; port = mkOption { - type = types.int; + type = types.port; default = 8080; description = '' - Gitlab server port for copy-paste URLs, e.g. 80 or 443 if you're + GitLab server port for copy-paste URLs, e.g. 80 or 443 if you're service over https. ''; }; @@ -390,6 +524,58 @@ in { ''; }; + registry = { + enable = mkOption { + type = types.bool; + default = false; + description = "Enable GitLab container registry."; + }; + host = mkOption { + type = types.str; + default = config.services.gitlab.host; + description = "GitLab container registry host name."; + }; + port = mkOption { + type = types.int; + default = 4567; + description = "GitLab container registry port."; + }; + certFile = mkOption { + type = types.path; + default = null; + description = "Path to GitLab container registry certificate."; + }; + keyFile = mkOption { + type = types.path; + default = null; + description = "Path to GitLab container registry certificate-key."; + }; + defaultForProjects = mkOption { + type = types.bool; + default = cfg.registry.enable; + description = "If GitLab container registry should be enabled by default for projects."; + }; + issuer = mkOption { + type = types.str; + default = "gitlab-issuer"; + description = "GitLab container registry issuer."; + }; + serviceName = mkOption { + type = types.str; + default = "container_registry"; + description = "GitLab container registry service name."; + }; + externalAddress = mkOption { + type = types.str; + default = ""; + description = "External address used to access registry from the internet"; + }; + externalPort = mkOption { + type = types.int; + description = "External port used to access registry from the internet"; + }; + }; + smtp = { enable = mkOption { type = types.bool; @@ -400,26 +586,26 @@ in { address = mkOption { type = types.str; default = "localhost"; - description = "Address of the SMTP server for Gitlab."; + description = "Address of the SMTP server for GitLab."; }; port = mkOption { type = types.int; - default = 465; - description = "Port of the SMTP server for Gitlab."; + default = 25; + description = "Port of the SMTP server for GitLab."; }; username = mkOption { type = with types; nullOr str; default = null; - description = "Username of the SMTP server for Gitlab."; + description = "Username of the SMTP server for GitLab."; }; passwordFile = mkOption { type = types.nullOr types.path; default = null; description = '' - File containing the password of the SMTP server for Gitlab. + File containing the password of the SMTP server for GitLab. This should be a string, not a nix path, since nix paths are copied into the world-readable nix store. @@ -435,7 +621,7 @@ in { authentication = mkOption { type = with types; nullOr str; default = null; - description = "Authentitcation type to use, see http://api.rubyonrails.org/classes/ActionMailer/Base.html"; + description = "Authentication type to use, see http://api.rubyonrails.org/classes/ActionMailer/Base.html"; }; enableStartTLSAuto = mkOption { @@ -444,6 +630,12 @@ in { description = "Whether to try to use StartTLS."; }; + tls = mkOption { + type = types.bool; + default = false; + description = "Whether to use TLS wrapper-mode."; + }; + opensslVerifyMode = mkOption { type = types.str; default = "peer"; @@ -451,6 +643,12 @@ in { }; }; + pagesExtraArgs = mkOption { + type = types.listOf types.str; + default = [ "-listen-proxy" "127.0.0.1:8090" ]; + description = "Arguments to pass to the gitlab-pages daemon"; + }; + secrets.secretFile = mkOption { type = with types; nullOr path; default = null; @@ -459,7 +657,7 @@ in { the DB. If you change or lose this key you will be unable to access variables stored in database. - Make sure the secret is at least 30 characters and all random, + Make sure the secret is at least 32 characters and all random, no regular words or you'll be exposed to dictionary attacks. This should be a string, not a nix path, since nix paths are @@ -475,7 +673,7 @@ in { the DB. If you change or lose this key you will be unable to access variables stored in database. - Make sure the secret is at least 30 characters and all random, + Make sure the secret is at least 32 characters and all random, no regular words or you'll be exposed to dictionary attacks. This should be a string, not a nix path, since nix paths are @@ -491,7 +689,7 @@ in { tokens. If you change or lose this key, users which have 2FA enabled for login won't be able to login anymore. - Make sure the secret is at least 30 characters and all random, + Make sure the secret is at least 32 characters and all random, no regular words or you'll be exposed to dictionary attacks. This should be a string, not a nix path, since nix paths are @@ -523,6 +721,105 @@ in { description = "Extra configuration to merge into shell-config.yml"; }; + puma.workers = mkOption { + type = types.int; + default = 2; + apply = x: builtins.toString x; + description = '' + The number of worker processes Puma should spawn. This + controls the amount of parallel Ruby code can be + executed. GitLab recommends <quote>Number of CPU cores - + 1</quote>, but at least two. + + <note> + <para> + Each worker consumes quite a bit of memory, so + be careful when increasing this. + </para> + </note> + ''; + }; + + puma.threadsMin = mkOption { + type = types.int; + default = 0; + apply = x: builtins.toString x; + description = '' + The minimum number of threads Puma should use per + worker. + + <note> + <para> + Each thread consumes memory and contributes to Global VM + Lock contention, so be careful when increasing this. + </para> + </note> + ''; + }; + + puma.threadsMax = mkOption { + type = types.int; + default = 4; + apply = x: builtins.toString x; + description = '' + The maximum number of threads Puma should use per + worker. This limits how many threads Puma will automatically + spawn in response to requests. In contrast to workers, + threads will never be able to run Ruby code in parallel, but + give higher IO parallelism. + + <note> + <para> + Each thread consumes memory and contributes to Global VM + Lock contention, so be careful when increasing this. + </para> + </note> + ''; + }; + + sidekiq.memoryKiller.enable = mkOption { + type = types.bool; + default = true; + description = '' + Whether the Sidekiq MemoryKiller should be turned + on. MemoryKiller kills Sidekiq when its memory consumption + exceeds a certain limit. + + See <link xlink:href="https://docs.gitlab.com/ee/administration/operations/sidekiq_memory_killer.html"/> + for details. + ''; + }; + + sidekiq.memoryKiller.maxMemory = mkOption { + type = types.int; + default = 2000; + apply = x: builtins.toString (x * 1024); + description = '' + The maximum amount of memory, in MiB, a Sidekiq worker is + allowed to consume before being killed. + ''; + }; + + sidekiq.memoryKiller.graceTime = mkOption { + type = types.int; + default = 900; + apply = x: builtins.toString x; + description = '' + The time MemoryKiller waits after noticing excessive memory + consumption before killing Sidekiq. + ''; + }; + + sidekiq.memoryKiller.shutdownWait = mkOption { + type = types.int; + default = 30; + apply = x: builtins.toString x; + description = '' + The time allowed for all jobs to finish before Sidekiq is + killed forcefully. + ''; + }; + extraConfig = mkOption { type = types.attrs; default = {}; @@ -612,10 +909,19 @@ in { assertion = cfg.secrets.jwsFile != null; message = "services.gitlab.secrets.jwsFile must be set!"; } + { + assertion = versionAtLeast postgresqlPackage.version "12.0.0"; + message = "PostgreSQL >=12 is required to run GitLab 14. Follow the instructions in the manual section for upgrading PostgreSQL here: https://nixos.org/manual/nixos/stable/index.html#module-services-postgres-upgrading"; + } ]; environment.systemPackages = [ pkgs.git gitlab-rake gitlab-rails cfg.packages.gitlab-shell ]; + systemd.targets.gitlab = { + description = "Common target for all GitLab services."; + wantedBy = [ "multi-user.target" ]; + }; + # Redis is required for the sidekiq queue runner. services.redis.enable = mkDefault true; @@ -630,35 +936,83 @@ in { # here. systemd.services.gitlab-postgresql = let pgsql = config.services.postgresql; in mkIf databaseActuallyCreateLocally { after = [ "postgresql.service" ]; - wantedBy = [ "multi-user.target" ]; - path = [ pgsql.package ]; + bindsTo = [ "postgresql.service" ]; + wantedBy = [ "gitlab.target" ]; + partOf = [ "gitlab.target" ]; + path = [ + pgsql.package + pkgs.util-linux + ]; script = '' set -eu - PSQL="${pkgs.utillinux}/bin/runuser -u ${pgsql.superUser} -- psql --port=${toString pgsql.port}" + PSQL() { + psql --port=${toString pgsql.port} "$@" + } - $PSQL -tAc "SELECT 1 FROM pg_database WHERE datname = '${cfg.databaseName}'" | grep -q 1 || $PSQL -tAc 'CREATE DATABASE "${cfg.databaseName}" OWNER "${cfg.databaseUsername}"' - current_owner=$($PSQL -tAc "SELECT pg_catalog.pg_get_userbyid(datdba) FROM pg_catalog.pg_database WHERE datname = '${cfg.databaseName}'") + PSQL -tAc "SELECT 1 FROM pg_database WHERE datname = '${cfg.databaseName}'" | grep -q 1 || PSQL -tAc 'CREATE DATABASE "${cfg.databaseName}" OWNER "${cfg.databaseUsername}"' + current_owner=$(PSQL -tAc "SELECT pg_catalog.pg_get_userbyid(datdba) FROM pg_catalog.pg_database WHERE datname = '${cfg.databaseName}'") if [[ "$current_owner" != "${cfg.databaseUsername}" ]]; then - $PSQL -tAc 'ALTER DATABASE "${cfg.databaseName}" OWNER TO "${cfg.databaseUsername}"' + PSQL -tAc 'ALTER DATABASE "${cfg.databaseName}" OWNER TO "${cfg.databaseUsername}"' if [[ -e "${config.services.postgresql.dataDir}/.reassigning_${cfg.databaseName}" ]]; then echo "Reassigning ownership of database ${cfg.databaseName} to user ${cfg.databaseUsername} failed on last boot. Failing..." exit 1 fi touch "${config.services.postgresql.dataDir}/.reassigning_${cfg.databaseName}" - $PSQL "${cfg.databaseName}" -tAc "REASSIGN OWNED BY \"$current_owner\" TO \"${cfg.databaseUsername}\"" + PSQL "${cfg.databaseName}" -tAc "REASSIGN OWNED BY \"$current_owner\" TO \"${cfg.databaseUsername}\"" rm "${config.services.postgresql.dataDir}/.reassigning_${cfg.databaseName}" fi - $PSQL '${cfg.databaseName}' -tAc "CREATE EXTENSION IF NOT EXISTS pg_trgm" + PSQL '${cfg.databaseName}' -tAc "CREATE EXTENSION IF NOT EXISTS pg_trgm" + PSQL '${cfg.databaseName}' -tAc "CREATE EXTENSION IF NOT EXISTS btree_gist;" ''; serviceConfig = { + User = pgsql.superUser; Type = "oneshot"; + RemainAfterExit = true; + }; + }; + + systemd.services.gitlab-registry-cert = optionalAttrs cfg.registry.enable { + path = with pkgs; [ openssl ]; + + script = '' + mkdir -p $(dirname ${cfg.registry.keyFile}) + mkdir -p $(dirname ${cfg.registry.certFile}) + openssl req -nodes -newkey rsa:4096 -keyout ${cfg.registry.keyFile} -out /tmp/registry-auth.csr -subj "/CN=${cfg.registry.issuer}" + openssl x509 -in /tmp/registry-auth.csr -out ${cfg.registry.certFile} -req -signkey ${cfg.registry.keyFile} -days 3650 + chown ${cfg.user}:${cfg.group} $(dirname ${cfg.registry.keyFile}) + chown ${cfg.user}:${cfg.group} $(dirname ${cfg.registry.certFile}) + chown ${cfg.user}:${cfg.group} ${cfg.registry.keyFile} + chown ${cfg.user}:${cfg.group} ${cfg.registry.certFile} + ''; + + serviceConfig = { + ConditionPathExists = "!${cfg.registry.certFile}"; + }; + }; + + # Ensure Docker Registry launches after the certificate generation job + systemd.services.docker-registry = optionalAttrs cfg.registry.enable { + wants = [ "gitlab-registry-cert.service" ]; + }; + + # Enable Docker Registry, if GitLab-Container Registry is enabled + services.dockerRegistry = optionalAttrs cfg.registry.enable { + enable = true; + enableDelete = true; # This must be true, otherwise GitLab won't manage it correctly + extraConfig = { + auth.token = { + realm = "http${if cfg.https == true then "s" else ""}://${cfg.host}/jwt/auth"; + service = cfg.registry.serviceName; + issuer = cfg.registry.issuer; + rootcertbundle = cfg.registry.certFile; + }; }; }; # Use postfix to send out mails. - services.postfix.enable = mkDefault true; + services.postfix.enable = mkDefault (cfg.smtp.enable && cfg.smtp.address == "localhost"); users.users.${cfg.user} = { group = cfg.group; @@ -673,11 +1027,10 @@ in { "d /run/gitlab 0755 ${cfg.user} ${cfg.group} -" "d ${gitlabEnv.HOME} 0750 ${cfg.user} ${cfg.group} -" "z ${gitlabEnv.HOME}/.ssh/authorized_keys 0600 ${cfg.user} ${cfg.group} -" - "d ${cfg.backupPath} 0750 ${cfg.user} ${cfg.group} -" + "d ${cfg.backup.path} 0750 ${cfg.user} ${cfg.group} -" "d ${cfg.statePath} 0750 ${cfg.user} ${cfg.group} -" "d ${cfg.statePath}/builds 0750 ${cfg.user} ${cfg.group} -" "d ${cfg.statePath}/config 0750 ${cfg.user} ${cfg.group} -" - "d ${cfg.statePath}/config/initializers 0750 ${cfg.user} ${cfg.group} -" "d ${cfg.statePath}/db 0750 ${cfg.user} ${cfg.group} -" "d ${cfg.statePath}/log 0750 ${cfg.user} ${cfg.group} -" "d ${cfg.statePath}/repositories 2770 ${cfg.user} ${cfg.group} -" @@ -700,17 +1053,163 @@ in { "L+ /run/gitlab/uploads - - - - ${cfg.statePath}/uploads" "L+ /run/gitlab/shell-config.yml - - - - ${pkgs.writeText "config.yml" (builtins.toJSON gitlabShellConfig)}" - - "L+ ${cfg.statePath}/config/unicorn.rb - - - - ${./defaultUnicornConfig.rb}" ]; + + systemd.services.gitlab-config = { + wantedBy = [ "gitlab.target" ]; + partOf = [ "gitlab.target" ]; + path = with pkgs; [ + jq + openssl + replace-secret + git + ]; + serviceConfig = { + Type = "oneshot"; + User = cfg.user; + Group = cfg.group; + TimeoutSec = "infinity"; + Restart = "on-failure"; + WorkingDirectory = "${cfg.packages.gitlab}/share/gitlab"; + RemainAfterExit = true; + + ExecStartPre = let + preStartFullPrivileges = '' + shopt -s dotglob nullglob + set -eu + + chown --no-dereference '${cfg.user}':'${cfg.group}' '${cfg.statePath}'/* + if [[ -n "$(ls -A '${cfg.statePath}'/config/)" ]]; then + chown --no-dereference '${cfg.user}':'${cfg.group}' '${cfg.statePath}'/config/* + fi + ''; + in "+${pkgs.writeShellScript "gitlab-pre-start-full-privileges" preStartFullPrivileges}"; + + ExecStart = pkgs.writeShellScript "gitlab-config" '' + set -eu + + umask u=rwx,g=rx,o= + + cp -f ${cfg.packages.gitlab}/share/gitlab/VERSION ${cfg.statePath}/VERSION + rm -rf ${cfg.statePath}/db/* + rm -f ${cfg.statePath}/lib + find '${cfg.statePath}/config/' -maxdepth 1 -mindepth 1 -type d -execdir rm -rf {} \; + cp -rf --no-preserve=mode ${cfg.packages.gitlab}/share/gitlab/config.dist/* ${cfg.statePath}/config + cp -rf --no-preserve=mode ${cfg.packages.gitlab}/share/gitlab/db/* ${cfg.statePath}/db + ln -sf ${extraGitlabRb} ${cfg.statePath}/config/initializers/extra-gitlab.rb + + ${cfg.packages.gitlab-shell}/bin/install + + ${optionalString cfg.smtp.enable '' + install -m u=rw ${smtpSettings} ${cfg.statePath}/config/initializers/smtp_settings.rb + ${optionalString (cfg.smtp.passwordFile != null) '' + replace-secret '@smtpPassword@' '${cfg.smtp.passwordFile}' '${cfg.statePath}/config/initializers/smtp_settings.rb' + ''} + ''} + + ( + umask u=rwx,g=,o= + + openssl rand -hex 32 > ${cfg.statePath}/gitlab_shell_secret + + rm -f '${cfg.statePath}/config/database.yml' + + ${if cfg.databasePasswordFile != null then '' + export db_password="$(<'${cfg.databasePasswordFile}')" + + if [[ -z "$db_password" ]]; then + >&2 echo "Database password was an empty string!" + exit 1 + fi + + jq <${pkgs.writeText "database.yml" (builtins.toJSON databaseConfig)} \ + '.production.password = $ENV.db_password' \ + >'${cfg.statePath}/config/database.yml' + '' + else '' + jq <${pkgs.writeText "database.yml" (builtins.toJSON databaseConfig)} \ + >'${cfg.statePath}/config/database.yml' + '' + } + + ${utils.genJqSecretsReplacementSnippet + gitlabConfig + "${cfg.statePath}/config/gitlab.yml" + } + + rm -f '${cfg.statePath}/config/secrets.yml' + + export secret="$(<'${cfg.secrets.secretFile}')" + export db="$(<'${cfg.secrets.dbFile}')" + export otp="$(<'${cfg.secrets.otpFile}')" + export jws="$(<'${cfg.secrets.jwsFile}')" + jq -n '{production: {secret_key_base: $ENV.secret, + otp_key_base: $ENV.otp, + db_key_base: $ENV.db, + openid_connect_signing_key: $ENV.jws}}' \ + > '${cfg.statePath}/config/secrets.yml' + ) + + # We remove potentially broken links to old gitlab-shell versions + rm -Rf ${cfg.statePath}/repositories/**/*.git/hooks + + git config --global core.autocrlf "input" + ''; + }; + }; + + systemd.services.gitlab-db-config = { + after = [ "gitlab-config.service" "gitlab-postgresql.service" "postgresql.service" ]; + bindsTo = [ + "gitlab-config.service" + ] ++ optional (cfg.databaseHost == "") "postgresql.service" + ++ optional databaseActuallyCreateLocally "gitlab-postgresql.service"; + wantedBy = [ "gitlab.target" ]; + partOf = [ "gitlab.target" ]; + serviceConfig = { + Type = "oneshot"; + User = cfg.user; + Group = cfg.group; + TimeoutSec = "infinity"; + Restart = "on-failure"; + WorkingDirectory = "${cfg.packages.gitlab}/share/gitlab"; + RemainAfterExit = true; + + ExecStart = pkgs.writeShellScript "gitlab-db-config" '' + set -eu + umask u=rwx,g=rx,o= + + initial_root_password="$(<'${cfg.initialRootPasswordFile}')" + ${gitlab-rake}/bin/gitlab-rake gitlab:db:configure GITLAB_ROOT_PASSWORD="$initial_root_password" \ + GITLAB_ROOT_EMAIL='${cfg.initialRootEmail}' > /dev/null + ''; + }; + }; + systemd.services.gitlab-sidekiq = { - after = [ "network.target" "redis.service" "gitlab.service" ]; - wantedBy = [ "multi-user.target" ]; - environment = gitlabEnv; + after = [ + "network.target" + "redis.service" + "postgresql.service" + "gitlab-config.service" + "gitlab-db-config.service" + ]; + bindsTo = [ + "redis.service" + "gitlab-config.service" + "gitlab-db-config.service" + ] ++ optional (cfg.databaseHost == "") "postgresql.service"; + wantedBy = [ "gitlab.target" ]; + partOf = [ "gitlab.target" ]; + environment = gitlabEnv // (optionalAttrs cfg.sidekiq.memoryKiller.enable { + SIDEKIQ_MEMORY_KILLER_MAX_RSS = cfg.sidekiq.memoryKiller.maxMemory; + SIDEKIQ_MEMORY_KILLER_GRACE_TIME = cfg.sidekiq.memoryKiller.graceTime; + SIDEKIQ_MEMORY_KILLER_SHUTDOWN_WAIT = cfg.sidekiq.memoryKiller.shutdownWait; + }); path = with pkgs; [ postgresqlPackage - gitAndTools.git + git ruby openssh nodejs @@ -719,25 +1218,29 @@ in { # Needed for GitLab project imports gnutar gzip + + procps # Sidekiq MemoryKiller ]; serviceConfig = { Type = "simple"; User = cfg.user; Group = cfg.group; TimeoutSec = "infinity"; - Restart = "on-failure"; + Restart = "always"; WorkingDirectory = "${cfg.packages.gitlab}/share/gitlab"; ExecStart="${cfg.packages.gitlab.rubyEnv}/bin/sidekiq -C \"${cfg.packages.gitlab}/share/gitlab/config/sidekiq_queues.yml\" -e production"; }; }; systemd.services.gitaly = { - after = [ "network.target" ]; - wantedBy = [ "multi-user.target" ]; + after = [ "network.target" "gitlab-config.service" ]; + bindsTo = [ "gitlab-config.service" ]; + wantedBy = [ "gitlab.target" ]; + partOf = [ "gitlab.target" ]; path = with pkgs; [ openssh procps # See https://gitlab.com/gitlab-org/gitaly/issues/1562 - gitAndTools.git + git cfg.packages.gitaly.rubyEnv cfg.packages.gitaly.rubyEnv.wrappedRuby gzip @@ -754,12 +1257,35 @@ in { }; }; + systemd.services.gitlab-pages = mkIf (gitlabConfig.production.pages.enabled or false) { + description = "GitLab static pages daemon"; + after = [ "network.target" "gitlab-config.service" ]; + bindsTo = [ "gitlab-config.service" ]; + wantedBy = [ "gitlab.target" ]; + partOf = [ "gitlab.target" ]; + + path = [ pkgs.unzip ]; + + serviceConfig = { + Type = "simple"; + TimeoutSec = "infinity"; + Restart = "on-failure"; + + User = cfg.user; + Group = cfg.group; + + ExecStart = "${cfg.packages.pages}/bin/gitlab-pages ${escapeShellArgs pagesArgs}"; + WorkingDirectory = gitlabEnv.HOME; + }; + }; + systemd.services.gitlab-workhorse = { after = [ "network.target" ]; - wantedBy = [ "multi-user.target" ]; + wantedBy = [ "gitlab.target" ]; + partOf = [ "gitlab.target" ]; path = with pkgs; [ exiftool - gitAndTools.git + git gnutar gzip openssh @@ -783,14 +1309,44 @@ in { }; }; + systemd.services.gitlab-mailroom = mkIf (gitlabConfig.production.incoming_email.enabled or false) { + description = "GitLab incoming mail daemon"; + after = [ "network.target" "redis.service" "gitlab-config.service" ]; + bindsTo = [ "gitlab-config.service" ]; + wantedBy = [ "gitlab.target" ]; + partOf = [ "gitlab.target" ]; + environment = gitlabEnv; + serviceConfig = { + Type = "simple"; + TimeoutSec = "infinity"; + Restart = "on-failure"; + + User = cfg.user; + Group = cfg.group; + ExecStart = "${cfg.packages.gitlab.rubyEnv}/bin/bundle exec mail_room -c ${cfg.statePath}/config/mail_room.yml"; + WorkingDirectory = gitlabEnv.HOME; + }; + }; + systemd.services.gitlab = { - after = [ "gitlab-workhorse.service" "gitaly.service" "network.target" "gitlab-postgresql.service" "redis.service" ]; - requires = [ "gitlab-sidekiq.service" ]; - wantedBy = [ "multi-user.target" ]; + after = [ + "gitlab-workhorse.service" + "network.target" + "redis.service" + "gitlab-config.service" + "gitlab-db-config.service" + ]; + bindsTo = [ + "redis.service" + "gitlab-config.service" + "gitlab-db-config.service" + ] ++ optional (cfg.databaseHost == "") "postgresql.service"; + wantedBy = [ "gitlab.target" ]; + partOf = [ "gitlab.target" ]; environment = gitlabEnv; path = with pkgs; [ postgresqlPackage - gitAndTools.git + git openssh nodejs procps @@ -804,100 +1360,34 @@ in { TimeoutSec = "infinity"; Restart = "on-failure"; WorkingDirectory = "${cfg.packages.gitlab}/share/gitlab"; - ExecStartPre = let - preStartFullPrivileges = '' - shopt -s dotglob nullglob - set -eu - - chown --no-dereference '${cfg.user}':'${cfg.group}' '${cfg.statePath}'/* - chown --no-dereference '${cfg.user}':'${cfg.group}' '${cfg.statePath}'/config/* - ''; - preStart = '' - set -eu - - cp -f ${cfg.packages.gitlab}/share/gitlab/VERSION ${cfg.statePath}/VERSION - rm -rf ${cfg.statePath}/db/* - rm -rf ${cfg.statePath}/config/initializers/* - rm -f ${cfg.statePath}/lib - cp -rf --no-preserve=mode ${cfg.packages.gitlab}/share/gitlab/config.dist/* ${cfg.statePath}/config - cp -rf --no-preserve=mode ${cfg.packages.gitlab}/share/gitlab/db/* ${cfg.statePath}/db - ln -sf ${extraGitlabRb} ${cfg.statePath}/config/initializers/extra-gitlab.rb - - ${cfg.packages.gitlab-shell}/bin/install - - ${optionalString cfg.smtp.enable '' - install -m u=rw ${smtpSettings} ${cfg.statePath}/config/initializers/smtp_settings.rb - ${optionalString (cfg.smtp.passwordFile != null) '' - smtp_password=$(<'${cfg.smtp.passwordFile}') - ${pkgs.replace}/bin/replace-literal -e '@smtpPassword@' "$smtp_password" '${cfg.statePath}/config/initializers/smtp_settings.rb' - ''} - ''} - - ( - umask u=rwx,g=,o= - - ${pkgs.openssl}/bin/openssl rand -hex 32 > ${cfg.statePath}/gitlab_shell_secret - - if [[ -h '${cfg.statePath}/config/database.yml' ]]; then - rm '${cfg.statePath}/config/database.yml' - fi - - ${if cfg.databasePasswordFile != null then '' - export db_password="$(<'${cfg.databasePasswordFile}')" - - if [[ -z "$db_password" ]]; then - >&2 echo "Database password was an empty string!" - exit 1 - fi - - ${pkgs.jq}/bin/jq <${pkgs.writeText "database.yml" (builtins.toJSON databaseConfig)} \ - '.production.password = $ENV.db_password' \ - >'${cfg.statePath}/config/database.yml' - '' - else '' - ${pkgs.jq}/bin/jq <${pkgs.writeText "database.yml" (builtins.toJSON databaseConfig)} \ - >'${cfg.statePath}/config/database.yml' - '' - } - - ${utils.genJqSecretsReplacementSnippet - gitlabConfig - "${cfg.statePath}/config/gitlab.yml" - } - - if [[ -h '${cfg.statePath}/config/secrets.yml' ]]; then - rm '${cfg.statePath}/config/secrets.yml' - fi - - export secret="$(<'${cfg.secrets.secretFile}')" - export db="$(<'${cfg.secrets.dbFile}')" - export otp="$(<'${cfg.secrets.otpFile}')" - export jws="$(<'${cfg.secrets.jwsFile}')" - ${pkgs.jq}/bin/jq -n '{production: {secret_key_base: $ENV.secret, - otp_key_base: $ENV.otp, - db_key_base: $ENV.db, - openid_connect_signing_key: $ENV.jws}}' \ - > '${cfg.statePath}/config/secrets.yml' - ) - - initial_root_password="$(<'${cfg.initialRootPasswordFile}')" - ${gitlab-rake}/bin/gitlab-rake gitlab:db:configure GITLAB_ROOT_PASSWORD="$initial_root_password" \ - GITLAB_ROOT_EMAIL='${cfg.initialRootEmail}' > /dev/null - - # We remove potentially broken links to old gitlab-shell versions - rm -Rf ${cfg.statePath}/repositories/**/*.git/hooks - - ${pkgs.git}/bin/git config --global core.autocrlf "input" - ''; - in [ - "+${pkgs.writeShellScript "gitlab-pre-start-full-privileges" preStartFullPrivileges}" - "${pkgs.writeShellScript "gitlab-pre-start" preStart}" + ExecStart = concatStringsSep " " [ + "${cfg.packages.gitlab.rubyEnv}/bin/puma" + "-e production" + "-C ${cfg.statePath}/config/puma.rb" + "-w ${cfg.puma.workers}" + "-t ${cfg.puma.threadsMin}:${cfg.puma.threadsMax}" ]; - ExecStart = "${cfg.packages.gitlab.rubyEnv}/bin/unicorn -c ${cfg.statePath}/config/unicorn.rb -E production"; }; }; + systemd.services.gitlab-backup = { + after = [ "gitlab.service" ]; + bindsTo = [ "gitlab.service" ]; + startAt = cfg.backup.startAt; + environment = { + RAILS_ENV = "production"; + CRON = "1"; + } // optionalAttrs (stringLength cfg.backup.skip > 0) { + SKIP = cfg.backup.skip; + }; + serviceConfig = { + User = cfg.user; + Group = cfg.group; + ExecStart = "${gitlab-rake}/bin/gitlab-rake gitlab:backup:create"; + }; + }; + }; meta.doc = ./gitlab.xml; |