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 secretsYml = ''
45 production:
46 secret_key_base: ${cfg.secrets.secret}
47 otp_key_base: ${cfg.secrets.otp}
48 db_key_base: ${cfg.secrets.db}
49 '';
50
51 gitlabConfig = {
52 # These are the default settings from config/gitlab.example.yml
53 production = flip recursiveUpdate cfg.extraConfig {
54 gitlab = {
55 host = cfg.host;
56 port = cfg.port;
57 https = cfg.https;
58 user = cfg.user;
59 email_enabled = true;
60 email_display_name = "GitLab";
61 email_reply_to = "noreply@localhost";
62 default_theme = 2;
63 default_projects_features = {
64 issues = true;
65 merge_requests = true;
66 wiki = true;
67 snippets = true;
68 builds = true;
69 container_registry = true;
70 };
71 };
72 repositories.storages.default = "${cfg.statePath}/repositories";
73 artifacts.enabled = true;
74 lfs.enabled = true;
75 gravatar.enabled = true;
76 cron_jobs = { };
77 gitlab_ci.builds_path = "${cfg.statePath}/builds";
78 ldap.enabled = false;
79 omniauth.enabled = false;
80 shared.path = "${cfg.statePath}/shared";
81 backup.path = "${cfg.backupPath}";
82 gitlab_shell = {
83 path = "${cfg.packages.gitlab-shell}";
84 hooks_path = "${cfg.statePath}/shell/hooks";
85 secret_file = "${cfg.statePath}/config/gitlab_shell_secret";
86 upload_pack = true;
87 receive_pack = true;
88 };
89 git = {
90 bin_path = "git";
91 max_size = 20971520; # 20MB
92 timeout = 10;
93 };
94 extra = {};
95 };
96 };
97
98 gitlabEnv = {
99 HOME = "${cfg.statePath}/home";
100 GEM_HOME = gemHome;
101 BUNDLE_GEMFILE = "${cfg.packages.gitlab}/share/gitlab/Gemfile";
102 UNICORN_PATH = "${cfg.statePath}/";
103 GITLAB_PATH = "${cfg.packages.gitlab}/share/gitlab/";
104 GITLAB_STATE_PATH = "${cfg.statePath}";
105 GITLAB_UPLOADS_PATH = "${cfg.statePath}/uploads";
106 GITLAB_LOG_PATH = "${cfg.statePath}/log";
107 GITLAB_SHELL_PATH = "${cfg.packages.gitlab-shell}";
108 GITLAB_SHELL_CONFIG_PATH = "${cfg.statePath}/shell/config.yml";
109 GITLAB_SHELL_SECRET_PATH = "${cfg.statePath}/config/gitlab_shell_secret";
110 GITLAB_SHELL_HOOKS_PATH = "${cfg.statePath}/shell/hooks";
111 RAILS_ENV = "production";
112 };
113
114 unicornConfig = builtins.readFile ./defaultUnicornConfig.rb;
115
116 gitlab-rake = pkgs.stdenv.mkDerivation rec {
117 name = "gitlab-rake";
118 buildInputs = [ cfg.packages.gitlab cfg.packages.gitlab.env pkgs.makeWrapper ];
119 phases = "installPhase fixupPhase";
120 buildPhase = "";
121 installPhase = ''
122 mkdir -p $out/bin
123 makeWrapper ${cfg.packages.gitlab.env}/bin/bundle $out/bin/gitlab-bundle \
124 ${concatStrings (mapAttrsToList (name: value: "--set ${name} '${value}' ") gitlabEnv)} \
125 --set GITLAB_CONFIG_PATH '${cfg.statePath}/config' \
126 --set PATH '${lib.makeBinPath [ pkgs.nodejs pkgs.gzip config.services.postgresql.package ]}:$PATH' \
127 --set RAKEOPT '-f ${cfg.packages.gitlab}/share/gitlab/Rakefile' \
128 --run 'cd ${cfg.packages.gitlab}/share/gitlab'
129 makeWrapper $out/bin/gitlab-bundle $out/bin/gitlab-rake \
130 --add-flags "exec rake"
131 '';
132 };
133
134 smtpSettings = pkgs.writeText "gitlab-smtp-settings.rb" ''
135 if Rails.env.production?
136 Rails.application.config.action_mailer.delivery_method = :smtp
137
138 ActionMailer::Base.delivery_method = :smtp
139 ActionMailer::Base.smtp_settings = {
140 address: "${cfg.smtp.address}",
141 port: ${toString cfg.smtp.port},
142 ${optionalString (cfg.smtp.username != null) ''user_name: "${cfg.smtp.username}",''}
143 ${optionalString (cfg.smtp.password != null) ''password: "${cfg.smtp.password}",''}
144 domain: "${cfg.smtp.domain}",
145 ${optionalString (cfg.smtp.authentication != null) "authentication: :${cfg.smtp.authentication},"}
146 enable_starttls_auto: ${toString cfg.smtp.enableStartTLSAuto},
147 openssl_verify_mode: '${cfg.smtp.opensslVerifyMode}'
148 }
149 end
150 '';
151
152in {
153
154 options = {
155 services.gitlab = {
156 enable = mkOption {
157 type = types.bool;
158 default = false;
159 description = ''
160 Enable the gitlab service.
161 '';
162 };
163
164 packages.gitlab = mkOption {
165 type = types.package;
166 default = pkgs.gitlab;
167 description = "Reference to the gitlab package";
168 };
169
170 packages.gitlab-shell = mkOption {
171 type = types.package;
172 default = pkgs.gitlab-shell;
173 description = "Reference to the gitlab-shell package";
174 };
175
176 packages.gitlab-workhorse = mkOption {
177 type = types.package;
178 default = pkgs.gitlab-workhorse;
179 description = "Reference to the gitlab-workhorse package";
180 };
181
182 statePath = mkOption {
183 type = types.str;
184 default = "/var/gitlab/state";
185 description = "Gitlab state directory, logs are stored here.";
186 };
187
188 backupPath = mkOption {
189 type = types.str;
190 default = cfg.statePath + "/backup";
191 description = "Gitlab path for backups.";
192 };
193
194 databaseHost = mkOption {
195 type = types.str;
196 default = "127.0.0.1";
197 description = "Gitlab database hostname.";
198 };
199
200 databasePassword = mkOption {
201 type = types.str;
202 default = "";
203 description = "Gitlab database user password.";
204 };
205
206 databaseName = mkOption {
207 type = types.str;
208 default = "gitlab";
209 description = "Gitlab database name.";
210 };
211
212 databaseUsername = mkOption {
213 type = types.str;
214 default = "gitlab";
215 description = "Gitlab database user.";
216 };
217
218 host = mkOption {
219 type = types.str;
220 default = config.networking.hostName;
221 description = "Gitlab host name. Used e.g. for copy-paste URLs.";
222 };
223
224 port = mkOption {
225 type = types.int;
226 default = 8080;
227 description = ''
228 Gitlab server port for copy-paste URLs, e.g. 80 or 443 if you're
229 service over https.
230 '';
231 };
232
233 https = mkOption {
234 type = types.bool;
235 default = false;
236 description = "Whether gitlab prints URLs with https as scheme.";
237 };
238
239 user = mkOption {
240 type = types.str;
241 default = "gitlab";
242 description = "User to run gitlab and all related services.";
243 };
244
245 group = mkOption {
246 type = types.str;
247 default = "gitlab";
248 description = "Group to run gitlab and all related services.";
249 };
250
251 initialRootEmail = mkOption {
252 type = types.str;
253 default = "admin@local.host";
254 description = ''
255 Initial email address of the root account if this is a new install.
256 '';
257 };
258
259 initialRootPassword = mkOption {
260 type = types.str;
261 default = "UseNixOS!";
262 description = ''
263 Initial password of the root account if this is a new install.
264 '';
265 };
266
267 smtp = {
268 enable = mkOption {
269 type = types.bool;
270 default = false;
271 description = "Enable gitlab mail delivery over SMTP.";
272 };
273
274 address = mkOption {
275 type = types.str;
276 default = "localhost";
277 description = "Address of the SMTP server for Gitlab.";
278 };
279
280 port = mkOption {
281 type = types.int;
282 default = 465;
283 description = "Port of the SMTP server for Gitlab.";
284 };
285
286 username = mkOption {
287 type = types.nullOr types.str;
288 default = null;
289 description = "Username of the SMTP server for Gitlab.";
290 };
291
292 password = mkOption {
293 type = types.nullOr types.str;
294 default = null;
295 description = "Password of the SMTP server for Gitlab.";
296 };
297
298 domain = mkOption {
299 type = types.str;
300 default = "localhost";
301 description = "HELO domain to use for outgoing mail.";
302 };
303
304 authentication = mkOption {
305 type = types.nullOr types.str;
306 default = null;
307 description = "Authentitcation type to use, see http://api.rubyonrails.org/classes/ActionMailer/Base.html";
308 };
309
310 enableStartTLSAuto = mkOption {
311 type = types.bool;
312 default = true;
313 description = "Whether to try to use StartTLS.";
314 };
315
316 opensslVerifyMode = mkOption {
317 type = types.str;
318 default = "peer";
319 description = "How OpenSSL checks the certificate, see http://api.rubyonrails.org/classes/ActionMailer/Base.html";
320 };
321 };
322
323 secrets.secret = mkOption {
324 type = types.str;
325 description = ''
326 The secret is used to encrypt variables in the DB. If
327 you change or lose this key you will be unable to access variables
328 stored in database.
329
330 Make sure the secret is at least 30 characters and all random,
331 no regular words or you'll be exposed to dictionary attacks.
332 '';
333 };
334
335 secrets.db = mkOption {
336 type = types.str;
337 description = ''
338 The secret is used to encrypt variables in the DB. If
339 you change or lose this key you will be unable to access variables
340 stored in database.
341
342 Make sure the secret is at least 30 characters and all random,
343 no regular words or you'll be exposed to dictionary attacks.
344 '';
345 };
346
347 secrets.otp = mkOption {
348 type = types.str;
349 description = ''
350 The secret is used to encrypt secrets for OTP tokens. If
351 you change or lose this key, users which have 2FA enabled for login
352 won't be able to login anymore.
353
354 Make sure the secret is at least 30 characters and all random,
355 no regular words or you'll be exposed to dictionary attacks.
356 '';
357 };
358
359 extraConfig = mkOption {
360 type = types.attrs;
361 default = {};
362 example = {
363 gitlab = {
364 default_projects_features = {
365 builds = false;
366 };
367 };
368 };
369 description = ''
370 Extra options to be merged into config/gitlab.yml as nix
371 attribute set.
372 '';
373 };
374 };
375 };
376
377 config = mkIf cfg.enable {
378
379 environment.systemPackages = [ pkgs.git gitlab-rake cfg.packages.gitlab-shell ];
380
381 assertions = [
382 { assertion = cfg.databasePassword != "";
383 message = "databasePassword must be set";
384 }
385 ];
386
387 # Redis is required for the sidekiq queue runner.
388 services.redis.enable = mkDefault true;
389 # We use postgres as the main data store.
390 services.postgresql.enable = mkDefault true;
391 # Use postfix to send out mails.
392 services.postfix.enable = mkDefault true;
393
394 users.extraUsers = [
395 { name = cfg.user;
396 group = cfg.group;
397 home = "${cfg.statePath}/home";
398 shell = "${pkgs.bash}/bin/bash";
399 uid = config.ids.uids.gitlab;
400 }
401 ];
402
403 users.extraGroups = [
404 { name = cfg.group;
405 gid = config.ids.gids.gitlab;
406 }
407 ];
408
409 systemd.services.gitlab-sidekiq = {
410 after = [ "network.target" "redis.service" ];
411 wantedBy = [ "multi-user.target" ];
412 partOf = [ "gitlab.service" ];
413 environment = gitlabEnv;
414 path = with pkgs; [
415 config.services.postgresql.package
416 gitAndTools.git
417 ruby
418 openssh
419 nodejs
420 ];
421 serviceConfig = {
422 Type = "simple";
423 User = cfg.user;
424 Group = cfg.group;
425 TimeoutSec = "300";
426 Restart = "on-failure";
427 WorkingDirectory = "${cfg.packages.gitlab}/share/gitlab";
428 ExecStart="${cfg.packages.gitlab.env}/bin/bundle exec \"sidekiq -q post_receive -q mailers -q system_hook -q project_web_hook -q gitlab_shell -q common -q default -e production -P ${cfg.statePath}/tmp/sidekiq.pid\"";
429 };
430 };
431
432 systemd.services.gitlab-workhorse = {
433 after = [ "network.target" "gitlab.service" ];
434 wantedBy = [ "multi-user.target" ];
435 environment.HOME = gitlabEnv.HOME;
436 environment.GITLAB_SHELL_CONFIG_PATH = gitlabEnv.GITLAB_SHELL_CONFIG_PATH;
437 path = with pkgs; [
438 gitAndTools.git
439 openssh
440 ];
441 preStart = ''
442 mkdir -p /run/gitlab
443 chown ${cfg.user}:${cfg.group} /run/gitlab
444 '';
445 serviceConfig = {
446 PermissionsStartOnly = true; # preStart must be run as root
447 Type = "simple";
448 User = cfg.user;
449 Group = cfg.group;
450 TimeoutSec = "300";
451 Restart = "on-failure";
452 ExecStart =
453 "${cfg.packages.gitlab-workhorse}/bin/gitlab-workhorse "
454 + "-listenUmask 0 "
455 + "-listenNetwork unix "
456 + "-listenAddr /run/gitlab/gitlab-workhorse.socket "
457 + "-authSocket ${gitlabSocket} "
458 + "-documentRoot ${cfg.packages.gitlab}/share/gitlab/public";
459 };
460 };
461
462 systemd.services.gitlab = {
463 after = [ "network.target" "postgresql.service" "redis.service" ];
464 wantedBy = [ "multi-user.target" ];
465 environment = gitlabEnv;
466 path = with pkgs; [
467 config.services.postgresql.package
468 gitAndTools.git
469 openssh
470 nodejs
471 ];
472 preStart = ''
473 mkdir -p ${cfg.backupPath}
474 mkdir -p ${cfg.statePath}/builds
475 mkdir -p ${cfg.statePath}/repositories
476 mkdir -p ${gitlabConfig.production.shared.path}/artifacts
477 mkdir -p ${gitlabConfig.production.shared.path}/lfs-objects
478 mkdir -p ${cfg.statePath}/log
479 mkdir -p ${cfg.statePath}/shell
480 mkdir -p ${cfg.statePath}/tmp/pids
481 mkdir -p ${cfg.statePath}/tmp/sockets
482
483 rm -rf ${cfg.statePath}/config ${cfg.statePath}/shell/hooks
484 mkdir -p ${cfg.statePath}/config ${cfg.statePath}/shell
485
486 tr -dc A-Za-z0-9 < /dev/urandom | head -c 32 > ${cfg.statePath}/config/gitlab_shell_secret
487
488 # The uploads directory is hardcoded somewhere deep in rails. It is
489 # symlinked in the gitlab package to /run/gitlab/uploads to make it
490 # configurable
491 mkdir -p /run/gitlab
492 mkdir -p ${cfg.statePath}/uploads
493 ln -sf ${cfg.statePath}/uploads /run/gitlab/uploads
494 chown -R ${cfg.user}:${cfg.group} /run/gitlab
495
496 # Prepare home directory
497 mkdir -p ${gitlabEnv.HOME}/.ssh
498 touch ${gitlabEnv.HOME}/.ssh/authorized_keys
499 chown -R ${cfg.user}:${cfg.group} ${gitlabEnv.HOME}/
500 chmod -R u+rwX,go-rwx+X ${gitlabEnv.HOME}/
501
502 cp -rf ${cfg.packages.gitlab}/share/gitlab/config.dist/* ${cfg.statePath}/config
503 ${optionalString cfg.smtp.enable ''
504 ln -sf ${smtpSettings} ${cfg.statePath}/config/initializers/smtp_settings.rb
505 ''}
506 ln -sf ${cfg.statePath}/config /run/gitlab/config
507 cp ${cfg.packages.gitlab}/share/gitlab/VERSION ${cfg.statePath}/VERSION
508
509 # JSON is a subset of YAML
510 ln -fs ${pkgs.writeText "gitlab.yml" (builtins.toJSON gitlabConfig)} ${cfg.statePath}/config/gitlab.yml
511 ln -fs ${pkgs.writeText "database.yml" databaseYml} ${cfg.statePath}/config/database.yml
512 ln -fs ${pkgs.writeText "secrets.yml" secretsYml} ${cfg.statePath}/config/secrets.yml
513 ln -fs ${pkgs.writeText "unicorn.rb" unicornConfig} ${cfg.statePath}/config/unicorn.rb
514
515 chown -R ${cfg.user}:${cfg.group} ${cfg.statePath}/
516 chmod -R ug+rwX,o-rwx+X ${cfg.statePath}/
517
518 # Install the shell required to push repositories
519 ln -fs ${pkgs.writeText "config.yml" gitlabShellYml} "$GITLAB_SHELL_CONFIG_PATH"
520 ln -fs ${cfg.packages.gitlab-shell}/hooks "$GITLAB_SHELL_HOOKS_PATH"
521 ${cfg.packages.gitlab-shell}/bin/install
522
523 if [ "${cfg.databaseHost}" = "127.0.0.1" ]; then
524 if ! test -e "${cfg.statePath}/db-created"; then
525 psql postgres -c "CREATE ROLE gitlab WITH LOGIN NOCREATEDB NOCREATEROLE NOCREATEUSER ENCRYPTED PASSWORD '${cfg.databasePassword}'"
526 ${config.services.postgresql.package}/bin/createdb --owner gitlab gitlab || true
527 touch "${cfg.statePath}/db-created"
528
529 # The gitlab:setup task is horribly broken somehow, these two tasks will do the same for setting up the initial database
530 ${gitlab-rake}/bin/gitlab-rake db:migrate RAILS_ENV=production
531 ${gitlab-rake}/bin/gitlab-rake db:seed_fu RAILS_ENV=production \
532 GITLAB_ROOT_PASSWORD="${cfg.initialRootPassword}" GITLAB_ROOT_EMAIL="${cfg.initialRootEmail}";
533 fi
534 fi
535
536 # Always do the db migrations just to be sure the database is up-to-date
537 ${gitlab-rake}/bin/gitlab-rake db:migrate RAILS_ENV=production
538
539 # Change permissions in the last step because some of the
540 # intermediary scripts like to create directories as root.
541 chown -R ${cfg.user}:${cfg.group} ${cfg.statePath}
542 chmod -R u+rwX,go-rwx+X ${cfg.statePath}
543 '';
544
545 serviceConfig = {
546 PermissionsStartOnly = true; # preStart must be run as root
547 Type = "simple";
548 User = cfg.user;
549 Group = cfg.group;
550 TimeoutSec = "300";
551 Restart = "on-failure";
552 WorkingDirectory = "${cfg.packages.gitlab}/share/gitlab";
553 ExecStart = "${cfg.packages.gitlab.env}/bin/bundle exec \"unicorn -c ${cfg.statePath}/config/unicorn.rb -E production\"";
554 };
555
556 };
557
558 };
559
560 meta.doc = ./gitlab.xml;
561
562}