]> Git — Sourcephile - sourcephile-nix.git/blob - nixos/modules/services/misc/sourcehut/builds.nix
sourcehut: commit upstream NixOS modules
[sourcephile-nix.git] / nixos / modules / services / misc / sourcehut / builds.nix
1 { config, lib, pkgs, ... }:
2
3 with lib;
4 let
5 cfg = config.services.sourcehut;
6 scfg = cfg.builds;
7 rcfg = config.services.redis;
8 iniKey = "builds.sr.ht";
9
10 drv = pkgs.sourcehut.buildsrht;
11 in
12 {
13 options.services.sourcehut.builds = {
14 user = mkOption {
15 type = types.str;
16 default = "buildsrht";
17 description = ''
18 User for builds.sr.ht.
19 '';
20 };
21
22 port = mkOption {
23 type = types.port;
24 default = 5002;
25 description = ''
26 Port on which the "builds" module should listen.
27 '';
28 };
29
30 database = mkOption {
31 type = types.str;
32 default = "builds.sr.ht";
33 description = ''
34 PostgreSQL database name for builds.sr.ht.
35 '';
36 };
37
38 statePath = mkOption {
39 type = types.path;
40 default = "${cfg.statePath}/buildsrht";
41 description = ''
42 State path for builds.sr.ht.
43 '';
44 };
45
46 enableWorker = mkOption {
47 type = types.bool;
48 default = false;
49 description = ''
50 Run workers for builds.sr.ht.
51 '';
52 };
53
54 images = mkOption {
55 type = types.attrsOf (types.attrsOf (types.attrsOf types.package));
56 default = { };
57 example = lib.literalExample ''(let
58 # Pinning unstable to allow usage with flakes and limit rebuilds.
59 pkgs_unstable = builtins.fetchGit {
60 url = "https://github.com/NixOS/nixpkgs";
61 rev = "ff96a0fa5635770390b184ae74debea75c3fd534";
62 ref = "nixos-unstable";
63 };
64 image_from_nixpkgs = pkgs_unstable: (import ("${pkgs.sourcehut.buildsrht}/lib/images/nixos/image.nix") {
65 pkgs = (import pkgs_unstable {});
66 });
67 in
68 {
69 nixos.unstable.x86_64 = image_from_nixpkgs pkgs_unstable;
70 }
71 )'';
72 description = ''
73 Images for builds.sr.ht. Each package should be distro.release.arch and point to a /nix/store/package/root.img.qcow2.
74 '';
75 };
76
77 };
78
79 config = with scfg; let
80 image_dirs = lib.lists.flatten (
81 lib.attrsets.mapAttrsToList
82 (distro: revs:
83 lib.attrsets.mapAttrsToList
84 (rev: archs:
85 lib.attrsets.mapAttrsToList
86 (arch: image:
87 pkgs.runCommandNoCC "buildsrht-images" { } ''
88 mkdir -p $out/${distro}/${rev}/${arch}
89 ln -s ${image}/*.qcow2 $out/${distro}/${rev}/${arch}/root.img.qcow2
90 '')
91 archs)
92 revs)
93 scfg.images);
94 image_dir_pre = pkgs.symlinkJoin {
95 name = "builds.sr.ht-worker-images-pre";
96 paths = image_dirs ++ [
97 "${pkgs.sourcehut.buildsrht}/lib/images"
98 ];
99 };
100 image_dir = pkgs.runCommandNoCC "builds.sr.ht-worker-images" { } ''
101 mkdir -p $out/images
102 cp -Lr ${image_dir_pre}/* $out/images
103 '';
104 in
105 lib.mkIf (cfg.enable && elem "builds" cfg.services) {
106 users = {
107 users = {
108 "${user}" = {
109 isSystemUser = true;
110 group = user;
111 extraGroups = lib.optionals cfg.builds.enableWorker [ "docker" ];
112 description = "builds.sr.ht user";
113 };
114 };
115
116 groups = {
117 "${user}" = { };
118 };
119 };
120
121 services.postgresql = {
122 authentication = ''
123 local ${database} ${user} trust
124 '';
125 ensureDatabases = [ database ];
126 ensureUsers = [
127 {
128 name = user;
129 ensurePermissions = { "DATABASE \"${database}\"" = "ALL PRIVILEGES"; };
130 }
131 ];
132 };
133
134 systemd = {
135 tmpfiles.rules = [
136 "d ${statePath} 0755 ${user} ${user} -"
137 ] ++ (lib.optionals cfg.builds.enableWorker
138 [ "d ${statePath}/logs 0775 ${user} ${user} - -" ]
139 );
140
141 services = {
142 buildsrht = import ./service.nix { inherit config pkgs lib; } scfg drv iniKey
143 {
144 after = [ "postgresql.service" "network.target" ];
145 requires = [ "postgresql.service" ];
146 wantedBy = [ "multi-user.target" ];
147
148 description = "builds.sr.ht website service";
149
150 serviceConfig.ExecStart = "${cfg.python}/bin/gunicorn ${drv.pname}.app:app -b ${cfg.address}:${toString port}";
151
152 # Hack to bypass this hack: https://git.sr.ht/~sircmpwn/core.sr.ht/tree/master/item/srht-update-profiles#L6
153 } // { preStart = " "; };
154
155 buildsrht-worker = {
156 enable = scfg.enableWorker;
157 after = [ "postgresql.service" "network.target" ];
158 requires = [ "postgresql.service" ];
159 wantedBy = [ "multi-user.target" ];
160 partOf = [ "buildsrht.service" ];
161 description = "builds.sr.ht worker service";
162 path = [ pkgs.openssh pkgs.docker ];
163 preStart = let qemuPackage = pkgs.qemu_kvm;
164 in ''
165 if [[ "$(docker images -q qemu:latest 2> /dev/null)" == "" || "$(cat ${statePath}/docker-image-qemu 2> /dev/null || true)" != "${qemuPackage.version}" ]]; then
166 # Create and import qemu:latest image for docker
167 ${
168 pkgs.dockerTools.streamLayeredImage {
169 name = "qemu";
170 tag = "latest";
171 contents = [ qemuPackage ];
172 }
173 } | docker load
174 # Mark down current package version
175 printf "%s" "${qemuPackage.version}" > ${statePath}/docker-image-qemu
176 fi
177 '';
178 serviceConfig = {
179 Type = "simple";
180 User = user;
181 Group = "nginx";
182 Restart = "always";
183 };
184 serviceConfig.ExecStart = "${pkgs.sourcehut.buildsrht}/bin/builds.sr.ht-worker";
185 };
186 };
187 };
188
189 services.sourcehut.settings = {
190 # URL builds.sr.ht is being served at (protocol://domain)
191 "builds.sr.ht".origin = mkDefault "http://builds.${cfg.originBase}";
192 # Address and port to bind the debug server to
193 "builds.sr.ht".debug-host = mkDefault "0.0.0.0";
194 "builds.sr.ht".debug-port = mkDefault port;
195 # Configures the SQLAlchemy connection string for the database.
196 "builds.sr.ht".connection-string = mkDefault "postgresql:///${database}?user=${user}&host=/var/run/postgresql";
197 # Set to "yes" to automatically run migrations on package upgrade.
198 "builds.sr.ht".migrate-on-upgrade = mkDefault "yes";
199 # builds.sr.ht's OAuth client ID and secret for meta.sr.ht
200 # Register your client at meta.example.org/oauth
201 "builds.sr.ht".oauth-client-id = mkDefault null;
202 "builds.sr.ht".oauth-client-secret = mkDefault null;
203 # The redis connection used for the celery worker
204 "builds.sr.ht".redis = mkDefault "redis://${rcfg.bind}:${toString rcfg.port}/3";
205 # The shell used for ssh
206 "builds.sr.ht".shell = mkDefault "runner-shell";
207 # Register the builds.sr.ht dispatcher
208 "git.sr.ht::dispatch".${builtins.unsafeDiscardStringContext "${pkgs.sourcehut.buildsrht}/bin/buildsrht-keys"} = mkDefault "${user}:${user}";
209
210 # Location for build logs, images, and control command
211 } // lib.attrsets.optionalAttrs scfg.enableWorker {
212 # Default worker stores logs that are accessible via this address:port
213 "builds.sr.ht::worker".name = mkDefault "127.0.0.1:5020";
214 "builds.sr.ht::worker".buildlogs = mkDefault "${scfg.statePath}/logs";
215 "builds.sr.ht::worker".images = mkDefault "${image_dir}/images";
216 "builds.sr.ht::worker".controlcmd = mkDefault "${image_dir}/images/control";
217 "builds.sr.ht::worker".timeout = mkDefault "3m";
218 };
219
220 services.nginx.virtualHosts."logs.${cfg.originBase}" =
221 if scfg.enableWorker then {
222 listen = with builtins; let address = split ":" cfg.settings."builds.sr.ht::worker".name;
223 in [{ addr = elemAt address 0; port = lib.toInt (elemAt address 2); }];
224 locations."/logs".root = "${scfg.statePath}";
225 } else { };
226
227 services.nginx.virtualHosts."builds.${cfg.originBase}" = {
228 forceSSL = true;
229 locations."/".proxyPass = "http://${cfg.address}:${toString port}";
230 locations."/query".proxyPass = "http://${cfg.address}:${toString (port + 100)}";
231 locations."/static".root = "${pkgs.sourcehut.buildsrht}/${pkgs.sourcehut.python.sitePackages}/buildsrht";
232 };
233 };
234 }