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