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