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}