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