nixos/sourcehut: init (#113244)

* nixos/sourcehut: init

* sourcehut: default nginx setup

* sourcehut: documentation

* sourcehut: re-structure settings

* sourcehut: tests

* nixos/sourcehut: adopt StateDirectory

* Apply suggestions from code review

Co-authored-by: Aaron Andersen <aaron@fosslib.net>
Co-authored-by: Thibaut Marty <github@thibautmarty.fr>
Co-authored-by: malte-v <34393802+malte-v@users.noreply.github.com>

* nixos/sourcehut: PR suggestions

* nixos/sourcehut: malte-v patch

* nixos/sourcehut: add base virtualhost

* nixos/sourcehut: remove superfluous key

* nixos/sourcehut: use default from cfg

* nixos/sourcehut: use originBase for logs

* nixos/sourcehut: use toPythonApplication in systemPackages

* nixos/sourcehut: directly use ExecStart

* nixos/sourcehut: update docs

Co-authored-by: Aaron Andersen <aaron@fosslib.net>
Co-authored-by: Thibaut Marty <github@thibautmarty.fr>
Co-authored-by: malte-v <34393802+malte-v@users.noreply.github.com>

authored by

tomberek
Aaron Andersen
Thibaut Marty
malte-v
and committed by
GitHub
157aee00 81e982a7

+2072
+1
nixos/modules/module-list.nix
··· 553 ./services/misc/siproxd.nix 554 ./services/misc/snapper.nix 555 ./services/misc/sonarr.nix 556 ./services/misc/spice-vdagentd.nix 557 ./services/misc/ssm-agent.nix 558 ./services/misc/sssd.nix
··· 553 ./services/misc/siproxd.nix 554 ./services/misc/snapper.nix 555 ./services/misc/sonarr.nix 556 + ./services/misc/sourcehut 557 ./services/misc/spice-vdagentd.nix 558 ./services/misc/ssm-agent.nix 559 ./services/misc/sssd.nix
+220
nixos/modules/services/misc/sourcehut/builds.nix
···
··· 1 + { config, lib, pkgs, ... }: 2 + 3 + with lib; 4 + let 5 + cfg = config.services.sourcehut; 6 + scfg = cfg.builds; 7 + rcfg = config.services.redis; 8 + iniKey = "builds.sr.ht"; 9 + 10 + drv = pkgs.sourcehut.buildsrht; 11 + in 12 + { 13 + options.services.sourcehut.builds = { 14 + user = mkOption { 15 + type = types.str; 16 + default = "buildsrht"; 17 + description = '' 18 + User for builds.sr.ht. 19 + ''; 20 + }; 21 + 22 + port = mkOption { 23 + type = types.port; 24 + default = 5002; 25 + description = '' 26 + Port on which the "builds" module should listen. 27 + ''; 28 + }; 29 + 30 + database = mkOption { 31 + type = types.str; 32 + default = "builds.sr.ht"; 33 + description = '' 34 + PostgreSQL database name for builds.sr.ht. 35 + ''; 36 + }; 37 + 38 + statePath = mkOption { 39 + type = types.path; 40 + default = "${cfg.statePath}/buildsrht"; 41 + description = '' 42 + State path for builds.sr.ht. 43 + ''; 44 + }; 45 + 46 + enableWorker = mkOption { 47 + type = types.bool; 48 + default = false; 49 + description = '' 50 + Run workers for builds.sr.ht. 51 + Perform manually on machine: `cd ${scfg.statePath}/images; docker build -t qemu -f qemu/Dockerfile .` 52 + ''; 53 + }; 54 + 55 + images = mkOption { 56 + type = types.attrsOf (types.attrsOf (types.attrsOf types.package)); 57 + default = { }; 58 + example = lib.literalExample ''(let 59 + # Pinning unstable to allow usage with flakes and limit rebuilds. 60 + pkgs_unstable = builtins.fetchGit { 61 + url = "https://github.com/NixOS/nixpkgs"; 62 + rev = "ff96a0fa5635770390b184ae74debea75c3fd534"; 63 + ref = "nixos-unstable"; 64 + }; 65 + image_from_nixpkgs = pkgs_unstable: (import ("${pkgs.sourcehut.buildsrht}/lib/images/nixos/image.nix") { 66 + pkgs = (import pkgs_unstable {}); 67 + }); 68 + in 69 + { 70 + nixos.unstable.x86_64 = image_from_nixpkgs pkgs_unstable; 71 + } 72 + )''; 73 + description = '' 74 + Images for builds.sr.ht. Each package should be distro.release.arch and point to a /nix/store/package/root.img.qcow2. 75 + ''; 76 + }; 77 + 78 + }; 79 + 80 + config = with scfg; let 81 + image_dirs = lib.lists.flatten ( 82 + lib.attrsets.mapAttrsToList 83 + (distro: revs: 84 + lib.attrsets.mapAttrsToList 85 + (rev: archs: 86 + lib.attrsets.mapAttrsToList 87 + (arch: image: 88 + pkgs.runCommandNoCC "buildsrht-images" { } '' 89 + mkdir -p $out/${distro}/${rev}/${arch} 90 + ln -s ${image}/*.qcow2 $out/${distro}/${rev}/${arch}/root.img.qcow2 91 + '') 92 + archs) 93 + revs) 94 + scfg.images); 95 + image_dir_pre = pkgs.symlinkJoin { 96 + name = "builds.sr.ht-worker-images-pre"; 97 + paths = image_dirs ++ [ 98 + "${pkgs.sourcehut.buildsrht}/lib/images" 99 + ]; 100 + }; 101 + image_dir = pkgs.runCommandNoCC "builds.sr.ht-worker-images" { } '' 102 + mkdir -p $out/images 103 + cp -Lr ${image_dir_pre}/* $out/images 104 + ''; 105 + in 106 + lib.mkIf (cfg.enable && elem "builds" cfg.services) { 107 + users = { 108 + users = { 109 + "${user}" = { 110 + isSystemUser = true; 111 + group = user; 112 + extraGroups = lib.optionals cfg.builds.enableWorker [ "docker" ]; 113 + description = "builds.sr.ht user"; 114 + }; 115 + }; 116 + 117 + groups = { 118 + "${user}" = { }; 119 + }; 120 + }; 121 + 122 + services.postgresql = { 123 + authentication = '' 124 + local ${database} ${user} trust 125 + ''; 126 + ensureDatabases = [ database ]; 127 + ensureUsers = [ 128 + { 129 + name = user; 130 + ensurePermissions = { "DATABASE \"${database}\"" = "ALL PRIVILEGES"; }; 131 + } 132 + ]; 133 + }; 134 + 135 + systemd = { 136 + tmpfiles.rules = [ 137 + "d ${statePath} 0755 ${user} ${user} -" 138 + ] ++ (lib.optionals cfg.builds.enableWorker 139 + [ "d ${statePath}/logs 0775 ${user} ${user} - -" ] 140 + ); 141 + 142 + services = { 143 + buildsrht = import ./service.nix { inherit config pkgs lib; } scfg drv iniKey 144 + { 145 + after = [ "postgresql.service" "network.target" ]; 146 + requires = [ "postgresql.service" ]; 147 + wantedBy = [ "multi-user.target" ]; 148 + 149 + description = "builds.sr.ht website service"; 150 + 151 + serviceConfig.ExecStart = "${cfg.python}/bin/gunicorn ${drv.pname}.app:app -b ${cfg.address}:${toString port}"; 152 + 153 + # Hack to bypass this hack: https://git.sr.ht/~sircmpwn/core.sr.ht/tree/master/item/srht-update-profiles#L6 154 + } // { preStart = " "; }; 155 + 156 + buildsrht-worker = { 157 + enable = scfg.enableWorker; 158 + after = [ "postgresql.service" "network.target" ]; 159 + requires = [ "postgresql.service" ]; 160 + wantedBy = [ "multi-user.target" ]; 161 + partOf = [ "buildsrht.service" ]; 162 + description = "builds.sr.ht worker service"; 163 + path = [ pkgs.openssh pkgs.docker ]; 164 + serviceConfig = { 165 + Type = "simple"; 166 + User = user; 167 + Group = "nginx"; 168 + Restart = "always"; 169 + }; 170 + serviceConfig.ExecStart = "${pkgs.sourcehut.buildsrht}/bin/builds.sr.ht-worker"; 171 + }; 172 + }; 173 + }; 174 + 175 + services.sourcehut.settings = { 176 + # URL builds.sr.ht is being served at (protocol://domain) 177 + "builds.sr.ht".origin = mkDefault "http://builds.${cfg.originBase}"; 178 + # Address and port to bind the debug server to 179 + "builds.sr.ht".debug-host = mkDefault "0.0.0.0"; 180 + "builds.sr.ht".debug-port = mkDefault port; 181 + # Configures the SQLAlchemy connection string for the database. 182 + "builds.sr.ht".connection-string = mkDefault "postgresql:///${database}?user=${user}&host=/var/run/postgresql"; 183 + # Set to "yes" to automatically run migrations on package upgrade. 184 + "builds.sr.ht".migrate-on-upgrade = mkDefault "yes"; 185 + # builds.sr.ht's OAuth client ID and secret for meta.sr.ht 186 + # Register your client at meta.example.org/oauth 187 + "builds.sr.ht".oauth-client-id = mkDefault null; 188 + "builds.sr.ht".oauth-client-secret = mkDefault null; 189 + # The redis connection used for the celery worker 190 + "builds.sr.ht".redis = mkDefault "redis://${rcfg.bind}:${toString rcfg.port}/3"; 191 + # The shell used for ssh 192 + "builds.sr.ht".shell = mkDefault "runner-shell"; 193 + # Register the builds.sr.ht dispatcher 194 + "git.sr.ht::dispatch".${builtins.unsafeDiscardStringContext "${pkgs.sourcehut.buildsrht}/bin/buildsrht-keys"} = mkDefault "${user}:${user}"; 195 + 196 + # Location for build logs, images, and control command 197 + } // lib.attrsets.optionalAttrs scfg.enableWorker { 198 + # Default worker stores logs that are accessible via this address:port 199 + "builds.sr.ht::worker".name = mkDefault "127.0.0.1:5020"; 200 + "builds.sr.ht::worker".buildlogs = mkDefault "${scfg.statePath}/logs"; 201 + "builds.sr.ht::worker".images = mkDefault "${image_dir}/images"; 202 + "builds.sr.ht::worker".controlcmd = mkDefault "${image_dir}/images/control"; 203 + "builds.sr.ht::worker".timeout = mkDefault "3m"; 204 + }; 205 + 206 + services.nginx.virtualHosts."logs.${cfg.originBase}" = 207 + if scfg.enableWorker then { 208 + listen = with builtins; let address = split ":" cfg.settings."builds.sr.ht::worker".name; 209 + in [{ addr = elemAt address 0; port = lib.toInt (elemAt address 2); }]; 210 + locations."/logs".root = "${scfg.statePath}"; 211 + } else { }; 212 + 213 + services.nginx.virtualHosts."builds.${cfg.originBase}" = { 214 + forceSSL = true; 215 + locations."/".proxyPass = "http://${cfg.address}:${toString port}"; 216 + locations."/query".proxyPass = "http://${cfg.address}:${toString (port + 100)}"; 217 + locations."/static".root = "${pkgs.sourcehut.buildsrht}/${pkgs.sourcehut.python.sitePackages}/buildsrht"; 218 + }; 219 + }; 220 + }
+198
nixos/modules/services/misc/sourcehut/default.nix
···
··· 1 + { config, pkgs, lib, ... }: 2 + 3 + with lib; 4 + let 5 + cfg = config.services.sourcehut; 6 + cfgIni = cfg.settings; 7 + settingsFormat = pkgs.formats.ini { }; 8 + 9 + # Specialized python containing all the modules 10 + python = pkgs.sourcehut.python.withPackages (ps: with ps; [ 11 + gunicorn 12 + # Sourcehut services 13 + srht 14 + buildsrht 15 + dispatchsrht 16 + gitsrht 17 + hgsrht 18 + hubsrht 19 + listssrht 20 + mansrht 21 + metasrht 22 + pastesrht 23 + todosrht 24 + ]); 25 + in 26 + { 27 + imports = 28 + [ 29 + ./git.nix 30 + ./hg.nix 31 + ./hub.nix 32 + ./todo.nix 33 + ./man.nix 34 + ./meta.nix 35 + ./paste.nix 36 + ./builds.nix 37 + ./lists.nix 38 + ./dispatch.nix 39 + (mkRemovedOptionModule [ "services" "sourcehut" "nginx" "enable" ] '' 40 + The sourcehut module supports `nginx` as a local reverse-proxy by default and doesn't 41 + support other reverse-proxies officially. 42 + 43 + However it's possible to use an alternative reverse-proxy by 44 + 45 + * disabling nginx 46 + * adjusting the relevant settings for server addresses and ports directly 47 + 48 + Further details about this can be found in the `Sourcehut`-section of the NixOS-manual. 49 + '') 50 + ]; 51 + 52 + options.services.sourcehut = { 53 + enable = mkOption { 54 + type = types.bool; 55 + default = false; 56 + description = '' 57 + Enable sourcehut - git hosting, continuous integration, mailing list, ticket tracking, 58 + task dispatching, wiki and account management services 59 + ''; 60 + }; 61 + 62 + services = mkOption { 63 + type = types.nonEmptyListOf (types.enum [ "builds" "dispatch" "git" "hub" "hg" "lists" "man" "meta" "paste" "todo" ]); 64 + default = [ "man" "meta" "paste" ]; 65 + example = [ "builds" "dispatch" "git" "hub" "hg" "lists" "man" "meta" "paste" "todo" ]; 66 + description = '' 67 + Services to enable on the sourcehut network. 68 + ''; 69 + }; 70 + 71 + originBase = mkOption { 72 + type = types.str; 73 + default = with config.networking; hostName + lib.optionalString (domain != null) ".${domain}"; 74 + description = '' 75 + Host name used by reverse-proxy and for default settings. Will host services at git."''${originBase}". For example: git.sr.ht 76 + ''; 77 + }; 78 + 79 + address = mkOption { 80 + type = types.str; 81 + default = "127.0.0.1"; 82 + description = '' 83 + Address to bind to. 84 + ''; 85 + }; 86 + 87 + python = mkOption { 88 + internal = true; 89 + type = types.package; 90 + default = python; 91 + description = '' 92 + The python package to use. It should contain references to the *srht modules and also 93 + gunicorn. 94 + ''; 95 + }; 96 + 97 + statePath = mkOption { 98 + type = types.path; 99 + default = "/var/lib/sourcehut"; 100 + description = '' 101 + Root state path for the sourcehut network. If left as the default value 102 + this directory will automatically be created before the sourcehut server 103 + starts, otherwise the sysadmin is responsible for ensuring the 104 + directory exists with appropriate ownership and permissions. 105 + ''; 106 + }; 107 + 108 + settings = mkOption { 109 + type = lib.types.submodule { 110 + freeformType = settingsFormat.type; 111 + }; 112 + default = { }; 113 + description = '' 114 + The configuration for the sourcehut network. 115 + ''; 116 + }; 117 + }; 118 + 119 + config = mkIf cfg.enable { 120 + assertions = 121 + [ 122 + { 123 + assertion = with cfgIni.webhooks; private-key != null && stringLength private-key == 44; 124 + message = "The webhook's private key must be defined and of a 44 byte length."; 125 + } 126 + 127 + { 128 + assertion = hasAttrByPath [ "meta.sr.ht" "origin" ] cfgIni && cfgIni."meta.sr.ht".origin != null; 129 + message = "meta.sr.ht's origin must be defined."; 130 + } 131 + ]; 132 + 133 + virtualisation.docker.enable = true; 134 + environment.etc."sr.ht/config.ini".source = 135 + settingsFormat.generate "sourcehut-config.ini" (mapAttrsRecursive 136 + ( 137 + path: v: if v == null then "" else v 138 + ) 139 + cfg.settings); 140 + 141 + environment.systemPackages = [ pkgs.sourcehut.coresrht ]; 142 + 143 + # PostgreSQL server 144 + services.postgresql.enable = mkOverride 999 true; 145 + # Mail server 146 + services.postfix.enable = mkOverride 999 true; 147 + # Cron daemon 148 + services.cron.enable = mkOverride 999 true; 149 + # Redis server 150 + services.redis.enable = mkOverride 999 true; 151 + services.redis.bind = mkOverride 999 "127.0.0.1"; 152 + 153 + services.sourcehut.settings = { 154 + # The name of your network of sr.ht-based sites 155 + "sr.ht".site-name = mkDefault "sourcehut"; 156 + # The top-level info page for your site 157 + "sr.ht".site-info = mkDefault "https://sourcehut.org"; 158 + # {{ site-name }}, {{ site-blurb }} 159 + "sr.ht".site-blurb = mkDefault "the hacker's forge"; 160 + # If this != production, we add a banner to each page 161 + "sr.ht".environment = mkDefault "development"; 162 + # Contact information for the site owners 163 + "sr.ht".owner-name = mkDefault "Drew DeVault"; 164 + "sr.ht".owner-email = mkDefault "sir@cmpwn.com"; 165 + # The source code for your fork of sr.ht 166 + "sr.ht".source-url = mkDefault "https://git.sr.ht/~sircmpwn/srht"; 167 + # A secret key to encrypt session cookies with 168 + "sr.ht".secret-key = mkDefault null; 169 + "sr.ht".global-domain = mkDefault null; 170 + 171 + # Outgoing SMTP settings 172 + mail.smtp-host = mkDefault null; 173 + mail.smtp-port = mkDefault null; 174 + mail.smtp-user = mkDefault null; 175 + mail.smtp-password = mkDefault null; 176 + mail.smtp-from = mkDefault null; 177 + # Application exceptions are emailed to this address 178 + mail.error-to = mkDefault null; 179 + mail.error-from = mkDefault null; 180 + # Your PGP key information (DO NOT mix up pub and priv here) 181 + # You must remove the password from your secret key, if present. 182 + # You can do this with gpg --edit-key [key-id], then use the passwd 183 + # command and do not enter a new password. 184 + mail.pgp-privkey = mkDefault null; 185 + mail.pgp-pubkey = mkDefault null; 186 + mail.pgp-key-id = mkDefault null; 187 + 188 + # base64-encoded Ed25519 key for signing webhook payloads. This should be 189 + # consistent for all *.sr.ht sites, as we'll use this key to verify signatures 190 + # from other sites in your network. 191 + # 192 + # Use the srht-webhook-keygen command to generate a key. 193 + webhooks.private-key = mkDefault null; 194 + }; 195 + }; 196 + meta.doc = ./sourcehut.xml; 197 + meta.maintainers = with maintainers; [ tomberek ]; 198 + }
+125
nixos/modules/services/misc/sourcehut/dispatch.nix
···
··· 1 + { config, lib, pkgs, ... }: 2 + 3 + with lib; 4 + let 5 + cfg = config.services.sourcehut; 6 + cfgIni = cfg.settings; 7 + scfg = cfg.dispatch; 8 + iniKey = "dispatch.sr.ht"; 9 + 10 + drv = pkgs.sourcehut.dispatchsrht; 11 + in 12 + { 13 + options.services.sourcehut.dispatch = { 14 + user = mkOption { 15 + type = types.str; 16 + default = "dispatchsrht"; 17 + description = '' 18 + User for dispatch.sr.ht. 19 + ''; 20 + }; 21 + 22 + port = mkOption { 23 + type = types.port; 24 + default = 5005; 25 + description = '' 26 + Port on which the "dispatch" module should listen. 27 + ''; 28 + }; 29 + 30 + database = mkOption { 31 + type = types.str; 32 + default = "dispatch.sr.ht"; 33 + description = '' 34 + PostgreSQL database name for dispatch.sr.ht. 35 + ''; 36 + }; 37 + 38 + statePath = mkOption { 39 + type = types.path; 40 + default = "${cfg.statePath}/dispatchsrht"; 41 + description = '' 42 + State path for dispatch.sr.ht. 43 + ''; 44 + }; 45 + }; 46 + 47 + config = with scfg; lib.mkIf (cfg.enable && elem "dispatch" cfg.services) { 48 + 49 + users = { 50 + users = { 51 + "${user}" = { 52 + isSystemUser = true; 53 + group = user; 54 + description = "dispatch.sr.ht user"; 55 + }; 56 + }; 57 + 58 + groups = { 59 + "${user}" = { }; 60 + }; 61 + }; 62 + 63 + services.postgresql = { 64 + authentication = '' 65 + local ${database} ${user} trust 66 + ''; 67 + ensureDatabases = [ database ]; 68 + ensureUsers = [ 69 + { 70 + name = user; 71 + ensurePermissions = { "DATABASE \"${database}\"" = "ALL PRIVILEGES"; }; 72 + } 73 + ]; 74 + }; 75 + 76 + systemd = { 77 + tmpfiles.rules = [ 78 + "d ${statePath} 0750 ${user} ${user} -" 79 + ]; 80 + 81 + services.dispatchsrht = import ./service.nix { inherit config pkgs lib; } scfg drv iniKey { 82 + after = [ "postgresql.service" "network.target" ]; 83 + requires = [ "postgresql.service" ]; 84 + wantedBy = [ "multi-user.target" ]; 85 + 86 + description = "dispatch.sr.ht website service"; 87 + 88 + serviceConfig.ExecStart = "${cfg.python}/bin/gunicorn ${drv.pname}.app:app -b ${cfg.address}:${toString port}"; 89 + }; 90 + }; 91 + 92 + services.sourcehut.settings = { 93 + # URL dispatch.sr.ht is being served at (protocol://domain) 94 + "dispatch.sr.ht".origin = mkDefault "http://dispatch.${cfg.originBase}"; 95 + # Address and port to bind the debug server to 96 + "dispatch.sr.ht".debug-host = mkDefault "0.0.0.0"; 97 + "dispatch.sr.ht".debug-port = mkDefault port; 98 + # Configures the SQLAlchemy connection string for the database. 99 + "dispatch.sr.ht".connection-string = mkDefault "postgresql:///${database}?user=${user}&host=/var/run/postgresql"; 100 + # Set to "yes" to automatically run migrations on package upgrade. 101 + "dispatch.sr.ht".migrate-on-upgrade = mkDefault "yes"; 102 + # dispatch.sr.ht's OAuth client ID and secret for meta.sr.ht 103 + # Register your client at meta.example.org/oauth 104 + "dispatch.sr.ht".oauth-client-id = mkDefault null; 105 + "dispatch.sr.ht".oauth-client-secret = mkDefault null; 106 + 107 + # Github Integration 108 + "dispatch.sr.ht::github".oauth-client-id = mkDefault null; 109 + "dispatch.sr.ht::github".oauth-client-secret = mkDefault null; 110 + 111 + # Gitlab Integration 112 + "dispatch.sr.ht::gitlab".enabled = mkDefault null; 113 + "dispatch.sr.ht::gitlab".canonical-upstream = mkDefault "gitlab.com"; 114 + "dispatch.sr.ht::gitlab".repo-cache = mkDefault "./repo-cache"; 115 + # "dispatch.sr.ht::gitlab"."gitlab.com" = mkDefault "GitLab:application id:secret"; 116 + }; 117 + 118 + services.nginx.virtualHosts."dispatch.${cfg.originBase}" = { 119 + forceSSL = true; 120 + locations."/".proxyPass = "http://${cfg.address}:${toString port}"; 121 + locations."/query".proxyPass = "http://${cfg.address}:${toString (port + 100)}"; 122 + locations."/static".root = "${pkgs.sourcehut.dispatchsrht}/${pkgs.sourcehut.python.sitePackages}/dispatchsrht"; 123 + }; 124 + }; 125 + }
+214
nixos/modules/services/misc/sourcehut/git.nix
···
··· 1 + { config, lib, pkgs, ... }: 2 + 3 + with lib; 4 + let 5 + cfg = config.services.sourcehut; 6 + scfg = cfg.git; 7 + iniKey = "git.sr.ht"; 8 + 9 + rcfg = config.services.redis; 10 + drv = pkgs.sourcehut.gitsrht; 11 + in 12 + { 13 + options.services.sourcehut.git = { 14 + user = mkOption { 15 + type = types.str; 16 + visible = false; 17 + internal = true; 18 + readOnly = true; 19 + default = "git"; 20 + description = '' 21 + User for git.sr.ht. 22 + ''; 23 + }; 24 + 25 + port = mkOption { 26 + type = types.port; 27 + default = 5001; 28 + description = '' 29 + Port on which the "git" module should listen. 30 + ''; 31 + }; 32 + 33 + database = mkOption { 34 + type = types.str; 35 + default = "git.sr.ht"; 36 + description = '' 37 + PostgreSQL database name for git.sr.ht. 38 + ''; 39 + }; 40 + 41 + statePath = mkOption { 42 + type = types.path; 43 + default = "${cfg.statePath}/gitsrht"; 44 + description = '' 45 + State path for git.sr.ht. 46 + ''; 47 + }; 48 + 49 + package = mkOption { 50 + type = types.package; 51 + default = pkgs.git; 52 + example = literalExample "pkgs.gitFull"; 53 + description = '' 54 + Git package for git.sr.ht. This can help silence collisions. 55 + ''; 56 + }; 57 + }; 58 + 59 + config = with scfg; lib.mkIf (cfg.enable && elem "git" cfg.services) { 60 + # sshd refuses to run with `Unsafe AuthorizedKeysCommand ... bad ownership or modes for directory /nix/store` 61 + environment.etc."ssh/gitsrht-dispatch" = { 62 + mode = "0755"; 63 + text = '' 64 + #! ${pkgs.stdenv.shell} 65 + ${cfg.python}/bin/gitsrht-dispatch "$@" 66 + ''; 67 + }; 68 + 69 + # Needs this in the $PATH when sshing into the server 70 + environment.systemPackages = [ cfg.git.package ]; 71 + 72 + users = { 73 + users = { 74 + "${user}" = { 75 + isSystemUser = true; 76 + group = user; 77 + # https://stackoverflow.com/questions/22314298/git-push-results-in-fatal-protocol-error-bad-line-length-character-this 78 + # Probably could use gitsrht-shell if output is restricted to just parameters... 79 + shell = pkgs.bash; 80 + description = "git.sr.ht user"; 81 + }; 82 + }; 83 + 84 + groups = { 85 + "${user}" = { }; 86 + }; 87 + }; 88 + 89 + services = { 90 + cron.systemCronJobs = [ "*/20 * * * * ${cfg.python}/bin/gitsrht-periodic" ]; 91 + fcgiwrap.enable = true; 92 + 93 + openssh.authorizedKeysCommand = ''/etc/ssh/gitsrht-dispatch "%u" "%h" "%t" "%k"''; 94 + openssh.authorizedKeysCommandUser = "root"; 95 + openssh.extraConfig = '' 96 + PermitUserEnvironment SRHT_* 97 + ''; 98 + 99 + postgresql = { 100 + authentication = '' 101 + local ${database} ${user} trust 102 + ''; 103 + ensureDatabases = [ database ]; 104 + ensureUsers = [ 105 + { 106 + name = user; 107 + ensurePermissions = { "DATABASE \"${database}\"" = "ALL PRIVILEGES"; }; 108 + } 109 + ]; 110 + }; 111 + }; 112 + 113 + systemd = { 114 + tmpfiles.rules = [ 115 + # /var/log is owned by root 116 + "f /var/log/git-srht-shell 0644 ${user} ${user} -" 117 + 118 + "d ${statePath} 0750 ${user} ${user} -" 119 + "d ${cfg.settings."${iniKey}".repos} 2755 ${user} ${user} -" 120 + ]; 121 + 122 + services = { 123 + gitsrht = import ./service.nix { inherit config pkgs lib; } scfg drv iniKey { 124 + after = [ "redis.service" "postgresql.service" "network.target" ]; 125 + requires = [ "redis.service" "postgresql.service" ]; 126 + wantedBy = [ "multi-user.target" ]; 127 + 128 + # Needs internally to create repos at the very least 129 + path = [ pkgs.git ]; 130 + description = "git.sr.ht website service"; 131 + 132 + serviceConfig.ExecStart = "${cfg.python}/bin/gunicorn ${drv.pname}.app:app -b ${cfg.address}:${toString port}"; 133 + }; 134 + 135 + gitsrht-webhooks = { 136 + after = [ "postgresql.service" "network.target" ]; 137 + requires = [ "postgresql.service" ]; 138 + wantedBy = [ "multi-user.target" ]; 139 + 140 + description = "git.sr.ht webhooks service"; 141 + serviceConfig = { 142 + Type = "simple"; 143 + User = user; 144 + Restart = "always"; 145 + }; 146 + 147 + serviceConfig.ExecStart = "${cfg.python}/bin/celery -A ${drv.pname}.webhooks worker --loglevel=info"; 148 + }; 149 + }; 150 + }; 151 + 152 + services.sourcehut.settings = { 153 + # URL git.sr.ht is being served at (protocol://domain) 154 + "git.sr.ht".origin = mkDefault "http://git.${cfg.originBase}"; 155 + # Address and port to bind the debug server to 156 + "git.sr.ht".debug-host = mkDefault "0.0.0.0"; 157 + "git.sr.ht".debug-port = mkDefault port; 158 + # Configures the SQLAlchemy connection string for the database. 159 + "git.sr.ht".connection-string = mkDefault "postgresql:///${database}?user=${user}&host=/var/run/postgresql"; 160 + # Set to "yes" to automatically run migrations on package upgrade. 161 + "git.sr.ht".migrate-on-upgrade = mkDefault "yes"; 162 + # The redis connection used for the webhooks worker 163 + "git.sr.ht".webhooks = mkDefault "redis://${rcfg.bind}:${toString rcfg.port}/1"; 164 + 165 + # A post-update script which is installed in every git repo. 166 + "git.sr.ht".post-update-script = mkDefault "${pkgs.sourcehut.gitsrht}/bin/gitsrht-update-hook"; 167 + 168 + # git.sr.ht's OAuth client ID and secret for meta.sr.ht 169 + # Register your client at meta.example.org/oauth 170 + "git.sr.ht".oauth-client-id = mkDefault null; 171 + "git.sr.ht".oauth-client-secret = mkDefault null; 172 + # Path to git repositories on disk 173 + "git.sr.ht".repos = mkDefault "/var/lib/git"; 174 + 175 + "git.sr.ht".outgoing-domain = mkDefault "http://git.${cfg.originBase}"; 176 + 177 + # The authorized keys hook uses this to dispatch to various handlers 178 + # The format is a program to exec into as the key, and the user to match as the 179 + # value. When someone tries to log in as this user, this program is executed 180 + # and is expected to omit an AuthorizedKeys file. 181 + # 182 + # Discard of the string context is in order to allow derivation-derived strings. 183 + # This is safe if the relevant package is installed which will be the case if the setting is utilized. 184 + "git.sr.ht::dispatch".${builtins.unsafeDiscardStringContext "${pkgs.sourcehut.gitsrht}/bin/gitsrht-keys"} = mkDefault "${user}:${user}"; 185 + }; 186 + 187 + services.nginx.virtualHosts."git.${cfg.originBase}" = { 188 + forceSSL = true; 189 + locations."/".proxyPass = "http://${cfg.address}:${toString port}"; 190 + locations."/query".proxyPass = "http://${cfg.address}:${toString (port + 100)}"; 191 + locations."/static".root = "${pkgs.sourcehut.gitsrht}/${pkgs.sourcehut.python.sitePackages}/gitsrht"; 192 + extraConfig = '' 193 + location = /authorize { 194 + proxy_pass http://${cfg.address}:${toString port}; 195 + proxy_pass_request_body off; 196 + proxy_set_header Content-Length ""; 197 + proxy_set_header X-Original-URI $request_uri; 198 + } 199 + location ~ ^/([^/]+)/([^/]+)/(HEAD|info/refs|objects/info/.*|git-upload-pack).*$ { 200 + auth_request /authorize; 201 + root /var/lib/git; 202 + fastcgi_pass unix:/run/fcgiwrap.sock; 203 + fastcgi_param SCRIPT_FILENAME ${pkgs.git}/bin/git-http-backend; 204 + fastcgi_param PATH_INFO $uri; 205 + fastcgi_param GIT_PROJECT_ROOT $document_root; 206 + fastcgi_read_timeout 500s; 207 + include ${pkgs.nginx}/conf/fastcgi_params; 208 + gzip off; 209 + } 210 + ''; 211 + 212 + }; 213 + }; 214 + }
+173
nixos/modules/services/misc/sourcehut/hg.nix
···
··· 1 + { config, lib, pkgs, ... }: 2 + 3 + with lib; 4 + let 5 + cfg = config.services.sourcehut; 6 + scfg = cfg.hg; 7 + iniKey = "hg.sr.ht"; 8 + 9 + rcfg = config.services.redis; 10 + drv = pkgs.sourcehut.hgsrht; 11 + in 12 + { 13 + options.services.sourcehut.hg = { 14 + user = mkOption { 15 + type = types.str; 16 + internal = true; 17 + readOnly = true; 18 + default = "hg"; 19 + description = '' 20 + User for hg.sr.ht. 21 + ''; 22 + }; 23 + 24 + port = mkOption { 25 + type = types.port; 26 + default = 5010; 27 + description = '' 28 + Port on which the "hg" module should listen. 29 + ''; 30 + }; 31 + 32 + database = mkOption { 33 + type = types.str; 34 + default = "hg.sr.ht"; 35 + description = '' 36 + PostgreSQL database name for hg.sr.ht. 37 + ''; 38 + }; 39 + 40 + statePath = mkOption { 41 + type = types.path; 42 + default = "${cfg.statePath}/hgsrht"; 43 + description = '' 44 + State path for hg.sr.ht. 45 + ''; 46 + }; 47 + 48 + cloneBundles = mkOption { 49 + type = types.bool; 50 + default = false; 51 + description = '' 52 + Generate clonebundles (which require more disk space but dramatically speed up cloning large repositories). 53 + ''; 54 + }; 55 + }; 56 + 57 + config = with scfg; lib.mkIf (cfg.enable && elem "hg" cfg.services) { 58 + # In case it ever comes into being 59 + environment.etc."ssh/hgsrht-dispatch" = { 60 + mode = "0755"; 61 + text = '' 62 + #! ${pkgs.stdenv.shell} 63 + ${cfg.python}/bin/gitsrht-dispatch $@ 64 + ''; 65 + }; 66 + 67 + environment.systemPackages = [ pkgs.mercurial ]; 68 + 69 + users = { 70 + users = { 71 + "${user}" = { 72 + isSystemUser = true; 73 + group = user; 74 + # Assuming hg.sr.ht needs this too 75 + shell = pkgs.bash; 76 + description = "hg.sr.ht user"; 77 + }; 78 + }; 79 + 80 + groups = { 81 + "${user}" = { }; 82 + }; 83 + }; 84 + 85 + services = { 86 + cron.systemCronJobs = [ "*/20 * * * * ${cfg.python}/bin/hgsrht-periodic" ] 87 + ++ optional cloneBundles "0 * * * * ${cfg.python}/bin/hgsrht-clonebundles"; 88 + 89 + openssh.authorizedKeysCommand = ''/etc/ssh/hgsrht-dispatch "%u" "%h" "%t" "%k"''; 90 + openssh.authorizedKeysCommandUser = "root"; 91 + openssh.extraConfig = '' 92 + PermitUserEnvironment SRHT_* 93 + ''; 94 + 95 + postgresql = { 96 + authentication = '' 97 + local ${database} ${user} trust 98 + ''; 99 + ensureDatabases = [ database ]; 100 + ensureUsers = [ 101 + { 102 + name = user; 103 + ensurePermissions = { "DATABASE \"${database}\"" = "ALL PRIVILEGES"; }; 104 + } 105 + ]; 106 + }; 107 + }; 108 + 109 + systemd = { 110 + tmpfiles.rules = [ 111 + # /var/log is owned by root 112 + "f /var/log/hg-srht-shell 0644 ${user} ${user} -" 113 + 114 + "d ${statePath} 0750 ${user} ${user} -" 115 + "d ${cfg.settings."${iniKey}".repos} 2755 ${user} ${user} -" 116 + ]; 117 + 118 + services.hgsrht = import ./service.nix { inherit config pkgs lib; } scfg drv iniKey { 119 + after = [ "redis.service" "postgresql.service" "network.target" ]; 120 + requires = [ "redis.service" "postgresql.service" ]; 121 + wantedBy = [ "multi-user.target" ]; 122 + 123 + path = [ pkgs.mercurial ]; 124 + description = "hg.sr.ht website service"; 125 + 126 + serviceConfig.ExecStart = "${cfg.python}/bin/gunicorn ${drv.pname}.app:app -b ${cfg.address}:${toString port}"; 127 + }; 128 + }; 129 + 130 + services.sourcehut.settings = { 131 + # URL hg.sr.ht is being served at (protocol://domain) 132 + "hg.sr.ht".origin = mkDefault "http://hg.${cfg.originBase}"; 133 + # Address and port to bind the debug server to 134 + "hg.sr.ht".debug-host = mkDefault "0.0.0.0"; 135 + "hg.sr.ht".debug-port = mkDefault port; 136 + # Configures the SQLAlchemy connection string for the database. 137 + "hg.sr.ht".connection-string = mkDefault "postgresql:///${database}?user=${user}&host=/var/run/postgresql"; 138 + # The redis connection used for the webhooks worker 139 + "hg.sr.ht".webhooks = mkDefault "redis://${rcfg.bind}:${toString rcfg.port}/1"; 140 + # A post-update script which is installed in every mercurial repo. 141 + "hg.sr.ht".changegroup-script = mkDefault "${cfg.python}/bin/hgsrht-hook-changegroup"; 142 + # hg.sr.ht's OAuth client ID and secret for meta.sr.ht 143 + # Register your client at meta.example.org/oauth 144 + "hg.sr.ht".oauth-client-id = mkDefault null; 145 + "hg.sr.ht".oauth-client-secret = mkDefault null; 146 + # Path to mercurial repositories on disk 147 + "hg.sr.ht".repos = mkDefault "/var/lib/hg"; 148 + # Path to the srht mercurial extension 149 + # (defaults to where the hgsrht code is) 150 + # "hg.sr.ht".srhtext = mkDefault null; 151 + # .hg/store size (in MB) past which the nightly job generates clone bundles. 152 + # "hg.sr.ht".clone_bundle_threshold = mkDefault 50; 153 + # Path to hg-ssh (if not in $PATH) 154 + # "hg.sr.ht".hg_ssh = mkDefault /path/to/hg-ssh; 155 + 156 + # The authorized keys hook uses this to dispatch to various handlers 157 + # The format is a program to exec into as the key, and the user to match as the 158 + # value. When someone tries to log in as this user, this program is executed 159 + # and is expected to omit an AuthorizedKeys file. 160 + # 161 + # Uncomment the relevant lines to enable the various sr.ht dispatchers. 162 + "hg.sr.ht::dispatch"."/run/current-system/sw/bin/hgsrht-keys" = mkDefault "${user}:${user}"; 163 + }; 164 + 165 + # TODO: requires testing and addition of hg-specific requirements 166 + services.nginx.virtualHosts."hg.${cfg.originBase}" = { 167 + forceSSL = true; 168 + locations."/".proxyPass = "http://${cfg.address}:${toString port}"; 169 + locations."/query".proxyPass = "http://${cfg.address}:${toString (port + 100)}"; 170 + locations."/static".root = "${pkgs.sourcehut.hgsrht}/${pkgs.sourcehut.python.sitePackages}/hgsrht"; 171 + }; 172 + }; 173 + }
+118
nixos/modules/services/misc/sourcehut/hub.nix
···
··· 1 + { config, lib, pkgs, ... }: 2 + 3 + with lib; 4 + let 5 + cfg = config.services.sourcehut; 6 + cfgIni = cfg.settings; 7 + scfg = cfg.hub; 8 + iniKey = "hub.sr.ht"; 9 + 10 + drv = pkgs.sourcehut.hubsrht; 11 + in 12 + { 13 + options.services.sourcehut.hub = { 14 + user = mkOption { 15 + type = types.str; 16 + default = "hubsrht"; 17 + description = '' 18 + User for hub.sr.ht. 19 + ''; 20 + }; 21 + 22 + port = mkOption { 23 + type = types.port; 24 + default = 5014; 25 + description = '' 26 + Port on which the "hub" module should listen. 27 + ''; 28 + }; 29 + 30 + database = mkOption { 31 + type = types.str; 32 + default = "hub.sr.ht"; 33 + description = '' 34 + PostgreSQL database name for hub.sr.ht. 35 + ''; 36 + }; 37 + 38 + statePath = mkOption { 39 + type = types.path; 40 + default = "${cfg.statePath}/hubsrht"; 41 + description = '' 42 + State path for hub.sr.ht. 43 + ''; 44 + }; 45 + }; 46 + 47 + config = with scfg; lib.mkIf (cfg.enable && elem "hub" cfg.services) { 48 + users = { 49 + users = { 50 + "${user}" = { 51 + isSystemUser = true; 52 + group = user; 53 + description = "hub.sr.ht user"; 54 + }; 55 + }; 56 + 57 + groups = { 58 + "${user}" = { }; 59 + }; 60 + }; 61 + 62 + services.postgresql = { 63 + authentication = '' 64 + local ${database} ${user} trust 65 + ''; 66 + ensureDatabases = [ database ]; 67 + ensureUsers = [ 68 + { 69 + name = user; 70 + ensurePermissions = { "DATABASE \"${database}\"" = "ALL PRIVILEGES"; }; 71 + } 72 + ]; 73 + }; 74 + 75 + systemd = { 76 + tmpfiles.rules = [ 77 + "d ${statePath} 0750 ${user} ${user} -" 78 + ]; 79 + 80 + services.hubsrht = import ./service.nix { inherit config pkgs lib; } scfg drv iniKey { 81 + after = [ "postgresql.service" "network.target" ]; 82 + requires = [ "postgresql.service" ]; 83 + wantedBy = [ "multi-user.target" ]; 84 + 85 + description = "hub.sr.ht website service"; 86 + 87 + serviceConfig.ExecStart = "${cfg.python}/bin/gunicorn ${drv.pname}.app:app -b ${cfg.address}:${toString port}"; 88 + }; 89 + }; 90 + 91 + services.sourcehut.settings = { 92 + # URL hub.sr.ht is being served at (protocol://domain) 93 + "hub.sr.ht".origin = mkDefault "http://hub.${cfg.originBase}"; 94 + # Address and port to bind the debug server to 95 + "hub.sr.ht".debug-host = mkDefault "0.0.0.0"; 96 + "hub.sr.ht".debug-port = mkDefault port; 97 + # Configures the SQLAlchemy connection string for the database. 98 + "hub.sr.ht".connection-string = mkDefault "postgresql:///${database}?user=${user}&host=/var/run/postgresql"; 99 + # Set to "yes" to automatically run migrations on package upgrade. 100 + "hub.sr.ht".migrate-on-upgrade = mkDefault "yes"; 101 + # hub.sr.ht's OAuth client ID and secret for meta.sr.ht 102 + # Register your client at meta.example.org/oauth 103 + "hub.sr.ht".oauth-client-id = mkDefault null; 104 + "hub.sr.ht".oauth-client-secret = mkDefault null; 105 + }; 106 + 107 + services.nginx.virtualHosts."${cfg.originBase}" = { 108 + forceSSL = true; 109 + locations."/".proxyPass = "http://${cfg.address}:${toString port}"; 110 + locations."/query".proxyPass = "http://${cfg.address}:${toString (port + 100)}"; 111 + locations."/static".root = "${pkgs.sourcehut.hubsrht}/${pkgs.sourcehut.python.sitePackages}/hubsrht"; 112 + }; 113 + services.nginx.virtualHosts."hub.${cfg.originBase}" = { 114 + globalRedirect = "${cfg.originBase}"; 115 + forceSSL = true; 116 + }; 117 + }; 118 + }
+185
nixos/modules/services/misc/sourcehut/lists.nix
···
··· 1 + # Email setup is fairly involved, useful references: 2 + # https://drewdevault.com/2018/08/05/Local-mail-server.html 3 + 4 + { config, lib, pkgs, ... }: 5 + 6 + with lib; 7 + let 8 + cfg = config.services.sourcehut; 9 + cfgIni = cfg.settings; 10 + scfg = cfg.lists; 11 + iniKey = "lists.sr.ht"; 12 + 13 + rcfg = config.services.redis; 14 + drv = pkgs.sourcehut.listssrht; 15 + in 16 + { 17 + options.services.sourcehut.lists = { 18 + user = mkOption { 19 + type = types.str; 20 + default = "listssrht"; 21 + description = '' 22 + User for lists.sr.ht. 23 + ''; 24 + }; 25 + 26 + port = mkOption { 27 + type = types.port; 28 + default = 5006; 29 + description = '' 30 + Port on which the "lists" module should listen. 31 + ''; 32 + }; 33 + 34 + database = mkOption { 35 + type = types.str; 36 + default = "lists.sr.ht"; 37 + description = '' 38 + PostgreSQL database name for lists.sr.ht. 39 + ''; 40 + }; 41 + 42 + statePath = mkOption { 43 + type = types.path; 44 + default = "${cfg.statePath}/listssrht"; 45 + description = '' 46 + State path for lists.sr.ht. 47 + ''; 48 + }; 49 + }; 50 + 51 + config = with scfg; lib.mkIf (cfg.enable && elem "lists" cfg.services) { 52 + users = { 53 + users = { 54 + "${user}" = { 55 + isSystemUser = true; 56 + group = user; 57 + extraGroups = [ "postfix" ]; 58 + description = "lists.sr.ht user"; 59 + }; 60 + }; 61 + groups = { 62 + "${user}" = { }; 63 + }; 64 + }; 65 + 66 + services.postgresql = { 67 + authentication = '' 68 + local ${database} ${user} trust 69 + ''; 70 + ensureDatabases = [ database ]; 71 + ensureUsers = [ 72 + { 73 + name = user; 74 + ensurePermissions = { "DATABASE \"${database}\"" = "ALL PRIVILEGES"; }; 75 + } 76 + ]; 77 + }; 78 + 79 + systemd = { 80 + tmpfiles.rules = [ 81 + "d ${statePath} 0750 ${user} ${user} -" 82 + ]; 83 + 84 + services = { 85 + listssrht = import ./service.nix { inherit config pkgs lib; } scfg drv iniKey { 86 + after = [ "postgresql.service" "network.target" ]; 87 + requires = [ "postgresql.service" ]; 88 + wantedBy = [ "multi-user.target" ]; 89 + 90 + description = "lists.sr.ht website service"; 91 + 92 + serviceConfig.ExecStart = "${cfg.python}/bin/gunicorn ${drv.pname}.app:app -b ${cfg.address}:${toString port}"; 93 + }; 94 + 95 + listssrht-process = { 96 + after = [ "postgresql.service" "network.target" ]; 97 + requires = [ "postgresql.service" ]; 98 + wantedBy = [ "multi-user.target" ]; 99 + 100 + description = "lists.sr.ht process service"; 101 + serviceConfig = { 102 + Type = "simple"; 103 + User = user; 104 + Restart = "always"; 105 + ExecStart = "${cfg.python}/bin/celery -A ${drv.pname}.process worker --loglevel=info"; 106 + }; 107 + }; 108 + 109 + listssrht-lmtp = { 110 + after = [ "postgresql.service" "network.target" ]; 111 + requires = [ "postgresql.service" ]; 112 + wantedBy = [ "multi-user.target" ]; 113 + 114 + description = "lists.sr.ht process service"; 115 + serviceConfig = { 116 + Type = "simple"; 117 + User = user; 118 + Restart = "always"; 119 + ExecStart = "${cfg.python}/bin/listssrht-lmtp"; 120 + }; 121 + }; 122 + 123 + 124 + listssrht-webhooks = { 125 + after = [ "postgresql.service" "network.target" ]; 126 + requires = [ "postgresql.service" ]; 127 + wantedBy = [ "multi-user.target" ]; 128 + 129 + description = "lists.sr.ht webhooks service"; 130 + serviceConfig = { 131 + Type = "simple"; 132 + User = user; 133 + Restart = "always"; 134 + ExecStart = "${cfg.python}/bin/celery -A ${drv.pname}.webhooks worker --loglevel=info"; 135 + }; 136 + }; 137 + }; 138 + }; 139 + 140 + services.sourcehut.settings = { 141 + # URL lists.sr.ht is being served at (protocol://domain) 142 + "lists.sr.ht".origin = mkDefault "http://lists.${cfg.originBase}"; 143 + # Address and port to bind the debug server to 144 + "lists.sr.ht".debug-host = mkDefault "0.0.0.0"; 145 + "lists.sr.ht".debug-port = mkDefault port; 146 + # Configures the SQLAlchemy connection string for the database. 147 + "lists.sr.ht".connection-string = mkDefault "postgresql:///${database}?user=${user}&host=/var/run/postgresql"; 148 + # Set to "yes" to automatically run migrations on package upgrade. 149 + "lists.sr.ht".migrate-on-upgrade = mkDefault "yes"; 150 + # lists.sr.ht's OAuth client ID and secret for meta.sr.ht 151 + # Register your client at meta.example.org/oauth 152 + "lists.sr.ht".oauth-client-id = mkDefault null; 153 + "lists.sr.ht".oauth-client-secret = mkDefault null; 154 + # Outgoing email for notifications generated by users 155 + "lists.sr.ht".notify-from = mkDefault "CHANGEME@example.org"; 156 + # The redis connection used for the webhooks worker 157 + "lists.sr.ht".webhooks = mkDefault "redis://${rcfg.bind}:${toString rcfg.port}/2"; 158 + # The redis connection used for the celery worker 159 + "lists.sr.ht".redis = mkDefault "redis://${rcfg.bind}:${toString rcfg.port}/4"; 160 + # Network-key 161 + "lists.sr.ht".network-key = mkDefault null; 162 + # Allow creation 163 + "lists.sr.ht".allow-new-lists = mkDefault "no"; 164 + # Posting Domain 165 + "lists.sr.ht".posting-domain = mkDefault "lists.${cfg.originBase}"; 166 + 167 + # Path for the lmtp daemon's unix socket. Direct incoming mail to this socket. 168 + # Alternatively, specify IP:PORT and an SMTP server will be run instead. 169 + "lists.sr.ht::worker".sock = mkDefault "/tmp/lists.sr.ht-lmtp.sock"; 170 + # The lmtp daemon will make the unix socket group-read/write for users in this 171 + # group. 172 + "lists.sr.ht::worker".sock-group = mkDefault "postfix"; 173 + "lists.sr.ht::worker".reject-url = mkDefault "https://man.sr.ht/lists.sr.ht/etiquette.md"; 174 + "lists.sr.ht::worker".reject-mimetypes = mkDefault "text/html"; 175 + 176 + }; 177 + 178 + services.nginx.virtualHosts."lists.${cfg.originBase}" = { 179 + forceSSL = true; 180 + locations."/".proxyPass = "http://${cfg.address}:${toString port}"; 181 + locations."/query".proxyPass = "http://${cfg.address}:${toString (port + 100)}"; 182 + locations."/static".root = "${pkgs.sourcehut.listssrht}/${pkgs.sourcehut.python.sitePackages}/listssrht"; 183 + }; 184 + }; 185 + }
+122
nixos/modules/services/misc/sourcehut/man.nix
···
··· 1 + { config, lib, pkgs, ... }: 2 + 3 + with lib; 4 + let 5 + cfg = config.services.sourcehut; 6 + cfgIni = cfg.settings; 7 + scfg = cfg.man; 8 + iniKey = "man.sr.ht"; 9 + 10 + drv = pkgs.sourcehut.mansrht; 11 + in 12 + { 13 + options.services.sourcehut.man = { 14 + user = mkOption { 15 + type = types.str; 16 + default = "mansrht"; 17 + description = '' 18 + User for man.sr.ht. 19 + ''; 20 + }; 21 + 22 + port = mkOption { 23 + type = types.port; 24 + default = 5004; 25 + description = '' 26 + Port on which the "man" module should listen. 27 + ''; 28 + }; 29 + 30 + database = mkOption { 31 + type = types.str; 32 + default = "man.sr.ht"; 33 + description = '' 34 + PostgreSQL database name for man.sr.ht. 35 + ''; 36 + }; 37 + 38 + statePath = mkOption { 39 + type = types.path; 40 + default = "${cfg.statePath}/mansrht"; 41 + description = '' 42 + State path for man.sr.ht. 43 + ''; 44 + }; 45 + }; 46 + 47 + config = with scfg; lib.mkIf (cfg.enable && elem "man" cfg.services) { 48 + assertions = 49 + [ 50 + { 51 + assertion = hasAttrByPath [ "git.sr.ht" "oauth-client-id" ] cfgIni; 52 + message = "man.sr.ht needs access to git.sr.ht."; 53 + } 54 + ]; 55 + 56 + users = { 57 + users = { 58 + "${user}" = { 59 + isSystemUser = true; 60 + group = user; 61 + description = "man.sr.ht user"; 62 + }; 63 + }; 64 + 65 + groups = { 66 + "${user}" = { }; 67 + }; 68 + }; 69 + 70 + services.postgresql = { 71 + authentication = '' 72 + local ${database} ${user} trust 73 + ''; 74 + ensureDatabases = [ database ]; 75 + ensureUsers = [ 76 + { 77 + name = user; 78 + ensurePermissions = { "DATABASE \"${database}\"" = "ALL PRIVILEGES"; }; 79 + } 80 + ]; 81 + }; 82 + 83 + systemd = { 84 + tmpfiles.rules = [ 85 + "d ${statePath} 0750 ${user} ${user} -" 86 + ]; 87 + 88 + services.mansrht = import ./service.nix { inherit config pkgs lib; } scfg drv iniKey { 89 + after = [ "postgresql.service" "network.target" ]; 90 + requires = [ "postgresql.service" ]; 91 + wantedBy = [ "multi-user.target" ]; 92 + 93 + description = "man.sr.ht website service"; 94 + 95 + serviceConfig.ExecStart = "${cfg.python}/bin/gunicorn ${drv.pname}.app:app -b ${cfg.address}:${toString port}"; 96 + }; 97 + }; 98 + 99 + services.sourcehut.settings = { 100 + # URL man.sr.ht is being served at (protocol://domain) 101 + "man.sr.ht".origin = mkDefault "http://man.${cfg.originBase}"; 102 + # Address and port to bind the debug server to 103 + "man.sr.ht".debug-host = mkDefault "0.0.0.0"; 104 + "man.sr.ht".debug-port = mkDefault port; 105 + # Configures the SQLAlchemy connection string for the database. 106 + "man.sr.ht".connection-string = mkDefault "postgresql:///${database}?user=${user}&host=/var/run/postgresql"; 107 + # Set to "yes" to automatically run migrations on package upgrade. 108 + "man.sr.ht".migrate-on-upgrade = mkDefault "yes"; 109 + # man.sr.ht's OAuth client ID and secret for meta.sr.ht 110 + # Register your client at meta.example.org/oauth 111 + "man.sr.ht".oauth-client-id = mkDefault null; 112 + "man.sr.ht".oauth-client-secret = mkDefault null; 113 + }; 114 + 115 + services.nginx.virtualHosts."man.${cfg.originBase}" = { 116 + forceSSL = true; 117 + locations."/".proxyPass = "http://${cfg.address}:${toString port}"; 118 + locations."/query".proxyPass = "http://${cfg.address}:${toString (port + 100)}"; 119 + locations."/static".root = "${pkgs.sourcehut.mansrht}/${pkgs.sourcehut.python.sitePackages}/mansrht"; 120 + }; 121 + }; 122 + }
+211
nixos/modules/services/misc/sourcehut/meta.nix
···
··· 1 + { config, lib, pkgs, ... }: 2 + 3 + with lib; 4 + let 5 + cfg = config.services.sourcehut; 6 + cfgIni = cfg.settings; 7 + scfg = cfg.meta; 8 + iniKey = "meta.sr.ht"; 9 + 10 + rcfg = config.services.redis; 11 + drv = pkgs.sourcehut.metasrht; 12 + in 13 + { 14 + options.services.sourcehut.meta = { 15 + user = mkOption { 16 + type = types.str; 17 + default = "metasrht"; 18 + description = '' 19 + User for meta.sr.ht. 20 + ''; 21 + }; 22 + 23 + port = mkOption { 24 + type = types.port; 25 + default = 5000; 26 + description = '' 27 + Port on which the "meta" module should listen. 28 + ''; 29 + }; 30 + 31 + database = mkOption { 32 + type = types.str; 33 + default = "meta.sr.ht"; 34 + description = '' 35 + PostgreSQL database name for meta.sr.ht. 36 + ''; 37 + }; 38 + 39 + statePath = mkOption { 40 + type = types.path; 41 + default = "${cfg.statePath}/metasrht"; 42 + description = '' 43 + State path for meta.sr.ht. 44 + ''; 45 + }; 46 + }; 47 + 48 + config = with scfg; lib.mkIf (cfg.enable && elem "meta" cfg.services) { 49 + assertions = 50 + [ 51 + { 52 + assertion = with cfgIni."meta.sr.ht::billing"; enabled == "yes" -> (stripe-public-key != null && stripe-secret-key != null); 53 + message = "If meta.sr.ht::billing is enabled, the keys should be defined."; 54 + } 55 + ]; 56 + 57 + users = { 58 + users = { 59 + ${user} = { 60 + isSystemUser = true; 61 + group = user; 62 + description = "meta.sr.ht user"; 63 + }; 64 + }; 65 + 66 + groups = { 67 + "${user}" = { }; 68 + }; 69 + }; 70 + 71 + services.cron.systemCronJobs = [ "0 0 * * * ${cfg.python}/bin/metasrht-daily" ]; 72 + services.postgresql = { 73 + authentication = '' 74 + local ${database} ${user} trust 75 + ''; 76 + ensureDatabases = [ database ]; 77 + ensureUsers = [ 78 + { 79 + name = user; 80 + ensurePermissions = { "DATABASE \"${database}\"" = "ALL PRIVILEGES"; }; 81 + } 82 + ]; 83 + }; 84 + 85 + systemd = { 86 + tmpfiles.rules = [ 87 + "d ${statePath} 0750 ${user} ${user} -" 88 + ]; 89 + 90 + services = { 91 + metasrht = import ./service.nix { inherit config pkgs lib; } scfg drv iniKey { 92 + after = [ "postgresql.service" "network.target" ]; 93 + requires = [ "postgresql.service" ]; 94 + wantedBy = [ "multi-user.target" ]; 95 + 96 + description = "meta.sr.ht website service"; 97 + 98 + preStart = '' 99 + # Configure client(s) as "preauthorized" 100 + ${concatMapStringsSep "\n\n" 101 + (attr: '' 102 + if ! test -e "${statePath}/${attr}.oauth" || [ "$(cat ${statePath}/${attr}.oauth)" != "${cfgIni."${attr}".oauth-client-id}" ]; then 103 + # Configure ${attr}'s OAuth client as "preauthorized" 104 + psql ${database} \ 105 + -c "UPDATE oauthclient SET preauthorized = true WHERE client_id = '${cfgIni."${attr}".oauth-client-id}'" 106 + 107 + printf "%s" "${cfgIni."${attr}".oauth-client-id}" > "${statePath}/${attr}.oauth" 108 + fi 109 + '') 110 + (builtins.attrNames (filterAttrs 111 + (k: v: !(hasInfix "::" k) && builtins.hasAttr "oauth-client-id" v && v.oauth-client-id != null) 112 + cfg.settings))} 113 + ''; 114 + 115 + serviceConfig.ExecStart = "${cfg.python}/bin/gunicorn ${drv.pname}.app:app -b ${cfg.address}:${toString port}"; 116 + }; 117 + 118 + metasrht-api = import ./service.nix { inherit config pkgs lib; } scfg drv iniKey { 119 + after = [ "postgresql.service" "network.target" ]; 120 + requires = [ "postgresql.service" ]; 121 + wantedBy = [ "multi-user.target" ]; 122 + 123 + description = "meta.sr.ht api service"; 124 + 125 + preStart = '' 126 + # Configure client(s) as "preauthorized" 127 + ${concatMapStringsSep "\n\n" 128 + (attr: '' 129 + if ! test -e "${statePath}/${attr}.oauth" || [ "$(cat ${statePath}/${attr}.oauth)" != "${cfgIni."${attr}".oauth-client-id}" ]; then 130 + # Configure ${attr}'s OAuth client as "preauthorized" 131 + psql ${database} \ 132 + -c "UPDATE oauthclient SET preauthorized = true WHERE client_id = '${cfgIni."${attr}".oauth-client-id}'" 133 + 134 + printf "%s" "${cfgIni."${attr}".oauth-client-id}" > "${statePath}/${attr}.oauth" 135 + fi 136 + '') 137 + (builtins.attrNames (filterAttrs 138 + (k: v: !(hasInfix "::" k) && builtins.hasAttr "oauth-client-id" v && v.oauth-client-id != null) 139 + cfg.settings))} 140 + ''; 141 + 142 + serviceConfig.ExecStart = "${pkgs.sourcehut.metasrht}/bin/metasrht-api -b :${toString (port + 100)}"; 143 + }; 144 + 145 + metasrht-webhooks = { 146 + after = [ "postgresql.service" "network.target" ]; 147 + requires = [ "postgresql.service" ]; 148 + wantedBy = [ "multi-user.target" ]; 149 + 150 + description = "meta.sr.ht webhooks service"; 151 + serviceConfig = { 152 + Type = "simple"; 153 + User = user; 154 + Restart = "always"; 155 + ExecStart = "${cfg.python}/bin/celery -A ${drv.pname}.webhooks worker --loglevel=info"; 156 + }; 157 + 158 + }; 159 + }; 160 + }; 161 + 162 + services.sourcehut.settings = { 163 + # URL meta.sr.ht is being served at (protocol://domain) 164 + "meta.sr.ht".origin = mkDefault "https://meta.${cfg.originBase}"; 165 + # Address and port to bind the debug server to 166 + "meta.sr.ht".debug-host = mkDefault "0.0.0.0"; 167 + "meta.sr.ht".debug-port = mkDefault port; 168 + # Configures the SQLAlchemy connection string for the database. 169 + "meta.sr.ht".connection-string = mkDefault "postgresql:///${database}?user=${user}&host=/var/run/postgresql"; 170 + # Set to "yes" to automatically run migrations on package upgrade. 171 + "meta.sr.ht".migrate-on-upgrade = mkDefault "yes"; 172 + # If "yes", the user will be sent the stock sourcehut welcome emails after 173 + # signup (requires cron to be configured properly). These are specific to the 174 + # sr.ht instance so you probably want to patch these before enabling this. 175 + "meta.sr.ht".welcome-emails = mkDefault "no"; 176 + 177 + # The redis connection used for the webhooks worker 178 + "meta.sr.ht".webhooks = mkDefault "redis://${rcfg.bind}:${toString rcfg.port}/6"; 179 + 180 + # If "no", public registration will not be permitted. 181 + "meta.sr.ht::settings".registration = mkDefault "no"; 182 + # Where to redirect new users upon registration 183 + "meta.sr.ht::settings".onboarding-redirect = mkDefault "https://meta.${cfg.originBase}"; 184 + # How many invites each user is issued upon registration (only applicable if 185 + # open registration is disabled) 186 + "meta.sr.ht::settings".user-invites = mkDefault 5; 187 + 188 + # Origin URL for API, 100 more than web 189 + "meta.sr.ht".api-origin = mkDefault "http://localhost:5100"; 190 + 191 + # You can add aliases for the client IDs of commonly used OAuth clients here. 192 + # 193 + # Example: 194 + "meta.sr.ht::aliases" = mkDefault { }; 195 + # "meta.sr.ht::aliases"."git.sr.ht" = 12345; 196 + 197 + # "yes" to enable the billing system 198 + "meta.sr.ht::billing".enabled = mkDefault "no"; 199 + # Get your keys at https://dashboard.stripe.com/account/apikeys 200 + "meta.sr.ht::billing".stripe-public-key = mkDefault null; 201 + "meta.sr.ht::billing".stripe-secret-key = mkDefault null; 202 + }; 203 + 204 + services.nginx.virtualHosts."meta.${cfg.originBase}" = { 205 + forceSSL = true; 206 + locations."/".proxyPass = "http://${cfg.address}:${toString port}"; 207 + locations."/query".proxyPass = "http://${cfg.address}:${toString (port + 100)}"; 208 + locations."/static".root = "${pkgs.sourcehut.metasrht}/${pkgs.sourcehut.python.sitePackages}/metasrht"; 209 + }; 210 + }; 211 + }
+133
nixos/modules/services/misc/sourcehut/paste.nix
···
··· 1 + { config, lib, pkgs, ... }: 2 + 3 + with lib; 4 + let 5 + cfg = config.services.sourcehut; 6 + cfgIni = cfg.settings; 7 + scfg = cfg.paste; 8 + iniKey = "paste.sr.ht"; 9 + 10 + rcfg = config.services.redis; 11 + drv = pkgs.sourcehut.pastesrht; 12 + in 13 + { 14 + options.services.sourcehut.paste = { 15 + user = mkOption { 16 + type = types.str; 17 + default = "pastesrht"; 18 + description = '' 19 + User for paste.sr.ht. 20 + ''; 21 + }; 22 + 23 + port = mkOption { 24 + type = types.port; 25 + default = 5011; 26 + description = '' 27 + Port on which the "paste" module should listen. 28 + ''; 29 + }; 30 + 31 + database = mkOption { 32 + type = types.str; 33 + default = "paste.sr.ht"; 34 + description = '' 35 + PostgreSQL database name for paste.sr.ht. 36 + ''; 37 + }; 38 + 39 + statePath = mkOption { 40 + type = types.path; 41 + default = "${cfg.statePath}/pastesrht"; 42 + description = '' 43 + State path for pastesrht.sr.ht. 44 + ''; 45 + }; 46 + }; 47 + 48 + config = with scfg; lib.mkIf (cfg.enable && elem "paste" cfg.services) { 49 + users = { 50 + users = { 51 + "${user}" = { 52 + isSystemUser = true; 53 + group = user; 54 + description = "paste.sr.ht user"; 55 + }; 56 + }; 57 + 58 + groups = { 59 + "${user}" = { }; 60 + }; 61 + }; 62 + 63 + services.postgresql = { 64 + authentication = '' 65 + local ${database} ${user} trust 66 + ''; 67 + ensureDatabases = [ database ]; 68 + ensureUsers = [ 69 + { 70 + name = user; 71 + ensurePermissions = { "DATABASE \"${database}\"" = "ALL PRIVILEGES"; }; 72 + } 73 + ]; 74 + }; 75 + 76 + systemd = { 77 + tmpfiles.rules = [ 78 + "d ${statePath} 0750 ${user} ${user} -" 79 + ]; 80 + 81 + services = { 82 + pastesrht = import ./service.nix { inherit config pkgs lib; } scfg drv iniKey { 83 + after = [ "postgresql.service" "network.target" ]; 84 + requires = [ "postgresql.service" ]; 85 + wantedBy = [ "multi-user.target" ]; 86 + 87 + description = "paste.sr.ht website service"; 88 + 89 + serviceConfig.ExecStart = "${cfg.python}/bin/gunicorn ${drv.pname}.app:app -b ${cfg.address}:${toString port}"; 90 + }; 91 + 92 + pastesrht-webhooks = { 93 + after = [ "postgresql.service" "network.target" ]; 94 + requires = [ "postgresql.service" ]; 95 + wantedBy = [ "multi-user.target" ]; 96 + 97 + description = "paste.sr.ht webhooks service"; 98 + serviceConfig = { 99 + Type = "simple"; 100 + User = user; 101 + Restart = "always"; 102 + ExecStart = "${cfg.python}/bin/celery -A ${drv.pname}.webhooks worker --loglevel=info"; 103 + }; 104 + 105 + }; 106 + }; 107 + }; 108 + 109 + services.sourcehut.settings = { 110 + # URL paste.sr.ht is being served at (protocol://domain) 111 + "paste.sr.ht".origin = mkDefault "http://paste.${cfg.originBase}"; 112 + # Address and port to bind the debug server to 113 + "paste.sr.ht".debug-host = mkDefault "0.0.0.0"; 114 + "paste.sr.ht".debug-port = mkDefault port; 115 + # Configures the SQLAlchemy connection string for the database. 116 + "paste.sr.ht".connection-string = mkDefault "postgresql:///${database}?user=${user}&host=/var/run/postgresql"; 117 + # Set to "yes" to automatically run migrations on package upgrade. 118 + "paste.sr.ht".migrate-on-upgrade = mkDefault "yes"; 119 + # paste.sr.ht's OAuth client ID and secret for meta.sr.ht 120 + # Register your client at meta.example.org/oauth 121 + "paste.sr.ht".oauth-client-id = mkDefault null; 122 + "paste.sr.ht".oauth-client-secret = mkDefault null; 123 + "paste.sr.ht".webhooks = mkDefault "redis://${rcfg.bind}:${toString rcfg.port}/5"; 124 + }; 125 + 126 + services.nginx.virtualHosts."paste.${cfg.originBase}" = { 127 + forceSSL = true; 128 + locations."/".proxyPass = "http://${cfg.address}:${toString port}"; 129 + locations."/query".proxyPass = "http://${cfg.address}:${toString (port + 100)}"; 130 + locations."/static".root = "${pkgs.sourcehut.pastesrht}/${pkgs.sourcehut.python.sitePackages}/pastesrht"; 131 + }; 132 + }; 133 + }
+66
nixos/modules/services/misc/sourcehut/service.nix
···
··· 1 + { config, pkgs, lib }: 2 + serviceCfg: serviceDrv: iniKey: attrs: 3 + let 4 + cfg = config.services.sourcehut; 5 + cfgIni = cfg.settings."${iniKey}"; 6 + pgSuperUser = config.services.postgresql.superUser; 7 + 8 + setupDB = pkgs.writeScript "${serviceDrv.pname}-gen-db" '' 9 + #! ${cfg.python}/bin/python 10 + from ${serviceDrv.pname}.app import db 11 + db.create() 12 + ''; 13 + in 14 + with serviceCfg; with lib; recursiveUpdate 15 + { 16 + environment.HOME = statePath; 17 + path = [ config.services.postgresql.package ] ++ (attrs.path or [ ]); 18 + restartTriggers = [ config.environment.etc."sr.ht/config.ini".source ]; 19 + serviceConfig = { 20 + Type = "simple"; 21 + User = user; 22 + Group = user; 23 + Restart = "always"; 24 + WorkingDirectory = statePath; 25 + } // (if (cfg.statePath == "/var/lib/sourcehut/${serviceDrv.pname}") then { 26 + StateDirectory = [ "sourcehut/${serviceDrv.pname}" ]; 27 + } else {}) 28 + ; 29 + 30 + preStart = '' 31 + if ! test -e ${statePath}/db; then 32 + # Setup the initial database 33 + ${setupDB} 34 + 35 + # Set the initial state of the database for future database upgrades 36 + if test -e ${cfg.python}/bin/${serviceDrv.pname}-migrate; then 37 + # Run alembic stamp head once to tell alembic the schema is up-to-date 38 + ${cfg.python}/bin/${serviceDrv.pname}-migrate stamp head 39 + fi 40 + 41 + printf "%s" "${serviceDrv.version}" > ${statePath}/db 42 + fi 43 + 44 + # Update copy of each users' profile to the latest 45 + # See https://lists.sr.ht/~sircmpwn/sr.ht-admins/<20190302181207.GA13778%40cirno.my.domain> 46 + if ! test -e ${statePath}/webhook; then 47 + # Update ${iniKey}'s users' profile copy to the latest 48 + ${cfg.python}/bin/srht-update-profiles ${iniKey} 49 + 50 + touch ${statePath}/webhook 51 + fi 52 + 53 + ${optionalString (builtins.hasAttr "migrate-on-upgrade" cfgIni && cfgIni.migrate-on-upgrade == "yes") '' 54 + if [ "$(cat ${statePath}/db)" != "${serviceDrv.version}" ]; then 55 + # Manage schema migrations using alembic 56 + ${cfg.python}/bin/${serviceDrv.pname}-migrate -a upgrade head 57 + 58 + # Mark down current package version 59 + printf "%s" "${serviceDrv.version}" > ${statePath}/db 60 + fi 61 + ''} 62 + 63 + ${attrs.preStart or ""} 64 + ''; 65 + } 66 + (builtins.removeAttrs attrs [ "path" "preStart" ])
+115
nixos/modules/services/misc/sourcehut/sourcehut.xml
···
··· 1 + <chapter xmlns="http://docbook.org/ns/docbook" 2 + xmlns:xlink="http://www.w3.org/1999/xlink" 3 + xmlns:xi="http://www.w3.org/2001/XInclude" 4 + version="5.0" 5 + xml:id="module-services-sourcehut"> 6 + <title>Sourcehut</title> 7 + <para> 8 + <link xlink:href="https://sr.ht.com/">Sourcehut</link> is an open-source, 9 + self-hostable software development platform. The server setup can be automated using 10 + <link linkend="opt-services.sourcehut.enable">services.sourcehut</link>. 11 + </para> 12 + 13 + <section xml:id="module-services-sourcehut-basic-usage"> 14 + <title>Basic usage</title> 15 + <para> 16 + Sourcehut is a Python and Go based set of applications. 17 + <literal><link linkend="opt-services.sourcehut.enable">services.sourcehut</link></literal> 18 + by default will use 19 + <literal><link linkend="opt-services.nginx.enable">services.nginx</link></literal>, 20 + <literal><link linkend="opt-services.nginx.enable">services.redis</link></literal>, 21 + <literal><link linkend="opt-services.nginx.enable">services.cron</link></literal>, 22 + and 23 + <literal><link linkend="opt-services.postgresql.enable">services.postgresql</link></literal>. 24 + </para> 25 + 26 + <para> 27 + A very basic configuration may look like this: 28 + <programlisting> 29 + { pkgs, ... }: 30 + let 31 + fqdn = 32 + let 33 + join = hostName: domain: hostName + optionalString (domain != null) ".${domain}"; 34 + in join config.networking.hostName config.networking.domain; 35 + in { 36 + 37 + networking = { 38 + <link linkend="opt-networking.hostName">hostName</link> = "srht"; 39 + <link linkend="opt-networking.domain">domain</link> = "tld"; 40 + <link linkend="opt-networking.firewall.allowedTCPPorts">firewall.allowedTCPPorts</link> = [ 22 80 443 ]; 41 + }; 42 + 43 + services.sourcehut = { 44 + <link linkend="opt-services.sourcehut.enable">enable</link> = true; 45 + <link linkend="opt-services.sourcehut.originBase">originBase</link> = fqdn; 46 + <link linkend="opt-services.sourcehut.services">services</link> = [ "meta" "man" "git" ]; 47 + <link linkend="opt-services.sourcehut.settings">settings</link> = { 48 + "sr.ht" = { 49 + environment = "production"; 50 + global-domain = fqdn; 51 + origin = "https://${fqdn}"; 52 + # Produce keys with srht-keygen from <package>sourcehut.coresrht</package>. 53 + network-key = "SECRET"; 54 + service-key = "SECRET"; 55 + }; 56 + webhooks.private-key= "SECRET"; 57 + }; 58 + }; 59 + 60 + <link linkend="opt-security.acme.certs._name_.extraDomainNames">security.acme.certs."${fqdn}".extraDomainNames</link> = [ 61 + "meta.${fqdn}" 62 + "man.${fqdn}" 63 + "git.${fqdn}" 64 + ]; 65 + 66 + services.nginx = { 67 + <link linkend="opt-services.nginx.enable">enable</link> = true; 68 + # only recommendedProxySettings are strictly required, but the rest make sense as well. 69 + <link linkend="opt-services.nginx.recommendedTlsSettings">recommendedTlsSettings</link> = true; 70 + <link linkend="opt-services.nginx.recommendedOptimisation">recommendedOptimisation</link> = true; 71 + <link linkend="opt-services.nginx.recommendedGzipSettings">recommendedGzipSettings</link> = true; 72 + <link linkend="opt-services.nginx.recommendedProxySettings">recommendedProxySettings</link> = true; 73 + 74 + # Settings to setup what certificates are used for which endpoint. 75 + <link linkend="opt-services.nginx.virtualHosts">virtualHosts</link> = { 76 + <link linkend="opt-services.nginx.virtualHosts._name_.enableACME">"${fqdn}".enableACME</link> = true; 77 + <link linkend="opt-services.nginx.virtualHosts._name_.useACMEHost">"meta.${fqdn}".useACMEHost</link> = fqdn: 78 + <link linkend="opt-services.nginx.virtualHosts._name_.useACMEHost">"man.${fqdn}".useACMEHost</link> = fqdn: 79 + <link linkend="opt-services.nginx.virtualHosts._name_.useACMEHost">"git.${fqdn}".useACMEHost</link> = fqdn: 80 + }; 81 + }; 82 + } 83 + </programlisting> 84 + </para> 85 + 86 + <para> 87 + The <literal>hostName</literal> option is used internally to configure the nginx 88 + reverse-proxy. The <literal>settings</literal> attribute set is 89 + used by the configuration generator and the result is placed in <literal>/etc/sr.ht/config.ini</literal>. 90 + </para> 91 + </section> 92 + 93 + <section xml:id="module-services-sourcehut-configuration"> 94 + <title>Configuration</title> 95 + 96 + <para> 97 + All configuration parameters are also stored in 98 + <literal>/etc/sr.ht/config.ini</literal> which is generated by 99 + the module and linked from the store to ensure that all values from <literal>config.ini</literal> 100 + can be modified by the module. 101 + </para> 102 + 103 + </section> 104 + 105 + <section xml:id="module-services-sourcehut-httpd"> 106 + <title>Using an alternative webserver as reverse-proxy (e.g. <literal>httpd</literal>)</title> 107 + <para> 108 + By default, <package>nginx</package> is used as reverse-proxy for <package>sourcehut</package>. 109 + However, it's possible to use e.g. <package>httpd</package> by explicitly disabling 110 + <package>nginx</package> using <xref linkend="opt-services.nginx.enable" /> and fixing the 111 + <literal>settings</literal>. 112 + </para> 113 + </section> 114 + 115 + </chapter>
+161
nixos/modules/services/misc/sourcehut/todo.nix
···
··· 1 + { config, lib, pkgs, ... }: 2 + 3 + with lib; 4 + let 5 + cfg = config.services.sourcehut; 6 + cfgIni = cfg.settings; 7 + scfg = cfg.todo; 8 + iniKey = "todo.sr.ht"; 9 + 10 + rcfg = config.services.redis; 11 + drv = pkgs.sourcehut.todosrht; 12 + in 13 + { 14 + options.services.sourcehut.todo = { 15 + user = mkOption { 16 + type = types.str; 17 + default = "todosrht"; 18 + description = '' 19 + User for todo.sr.ht. 20 + ''; 21 + }; 22 + 23 + port = mkOption { 24 + type = types.port; 25 + default = 5003; 26 + description = '' 27 + Port on which the "todo" module should listen. 28 + ''; 29 + }; 30 + 31 + database = mkOption { 32 + type = types.str; 33 + default = "todo.sr.ht"; 34 + description = '' 35 + PostgreSQL database name for todo.sr.ht. 36 + ''; 37 + }; 38 + 39 + statePath = mkOption { 40 + type = types.path; 41 + default = "${cfg.statePath}/todosrht"; 42 + description = '' 43 + State path for todo.sr.ht. 44 + ''; 45 + }; 46 + }; 47 + 48 + config = with scfg; lib.mkIf (cfg.enable && elem "todo" cfg.services) { 49 + users = { 50 + users = { 51 + "${user}" = { 52 + isSystemUser = true; 53 + group = user; 54 + extraGroups = [ "postfix" ]; 55 + description = "todo.sr.ht user"; 56 + }; 57 + }; 58 + groups = { 59 + "${user}" = { }; 60 + }; 61 + }; 62 + 63 + services.postgresql = { 64 + authentication = '' 65 + local ${database} ${user} trust 66 + ''; 67 + ensureDatabases = [ database ]; 68 + ensureUsers = [ 69 + { 70 + name = user; 71 + ensurePermissions = { "DATABASE \"${database}\"" = "ALL PRIVILEGES"; }; 72 + } 73 + ]; 74 + }; 75 + 76 + systemd = { 77 + tmpfiles.rules = [ 78 + "d ${statePath} 0750 ${user} ${user} -" 79 + ]; 80 + 81 + services = { 82 + todosrht = import ./service.nix { inherit config pkgs lib; } scfg drv iniKey { 83 + after = [ "postgresql.service" "network.target" ]; 84 + requires = [ "postgresql.service" ]; 85 + wantedBy = [ "multi-user.target" ]; 86 + 87 + description = "todo.sr.ht website service"; 88 + 89 + serviceConfig.ExecStart = "${cfg.python}/bin/gunicorn ${drv.pname}.app:app -b ${cfg.address}:${toString port}"; 90 + }; 91 + 92 + todosrht-lmtp = { 93 + after = [ "postgresql.service" "network.target" ]; 94 + bindsTo = [ "postgresql.service" ]; 95 + wantedBy = [ "multi-user.target" ]; 96 + 97 + description = "todo.sr.ht process service"; 98 + serviceConfig = { 99 + Type = "simple"; 100 + User = user; 101 + Restart = "always"; 102 + ExecStart = "${cfg.python}/bin/todosrht-lmtp"; 103 + }; 104 + }; 105 + 106 + todosrht-webhooks = { 107 + after = [ "postgresql.service" "network.target" ]; 108 + requires = [ "postgresql.service" ]; 109 + wantedBy = [ "multi-user.target" ]; 110 + 111 + description = "todo.sr.ht webhooks service"; 112 + serviceConfig = { 113 + Type = "simple"; 114 + User = user; 115 + Restart = "always"; 116 + ExecStart = "${cfg.python}/bin/celery -A ${drv.pname}.webhooks worker --loglevel=info"; 117 + }; 118 + 119 + }; 120 + }; 121 + }; 122 + 123 + services.sourcehut.settings = { 124 + # URL todo.sr.ht is being served at (protocol://domain) 125 + "todo.sr.ht".origin = mkDefault "http://todo.${cfg.originBase}"; 126 + # Address and port to bind the debug server to 127 + "todo.sr.ht".debug-host = mkDefault "0.0.0.0"; 128 + "todo.sr.ht".debug-port = mkDefault port; 129 + # Configures the SQLAlchemy connection string for the database. 130 + "todo.sr.ht".connection-string = mkDefault "postgresql:///${database}?user=${user}&host=/var/run/postgresql"; 131 + # Set to "yes" to automatically run migrations on package upgrade. 132 + "todo.sr.ht".migrate-on-upgrade = mkDefault "yes"; 133 + # todo.sr.ht's OAuth client ID and secret for meta.sr.ht 134 + # Register your client at meta.example.org/oauth 135 + "todo.sr.ht".oauth-client-id = mkDefault null; 136 + "todo.sr.ht".oauth-client-secret = mkDefault null; 137 + # Outgoing email for notifications generated by users 138 + "todo.sr.ht".notify-from = mkDefault "CHANGEME@example.org"; 139 + # The redis connection used for the webhooks worker 140 + "todo.sr.ht".webhooks = mkDefault "redis://${rcfg.bind}:${toString rcfg.port}/1"; 141 + # Network-key 142 + "todo.sr.ht".network-key = mkDefault null; 143 + 144 + # Path for the lmtp daemon's unix socket. Direct incoming mail to this socket. 145 + # Alternatively, specify IP:PORT and an SMTP server will be run instead. 146 + "todo.sr.ht::mail".sock = mkDefault "/tmp/todo.sr.ht-lmtp.sock"; 147 + # The lmtp daemon will make the unix socket group-read/write for users in this 148 + # group. 149 + "todo.sr.ht::mail".sock-group = mkDefault "postfix"; 150 + 151 + "todo.sr.ht::mail".posting-domain = mkDefault "todo.${cfg.originBase}"; 152 + }; 153 + 154 + services.nginx.virtualHosts."todo.${cfg.originBase}" = { 155 + forceSSL = true; 156 + locations."/".proxyPass = "http://${cfg.address}:${toString port}"; 157 + locations."/query".proxyPass = "http://${cfg.address}:${toString (port + 100)}"; 158 + locations."/static".root = "${pkgs.sourcehut.todosrht}/${pkgs.sourcehut.python.sitePackages}/todosrht"; 159 + }; 160 + }; 161 + }
+29
nixos/tests/sourcehut.nix
···
··· 1 + import ./make-test-python.nix ({ pkgs, ... }: 2 + 3 + { 4 + name = "sourcehut"; 5 + 6 + meta.maintainers = [ pkgs.lib.maintainers.tomberek ]; 7 + 8 + machine = { config, pkgs, ... }: { 9 + virtualisation.memorySize = 2048; 10 + networking.firewall.allowedTCPPorts = [ 80 ]; 11 + 12 + services.sourcehut = { 13 + enable = true; 14 + services = [ "meta" ]; 15 + originBase = "sourcehut"; 16 + settings."sr.ht".service-key = "8888888888888888888888888888888888888888888888888888888888888888"; 17 + settings."sr.ht".network-key = "0000000000000000000000000000000000000000000="; 18 + settings.webhooks.private-key = "0000000000000000000000000000000000000000000="; 19 + }; 20 + }; 21 + 22 + testScript = '' 23 + start_all() 24 + machine.wait_for_unit("multi-user.target") 25 + machine.wait_for_unit("metasrht.service") 26 + machine.wait_for_open_port(5000) 27 + machine.succeed("curl -sL http://localhost:5000 | grep meta.sourcehut") 28 + ''; 29 + })
+1
pkgs/applications/version-management/sourcehut/default.nix
··· 31 in 32 with python.pkgs; recurseIntoAttrs { 33 inherit python; 34 buildsrht = toPythonApplication buildsrht; 35 dispatchsrht = toPythonApplication dispatchsrht; 36 gitsrht = toPythonApplication gitsrht;
··· 31 in 32 with python.pkgs; recurseIntoAttrs { 33 inherit python; 34 + coresrht = toPythonApplication srht; 35 buildsrht = toPythonApplication buildsrht; 36 dispatchsrht = toPythonApplication dispatchsrht; 37 gitsrht = toPythonApplication gitsrht;