at 25.11-pre 9.7 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 } // lib.filterAttrs (_: v: !builtins.isNull v) cfg.settings; 22 23 environmentFile = pkgs.writeText "healthchecks-environment" ( 24 lib.generators.toKeyValue { } environment 25 ); 26 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 ''; 36in 37{ 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 }; 45 46 package = mkPackageOption pkgs "healthchecks" { }; 47 48 user = mkOption { 49 default = defaultUser; 50 type = types.str; 51 description = '' 52 User account under which healthchecks runs. 53 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 }; 61 62 group = mkOption { 63 default = defaultUser; 64 type = types.str; 65 description = '' 66 Group account under which healthchecks runs. 67 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 }; 75 76 listenAddress = mkOption { 77 type = types.str; 78 default = "localhost"; 79 description = "Address the server will listen on."; 80 }; 81 82 port = mkOption { 83 type = types.port; 84 default = 8000; 85 description = "Port the server will listen on."; 86 }; 87 88 dataDir = mkOption { 89 type = types.str; 90 default = "/var/lib/healthchecks"; 91 description = '' 92 The directory used to store all data for healthchecks. 93 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 }; 101 102 settingsFile = lib.mkOption { 103 type = lib.types.nullOr lib.types.path; 104 default = null; 105 description = opt.settings.description; 106 }; 107 108 settings = lib.mkOption { 109 description = '' 110 Environment variables which are read by healthchecks `(local)_settings.py`. 111 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. 114 115 See <https://healthchecks.io/docs/self_hosted_configuration/> 116 for a full documentation of settings. 117 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. 128 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 }; 140 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 }; 146 147 DEBUG = mkOption { 148 type = types.bool; 149 default = false; 150 description = "Enable debug mode."; 151 apply = boolToPython; 152 }; 153 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 }; 167 168 DB = mkOption { 169 type = types.enum [ 170 "sqlite" 171 "postgres" 172 "mysql" 173 ]; 174 default = "sqlite"; 175 description = "Database engine to use."; 176 }; 177 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 }; 192 193 config = mkIf cfg.enable { 194 environment.systemPackages = [ healthchecksManageScript ]; 195 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 }; 205 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" ]; 223 224 serviceConfig = commonConfig // { 225 Restart = "on-failure"; 226 Type = "oneshot"; 227 ExecStart = '' 228 ${pkg}/opt/healthchecks/manage.py migrate 229 ''; 230 }; 231 }; 232 233 healthchecks = { 234 description = "Healthchecks WSGI Service"; 235 wantedBy = [ "healthchecks.target" ]; 236 after = [ "healthchecks-migration.service" ]; 237 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"; 244 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 }; 254 255 healthchecks-sendalerts = { 256 description = "Healthchecks Alert Service"; 257 wantedBy = [ "healthchecks.target" ]; 258 after = [ "healthchecks.service" ]; 259 260 serviceConfig = commonConfig // { 261 Restart = "always"; 262 ExecStart = '' 263 ${pkg}/opt/healthchecks/manage.py sendalerts 264 ''; 265 }; 266 }; 267 268 healthchecks-sendreports = { 269 description = "Healthchecks Reporting Service"; 270 wantedBy = [ "healthchecks.target" ]; 271 after = [ "healthchecks.service" ]; 272 273 serviceConfig = commonConfig // { 274 Restart = "always"; 275 ExecStart = '' 276 ${pkg}/opt/healthchecks/manage.py sendreports --loop 277 ''; 278 }; 279 }; 280 }; 281 282 users.users = optionalAttrs (cfg.user == defaultUser) { 283 ${defaultUser} = { 284 description = "healthchecks service owner"; 285 isSystemUser = true; 286 group = defaultUser; 287 }; 288 }; 289 290 users.groups = optionalAttrs (cfg.user == defaultUser) { 291 ${defaultUser} = { 292 members = [ defaultUser ]; 293 }; 294 }; 295 }; 296}