1{ config, lib, pkgs, ... }: 2 3# TODO: support non-postgresql 4 5with lib; 6 7let 8 cfg = config.services.gitlab; 9 10 ruby = cfg.packages.gitlab.ruby; 11 bundler = pkgs.bundler; 12 13 gemHome = "${cfg.packages.gitlab.env}/${ruby.gemPath}"; 14 15 gitlabSocket = "${cfg.statePath}/tmp/sockets/gitlab.socket"; 16 pathUrlQuote = url: replaceStrings ["/"] ["%2F"] url; 17 18 databaseYml = '' 19 production: 20 adapter: postgresql 21 database: ${cfg.databaseName} 22 host: ${cfg.databaseHost} 23 password: ${cfg.databasePassword} 24 username: ${cfg.databaseUsername} 25 encoding: utf8 26 ''; 27 28 gitlabShellYml = '' 29 user: ${cfg.user} 30 gitlab_url: "http+unix://${pathUrlQuote gitlabSocket}" 31 http_settings: 32 self_signed_cert: false 33 repos_path: "${cfg.statePath}/repositories" 34 secret_file: "${cfg.statePath}/config/gitlab_shell_secret" 35 log_file: "${cfg.statePath}/log/gitlab-shell.log" 36 redis: 37 bin: ${pkgs.redis}/bin/redis-cli 38 host: 127.0.0.1 39 port: 6379 40 database: 0 41 namespace: resque:gitlab 42 ''; 43 44 gitlabConfig = { 45 # These are the default settings from config/gitlab.example.yml 46 production = flip recursiveUpdate cfg.extraConfig { 47 gitlab = { 48 host = cfg.host; 49 port = cfg.port; 50 https = cfg.https; 51 user = cfg.user; 52 email_enabled = true; 53 email_display_name = "GitLab"; 54 email_reply_to = "noreply@localhost"; 55 default_theme = 2; 56 default_projects_features = { 57 issues = true; 58 merge_requests = true; 59 wiki = true; 60 snippets = false; 61 builds = true; 62 }; 63 }; 64 artifacts = { 65 enabled = true; 66 }; 67 lfs = { 68 enabled = true; 69 }; 70 gravatar = { 71 enabled = true; 72 }; 73 cron_jobs = { 74 stuck_ci_builds_worker = { 75 cron = "0 0 * * *"; 76 }; 77 }; 78 gitlab_ci = { 79 builds_path = "${cfg.statePath}/builds"; 80 }; 81 ldap = { 82 enabled = false; 83 }; 84 omniauth = { 85 enabled = false; 86 }; 87 shared = { 88 path = "${cfg.statePath}/shared"; 89 }; 90 backup = { 91 path = "${cfg.backupPath}"; 92 }; 93 gitlab_shell = { 94 path = "${cfg.packages.gitlab-shell}"; 95 repos_path = "${cfg.statePath}/repositories"; 96 hooks_path = "${cfg.statePath}/shell/hooks"; 97 secret_file = "${cfg.statePath}/config/gitlab_shell_secret"; 98 upload_pack = true; 99 receive_pack = true; 100 }; 101 git = { 102 bin_path = "git"; 103 max_size = 20971520; # 20MB 104 timeout = 10; 105 }; 106 extra = {}; 107 }; 108 }; 109 110 gitlabEnv = { 111 HOME = "${cfg.statePath}/home"; 112 GEM_HOME = gemHome; 113 BUNDLE_GEMFILE = "${cfg.packages.gitlab}/share/gitlab/Gemfile"; 114 UNICORN_PATH = "${cfg.statePath}/"; 115 GITLAB_PATH = "${cfg.packages.gitlab}/share/gitlab/"; 116 GITLAB_STATE_PATH = "${cfg.statePath}"; 117 GITLAB_UPLOADS_PATH = "${cfg.statePath}/uploads"; 118 GITLAB_LOG_PATH = "${cfg.statePath}/log"; 119 GITLAB_SHELL_PATH = "${cfg.packages.gitlab-shell}"; 120 GITLAB_SHELL_CONFIG_PATH = "${cfg.statePath}/shell/config.yml"; 121 GITLAB_SHELL_SECRET_PATH = "${cfg.statePath}/config/gitlab_shell_secret"; 122 GITLAB_SHELL_HOOKS_PATH = "${cfg.statePath}/shell/hooks"; 123 RAILS_ENV = "production"; 124 }; 125 126 unicornConfig = builtins.readFile ./defaultUnicornConfig.rb; 127 128 gitlab-runner = pkgs.stdenv.mkDerivation rec { 129 name = "gitlab-runner"; 130 buildInputs = [ cfg.packages.gitlab bundler pkgs.makeWrapper ]; 131 phases = "installPhase fixupPhase"; 132 buildPhase = ""; 133 installPhase = '' 134 mkdir -p $out/bin 135 makeWrapper ${bundler}/bin/bundle $out/bin/gitlab-runner \ 136 ${concatStrings (mapAttrsToList (name: value: "--set ${name} '\"${value}\"' ") gitlabEnv)} \ 137 --set GITLAB_CONFIG_PATH '"${cfg.statePath}/config"' \ 138 --set PATH '"${pkgs.nodejs}/bin:${pkgs.gzip}/bin:${config.services.postgresql.package}/bin:$PATH"' \ 139 --set RAKEOPT '"-f ${cfg.packages.gitlab}/share/gitlab/Rakefile"' 140 ''; 141 }; 142 143in { 144 145 options = { 146 services.gitlab = { 147 enable = mkOption { 148 type = types.bool; 149 default = false; 150 description = '' 151 Enable the gitlab service. 152 ''; 153 }; 154 155 packages.gitlab = mkOption { 156 type = types.package; 157 default = pkgs.gitlab; 158 description = "Reference to the gitlab package"; 159 }; 160 161 packages.gitlab-shell = mkOption { 162 type = types.package; 163 default = pkgs.gitlab-shell; 164 description = "Reference to the gitlab-shell package"; 165 }; 166 167 packages.gitlab-workhorse = mkOption { 168 type = types.package; 169 default = pkgs.gitlab-workhorse; 170 description = "Reference to the gitlab-workhorse package"; 171 }; 172 173 statePath = mkOption { 174 type = types.str; 175 default = "/var/gitlab/state"; 176 description = "Gitlab state directory, logs are stored here."; 177 }; 178 179 backupPath = mkOption { 180 type = types.str; 181 default = cfg.statePath + "/backup"; 182 description = "Gitlab path for backups."; 183 }; 184 185 databaseHost = mkOption { 186 type = types.str; 187 default = "127.0.0.1"; 188 description = "Gitlab database hostname."; 189 }; 190 191 databasePassword = mkOption { 192 type = types.str; 193 default = ""; 194 description = "Gitlab database user password."; 195 }; 196 197 databaseName = mkOption { 198 type = types.str; 199 default = "gitlab"; 200 description = "Gitlab database name."; 201 }; 202 203 databaseUsername = mkOption { 204 type = types.str; 205 default = "gitlab"; 206 description = "Gitlab database user."; 207 }; 208 209 emailFrom = mkOption { 210 type = types.str; 211 default = "example@example.org"; 212 description = "The source address for emails sent by gitlab."; 213 }; 214 215 host = mkOption { 216 type = types.str; 217 default = config.networking.hostName; 218 description = "Gitlab host name. Used e.g. for copy-paste URLs."; 219 }; 220 221 port = mkOption { 222 type = types.int; 223 default = 8080; 224 description = '' 225 Gitlab server port for copy-paste URLs, e.g. 80 or 443 if you're 226 service over https. 227 ''; 228 }; 229 230 https = mkOption { 231 type = types.bool; 232 default = false; 233 description = "Whether gitlab prints URLs with https as scheme."; 234 }; 235 236 user = mkOption { 237 type = types.str; 238 default = "gitlab"; 239 description = "User to run gitlab and all related services."; 240 }; 241 242 group = mkOption { 243 type = types.str; 244 default = "gitlab"; 245 description = "Group to run gitlab and all related services."; 246 }; 247 248 initialRootEmail = mkOption { 249 type = types.str; 250 default = "admin@local.host"; 251 description = '' 252 Initial email address of the root account if this is a new install. 253 ''; 254 }; 255 256 initialRootPassword = mkOption { 257 type = types.str; 258 default = "UseNixOS!"; 259 description = '' 260 Initial password of the root account if this is a new install. 261 ''; 262 }; 263 264 extraConfig = mkOption { 265 type = types.attrs; 266 default = {}; 267 example = { 268 gitlab = { 269 default_projects_features = { 270 builds = false; 271 }; 272 }; 273 }; 274 description = '' 275 Extra options to be merged into config/gitlab.yml as nix 276 attribute set. 277 ''; 278 }; 279 }; 280 }; 281 282 config = mkIf cfg.enable { 283 284 environment.systemPackages = [ pkgs.git gitlab-runner cfg.packages.gitlab-shell ]; 285 286 assertions = [ 287 { assertion = cfg.databasePassword != ""; 288 message = "databasePassword must be set"; 289 } 290 ]; 291 292 # Redis is required for the sidekiq queue runner. 293 services.redis.enable = mkDefault true; 294 # We use postgres as the main data store. 295 services.postgresql.enable = mkDefault true; 296 # Use postfix to send out mails. 297 services.postfix.enable = mkDefault true; 298 299 users.extraUsers = [ 300 { name = cfg.user; 301 group = cfg.group; 302 home = "${cfg.statePath}/home"; 303 shell = "${pkgs.bash}/bin/bash"; 304 uid = config.ids.uids.gitlab; 305 } 306 ]; 307 308 users.extraGroups = [ 309 { name = cfg.group; 310 gid = config.ids.gids.gitlab; 311 } 312 ]; 313 314 systemd.services.gitlab-sidekiq = { 315 after = [ "network.target" "redis.service" ]; 316 wantedBy = [ "multi-user.target" ]; 317 environment = gitlabEnv; 318 path = with pkgs; [ 319 config.services.postgresql.package 320 gitAndTools.git 321 ruby 322 openssh 323 nodejs 324 ]; 325 serviceConfig = { 326 Type = "simple"; 327 User = cfg.user; 328 Group = cfg.group; 329 TimeoutSec = "300"; 330 WorkingDirectory = "${cfg.packages.gitlab}/share/gitlab"; 331 ExecStart="${bundler}/bin/bundle exec \"sidekiq -q post_receive -q mailer -q system_hook -q project_web_hook -q gitlab_shell -q common -q default -e production -P ${cfg.statePath}/tmp/sidekiq.pid\""; 332 }; 333 }; 334 335 systemd.services.gitlab-workhorse = { 336 after = [ "network.target" "gitlab.service" ]; 337 wantedBy = [ "multi-user.target" ]; 338 environment.HOME = gitlabEnv.HOME; 339 environment.GITLAB_SHELL_CONFIG_PATH = gitlabEnv.GITLAB_SHELL_CONFIG_PATH; 340 path = with pkgs; [ 341 gitAndTools.git 342 openssh 343 ]; 344 preStart = '' 345 mkdir -p /run/gitlab 346 chown ${cfg.user}:${cfg.group} /run/gitlab 347 ''; 348 serviceConfig = { 349 PermissionsStartOnly = true; # preStart must be run as root 350 Type = "simple"; 351 User = cfg.user; 352 Group = cfg.group; 353 TimeoutSec = "300"; 354 ExecStart = 355 "${cfg.packages.gitlab-workhorse}/bin/gitlab-workhorse " 356 + "-listenUmask 0 " 357 + "-listenNetwork unix " 358 + "-listenAddr /run/gitlab/gitlab-workhorse.socket " 359 + "-authSocket ${gitlabSocket} " 360 + "-documentRoot ${cfg.packages.gitlab}/share/gitlab/public"; 361 }; 362 }; 363 364 systemd.services.gitlab = { 365 after = [ "network.target" "postgresql.service" "redis.service" ]; 366 wantedBy = [ "multi-user.target" ]; 367 environment = gitlabEnv; 368 path = with pkgs; [ 369 config.services.postgresql.package 370 gitAndTools.git 371 openssh 372 nodejs 373 ]; 374 preStart = '' 375 mkdir -p ${cfg.backupPath} 376 mkdir -p ${cfg.statePath}/builds 377 mkdir -p ${cfg.statePath}/repositories 378 mkdir -p ${gitlabConfig.production.shared.path}/artifacts 379 mkdir -p ${gitlabConfig.production.shared.path}/lfs-objects 380 mkdir -p ${cfg.statePath}/log 381 mkdir -p ${cfg.statePath}/shell 382 mkdir -p ${cfg.statePath}/tmp/pids 383 mkdir -p ${cfg.statePath}/tmp/sockets 384 385 rm -rf ${cfg.statePath}/config ${cfg.statePath}/shell/hooks 386 mkdir -p ${cfg.statePath}/config ${cfg.statePath}/shell 387 388 # TODO: What exactly is gitlab-shell doing with the secret? 389 tr -dc _A-Z-a-z-0-9 < /dev/urandom | head -c 20 > ${cfg.statePath}/config/gitlab_shell_secret 390 391 # The uploads directory is hardcoded somewhere deep in rails. It is 392 # symlinked in the gitlab package to /run/gitlab/uploads to make it 393 # configurable 394 mkdir -p /run/gitlab 395 mkdir -p ${cfg.statePath}/uploads 396 ln -sf ${cfg.statePath}/uploads /run/gitlab/uploads 397 chown -R ${cfg.user}:${cfg.group} /run/gitlab 398 399 # Prepare home directory 400 mkdir -p ${gitlabEnv.HOME}/.ssh 401 touch ${gitlabEnv.HOME}/.ssh/authorized_keys 402 chown -R ${cfg.user}:${cfg.group} ${gitlabEnv.HOME}/ 403 chmod -R u+rwX,go-rwx+X ${gitlabEnv.HOME}/ 404 405 cp -rf ${cfg.packages.gitlab}/share/gitlab/config.dist/* ${cfg.statePath}/config 406 ln -sf ${cfg.statePath}/config /run/gitlab/config 407 cp ${cfg.packages.gitlab}/share/gitlab/VERSION ${cfg.statePath}/VERSION 408 409 # JSON is a subset of YAML 410 ln -fs ${pkgs.writeText "gitlab.yml" (builtins.toJSON gitlabConfig)} ${cfg.statePath}/config/gitlab.yml 411 ln -fs ${pkgs.writeText "database.yml" databaseYml} ${cfg.statePath}/config/database.yml 412 ln -fs ${pkgs.writeText "unicorn.rb" unicornConfig} ${cfg.statePath}/config/unicorn.rb 413 414 chown -R ${cfg.user}:${cfg.group} ${cfg.statePath}/ 415 chmod -R ug+rwX,o-rwx+X ${cfg.statePath}/ 416 417 # Install the shell required to push repositories 418 ln -fs ${pkgs.writeText "config.yml" gitlabShellYml} "$GITLAB_SHELL_CONFIG_PATH" 419 ln -fs ${cfg.packages.gitlab-shell}/hooks "$GITLAB_SHELL_HOOKS_PATH" 420 ${cfg.packages.gitlab-shell}/bin/install 421 422 if [ "${cfg.databaseHost}" = "127.0.0.1" ]; then 423 if ! test -e "${cfg.statePath}/db-created"; then 424 psql postgres -c "CREATE ROLE gitlab WITH LOGIN NOCREATEDB NOCREATEROLE NOCREATEUSER ENCRYPTED PASSWORD '${cfg.databasePassword}'" 425 ${config.services.postgresql.package}/bin/createdb --owner gitlab gitlab || true 426 touch "${cfg.statePath}/db-created" 427 428 # The gitlab:setup task is horribly broken somehow, these two tasks will do the same for setting up the initial database 429 ${gitlab-runner}/bin/gitlab-runner exec rake db:migrate RAILS_ENV=production 430 ${gitlab-runner}/bin/gitlab-runner exec rake db:seed_fu RAILS_ENV=production \ 431 GITLAB_ROOT_PASSWORD="${cfg.initialRootPassword}" GITLAB_ROOT_EMAIL="${cfg.initialRootEmail}"; 432 fi 433 fi 434 435 # Always do the db migrations just to be sure the database is up-to-date 436 ${gitlab-runner}/bin/gitlab-runner exec rake db:migrate RAILS_ENV=production 437 438 # Change permissions in the last step because some of the 439 # intermediary scripts like to create directories as root. 440 chown -R ${cfg.user}:${cfg.group} ${cfg.statePath} 441 chmod -R u+rwX,go-rwx+X ${cfg.statePath} 442 ''; 443 444 serviceConfig = { 445 PermissionsStartOnly = true; # preStart must be run as root 446 Type = "simple"; 447 User = cfg.user; 448 Group = cfg.group; 449 TimeoutSec = "300"; 450 WorkingDirectory = "${cfg.packages.gitlab}/share/gitlab"; 451 ExecStart="${bundler}/bin/bundle exec \"unicorn -c ${cfg.statePath}/config/unicorn.rb -E production\""; 452 }; 453 454 }; 455 456 }; 457}