mirror of
https://github.com/NixOS/nixpkgs.git
synced 2025-07-20 00:50:38 +03:00

This avoids restarting the postgresql server, when only ensureDatabases or ensureUsers have been changed. It will also allow to properly wait for recovery to finish later. To wait for "postgresql is ready" in other services, we now provide a postgresql.target. Resolves #400018 Co-authored-by: Marcel <me@m4rc3l.de>
566 lines
18 KiB
Nix
566 lines
18 KiB
Nix
{
|
|
config,
|
|
lib,
|
|
pkgs,
|
|
...
|
|
}:
|
|
|
|
let
|
|
cfg = config.services.davis;
|
|
db = cfg.database;
|
|
mail = cfg.mail;
|
|
|
|
mysqlLocal = db.createLocally && db.driver == "mysql";
|
|
pgsqlLocal = db.createLocally && db.driver == "postgresql";
|
|
|
|
user = cfg.user;
|
|
group = cfg.group;
|
|
|
|
isSecret = v: lib.isAttrs v && v ? _secret && (lib.isString v._secret || builtins.isPath v._secret);
|
|
davisEnvVars = lib.generators.toKeyValue {
|
|
mkKeyValue = lib.flip lib.generators.mkKeyValueDefault "=" {
|
|
mkValueString =
|
|
v:
|
|
if builtins.isInt v then
|
|
toString v
|
|
else if lib.isString v then
|
|
"\"${v}\""
|
|
else if true == v then
|
|
"true"
|
|
else if false == v then
|
|
"false"
|
|
else if null == v then
|
|
""
|
|
else if isSecret v then
|
|
if (lib.isString v._secret) then
|
|
builtins.hashString "sha256" v._secret
|
|
else
|
|
builtins.hashString "sha256" (builtins.readFile v._secret)
|
|
else
|
|
throw "unsupported type ${builtins.typeOf v}: ${(lib.generators.toPretty { }) v}";
|
|
};
|
|
};
|
|
secretPaths = lib.mapAttrsToList (_: v: v._secret) (lib.filterAttrs (_: isSecret) cfg.config);
|
|
mkSecretReplacement = file: ''
|
|
replace-secret ${
|
|
lib.escapeShellArgs [
|
|
(
|
|
if (lib.isString file) then
|
|
builtins.hashString "sha256" file
|
|
else
|
|
builtins.hashString "sha256" (builtins.readFile file)
|
|
)
|
|
file
|
|
"${cfg.dataDir}/.env.local"
|
|
]
|
|
}
|
|
'';
|
|
secretReplacements = lib.concatMapStrings mkSecretReplacement secretPaths;
|
|
filteredConfig = lib.converge (lib.filterAttrsRecursive (
|
|
_: v:
|
|
!lib.elem v [
|
|
{ }
|
|
null
|
|
]
|
|
)) cfg.config;
|
|
davisEnv = pkgs.writeText "davis.env" (davisEnvVars filteredConfig);
|
|
in
|
|
{
|
|
options.services.davis = {
|
|
enable = lib.mkEnableOption "Davis is a caldav and carddav server";
|
|
|
|
user = lib.mkOption {
|
|
default = "davis";
|
|
description = "User davis runs as.";
|
|
type = lib.types.str;
|
|
};
|
|
|
|
group = lib.mkOption {
|
|
default = "davis";
|
|
description = "Group davis runs as.";
|
|
type = lib.types.str;
|
|
};
|
|
|
|
package = lib.mkPackageOption pkgs "davis" { };
|
|
|
|
dataDir = lib.mkOption {
|
|
type = lib.types.path;
|
|
default = "/var/lib/davis";
|
|
description = ''
|
|
Davis data directory.
|
|
'';
|
|
};
|
|
|
|
hostname = lib.mkOption {
|
|
type = lib.types.str;
|
|
example = "davis.yourdomain.org";
|
|
description = ''
|
|
Domain of the host to serve davis under. You may want to change it if you
|
|
run Davis on a different URL than davis.yourdomain.
|
|
'';
|
|
};
|
|
|
|
config = lib.mkOption {
|
|
type = lib.types.attrsOf (
|
|
lib.types.nullOr (
|
|
lib.types.either
|
|
(lib.types.oneOf [
|
|
lib.types.bool
|
|
lib.types.int
|
|
lib.types.port
|
|
lib.types.path
|
|
lib.types.str
|
|
])
|
|
(
|
|
lib.types.submodule {
|
|
options = {
|
|
_secret = lib.mkOption {
|
|
type = lib.types.nullOr (
|
|
lib.types.oneOf [
|
|
lib.types.str
|
|
lib.types.path
|
|
]
|
|
);
|
|
description = ''
|
|
The path to a file containing the value the
|
|
option should be set to in the final
|
|
configuration file.
|
|
'';
|
|
};
|
|
};
|
|
}
|
|
)
|
|
)
|
|
);
|
|
default = { };
|
|
|
|
example = '''';
|
|
description = '''';
|
|
};
|
|
|
|
adminLogin = lib.mkOption {
|
|
type = lib.types.str;
|
|
default = "root";
|
|
description = ''
|
|
Username for the admin account.
|
|
'';
|
|
};
|
|
adminPasswordFile = lib.mkOption {
|
|
type = lib.types.path;
|
|
description = ''
|
|
The full path to a file that contains the admin's password. Must be
|
|
readable by the user.
|
|
'';
|
|
example = "/run/secrets/davis-admin-pass";
|
|
};
|
|
|
|
appSecretFile = lib.mkOption {
|
|
type = lib.types.path;
|
|
description = ''
|
|
A file containing the Symfony APP_SECRET - Its value should be a series
|
|
of characters, numbers and symbols chosen randomly and the recommended
|
|
length is around 32 characters. Can be generated with <code>cat
|
|
/dev/urandom | tr -dc a-zA-Z0-9 | fold -w 48 | head -n 1</code>.
|
|
'';
|
|
example = "/run/secrets/davis-appsecret";
|
|
};
|
|
|
|
database = {
|
|
driver = lib.mkOption {
|
|
type = lib.types.enum [
|
|
"sqlite"
|
|
"postgresql"
|
|
"mysql"
|
|
];
|
|
default = "sqlite";
|
|
description = "Database type, required in all circumstances.";
|
|
};
|
|
urlFile = lib.mkOption {
|
|
type = lib.types.nullOr lib.types.path;
|
|
default = null;
|
|
example = "/run/secrets/davis-db-url";
|
|
description = ''
|
|
A file containing the database connection url. If set then it
|
|
overrides all other database settings (except driver). This is
|
|
mandatory if you want to use an external database, that is when
|
|
`services.davis.database.createLocally` is `false`.
|
|
'';
|
|
};
|
|
name = lib.mkOption {
|
|
type = lib.types.nullOr lib.types.str;
|
|
default = "davis";
|
|
description = "Database name, only used when the databse is created locally.";
|
|
};
|
|
createLocally = lib.mkOption {
|
|
type = lib.types.bool;
|
|
default = true;
|
|
description = "Create the database and database user locally.";
|
|
};
|
|
};
|
|
|
|
mail = {
|
|
dsn = lib.mkOption {
|
|
type = lib.types.nullOr lib.types.str;
|
|
default = null;
|
|
description = "Mail DSN for sending emails. Mutually exclusive with `services.davis.mail.dsnFile`.";
|
|
example = "smtp://username:password@example.com:25";
|
|
};
|
|
dsnFile = lib.mkOption {
|
|
type = lib.types.nullOr lib.types.str;
|
|
default = null;
|
|
example = "/run/secrets/davis-mail-dsn";
|
|
description = "A file containing the mail DSN for sending emails. Mutually exclusive with `servies.davis.mail.dsn`.";
|
|
};
|
|
inviteFromAddress = lib.mkOption {
|
|
type = lib.types.nullOr lib.types.str;
|
|
default = null;
|
|
description = "Email address to send invitations from.";
|
|
example = "no-reply@dav.example.com";
|
|
};
|
|
};
|
|
|
|
nginx = lib.mkOption {
|
|
type = lib.types.nullOr (
|
|
lib.types.submodule (
|
|
lib.recursiveUpdate (import ../web-servers/nginx/vhost-options.nix { inherit config lib; }) {
|
|
}
|
|
)
|
|
);
|
|
default = { };
|
|
example = ''
|
|
{
|
|
serverAliases = [
|
|
"dav.''${config.networking.domain}"
|
|
];
|
|
# To enable encryption and let let's encrypt take care of certificate
|
|
forceSSL = true;
|
|
enableACME = true;
|
|
}
|
|
'';
|
|
description = ''
|
|
Use this option to customize an nginx virtual host. To disable the nginx set this to null.
|
|
'';
|
|
};
|
|
|
|
poolConfig = lib.mkOption {
|
|
type = lib.types.attrsOf (
|
|
lib.types.oneOf [
|
|
lib.types.str
|
|
lib.types.int
|
|
lib.types.bool
|
|
]
|
|
);
|
|
default = {
|
|
"pm" = "dynamic";
|
|
"pm.max_children" = 32;
|
|
"pm.start_servers" = 2;
|
|
"pm.min_spare_servers" = 2;
|
|
"pm.max_spare_servers" = 4;
|
|
"pm.max_requests" = 500;
|
|
};
|
|
description = ''
|
|
Options for the davis PHP pool. See the documentation on <literal>php-fpm.conf</literal>
|
|
for details on configuration directives.
|
|
'';
|
|
};
|
|
};
|
|
|
|
config =
|
|
let
|
|
defaultServiceConfig = {
|
|
ReadWritePaths = "${cfg.dataDir}";
|
|
User = user;
|
|
UMask = 77;
|
|
DeviceAllow = "";
|
|
LockPersonality = true;
|
|
NoNewPrivileges = true;
|
|
PrivateDevices = true;
|
|
PrivateTmp = true;
|
|
PrivateUsers = true;
|
|
ProcSubset = "pid";
|
|
ProtectClock = true;
|
|
ProtectControlGroups = true;
|
|
ProtectHome = true;
|
|
ProtectHostname = true;
|
|
ProtectKernelLogs = true;
|
|
ProtectKernelModules = true;
|
|
ProtectKernelTunables = true;
|
|
ProtectProc = "invisible";
|
|
ProtectSystem = "strict";
|
|
RemoveIPC = true;
|
|
RestrictNamespaces = true;
|
|
RestrictRealtime = true;
|
|
RestrictSUIDSGID = true;
|
|
SystemCallArchitectures = "native";
|
|
SystemCallFilter = [
|
|
"@system-service"
|
|
"~@resources"
|
|
"~@privileged"
|
|
];
|
|
WorkingDirectory = "${cfg.package}/";
|
|
};
|
|
in
|
|
lib.mkIf cfg.enable {
|
|
assertions = [
|
|
{
|
|
assertion = db.createLocally -> db.urlFile == null;
|
|
message = "services.davis.database.urlFile must be unset if services.davis.database.createLocally is set true.";
|
|
}
|
|
{
|
|
assertion = db.createLocally || db.urlFile != null;
|
|
message = "One of services.davis.database.urlFile or services.davis.database.createLocally must be set.";
|
|
}
|
|
{
|
|
assertion = !(mail.dsn != null && mail.dsnFile != null);
|
|
message = "services.davis.mail.dsn and services.davis.mail.dsnFile cannot both be set.";
|
|
}
|
|
];
|
|
services.davis.config =
|
|
{
|
|
APP_ENV = "prod";
|
|
APP_CACHE_DIR = "${cfg.dataDir}/var/cache";
|
|
APP_LOG_DIR = "${cfg.dataDir}/var/log";
|
|
LOG_FILE_PATH = "%kernel.logs_dir%/%kernel.environment%.log";
|
|
DATABASE_DRIVER = db.driver;
|
|
INVITE_FROM_ADDRESS = mail.inviteFromAddress;
|
|
APP_SECRET._secret = cfg.appSecretFile;
|
|
ADMIN_LOGIN = cfg.adminLogin;
|
|
ADMIN_PASSWORD._secret = cfg.adminPasswordFile;
|
|
APP_TIMEZONE = config.time.timeZone;
|
|
WEBDAV_ENABLED = false;
|
|
CALDAV_ENABLED = true;
|
|
CARDDAV_ENABLED = true;
|
|
}
|
|
// (
|
|
if mail.dsn != null then
|
|
{ MAILER_DSN = mail.dsn; }
|
|
else if mail.dsnFile != null then
|
|
{ MAILER_DSN._secret = mail.dsnFile; }
|
|
else
|
|
{ }
|
|
)
|
|
// (
|
|
if db.createLocally then
|
|
{
|
|
DATABASE_URL =
|
|
if db.driver == "sqlite" then
|
|
"sqlite:///${cfg.dataDir}/davis.db" # note: sqlite needs 4 slashes for an absolute path
|
|
else if
|
|
pgsqlLocal
|
|
# note: davis expects a non-standard postgres uri (due to the underlying doctrine library)
|
|
# specifically the dummy hostname which is overridden by the host query parameter
|
|
then
|
|
"postgres://${user}@localhost/${db.name}?host=/run/postgresql"
|
|
else if mysqlLocal then
|
|
"mysql://${user}@localhost/${db.name}?socket=/run/mysqld/mysqld.sock"
|
|
else
|
|
null;
|
|
}
|
|
else
|
|
{ DATABASE_URL._secret = db.urlFile; }
|
|
);
|
|
|
|
users = {
|
|
users = lib.mkIf (user == "davis") {
|
|
davis = {
|
|
description = "Davis service user";
|
|
group = cfg.group;
|
|
isSystemUser = true;
|
|
home = cfg.dataDir;
|
|
};
|
|
};
|
|
groups = lib.mkIf (group == "davis") { davis = { }; };
|
|
};
|
|
|
|
systemd.tmpfiles.rules = [
|
|
"d ${cfg.dataDir} 0710 ${user} ${group} - -"
|
|
"d ${cfg.dataDir}/var 0700 ${user} ${group} - -"
|
|
"d ${cfg.dataDir}/var/log 0700 ${user} ${group} - -"
|
|
"d ${cfg.dataDir}/var/cache 0700 ${user} ${group} - -"
|
|
];
|
|
|
|
services.phpfpm.pools.davis = {
|
|
inherit user group;
|
|
phpOptions = ''
|
|
log_errors = on
|
|
'';
|
|
phpEnv = {
|
|
ENV_DIR = "${cfg.dataDir}";
|
|
APP_CACHE_DIR = "${cfg.dataDir}/var/cache";
|
|
APP_LOG_DIR = "${cfg.dataDir}/var/log";
|
|
};
|
|
phpPackage = lib.mkDefault cfg.package.passthru.php;
|
|
settings =
|
|
{
|
|
"listen.mode" = "0660";
|
|
"pm" = "dynamic";
|
|
"pm.max_children" = 256;
|
|
"pm.start_servers" = 10;
|
|
"pm.min_spare_servers" = 5;
|
|
"pm.max_spare_servers" = 20;
|
|
}
|
|
// (
|
|
if cfg.nginx != null then
|
|
{
|
|
"listen.owner" = config.services.nginx.user;
|
|
"listen.group" = config.services.nginx.group;
|
|
}
|
|
else
|
|
{ }
|
|
)
|
|
// cfg.poolConfig;
|
|
};
|
|
|
|
# Reading the user-provided secret files requires root access
|
|
systemd.services.davis-env-setup = {
|
|
description = "Setup davis environment";
|
|
before = [
|
|
"phpfpm-davis.service"
|
|
"davis-db-migrate.service"
|
|
];
|
|
wantedBy = [ "multi-user.target" ];
|
|
serviceConfig = {
|
|
Type = "oneshot";
|
|
RemainAfterExit = true;
|
|
};
|
|
path = [ pkgs.replace-secret ];
|
|
restartTriggers = [
|
|
cfg.package
|
|
davisEnv
|
|
];
|
|
script = ''
|
|
# error handling
|
|
set -euo pipefail
|
|
# create .env file with the upstream values
|
|
install -T -m 0600 -o ${user} ${cfg.package}/env-upstream "${cfg.dataDir}/.env"
|
|
# create .env.local file with the user-provided values
|
|
install -T -m 0600 -o ${user} ${davisEnv} "${cfg.dataDir}/.env.local"
|
|
${secretReplacements}
|
|
'';
|
|
};
|
|
|
|
systemd.services.davis-db-migrate = {
|
|
description = "Migrate davis database";
|
|
before = [ "phpfpm-davis.service" ];
|
|
after =
|
|
lib.optional mysqlLocal "mysql.service"
|
|
++ lib.optional pgsqlLocal "postgresql.target"
|
|
++ [ "davis-env-setup.service" ];
|
|
requires =
|
|
lib.optional mysqlLocal "mysql.service"
|
|
++ lib.optional pgsqlLocal "postgresql.target"
|
|
++ [ "davis-env-setup.service" ];
|
|
wantedBy = [ "multi-user.target" ];
|
|
serviceConfig = defaultServiceConfig // {
|
|
Type = "oneshot";
|
|
RemainAfterExit = true;
|
|
Environment = [
|
|
"ENV_DIR=${cfg.dataDir}"
|
|
"APP_CACHE_DIR=${cfg.dataDir}/var/cache"
|
|
"APP_LOG_DIR=${cfg.dataDir}/var/log"
|
|
];
|
|
EnvironmentFile = "${cfg.dataDir}/.env.local";
|
|
};
|
|
restartTriggers = [
|
|
cfg.package
|
|
davisEnv
|
|
];
|
|
script = ''
|
|
set -euo pipefail
|
|
${cfg.package}/bin/console cache:clear --no-debug
|
|
${cfg.package}/bin/console cache:warmup --no-debug
|
|
${cfg.package}/bin/console doctrine:migrations:migrate
|
|
'';
|
|
};
|
|
|
|
systemd.services.phpfpm-davis.after = [
|
|
"davis-env-setup.service"
|
|
"davis-db-migrate.service"
|
|
];
|
|
systemd.services.phpfpm-davis.requires =
|
|
[
|
|
"davis-env-setup.service"
|
|
"davis-db-migrate.service"
|
|
]
|
|
++ lib.optional mysqlLocal "mysql.service"
|
|
++ lib.optional pgsqlLocal "postgresql.target";
|
|
systemd.services.phpfpm-davis.serviceConfig.ReadWritePaths = [ cfg.dataDir ];
|
|
|
|
services.nginx = lib.mkIf (cfg.nginx != null) {
|
|
enable = lib.mkDefault true;
|
|
virtualHosts = {
|
|
"${cfg.hostname}" = lib.mkMerge [
|
|
cfg.nginx
|
|
{
|
|
root = lib.mkForce "${cfg.package}/public";
|
|
extraConfig = ''
|
|
charset utf-8;
|
|
index index.php;
|
|
'';
|
|
locations = {
|
|
"/" = {
|
|
extraConfig = ''
|
|
try_files $uri $uri/ /index.php$is_args$args;
|
|
'';
|
|
};
|
|
"~* ^/.well-known/(caldav|carddav)$" = {
|
|
extraConfig = ''
|
|
return 302 https://$host/dav/;
|
|
'';
|
|
};
|
|
"~ ^(.+\\.php)(.*)$" = {
|
|
extraConfig = ''
|
|
try_files $fastcgi_script_name =404;
|
|
include ${config.services.nginx.package}/conf/fastcgi_params;
|
|
include ${config.services.nginx.package}/conf/fastcgi.conf;
|
|
fastcgi_pass unix:${config.services.phpfpm.pools.davis.socket};
|
|
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
|
|
fastcgi_param PATH_INFO $fastcgi_path_info;
|
|
fastcgi_split_path_info ^(.+\.php)(.*)$;
|
|
fastcgi_param X-Forwarded-Proto https;
|
|
fastcgi_param X-Forwarded-Port $http_x_forwarded_port;
|
|
'';
|
|
};
|
|
"~ /(\\.ht)" = {
|
|
extraConfig = ''
|
|
deny all;
|
|
return 404;
|
|
'';
|
|
};
|
|
};
|
|
}
|
|
];
|
|
};
|
|
};
|
|
|
|
services.mysql = lib.mkIf mysqlLocal {
|
|
enable = true;
|
|
package = lib.mkDefault pkgs.mariadb;
|
|
ensureDatabases = [ db.name ];
|
|
ensureUsers = [
|
|
{
|
|
name = user;
|
|
ensurePermissions = {
|
|
"${db.name}.*" = "ALL PRIVILEGES";
|
|
};
|
|
}
|
|
];
|
|
};
|
|
|
|
services.postgresql = lib.mkIf pgsqlLocal {
|
|
enable = true;
|
|
ensureDatabases = [ db.name ];
|
|
ensureUsers = [
|
|
{
|
|
name = user;
|
|
ensureDBOwnership = true;
|
|
}
|
|
];
|
|
};
|
|
};
|
|
|
|
meta = {
|
|
doc = ./davis.md;
|
|
maintainers = pkgs.davis.meta.maintainers;
|
|
};
|
|
}
|