vuls: init at 0.27.0
[NixPkgs.git] / nixos / modules / services / continuous-integration / hydra / default.nix
blob189aaf86dd2d9fe890f2462fbaec0e993a3e8eb2
1 { config, pkgs, lib, ... }:
2 let
4   cfg = config.services.hydra;
6   baseDir = "/var/lib/hydra";
8   hydraConf = pkgs.writeScript "hydra.conf" cfg.extraConfig;
10   hydraEnv =
11     { HYDRA_DBI = cfg.dbi;
12       HYDRA_CONFIG = "${baseDir}/hydra.conf";
13       HYDRA_DATA = "${baseDir}";
14     };
16   env =
17     { NIX_REMOTE = "daemon";
18       PGPASSFILE = "${baseDir}/pgpass";
19       NIX_REMOTE_SYSTEMS = lib.concatStringsSep ":" cfg.buildMachinesFiles;
20     } // lib.optionalAttrs (cfg.smtpHost != null) {
21       EMAIL_SENDER_TRANSPORT = "SMTP";
22       EMAIL_SENDER_TRANSPORT_host = cfg.smtpHost;
23     } // hydraEnv // cfg.extraEnv;
25   serverEnv = env //
26     { HYDRA_TRACKER = cfg.tracker;
27       XDG_CACHE_HOME = "${baseDir}/www/.cache";
28       COLUMNS = "80";
29       PGPASSFILE = "${baseDir}/pgpass-www"; # grrr
30     } // (lib.optionalAttrs cfg.debugServer { DBIC_TRACE = "1"; });
32   localDB = "dbi:Pg:dbname=hydra;user=hydra;";
34   haveLocalDB = cfg.dbi == localDB;
36   hydra-package =
37   let
38     makeWrapperArgs = lib.concatStringsSep " " (lib.mapAttrsToList (key: value: "--set-default \"${key}\" \"${value}\"") hydraEnv);
39   in pkgs.buildEnv rec {
40     name = "hydra-env";
41     nativeBuildInputs = [ pkgs.makeWrapper ];
42     paths = [ cfg.package ];
44     postBuild = ''
45       if [ -L "$out/bin" ]; then
46           unlink "$out/bin"
47       fi
48       mkdir -p "$out/bin"
50       for path in ${lib.concatStringsSep " " paths}; do
51         if [ -d "$path/bin" ]; then
52           cd "$path/bin"
53           for prg in *; do
54             if [ -f "$prg" ]; then
55               rm -f "$out/bin/$prg"
56               if [ -x "$prg" ]; then
57                 makeWrapper "$path/bin/$prg" "$out/bin/$prg" ${makeWrapperArgs}
58               fi
59             fi
60           done
61         fi
62       done
63    '';
64   };
69   ###### interface
70   options = {
72     services.hydra = {
74       enable = lib.mkOption {
75         type = lib.types.bool;
76         default = false;
77         description = ''
78           Whether to run Hydra services.
79         '';
80       };
82       dbi = lib.mkOption {
83         type = lib.types.str;
84         default = localDB;
85         example = "dbi:Pg:dbname=hydra;host=postgres.example.org;user=foo;";
86         description = ''
87           The DBI string for Hydra database connection.
89           NOTE: Attempts to set `application_name` will be overridden by
90           `hydra-TYPE` (where TYPE is e.g. `evaluator`, `queue-runner`,
91           etc.) in all hydra services to more easily distinguish where
92           queries are coming from.
93         '';
94       };
96       package = lib.mkPackageOption pkgs "hydra" { };
98       hydraURL = lib.mkOption {
99         type = lib.types.str;
100         description = ''
101           The base URL for the Hydra webserver instance. Used for links in emails.
102         '';
103       };
105       listenHost = lib.mkOption {
106         type = lib.types.str;
107         default = "*";
108         example = "localhost";
109         description = ''
110           The hostname or address to listen on or `*` to listen
111           on all interfaces.
112         '';
113       };
115       port = lib.mkOption {
116         type = lib.types.port;
117         default = 3000;
118         description = ''
119           TCP port the web server should listen to.
120         '';
121       };
123       minimumDiskFree = lib.mkOption {
124         type = lib.types.int;
125         default = 0;
126         description = ''
127           Threshold of minimum disk space (GiB) to determine if the queue runner should run or not.
128         '';
129       };
131       minimumDiskFreeEvaluator = lib.mkOption {
132         type = lib.types.int;
133         default = 0;
134         description = ''
135           Threshold of minimum disk space (GiB) to determine if the evaluator should run or not.
136         '';
137       };
139       notificationSender = lib.mkOption {
140         type = lib.types.str;
141         description = ''
142           Sender email address used for email notifications.
143         '';
144       };
146       smtpHost = lib.mkOption {
147         type = lib.types.nullOr lib.types.str;
148         default = null;
149         example = "localhost";
150         description = ''
151           Hostname of the SMTP server to use to send email.
152         '';
153       };
155       tracker = lib.mkOption {
156         type = lib.types.str;
157         default = "";
158         description = ''
159           Piece of HTML that is included on all pages.
160         '';
161       };
163       logo = lib.mkOption {
164         type = lib.types.nullOr lib.types.path;
165         default = null;
166         description = ''
167           Path to a file containing the logo of your Hydra instance.
168         '';
169       };
171       debugServer = lib.mkOption {
172         type = lib.types.bool;
173         default = false;
174         description = "Whether to run the server in debug mode.";
175       };
177       maxServers = lib.mkOption {
178         type = lib.types.int;
179         default = 25;
180         description = "Maximum number of starman workers to spawn.";
181       };
183       minSpareServers = lib.mkOption {
184         type = lib.types.int;
185         default = 4;
186         description = "Minimum number of spare starman workers to keep.";
187       };
189       maxSpareServers = lib.mkOption {
190         type = lib.types.int;
191         default = 5;
192         description = "Maximum number of spare starman workers to keep.";
193       };
195       extraConfig = lib.mkOption {
196         type = lib.types.lines;
197         description = "Extra lines for the Hydra configuration.";
198       };
200       extraEnv = lib.mkOption {
201         type = lib.types.attrsOf lib.types.str;
202         default = {};
203         description = "Extra environment variables for Hydra.";
204       };
206       gcRootsDir = lib.mkOption {
207         type = lib.types.path;
208         default = "/nix/var/nix/gcroots/hydra";
209         description = "Directory that holds Hydra garbage collector roots.";
210       };
212       buildMachinesFiles = lib.mkOption {
213         type = lib.types.listOf lib.types.path;
214         default = lib.optional (config.nix.buildMachines != []) "/etc/nix/machines";
215         defaultText = lib.literalExpression ''lib.optional (config.nix.buildMachines != []) "/etc/nix/machines"'';
216         example = [ "/etc/nix/machines" "/var/lib/hydra/provisioner/machines" ];
217         description = "List of files containing build machines.";
218       };
220       useSubstitutes = lib.mkOption {
221         type = lib.types.bool;
222         default = false;
223         description = ''
224           Whether to use binary caches for downloading store paths. Note that
225           binary substitutions trigger (a potentially large number of) additional
226           HTTP requests that slow down the queue monitor thread significantly.
227           Also, this Hydra instance will serve those downloaded store paths to
228           its users with its own signature attached as if it had built them
229           itself, so don't enable this feature unless your active binary caches
230           are absolute trustworthy.
231         '';
232       };
233     };
235   };
238   ###### implementation
240   config = lib.mkIf cfg.enable {
241     assertions = [
242       {
243         assertion = cfg.maxServers != 0 && cfg.maxSpareServers != 0 && cfg.minSpareServers != 0;
244         message = "services.hydra.{minSpareServers,maxSpareServers,minSpareServers} cannot be 0";
245       }
246       {
247         assertion = cfg.minSpareServers < cfg.maxSpareServers;
248         message = "services.hydra.minSpareServers cannot be bigger than services.hydra.maxSpareServers";
249       }
250     ];
252     users.groups.hydra = {
253       gid = config.ids.gids.hydra;
254     };
256     users.users.hydra =
257       { description = "Hydra";
258         group = "hydra";
259         # We don't enable `createHome` here because the creation of the home directory is handled by the hydra-init service below.
260         home = baseDir;
261         useDefaultShell = true;
262         uid = config.ids.uids.hydra;
263       };
265     users.users.hydra-queue-runner =
266       { description = "Hydra queue runner";
267         group = "hydra";
268         useDefaultShell = true;
269         home = "${baseDir}/queue-runner"; # really only to keep SSH happy
270         uid = config.ids.uids.hydra-queue-runner;
271       };
273     users.users.hydra-www =
274       { description = "Hydra web server";
275         group = "hydra";
276         useDefaultShell = true;
277         uid = config.ids.uids.hydra-www;
278       };
280     services.hydra.extraConfig =
281       ''
282         using_frontend_proxy = 1
283         base_uri = ${cfg.hydraURL}
284         notification_sender = ${cfg.notificationSender}
285         max_servers = ${toString cfg.maxServers}
286         ${lib.optionalString (cfg.logo != null) ''
287           hydra_logo = ${cfg.logo}
288         ''}
289         gc_roots_dir = ${cfg.gcRootsDir}
290         use-substitutes = ${if cfg.useSubstitutes then "1" else "0"}
291       '';
293     environment.systemPackages = [ hydra-package ];
295     environment.variables = hydraEnv;
297     nix.settings = lib.mkMerge [
298       {
299         keep-outputs = true;
300         keep-derivations = true;
301         trusted-users = [ "hydra-queue-runner" ];
302       }
304       (lib.mkIf (lib.versionOlder (lib.getVersion config.nix.package.out) "2.4pre")
305         {
306           # The default (`true') slows Nix down a lot since the build farm
307           # has so many GC roots.
308           gc-check-reachability = false;
309         }
310       )
311     ];
313     systemd.slices.system-hydra = {
314       description = "Hydra CI Server Slice";
315       documentation = [ "file://${cfg.package}/share/doc/hydra/index.html" "https://nixos.org/hydra/manual/" ];
316     };
318     systemd.services.hydra-init =
319       { wantedBy = [ "multi-user.target" ];
320         requires = lib.optional haveLocalDB "postgresql.service";
321         after = lib.optional haveLocalDB "postgresql.service";
322         environment = env // {
323           HYDRA_DBI = "${env.HYDRA_DBI};application_name=hydra-init";
324         };
325         path = [ pkgs.util-linux ];
326         preStart = ''
327           mkdir -p ${baseDir}
328           chown hydra:hydra ${baseDir}
329           chmod 0750 ${baseDir}
331           ln -sf ${hydraConf} ${baseDir}/hydra.conf
333           mkdir -m 0700 -p ${baseDir}/www
334           chown hydra-www:hydra ${baseDir}/www
336           mkdir -m 0700 -p ${baseDir}/queue-runner
337           mkdir -m 0750 -p ${baseDir}/build-logs
338           mkdir -m 0750 -p ${baseDir}/runcommand-logs
339           chown hydra-queue-runner:hydra \
340             ${baseDir}/queue-runner \
341             ${baseDir}/build-logs \
342             ${baseDir}/runcommand-logs
344           ${lib.optionalString haveLocalDB ''
345             if ! [ -e ${baseDir}/.db-created ]; then
346               runuser -u ${config.services.postgresql.superUser} ${config.services.postgresql.package}/bin/createuser hydra
347               runuser -u ${config.services.postgresql.superUser} ${config.services.postgresql.package}/bin/createdb -- -O hydra hydra
348               touch ${baseDir}/.db-created
349             fi
350             echo "create extension if not exists pg_trgm" | runuser -u ${config.services.postgresql.superUser} -- ${config.services.postgresql.package}/bin/psql hydra
351           ''}
353           if [ ! -e ${cfg.gcRootsDir} ]; then
355             # Move legacy roots directory.
356             if [ -e /nix/var/nix/gcroots/per-user/hydra/hydra-roots ]; then
357               mv /nix/var/nix/gcroots/per-user/hydra/hydra-roots ${cfg.gcRootsDir}
358             fi
360             mkdir -p ${cfg.gcRootsDir}
361           fi
363           # Move legacy hydra-www roots.
364           if [ -e /nix/var/nix/gcroots/per-user/hydra-www/hydra-roots ]; then
365             find /nix/var/nix/gcroots/per-user/hydra-www/hydra-roots/ -type f \
366               | xargs -r mv -f -t ${cfg.gcRootsDir}/
367             rmdir /nix/var/nix/gcroots/per-user/hydra-www/hydra-roots
368           fi
370           chown hydra:hydra ${cfg.gcRootsDir}
371           chmod 2775 ${cfg.gcRootsDir}
372         '';
373         serviceConfig.ExecStart = "${hydra-package}/bin/hydra-init";
374         serviceConfig.PermissionsStartOnly = true;
375         serviceConfig.User = "hydra";
376         serviceConfig.Type = "oneshot";
377         serviceConfig.RemainAfterExit = true;
378         serviceConfig.Slice = "system-hydra.slice";
379       };
381     systemd.services.hydra-server =
382       { wantedBy = [ "multi-user.target" ];
383         requires = [ "hydra-init.service" ];
384         after = [ "hydra-init.service" ];
385         environment = serverEnv // {
386           HYDRA_DBI = "${serverEnv.HYDRA_DBI};application_name=hydra-server";
387         };
388         restartTriggers = [ hydraConf ];
389         serviceConfig =
390           { ExecStart =
391               "@${hydra-package}/bin/hydra-server hydra-server -f -h '${cfg.listenHost}' "
392               + "-p ${toString cfg.port} --min_spare_servers ${toString cfg.minSpareServers} --max_spare_servers ${toString cfg.maxSpareServers} "
393               + "--max_servers ${toString cfg.maxServers} --max_requests 100 ${lib.optionalString cfg.debugServer "-d"}";
394             User = "hydra-www";
395             PermissionsStartOnly = true;
396             Restart = "always";
397             Slice = "system-hydra.slice";
398           };
399       };
401     systemd.services.hydra-queue-runner =
402       { wantedBy = [ "multi-user.target" ];
403         requires = [ "hydra-init.service" ];
404         after = [ "hydra-init.service" "network.target" ];
405         path = [ hydra-package pkgs.nettools pkgs.openssh pkgs.bzip2 config.nix.package ];
406         restartTriggers = [ hydraConf ];
407         environment = env // {
408           PGPASSFILE = "${baseDir}/pgpass-queue-runner"; # grrr
409           IN_SYSTEMD = "1"; # to get log severity levels
410           HYDRA_DBI = "${env.HYDRA_DBI};application_name=hydra-queue-runner";
411         };
412         serviceConfig =
413           { ExecStart = "@${hydra-package}/bin/hydra-queue-runner hydra-queue-runner -v";
414             ExecStopPost = "${hydra-package}/bin/hydra-queue-runner --unlock";
415             User = "hydra-queue-runner";
416             Restart = "always";
417             Slice = "system-hydra.slice";
419             # Ensure we can get core dumps.
420             LimitCORE = "infinity";
421             WorkingDirectory = "${baseDir}/queue-runner";
422           };
423       };
425     systemd.services.hydra-evaluator =
426       { wantedBy = [ "multi-user.target" ];
427         requires = [ "hydra-init.service" ];
428         wants = [ "network-online.target" ];
429         after = [ "hydra-init.service" "network.target" "network-online.target" ];
430         path = with pkgs; [ hydra-package nettools jq ];
431         restartTriggers = [ hydraConf ];
432         environment = env // {
433           HYDRA_DBI = "${env.HYDRA_DBI};application_name=hydra-evaluator";
434         };
435         serviceConfig =
436           { ExecStart = "@${hydra-package}/bin/hydra-evaluator hydra-evaluator";
437             User = "hydra";
438             Restart = "always";
439             WorkingDirectory = baseDir;
440             Slice = "system-hydra.slice";
441           };
442       };
444     systemd.services.hydra-update-gc-roots =
445       { requires = [ "hydra-init.service" ];
446         after = [ "hydra-init.service" ];
447         environment = env // {
448           HYDRA_DBI = "${env.HYDRA_DBI};application_name=hydra-update-gc-roots";
449         };
450         serviceConfig =
451           { ExecStart = "@${hydra-package}/bin/hydra-update-gc-roots hydra-update-gc-roots";
452             User = "hydra";
453             Slice = "system-hydra.slice";
454           };
455         startAt = "2,14:15";
456       };
458     systemd.services.hydra-send-stats =
459       { wantedBy = [ "multi-user.target" ];
460         after = [ "hydra-init.service" ];
461         environment = env // {
462           HYDRA_DBI = "${env.HYDRA_DBI};application_name=hydra-send-stats";
463         };
464         serviceConfig =
465           { ExecStart = "@${hydra-package}/bin/hydra-send-stats hydra-send-stats";
466             User = "hydra";
467             Slice = "system-hydra.slice";
468           };
469       };
471     systemd.services.hydra-notify =
472       { wantedBy = [ "multi-user.target" ];
473         requires = [ "hydra-init.service" ];
474         after = [ "hydra-init.service" ];
475         restartTriggers = [ hydraConf ];
476         path = [ pkgs.zstd ];
477         environment = env // {
478           PGPASSFILE = "${baseDir}/pgpass-queue-runner";
479           HYDRA_DBI = "${env.HYDRA_DBI};application_name=hydra-notify";
480         };
481         serviceConfig =
482           { ExecStart = "@${hydra-package}/bin/hydra-notify hydra-notify";
483             # FIXME: run this under a less privileged user?
484             User = "hydra-queue-runner";
485             Restart = "always";
486             RestartSec = 5;
487             Slice = "system-hydra.slice";
488           };
489       };
491     # If there is less than a certain amount of free disk space, stop
492     # the queue/evaluator to prevent builds from failing or aborting.
493     systemd.services.hydra-check-space =
494       { script =
495           ''
496             if [ $(($(stat -f -c '%a' /nix/store) * $(stat -f -c '%S' /nix/store))) -lt $((${toString cfg.minimumDiskFree} * 1024**3)) ]; then
497                 echo "stopping Hydra queue runner due to lack of free space..."
498                 systemctl stop hydra-queue-runner
499             fi
500             if [ $(($(stat -f -c '%a' /nix/store) * $(stat -f -c '%S' /nix/store))) -lt $((${toString cfg.minimumDiskFreeEvaluator} * 1024**3)) ]; then
501                 echo "stopping Hydra evaluator due to lack of free space..."
502                 systemctl stop hydra-evaluator
503             fi
504           '';
505         startAt = "*:0/5";
506         serviceConfig.Slice = "system-hydra.slice";
507       };
509     # Periodically compress build logs. The queue runner compresses
510     # logs automatically after a step finishes, but this doesn't work
511     # if the queue runner is stopped prematurely.
512     systemd.services.hydra-compress-logs =
513       { path = [ pkgs.bzip2 pkgs.zstd ];
514         script =
515           ''
516             set -eou pipefail
517             compression=$(sed -nr 's/compress_build_logs_compression = ()/\1/p' ${baseDir}/hydra.conf)
518             if [[ $compression == "" ]]; then
519               compression="bzip2"
520             elif [[ $compression == zstd ]]; then
521               compression="zstd --rm"
522             fi
523             find ${baseDir}/build-logs -type f -name "*.drv" -mtime +3 -size +0c | xargs -r "$compression" --force --quiet
524           '';
525         startAt = "Sun 01:45";
526         serviceConfig.Slice = "system-hydra.slice";
527       };
529     services.postgresql.enable = lib.mkIf haveLocalDB true;
531     services.postgresql.identMap = lib.optionalString haveLocalDB
532       ''
533         hydra-users hydra hydra
534         hydra-users hydra-queue-runner hydra
535         hydra-users hydra-www hydra
536         hydra-users root hydra
537         # The postgres user is used to create the pg_trgm extension for the hydra database
538         hydra-users postgres postgres
539       '';
541     services.postgresql.authentication = lib.optionalString haveLocalDB
542       ''
543         local hydra all ident map=hydra-users
544       '';
546   };