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