]> Git — Sourcephile - sourcephile-nix.git/blob - nixos/modules/services/misc/sourcehut/service.nix
sourcehut: wip
[sourcephile-nix.git] / nixos / modules / services / misc / sourcehut / service.nix
1 srv:
2 { configIniOfService
3 , srvsrht ? "${srv}srht" # Because "buildsrht" does not follow that pattern (missing an "s").
4 , iniKey ? "${srv}.sr.ht"
5 , webhooks ? null
6 , redisDatabase ? null
7 , extraTimers ? {}
8 , commonService ? {}
9 , mainService ? {}
10 , extraServices ? {}
11 , extraConfig ? {}
12 , port
13 }:
14 { config, lib, pkgs, ... }:
15
16 with lib;
17 let
18 inherit (config.services) postgresql redis;
19 inherit (config.users) users;
20 cfg = config.services.sourcehut;
21 configIni = configIniOfService srv;
22 srvCfg = cfg.${srv};
23 baseService = serviceName: { noStripe ? true }: extraService: let
24 runDir = "/run/sourcehut/${serviceName}";
25 rootDir = "/run/sourcehut/chroots/${serviceName}";
26 in
27 mkMerge [ commonService extraService {
28 path = [ pkgs.gawk ];
29 environment.HOME = runDir;
30 serviceConfig = {
31 User = srvCfg.user;
32 Group = mkDefault srvCfg.user;
33 RuntimeDirectory = [
34 "sourcehut/${serviceName}"
35 "sourcehut/chroots/${serviceName}"
36 ];
37 RuntimeDirectoryMode = "2750";
38 # No need for the chroot path once inside the chroot
39 InaccessiblePaths = [ "-+${rootDir}" ];
40 # For intermediate directories created by BindPaths= and others
41 UMask = "0066";
42 RootDirectory = rootDir;
43 RootDirectoryStartOnly = true;
44 PrivateTmp = true;
45 MountAPIVFS = true;
46 # config.ini is looked up in there, before /etc/srht/config.ini
47 # Note that it fails to be set in ExecStartPre=
48 WorkingDirectory = mkDefault ("-"+runDir);
49 BindReadOnlyPaths = [
50 builtins.storeDir
51 "/etc"
52 "/run/booted-system"
53 "/run/current-system"
54 "/run/systemd"
55 "/run/wrappers"
56 ] ++
57 optional cfg.redis.enable "/run/redis" ++
58 optional cfg.postgresql.enable "/run/postgresql";
59 # LoadCredential= are unfortunately not available in ExecStartPre=
60 # Hence this one is run as root (the +) with RootDirectoryStartOnly=
61 # to reach credentials wherever they are.
62 # Note that each systemd service gets its own ${runDir}/config.ini file.
63 ExecStartPre = mkBefore [("+"+pkgs.writeShellScript "${serviceName}-credentials" ''
64 set -x
65 # Replace values begining with a '<' by the content of the file whose name is after.
66 gawk '{ if (match($0,/^([^=]+=)<(.+)/,m)) { getline f < m[2]; print m[1] f } else print $0 }' ${configIni} |
67 ${optionalString noStripe "gawk '!/^stripe-secret-key=/' |"}
68 install -o ${srvCfg.user} -g root -m 400 /dev/stdin ${runDir}/config.ini
69 '')];
70 LogsDirectory = [ "sourcehut/${serviceName}" ];
71 # The following options are only for optimizing:
72 # systemd-analyze security
73 AmbientCapabilities = "";
74 CapabilityBoundingSet = "";
75 # ProtectClock= adds DeviceAllow=char-rtc r
76 DeviceAllow = "";
77 LockPersonality = true;
78 MemoryDenyWriteExecute = true;
79 NoNewPrivileges = true;
80 PrivateDevices = true;
81 PrivateMounts = true;
82 PrivateNetwork = mkDefault false;
83 PrivateUsers = true;
84 ProcSubset = "pid";
85 ProtectClock = true;
86 ProtectControlGroups = true;
87 ProtectHome = true;
88 ProtectHostname = true;
89 ProtectKernelLogs = true;
90 ProtectKernelModules = true;
91 ProtectKernelTunables = true;
92 ProtectProc = "invisible";
93 ProtectSystem = "strict";
94 RemoveIPC = true;
95 RestrictAddressFamilies = [ "AF_UNIX" "AF_INET" "AF_INET6" ];
96 RestrictNamespaces = true;
97 RestrictRealtime = true;
98 RestrictSUIDSGID = true;
99 #SocketBindAllow = [ "tcp:${toString srvCfg.port}" "tcp:${toString srvCfg.prometheusPort}" ];
100 #SocketBindDeny = "any";
101 SystemCallFilter = [
102 "@system-service"
103 "~@aio" "~@keyring" "~@memlock" "~@privileged" "~@resources" "~@timer"
104 "@chown" "@setuid"
105 ];
106 SystemCallArchitectures = "native";
107 };
108 } ];
109 in
110 {
111 options.services.sourcehut.${srv} = {
112 enable = mkEnableOption "${srv} service";
113
114 user = mkOption {
115 type = types.str;
116 default = srvsrht;
117 description = ''
118 User for ${srv}.sr.ht.
119 '';
120 };
121
122 port = mkOption {
123 type = types.port;
124 default = port;
125 description = ''
126 Port on which the "${srv}" module should listen.
127 '';
128 };
129
130 database = mkOption {
131 type = types.str;
132 default = "${srv}.sr.ht";
133 description = ''
134 PostgreSQL database name for ${srv}.sr.ht.
135 '';
136 };
137 };
138
139 config = lib.mkIf (cfg.enable && srvCfg.enable) (mkMerge [ extraConfig {
140 users = {
141 users = {
142 "${srvCfg.user}" = {
143 isSystemUser = true;
144 group = mkDefault srvCfg.user;
145 description = mkDefault "sourcehut user for ${srv}.sr.ht";
146 };
147 };
148 groups = {
149 "${srvCfg.user}" = { };
150 } // optionalAttrs (cfg.postgresql.enable
151 && hasSuffix "0" (postgresql.settings.unix_socket_permissions or "")) {
152 "postgres".members = [ srvCfg.user ];
153 };
154 };
155
156 services.nginx.virtualHosts = mkIf cfg.nginx.enable {
157 "${srv}.${cfg.settings."sr.ht".global-domain}" = {
158 forceSSL = true;
159 locations."/".proxyPass = "http://${cfg.listenAddress}:${toString srvCfg.port}";
160 locations."/query".proxyPass = cfg.settings."meta.sr.ht".api-origin;
161 locations."/static".root = "${pkgs.sourcehut.${srvsrht}}/${pkgs.sourcehut.python.sitePackages}/${srvsrht}";
162 };
163 };
164
165 services.postgresql = mkIf cfg.postgresql.enable {
166 authentication = ''
167 local ${srvCfg.database} ${srvCfg.user} trust
168 ''
169 # shrt-keys stores SSH keys in the PostgreSQL database of the service calling it
170 + optionalString (elem srv ["builds" "git" "hg"]) ''
171 local ${srvCfg.database} ${users."sshsrht".name} trust
172 '';
173 ensureDatabases = [ srvCfg.database ];
174 ensureUsers = map (name: {
175 inherit name;
176 ensurePermissions = { "DATABASE \"${srvCfg.database}\"" = "ALL PRIVILEGES"; };
177 }) ([srvCfg.user] ++ optional (elem srv ["builds" "git" "hg"]) users."sshsrht".name);
178 };
179
180 services.sourcehut.services = mkDefault (filter (s: cfg.${s}.enable)
181 [ "builds" "dispatch" "git" "hg" "hub" "lists" "man" "meta" "pages" "paste" "todo" ]);
182
183 services.sourcehut.settings = mkMerge [
184 {
185 "${srv}.sr.ht" = {
186 origin = mkDefault "https://${srv}.${cfg.settings."sr.ht".global-domain}";
187 };
188 }
189
190 (mkIf (cfg.redis.enable && webhooks != null) {
191 "${srv}.sr.ht".webhooks = mkDefault "redis://localhost:${toString redis.port}/${toString (cfg.redis.firstDatabase + webhooks.redisDatabase)}";
192 })
193
194 (mkIf (cfg.redis.enable && redisDatabase != null) {
195 "${srv}.sr.ht".redis = mkDefault "redis://localhost:${toString redis.port}/${toString (cfg.redis.firstDatabase + redisDatabase)}";
196 })
197
198 (mkIf cfg.postgresql.enable {
199 "${srv}.sr.ht".connection-string = mkDefault "postgresql:///${srvCfg.database}?user=${srvCfg.user}&host=/run/postgresql";
200 })
201 ];
202
203 systemd.services = mkMerge [
204 { "${srvsrht}" = baseService srvsrht { noStripe = srv != "meta"; }
205 (mkMerge [ {
206 description = "sourcehut ${srv}.sr.ht website service";
207 after = [ "network.target" ]
208 ++ optional cfg.postgresql.enable "postgresql.service"
209 ++ optional (srv != "meta" && cfg.meta.enable) "metasrht-api.service";
210 requires = optional cfg.postgresql.enable "postgresql.service";
211 wantedBy = [ "multi-user.target" ];
212 path = optional cfg.postgresql.enable config.services.postgresql.package;
213 # Beware: change in credentials' content will not trigger restart.
214 restartTriggers = [ configIni ];
215 serviceConfig = {
216 Type = "simple";
217 Restart = mkDefault "always";
218 #RestartSec = mkDefault "2min";
219 StateDirectory = [ "sourcehut/${srvsrht}" ];
220 StateDirectoryMode = "2750";
221 ExecStart = "${cfg.python}/bin/gunicorn ${srvsrht}.app:app -b ${cfg.listenAddress}:${toString srvCfg.port}";
222 };
223 preStart = let
224 version = pkgs.sourcehut.${srvsrht}.version;
225 stateDir = "/var/lib/sourcehut/${srvsrht}";
226 in mkBefore ''
227 set -x
228 # Use the /run/sourcehut/${srvsrht}/config.ini
229 # installed by a previous ExecStartPre= in baseService
230 cd /run/sourcehut/${srvsrht}
231
232 if test ! -e ${stateDir}/db; then
233 # Setup the initial database.
234 # Note that it stamps the alembic head afterward
235 ${cfg.python}/bin/${srvsrht}-initdb
236 echo ${version} >${stateDir}/db
237 fi
238
239 ${optionalString cfg.settings.${iniKey}.migrate-on-upgrade ''
240 if [ "$(cat ${stateDir}/db)" != "${version}" ]; then
241 # Manage schema migrations using alembic
242 ${cfg.python}/bin/${srvsrht}-migrate -a upgrade head
243 echo ${version} >${stateDir}/db
244 fi
245 ''}
246
247 # Update copy of each users' profile to the latest
248 # See https://lists.sr.ht/~sircmpwn/sr.ht-admins/<20190302181207.GA13778%40cirno.my.domain>
249 if test ! -e ${stateDir}/webhook; then
250 # Update ${iniKey}'s users' profile copy to the latest
251 ${cfg.python}/bin/srht-update-profiles ${iniKey}
252 touch ${stateDir}/webhook
253 fi
254 '';
255 } mainService ]);
256 }
257
258 (mkIf (webhooks != null) {
259 "${srvsrht}-webhooks" = baseService "${srvsrht}-webhooks" {}
260 {
261 description = "sourcehut ${srv}.sr.ht webhooks service";
262 after = [ "${srvsrht}.service" ]
263 ++ optional cfg.redis.enable "redis.service";
264 requires = [ "${srvsrht}.service" ]
265 ++ optional cfg.redis.enable "redis.service";
266 wantedBy = [ "${srvsrht}.service" ];
267 requiredBy = [ "${srvsrht}.service" ];
268 serviceConfig = {
269 Type = "simple";
270 User = srvCfg.user;
271 Group = mkDefault srvCfg.user;
272 Restart = "always";
273 ExecStart = "${cfg.python}/bin/celery -A ${srvsrht}.webhooks worker --loglevel INFO --pool eventlet";
274 # Avoid crashing: os.getloadavg()
275 ProcSubset = mkForce "all";
276 };
277 };
278 })
279
280 (mapAttrs (timerName: timerConfig: (baseService timerName {}
281 {
282 description = "sourcehut ${timerName} service";
283 after = [ "${srvsrht}.service" ];
284 requires = [ "${srvsrht}.service" ];
285 serviceConfig = {
286 Type = "oneshot";
287 User = srvCfg.user;
288 Group = mkDefault srvCfg.user;
289 Restart = "no";
290 ExecStart = "${cfg.python}/bin/${timerName}";
291 };
292 })
293 ) extraTimers)
294
295 (mapAttrs (serviceName: service: baseService serviceName {}
296 (mkMerge [ service {
297 description = "sourcehut ${serviceName}";
298 after = [ "${srvsrht}.service" ];
299 requires = [ "${srvsrht}.service" ];
300 wantedBy = [ "multi-user.target" ];
301 serviceConfig = {
302 Type = "simple";
303 User = srvCfg.user;
304 Group = mkDefault srvCfg.user;
305 Restart = mkDefault "always";
306 };
307 } ])
308 ) extraServices)
309 ];
310
311 systemd.timers = mapAttrs (timerName: timerConfig:
312 {
313 description = "sourcehut timer for ${timerName}";
314 wantedBy = [ "timers.target" ];
315 inherit timerConfig;
316 }) extraTimers;
317 } ]);
318 }