{ config, pkgs, lib, ... }: let inherit (lib) concatStringsSep mapAttrsToList mkDefault mkEnableOption mkIf mkOption mkPackageOption optionalString types ; inherit (pkgs) writeShellApplication; # TODO: configure the name globally for everywhere it's used name = "panel"; cfg = config.services.${name}; database-url = "sqlite:////var/lib/${name}/db.sqlite3"; python-environment = pkgs.python3.withPackages ( ps: with ps; [ cfg.package uvicorn ] ); configFile = pkgs.concatText "configuration.py" [ ((pkgs.formats.pythonVars { }).generate "settings.py" cfg.settings) (builtins.toFile "extra-settings.py" cfg.extra-settings) ]; manage-service = writeShellApplication { name = "manage"; text = ''exec ${cfg.package}/bin/manage.py "$@"''; }; manage-admin = writeShellApplication { # This allows running the `manage` command in the system environment, e.g. to initialise an admin user # Executing name = "manage"; text = '' systemd-run --pty \ --same-dir \ --wait \ --collect \ --service-type=exec \ --unit "manage-${name}.service" \ --property "User=${name}" \ --property "Group=${name}" \ --property "Environment=DATABASE_URL=${database-url} USER_SETTINGS_FILE=${configFile}" \ '' + optionalString (credentials != [ ]) ( (concatStringsSep " \\\n" (map (cred: "--property 'LoadCredential=${cred}'") credentials)) + " \\\n" ) + '' ${lib.getExe manage-service} "$@" ''; }; credentials = mapAttrsToList (name: secretPath: "${name}:${secretPath}") cfg.secrets; in # TODO: for a more clever and generic way of running Django services: # https://git.dgnum.eu/mdebray/djangonix/ # unlicensed at the time of writing, but surely worth taking some inspiration from... { options.services.${name} = { enable = mkEnableOption "Service configuration for `${name}`"; # NOTE: this requires that the package is present in `pkgs` package = mkPackageOption pkgs name { }; production = mkOption { type = types.bool; default = true; }; restart = mkOption { description = "systemd restart behavior"; type = types.enum [ "no" "on-success" "on-failure" "on-abnormal" "on-abort" "always" ]; default = "always"; }; domain = mkOption { type = types.str; }; host = mkOption { type = types.str; default = "127.0.0.1"; }; port = mkOption { type = types.port; default = 8000; }; settings = mkOption { type = types.attrsOf types.anything; default = { STATIC_ROOT = mkDefault "/var/lib/${name}/static"; DEBUG = mkDefault false; ALLOWED_HOSTS = mkDefault [ cfg.domain cfg.host "localhost" "[::1]" ]; CSRF_TRUSTED_ORIGINS = mkDefault [ "https://${cfg.domain}" ]; COMPRESS_OFFLINE = true; LIBSASS_OUTPUT_STYLE = "compressed"; }; description = '' Django configuration as an attribute set. Name-value pairs will be converted to Python variable assignments. ''; }; extra-settings = mkOption { type = types.lines; default = ""; description = '' Django configuration written in Python verbatim. Contents will be appended to the definitions in `settings`. ''; }; secrets = mkOption { type = types.attrsOf types.path; default = { }; }; }; config = mkIf cfg.enable { environment.systemPackages = [ manage-admin ]; services = { nginx.enable = true; nginx.virtualHosts = { ${cfg.domain} = { locations = { "/".proxyPass = "http://localhost:${toString cfg.port}"; "/static/".alias = "/var/lib/${name}/static/"; }; } // lib.optionalAttrs cfg.production { enableACME = true; forceSSL = true; }; }; }; users.users.${name} = { isSystemUser = true; group = name; }; users.groups.${name} = { }; systemd.services.${name} = { description = "${name} ASGI server"; after = [ "network.target" ]; wantedBy = [ "multi-user.target" ]; path = [ python-environment manage-service ]; preStart = '' # Auto-migrate on first run or if the package has changed versionFile="/var/lib/${name}/package-version" if [[ $(cat "$versionFile" 2>/dev/null) != ${cfg.package} ]]; then manage migrate --no-input manage collectstatic --no-input --clear manage compress --force echo ${cfg.package} > "$versionFile" fi ''; script = '' uvicorn ${name}.asgi:application --host ${cfg.host} --port ${toString cfg.port} ''; serviceConfig = { Restart = "always"; User = name; WorkingDirectory = "/var/lib/${name}"; StateDirectory = name; RuntimeDirectory = name; LogsDirectory = name; } // lib.optionalAttrs (credentials != [ ]) { LoadCredential = credentials; }; environment = { USER_SETTINGS_FILE = "${configFile}"; DATABASE_URL = database-url; }; }; }; }