]> 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) 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" ''
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 };
424 repos = mkOption {
425 description = ''
426 Path to mercurial repositories on disk.
427 If changing the default, you must ensure that
428 the hgsrht's user as read and write access to it.
429 '';
430 type = types.str;
431 default = "/var/lib/sourcehut/hgsrht/repos";
432 };
433 srhtext = mkOptionNullOrStr ''
434 Path to the srht mercurial extension
435 (defaults to where the hgsrht code is)
436 '';
437 clone_bundle_threshold = mkOption {
438 description = ".hg/store size (in MB) past which the nightly job generates clone bundles.";
439 type = types.ints.unsigned;
440 default = 50;
441 };
442 hg_ssh = mkOption {
443 description = "Path to hg-ssh (if not in $PATH).";
444 type = types.str;
445 default = "${pkgs.mercurial}/bin/hg-ssh";
446 defaultText = "\${pkgs.mercurial}/bin/hg-ssh";
447 };
448 webhooks = mkOption {
449 description = "The redis connection used for the webhooks worker.";
450 type = types.str;
451 default = "redis://localhost:6379/8";
452 };
453 };
454
455 options."hub.sr.ht" = commonServiceSettings "hub" // {
456 };
457
458 options."lists.sr.ht" = commonServiceSettings "lists" // {
459 allow-new-lists = mkEnableOption "Allow creation of new lists.";
460 notify-from = mkOption {
461 description = "Outgoing email for notifications generated by users.";
462 type = types.str;
463 default = "lists-notify@localhost.localdomain";
464 };
465 posting-domain = mkOption {
466 description = "Posting domain.";
467 type = types.str;
468 default = "lists.localhost.localdomain";
469 };
470 redis = mkOption {
471 description = "The redis connection used for the celery worker.";
472 type = types.str;
473 default = "redis://localhost:6379/4";
474 };
475 webhooks = mkOption {
476 description = "The redis connection used for the webhooks worker.";
477 type = types.str;
478 default = "redis://localhost:6379/2";
479 };
480 };
481 options."lists.sr.ht::worker" = {
482 reject-mimetypes = mkOption {
483 description = ''
484 Comma-delimited list of Content-Types to reject. Messages with Content-Types
485 included in this list are rejected. Multipart messages are always supported,
486 and each part is checked against this list.
487
488 Uses fnmatch for wildcard expansion.
489 '';
490 type = with types; listOf str;
491 default = ["text/html"];
492 };
493 reject-url = mkOption {
494 description = "Reject URL.";
495 type = types.str;
496 default = "https://man.sr.ht/lists.sr.ht/etiquette.md";
497 };
498 sock = mkOption {
499 description = ''
500 Path for the lmtp daemon's unix socket. Direct incoming mail to this socket.
501 Alternatively, specify IP:PORT and an SMTP server will be run instead.
502 '';
503 type = types.str;
504 default = "/tmp/lists.sr.ht-lmtp.sock";
505 };
506 sock-group = mkOption {
507 description = ''
508 The lmtp daemon will make the unix socket group-read/write
509 for users in this group.
510 '';
511 type = types.str;
512 default = "postfix";
513 };
514 };
515
516 options."man.sr.ht" = commonServiceSettings "man" // {
517 };
518
519 options."meta.sr.ht" =
520 removeAttrs (commonServiceSettings "meta")
521 ["oauth-client-id" "oauth-client-secret"] // {
522 api-origin = mkOption {
523 description = "Origin URL for API, 100 more than web.";
524 type = types.str;
525 default = "http://localhost:5100";
526 };
527 webhooks = mkOption {
528 description = "The redis connection used for the webhooks worker.";
529 type = types.str;
530 default = "redis://localhost:6379/6";
531 };
532 welcome-emails = mkEnableOption "sending stock sourcehut welcome emails after signup";
533 };
534 options."meta.sr.ht::settings" = {
535 registration = mkEnableOption "public registration";
536 onboarding-redirect = mkOption {
537 description = "Where to redirect new users upon registration.";
538 type = types.str;
539 default = "https://meta.localhost.localdomain";
540 };
541 user-invites = mkOption {
542 description = ''
543 How many invites each user is issued upon registration
544 (only applicable if open registration is disabled).
545 '';
546 type = types.ints.unsigned;
547 default = 5;
548 };
549 };
550 options."meta.sr.ht::aliases" = mkOption {
551 description = "Aliases for the client IDs of commonly used OAuth clients.";
552 type = with types; attrsOf int;
553 default = {};
554 example = { "git.sr.ht" = 12345; };
555 };
556 options."meta.sr.ht::billing" = {
557 enabled = mkEnableOption "the billing system";
558 stripe-public-key = mkOptionNullOrStr "Public key for Stripe. Get your keys at https://dashboard.stripe.com/account/apikeys";
559 stripe-secret-key = mkOptionNullOrStr ''
560 An absolute file path (which should be outside the Nix-store)
561 to a secret key for Stripe. Get your keys at https://dashboard.stripe.com/account/apikeys
562 '' // {
563 apply = mapNullable (s: "<" + toString s);
564 };
565 };
566
567 options."pages.sr.ht" = commonServiceSettings "pages" // {
568 gemini-certs = mkOption {
569 description = ''
570 An absolute file path (which should be outside the Nix-store)
571 to Gemini certificates.
572 '';
573 type = with types; nullOr path;
574 default = null;
575 };
576 max-site-size = mkOption {
577 description = "Maximum size of any given site (post-gunzip), in MiB.";
578 type = types.int;
579 default = 1024;
580 };
581 user-domain = mkOption {
582 description = ''
583 Configures the user domain, if enabled.
584 All users are given &lt;username&gt;.this.domain.
585 '';
586 type = with types; nullOr str;
587 default = null;
588 };
589 };
590 options."pages.sr.ht::api" = {
591 };
592
593 options."paste.sr.ht" = commonServiceSettings "paste" // {
594 webhooks = mkOption {
595 description = "The redis connection used for the webhooks worker.";
596 type = types.str;
597 default = "redis://localhost:6379/5";
598 };
599 };
600
601 options."todo.sr.ht" = commonServiceSettings "todo" // {
602 notify-from = mkOption {
603 description = "Outgoing email for notifications generated by users.";
604 type = types.str;
605 default = "todo-notify@localhost.localdomain";
606 };
607 webhooks = mkOption {
608 description = "The redis connection used for the webhooks worker.";
609 type = types.str;
610 default = "redis://localhost:6379/7";
611 };
612 };
613 options."todo.sr.ht::mail" = {
614 posting-domain = mkOption {
615 description = "Posting domain.";
616 type = types.str;
617 default = "todo.localhost.localdomain";
618 };
619 sock = mkOption {
620 description = ''
621 Path for the lmtp daemon's unix socket. Direct incoming mail to this socket.
622 Alternatively, specify IP:PORT and an SMTP server will be run instead.
623 '';
624 type = types.str;
625 default = "/tmp/todo.sr.ht-lmtp.sock";
626 };
627 sock-group = mkOption {
628 description = ''
629 The lmtp daemon will make the unix socket group-read/write
630 for users in this group.
631 '';
632 type = types.str;
633 default = "postfix";
634 };
635 };
636 };
637 default = { };
638 description = ''
639 The configuration for the sourcehut network.
640 '';
641 };
642
643 builds = {
644 enableWorker = mkEnableOption "worker for builds.sr.ht";
645
646 images = mkOption {
647 type = with types; attrsOf (attrsOf (attrsOf package));
648 default = { };
649 example = lib.literalExample ''(let
650 # Pinning unstable to allow usage with flakes and limit rebuilds.
651 pkgs_unstable = builtins.fetchGit {
652 url = "https://github.com/NixOS/nixpkgs";
653 rev = "ff96a0fa5635770390b184ae74debea75c3fd534";
654 ref = "nixos-unstable";
655 };
656 image_from_nixpkgs = (import ("${pkgs.sourcehut.buildsrht}/lib/images/nixos/image.nix") {
657 pkgs = (import pkgs_unstable {});
658 });
659 in
660 {
661 nixos.unstable.x86_64 = image_from_nixpkgs;
662 }
663 )'';
664 description = ''
665 Images for builds.sr.ht. Each package should be distro.release.arch and point to a /nix/store/package/root.img.qcow2.
666 '';
667 };
668 };
669
670 git = {
671 package = mkOption {
672 type = types.package;
673 default = pkgs.git;
674 example = literalExample "pkgs.gitFull";
675 description = ''
676 Git package for git.sr.ht. This can help silence collisions.
677 '';
678 };
679 };
680
681 hg = {
682 package = mkOption {
683 type = types.package;
684 default = pkgs.mercurial;
685 description = ''
686 Mercurial package for hg.sr.ht. This can help silence collisions.
687 '';
688 };
689 cloneBundles = mkOption {
690 type = types.bool;
691 default = false;
692 description = ''
693 Generate clonebundles (which require more disk space but dramatically speed up cloning large repositories).
694 '';
695 };
696 };
697 };
698
699 config = mkIf cfg.enable (mkMerge [
700 {
701 environment.systemPackages = [ pkgs.sourcehut.coresrht ];
702
703 services.sourcehut.settings = {
704 "git.sr.ht".outgoing-domain = mkDefault "https://git.${domain}";
705 "lists.sr.ht".notify-from = mkDefault "lists-notify@${domain}";
706 "lists.sr.ht".posting-domain = mkDefault "lists.${domain}";
707 "meta.sr.ht::settings".onboarding-redirect = mkDefault "https://meta.${domain}";
708 "todo.sr.ht".notify-from = mkDefault "todo-notify@${domain}";
709 "todo.sr.ht::mail".posting-domain = mkDefault "todo.${domain}";
710 };
711 }
712 (mkIf cfg.postgresql.enable {
713 services.postgresql.enable = true;
714 })
715 (mkIf cfg.postfix.enable {
716 services.postfix.enable = true;
717 # Needed for sharing the LMTP sockets with JoinsNamespaceOf=
718 systemd.services.postfix.serviceConfig.PrivateTmp = true;
719 })
720 (mkIf cfg.redis.enable {
721 services.redis.enable = true;
722 services.sourcehut.settings."sr.ht".redis-host = mkDefault ("redis://localhost:6379/" + toString cfg.redis.firstDatabase);
723 })
724 (mkIf cfg.nginx.enable {
725 services.nginx.enable = true;
726 })
727 (mkIf (cfg.builds.enable || cfg.git.enable || cfg.hg.enable) {
728 services.openssh = {
729 # Note that sshd will continue to honor AuthorizedKeysFile
730 authorizedKeysCommand = ''/etc/ssh/srht-dispatch "%u" "%h" "%t" "%k"'';
731 # The sshsrht-dispatch user needs:
732 # 1. to read ${users."sshsrht".home}/../config.ini,
733 # 2. to access the redis server in redis-host,
734 # 3. to access the postgresql server in the service's connection-string,
735 # 4. to query metasrht-api (through the HTTP API).
736 # Note that *srht-{dispatch,keys,shell,update-hook} will likely fail
737 # to write their log on /var/log with that user, and will fallback to stderr,
738 # making their log visible in sshd's log when sshd is in debug mode (-d).
739 # Alternatively, you can touch and chown sshsrht /var/log/gitsrht-{dispatch,keys,shell,update-hook}
740 # during your debug.
741 authorizedKeysCommandUser = users."sshsrht".name;
742 extraConfig = ''
743 PermitUserEnvironment SRHT_*
744 '';
745 };
746 environment.etc."ssh/srht-dispatch" = {
747 # sshd_config(5): The program must be owned by root, not writable by group or others
748 mode = "0755";
749 source = pkgs.writeShellScript "srht-dispatch" ''
750 set -e
751 cd ${users."sshsrht".home}
752 exec ${cfg.python}/bin/gitsrht-dispatch "$@"
753 '';
754 };
755 systemd.services.sshd = let configIni = configIniOfService "ssh"; in {
756 #path = optional cfg.git.enable [ cfg.git.package ];
757 restartTriggers = [ configIni ];
758 serviceConfig = {
759 RuntimeDirectory = [ "sourcehut/sshsrht/subdir" ];
760 BindReadOnlyPaths =
761 # Note that those /usr/bin/* paths are hardcoded in multiple places in *.sr.ht,
762 # for instance to get the user from the [*.sr.ht::dispatch] settings.
763 optionals cfg.builds.enable [
764 "${pkgs.sourcehut.buildsrht}/bin/buildsrht-keys:/usr/bin/buildsrht-keys"
765 "${pkgs.sourcehut.buildsrht}/bin/master-shell:/usr/bin/master-shell"
766 "${pkgs.sourcehut.buildsrht}/bin/runner-shell:/usr/bin/runner-shell"
767 ] ++
768 optionals cfg.git.enable [
769 "${pkgs.sourcehut.gitsrht}/bin/gitsrht-keys:/usr/bin/gitsrht-keys"
770 "${pkgs.sourcehut.gitsrht}/bin/gitsrht-shell:/usr/bin/gitsrht-shell"
771 ] ++
772 optionals cfg.hg.enable [
773 "${pkgs.sourcehut.hgsrht}/bin/hgsrht-keys:/usr/bin/htsrht-keys"
774 "${pkgs.sourcehut.hgsrht}/bin/hgsrht-shell:/usr/bin/htsrht-shell"
775 ];
776 ExecStartPre = mkBefore [("+"+pkgs.writeShellScript "sshsrht-credentials" ''
777 # Replace values begining with a '<' by the content of the file whose name is after.
778 ${pkgs.gawk}/bin/gawk '{ if (match($0,/^([^=]+=)<(.+)/,m)) { getline f < m[2]; print m[1] f } else print $0 }' ${configIni} |
779 install -o ${users."sshsrht".name} -g ${groups."sshsrht".name} -m 440 \
780 /dev/stdin ${users."sshsrht".home}/../config.ini
781 '')];
782 };
783 };
784 users = {
785 users."sshsrht" = {
786 isSystemUser = true;
787 # srht-dispatch, *srht-keys, and *srht-shell
788 # look up in ../config.ini from this directory;
789 # that config.ini being set in *srht.service's ExecStartPre=
790 home = "/run/sourcehut/sshsrht/subdir";
791 group =
792 # Unfortunately, AuthorizedKeysCommandUser does not honor supplementary groups,
793 # hence the main group is used.
794 if cfg.postgresql.enable
795 && hasSuffix "0" (postgresql.settings.unix_socket_permissions or "")
796 then groups.postgres.name
797 else groups.nogroup.name;
798 description = "sourcehut user for sshd's AuthorizedKeysCommandUser";
799 };
800 groups."sshsrht" = {};
801 };
802 })
803 ]);
804
805 imports = [
806 (import ./service.nix "builds" {
807 inherit configIniOfService;
808 srvsrht = "buildsrht";
809 port = 5002;
810 redisDatabase = 3;
811 extraServices.buildsrht-worker = let
812 qemuPackage = pkgs.qemu_kvm;
813 serviceName = "buildsrht-worker";
814 statePath = "/var/lib/sourcehut/${serviceName}";
815 in mkIf cfg.builds.enableWorker {
816 partOf = [ "buildsrht.service" ];
817 path = [ pkgs.openssh pkgs.docker ];
818 preStart = ''
819 set -x
820 if test -z "$(docker images -q qemu:latest 2>/dev/null)" \
821 || test "$(cat ${statePath}/docker-image-qemu)" != "${qemuPackage.version}"
822 then
823 # Create and import qemu:latest image for docker
824 ${pkgs.dockerTools.streamLayeredImage {
825 name = "qemu";
826 tag = "latest";
827 contents = [ qemuPackage ];
828 }} | docker load
829 # Mark down current package version
830 echo '${qemuPackage.version}' >${statePath}/docker-image-qemu
831 fi
832 '';
833 serviceConfig = {
834 ExecStart = "${pkgs.sourcehut.buildsrht}/bin/builds.sr.ht-worker";
835 RuntimeDirectory = [ "sourcehut/${serviceName}/subdir" ];
836 # builds.sr.ht-worker looks up ../config.ini
837 WorkingDirectory = "-"+"/run/sourcehut/${serviceName}/subdir";
838 StateDirectory = [ "sourcehut/${serviceName}" ];
839 Group = mkIf cfg.nginx.enable "nginx";
840 };
841 };
842 extraConfig = let
843 image_dirs = flatten (
844 mapAttrsToList (distro: revs:
845 mapAttrsToList (rev: archs:
846 mapAttrsToList (arch: image:
847 pkgs.runCommand "buildsrht-images" { } ''
848 mkdir -p $out/${distro}/${rev}/${arch}
849 ln -s ${image}/*.qcow2 $out/${distro}/${rev}/${arch}/root.img.qcow2
850 ''
851 ) archs
852 ) revs
853 ) cfg.builds.images
854 );
855 image_dir_pre = pkgs.symlinkJoin {
856 name = "builds.sr.ht-worker-images-pre";
857 paths = image_dirs;
858 # FIXME: not working, apparently because ubuntu/latest is a broken link
859 # ++ [ "${pkgs.sourcehut.buildsrht}/lib/images" ];
860 };
861 image_dir = pkgs.runCommand "builds.sr.ht-worker-images" { } ''
862 mkdir -p $out/images
863 cp -Lr ${image_dir_pre}/* $out/images
864 '';
865 in mkMerge [
866 {
867 users.users.${cfg.builds.user} = {
868 shell = pkgs.bash;
869 extraGroups = [ groups."sshsrht".name ];
870 };
871
872 virtualisation.docker.enable = true;
873
874 services.sourcehut.settings = mkMerge [
875 { # Register the builds.sr.ht dispatcher
876 "git.sr.ht::dispatch"."/usr/bin/buildsrht-keys" =
877 mkDefault "${cfg.builds.user}:${cfg.builds.user}";
878 }
879 (mkIf cfg.builds.enableWorker {
880 "builds.sr.ht::worker".shell = "/usr/bin/runner-shell";
881 "builds.sr.ht::worker".images = mkDefault "${image_dir}/images";
882 "builds.sr.ht::worker".controlcmd = mkDefault "${image_dir}/images/control";
883 })
884 ];
885 }
886 (mkIf cfg.builds.enableWorker {
887 users.groups = {
888 docker.members = [ cfg.builds.user ];
889 };
890 })
891 (mkIf (cfg.builds.enableWorker && cfg.nginx.enable) {
892 systemd.services.nginx = {
893 serviceConfig.BindReadOnlyPaths = [
894 "${cfg.settings."builds.sr.ht::worker".buildlogs}:/var/log/sourcehut/buildsrht/logs"
895 ];
896 };
897 services.nginx.virtualHosts."logs.${domain}" = {
898 /*
899 listen = with builtins;
900 # FIXME: not compatible with IPv6
901 let address = split ":" cfg.settings."builds.sr.ht::worker".name; in
902 [{ addr = elemAt address 0; port = lib.toInt (elemAt address 2); }];
903 */
904 locations."/logs/".alias = "/var/log/sourcehut/buildsrht/logs/";
905 };
906 })
907 ];
908 })
909 (import ./service.nix "dispatch" {
910 inherit configIniOfService;
911 port = 5005;
912 })
913 (import ./service.nix "git" (let
914 commonService = {
915 path = [ cfg.git.package ];
916 serviceConfig.BindPaths = [
917 "${cfg.settings."git.sr.ht".repos}:/var/lib/sourcehut/gitsrht/repos"
918 ];
919 serviceConfig.BindReadOnlyPaths = [
920 "${cfg.settings."git.sr.ht".post-update-script}:/var/lib/sourcehut/gitsrht/bin/post-update-script"
921 ];
922 }; in {
923 inherit configIniOfService;
924 commonService = mkMerge [ commonService {
925 serviceConfig.StateDirectory = [ "sourcehut/gitsrht/repos" ];
926 } ];
927 port = 5001;
928 webhooks.redisDatabase = 1;
929 extraTimers.gitsrht-periodic = {
930 OnCalendar = ["20min"];
931 };
932 extraConfig = {
933 # https://stackoverflow.com/questions/22314298/git-push-results-in-fatal-protocol-error-bad-line-length-character-this
934 # Probably could use gitsrht-shell if output is restricted to just parameters...
935 users.users.${cfg.git.user} = {
936 shell = pkgs.bash;
937 # Allow reading of ${users."sshsrht".home}/../config.ini
938 extraGroups = [ groups."sshsrht".name ];
939 home = users.sshsrht.home;
940 };
941
942 services.sourcehut.settings = {
943 # Register the git.sr.ht dispatcher
944 "git.sr.ht::dispatch"."/usr/bin/gitsrht-keys" =
945 mkDefault "${cfg.git.user}:${cfg.git.user}";
946 };
947
948 services.fcgiwrap.enable = mkIf cfg.nginx.enable true;
949 services.nginx.virtualHosts."git.${domain}" = mkIf cfg.nginx.enable {
950 extraConfig = ''
951 location = /authorize {
952 proxy_pass http://${cfg.listenAddress}:${toString cfg.git.port};
953 proxy_pass_request_body off;
954 proxy_set_header Content-Length "";
955 proxy_set_header X-Original-URI $request_uri;
956 }
957 location ~ ^/([^/]+)/([^/]+)/(HEAD|info/refs|objects/info/.*|git-upload-pack).*$ {
958 auth_request /authorize;
959 root ${cfg.settings."git.sr.ht".repos};
960 fastcgi_pass unix:/run/fcgiwrap.sock;
961 fastcgi_param SCRIPT_FILENAME ${cfg.git.package}/bin/git-http-backend;
962 fastcgi_param PATH_INFO $uri;
963 fastcgi_param GIT_PROJECT_ROOT $document_root;
964 fastcgi_param GIT_HTTP_EXPORT_ALL "";
965 fastcgi_read_timeout 500s;
966 include ${config.services.nginx.package}/conf/fastcgi_params;
967 gzip off;
968 }
969 '';
970 };
971 systemd.services.nginx = mkIf cfg.nginx.enable {
972 serviceConfig.BindReadOnlyPaths = [ cfg.settings."git.sr.ht".repos ];
973 };
974
975 systemd.services.sshd = commonService;
976 };
977 }))
978 (import ./service.nix "hg" (let
979 commonService = {
980 path = [ cfg.hg.package ];
981 serviceConfig.BindPaths = [
982 "${cfg.settings."hg.sr.ht".repos}:/var/lib/sourcehut/hgsrht/repos"
983 ];
984 serviceConfig.BindReadOnlyPaths = [
985 "${cfg.settings."ht.sr.ht".changegroup-script}:/var/lib/sourcehut/hgsrht/bin/changegroup-script"
986 ];
987 }; in {
988 inherit configIniOfService;
989 commonService = mkMerge [ commonService {
990 serviceConfig.StateDirectory = [ "sourcehut/hgsrht/repos" ];
991 } ];
992 port = 5010;
993 webhooks.redisDatabase = 8;
994 extraTimers.hgsrht-periodic = {
995 OnCalendar = ["20min"];
996 };
997 extraTimers.hgsrht-clonebundles = mkIf cfg.hg.cloneBundles {
998 OnCalendar = ["daily"];
999 AccuracySec = "1h";
1000 };
1001 extraConfig = {
1002 users.users.${cfg.hg.user} = {
1003 shell = pkgs.bash;
1004 extraGroups = [ groups."sshsrht".name ];
1005 };
1006
1007 services.sourcehut.settings = {
1008 # Register the hg.sr.ht dispatcher
1009 "hg.sr.ht::dispatch"."/usr/bin/hgsrht-keys" =
1010 mkDefault "${cfg.hg.user}:${cfg.hg.user}";
1011 };
1012
1013 systemd.services.sshd = commonService;
1014 };
1015 }))
1016 (import ./service.nix "hub" {
1017 inherit configIniOfService;
1018 port = 5014;
1019 extraConfig = {
1020 services.nginx = mkIf cfg.nginx.enable {
1021 virtualHosts."hub.${domain}" = {
1022 serverAliases = [ domain ];
1023 };
1024 };
1025 };
1026 })
1027 (import ./service.nix "lists" {
1028 inherit configIniOfService;
1029 port = 5006;
1030 redisDatabase = 4;
1031 webhooks.redisDatabase = 2;
1032 extraServices.listssrht-lmtp = {
1033 requires = [ "postfix.service" ];
1034 unitConfig.JoinsNamespaceOf = optional cfg.postfix.enable "postfix.service";
1035 serviceConfig.ExecStart = "${cfg.python}/bin/listssrht-lmtp";
1036 # Avoid crashing: os.chown(sock, os.getuid(), sock_gid)
1037 serviceConfig.PrivateUsers = mkForce false;
1038 };
1039 extraServices.listssrht-process = {
1040 serviceConfig.ExecStart = "${cfg.python}/bin/celery -A listssrht.process worker --loglevel INFO --pool eventlet";
1041 # Avoid crashing: os.getloadavg()
1042 serviceConfig.ProcSubset = mkForce "all";
1043 };
1044 extraConfig = mkIf cfg.postfix.enable {
1045 users.groups.${postfix.group}.members = [ cfg.lists.user ];
1046 services.sourcehut.settings."lists.sr.ht::mail".sock-group = postfix.group;
1047 services.postfix.transport = ''
1048 lists.${domain} lmtp:unix:${cfg.settings."lists.sr.ht::worker".sock}
1049 '';
1050 };
1051 })
1052 (import ./service.nix "man" {
1053 inherit configIniOfService;
1054 port = 5004;
1055 })
1056 (import ./service.nix "meta" {
1057 inherit configIniOfService;
1058 port = 5000;
1059 webhooks.redisDatabase = 6;
1060 extraServices.metasrht-api = {
1061 serviceConfig.Restart = "always";
1062 serviceConfig.RestartSec = "2s";
1063 preStart = "set -x\n" + concatStringsSep "\n\n" (attrValues (mapAttrs (k: s:
1064 let srvMatch = builtins.match "^([a-z]*)\\.sr\\.ht$" k;
1065 srv = head srvMatch;
1066 in
1067 # Configure client(s) as "preauthorized"
1068 optionalString (srvMatch != null && cfg.${srv}.enable && ((s.oauth-client-id or null) != null)) ''
1069 # Configure ${srv}'s OAuth client as "preauthorized"
1070 ${postgresql.package}/bin/psql '${cfg.settings."meta.sr.ht".connection-string}' \
1071 -c "UPDATE oauthclient SET preauthorized = true WHERE client_id = '${s.oauth-client-id}'"
1072 ''
1073 ) cfg.settings));
1074 serviceConfig.ExecStart = "${pkgs.sourcehut.metasrht}/bin/metasrht-api -b ${cfg.listenAddress}:${toString (cfg.meta.port + 100)}";
1075 };
1076 extraTimers.metasrht-daily = {
1077 OnCalendar = ["daily"];
1078 AccuracySec = "1h";
1079 };
1080 extraConfig = {
1081 assertions = [
1082 { assertion = let s = cfg.settings."meta.sr.ht::billing"; in
1083 s.enabled == "yes" -> (s.stripe-public-key != null && s.stripe-secret-key != null);
1084 message = "If meta.sr.ht::billing is enabled, the keys must be defined.";
1085 }
1086 ];
1087 environment.systemPackages = [
1088 (pkgs.writeShellScriptBin "metasrht-manageuser" ''
1089 set -eux
1090 test "$(${pkgs.coreutils}/bin/id -n -u)" = '${cfg.meta.user}' ||
1091 sudo -u '${cfg.meta.user}' "$0" "$@"
1092 # In order to load config.ini
1093 cd /run/sourcehut/metasrht ||
1094 cat <<EOF
1095 Please run: sudo systemctl start metasrht
1096 EOF
1097 ${cfg.python}/bin/metasrht-manageuser "$@"
1098 '')
1099 ];
1100 };
1101 })
1102 (import ./service.nix "pages" {
1103 inherit configIniOfService;
1104 port = 5112;
1105 #webhooks.redisDatabase = 9;
1106 mainService = let
1107 srvsrht = "pagessrht";
1108 version = pkgs.sourcehut.${srvsrht}.version;
1109 stateDir = "/var/lib/sourcehut/${srvsrht}";
1110 iniKey = "pages.sr.ht";
1111 in {
1112 preStart = mkBefore ''
1113 set -x
1114 # Use the /run/sourcehut/${srvsrht}/config.ini
1115 # installed by a previous ExecStartPre= in baseService
1116 cd /run/sourcehut/${srvsrht}
1117
1118 if test ! -e ${stateDir}/db; then
1119 ${postgresql.package}/bin/psql '${cfg.settings.${iniKey}.connection-string}' -f ${pkgs.sourcehut.pagessrht}/share/sql/schema.sql
1120 echo ${version} >${stateDir}/db
1121 fi
1122
1123 ${optionalString cfg.settings.${iniKey}.migrate-on-upgrade ''
1124 # Just try all the migrations because they're not linked to the version
1125 for sql in ${pkgs.sourcehut.pagessrht}/share/sql/migrations/*.sql; do
1126 ${postgresql.package}/bin/psql '${cfg.settings.${iniKey}.connection-string}' -f "$sql" || true
1127 done
1128 ''}
1129
1130 # Disable webhook
1131 touch ${stateDir}/webhook
1132 '';
1133 serviceConfig = {
1134 ExecStart = mkForce "${pkgs.sourcehut.pagessrht}/bin/pages.sr.ht -b ${cfg.listenAddress}:${toString cfg.pages.port}";
1135 };
1136 };
1137 })
1138 (import ./service.nix "paste" {
1139 inherit configIniOfService;
1140 port = 5011;
1141 webhooks.redisDatabase = 5;
1142 })
1143 (import ./service.nix "todo" {
1144 inherit configIniOfService;
1145 port = 5003;
1146 webhooks.redisDatabase = 7;
1147 extraServices.todosrht-lmtp = {
1148 requires = [ "postfix.service" ];
1149 unitConfig.JoinsNamespaceOf = optional cfg.postfix.enable "postfix.service";
1150 serviceConfig.ExecStart = "${cfg.python}/bin/todosrht-lmtp";
1151 # Avoid crashing: os.chown(sock, os.getuid(), sock_gid)
1152 serviceConfig.PrivateUsers = mkForce false;
1153 };
1154 extraConfig = mkIf cfg.postfix.enable {
1155 users.groups.${postfix.group}.members = [ cfg.todo.user ];
1156 services.sourcehut.settings."todo.sr.ht::mail".sock-group = postfix.group;
1157 services.postfix.transport = ''
1158 todo.${domain} lmtp:unix:${cfg.settings."todo.sr.ht::mail".sock}
1159 '';
1160 };
1161 })
1162 (mkRenamedOptionModule [ "services" "sourcehut" "originBase" ]
1163 [ "services" "sourcehut" "settings" "sr.ht" "global-domain" ])
1164 (mkRenamedOptionModule [ "services" "sourcehut" "address" ]
1165 [ "services" "sourcehut" "listenAddress" ])
1166 ];
1167
1168 meta.doc = ./sourcehut.xml;
1169 meta.maintainers = with maintainers; [ julm tomberek ];
1170 }