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