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