]> Git — Sourcephile - sourcephile-nix.git/blob - nixos/modules/services/misc/sourcehut/default.nix
sourcehut: handle lists in settings
[sourcephile-nix.git] / nixos / modules / services / misc / sourcehut / default.nix
1 { config, pkgs, lib, ... }:
2
3 with lib;
4 let
5 cfg = config.services.sourcehut;
6 rcfg = config.services.redis;
7 cfgIni = cfg.settings;
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 commonServiceSettings = service: {
20 origin = mkOption {
21 description = "URL ${service}.sr.ht is being served at (protocol://domain)";
22 type = types.str;
23 default = "https://${service}.${cfg.originBase}";
24 };
25 debug-host = mkOption {
26 description = "Address to bind the debug server to.";
27 type = types.str;
28 default = "0.0.0.0";
29 };
30 debug-port = mkOption {
31 description = "Port to bind the debug server to.";
32 type = types.port;
33 default = cfg.${service}.port;
34 };
35 connection-string = mkOption {
36 description = "SQLAlchemy connection string for the database.";
37 type = types.str;
38 default = "postgresql:///${cfg.${service}.database}?user=${cfg.${service}.user}&host=/var/run/postgresql";
39 };
40 migrate-on-upgrade = mkEnableOption "automatic migrations on package upgrade";
41 oauth-client-id = mkOptionNullOrStr "OAUTH client id for meta.sr.ht.";
42 oauth-client-secret = mkOptionNullOrStr "OAUTH client secret for meta.sr.ht.";
43 };
44
45 # Specialized python containing all the modules
46 python = pkgs.sourcehut.python.withPackages (ps: with ps; [
47 gunicorn
48 eventlet
49 # Sourcehut services
50 srht
51 buildsrht
52 dispatchsrht
53 gitsrht
54 hgsrht
55 hubsrht
56 listssrht
57 mansrht
58 metasrht
59 pastesrht
60 todosrht
61 ]);
62 mkOptionNullOrStr = description: mkOption {
63 inherit description;
64 type = with types; nullOr str;
65 default = null;
66 };
67 in
68 {
69 imports =
70 [
71 ./git.nix
72 ./hg.nix
73 ./hub.nix
74 ./todo.nix
75 ./man.nix
76 ./meta.nix
77 ./paste.nix
78 ./builds.nix
79 ./lists.nix
80 ./dispatch.nix
81 (mkRemovedOptionModule [ "services" "sourcehut" "nginx" "enable" ] ''
82 The sourcehut module supports `nginx` as a local reverse-proxy by default and doesn't
83 support other reverse-proxies officially.
84
85 However it's possible to use an alternative reverse-proxy by
86
87 * disabling nginx
88 * adjusting the relevant settings for server addresses and ports directly
89
90 Further details about this can be found in the `Sourcehut`-section of the NixOS-manual.
91 '')
92 ];
93
94 options.services.sourcehut = {
95 enable = mkEnableOption ''
96 sourcehut - git hosting, continuous integration, mailing list, ticket tracking,
97 task dispatching, wiki and account management services
98 '';
99
100 services = mkOption {
101 type = types.nonEmptyListOf (types.enum [ "builds" "dispatch" "git" "hub" "hg" "lists" "man" "meta" "paste" "todo" ]);
102 default = [ "man" "meta" "paste" ];
103 example = [ "builds" "dispatch" "git" "hub" "hg" "lists" "man" "meta" "paste" "todo" ];
104 description = ''
105 Services to enable on the sourcehut network.
106 '';
107 };
108
109 originBase = mkOption {
110 type = types.str;
111 default = with config.networking; hostName + lib.optionalString (domain != null) ".${domain}";
112 description = ''
113 Host name used by reverse-proxy and for default settings. Will host services at git."''${originBase}". For example: git.sr.ht
114 '';
115 };
116
117 address = mkOption {
118 type = types.str;
119 default = "127.0.0.1";
120 description = ''
121 Address to bind to.
122 '';
123 };
124
125 python = mkOption {
126 internal = true;
127 type = types.package;
128 default = python;
129 description = ''
130 The python package to use. It should contain references to the *srht modules and also
131 gunicorn.
132 '';
133 };
134
135 settings = mkOption {
136 type = lib.types.submodule {
137 freeformType = settingsFormat.type;
138 options."sr.ht" = {
139 environment = mkOption {
140 description = "Values other than \"production\" adds a banner to each page.";
141 type = types.enum [ "development" "production" ];
142 default = "development";
143 };
144 global-domain = mkOptionNullOrStr "Global domain name.";
145 owner-email = mkOption {
146 description = "Owner's email.";
147 type = types.str;
148 default = "contact@example.com";
149 };
150 owner-name = mkOption {
151 description = "Owner's name.";
152 type = types.str;
153 default = "John Doe";
154 };
155 secret-key = mkOptionNullOrStr "Secret key to encrypt session cookies with.";
156 site-blurb = mkOption {
157 description = "Blurb for your site.";
158 type = types.str;
159 default = "the hacker's forge";
160 };
161 site-info = mkOption {
162 description = "The top-level info page for your site.";
163 type = types.str;
164 default = "https://sourcehut.org";
165 };
166 site-name = mkOption {
167 description = "The name of your network of sr.ht-based sites.";
168 type = types.str;
169 default = "sourcehut";
170 };
171 source-url = mkOption {
172 description = "The source code for your fork of sr.ht.";
173 type = types.str;
174 default = "https://git.sr.ht/~sircmpwn/srht";
175 };
176 };
177 options.mail = {
178 smtp-host = mkOptionNullOrStr "Outgoing SMTP host.";
179 smtp-port = mkOption {
180 description = "Outgoing SMTP port.";
181 type = with types; nullOr port;
182 default = null;
183 };
184 smtp-user = mkOptionNullOrStr "Outgoing SMTP user.";
185 smtp-password = mkOptionNullOrStr "Outgoing SMTP password.";
186 smtp-from = mkOptionNullOrStr "Outgoing SMTP FROM.";
187 error-to = mkOptionNullOrStr "Address receiving application exceptions";
188 error-from = mkOptionNullOrStr "Address sending application exceptions";
189 pgp-privkey = mkOptionNullOrStr ''
190 OpenPGP private key.
191
192 Your PGP key information (DO NOT mix up pub and priv here)
193 You must remove the password from your secret key, if present.
194 You can do this with <code>gpg --edit-key [key-id]</code>, then use the <code>passwd</code> command and do not enter a new password.
195 '';
196 pgp-pubkey = mkOptionNullOrStr "OpenPGP public key.";
197 pgp-key-id = mkOptionNullOrStr "OpenPGP key identifier.";
198 };
199 options.webhooks = {
200 private-key = mkOptionNullOrStr ''
201 base64-encoded Ed25519 key for signing webhook payloads.
202 This should be consistent for all *.sr.ht sites,
203 as this key will be used to verify signatures
204 from other sites in your network.
205 Use the <code>srht-webhook-keygen</code> command to generate a key.
206 '';
207 };
208 options."dispatch.sr.ht" = commonServiceSettings "dispatch";
209 options."dispatch.sr.ht::github" = {
210 oauth-client-id = mkOptionNullOrStr "OAUTH client id.";
211 oauth-client-secret = mkOptionNullOrStr "OAUTH client secret.";
212 };
213 options."dispatch.sr.ht::gitlab" = {
214 enabled = mkEnableOption "GitLab integration";
215 canonical-upstream = mkOption {
216 type = types.str;
217 description = "Canonical upstream.";
218 default = "gitlab.com";
219 };
220 repo-cache = mkOption {
221 type = types.str;
222 description = "Repository cache directory.";
223 default = "./repo-cache";
224 };
225 "gitlab.com" = mkOption {
226 type = types.str;
227 description = "GitLab id and secret.";
228 default = "";
229 example = "GitLab:application id:secret";
230 };
231 };
232 options."builds.sr.ht" = commonServiceSettings "builds" // {
233 redis = mkOption {
234 description = "The redis connection used for the celery worker.";
235 type = types.str;
236 default = "redis://${rcfg.bind}:${toString rcfg.port}/3";
237 };
238 shell = mkOption {
239 description = "The shell used for ssh.";
240 type = types.str;
241 default = "runner-shell";
242 };
243 };
244 options."git.sr.ht" = commonServiceSettings "git" // {
245 outgoing-domain = mkOption {
246 description = "Outgoing domain.";
247 type = types.str;
248 default = "http://git.${cfg.originBase}";
249 };
250 post-update-script = mkOption {
251 description = "A post-update script which is installed in every git repo.";
252 type = types.str;
253 default = "${pkgs.sourcehut.gitsrht}/bin/gitsrht-update-hook";
254 };
255 repos = mkOption {
256 description = "Path to git repositories on disk.";
257 type = types.str;
258 default = "/var/lib/git";
259 };
260 webhooks = mkOption {
261 description = "The redis connection used for the webhooks worker.";
262 type = types.str;
263 default = "redis://${rcfg.bind}:${toString rcfg.port}/1";
264 };
265 };
266 options."hg.sr.ht" = commonServiceSettings "hg" // {
267 changegroup-script = mkOption {
268 description = "A post-update script which is installed in every mercurial repo..";
269 type = types.str;
270 default = "${cfg.python}/bin/hgsrht-hook-changegroup";
271 };
272 repos = mkOption {
273 description = "Path to mercurial repositories on disk.";
274 type = types.str;
275 default = "/var/lib/hg";
276 };
277 srhtext = mkOptionNullOrStr ''
278 Path to the srht mercurial extension
279 (defaults to where the hgsrht code is)
280 '';
281 clone_bundle_threshold = mkOption {
282 description = ".hg/store size (in MB) past which the nightly job generates clone bundles.";
283 type = types.ints.unsigned;
284 default = 50;
285 };
286 hg_ssh = mkOption {
287 description = "Path to hg-ssh (if not in $PATH).";
288 type = types.str;
289 default = "${pkgs.mercurial}/bin/hg-ssh";
290 };
291 webhooks = mkOption {
292 description = "The redis connection used for the webhooks worker.";
293 type = types.str;
294 default = "redis://${rcfg.bind}:${toString rcfg.port}/1";
295 };
296 };
297 options."hub.sr.ht" = commonServiceSettings "hub" // {
298 };
299 options."lists.sr.ht" = commonServiceSettings "lists" // {
300 allow-new-lists = mkEnableOption "Allow creation of new lists.";
301 network-key = mkOptionNullOrStr "Network key.";
302 notify-from = mkOption {
303 description = "Outgoing email for notifications generated by users.";
304 type = types.str;
305 default = "lists-notify@${cfg.originBase}";
306 };
307 posting-domain = mkOption {
308 description = "Posting domain.";
309 type = types.str;
310 default = "lists.${cfg.originBase}";
311 };
312 redis = mkOption {
313 description = "The redis connection used for the celery worker.";
314 type = types.str;
315 default = "redis://${rcfg.bind}:${toString rcfg.port}/4";
316 };
317 webhooks = mkOption {
318 description = "The redis connection used for the webhooks worker.";
319 type = types.str;
320 default = "redis://${rcfg.bind}:${toString rcfg.port}/2";
321 };
322 };
323 options."lists.sr.ht::worker" = {
324 reject-mimetypes = mkOption {
325 type = with types; listOf str;
326 default = ["text/html"];
327 };
328 reject-url = mkOption {
329 description = "Reject URL.";
330 default = "https://man.sr.ht/lists.sr.ht/etiquette.md";
331 type = types.str;
332 };
333 sock = mkOption {
334 description = ''
335 Path for the lmtp daemon's unix socket. Direct incoming mail to this socket.
336 Alternatively, specify IP:PORT and an SMTP server will be run instead.
337 '';
338 type = types.str;
339 default = "/tmp/lists.sr.ht-lmtp.sock";
340 };
341 sock-group = mkOption {
342 description = ''
343 The lmtp daemon will make the unix socket group-read/write
344 for users in this group.
345 '';
346 type = types.str;
347 default = "postfix";
348 };
349 };
350 options."man.sr.ht" = commonServiceSettings "man" // {
351 };
352 options."meta.sr.ht" = commonServiceSettings "meta" // {
353 api-origin = mkOption {
354 description = "Origin URL for API, 100 more than web.";
355 type = types.str;
356 default = "http://localhost:5100";
357 };
358 webhooks = mkOption {
359 description = "The redis connection used for the webhooks worker.";
360 type = types.str;
361 default = "redis://${rcfg.bind}:${toString rcfg.port}/6";
362 };
363 welcome-emails = mkEnableOption "sending stock sourcehut welcome emails after signup";
364 };
365 options."meta.sr.ht::settings" = {
366 registration = mkEnableOption "public registration";
367 onboarding-redirect = mkOption {
368 description = "Where to redirect new users upon registration.";
369 type = types.str;
370 default = "https://meta.${cfg.originBase}";
371 };
372 user-invites = mkOption {
373 description = ''
374 How many invites each user is issued upon registration
375 (only applicable if open registration is disabled).
376 '';
377 type = types.ints.unsigned;
378 default = 5;
379 };
380 };
381 options."meta.sr.ht::aliases" = mkOption {
382 description = "Aliases for the client IDs of commonly used OAuth clients.";
383 type = with types; attrsOf int;
384 default = {};
385 example = { "git.sr.ht" = 12345; };
386 };
387 options."meta.sr.ht::billing" = {
388 enabled = mkEnableOption "the billing system";
389 stripe-public-key = mkOptionNullOrStr "Public key for Stripe. Get your keys at https://dashboard.stripe.com/account/apikeys";
390 stripe-secret-key = mkOptionNullOrStr "Secret key for Stripe. Get your keys at https://dashboard.stripe.com/account/apikeys";
391 };
392 options."paste.sr.ht" = commonServiceSettings "paste" // {
393 webhooks = mkOption {
394 type = types.str;
395 default = "redis://${rcfg.bind}:${toString rcfg.port}/5";
396 };
397 };
398 options."todo.sr.ht" = commonServiceSettings "todo" // {
399 network-key = mkOptionNullOrStr "Network key.";
400 notify-from = mkOption {
401 description = "Outgoing email for notifications generated by users.";
402 type = types.str;
403 default = "todo-notify@${cfg.originBase}";
404 };
405 webhooks = mkOption {
406 description = "The redis connection used for the webhooks worker.";
407 type = types.str;
408 default = "redis://${rcfg.bind}:${toString rcfg.port}/7";
409 };
410 };
411 options."todo.sr.ht::mail" = {
412 posting-domain = mkOption {
413 description = "Posting domain.";
414 type = types.str;
415 default = "todo.${cfg.originBase}";
416 };
417 sock = mkOption {
418 description = ''
419 Path for the lmtp daemon's unix socket. Direct incoming mail to this socket.
420 Alternatively, specify IP:PORT and an SMTP server will be run instead.
421 '';
422 type = types.str;
423 default = "/tmp/todo.sr.ht-lmtp.sock";
424 };
425 sock-group = mkOption {
426 description = ''
427 The lmtp daemon will make the unix socket group-read/write
428 for users in this group.
429 '';
430 type = types.str;
431 default = "postfix";
432 };
433 };
434 };
435 default = { };
436 description = ''
437 The configuration for the sourcehut network.
438 '';
439 };
440 };
441
442 config = mkIf cfg.enable {
443 assertions =
444 [
445 {
446 assertion = with cfgIni.webhooks; private-key != null && stringLength private-key == 44;
447 message = "The webhook's private key must be defined and of a 44 byte length.";
448 }
449
450 {
451 assertion = hasAttrByPath [ "meta.sr.ht" "origin" ] cfgIni && cfgIni."meta.sr.ht".origin != null;
452 message = "meta.sr.ht's origin must be defined.";
453 }
454 ];
455
456 environment.etc."sr.ht/config.ini".source =
457 settingsFormat.generate "sourcehut-config.ini"
458 # Disabled services must be removed from the config
459 # to be effectively disabled.
460 (filterAttrs (k: v:
461 let srv = builtins.match "^([a-z]*)\\.sr\\.ht(::.*)?$" k; in
462 srv == null || elem (head srv) cfg.services
463 ) cfg.settings);
464
465 environment.systemPackages = [ pkgs.sourcehut.coresrht ];
466
467 # PostgreSQL server
468 services.postgresql.enable = mkOverride 999 true;
469 # Mail server
470 services.postfix.enable = mkOverride 999 true;
471 # Cron daemon
472 services.cron.enable = mkOverride 999 true;
473 # Redis server
474 services.redis.enable = mkOverride 999 true;
475 services.redis.bind = mkOverride 999 "127.0.0.1";
476
477 };
478 meta.doc = ./sourcehut.xml;
479 meta.maintainers = with maintainers; [ tomberek ];
480 }