grafana-alloy: don't build the frontend twice
[NixPkgs.git] / nixos / modules / services / web-apps / healthchecks.nix
blobc7db999a62c21e8bd3ff4985320858938f0d8878
1 { config, lib, options, pkgs, buildEnv, ... }:
3 with lib;
5 let
6   defaultUser = "healthchecks";
7   cfg = config.services.healthchecks;
8   opt = options.services.healthchecks;
9   pkg = cfg.package;
10   boolToPython = b: if b then "True" else "False";
11   environment = {
12     PYTHONPATH = pkg.pythonPath;
13     STATIC_ROOT = cfg.dataDir + "/static";
14   } // lib.filterAttrs (_: v: !builtins.isNull v) cfg.settings;
16   environmentFile = pkgs.writeText "healthchecks-environment" (lib.generators.toKeyValue { } environment);
18   healthchecksManageScript = pkgs.writeShellScriptBin "healthchecks-manage" ''
19     sudo=exec
20     if [[ "$USER" != "${cfg.user}" ]]; then
21       sudo='exec /run/wrappers/bin/sudo -u ${cfg.user} --preserve-env --preserve-env=PYTHONPATH'
22     fi
23     export $(cat ${environmentFile} | xargs)
24     ${lib.optionalString (cfg.settingsFile != null) "export $(cat ${cfg.settingsFile} | xargs)"}
25     $sudo ${pkg}/opt/healthchecks/manage.py "$@"
26   '';
29   options.services.healthchecks = {
30     enable = mkEnableOption "healthchecks" // {
31       description = ''
32         Enable healthchecks.
33         It is expected to be run behind a HTTP reverse proxy.
34       '';
35     };
37     package = mkPackageOption pkgs "healthchecks" { };
39     user = mkOption {
40       default = defaultUser;
41       type = types.str;
42       description = ''
43         User account under which healthchecks runs.
45         ::: {.note}
46         If left as the default value this user will automatically be created
47         on system activation, otherwise you are responsible for
48         ensuring the user exists before the healthchecks service starts.
49         :::
50       '';
51     };
53     group = mkOption {
54       default = defaultUser;
55       type = types.str;
56       description = ''
57         Group account under which healthchecks runs.
59         ::: {.note}
60         If left as the default value this group will automatically be created
61         on system activation, otherwise you are responsible for
62         ensuring the group exists before the healthchecks service starts.
63         :::
64       '';
65     };
67     listenAddress = mkOption {
68       type = types.str;
69       default = "localhost";
70       description = "Address the server will listen on.";
71     };
73     port = mkOption {
74       type = types.port;
75       default = 8000;
76       description = "Port the server will listen on.";
77     };
79     dataDir = mkOption {
80       type = types.str;
81       default = "/var/lib/healthchecks";
82       description = ''
83         The directory used to store all data for healthchecks.
85         ::: {.note}
86         If left as the default value this directory will automatically be created before
87         the healthchecks server starts, otherwise you are responsible for ensuring the
88         directory exists with appropriate ownership and permissions.
89         :::
90       '';
91     };
93     settingsFile = lib.mkOption {
94       type = lib.types.nullOr lib.types.path;
95       default = null;
96       description = opt.settings.description;
97     };
99     settings = lib.mkOption {
100       description = ''
101         Environment variables which are read by healthchecks `(local)_settings.py`.
103         Settings which are explicitly covered in options below, are type-checked and/or transformed
104         before added to the environment, everything else is passed as a string.
106         See <https://healthchecks.io/docs/self_hosted_configuration/>
107         for a full documentation of settings.
109         We add additional variables to this list inside the packages `local_settings.py.`
110         - `STATIC_ROOT` to set a state directory for dynamically generated static files.
111         - `SECRET_KEY_FILE` to read `SECRET_KEY` from a file at runtime and keep it out of
112           /nix/store.
113         - `_FILE` variants for several values that hold sensitive information in
114           [Healthchecks configuration](https://healthchecks.io/docs/self_hosted_configuration/) so
115           that they also can be read from a file and kept out of /nix/store. To see which values
116           have support for a `_FILE` variant, run:
117           - `nix-instantiate --eval --expr '(import <nixpkgs> {}).healthchecks.secrets'`
118           - or `nix eval 'nixpkgs#healthchecks.secrets'` if the flake support has been enabled.
120         If the same variable is set in both `settings` and `settingsFile` the value from `settingsFile` has priority.
121       '';
122       type = types.submodule (settings: {
123         freeformType = types.attrsOf types.str;
124         options = {
125           ALLOWED_HOSTS = lib.mkOption {
126             type = types.listOf types.str;
127             default = [ "*" ];
128             description = "The host/domain names that this site can serve.";
129             apply = lib.concatStringsSep ",";
130           };
132           SECRET_KEY_FILE = mkOption {
133             type = types.nullOr types.path;
134             description = "Path to a file containing the secret key.";
135             default = null;
136           };
138           DEBUG = mkOption {
139             type = types.bool;
140             default = false;
141             description = "Enable debug mode.";
142             apply = boolToPython;
143           };
145           REGISTRATION_OPEN = mkOption {
146             type = types.bool;
147             default = false;
148             description = ''
149               A boolean that controls whether site visitors can create new accounts.
150               Set it to false if you are setting up a private Healthchecks instance,
151               but it needs to be publicly accessible (so, for example, your cloud
152               services can send pings to it).
153               If you close new user registration, you can still selectively invite
154               users to your team account.
155             '';
156             apply = boolToPython;
157           };
159           DB = mkOption {
160             type = types.enum [ "sqlite" "postgres" "mysql" ];
161             default = "sqlite";
162             description = "Database engine to use.";
163           };
165           DB_NAME = mkOption {
166             type = types.str;
167             default =
168               if settings.config.DB == "sqlite"
169               then "${cfg.dataDir}/healthchecks.sqlite"
170               else "hc";
171             defaultText = lib.literalExpression ''
172               if config.${settings.options.DB} == "sqlite"
173               then "''${config.${opt.dataDir}}/healthchecks.sqlite"
174               else "hc"
175             '';
176             description = "Database name.";
177           };
178         };
179       });
180     };
181   };
183   config = mkIf cfg.enable {
184     environment.systemPackages = [ healthchecksManageScript ];
186     systemd.targets.healthchecks = {
187       description = "Target for all Healthchecks services";
188       wantedBy = [ "multi-user.target" ];
189       wants = [ "network-online.target" ];
190       after = [ "network.target" "network-online.target" ];
191     };
193     systemd.services =
194       let
195         commonConfig = {
196           WorkingDirectory = cfg.dataDir;
197           User = cfg.user;
198           Group = cfg.group;
199           EnvironmentFile = [
200             environmentFile
201           ] ++ lib.optional (cfg.settingsFile != null) cfg.settingsFile;
202           StateDirectory = mkIf (cfg.dataDir == "/var/lib/healthchecks") "healthchecks";
203           StateDirectoryMode = mkIf (cfg.dataDir == "/var/lib/healthchecks") "0750";
204         };
205       in
206       {
207         healthchecks-migration = {
208           description = "Healthchecks migrations";
209           wantedBy = [ "healthchecks.target" ];
211           serviceConfig = commonConfig // {
212             Restart = "on-failure";
213             Type = "oneshot";
214             ExecStart = ''
215               ${pkg}/opt/healthchecks/manage.py migrate
216             '';
217           };
218         };
220         healthchecks = {
221           description = "Healthchecks WSGI Service";
222           wantedBy = [ "healthchecks.target" ];
223           after = [ "healthchecks-migration.service" ];
225           preStart = ''
226             ${pkg}/opt/healthchecks/manage.py collectstatic --no-input
227             ${pkg}/opt/healthchecks/manage.py remove_stale_contenttypes --no-input
228           '' + lib.optionalString (cfg.settings.DEBUG != "True") "${pkg}/opt/healthchecks/manage.py compress";
230           serviceConfig = commonConfig // {
231             Restart = "always";
232             ExecStart = ''
233               ${pkgs.python3Packages.gunicorn}/bin/gunicorn hc.wsgi \
234                 --bind ${cfg.listenAddress}:${toString cfg.port} \
235                 --pythonpath ${pkg}/opt/healthchecks
236             '';
237           };
238         };
240         healthchecks-sendalerts = {
241           description = "Healthchecks Alert Service";
242           wantedBy = [ "healthchecks.target" ];
243           after = [ "healthchecks.service" ];
245           serviceConfig = commonConfig // {
246             Restart = "always";
247             ExecStart = ''
248               ${pkg}/opt/healthchecks/manage.py sendalerts
249             '';
250           };
251         };
253         healthchecks-sendreports = {
254           description = "Healthchecks Reporting Service";
255           wantedBy = [ "healthchecks.target" ];
256           after = [ "healthchecks.service" ];
258           serviceConfig = commonConfig // {
259             Restart = "always";
260             ExecStart = ''
261               ${pkg}/opt/healthchecks/manage.py sendreports --loop
262             '';
263           };
264         };
265       };
267     users.users = optionalAttrs (cfg.user == defaultUser) {
268       ${defaultUser} =
269         {
270           description = "healthchecks service owner";
271           isSystemUser = true;
272           group = defaultUser;
273         };
274     };
276     users.groups = optionalAttrs (cfg.user == defaultUser) {
277       ${defaultUser} =
278         {
279           members = [ defaultUser ];
280         };
281     };
282   };