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