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