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