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