{ config, pkgs, lib, ... }: with lib; let cfg = config.services.sourcehut; rcfg = config.services.redis; cfgIni = cfg.settings; settingsFormat = pkgs.formats.ini { listToValue = concatMapStringsSep "," (generators.mkValueStringDefault {}); mkKeyValue = k: v: if v == null then "" else generators.mkKeyValueDefault { mkValueString = v: if v == true then "yes" else if v == false then "no" else generators.mkValueStringDefault {} v; } "=" k v; }; commonServiceSettings = service: { origin = mkOption { description = "URL ${service}.sr.ht is being served at (protocol://domain)"; type = types.str; default = "https://${service}.${cfg.originBase}"; }; debug-host = mkOption { description = "Address to bind the debug server to."; type = types.str; default = "0.0.0.0"; }; debug-port = mkOption { description = "Port to bind the debug server to."; type = types.port; default = cfg.${service}.port; }; connection-string = mkOption { description = "SQLAlchemy connection string for the database."; type = types.str; default = "postgresql:///${cfg.${service}.database}?user=${cfg.${service}.user}&host=/var/run/postgresql"; }; migrate-on-upgrade = mkEnableOption "automatic migrations on package upgrade" // { default = true; }; oauth-client-id = mkOption { description = "${service}.sr.ht's OAuth client id for meta.sr.ht."; type = types.str; }; oauth-client-secret = mkOption { description = "${service}.sr.ht's OAuth client secret for meta.sr.ht."; type = types.str; }; }; # Specialized python containing all the modules python = pkgs.sourcehut.python.withPackages (ps: with ps; [ gunicorn eventlet # Sourcehut services srht buildsrht dispatchsrht gitsrht hgsrht hubsrht listssrht mansrht metasrht pastesrht todosrht ]); mkOptionNullOrStr = description: mkOption { inherit description; type = with types; nullOr str; default = null; }; in { imports = [ ./git.nix ./hg.nix ./hub.nix ./todo.nix ./man.nix ./meta.nix ./paste.nix ./builds.nix ./lists.nix ./dispatch.nix (mkRemovedOptionModule [ "services" "sourcehut" "nginx" "enable" ] '' The sourcehut module supports `nginx` as a local reverse-proxy by default and doesn't support other reverse-proxies officially. However it's possible to use an alternative reverse-proxy by * disabling nginx * adjusting the relevant settings for server addresses and ports directly Further details about this can be found in the `Sourcehut`-section of the NixOS-manual. '') ]; options.services.sourcehut = { enable = mkEnableOption '' sourcehut - git hosting, continuous integration, mailing list, ticket tracking, task dispatching, wiki and account management services ''; services = mkOption { type = types.nonEmptyListOf (types.enum [ "builds" "dispatch" "git" "hub" "hg" "lists" "man" "meta" "paste" "todo" ]); default = [ "man" "meta" "paste" ]; example = [ "builds" "dispatch" "git" "hub" "hg" "lists" "man" "meta" "paste" "todo" ]; description = '' Services to enable on the sourcehut network. ''; }; originBase = mkOption { type = types.str; default = with config.networking; hostName + lib.optionalString (domain != null) ".${domain}"; description = '' Host name used by reverse-proxy and for default settings. Will host services at git."''${originBase}". For example: git.sr.ht ''; }; address = mkOption { type = types.str; default = "127.0.0.1"; description = '' Address to bind to. ''; }; python = mkOption { internal = true; type = types.package; default = python; description = '' The python package to use. It should contain references to the *srht modules and also gunicorn. ''; }; settings = mkOption { type = lib.types.submodule { freeformType = settingsFormat.type; options."sr.ht" = { environment = mkOption { description = "Values other than \"production\" adds a banner to each page."; type = types.enum [ "development" "production" ]; default = "development"; }; global-domain = mkOptionNullOrStr "Global domain name."; owner-email = mkOption { description = "Owner's email."; type = types.str; default = "contact@example.com"; }; owner-name = mkOption { description = "Owner's name."; type = types.str; default = "John Doe"; }; secret-key = mkOptionNullOrStr "Secret key to encrypt session cookies with."; site-blurb = mkOption { description = "Blurb for your site."; type = types.str; default = "the hacker's forge"; }; site-info = mkOption { description = "The top-level info page for your site."; type = types.str; default = "https://sourcehut.org"; }; site-name = mkOption { description = "The name of your network of sr.ht-based sites."; type = types.str; default = "sourcehut"; }; source-url = mkOption { description = "The source code for your fork of sr.ht."; type = types.str; default = "https://git.sr.ht/~sircmpwn/srht"; }; }; options.mail = { smtp-host = mkOptionNullOrStr "Outgoing SMTP host."; smtp-port = mkOption { description = "Outgoing SMTP port."; type = with types; nullOr port; default = null; }; smtp-user = mkOptionNullOrStr "Outgoing SMTP user."; smtp-password = mkOptionNullOrStr "Outgoing SMTP password."; smtp-from = mkOptionNullOrStr "Outgoing SMTP FROM."; error-to = mkOptionNullOrStr "Address receiving application exceptions"; error-from = mkOptionNullOrStr "Address sending application exceptions"; pgp-privkey = mkOptionNullOrStr '' OpenPGP private key. Your PGP key information (DO NOT mix up pub and priv here) You must remove the password from your secret key, if present. You can do this with gpg --edit-key [key-id], then use the passwd command and do not enter a new password. ''; pgp-pubkey = mkOptionNullOrStr "OpenPGP public key."; pgp-key-id = mkOptionNullOrStr "OpenPGP key identifier."; }; options.webhooks = { private-key = mkOptionNullOrStr '' base64-encoded Ed25519 key for signing webhook payloads. This should be consistent for all *.sr.ht sites, as this key will be used to verify signatures from other sites in your network. Use the srht-webhook-keygen command to generate a key. ''; }; options."dispatch.sr.ht" = commonServiceSettings "dispatch" // { }; options."dispatch.sr.ht::github" = { oauth-client-id = mkOptionNullOrStr "OAuth client id."; oauth-client-secret = mkOptionNullOrStr "OAuth client secret."; }; options."dispatch.sr.ht::gitlab" = { enabled = mkEnableOption "GitLab integration"; canonical-upstream = mkOption { type = types.str; description = "Canonical upstream."; default = "gitlab.com"; }; repo-cache = mkOption { type = types.str; description = "Repository cache directory."; default = "./repo-cache"; }; "gitlab.com" = mkOption { type = types.str; description = "GitLab id and secret."; default = ""; example = "GitLab:application id:secret"; }; }; options."builds.sr.ht" = commonServiceSettings "builds" // { redis = mkOption { description = "The redis connection used for the celery worker."; type = types.str; default = "redis://${rcfg.bind}:${toString rcfg.port}/3"; }; shell = mkOption { description = "The shell used for ssh."; type = types.str; default = "runner-shell"; }; }; options."git.sr.ht" = commonServiceSettings "git" // { outgoing-domain = mkOption { description = "Outgoing domain."; type = types.str; default = "http://git.${cfg.originBase}"; }; post-update-script = mkOption { description = "A post-update script which is installed in every git repo."; type = types.str; default = "${pkgs.sourcehut.gitsrht}/bin/gitsrht-update-hook"; }; repos = mkOption { description = "Path to git repositories on disk."; type = types.str; default = "/var/lib/git"; }; webhooks = mkOption { description = "The redis connection used for the webhooks worker."; type = types.str; default = "redis://${rcfg.bind}:${toString rcfg.port}/1"; }; }; options."hg.sr.ht" = commonServiceSettings "hg" // { changegroup-script = mkOption { description = "A post-update script which is installed in every mercurial repo.."; type = types.str; default = "${cfg.python}/bin/hgsrht-hook-changegroup"; }; repos = mkOption { description = "Path to mercurial repositories on disk."; type = types.str; default = "/var/lib/hg"; }; srhtext = mkOptionNullOrStr '' Path to the srht mercurial extension (defaults to where the hgsrht code is) ''; clone_bundle_threshold = mkOption { description = ".hg/store size (in MB) past which the nightly job generates clone bundles."; type = types.ints.unsigned; default = 50; }; hg_ssh = mkOption { description = "Path to hg-ssh (if not in $PATH)."; type = types.str; default = "${pkgs.mercurial}/bin/hg-ssh"; }; webhooks = mkOption { description = "The redis connection used for the webhooks worker."; type = types.str; default = "redis://${rcfg.bind}:${toString rcfg.port}/1"; }; }; options."hub.sr.ht" = commonServiceSettings "hub" // { }; options."lists.sr.ht" = commonServiceSettings "lists" // { allow-new-lists = mkEnableOption "Allow creation of new lists."; network-key = mkOptionNullOrStr "Network key."; notify-from = mkOption { description = "Outgoing email for notifications generated by users."; type = types.str; default = "lists-notify@${cfg.originBase}"; }; posting-domain = mkOption { description = "Posting domain."; type = types.str; default = "lists.${cfg.originBase}"; }; redis = mkOption { description = "The redis connection used for the celery worker."; type = types.str; default = "redis://${rcfg.bind}:${toString rcfg.port}/4"; }; webhooks = mkOption { description = "The redis connection used for the webhooks worker."; type = types.str; default = "redis://${rcfg.bind}:${toString rcfg.port}/2"; }; }; options."lists.sr.ht::worker" = { reject-mimetypes = mkOption { type = with types; listOf str; default = ["text/html"]; }; reject-url = mkOption { description = "Reject URL."; default = "https://man.sr.ht/lists.sr.ht/etiquette.md"; type = types.str; }; sock = mkOption { description = '' Path for the lmtp daemon's unix socket. Direct incoming mail to this socket. Alternatively, specify IP:PORT and an SMTP server will be run instead. ''; type = types.str; default = "/tmp/lists.sr.ht-lmtp.sock"; }; sock-group = mkOption { description = '' The lmtp daemon will make the unix socket group-read/write for users in this group. ''; type = types.str; default = "postfix"; }; }; options."man.sr.ht" = commonServiceSettings "man" // { }; options."meta.sr.ht" = commonServiceSettings "meta" // { oauth-client-id = mkOptionNullOrStr "OAuth client id."; oauth-client-secret = mkOptionNullOrStr "OAuth client secret."; api-origin = mkOption { description = "Origin URL for API, 100 more than web."; type = types.str; default = "http://localhost:5100"; }; webhooks = mkOption { description = "The redis connection used for the webhooks worker."; type = types.str; default = "redis://${rcfg.bind}:${toString rcfg.port}/6"; }; welcome-emails = mkEnableOption "sending stock sourcehut welcome emails after signup"; }; options."meta.sr.ht::settings" = { registration = mkEnableOption "public registration"; onboarding-redirect = mkOption { description = "Where to redirect new users upon registration."; type = types.str; default = "https://meta.${cfg.originBase}"; }; user-invites = mkOption { description = '' How many invites each user is issued upon registration (only applicable if open registration is disabled). ''; type = types.ints.unsigned; default = 5; }; }; options."meta.sr.ht::aliases" = mkOption { description = "Aliases for the client IDs of commonly used OAuth clients."; type = with types; attrsOf int; default = {}; example = { "git.sr.ht" = 12345; }; }; options."meta.sr.ht::billing" = { enabled = mkEnableOption "the billing system"; stripe-public-key = mkOptionNullOrStr "Public key for Stripe. Get your keys at https://dashboard.stripe.com/account/apikeys"; stripe-secret-key = mkOptionNullOrStr "Secret key for Stripe. Get your keys at https://dashboard.stripe.com/account/apikeys"; }; options."paste.sr.ht" = commonServiceSettings "paste" // { webhooks = mkOption { type = types.str; default = "redis://${rcfg.bind}:${toString rcfg.port}/5"; }; }; options."todo.sr.ht" = commonServiceSettings "todo" // { network-key = mkOptionNullOrStr "Network key."; notify-from = mkOption { description = "Outgoing email for notifications generated by users."; type = types.str; default = "todo-notify@${cfg.originBase}"; }; webhooks = mkOption { description = "The redis connection used for the webhooks worker."; type = types.str; default = "redis://${rcfg.bind}:${toString rcfg.port}/7"; }; }; options."todo.sr.ht::mail" = { posting-domain = mkOption { description = "Posting domain."; type = types.str; default = "todo.${cfg.originBase}"; }; sock = mkOption { description = '' Path for the lmtp daemon's unix socket. Direct incoming mail to this socket. Alternatively, specify IP:PORT and an SMTP server will be run instead. ''; type = types.str; default = "/tmp/todo.sr.ht-lmtp.sock"; }; sock-group = mkOption { description = '' The lmtp daemon will make the unix socket group-read/write for users in this group. ''; type = types.str; default = "postfix"; }; }; }; default = { }; description = '' The configuration for the sourcehut network. ''; }; }; config = mkIf cfg.enable { assertions = [ { assertion = with cfgIni.webhooks; private-key != null && stringLength private-key == 44; message = "The webhook's private key must be defined and of a 44 byte length."; } { assertion = hasAttrByPath [ "meta.sr.ht" "origin" ] cfgIni && cfgIni."meta.sr.ht".origin != null; message = "meta.sr.ht's origin must be defined."; } ]; environment.etc."sr.ht/config.ini".source = settingsFormat.generate "sourcehut-config.ini" # Disabled services must be removed from the config # to be effectively disabled. (filterAttrs (k: v: let srv = builtins.match "^([a-z]*)\\.sr\\.ht(::.*)?$" k; in srv == null || elem (head srv) cfg.services ) cfg.settings); environment.systemPackages = [ pkgs.sourcehut.coresrht ]; # PostgreSQL server services.postgresql.enable = mkOverride 999 true; # Mail server services.postfix.enable = mkOverride 999 true; # Cron daemon services.cron.enable = mkOverride 999 true; # Redis server services.redis.enable = mkOverride 999 true; services.redis.bind = mkOverride 999 "127.0.0.1"; }; meta.doc = ./sourcehut.xml; meta.maintainers = with maintainers; [ tomberek ]; }