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