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