]> Git — Sourcephile - sourcephile-nix.git/blob - nixos/modules/services/misc/sourcehut/default.nix
nixos/sourcehut: massive rewrite
[sourcephile-nix.git] / nixos / modules / services / misc / sourcehut / default.nix
1 { config, pkgs, lib, ... }:
2 with lib;
3 let
4 inherit (config.services) postfix postgresql redis;
5 inherit (config.users) users groups;
6 cfg = config.services.sourcehut;
7 domain = cfg.settings."sr.ht".global-domain;
8 settingsFormat = pkgs.formats.ini {
9 listToValue = concatMapStringsSep "," (generators.mkValueStringDefault {});
10 mkKeyValue = k: v:
11 if v == null then ""
12 else generators.mkKeyValueDefault {
13 mkValueString = v:
14 if v == true then "yes"
15 else if v == false then "no"
16 else generators.mkValueStringDefault {} v;
17 } "=" k v;
18 };
19 configIniOfService = srv: settingsFormat.generate "sourcehut-${srv}-config.ini"
20 # Each service needs access to only a subset of sections (and secrets).
21 (filterAttrs (k: v: v != null)
22 (mapAttrs (section: v:
23 let srvMatch = builtins.match "^([a-z]*)\\.sr\\.ht(::.*)?$" section; in
24 if srvMatch == null # Include sections shared by all services
25 || head srvMatch == srv # Include sections for the service being configured
26 then v
27 # *srht-{dispatch,keys,shell,update-hook} share the same config.ini
28 else if srv == "ssh" && elem (head srvMatch) ["builds" "git" "hg"] && cfg.${head srvMatch}.enable then v
29 # Enable Web links and integrations between services.
30 else if tail srvMatch == [ null ] && elem (head srvMatch) cfg.services
31 then {
32 inherit (v) origin;
33 # mansrht crashes without it
34 oauth-client-id = v.oauth-client-id or null;
35 }
36 # Drop sub-sections of other services
37 else null)
38 (recursiveUpdate cfg.settings {
39 # Those paths are mounted using BindPaths= or BindReadOnlyPaths=
40 # for services needing access to them.
41 "git.sr.ht".repos = "/var/lib/sourcehut/gitsrht/repos";
42 "hg.sr.ht".repos = "/var/lib/sourcehut/hgsrht/repos";
43 "git.sr.ht".post-update-script = "/var/lib/sourcehut/gitsrht/bin/post-update-script";
44 "hg.sr.ht".changegroup-script = "/var/lib/sourcehut/hgsrht/bin/changegroup-script";
45 })));
46 commonServiceSettings = srv: {
47 origin = mkOption {
48 description = "URL ${srv}.sr.ht is being served at (protocol://domain)";
49 type = types.str;
50 default = "https://${srv}.localhost.localdomain";
51 };
52 debug-host = mkOption {
53 description = "Address to bind the debug server to.";
54 type = with types; nullOr str;
55 default = null;
56 };
57 debug-port = mkOption {
58 description = "Port to bind the debug server to.";
59 type = with types; nullOr str;
60 default = null;
61 };
62 connection-string = mkOption {
63 description = "SQLAlchemy connection string for the database.";
64 type = types.str;
65 default = "postgresql:///localhost?user=${srv}srht&host=/run/postgresql";
66 };
67 migrate-on-upgrade = mkEnableOption "automatic migrations on package upgrade" // { default = true; };
68 oauth-client-id = mkOption {
69 description = "${srv}.sr.ht's OAuth client id for meta.sr.ht.";
70 type = types.str;
71 };
72 oauth-client-secret = mkOption {
73 description = "${srv}.sr.ht's OAuth client secret for meta.sr.ht.";
74 type = types.path;
75 apply = s: "<" + toString s;
76 };
77 };
78
79 # Specialized python containing all the modules
80 python = pkgs.sourcehut.python.withPackages (ps: with ps; [
81 gunicorn
82 eventlet
83 # Sourcehut services
84 srht
85 buildsrht
86 dispatchsrht
87 gitsrht
88 hgsrht
89 hubsrht
90 listssrht
91 mansrht
92 metasrht
93 # Not a python package
94 #pagessrht
95 pastesrht
96 todosrht
97 ]);
98 mkOptionNullOrStr = description: mkOption {
99 inherit description;
100 type = with types; nullOr str;
101 default = null;
102 };
103 in
104 {
105 options.services.sourcehut = {
106 enable = mkEnableOption ''
107 sourcehut - git hosting, continuous integration, mailing list, ticket tracking,
108 task dispatching, wiki and account management services
109 '';
110
111 services = mkOption {
112 type = with types; listOf (enum
113 [ "builds" "dispatch" "git" "hg" "hub" "lists" "man" "meta" "pages" "paste" "todo" ]);
114 defaultText = "locally enabled services";
115 description = ''
116 Services that may be displayed as links in the title bar of the Web interface.
117 '';
118 };
119
120 listenAddress = mkOption {
121 type = types.str;
122 default = "localhost";
123 description = "Address to bind to.";
124 };
125
126 python = mkOption {
127 internal = true;
128 type = types.package;
129 default = python;
130 description = ''
131 The python package to use. It should contain references to the *srht modules and also
132 gunicorn.
133 '';
134 };
135
136 minio = {
137 enable = mkEnableOption ''local minio integration'';
138 };
139
140 nginx = {
141 enable = mkEnableOption ''local nginx integration'';
142 };
143
144 postfix = {
145 enable = mkEnableOption ''local postfix integration'';
146 };
147
148 postgresql = {
149 enable = mkEnableOption ''local postgresql integration'';
150 };
151
152 redis = {
153 enable = mkEnableOption ''local redis integration'';
154 firstDatabase = mkOption {
155 type = types.int;
156 default = 0;
157 description = ''
158 Number of the first Redis database to use.
159 At most 9 consecutive databases are currently used.
160 '';
161 };
162 };
163
164 settings = mkOption {
165 type = lib.types.submodule {
166 freeformType = settingsFormat.type;
167 options."sr.ht" = {
168 global-domain = mkOption {
169 description = "Global domain name.";
170 type = types.str;
171 example = "example.com";
172 };
173 environment = mkOption {
174 description = "Values other than \"production\" adds a banner to each page.";
175 type = types.enum [ "development" "production" ];
176 default = "development";
177 };
178 network-key = mkOption {
179 description = ''
180 An absolute file path (which should be outside the Nix-store)
181 to a secret key to encrypt internal messages with. Use <code>srht-keygen network</code> to
182 generate this key. It must be consistent between all services and nodes.
183 '';
184 type = types.path;
185 apply = s: "<" + toString s;
186 };
187 owner-email = mkOption {
188 description = "Owner's email.";
189 type = types.str;
190 default = "contact@example.com";
191 };
192 owner-name = mkOption {
193 description = "Owner's name.";
194 type = types.str;
195 default = "John Doe";
196 };
197 redis-host = mkOption {
198 type = with types; nullOr str;
199 description = ''
200 The redis host URL. This is used for caching and temporary storage, and must
201 be shared between nodes (e.g. g - 1it1.sr.ht and git2.sr.ht), but need not be
202 shared between services. It may be shared between services, however, with no
203 ill effect, if this better suits your infrastructure.
204 '';
205 };
206 site-blurb = mkOption {
207 description = "Blurb for your site.";
208 type = types.str;
209 default = "the hacker's forge";
210 };
211 site-info = mkOption {
212 description = "The top-level info page for your site.";
213 type = types.str;
214 default = "https://sourcehut.org";
215 };
216 service-key = mkOption {
217 description = ''
218 An absolute file path (which should be outside the Nix-store)
219 to a key used for encrypting session cookies. Use <code>srht-keygen service</code> to
220 generate the service key. This must be shared between each node of the same
221 service (e.g. git1.sr.ht and git2.sr.ht), but different services may use
222 different keys. If you configure all of your services with the same
223 config.ini, you may use the same service-key for all of them.
224 '';
225 type = types.path;
226 apply = s: "<" + toString s;
227 };
228 site-name = mkOption {
229 description = "The name of your network of sr.ht-based sites.";
230 type = types.str;
231 default = "sourcehut";
232 };
233 source-url = mkOption {
234 description = "The source code for your fork of sr.ht.";
235 type = types.str;
236 default = "https://git.sr.ht/~sircmpwn/srht";
237 };
238 };
239 options.mail = {
240 smtp-host = mkOptionNullOrStr "Outgoing SMTP host.";
241 smtp-port = mkOption {
242 description = "Outgoing SMTP port.";
243 type = with types; nullOr port;
244 default = null;
245 };
246 smtp-user = mkOptionNullOrStr "Outgoing SMTP user.";
247 smtp-password = mkOptionNullOrStr "Outgoing SMTP password.";
248 smtp-from = mkOptionNullOrStr "Outgoing SMTP FROM.";
249 error-to = mkOptionNullOrStr "Address receiving application exceptions";
250 error-from = mkOptionNullOrStr "Address sending application exceptions";
251 pgp-privkey = mkOptionNullOrStr ''
252 An absolute file path (which should be outside the Nix-store)
253 to an OpenPGP private key.
254
255 Your PGP key information (DO NOT mix up pub and priv here)
256 You must remove the password from your secret key, if present.
257 You can do this with <code>gpg --edit-key [key-id]</code>, then use the <code>passwd</code> command and do not enter a new password.
258 '';
259 pgp-pubkey = mkOptionNullOrStr "OpenPGP public key.";
260 pgp-key-id = mkOptionNullOrStr "OpenPGP key identifier.";
261 };
262 options.objects = {
263 s3-upstream = mkOption {
264 description = "Configure the S3-compatible object storage service.";
265 type = with types; nullOr str;
266 default = null;
267 };
268 s3-access-key = mkOption {
269 description = "Access key to the S3-compatible object storage service";
270 type = with types; nullOr str;
271 default = null;
272 };
273 s3-secret-key = mkOption {
274 description = ''
275 An absolute file path (which should be outside the Nix-store)
276 to the secret key of the S3-compatible object storage service.
277 '';
278 type = with types; nullOr path;
279 default = null;
280 apply = mapNullable (s: "<" + toString s);
281 };
282 };
283 options.webhooks = {
284 private-key = mkOption {
285 description = ''
286 An absolute file path (which should be outside the Nix-store)
287 to a base64-encoded Ed25519 key for signing webhook payloads.
288 This should be consistent for all *.sr.ht sites,
289 as this key will be used to verify signatures
290 from other sites in your network.
291 Use the <code>srht-keygen webhook</code> command to generate a key.
292 '';
293 type = types.path;
294 apply = s: "<" + toString s;
295 };
296 };
297
298 options."dispatch.sr.ht" = commonServiceSettings "dispatch" // {
299 };
300 options."dispatch.sr.ht::github" = {
301 oauth-client-id = mkOptionNullOrStr "OAuth client id.";
302 oauth-client-secret = mkOptionNullOrStr "OAuth client secret.";
303 };
304 options."dispatch.sr.ht::gitlab" = {
305 enabled = mkEnableOption "GitLab integration";
306 canonical-upstream = mkOption {
307 type = types.str;
308 description = "Canonical upstream.";
309 default = "gitlab.com";
310 };
311 repo-cache = mkOption {
312 type = types.str;
313 description = "Repository cache directory.";
314 default = "./repo-cache";
315 };
316 "gitlab.com" = mkOption {
317 type = with types; nullOr str;
318 description = "GitLab id and secret.";
319 default = null;
320 example = "GitLab:application id:secret";
321 };
322 };
323
324 options."builds.sr.ht" = commonServiceSettings "builds" // {
325 redis = mkOption {
326 description = "The redis connection used for the celery worker.";
327 type = types.str;
328 default = "redis://localhost:6379/3";
329 };
330 shell = mkOption {
331 description = "The shell used for ssh.";
332 type = types.str;
333 default = "runner-shell";
334 };
335 };
336
337 options."git.sr.ht" = commonServiceSettings "git" // {
338 outgoing-domain = mkOption {
339 description = "Outgoing domain.";
340 type = types.str;
341 default = "https://git.localhost.localdomain";
342 };
343 post-update-script = mkOption {
344 description = ''
345 A post-update script which is installed in every git repo.
346 This setting is propagated to newer and existing repositories.
347 '';
348 type = types.path;
349 default = "${pkgs.sourcehut.gitsrht}/bin/gitsrht-update-hook";
350 defaultText = "\${pkgs.sourcehut.gitsrht}/bin/gitsrht-update-hook";
351 # Git hooks are run relative to their repository's directory,
352 # but gitsrht-update-hook looks up ../config.ini
353 apply = p: pkgs.writeShellScript "update-hook" ''
354 test -e "''${PWD%/*}"/config.ini ||
355 ln -s ${users."sshsrht".home}/../config.ini "''${PWD%/*}"/config.ini
356 exec -a "$0" '${p}' "$@"
357 '';
358 };
359 repos = mkOption {
360 description = ''
361 Path to git repositories on disk.
362 If changing the default, you must ensure that
363 the gitsrht's user as read and write access to it.
364 '';
365 type = types.str;
366 default = "/var/lib/sourcehut/gitsrht/repos";
367 };
368 webhooks = mkOption {
369 description = "The redis connection used for the webhooks worker.";
370 type = types.str;
371 default = "redis://localhost:6379/1";
372 };
373 };
374
375 options."hg.sr.ht" = commonServiceSettings "hg" // {
376 changegroup-script = mkOption {
377 description = ''
378 A changegroup script which is installed in every mercurial repo.
379 This setting is propagated to newer and existing repositories.
380 '';
381 type = types.str;
382 default = "${cfg.python}/bin/hgsrht-hook-changegroup";
383 };
384 repos = mkOption {
385 description = ''
386 Path to mercurial repositories on disk.
387 If changing the default, you must ensure that
388 the hgsrht's user as read and write access to it.
389 '';
390 type = types.str;
391 default = "/var/lib/sourcehut/hgsrht/repos";
392 };
393 srhtext = mkOptionNullOrStr ''
394 Path to the srht mercurial extension
395 (defaults to where the hgsrht code is)
396 '';
397 clone_bundle_threshold = mkOption {
398 description = ".hg/store size (in MB) past which the nightly job generates clone bundles.";
399 type = types.ints.unsigned;
400 default = 50;
401 };
402 hg_ssh = mkOption {
403 description = "Path to hg-ssh (if not in $PATH).";
404 type = types.str;
405 default = "${pkgs.mercurial}/bin/hg-ssh";
406 };
407 webhooks = mkOption {
408 description = "The redis connection used for the webhooks worker.";
409 type = types.str;
410 default = "redis://localhost:6379/8";
411 };
412 };
413
414 options."hub.sr.ht" = commonServiceSettings "hub" // {
415 };
416
417 options."lists.sr.ht" = commonServiceSettings "lists" // {
418 allow-new-lists = mkEnableOption "Allow creation of new lists.";
419 notify-from = mkOption {
420 description = "Outgoing email for notifications generated by users.";
421 type = types.str;
422 default = "lists-notify@localhost.localdomain";
423 };
424 posting-domain = mkOption {
425 description = "Posting domain.";
426 type = types.str;
427 default = "lists.localhost.localdomain";
428 };
429 redis = mkOption {
430 description = "The redis connection used for the celery worker.";
431 type = types.str;
432 default = "redis://localhost:6379/4";
433 };
434 webhooks = mkOption {
435 description = "The redis connection used for the webhooks worker.";
436 type = types.str;
437 default = "redis://localhost:6379/2";
438 };
439 };
440 options."lists.sr.ht::worker" = {
441 reject-mimetypes = mkOption {
442 type = with types; listOf str;
443 default = ["text/html"];
444 };
445 reject-url = mkOption {
446 description = "Reject URL.";
447 type = types.str;
448 default = "https://man.sr.ht/lists.sr.ht/etiquette.md";
449 };
450 sock = mkOption {
451 description = ''
452 Path for the lmtp daemon's unix socket. Direct incoming mail to this socket.
453 Alternatively, specify IP:PORT and an SMTP server will be run instead.
454 '';
455 type = types.str;
456 default = "/tmp/lists.sr.ht-lmtp.sock";
457 };
458 sock-group = mkOption {
459 description = ''
460 The lmtp daemon will make the unix socket group-read/write
461 for users in this group.
462 '';
463 type = types.str;
464 default = "postfix";
465 };
466 };
467
468 options."man.sr.ht" = commonServiceSettings "man" // {
469 };
470
471 options."meta.sr.ht" =
472 removeAttrs (commonServiceSettings "meta")
473 ["oauth-client-id" "oauth-client-secret"] // {
474 api-origin = mkOption {
475 description = "Origin URL for API, 100 more than web.";
476 type = types.str;
477 default = "http://localhost:5100";
478 };
479 webhooks = mkOption {
480 description = "The redis connection used for the webhooks worker.";
481 type = types.str;
482 default = "redis://localhost:6379/6";
483 };
484 welcome-emails = mkEnableOption "sending stock sourcehut welcome emails after signup";
485 };
486 options."meta.sr.ht::settings" = {
487 registration = mkEnableOption "public registration";
488 onboarding-redirect = mkOption {
489 description = "Where to redirect new users upon registration.";
490 type = types.str;
491 default = "https://meta.localhost.localdomain";
492 };
493 user-invites = mkOption {
494 description = ''
495 How many invites each user is issued upon registration
496 (only applicable if open registration is disabled).
497 '';
498 type = types.ints.unsigned;
499 default = 5;
500 };
501 };
502 options."meta.sr.ht::aliases" = mkOption {
503 description = "Aliases for the client IDs of commonly used OAuth clients.";
504 type = with types; attrsOf int;
505 default = {};
506 example = { "git.sr.ht" = 12345; };
507 };
508 options."meta.sr.ht::billing" = {
509 enabled = mkEnableOption "the billing system";
510 stripe-public-key = mkOptionNullOrStr "Public key for Stripe. Get your keys at https://dashboard.stripe.com/account/apikeys";
511 stripe-secret-key = mkOptionNullOrStr ''
512 An absolute file path (which should be outside the Nix-store)
513 to a secret key for Stripe. Get your keys at https://dashboard.stripe.com/account/apikeys
514 '' // {
515 apply = mapNullable (s: "<" + toString s);
516 };
517 };
518
519 options."pages.sr.ht" = commonServiceSettings "pages" // {
520 gemini-certs = mkOption {
521 description = ''
522 An absolute file path (which should be outside the Nix-store)
523 to Gemini certificates.
524 '';
525 type = with types; nullOr path;
526 default = null;
527 };
528 max-site-size = mkOption {
529 description = "Maximum size of any given site (post-gunzip), in MiB.";
530 type = types.int;
531 default = 1024;
532 };
533 user-domain = mkOption {
534 description = ''
535 Configures the user domain, if enabled.
536 All users are given <username>.this.domain.
537 '';
538 type = with types; nullOr str;
539 default = null;
540 };
541 };
542 options."pages.sr.ht::api" = {
543 };
544
545 options."paste.sr.ht" = commonServiceSettings "paste" // {
546 webhooks = mkOption {
547 type = types.str;
548 default = "redis://localhost:6379/5";
549 };
550 };
551
552 options."todo.sr.ht" = commonServiceSettings "todo" // {
553 notify-from = mkOption {
554 description = "Outgoing email for notifications generated by users.";
555 type = types.str;
556 default = "todo-notify@localhost.localdomain";
557 };
558 webhooks = mkOption {
559 description = "The redis connection used for the webhooks worker.";
560 type = types.str;
561 default = "redis://localhost:6379/7";
562 };
563 };
564 options."todo.sr.ht::mail" = {
565 posting-domain = mkOption {
566 description = "Posting domain.";
567 type = types.str;
568 default = "todo.localhost.localdomain";
569 };
570 sock = mkOption {
571 description = ''
572 Path for the lmtp daemon's unix socket. Direct incoming mail to this socket.
573 Alternatively, specify IP:PORT and an SMTP server will be run instead.
574 '';
575 type = types.str;
576 default = "/tmp/todo.sr.ht-lmtp.sock";
577 };
578 sock-group = mkOption {
579 description = ''
580 The lmtp daemon will make the unix socket group-read/write
581 for users in this group.
582 '';
583 type = types.str;
584 default = "postfix";
585 };
586 };
587 };
588 default = { };
589 description = ''
590 The configuration for the sourcehut network.
591 '';
592 };
593
594 builds = {
595 enableWorker = mkEnableOption "worker for builds.sr.ht";
596
597 images = mkOption {
598 type = with types; attrsOf (attrsOf (attrsOf package));
599 default = { };
600 example = lib.literalExample ''(let
601 # Pinning unstable to allow usage with flakes and limit rebuilds.
602 pkgs_unstable = builtins.fetchGit {
603 url = "https://github.com/NixOS/nixpkgs";
604 rev = "ff96a0fa5635770390b184ae74debea75c3fd534";
605 ref = "nixos-unstable";
606 };
607 image_from_nixpkgs = pkgs_unstable: (import ("${pkgs.sourcehut.buildsrht}/lib/images/nixos/image.nix") {
608 pkgs = (import pkgs_unstable {});
609 });
610 in
611 {
612 nixos.unstable.x86_64 = image_from_nixpkgs pkgs_unstable;
613 }
614 )'';
615 description = ''
616 Images for builds.sr.ht. Each package should be distro.release.arch and point to a /nix/store/package/root.img.qcow2.
617 '';
618 };
619 };
620
621 git = {
622 package = mkOption {
623 type = types.package;
624 default = pkgs.git;
625 example = literalExample "pkgs.gitFull";
626 description = ''
627 Git package for git.sr.ht. This can help silence collisions.
628 '';
629 };
630 };
631
632 hg = {
633 package = mkOption {
634 type = types.package;
635 default = pkgs.mercurial;
636 description = ''
637 Mercurial package for hg.sr.ht. This can help silence collisions.
638 '';
639 };
640 cloneBundles = mkOption {
641 type = types.bool;
642 default = false;
643 description = ''
644 Generate clonebundles (which require more disk space but dramatically speed up cloning large repositories).
645 '';
646 };
647 };
648 };
649
650 config = mkIf cfg.enable (mkMerge [
651 {
652 environment.systemPackages = [ pkgs.sourcehut.coresrht ];
653
654 services.sourcehut.settings = {
655 "git.sr.ht".outgoing-domain = mkDefault "https://git.${domain}";
656 "lists.sr.ht".notify-from = mkDefault "lists-notify@${domain}";
657 "lists.sr.ht".posting-domain = mkDefault "lists.${domain}";
658 "meta.sr.ht::settings".onboarding-redirect = mkDefault "https://meta.${domain}";
659 "todo.sr.ht".notify-from = mkDefault "todo-notify@${domain}";
660 "todo.sr.ht::mail".posting-domain = mkDefault "todo.${domain}";
661 };
662 }
663 (mkIf cfg.postgresql.enable {
664 services.postgresql.enable = true;
665 })
666 (mkIf cfg.postfix.enable {
667 services.postfix.enable = true;
668 # Needed for sharing the LMTP sockets with JoinsNamespaceOf=
669 systemd.services.postfix.serviceConfig.PrivateTmp = true;
670 })
671 (mkIf cfg.redis.enable {
672 services.redis.enable = true;
673 services.sourcehut.settings."sr.ht".redis-host = mkDefault ("redis://localhost:6379/" + toString cfg.redis.firstDatabase);
674 })
675 (mkIf cfg.nginx.enable {
676 services.nginx.enable = true;
677 })
678 (mkIf (cfg.builds.enable || cfg.git.enable || cfg.hg.enable) {
679 services.openssh = {
680 # Note that sshd will continue to honor AuthorizedKeysFile
681 authorizedKeysCommand = ''/etc/ssh/srht-dispatch "%u" "%h" "%t" "%k"'';
682 # The sshsrht-dispatch user needs:
683 # 1. to read ${users."sshsrht".home}/../config.ini,
684 # 2. to access the redis server in redis-host,
685 # 3. to access the postgresql server in the service's connection-string,
686 # 4. to query metasrht-api (through the HTTP API).
687 # Note that *srht-{dispatch,keys,shell,update-hook} will likely fail
688 # to write their log on /var/log with that user, and will fallback to stderr,
689 # making their log visible in sshd's log when sshd is in debug mode (-d).
690 # Alternatively, you can touch and chown sshsrht /var/log/gitsrht-{dispatch,keys,shell,update-hook}
691 # during your debug.
692 authorizedKeysCommandUser = users."sshsrht".name;
693 extraConfig = ''
694 PermitUserEnvironment SRHT_*
695 '';
696 };
697 environment.etc."ssh/srht-dispatch" = {
698 # sshd_config(5): The program must be owned by root, not writable by group or others
699 mode = "0755";
700 source = pkgs.writeShellScript "srht-dispatch" ''
701 set -e
702 cd ${users."sshsrht".home}
703 exec ${cfg.python}/bin/gitsrht-dispatch "$@"
704 '';
705 };
706 systemd.services.sshd = let configIni = configIniOfService "ssh"; in {
707 #path = optional cfg.git.enable [ cfg.git.package ];
708 restartTriggers = [ configIni ];
709 serviceConfig = {
710 RuntimeDirectory = [ "sourcehut/sshsrht/subdir" ];
711 BindReadOnlyPaths =
712 # Note that the path /usr/bin/*srht-* are hardcoded in multiple places in *.sr.ht,
713 # for instance to get the user from the [*.sr.ht::dispatch] settings.
714 optionals cfg.builds.enable [
715 "${pkgs.sourcehut.buildsrht}/bin/buildsrht-keys:/usr/bin/buildsrht-keys"
716 "${pkgs.sourcehut.buildsrht}/bin/buildsrht-shell:/usr/bin/buildsrht-shell"
717 ] ++
718 optionals cfg.git.enable [
719 "${pkgs.sourcehut.gitsrht}/bin/gitsrht-keys:/usr/bin/gitsrht-keys"
720 "${pkgs.sourcehut.gitsrht}/bin/gitsrht-shell:/usr/bin/gitsrht-shell"
721 ] ++
722 optionals cfg.hg.enable [
723 "${pkgs.sourcehut.hgsrht}/bin/hgsrht-keys:/usr/bin/htsrht-keys"
724 "${pkgs.sourcehut.hgsrht}/bin/hgsrht-shell:/usr/bin/htsrht-shell"
725 ];
726 ExecStartPre = mkBefore [("+"+pkgs.writeShellScript "sshsrht-credentials" ''
727 # Replace values begining with a '<' by the content of the file whose name is after.
728 ${pkgs.gawk}/bin/gawk '{ if (match($0,/^([^=]+=)<(.+)/,m)) { getline f < m[2]; print m[1] f } else print $0 }' ${configIni} |
729 install -o ${users."sshsrht".name} -g ${groups."sshsrht".name} -m 440 \
730 /dev/stdin ${users."sshsrht".home}/../config.ini
731 '')];
732 };
733 };
734 users = {
735 users."sshsrht" = {
736 isSystemUser = true;
737 # srht-dispatch, *srht-keys, and *srht-shell
738 # look up in ../config.ini from this directory;
739 # that config.ini being set in *srht.service's ExecStartPre=
740 home = "/run/sourcehut/sshsrht/subdir";
741 group =
742 # Unfortunately, AuthorizedKeysCommandUser does not honor supplementary groups,
743 # hence the main group is used.
744 if cfg.postgresql.enable
745 && hasSuffix "0" (postgresql.settings.unix_socket_permissions or "")
746 then groups.postgres.name
747 else groups.nogroup.name;
748 description = "sourcehut user for AuthorizedKeysCommand";
749 };
750 groups."sshsrht" = {};
751 };
752 })
753 ]);
754
755 imports = [
756 (import ./service.nix "builds" {
757 inherit configIniOfService;
758 port = 5002;
759 redisDatabase = 3;
760 extraServices.buildsrht-worker = {
761 enable = cfg.builds.enableWorker;
762 partOf = [ "buildsrht.service" ];
763 path = [ pkgs.openssh pkgs.docker ];
764 preStart = let
765 qemuPackage = pkgs.qemu_kvm;
766 statePath = "/var/lib/sourcehut/buildsrht";
767 in ''
768 if [[ "$(docker images -q qemu:latest 2> /dev/null)" == "" || "$(cat ${statePath}/docker-image-qemu 2> /dev/null || true)" != "${qemuPackage.version}" ]]; then
769 # Create and import qemu:latest image for docker
770 ${pkgs.dockerTools.streamLayeredImage {
771 name = "qemu";
772 tag = "latest";
773 contents = [ qemuPackage ];
774 }} | docker load
775 # Mark down current package version
776 printf "%s" "${qemuPackage.version}" > ${statePath}/docker-image-qemu
777 fi
778 '';
779 serviceConfig = {
780 ExecStart = "${pkgs.sourcehut.buildsrht}/bin/builds.sr.ht-worker";
781 Group = mkIf cfg.nginx.enable "nginx";
782 };
783 };
784 extraConfig = let
785 image_dirs = flatten (
786 mapAttrsToList (distro: revs:
787 mapAttrsToList (rev: archs:
788 mapAttrsToList (arch: image:
789 pkgs.runCommandNoCC "buildsrht-images" { } ''
790 mkdir -p $out/${distro}/${rev}/${arch}
791 ln -s ${image}/*.qcow2 $out/${distro}/${rev}/${arch}/root.img.qcow2
792 ''
793 ) archs
794 ) revs
795 ) cfg.builds.images
796 );
797 image_dir_pre = pkgs.symlinkJoin {
798 name = "builds.sr.ht-worker-images-pre";
799 paths = image_dirs ++ [ "${pkgs.sourcehut.buildsrht}/lib/images" ];
800 };
801 image_dir = pkgs.runCommandNoCC "builds.sr.ht-worker-images" { } ''
802 mkdir -p $out/images
803 cp -Lr ${image_dir_pre}/* $out/images
804 '';
805 in {
806 users.users.${cfg.builds.user} = {
807 shell = pkgs.bash;
808 extraGroups = [ groups."sshsrht".name ];
809 };
810 users.groups.docker.members = mkIf cfg.builds.enableWorker [ cfg.builds.user ];
811
812 virtualisation.docker.enable = true;
813
814 # Hack to bypass this hack: https://git.sr.ht/~sircmpwn/core.sr.ht/tree/master/item/srht-update-profiles#L6
815 # FIXME: see if there is a better way than disabling preStart.
816 systemd.services.buildsrht.preStart = mkForce "";
817
818 services.sourcehut.settings = mkMerge [
819 { # Register the builds.sr.ht dispatcher
820 "git.sr.ht::dispatch"."/usr/bin/buildsrht-keys" =
821 mkDefault "${cfg.builds.user}:${cfg.builds.user}";
822 }
823 (mkIf cfg.builds.enableWorker {
824 # Default worker stores logs that are accessible via this address:port
825 "builds.sr.ht::worker".name = mkDefault "127.0.0.1:5020";
826 "builds.sr.ht::worker".buildlogs = mkDefault "/var/log/sourcehut/buildsrht";
827 "builds.sr.ht::worker".images = mkDefault "${image_dir}/images";
828 "builds.sr.ht::worker".controlcmd = mkDefault "${image_dir}/images/control";
829 "builds.sr.ht::worker".timeout = mkDefault "3m";
830 })
831 ];
832
833 services.nginx.virtualHosts."logs.${domain}" = mkIf (cfg.nginx.enable && cfg.builds.enableWorker) {
834 listen = with builtins;
835 let address = split ":" cfg.settings."builds.sr.ht::worker".name; in
836 [{ addr = elemAt address 0; port = lib.toInt (elemAt address 2); }];
837 locations."/logs".alias = cfg.settings."builds.sr.ht::worker".buildlogs + "/";
838 };
839 };
840 })
841 (import ./service.nix "dispatch" {
842 inherit configIniOfService;
843 port = 5005;
844 })
845 (import ./service.nix "git" (let
846 commonService = {
847 path = [ cfg.git.package ];
848 serviceConfig.BindPaths = [
849 "${cfg.settings."git.sr.ht".repos}:/var/lib/sourcehut/gitsrht/repos"
850 ];
851 serviceConfig.BindReadOnlyPaths = [
852 "${cfg.settings."git.sr.ht".post-update-script}:/var/lib/sourcehut/gitsrht/bin/post-update-script"
853 ];
854 }; in {
855 inherit configIniOfService;
856 commonService = mkMerge [ commonService {
857 serviceConfig.StateDirectory = [ "sourcehut/gitsrht/repos" ];
858 } ];
859 port = 5001;
860 webhooks.redisDatabase = 1;
861 extraTimers.gitsrht-periodic = {
862 OnCalendar = ["20min"];
863 };
864 extraConfig = {
865 # https://stackoverflow.com/questions/22314298/git-push-results-in-fatal-protocol-error-bad-line-length-character-this
866 # Probably could use gitsrht-shell if output is restricted to just parameters...
867 users.users.${cfg.git.user} = {
868 shell = pkgs.bash;
869 # Allow reading of ${users."sshsrht".home}/../config.ini
870 extraGroups = [ groups."sshsrht".name ];
871 home = users.sshsrht.home;
872 };
873
874 services.sourcehut.settings = {
875 # Register the git.sr.ht dispatcher
876 "git.sr.ht::dispatch"."/usr/bin/gitsrht-keys" =
877 mkDefault "${cfg.git.user}:${cfg.git.user}";
878 };
879
880 services.fcgiwrap.enable = mkIf cfg.nginx.enable true;
881 services.nginx.virtualHosts."git.${domain}" = mkIf cfg.nginx.enable {
882 extraConfig = ''
883 location = /authorize {
884 proxy_pass http://${cfg.listenAddress}:${toString cfg.git.port};
885 proxy_pass_request_body off;
886 proxy_set_header Content-Length "";
887 proxy_set_header X-Original-URI $request_uri;
888 }
889 location ~ ^/([^/]+)/([^/]+)/(HEAD|info/refs|objects/info/.*|git-upload-pack).*$ {
890 auth_request /authorize;
891 root ${cfg.settings."git.sr.ht".repos};
892 fastcgi_pass unix:/run/fcgiwrap.sock;
893 fastcgi_param SCRIPT_FILENAME ${cfg.git.package}/bin/git-http-backend;
894 fastcgi_param PATH_INFO $uri;
895 fastcgi_param GIT_PROJECT_ROOT $document_root;
896 fastcgi_param GIT_HTTP_EXPORT_ALL "";
897 fastcgi_read_timeout 500s;
898 include ${config.services.nginx.package}/conf/fastcgi_params;
899 gzip off;
900 }
901 '';
902 };
903 systemd.services.nginx = mkIf cfg.nginx.enable {
904 serviceConfig.BindReadOnlyPaths = [ cfg.settings."git.sr.ht".repos ];
905 };
906
907 systemd.services.sshd = commonService;
908 };
909 }))
910 (import ./service.nix "hg" (let
911 commonService = {
912 path = [ cfg.hg.package ];
913 serviceConfig.BindPaths = [
914 "${cfg.settings."hg.sr.ht".repos}:/var/lib/sourcehut/hgsrht/repos"
915 ];
916 serviceConfig.BindReadOnlyPaths = [
917 "${cfg.settings."ht.sr.ht".changegroup-script}:/var/lib/sourcehut/hgsrht/bin/changegroup-script"
918 ];
919 }; in {
920 inherit configIniOfService;
921 commonService = mkMerge [ commonService {
922 serviceConfig.StateDirectory = [ "sourcehut/hgsrht/repos" ];
923 } ];
924 port = 5010;
925 webhooks.redisDatabase = 8;
926 extraTimers.hgsrht-periodic = {
927 OnCalendar = ["20min"];
928 };
929 extraTimers.hgsrht-clonebundles = mkIf cfg.hg.cloneBundles {
930 OnCalendar = ["daily"];
931 AccuracySec = "1h";
932 };
933 extraConfig = {
934 users.users.${cfg.hg.user} = {
935 shell = pkgs.bash;
936 extraGroups = [ groups."sshsrht".name ];
937 };
938
939 services.sourcehut.settings = {
940 # Register the hg.sr.ht dispatcher
941 "hg.sr.ht::dispatch"."/usr/bin/hgsrht-keys" =
942 mkDefault "${cfg.hg.user}:${cfg.hg.user}";
943 };
944
945 systemd.services.sshd = commonService;
946 };
947 }))
948 (import ./service.nix "hub" {
949 inherit configIniOfService;
950 port = 5014;
951 extraConfig = {
952 services.nginx.virtualHosts."hub.${domain}" = mkIf cfg.nginx.enable {
953 serverAliases = [ domain ];
954 };
955 };
956 })
957 (import ./service.nix "lists" {
958 inherit configIniOfService;
959 port = 5006;
960 redisDatabase = 4;
961 webhooks.redisDatabase = 2;
962 extraServices.listssrht-lmtp = {
963 requires = [ "postfix.service" ];
964 unitConfig.JoinsNamespaceOf = optional cfg.postfix.enable "postfix.service";
965 serviceConfig.ExecStart = "${cfg.python}/bin/listssrht-lmtp";
966 # Avoid crashing: os.chown(sock, os.getuid(), sock_gid)
967 serviceConfig.PrivateUsers = mkForce false;
968 };
969 extraServices.listssrht-process = {
970 serviceConfig.ExecStart = "${cfg.python}/bin/celery -A listssrht.process worker --loglevel INFO --pool eventlet";
971 # Avoid crashing: os.getloadavg()
972 serviceConfig.ProcSubset = mkForce "all";
973 };
974 extraConfig = mkIf cfg.postfix.enable {
975 users.groups.${postfix.group}.members = [ cfg.lists.user ];
976 services.sourcehut.settings."lists.sr.ht::mail".sock-group = postfix.group;
977 services.postfix.transport = ''
978 lists.${domain} lmtp:unix:${cfg.settings."lists.sr.ht::worker".sock}
979 '';
980 };
981 })
982 (import ./service.nix "man" {
983 inherit configIniOfService;
984 port = 5004;
985 })
986 (import ./service.nix "meta" {
987 inherit configIniOfService;
988 port = 5000;
989 webhooks.redisDatabase = 6;
990 extraServices.metasrht-api = {
991 serviceConfig.Restart = "always";
992 serviceConfig.RestartSec = "2s";
993 preStart = concatStringsSep "\n\n" (attrValues (mapAttrs (k: s:
994 let srvMatch = builtins.match "^([a-z]*)\\.sr\\.ht$" k;
995 srv = head srvMatch;
996 oauthPath = "/var/lib/sourcehut/metasrht/${srv}.oauth";
997 in
998 # Configure client(s) as "preauthorized"
999 optionalString (srvMatch != null && cfg.${srv}.enable && ((s.oauth-client-id or null) != null)) ''
1000 if test ! -e "${oauthPath}" || [ "$(cat ${oauthPath})" != "${s.oauth-client-id}" ]; then
1001 # Configure ${srv}'s OAuth client as "preauthorized"
1002 psql '${cfg.meta.database}' \
1003 -c "UPDATE oauthclient SET preauthorized = true WHERE client_id = '${s.oauth-client-id}'"
1004
1005 printf "%s" "${s.oauth-client-id}" > "${oauthPath}"
1006 fi
1007 ''
1008 ) cfg.settings));
1009 path = [ config.services.postgresql.package ];
1010 serviceConfig.ExecStart = "${pkgs.sourcehut.metasrht}/bin/metasrht-api -b ${cfg.listenAddress}:${toString (cfg.meta.port + 100)}";
1011 };
1012 extraTimers.metasrht-daily = {
1013 OnCalendar = ["daily"];
1014 AccuracySec = "1h";
1015 };
1016 extraConfig = {
1017 assertions = [
1018 { assertion = let s = cfg.settings."meta.sr.ht::billing"; in
1019 s.enabled == "yes" -> (s.stripe-public-key != null && s.stripe-secret-key != null);
1020 message = "If meta.sr.ht::billing is enabled, the keys must be defined.";
1021 }
1022 ];
1023 environment.systemPackages = [
1024 (pkgs.writeShellScriptBin "metasrht-manageuser" ''
1025 set -eux
1026 test "$(${pkgs.coreutils}/bin/id -n -u)" = '${cfg.meta.user}' ||
1027 sudo -u '${cfg.meta.user}' "$0" "$@"
1028 # In order to load config.ini
1029 cd /run/sourcehut/metasrht ||
1030 cat <<EOF
1031 Please run: sudo systemctl start metasrht
1032 EOF
1033 ${cfg.python}/bin/metasrht-manageuser "$@"
1034 '')
1035 ];
1036 };
1037 })
1038 (import ./service.nix "pages" {
1039 inherit configIniOfService;
1040 port = 5112;
1041 #webhooks.redisDatabase = 9;
1042 mainService = let
1043 srvsrht = "pagessrht";
1044 version = pkgs.sourcehut.${srvsrht}.version;
1045 stateDir = "/var/lib/sourcehut/${srvsrht}";
1046 iniKey = "pages.sr.ht";
1047 in {
1048 preStart = mkBefore ''
1049 set -x
1050 # Use the /run/sourcehut/${srvsrht}/config.ini
1051 # installed by a previous ExecStartPre= in baseService
1052 cd /run/sourcehut/${srvsrht}
1053
1054 if test ! -e ${stateDir}/db; then
1055 ${postgresql.package}/bin/psql '${cfg.settings.${iniKey}.connection-string}' -f ${pkgs.sourcehut.pagessrht}/share/sql/schema.sql
1056 echo ${version} > ${stateDir}/db
1057 fi
1058
1059 ${optionalString cfg.settings.${iniKey}.migrate-on-upgrade ''
1060 # Just try all the migrations because they're not linked to the version
1061 for sql in ${pkgs.sourcehut.pagessrht}/share/sql/migrations/*.sql; do
1062 ${postgresql.package}/bin/psql '${cfg.settings.${iniKey}.connection-string}' -f "$sql" || true
1063 done
1064 ''}
1065
1066 # Disable webhook
1067 touch ${stateDir}/webhook
1068 '';
1069 serviceConfig = {
1070 ExecStart = mkForce "${pkgs.sourcehut.pagessrht}/bin/pages.sr.ht -b ${cfg.listenAddress}:${toString cfg.pages.port}";
1071 };
1072 };
1073 })
1074 (import ./service.nix "paste" {
1075 inherit configIniOfService;
1076 port = 5011;
1077 webhooks.redisDatabase = 5;
1078 })
1079 (import ./service.nix "todo" {
1080 inherit configIniOfService;
1081 port = 5003;
1082 webhooks.redisDatabase = 7;
1083 extraServices.todosrht-lmtp = {
1084 requires = [ "postfix.service" ];
1085 unitConfig.JoinsNamespaceOf = optional cfg.postfix.enable "postfix.service";
1086 serviceConfig.ExecStart = "${cfg.python}/bin/todosrht-lmtp";
1087 # Avoid crashing: os.chown(sock, os.getuid(), sock_gid)
1088 serviceConfig.PrivateUsers = mkForce false;
1089 };
1090 extraConfig = mkIf cfg.postfix.enable {
1091 users.groups.${postfix.group}.members = [ cfg.todo.user ];
1092 services.sourcehut.settings."todo.sr.ht::mail".sock-group = postfix.group;
1093 services.postfix.transport = ''
1094 todo.${domain} lmtp:unix:${cfg.settings."todo.sr.ht::mail".sock}
1095 '';
1096 };
1097 })
1098 (mkRenamedOptionModule [ "services" "sourcehut" "originBase" ]
1099 [ "services" "sourcehut" "settings" "sr.ht" "global-domain" ])
1100 (mkRenamedOptionModule [ "services" "sourcehut" "address" ]
1101 [ "services" "sourcehut" "listenAddress" ])
1102 ];
1103
1104 meta.doc = ./sourcehut.xml;
1105 meta.maintainers = with maintainers; [ julm tomberek ];
1106 }