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