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 defaultText = "pkgs.gitlab";
168 description = "Reference to the gitlab package";
169 };
170
171 packages.gitlab-shell = mkOption {
172 type = types.package;
173 default = pkgs.gitlab-shell;
174 defaultText = "pkgs.gitlab-shell";
175 description = "Reference to the gitlab-shell package";
176 };
177
178 packages.gitlab-workhorse = mkOption {
179 type = types.package;
180 default = pkgs.gitlab-workhorse;
181 defaultText = "pkgs.gitlab-workhorse";
182 description = "Reference to the gitlab-workhorse package";
183 };
184
185 statePath = mkOption {
186 type = types.str;
187 default = "/var/gitlab/state";
188 description = "Gitlab state directory, logs are stored here.";
189 };
190
191 backupPath = mkOption {
192 type = types.str;
193 default = cfg.statePath + "/backup";
194 description = "Gitlab path for backups.";
195 };
196
197 databaseHost = mkOption {
198 type = types.str;
199 default = "127.0.0.1";
200 description = "Gitlab database hostname.";
201 };
202
203 databasePassword = mkOption {
204 type = types.str;
205 default = "";
206 description = "Gitlab database user password.";
207 };
208
209 databaseName = mkOption {
210 type = types.str;
211 default = "gitlab";
212 description = "Gitlab database name.";
213 };
214
215 databaseUsername = mkOption {
216 type = types.str;
217 default = "gitlab";
218 description = "Gitlab database user.";
219 };
220
221 host = mkOption {
222 type = types.str;
223 default = config.networking.hostName;
224 description = "Gitlab host name. Used e.g. for copy-paste URLs.";
225 };
226
227 port = mkOption {
228 type = types.int;
229 default = 8080;
230 description = ''
231 Gitlab server port for copy-paste URLs, e.g. 80 or 443 if you're
232 service over https.
233 '';
234 };
235
236 https = mkOption {
237 type = types.bool;
238 default = false;
239 description = "Whether gitlab prints URLs with https as scheme.";
240 };
241
242 user = mkOption {
243 type = types.str;
244 default = "gitlab";
245 description = "User to run gitlab and all related services.";
246 };
247
248 group = mkOption {
249 type = types.str;
250 default = "gitlab";
251 description = "Group to run gitlab and all related services.";
252 };
253
254 initialRootEmail = mkOption {
255 type = types.str;
256 default = "admin@local.host";
257 description = ''
258 Initial email address of the root account if this is a new install.
259 '';
260 };
261
262 initialRootPassword = mkOption {
263 type = types.str;
264 default = "UseNixOS!";
265 description = ''
266 Initial password of the root account if this is a new install.
267 '';
268 };
269
270 smtp = {
271 enable = mkOption {
272 type = types.bool;
273 default = false;
274 description = "Enable gitlab mail delivery over SMTP.";
275 };
276
277 address = mkOption {
278 type = types.str;
279 default = "localhost";
280 description = "Address of the SMTP server for Gitlab.";
281 };
282
283 port = mkOption {
284 type = types.int;
285 default = 465;
286 description = "Port of the SMTP server for Gitlab.";
287 };
288
289 username = mkOption {
290 type = types.nullOr types.str;
291 default = null;
292 description = "Username of the SMTP server for Gitlab.";
293 };
294
295 password = mkOption {
296 type = types.nullOr types.str;
297 default = null;
298 description = "Password of the SMTP server for Gitlab.";
299 };
300
301 domain = mkOption {
302 type = types.str;
303 default = "localhost";
304 description = "HELO domain to use for outgoing mail.";
305 };
306
307 authentication = mkOption {
308 type = types.nullOr types.str;
309 default = null;
310 description = "Authentitcation type to use, see http://api.rubyonrails.org/classes/ActionMailer/Base.html";
311 };
312
313 enableStartTLSAuto = mkOption {
314 type = types.bool;
315 default = true;
316 description = "Whether to try to use StartTLS.";
317 };
318
319 opensslVerifyMode = mkOption {
320 type = types.str;
321 default = "peer";
322 description = "How OpenSSL checks the certificate, see http://api.rubyonrails.org/classes/ActionMailer/Base.html";
323 };
324 };
325
326 secrets.secret = mkOption {
327 type = types.str;
328 description = ''
329 The secret is used to encrypt variables in the DB. If
330 you change or lose this key you will be unable to access variables
331 stored in database.
332
333 Make sure the secret is at least 30 characters and all random,
334 no regular words or you'll be exposed to dictionary attacks.
335 '';
336 };
337
338 secrets.db = mkOption {
339 type = types.str;
340 description = ''
341 The secret is used to encrypt variables in the DB. If
342 you change or lose this key you will be unable to access variables
343 stored in database.
344
345 Make sure the secret is at least 30 characters and all random,
346 no regular words or you'll be exposed to dictionary attacks.
347 '';
348 };
349
350 secrets.otp = mkOption {
351 type = types.str;
352 description = ''
353 The secret is used to encrypt secrets for OTP tokens. If
354 you change or lose this key, users which have 2FA enabled for login
355 won't be able to login anymore.
356
357 Make sure the secret is at least 30 characters and all random,
358 no regular words or you'll be exposed to dictionary attacks.
359 '';
360 };
361
362 extraConfig = mkOption {
363 type = types.attrs;
364 default = {};
365 example = {
366 gitlab = {
367 default_projects_features = {
368 builds = false;
369 };
370 };
371 };
372 description = ''
373 Extra options to be merged into config/gitlab.yml as nix
374 attribute set.
375 '';
376 };
377 };
378 };
379
380 config = mkIf cfg.enable {
381
382 environment.systemPackages = [ pkgs.git gitlab-rake cfg.packages.gitlab-shell ];
383
384 assertions = [
385 { assertion = cfg.databasePassword != "";
386 message = "databasePassword must be set";
387 }
388 ];
389
390 # Redis is required for the sidekiq queue runner.
391 services.redis.enable = mkDefault true;
392 # We use postgres as the main data store.
393 services.postgresql.enable = mkDefault true;
394 # Use postfix to send out mails.
395 services.postfix.enable = mkDefault true;
396
397 users.extraUsers = [
398 { name = cfg.user;
399 group = cfg.group;
400 home = "${cfg.statePath}/home";
401 shell = "${pkgs.bash}/bin/bash";
402 uid = config.ids.uids.gitlab;
403 }
404 ];
405
406 users.extraGroups = [
407 { name = cfg.group;
408 gid = config.ids.gids.gitlab;
409 }
410 ];
411
412 systemd.services.gitlab-sidekiq = {
413 after = [ "network.target" "redis.service" ];
414 wantedBy = [ "multi-user.target" ];
415 partOf = [ "gitlab.service" ];
416 environment = gitlabEnv;
417 path = with pkgs; [
418 config.services.postgresql.package
419 gitAndTools.git
420 ruby
421 openssh
422 nodejs
423 ];
424 serviceConfig = {
425 Type = "simple";
426 User = cfg.user;
427 Group = cfg.group;
428 TimeoutSec = "300";
429 Restart = "on-failure";
430 WorkingDirectory = "${cfg.packages.gitlab}/share/gitlab";
431 ExecStart="${cfg.packages.gitlab.env}/bin/bundle exec \"sidekiq -C \"${cfg.packages.gitlab}/share/gitlab/config/sidekiq_queues.yml\" -e production -P ${cfg.statePath}/tmp/sidekiq.pid\"";
432 };
433 };
434
435 systemd.services.gitlab-workhorse = {
436 after = [ "network.target" "gitlab.service" ];
437 wantedBy = [ "multi-user.target" ];
438 environment.HOME = gitlabEnv.HOME;
439 environment.GITLAB_SHELL_CONFIG_PATH = gitlabEnv.GITLAB_SHELL_CONFIG_PATH;
440 path = with pkgs; [
441 gitAndTools.git
442 gnutar
443 gzip
444 openssh
445 gitlab-workhorse
446 ];
447 preStart = ''
448 mkdir -p /run/gitlab
449 chown ${cfg.user}:${cfg.group} /run/gitlab
450 '';
451 serviceConfig = {
452 PermissionsStartOnly = true; # preStart must be run as root
453 Type = "simple";
454 User = cfg.user;
455 Group = cfg.group;
456 TimeoutSec = "300";
457 Restart = "on-failure";
458 WorkingDirectory = gitlabEnv.HOME;
459 ExecStart =
460 "${cfg.packages.gitlab-workhorse}/bin/gitlab-workhorse "
461 + "-listenUmask 0 "
462 + "-listenNetwork unix "
463 + "-listenAddr /run/gitlab/gitlab-workhorse.socket "
464 + "-authSocket ${gitlabSocket} "
465 + "-documentRoot ${cfg.packages.gitlab}/share/gitlab/public "
466 + "-secretPath ${cfg.packages.gitlab}/share/gitlab/.gitlab_workhorse_secret";
467 };
468 };
469
470 systemd.services.gitlab = {
471 after = [ "network.target" "postgresql.service" "redis.service" ];
472 requires = [ "gitlab-sidekiq.service" ];
473 wantedBy = [ "multi-user.target" ];
474 environment = gitlabEnv;
475 path = with pkgs; [
476 config.services.postgresql.package
477 gitAndTools.git
478 openssh
479 nodejs
480 ];
481 preStart = ''
482 mkdir -p ${cfg.backupPath}
483 mkdir -p ${cfg.statePath}/builds
484 mkdir -p ${cfg.statePath}/repositories
485 mkdir -p ${gitlabConfig.production.shared.path}/artifacts
486 mkdir -p ${gitlabConfig.production.shared.path}/lfs-objects
487 mkdir -p ${gitlabConfig.production.shared.path}/pages
488 mkdir -p ${cfg.statePath}/log
489 mkdir -p ${cfg.statePath}/shell
490 mkdir -p ${cfg.statePath}/tmp/pids
491 mkdir -p ${cfg.statePath}/tmp/sockets
492
493 rm -rf ${cfg.statePath}/config ${cfg.statePath}/shell/hooks
494 mkdir -p ${cfg.statePath}/config ${cfg.statePath}/shell
495
496 tr -dc A-Za-z0-9 < /dev/urandom | head -c 32 > ${cfg.statePath}/config/gitlab_shell_secret
497
498 # The uploads directory is hardcoded somewhere deep in rails. It is
499 # symlinked in the gitlab package to /run/gitlab/uploads to make it
500 # configurable
501 mkdir -p /run/gitlab
502 mkdir -p ${cfg.statePath}/uploads
503 ln -sf ${cfg.statePath}/uploads /run/gitlab/uploads
504 chown -R ${cfg.user}:${cfg.group} /run/gitlab
505
506 # Prepare home directory
507 mkdir -p ${gitlabEnv.HOME}/.ssh
508 touch ${gitlabEnv.HOME}/.ssh/authorized_keys
509 chown -R ${cfg.user}:${cfg.group} ${gitlabEnv.HOME}/
510 chmod -R u+rwX,go-rwx+X ${gitlabEnv.HOME}/
511
512 cp -rf ${cfg.packages.gitlab}/share/gitlab/config.dist/* ${cfg.statePath}/config
513 ${optionalString cfg.smtp.enable ''
514 ln -sf ${smtpSettings} ${cfg.statePath}/config/initializers/smtp_settings.rb
515 ''}
516 ln -sf ${cfg.statePath}/config /run/gitlab/config
517 cp ${cfg.packages.gitlab}/share/gitlab/VERSION ${cfg.statePath}/VERSION
518
519 # JSON is a subset of YAML
520 ln -fs ${pkgs.writeText "gitlab.yml" (builtins.toJSON gitlabConfig)} ${cfg.statePath}/config/gitlab.yml
521 ln -fs ${pkgs.writeText "database.yml" databaseYml} ${cfg.statePath}/config/database.yml
522 ln -fs ${pkgs.writeText "secrets.yml" secretsYml} ${cfg.statePath}/config/secrets.yml
523 ln -fs ${pkgs.writeText "unicorn.rb" unicornConfig} ${cfg.statePath}/config/unicorn.rb
524
525 chown -R ${cfg.user}:${cfg.group} ${cfg.statePath}/
526 chmod -R ug+rwX,o-rwx+X ${cfg.statePath}/
527
528 # Install the shell required to push repositories
529 ln -fs ${pkgs.writeText "config.yml" gitlabShellYml} "$GITLAB_SHELL_CONFIG_PATH"
530 ln -fs ${cfg.packages.gitlab-shell}/hooks "$GITLAB_SHELL_HOOKS_PATH"
531 ${cfg.packages.gitlab-shell}/bin/install
532
533 if [ "${cfg.databaseHost}" = "127.0.0.1" ]; then
534 if ! test -e "${cfg.statePath}/db-created"; then
535 psql postgres -c "CREATE ROLE ${cfg.databaseUsername} WITH LOGIN NOCREATEDB NOCREATEROLE NOCREATEUSER ENCRYPTED PASSWORD '${cfg.databasePassword}'"
536 ${config.services.postgresql.package}/bin/createdb --owner ${cfg.databaseUsername} ${cfg.databaseName} || true
537 touch "${cfg.statePath}/db-created"
538 fi
539 fi
540
541 # enable required pg_trgm extension for gitlab
542 psql gitlab -c "CREATE EXTENSION IF NOT EXISTS pg_trgm"
543 # Always do the db migrations just to be sure the database is up-to-date
544 ${gitlab-rake}/bin/gitlab-rake db:migrate RAILS_ENV=production
545
546 # The gitlab:setup task is horribly broken somehow, the db:migrate
547 # task above and the db:seed_fu below will do the same for setting
548 # up the initial database
549 if ! test -e "${cfg.statePath}/db-seeded"; then
550 ${gitlab-rake}/bin/gitlab-rake db:seed_fu RAILS_ENV=production \
551 GITLAB_ROOT_PASSWORD="${cfg.initialRootPassword}" GITLAB_ROOT_EMAIL="${cfg.initialRootEmail}"
552 touch "${cfg.statePath}/db-seeded"
553 fi
554
555 # Change permissions in the last step because some of the
556 # intermediary scripts like to create directories as root.
557 chown -R ${cfg.user}:${cfg.group} ${cfg.statePath}
558 chmod -R u+rwX,go-rwx+X ${cfg.statePath}
559 '';
560
561 serviceConfig = {
562 PermissionsStartOnly = true; # preStart must be run as root
563 Type = "simple";
564 User = cfg.user;
565 Group = cfg.group;
566 TimeoutSec = "300";
567 Restart = "on-failure";
568 WorkingDirectory = "${cfg.packages.gitlab}/share/gitlab";
569 ExecStart = "${cfg.packages.gitlab.env}/bin/bundle exec \"unicorn -c ${cfg.statePath}/config/unicorn.rb -E production\"";
570 };
571
572 };
573
574 };
575
576 meta.doc = ./gitlab.xml;
577
578}