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