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