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