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;