1# This test runs gitlab and performs the following tests:
2# - Creating users
3# - Pushing commits
4# - over the API
5# - over SSH
6# - Creating Merge Requests and merging them
7# - Opening and closing issues.
8# - Downloading repository archives as tar.gz and tar.bz2
9# Run with
10# [nixpkgs]$ nix-build -A nixosTests.gitlab
11
12{ pkgs, lib, ... }:
13
14let
15 inherit (import ./ssh-keys.nix pkgs) snakeOilPrivateKey snakeOilPublicKey;
16 initialRootPassword = "notproduction";
17 rootProjectId = "2";
18
19 aliceUsername = "alice";
20 aliceUserId = "2";
21 alicePassword = "R5twyCgU0uXC71wT9BBTCqLs6HFZ7h3L";
22 aliceProjectId = "1";
23 aliceProjectName = "test-alice";
24
25 bobUsername = "bob";
26 bobUserId = "3";
27 bobPassword = "XwkkBbl2SiIwabQzgcoaTbhsotijEEtF";
28 bobProjectId = "2";
29in
30{
31 name = "gitlab";
32 meta.maintainers = with lib.maintainers; [
33 globin
34 yayayayaka
35 ];
36
37 nodes = {
38 gitlab =
39 { ... }:
40 {
41 imports = [ common/user-account.nix ];
42
43 environment.systemPackages = with pkgs; [ git ];
44
45 networking.hosts."127.0.0.1" = [
46 "registry.localhost"
47 "pages.localhost"
48 ];
49 virtualisation.memorySize = 6144;
50 virtualisation.cores = 4;
51 virtualisation.useNixStoreImage = true;
52 virtualisation.writableStore = false;
53
54 systemd.services.gitlab.serviceConfig.Restart = lib.mkForce "no";
55 systemd.services.gitlab-workhorse.serviceConfig.Restart = lib.mkForce "no";
56 systemd.services.gitaly.serviceConfig.Restart = lib.mkForce "no";
57 systemd.services.gitlab-sidekiq.serviceConfig.Restart = lib.mkForce "no";
58
59 services.nginx = {
60 enable = true;
61 recommendedProxySettings = true;
62 virtualHosts = {
63 localhost = {
64 locations."/".proxyPass = "http://unix:/run/gitlab/gitlab-workhorse.socket";
65 };
66 "pages.localhost" = {
67 locations."/".proxyPass = "http://localhost:8090";
68 };
69 "registry.localhost" = {
70 locations."/".proxyPass = "http://localhost:4567";
71 };
72 };
73 };
74
75 services.openssh.enable = true;
76
77 services.dovecot2 = {
78 enable = true;
79 enableImap = true;
80 };
81
82 systemd.services.gitlab-backup.environment.BACKUP = "dump";
83
84 services.gitlab = {
85 enable = true;
86 databasePasswordFile = pkgs.writeText "dbPassword" "xo0daiF4";
87 initialRootPasswordFile = pkgs.writeText "rootPassword" initialRootPassword;
88 smtp.enable = true;
89 pages = {
90 enable = true;
91 settings.pages-domain = "pages.localhost";
92 };
93 extraConfig = {
94 incoming_email = {
95 enabled = true;
96 mailbox = "inbox";
97 address = "alice@localhost";
98 user = "alice";
99 password = "foobar";
100 host = "localhost";
101 port = 143;
102 };
103 };
104 secrets = {
105 secretFile = pkgs.writeText "secret" "Aig5zaic";
106 otpFile = pkgs.writeText "otpsecret" "Riew9mue";
107 dbFile = pkgs.writeText "dbsecret" "we2quaeZ";
108 jwsFile = pkgs.runCommand "oidcKeyBase" { } "${pkgs.openssl}/bin/openssl genrsa 2048 > $out";
109 activeRecordPrimaryKeyFile = pkgs.writeText "arprimary" "vsaYPZjTRxcbG7W6gNr95AwBmzFUd4Eu";
110 activeRecordDeterministicKeyFile = pkgs.writeText "ardeterministic" "kQarv9wb2JVP7XzLTh5f6DFcMHms4nEC";
111 activeRecordSaltFile = pkgs.writeText "arsalt" "QkgR9CfFU3MXEWGqa7LbP24AntK5ZeYw";
112 };
113
114 registry = {
115 enable = true;
116 certFile = "/var/lib/gitlab/registry_auth_cert";
117 keyFile = "/var/lib/gitlab/registry_auth_key";
118 externalAddress = "registry.localhost";
119 externalPort = 443;
120 };
121
122 # reduce memory usage
123 sidekiq.concurrency = 1;
124 puma.workers = 2;
125 };
126 };
127 };
128
129 testScript =
130 { nodes, ... }:
131 let
132 auth = pkgs.writeText "auth.json" (
133 builtins.toJSON {
134 grant_type = "password";
135 username = "root";
136 password = initialRootPassword;
137 }
138 );
139
140 createUserAlice = pkgs.writeText "create-user-alice.json" (
141 builtins.toJSON rec {
142 username = aliceUsername;
143 name = username;
144 email = "alice@localhost";
145 password = alicePassword;
146 skip_confirmation = true;
147 }
148 );
149
150 createUserBob = pkgs.writeText "create-user-bob.json" (
151 builtins.toJSON rec {
152 username = bobUsername;
153 name = username;
154 email = "bob@localhost";
155 password = bobPassword;
156 skip_confirmation = true;
157 }
158 );
159
160 aliceAuth = pkgs.writeText "alice-auth.json" (
161 builtins.toJSON {
162 grant_type = "password";
163 username = aliceUsername;
164 password = alicePassword;
165 }
166 );
167
168 bobAuth = pkgs.writeText "bob-auth.json" (
169 builtins.toJSON {
170 grant_type = "password";
171 username = bobUsername;
172 password = bobPassword;
173 }
174 );
175
176 aliceAddSSHKey = pkgs.writeText "alice-add-ssh-key.json" (
177 builtins.toJSON {
178 id = aliceUserId;
179 title = "snakeoil@nixos";
180 key = snakeOilPublicKey;
181 }
182 );
183
184 createProjectAlice = pkgs.writeText "create-project-alice.json" (
185 builtins.toJSON {
186 name = aliceProjectName;
187 visibility = "public";
188 }
189 );
190
191 putFile = pkgs.writeText "put-file.json" (
192 builtins.toJSON {
193 branch = "master";
194 author_email = "author@example.com";
195 author_name = "Firstname Lastname";
196 content = "some content";
197 commit_message = "create a new file";
198 }
199 );
200
201 mergeRequest = pkgs.writeText "merge-request.json" (
202 builtins.toJSON {
203 id = bobProjectId;
204 target_project_id = aliceProjectId;
205 source_branch = "master";
206 target_branch = "master";
207 title = "Add some other file";
208 }
209 );
210
211 newIssue = pkgs.writeText "new-issue.json" (
212 builtins.toJSON {
213 title = "useful issue title";
214 }
215 );
216
217 closeIssue = pkgs.writeText "close-issue.json" (
218 builtins.toJSON {
219 issue_iid = 1;
220 state_event = "close";
221 }
222 );
223
224 # Wait for all GitLab services to be fully started.
225 waitForServices = ''
226 gitlab.wait_for_unit("gitaly.service")
227 gitlab.wait_for_unit("gitlab-workhorse.service")
228 gitlab.wait_for_unit("gitlab-mailroom.service")
229 gitlab.wait_for_unit("gitlab.service")
230 gitlab.wait_for_unit("gitlab-pages.service")
231 gitlab.wait_for_unit("gitlab-sidekiq.service")
232 gitlab.wait_for_file("${nodes.gitlab.services.gitlab.statePath}/tmp/sockets/gitlab.socket")
233 gitlab.wait_until_succeeds("curl -sSf http://gitlab/users/sign_in")
234 gitlab.wait_for_unit("docker-registry.service")
235 '';
236
237 # The actual test of GitLab. Only push data to GitLab if
238 # `doSetup` is is true.
239 test =
240 doSetup:
241 ''
242 GIT_SSH_COMMAND = "ssh -o StrictHostKeyChecking=accept-new -o UserKnownHostsFile=/dev/null"
243
244 gitlab.succeed(
245 "curl -isSf http://gitlab | grep -i location | grep http://gitlab/users/sign_in"
246 )
247 gitlab.succeed(
248 "${pkgs.sudo}/bin/sudo -u gitlab -H gitlab-rake gitlab:check 1>&2"
249 )
250 gitlab.succeed(
251 "echo \"Authorization: Bearer $(curl -X POST -H 'Content-Type: application/json' -d @${auth} http://gitlab/oauth/token | ${pkgs.jq}/bin/jq -r '.access_token')\" >/tmp/headers"
252 )
253 ''
254 + lib.optionalString doSetup ''
255 with subtest("Create user Alice"):
256 gitlab.succeed(
257 """[ "$(curl -o /dev/null -w '%{http_code}' -X POST -H 'Content-Type: application/json' -H @/tmp/headers -d @${createUserAlice} http://gitlab/api/v4/users)" = "201" ]"""
258 )
259 gitlab.succeed(
260 "echo \"Authorization: Bearer $(curl -X POST -H 'Content-Type: application/json' -d @${aliceAuth} http://gitlab/oauth/token | ${pkgs.jq}/bin/jq -r '.access_token')\" >/tmp/headers-alice"
261 )
262
263 with subtest("Create user Bob"):
264 gitlab.succeed(
265 """ [ "$(curl -o /dev/null -w '%{http_code}' -X POST -H 'Content-Type: application/json' -H @/tmp/headers -d @${createUserBob} http://gitlab/api/v4/users)" = "201" ]"""
266 )
267 gitlab.succeed(
268 "echo \"Authorization: Bearer $(curl -X POST -H 'Content-Type: application/json' -d @${bobAuth} http://gitlab/oauth/token | ${pkgs.jq}/bin/jq -r '.access_token')\" >/tmp/headers-bob"
269 )
270
271 with subtest("Setup Git and SSH for Alice"):
272 gitlab.succeed("git config --global user.name Alice")
273 gitlab.succeed("git config --global user.email alice@nixos.invalid")
274 gitlab.succeed("mkdir -p -m 700 /root/.ssh")
275 gitlab.succeed("cat ${snakeOilPrivateKey} > /root/.ssh/id_ecdsa")
276 gitlab.succeed("chmod 600 /root/.ssh/id_ecdsa")
277 gitlab.succeed(
278 """
279 [ "$(curl \
280 -o /dev/null \
281 -w '%{http_code}' \
282 -X POST \
283 -H 'Content-Type: application/json' \
284 -H @/tmp/headers-alice -d @${aliceAddSSHKey} \
285 http://gitlab/api/v4/user/keys)" = "201" ]
286 """
287 )
288
289 with subtest("Create a new repository"):
290 # Alice creates a new repository
291 gitlab.succeed(
292 """
293 [ "$(curl \
294 -o /dev/null \
295 -w '%{http_code}' \
296 -X POST \
297 -H 'Content-Type: application/json' \
298 -H @/tmp/headers-alice \
299 -d @${createProjectAlice} \
300 http://gitlab/api/v4/projects)" = "201" ]
301 """
302 )
303
304 # Alice commits an initial commit
305 gitlab.succeed(
306 """
307 [ "$(curl \
308 -o /dev/null \
309 -w '%{http_code}' \
310 -X POST \
311 -H 'Content-Type: application/json' \
312 -H @/tmp/headers-alice \
313 -d @${putFile} \
314 http://gitlab/api/v4/projects/${aliceProjectId}/repository/files/some-file.txt)" = "201" ]"""
315 )
316
317 with subtest("git clone over HTTP"):
318 gitlab.succeed(
319 """git clone http://gitlab/alice/${aliceProjectName}.git clone-via-http""",
320 timeout=15
321 )
322
323 with subtest("Push a commit via SSH"):
324 gitlab.succeed(
325 f"""GIT_SSH_COMMAND="{GIT_SSH_COMMAND}" git clone gitlab@gitlab:alice/${aliceProjectName}.git""",
326 timeout=15
327 )
328 gitlab.succeed(
329 """echo "a commit sent over ssh" > ${aliceProjectName}/ssh.txt"""
330 )
331 gitlab.succeed(
332 """
333 cd ${aliceProjectName} || exit 1
334 git add .
335 """
336 )
337 gitlab.succeed(
338 """
339 cd ${aliceProjectName} || exit 1
340 git commit -m "Add a commit to be sent over ssh"
341 """
342 )
343 gitlab.succeed(
344 f"""
345 cd ${aliceProjectName} || exit 1
346 GIT_SSH_COMMAND="{GIT_SSH_COMMAND}" git push --set-upstream origin master
347 """,
348 timeout=15
349 )
350
351 with subtest("Fork a project"):
352 # Bob forks Alice's project
353 gitlab.succeed(
354 """
355 [ "$(curl \
356 -o /dev/null \
357 -w '%{http_code}' \
358 -X POST \
359 -H 'Content-Type: application/json' \
360 -H @/tmp/headers-bob \
361 http://gitlab/api/v4/projects/${aliceProjectId}/fork)" = "201" ]
362 """
363 )
364
365 # Bob creates a commit
366 gitlab.wait_until_succeeds(
367 """
368 [ "$(curl \
369 -o /dev/null \
370 -w '%{http_code}' \
371 -X POST \
372 -H 'Content-Type: application/json' \
373 -H @/tmp/headers-bob \
374 -d @${putFile} \
375 http://gitlab/api/v4/projects/${bobProjectId}/repository/files/some-other-file.txt)" = "201" ]
376 """
377 )
378
379 with subtest("Create a Merge Request"):
380 # Bob opens a merge request against Alice's repository
381 gitlab.wait_until_succeeds(
382 """
383 [ "$(curl \
384 -o /dev/null \
385 -w '%{http_code}' \
386 -X POST \
387 -H 'Content-Type: application/json' \
388 -H @/tmp/headers-bob \
389 -d @${mergeRequest} \
390 http://gitlab/api/v4/projects/${bobProjectId}/merge_requests)" = "201" ]
391 """
392 )
393
394 # Alice merges the MR
395 gitlab.wait_until_succeeds(
396 """
397 [ "$(curl \
398 -o /dev/null \
399 -w '%{http_code}' \
400 -X PUT \
401 -H 'Content-Type: application/json' \
402 -H @/tmp/headers-alice \
403 -d @${mergeRequest} \
404 http://gitlab/api/v4/projects/${aliceProjectId}/merge_requests/1/merge)" = "200" ]
405 """
406 )
407
408 with subtest("Create an Issue"):
409 # Bob opens an issue on Alice's repository
410 gitlab.succeed(
411 """[ "$(curl \
412 -o /dev/null \
413 -w '%{http_code}' \
414 -X POST \
415 -H 'Content-Type: application/json' \
416 -H @/tmp/headers-bob \
417 -d @${newIssue} \
418 http://gitlab/api/v4/projects/${aliceProjectId}/issues)" = "201" ]
419 """
420 )
421
422 # Alice closes the issue
423 gitlab.wait_until_succeeds(
424 """
425 [ "$(curl \
426 -o /dev/null \
427 -w '%{http_code}' \
428 -X PUT \
429 -H 'Content-Type: application/json' \
430 -H @/tmp/headers-alice -d @${closeIssue} http://gitlab/api/v4/projects/${aliceProjectId}/issues/1)" = "200" ]
431 """
432 )
433 ''
434 + ''
435 with subtest("Download archive.tar.gz"):
436 gitlab.succeed(
437 """
438 [ "$(curl \
439 -o /dev/null \
440 -w '%{http_code}' \
441 -H @/tmp/headers-alice \
442 http://gitlab/api/v4/projects/${aliceProjectId}/repository/archive.tar.gz)" = "200" ]
443 """
444 )
445 gitlab.succeed(
446 """
447 curl \
448 -H @/tmp/headers-alice \
449 http://gitlab/api/v4/projects/${aliceProjectId}/repository/archive.tar.gz > /tmp/archive.tar.gz
450 """
451 )
452 gitlab.succeed("test -s /tmp/archive.tar.gz")
453
454 with subtest("Download archive.tar.bz2"):
455 gitlab.succeed(
456 """
457 [ "$(curl \
458 -o /dev/null \
459 -w '%{http_code}' \
460 -H @/tmp/headers-alice \
461 http://gitlab/api/v4/projects/${aliceProjectId}/repository/archive.tar.bz2)" = "200" ]
462 """
463 )
464 gitlab.succeed(
465 """
466 curl \
467 -H @/tmp/headers-alice \
468 http://gitlab/api/v4/projects/${aliceProjectId}/repository/archive.tar.bz2 > /tmp/archive.tar.bz2
469 """
470 )
471 gitlab.succeed("test -s /tmp/archive.tar.bz2")
472 ''
473 + ''
474 with subtest("Test docker registry http is available"):
475 gitlab.succeed("curl -sSf http://registry.localhost")
476 '';
477
478 in
479 ''
480 gitlab.start()
481 ''
482 + waitForServices
483 + ''
484 gitlab.succeed("cp /var/gitlab/state/config/secrets.yml /root/gitlab-secrets.yml")
485 ''
486 + test true
487 + ''
488 gitlab.systemctl("start gitlab-backup.service")
489 gitlab.wait_for_unit("gitlab-backup.service")
490 gitlab.wait_for_file("${nodes.gitlab.services.gitlab.statePath}/backup/dump_gitlab_backup.tar")
491 gitlab.systemctl("stop postgresql gitlab-config.service gitlab.target")
492 gitlab.succeed(
493 "find ${nodes.gitlab.services.gitlab.statePath} -mindepth 1 -maxdepth 1 -not -name backup -execdir rm -r {} +"
494 )
495 gitlab.succeed("systemd-tmpfiles --create")
496 gitlab.succeed("rm -rf ${nodes.gitlab.services.postgresql.dataDir}")
497 gitlab.systemctl("start gitlab-config.service gitaly.service gitlab-postgresql.service")
498 gitlab.wait_for_file("${nodes.gitlab.services.gitlab.statePath}/tmp/sockets/gitaly.socket")
499 gitlab.succeed(
500 "sudo -u gitlab -H gitlab-rake gitlab:backup:restore RAILS_ENV=production BACKUP=dump force=yes"
501 )
502 gitlab.systemctl("start gitlab.target")
503 ''
504 + waitForServices
505 + ''
506 with subtest("Check that no secrets were auto-generated as these would be non-persistent"):
507 gitlab.succeed("diff -u /root/gitlab-secrets.yml /var/gitlab/state/config/secrets.yml")
508 ''
509 + test false;
510}