diff options
Diffstat (limited to 'nixos/modules/services/misc/gitlab.nix')
-rw-r--r-- | nixos/modules/services/misc/gitlab.nix | 1458 |
1 files changed, 1458 insertions, 0 deletions
diff --git a/nixos/modules/services/misc/gitlab.nix b/nixos/modules/services/misc/gitlab.nix new file mode 100644 index 00000000000..e48444f7161 --- /dev/null +++ b/nixos/modules/services/misc/gitlab.nix @@ -0,0 +1,1458 @@ +{ config, lib, options, pkgs, utils, ... }: + +with lib; + +let + cfg = config.services.gitlab; + opt = options.services.gitlab; + + ruby = cfg.packages.gitlab.ruby; + + postgresqlPackage = if config.services.postgresql.enable then + config.services.postgresql.package + else + pkgs.postgresql_12; + + gitlabSocket = "${cfg.statePath}/tmp/sockets/gitlab.socket"; + gitalySocket = "${cfg.statePath}/tmp/sockets/gitaly.socket"; + pathUrlQuote = url: replaceStrings ["/"] ["%2F"] url; + + databaseConfig = { + production = { + adapter = "postgresql"; + database = cfg.databaseName; + host = cfg.databaseHost; + username = cfg.databaseUsername; + encoding = "utf8"; + pool = cfg.databasePool; + } // cfg.extraDatabaseConfig; + }; + + # We only want to create a database if we're actually going to connect to it. + databaseActuallyCreateLocally = cfg.databaseCreateLocally && cfg.databaseHost == ""; + + gitalyToml = pkgs.writeText "gitaly.toml" '' + socket_path = "${lib.escape ["\""] gitalySocket}" + bin_dir = "${cfg.packages.gitaly}/bin" + prometheus_listen_addr = "localhost:9236" + + [git] + bin_path = "${pkgs.git}/bin/git" + + [gitaly-ruby] + dir = "${cfg.packages.gitaly.ruby}" + + [gitlab-shell] + dir = "${cfg.packages.gitlab-shell}" + + [hooks] + custom_hooks_dir = "${cfg.statePath}/custom_hooks" + + [gitlab] + secret_file = "${cfg.statePath}/gitlab_shell_secret" + url = "http+unix://${pathUrlQuote gitlabSocket}" + + [gitlab.http-settings] + self_signed_cert = false + + ${concatStringsSep "\n" (attrValues (mapAttrs (k: v: '' + [[storage]] + name = "${lib.escape ["\""] k}" + path = "${lib.escape ["\""] v.path}" + '') gitlabConfig.production.repositories.storages))} + ''; + + gitlabShellConfig = flip recursiveUpdate cfg.extraShellConfig { + user = cfg.user; + gitlab_url = "http+unix://${pathUrlQuote gitlabSocket}"; + http_settings.self_signed_cert = false; + repos_path = "${cfg.statePath}/repositories"; + secret_file = "${cfg.statePath}/gitlab_shell_secret"; + log_file = "${cfg.statePath}/log/gitlab-shell.log"; + redis = { + bin = "${pkgs.redis}/bin/redis-cli"; + host = "127.0.0.1"; + port = config.services.redis.servers.gitlab.port; + database = 0; + namespace = "resque:gitlab"; + }; + }; + + 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 { + gitlab = { + host = cfg.host; + port = cfg.port; + https = cfg.https; + user = cfg.user; + email_enabled = true; + email_display_name = "GitLab"; + email_reply_to = "noreply@localhost"; + default_theme = 2; + default_projects_features = { + issues = true; + merge_requests = true; + wiki = true; + snippets = true; + builds = true; + container_registry = true; + }; + }; + repositories.storages.default.path = "${cfg.statePath}/repositories"; + repositories.storages.default.gitaly_address = "unix:${gitalySocket}"; + artifacts.enabled = true; + lfs.enabled = true; + gravatar.enabled = true; + cron_jobs = { }; + gitlab_ci.builds_path = "${cfg.statePath}/builds"; + ldap.enabled = false; + omniauth.enabled = false; + shared.path = "${cfg.statePath}/shared"; + gitaly.client_path = "${cfg.packages.gitaly}/bin"; + backup = { + gitaly_backup_path = "${cfg.packages.gitaly}/bin/gitaly-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"; + secret_file = "${cfg.statePath}/gitlab_shell_secret"; + upload_pack = true; + 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" ]; + sidekiq_exporter = { + enable = true; + address = "localhost"; + 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 = cfg.packages.gitlab.gitlabEnv // { + HOME = "${cfg.statePath}/home"; + PUMA_PATH = "${cfg.statePath}/"; + GITLAB_PATH = "${cfg.packages.gitlab}/share/gitlab/"; + SCHEMA = "${cfg.statePath}/db/structure.sql"; + GITLAB_UPLOADS_PATH = "${cfg.statePath}/uploads"; + GITLAB_LOG_PATH = "${cfg.statePath}/log"; + 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"; + buildInputs = [ pkgs.makeWrapper ]; + dontBuild = true; + dontUnpack = true; + installPhase = '' + mkdir -p $out/bin + makeWrapper ${cfg.packages.gitlab.rubyEnv}/bin/rake $out/bin/gitlab-rake \ + ${concatStrings (mapAttrsToList (name: value: "--set ${name} '${value}' ") gitlabEnv)} \ + --set PATH '${lib.makeBinPath [ pkgs.nodejs pkgs.gzip pkgs.git pkgs.gnutar postgresqlPackage pkgs.coreutils pkgs.procps ]}:$PATH' \ + --set RAKEOPT '-f ${cfg.packages.gitlab}/share/gitlab/Rakefile' \ + --run 'cd ${cfg.packages.gitlab}/share/gitlab' + ''; + }; + + gitlab-rails = pkgs.stdenv.mkDerivation { + name = "gitlab-rails"; + buildInputs = [ pkgs.makeWrapper ]; + dontBuild = true; + dontUnpack = true; + installPhase = '' + mkdir -p $out/bin + makeWrapper ${cfg.packages.gitlab.rubyEnv}/bin/rails $out/bin/gitlab-rails \ + ${concatStrings (mapAttrsToList (name: value: "--set ${name} '${value}' ") gitlabEnv)} \ + --set PATH '${lib.makeBinPath [ pkgs.nodejs pkgs.gzip pkgs.git pkgs.gnutar postgresqlPackage pkgs.coreutils pkgs.procps ]}:$PATH' \ + --run 'cd ${cfg.packages.gitlab}/share/gitlab' + ''; + }; + + extraGitlabRb = pkgs.writeText "extra-gitlab.rb" cfg.extraGitlabRb; + + smtpSettings = pkgs.writeText "gitlab-smtp-settings.rb" '' + if Rails.env.production? + Rails.application.config.action_mailer.delivery_method = :smtp + + ActionMailer::Base.delivery_method = :smtp + ActionMailer::Base.smtp_settings = { + address: "${cfg.smtp.address}", + port: ${toString cfg.smtp.port}, + ${optionalString (cfg.smtp.username != null) ''user_name: "${cfg.smtp.username}",''} + ${optionalString (cfg.smtp.passwordFile != null) ''password: "@smtpPassword@",''} + 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}' + } + end + ''; + +in { + + imports = [ + (mkRenamedOptionModule [ "services" "gitlab" "stateDir" ] [ "services" "gitlab" "statePath" ]) + (mkRenamedOptionModule [ "services" "gitlab" "backupPath" ] [ "services" "gitlab" "backup" "path" ]) + (mkRemovedOptionModule [ "services" "gitlab" "satelliteDir" ] "") + ]; + + options = { + services.gitlab = { + enable = mkOption { + type = types.bool; + default = false; + description = '' + Enable the gitlab service. + ''; + }; + + packages.gitlab = mkOption { + type = types.package; + default = pkgs.gitlab; + defaultText = literalExpression "pkgs.gitlab"; + description = "Reference to the gitlab package"; + example = literalExpression "pkgs.gitlab-ee"; + }; + + packages.gitlab-shell = mkOption { + type = types.package; + default = pkgs.gitlab-shell; + defaultText = literalExpression "pkgs.gitlab-shell"; + description = "Reference to the gitlab-shell package"; + }; + + packages.gitlab-workhorse = mkOption { + type = types.package; + default = pkgs.gitlab-workhorse; + defaultText = literalExpression "pkgs.gitlab-workhorse"; + description = "Reference to the gitlab-workhorse package"; + }; + + packages.gitaly = mkOption { + type = types.package; + default = pkgs.gitaly; + defaultText = literalExpression "pkgs.gitaly"; + description = "Reference to the gitaly package"; + }; + + packages.pages = mkOption { + type = types.package; + default = pkgs.gitlab-pages; + defaultText = literalExpression "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 + logs, among other things, are stored here. + + The directory will be created automatically if it doesn't + exist already. Its parent directories must be owned by + either <literal>root</literal> or the user set in + <option>services.gitlab.user</option>. + ''; + }; + + 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"; + defaultText = literalExpression ''config.${opt.statePath} + "/backup"''; + 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 = literalExpression '' + { + # 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 + local unix socket connection</quote>. + ''; + }; + + databasePasswordFile = mkOption { + type = with types; nullOr path; + default = null; + description = '' + 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. + ''; + }; + + databaseCreateLocally = mkOption { + type = types.bool; + default = true; + description = '' + Whether a database should be automatically created on the + local host. Set this to <literal>false</literal> if you plan + on provisioning a local database yourself. This has no effect + if <option>services.gitlab.databaseHost</option> is customized. + ''; + }; + + databaseName = mkOption { + type = types.str; + default = "gitlab"; + description = "GitLab database name."; + }; + + databaseUsername = mkOption { + type = types.str; + default = "gitlab"; + description = "GitLab database user."; + }; + + databasePool = mkOption { + type = types.int; + default = 5; + description = "Database connection pool size."; + }; + + extraDatabaseConfig = mkOption { + type = types.attrs; + default = {}; + description = "Extra configuration in config/database.yml."; + }; + + redisUrl = mkOption { + type = types.str; + default = "redis://localhost:${toString config.services.redis.servers.gitlab.port}/"; + defaultText = literalExpression ''redis://localhost:''${toString config.services.redis.servers.gitlab.port}/''; + description = "Redis URL for all GitLab services except gitlab-shell"; + }; + + extraGitlabRb = mkOption { + type = types.str; + default = ""; + example = '' + if Rails.env.production? + Rails.application.config.action_mailer.delivery_method = :sendmail + ActionMailer::Base.delivery_method = :sendmail + ActionMailer::Base.sendmail_settings = { + location: "/run/wrappers/bin/sendmail", + arguments: "-i -t" + } + end + ''; + description = '' + Extra configuration to be placed in config/extra-gitlab.rb. This can + be used to add configuration not otherwise exposed through this module's + options. + ''; + }; + + host = mkOption { + type = types.str; + default = config.networking.hostName; + defaultText = literalExpression "config.networking.hostName"; + description = "GitLab host name. Used e.g. for copy-paste URLs."; + }; + + port = mkOption { + type = types.port; + default = 8080; + description = '' + GitLab server port for copy-paste URLs, e.g. 80 or 443 if you're + service over https. + ''; + }; + + https = mkOption { + type = types.bool; + default = false; + description = "Whether gitlab prints URLs with https as scheme."; + }; + + user = mkOption { + type = types.str; + default = "gitlab"; + description = "User to run gitlab and all related services."; + }; + + group = mkOption { + type = types.str; + default = "gitlab"; + description = "Group to run gitlab and all related services."; + }; + + initialRootEmail = mkOption { + type = types.str; + default = "admin@local.host"; + description = '' + Initial email address of the root account if this is a new install. + ''; + }; + + initialRootPasswordFile = mkOption { + type = with types; nullOr path; + default = null; + description = '' + File containing the initial password of the root account if + this is a new install. + + This should be a string, not a nix path, since nix paths are + copied into the world-readable nix store. + ''; + }; + + registry = { + enable = mkOption { + type = types.bool; + default = false; + description = "Enable GitLab container registry."; + }; + host = mkOption { + type = types.str; + default = config.services.gitlab.host; + defaultText = literalExpression "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; + description = "Path to GitLab container registry certificate."; + }; + keyFile = mkOption { + type = types.path; + description = "Path to GitLab container registry certificate-key."; + }; + defaultForProjects = mkOption { + type = types.bool; + default = cfg.registry.enable; + defaultText = literalExpression "config.${opt.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; + default = false; + description = "Enable gitlab mail delivery over SMTP."; + }; + + address = mkOption { + type = types.str; + default = "localhost"; + description = "Address of the SMTP server for GitLab."; + }; + + port = mkOption { + type = types.int; + 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."; + }; + + passwordFile = mkOption { + type = types.nullOr types.path; + default = null; + description = '' + 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. + ''; + }; + + domain = mkOption { + type = types.str; + default = "localhost"; + description = "HELO domain to use for outgoing mail."; + }; + + authentication = mkOption { + type = with types; nullOr str; + default = null; + description = "Authentication type to use, see http://api.rubyonrails.org/classes/ActionMailer/Base.html"; + }; + + enableStartTLSAuto = mkOption { + type = types.bool; + default = true; + 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"; + description = "How OpenSSL checks the certificate, see http://api.rubyonrails.org/classes/ActionMailer/Base.html"; + }; + }; + + 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; + description = '' + A file containing the secret used to encrypt variables 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 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 + copied into the world-readable nix store. + ''; + }; + + secrets.dbFile = mkOption { + type = with types; nullOr path; + default = null; + description = '' + A file containing the secret used to encrypt variables 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 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 + copied into the world-readable nix store. + ''; + }; + + secrets.otpFile = mkOption { + type = with types; nullOr path; + default = null; + description = '' + A file containing the secret used to encrypt secrets for OTP + 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 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 + copied into the world-readable nix store. + ''; + }; + + secrets.jwsFile = mkOption { + type = with types; nullOr path; + default = null; + description = '' + A file containing the secret used to encrypt session + keys. If you change or lose this key, users will be + disconnected. + + Make sure the secret is an RSA private key in PEM format. You can + generate one with + + openssl genrsa 2048 + + This should be a string, not a nix path, since nix paths are + copied into the world-readable nix store. + ''; + }; + + extraShellConfig = mkOption { + type = types.attrs; + default = {}; + 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. + ''; + }; + + logrotate = { + enable = mkOption { + type = types.bool; + default = true; + description = '' + Enable rotation of log files. + ''; + }; + + frequency = mkOption { + type = types.str; + default = "daily"; + description = "How often to rotate the logs."; + }; + + keep = mkOption { + type = types.int; + default = 30; + description = "How many rotations to keep."; + }; + + extraConfig = mkOption { + type = types.lines; + default = '' + copytruncate + compress + ''; + description = '' + Extra logrotate config options for this path. Refer to + <link xlink:href="https://linux.die.net/man/8/logrotate"/> for details. + ''; + }; + }; + + extraConfig = mkOption { + type = types.attrs; + default = {}; + example = literalExpression '' + { + gitlab = { + default_projects_features = { + builds = false; + }; + }; + omniauth = { + enabled = true; + auto_sign_in_with_provider = "openid_connect"; + allow_single_sign_on = ["openid_connect"]; + block_auto_created_users = false; + providers = [ + { + name = "openid_connect"; + label = "OpenID Connect"; + args = { + name = "openid_connect"; + scope = ["openid" "profile"]; + response_type = "code"; + issuer = "https://keycloak.example.com/auth/realms/My%20Realm"; + discovery = true; + client_auth_method = "query"; + uid_field = "preferred_username"; + client_options = { + identifier = "gitlab"; + secret = { _secret = "/var/keys/gitlab_oidc_secret"; }; + redirect_uri = "https://git.example.com/users/auth/openid_connect/callback"; + }; + }; + } + ]; + }; + }; + ''; + description = '' + Extra options to be added under + <literal>production</literal> in + <filename>config/gitlab.yml</filename>, as a nix attribute + set. + + Options containing secret data should be set to an attribute + set containing the attribute <literal>_secret</literal> - a + string pointing to a file containing the value the option + should be set to. See the example to get a better picture of + this: in the resulting + <filename>config/gitlab.yml</filename> file, the + <literal>production.omniauth.providers[0].args.client_options.secret</literal> + key will be set to the contents of the + <filename>/var/keys/gitlab_oidc_secret</filename> file. + ''; + }; + }; + }; + + config = mkIf cfg.enable { + + assertions = [ + { + assertion = databaseActuallyCreateLocally -> (cfg.user == cfg.databaseUsername); + 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.''; + } + { + assertion = (cfg.databaseHost != "") -> (cfg.databasePasswordFile != null); + message = "When services.gitlab.databaseHost is customized, services.gitlab.databasePasswordFile must be set!"; + } + { + assertion = cfg.initialRootPasswordFile != null; + message = "services.gitlab.initialRootPasswordFile must be set!"; + } + { + assertion = cfg.secrets.secretFile != null; + message = "services.gitlab.secrets.secretFile must be set!"; + } + { + assertion = cfg.secrets.dbFile != null; + message = "services.gitlab.secrets.dbFile must be set!"; + } + { + assertion = cfg.secrets.otpFile != null; + message = "services.gitlab.secrets.otpFile must be set!"; + } + { + 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.servers.gitlab = { + enable = mkDefault true; + port = mkDefault 31636; + bind = mkDefault "127.0.0.1"; + }; + + # We use postgres as the main data store. + services.postgresql = optionalAttrs databaseActuallyCreateLocally { + enable = true; + ensureUsers = singleton { name = cfg.databaseUsername; }; + }; + + # Enable rotation of log files + services.logrotate = { + enable = cfg.logrotate.enable; + paths = { + gitlab = { + path = "${cfg.statePath}/log/*.log"; + user = cfg.user; + group = cfg.group; + frequency = cfg.logrotate.frequency; + keep = cfg.logrotate.keep; + extraConfig = cfg.logrotate.extraConfig; + }; + }; + }; + + # The postgresql module doesn't currently support concepts like + # objects owners and extensions; for now we tack on what's needed + # here. + systemd.services.gitlab-postgresql = let pgsql = config.services.postgresql; in mkIf databaseActuallyCreateLocally { + after = [ "postgresql.service" ]; + bindsTo = [ "postgresql.service" ]; + wantedBy = [ "gitlab.target" ]; + partOf = [ "gitlab.target" ]; + path = [ + pgsql.package + pkgs.util-linux + ]; + script = '' + set -eu + + 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}'") + if [[ "$current_owner" != "${cfg.databaseUsername}" ]]; then + 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}\"" + 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 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 (cfg.smtp.enable && cfg.smtp.address == "localhost"); + + users.users.${cfg.user} = + { group = cfg.group; + home = "${cfg.statePath}/home"; + shell = "${pkgs.bash}/bin/bash"; + uid = config.ids.uids.gitlab; + }; + + users.groups.${cfg.group}.gid = config.ids.gids.gitlab; + + systemd.tmpfiles.rules = [ + "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.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}/db 0750 ${cfg.user} ${cfg.group} -" + "d ${cfg.statePath}/log 0750 ${cfg.user} ${cfg.group} -" + "d ${cfg.statePath}/repositories 2770 ${cfg.user} ${cfg.group} -" + "d ${cfg.statePath}/shell 0750 ${cfg.user} ${cfg.group} -" + "d ${cfg.statePath}/tmp 0750 ${cfg.user} ${cfg.group} -" + "d ${cfg.statePath}/tmp/pids 0750 ${cfg.user} ${cfg.group} -" + "d ${cfg.statePath}/tmp/sockets 0750 ${cfg.user} ${cfg.group} -" + "d ${cfg.statePath}/uploads 0700 ${cfg.user} ${cfg.group} -" + "d ${cfg.statePath}/custom_hooks 0700 ${cfg.user} ${cfg.group} -" + "d ${cfg.statePath}/custom_hooks/pre-receive.d 0700 ${cfg.user} ${cfg.group} -" + "d ${cfg.statePath}/custom_hooks/post-receive.d 0700 ${cfg.user} ${cfg.group} -" + "d ${cfg.statePath}/custom_hooks/update.d 0700 ${cfg.user} ${cfg.group} -" + "d ${gitlabConfig.production.shared.path} 0750 ${cfg.user} ${cfg.group} -" + "d ${gitlabConfig.production.shared.path}/artifacts 0750 ${cfg.user} ${cfg.group} -" + "d ${gitlabConfig.production.shared.path}/lfs-objects 0750 ${cfg.user} ${cfg.group} -" + "d ${gitlabConfig.production.shared.path}/packages 0750 ${cfg.user} ${cfg.group} -" + "d ${gitlabConfig.production.shared.path}/pages 0750 ${cfg.user} ${cfg.group} -" + "d ${gitlabConfig.production.shared.path}/terraform_state 0750 ${cfg.user} ${cfg.group} -" + "L+ /run/gitlab/config - - - - ${cfg.statePath}/config" + "L+ /run/gitlab/log - - - - ${cfg.statePath}/log" + "L+ /run/gitlab/tmp - - - - ${cfg.statePath}/tmp" + "L+ /run/gitlab/uploads - - - - ${cfg.statePath}/uploads" + + "L+ /run/gitlab/shell-config.yml - - - - ${pkgs.writeText "config.yml" (builtins.toJSON gitlabShellConfig)}" + ]; + + + 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 = '' + set -o errexit -o pipefail -o nounset + shopt -s dotglob nullglob inherit_errexit + + 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 -o errexit -o pipefail -o nounset + shopt -s inherit_errexit + + 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 '' + db_password="$(<'${cfg.databasePasswordFile}')" + export db_password + + 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' + + secret="$(<'${cfg.secrets.secretFile}')" + db="$(<'${cfg.secrets.dbFile}')" + otp="$(<'${cfg.secrets.otpFile}')" + jws="$(<'${cfg.secrets.jwsFile}')" + export secret db otp jws + 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 -o errexit -o pipefail -o nounset + shopt -s inherit_errexit + 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-gitlab.service" + "postgresql.service" + "gitlab-config.service" + "gitlab-db-config.service" + ]; + bindsTo = [ + "redis-gitlab.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 + git + ruby + openssh + nodejs + gnupg + + # Needed for GitLab project imports + gnutar + gzip + + procps # Sidekiq MemoryKiller + ]; + serviceConfig = { + Type = "simple"; + User = cfg.user; + Group = cfg.group; + TimeoutSec = "infinity"; + 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" "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 + git + cfg.packages.gitaly.rubyEnv + cfg.packages.gitaly.rubyEnv.wrappedRuby + gzip + bzip2 + ]; + serviceConfig = { + Type = "simple"; + User = cfg.user; + Group = cfg.group; + TimeoutSec = "infinity"; + Restart = "on-failure"; + WorkingDirectory = gitlabEnv.HOME; + ExecStart = "${cfg.packages.gitaly}/bin/gitaly ${gitalyToml}"; + }; + }; + + 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 = [ "gitlab.target" ]; + partOf = [ "gitlab.target" ]; + path = with pkgs; [ + exiftool + git + gnutar + gzip + openssh + gitlab-workhorse + ]; + serviceConfig = { + Type = "simple"; + User = cfg.user; + Group = cfg.group; + TimeoutSec = "infinity"; + Restart = "on-failure"; + WorkingDirectory = gitlabEnv.HOME; + ExecStart = + "${cfg.packages.gitlab-workhorse}/bin/workhorse " + + "-listenUmask 0 " + + "-listenNetwork unix " + + "-listenAddr /run/gitlab/gitlab-workhorse.socket " + + "-authSocket ${gitlabSocket} " + + "-documentRoot ${cfg.packages.gitlab}/share/gitlab/public " + + "-secretPath ${cfg.statePath}/.gitlab_workhorse_secret"; + }; + }; + + systemd.services.gitlab-mailroom = mkIf (gitlabConfig.production.incoming_email.enabled or false) { + description = "GitLab incoming mail daemon"; + after = [ "network.target" "redis-gitlab.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" + "network.target" + "redis-gitlab.service" + "gitlab-config.service" + "gitlab-db-config.service" + ]; + bindsTo = [ + "redis-gitlab.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 + git + openssh + nodejs + procps + gnupg + ]; + serviceConfig = { + Type = "notify"; + User = cfg.user; + Group = cfg.group; + TimeoutSec = "infinity"; + Restart = "on-failure"; + WorkingDirectory = "${cfg.packages.gitlab}/share/gitlab"; + 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}" + ]; + }; + + }; + + 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; + +} |