personal-effectivity-applic.../flake.nix

277 lines
10 KiB
Nix

{
description = "Personal Effectity Application";
inputs = {
flake-utils.url = "github:numtide/flake-utils";
mach-nix.url = "mach-nix/3.4.0";
nixpkgs.url = "nixpkgs/release-21.11";
};
outputs = { self, nixpkgs, flake-utils, mach-nix }:
flake-utils.lib.eachDefaultSystem (system:
with mach-nix.lib.${system};
let
pkgs = nixpkgs.legacyPackages.${system};
apiPkg = buildPythonPackage { src = [ ./api ]; };
urlMod = lib:
with lib; {
options.scheme = mkOption {
type = with types; enum [ null "http" "https" ];
default = null;
example = "https";
};
options.domain = mkOption {
type = with types; nullOr str;
default = null;
example = "pea.example.com";
};
options.subdir = mkOption {
type = types.str;
default = "/";
example = "/pea";
};
};
in {
packages = {
api = mkPython [ apiPkg ];
app = pkgs.callPackage ./app { };
};
devShells = {
api = mkPythonShell [ apiPkg ];
app = pkgs.callPackage ./app/shell.nix { };
};
overlays = { api = mkOverlay [ apiPkg ]; };
nixosModules = let
nginxOpts = lib:
with lib; {
webAttrs =
mkOption { type = with types; submodule (urlMod lib); };
acmeEmail = mkOption {
type = with types; nullOr str;
default = null;
example = "admin@example.com";
description = "The email to use for Let's Encrypt certificates";
};
};
maybeLocalhost = domain:
if builtins.isNull domain then "localhost" else domain;
in {
api = { config, lib, pkgs, ... }:
with lib;
let cfg = config.services.pea.api;
in {
options.services.pea.api = {
enable =
mkEnableOption "Personal Effectivity Application API service";
package = mkOption {
type = types.package;
default = self.outputs.packages.${system}.api;
description = "The package to use for the service.";
};
user = mkOption {
type = types.str;
default = "pea-api";
description = "The name for the PEA API system user";
};
group = mkOption {
type = types.str;
default = "pea";
description = "The name for the PEA group";
};
socketFile = mkOption {
type = types.str;
default = "/run/pea-api.sock";
description = "The path for the Unix socket file";
};
dotEnvFile = mkOption {
type = types.str;
example = "/run/keys/pea.env";
description = "The path to the dotenv file";
};
} // nginxOpts lib;
config = mkIf cfg.enable
(let domain = maybeLocalhost cfg.webAttrs.domain;
in mkMerge [
{
# basics
environment.systemPackages = [ cfg.package ];
networking.firewall.allowedTCPPorts =
if builtins.isString cfg.acmeEmail then [
80
443
] else
[ 80 ];
# NGINX config
services.nginx = {
enable = true;
recommendedProxySettings = true;
recommendedGzipSettings = true;
recommendedOptimisation = true;
recommendedTlsSettings = true;
upstreams.pea-api.servers = {
"unix:${cfg.socketFile}" = { };
};
virtualHosts.${domain} = {
locations."${cfg.webAttrs.subdir}/".proxyPass =
"http://pea-api/";
};
};
# PostgreSQL config
services.postgresql = let
db = "pea-db";
usr = "pea-user";
in {
enable = true;
package = pkgs.postgresql_14;
authentication = ''
local ${db} ${usr} trust
'';
ensureDatabases = [ db ];
ensureUsers = [{
name = usr;
ensurePermissions = {
"DATABASE \"${db}\"" = "ALL PRIVILEGES";
};
}];
identMap = ''
pea-map ${cfg.user} ${usr}
'';
};
services.postgresqlBackup = {
enable = true;
backupAll = true;
};
# PEA API config
systemd.services.pea-api = {
description = "PEA API Uvicorn service";
after = [ "network.target" "postgresql.service" ];
requires = [ "pea-api.socket" ];
wantedBy = [ "multi-user.target" ];
serviceConfig = {
WorkingDirectory =
"${cfg.package}/lib/python3.9/site-packages/pea_api/";
EnvironmentFile = cfg.dotEnvFile;
};
script = ''
${cfg.package}/bin/uvicorn main:app --root-path ${cfg.webAttrs.subdir} --uds ${cfg.socketFile}
'';
reload = ''
/bin/kill -s HUP $MAINPID
'';
};
systemd.sockets.pea-api = {
description = "PEA API Uvicorn socket";
wantedBy = [ "sockets.target" ];
socketConfig = { ListenStream = cfg.socketFile; };
};
# system user config
users.extraGroups.${cfg.group} = { };
users.extraUsers.${cfg.user} = {
isSystemUser = true;
group = cfg.group;
description = "PEA API user";
};
}
(mkIf (isString cfg.acmeEmail) {
security.acme = {
acceptTerms = true;
certs.${domain}.email = cfg.acmeEmail;
};
services.nginx.virtualHosts.${domain} = {
enableACME = true;
forceSSL = true;
};
})
]);
};
app = { config, pkgs, lib, ... }:
with lib;
let cfg = config.services.pea.app;
in {
options.services.pea.app = {
enable =
mkEnableOption "Personal Effectivity Application UI service";
package = mkOption {
type = types.package;
default = self.outputs.packages.${system}.app.override {
inherit (cfg) webAttrs apiAttrs;
};
description = "The package to use for the service.";
};
apiAttrs =
mkOption { type = with types; submodule (urlMod lib); };
} // nginxOpts lib;
config = mkIf cfg.enable
(let domain = maybeLocalhost cfg.webAttrs.domain;
in mkMerge [
{
services.nginx = {
enable = true;
recommendedProxySettings = true;
recommendedGzipSettings = true;
recommendedOptimisation = true;
recommendedTlsSettings = true;
virtualHosts.${domain}.locations = let
loc = cfg.webAttrs.subdir;
pkg = "${cfg.package}/";
in {
"${loc}/" = {
alias = pkg;
tryFiles = "$uri /index.html";
};
"/index.html" = { root = pkg; };
};
};
}
(mkIf (isString cfg.acmeEmail) {
security.acme = {
acceptTerms = true;
certs.${domain}.email = cfg.acmeEmail;
};
services.nginx.virtualHosts.${domain} = {
enableACME = true;
forceSSL = true;
};
})
]);
};
};
}) // {
nixosConfigurations.pea-test = let system = "x86_64-linux";
in nixpkgs.lib.nixosSystem {
modules = [
({ lib, ... }: {
boot.isContainer = true;
system.configurationRevision = lib.mkIf (self ? rev) self.rev;
networking.useDHCP = false;
})
self.nixosModules.${system}.api
self.nixosModules.${system}.app
({ config, ... }:
let apiSubdir = "/api";
in {
services.pea = {
api = {
enable = true;
dotEnvFile = "/run/keys/pea.env";
webAttrs.subdir = apiSubdir;
};
app = {
enable = true;
webAttrs = {
scheme = null;
domain = null;
subdir = "/app";
};
apiAttrs.subdir = apiSubdir;
};
};
})
];
};
};
}