3 , srvsrht ? "${srv}srht" # Because "buildsrht" does not follow that pattern (missing an "s").
4 , iniKey ? "${srv}.sr.ht"
12 { config, lib, pkgs, ... }:
16 inherit (config.services) postgresql;
17 redis = config.services.redis.servers."sourcehut-${srvsrht}";
18 inherit (config.users) users;
19 cfg = config.services.sourcehut;
20 configIni = configIniOfService srv;
22 baseService = serviceName: { allowStripe ? false }: extraService: let
23 runDir = "/run/sourcehut/${serviceName}";
24 rootDir = "/run/sourcehut/chroots/${serviceName}";
26 mkMerge [ extraService {
27 after = [ "network.target" ] ++
28 optional cfg.postgresql.enable "postgresql.service" ++
29 optional cfg.redis.enable "redis-sourcehut-${srvsrht}.service";
31 optional cfg.postgresql.enable "postgresql.service" ++
32 optional cfg.redis.enable "redis-sourcehut-${srvsrht}.service";
34 environment.HOME = runDir;
36 User = mkDefault srvCfg.user;
37 Group = mkDefault srvCfg.group;
39 "sourcehut/${serviceName}"
40 # Used by *srht-keys which reads ../config.ini
41 "sourcehut/${serviceName}/subdir"
42 "sourcehut/chroots/${serviceName}"
44 RuntimeDirectoryMode = "2750";
45 # No need for the chroot path once inside the chroot
46 InaccessiblePaths = [ "-+${rootDir}" ];
47 # g+rx is for group members (eg. fcgiwrap or nginx)
48 # to read Git/Mercurial repositories, buildlogs, etc.
49 # o+x is for intermediate directories created by BindPaths= and like,
50 # as they're owned by root:root.
52 RootDirectory = rootDir;
53 RootDirectoryStartOnly = true;
56 # config.ini is looked up in there, before /etc/srht/config.ini
57 # Note that it fails to be set in ExecStartPre=
58 WorkingDirectory = mkDefault ("-"+runDir);
66 optional cfg.postgresql.enable "/run/postgresql" ++
67 optional cfg.redis.enable "/run/redis-sourcehut-${srvsrht}";
68 # LoadCredential= are unfortunately not available in ExecStartPre=
69 # Hence this one is run as root (the +) with RootDirectoryStartOnly=
70 # to reach credentials wherever they are.
71 # Note that each systemd service gets its own ${runDir}/config.ini file.
72 ExecStartPre = mkBefore [("+"+pkgs.writeShellScript "${serviceName}-credentials" ''
74 # Replace values begining with a '<' by the content of the file whose name is after.
75 gawk '{ if (match($0,/^([^=]+=)<(.+)/,m)) { getline f < m[2]; print m[1] f } else print $0 }' ${configIni} |
76 ${optionalString (!allowStripe) "gawk '!/^stripe-secret-key=/' |"}
77 install -o ${srvCfg.user} -g root -m 400 /dev/stdin ${runDir}/config.ini
79 # The following options are only for optimizing:
80 # systemd-analyze security
81 AmbientCapabilities = "";
82 CapabilityBoundingSet = "";
83 # ProtectClock= adds DeviceAllow=char-rtc r
85 LockPersonality = true;
86 MemoryDenyWriteExecute = true;
87 NoNewPrivileges = true;
88 PrivateDevices = true;
90 PrivateNetwork = mkDefault false;
94 ProtectControlGroups = true;
96 ProtectHostname = true;
97 ProtectKernelLogs = true;
98 ProtectKernelModules = true;
99 ProtectKernelTunables = true;
100 ProtectProc = "invisible";
101 ProtectSystem = "strict";
103 RestrictAddressFamilies = [ "AF_UNIX" "AF_INET" "AF_INET6" ];
104 RestrictNamespaces = true;
105 RestrictRealtime = true;
106 RestrictSUIDSGID = true;
107 #SocketBindAllow = [ "tcp:${toString srvCfg.port}" "tcp:${toString srvCfg.prometheusPort}" ];
108 #SocketBindDeny = "any";
111 "~@aio" "~@keyring" "~@memlock" "~@privileged" "~@resources" "~@timer"
114 SystemCallArchitectures = "native";
119 options.services.sourcehut.${srv} = {
120 enable = mkEnableOption "${srv} service";
126 User for ${srv}.sr.ht.
134 Group for ${srv}.sr.ht.
135 Membership grants access to the Git/Mercurial repositories by default,
136 but not to the config.ini file (where secrets are).
144 Port on which the "${srv}" backend should listen.
151 default = "unix:/run/redis-sourcehut-${srvsrht}/redis.sock?db=0";
152 example = "redis://shared.wireguard:6379/0";
154 The redis host URL. This is used for caching and temporary storage, and must
155 be shared between nodes (e.g. git1.sr.ht and git2.sr.ht), but need not be
156 shared between services. It may be shared between services, however, with no
157 ill effect, if this better suits your infrastructure.
163 database = mkOption {
165 default = "${srv}.sr.ht";
167 PostgreSQL database name for the ${srv}.sr.ht service,
168 used if <xref linkend="opt-services.sourcehut.postgresql.enable"/> is <literal>true</literal>.
174 extraArgs = mkOption {
175 type = with types; listOf str;
176 default = ["--timeout 120" "--workers 1" "--log-level=info"];
177 description = "Extra arguments passed to Gunicorn.";
180 } // optionalAttrs webhooks {
182 extraArgs = mkOption {
183 type = with types; listOf str;
184 default = ["--loglevel DEBUG" "--pool eventlet" "--without-heartbeat"];
185 description = "Extra arguments passed to the Celery responsible for webhooks.";
187 celeryConfig = mkOption {
190 description = "Content of the <literal>celeryconfig.py</literal> used by the Celery responsible for webhooks.";
195 config = lib.mkIf (cfg.enable && srvCfg.enable) (mkMerge [ extraConfig {
200 group = mkDefault srvCfg.group;
201 description = mkDefault "sourcehut user for ${srv}.sr.ht";
205 "${srvCfg.group}" = { };
206 } // optionalAttrs (cfg.postgresql.enable
207 && hasSuffix "0" (postgresql.settings.unix_socket_permissions or "")) {
208 "postgres".members = [ srvCfg.user ];
209 } // optionalAttrs (cfg.redis.enable
210 && hasSuffix "0" (redis.settings.unixsocketperm or "")) {
211 "redis-sourcehut-${srvsrht}".members = [ srvCfg.user ];
215 services.nginx = mkIf cfg.nginx.enable {
216 virtualHosts."${srv}.${cfg.settings."sr.ht".global-domain}" = mkMerge [ {
218 locations."/".proxyPass = "http://${cfg.listenAddress}:${toString srvCfg.port}";
219 locations."/static" = {
220 root = "${pkgs.sourcehut.${srvsrht}}/${pkgs.sourcehut.python.sitePackages}/${srvsrht}";
221 extraConfig = mkDefault ''
225 } cfg.nginx.virtualHost ];
228 services.postgresql = mkIf cfg.postgresql.enable {
230 local ${srvCfg.postgresql.database} ${srvCfg.user} trust
232 ensureDatabases = [ srvCfg.postgresql.database ];
233 ensureUsers = map (name: {
235 ensurePermissions = { "DATABASE \"${srvCfg.postgresql.database}\"" = "ALL PRIVILEGES"; };
239 services.sourcehut.services = mkDefault (filter (s: cfg.${s}.enable)
240 [ "builds" "dispatch" "git" "hg" "hub" "lists" "man" "meta" "pages" "paste" "todo" ]);
242 services.sourcehut.settings = mkMerge [
244 "${srv}.sr.ht".origin = mkDefault "https://${srv}.${cfg.settings."sr.ht".global-domain}";
247 (mkIf cfg.postgresql.enable {
248 "${srv}.sr.ht".connection-string = mkDefault "postgresql:///${srvCfg.postgresql.database}?user=${srvCfg.user}&host=/run/postgresql";
252 services.redis.servers."sourcehut-${srvsrht}" = mkIf cfg.redis.enable {
256 # TODO: set a more informed value
257 save = mkDefault [ [1800 10] [300 100] ];
259 # TODO: set a more informed value
261 maxmemory-policy = "volatile-ttl";
265 systemd.services = mkMerge [
267 "${srvsrht}" = baseService srvsrht { allowStripe = srv == "meta"; } (mkMerge [
269 description = "sourcehut ${srv}.sr.ht website service";
270 before = optional cfg.nginx.enable "nginx.service";
271 wants = optional cfg.nginx.enable "nginx.service";
272 wantedBy = [ "multi-user.target" ];
273 path = optional cfg.postgresql.enable postgresql.package;
274 # Beware: change in credentials' content will not trigger restart.
275 restartTriggers = [ configIni ];
278 Restart = mkDefault "always";
279 #RestartSec = mkDefault "2min";
280 StateDirectory = [ "sourcehut/${srvsrht}" ];
281 StateDirectoryMode = "2750";
282 ExecStart = "${cfg.python}/bin/gunicorn ${srvsrht}.app:app --name ${srvsrht} --bind ${cfg.listenAddress}:${toString srvCfg.port} " + concatStringsSep " " srvCfg.gunicorn.extraArgs;
285 version = pkgs.sourcehut.${srvsrht}.version;
286 stateDir = "/var/lib/sourcehut/${srvsrht}";
289 # Use the /run/sourcehut/${srvsrht}/config.ini
290 # installed by a previous ExecStartPre= in baseService
291 cd /run/sourcehut/${srvsrht}
293 if test ! -e ${stateDir}/db; then
294 # Setup the initial database.
295 # Note that it stamps the alembic head afterward
296 ${cfg.python}/bin/${srvsrht}-initdb
297 echo ${version} >${stateDir}/db
300 ${optionalString cfg.settings.${iniKey}.migrate-on-upgrade ''
301 if [ "$(cat ${stateDir}/db)" != "${version}" ]; then
302 # Manage schema migrations using alembic
303 ${cfg.python}/bin/${srvsrht}-migrate -a upgrade head
304 echo ${version} >${stateDir}/db
308 # Update copy of each users' profile to the latest
309 # See https://lists.sr.ht/~sircmpwn/sr.ht-admins/<20190302181207.GA13778%40cirno.my.domain>
310 if test ! -e ${stateDir}/webhook; then
311 # Update ${iniKey}'s users' profile copy to the latest
312 ${cfg.python}/bin/srht-update-profiles ${iniKey}
313 touch ${stateDir}/webhook
320 "${srvsrht}-webhooks" = baseService "${srvsrht}-webhooks" {}
322 description = "sourcehut ${srv}.sr.ht webhooks service";
323 after = [ "${srvsrht}.service" ];
324 wantedBy = [ "${srvsrht}.service" ];
325 partOf = [ "${srvsrht}.service" ];
327 cp ${pkgs.writeText "${srvsrht}-webhooks-celeryconfig.py" srvCfg.webhooks.celeryConfig} \
328 /run/sourcehut/${srvsrht}-webhooks/celeryconfig.py
333 ExecStart = "${cfg.python}/bin/celery --app ${srvsrht}.webhooks worker --hostname ${srvsrht}-webhooks@%%h " + concatStringsSep " " srvCfg.webhooks.extraArgs;
334 # Avoid crashing: os.getloadavg()
335 ProcSubset = mkForce "all";
340 (mapAttrs (timerName: timer: (baseService timerName {} (mkMerge [
342 description = "sourcehut ${timerName} service";
343 after = [ "network.target" "${srvsrht}.service" ];
346 ExecStart = "${cfg.python}/bin/${timerName}";
349 (timer.service or {})
352 (mapAttrs (serviceName: extraService: baseService serviceName {} (mkMerge [
354 description = "sourcehut ${serviceName} service";
355 # So that extraServices have the PostgreSQL database initialized.
356 after = [ "${srvsrht}.service" ];
357 wantedBy = [ "${srvsrht}.service" ];
358 partOf = [ "${srvsrht}.service" ];
361 Restart = mkDefault "always";
368 systemd.timers = mapAttrs (timerName: timer:
370 description = "sourcehut timer for ${timerName}";
371 wantedBy = [ "timers.target" ];
372 inherit (timer) timerConfig;