2024-03-25 14:32:01 +00:00
{
config ,
lib ,
pkgs ,
. . .
} :
let
cfg = config . services . davis ;
db = cfg . database ;
mail = cfg . mail ;
mysqlLocal = db . createLocally && db . driver == " m y s q l " ;
pgsqlLocal = db . createLocally && db . driver == " p o s t g r e s q l " ;
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
" t r u e "
else if false == v then
" f a l s e "
else if null == v then
" "
else if isSecret v then
if ( lib . isString v . _secret ) then
builtins . hashString " s h a 2 5 6 " v . _secret
else
builtins . hashString " s h a 2 5 6 " ( builtins . readFile v . _secret )
else
throw " u n s u p p o r t e d t y p e ${ 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 " s h a 2 5 6 " file
else
builtins . hashString " s h a 2 5 6 " ( builtins . readFile file )
)
file
" ${ cfg . dataDir } / . e n v . l o c a l "
]
}
'' ;
secretReplacements = lib . concatMapStrings mkSecretReplacement secretPaths ;
filteredConfig = lib . converge ( lib . filterAttrsRecursive (
_ : v :
! lib . elem v [
{ }
null
]
) ) cfg . config ;
davisEnv = pkgs . writeText " d a v i s . e n v " ( davisEnvVars filteredConfig ) ;
in
{
options . services . davis = {
enable = lib . mkEnableOption " D a v i s i s a c a l d a v a n d c a r d d a v s e r v e r " ;
user = lib . mkOption {
default = " d a v i s " ;
description = " U s e r d a v i s r u n s a s . " ;
type = lib . types . str ;
} ;
group = lib . mkOption {
default = " d a v i s " ;
description = " G r o u p d a v i s r u n s a s . " ;
type = lib . types . str ;
} ;
package = lib . mkPackageOption pkgs " d a v i s " { } ;
dataDir = lib . mkOption {
type = lib . types . path ;
default = " / v a r / l i b / d a v i s " ;
description = ''
Davis data directory .
'' ;
} ;
hostname = lib . mkOption {
type = lib . types . str ;
example = " d a v i s . y o u r d o m a i n . o r g " ;
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 = " r o o t " ;
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 = " / r u n / s e c r e t s / d a v i s - a d m i n - p a s s " ;
} ;
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 = " / r u n / s e c r e t s / d a v i s - a p p s e c r e t " ;
} ;
database = {
driver = lib . mkOption {
type = lib . types . enum [
" s q l i t e "
" p o s t g r e s q l "
" m y s q l "
] ;
default = " s q l i t e " ;
description = " D a t a b a s e t y p e , r e q u i r e d i n a l l c i r c u m s t a n c e s . " ;
} ;
urlFile = lib . mkOption {
type = lib . types . nullOr lib . types . path ;
default = null ;
example = " / r u n / s e c r e t s / d a v i s - d b - u r l " ;
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 = " d a v i s " ;
description = " D a t a b a s e n a m e , o n l y u s e d w h e n t h e d a t a b s e i s c r e a t e d l o c a l l y . " ;
} ;
createLocally = lib . mkOption {
type = lib . types . bool ;
default = true ;
description = " C r e a t e t h e d a t a b a s e a n d d a t a b a s e u s e r l o c a l l y . " ;
} ;
} ;
mail = {
dsn = lib . mkOption {
type = lib . types . nullOr lib . types . str ;
default = null ;
description = " M a i l D S N f o r s e n d i n g e m a i l s . M u t u a l l y e x c l u s i v e w i t h ` s e r v i c e s . d a v i s . m a i l . d s n F i l e ` . " ;
example = " s m t p : / / u s e r n a m e : p a s s w o r d @ e x a m p l e . c o m : 2 5 " ;
} ;
dsnFile = lib . mkOption {
type = lib . types . nullOr lib . types . str ;
default = null ;
example = " / r u n / s e c r e t s / d a v i s - m a i l - d s n " ;
description = " A f i l e c o n t a i n i n g t h e m a i l D S N f o r s e n d i n g e m a i l s . M u t u a l l y e x c l u s i v e w i t h ` s e r v i e s . d a v i s . m a i l . d s n ` . " ;
} ;
inviteFromAddress = lib . mkOption {
type = lib . types . nullOr lib . types . str ;
default = null ;
description = " E m a i l a d d r e s s t o s e n d i n v i t a t i o n s f r o m . " ;
example = " n o - r e p l y @ d a v . e x a m p l e . c o m " ;
} ;
} ;
nginx = lib . mkOption {
type = lib . types . submodule (
lib . recursiveUpdate ( import ../web-servers/nginx/vhost-options.nix { inherit config lib ; } ) { }
) ;
default = null ;
example = ''
{
serverAliases = [
" d a v . ' ' ${ config . networking . domain } "
] ;
# To enable encryption and let let's encrypt take care of certificate
forceSSL = true ;
enableACME = true ;
}
'' ;
description = ''
With this option , you can customize the nginx virtualHost settings .
'' ;
} ;
poolConfig = lib . mkOption {
type = lib . types . attrsOf (
lib . types . oneOf [
lib . types . str
lib . types . int
lib . types . bool
]
) ;
default = {
" p m " = " d y n a m i c " ;
" p m . m a x _ c h i l d r e n " = 32 ;
" p m . s t a r t _ s e r v e r s " = 2 ;
" p m . m i n _ s p a r e _ s e r v e r s " = 2 ;
" p m . m a x _ s p a r e _ s e r v e r s " = 4 ;
" p m . m a x _ r e q u e s t s " = 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 = " p i d " ;
ProtectClock = true ;
ProtectControlGroups = true ;
ProtectHome = true ;
ProtectHostname = true ;
ProtectKernelLogs = true ;
ProtectKernelModules = true ;
ProtectKernelTunables = true ;
ProtectProc = " i n v i s i b l e " ;
ProtectSystem = " s t r i c t " ;
RemoveIPC = true ;
RestrictNamespaces = true ;
RestrictRealtime = true ;
RestrictSUIDSGID = true ;
SystemCallArchitectures = " n a t i v e " ;
SystemCallFilter = [
" @ s y s t e m - s e r v i c e "
" ~ @ r e s o u r c e s "
" ~ @ p r i v i l e g e d "
] ;
WorkingDirectory = " ${ cfg . package } / " ;
} ;
in
lib . mkIf cfg . enable {
assertions = [
{
assertion = db . createLocally -> db . urlFile == null ;
message = " s e r v i c e s . d a v i s . d a t a b a s e . u r l F i l e m u s t b e u n s e t i f s e r v i c e s . d a v i s . d a t a b a s e . c r e a t e L o c a l l y i s s e t t r u e . " ;
}
{
assertion = db . createLocally || db . urlFile != null ;
message = " O n e o f s e r v i c e s . d a v i s . d a t a b a s e . u r l F i l e o r s e r v i c e s . d a v i s . d a t a b a s e . c r e a t e L o c a l l y m u s t b e s e t . " ;
}
{
assertion = ( mail . dsn != null ) != ( mail . dsnFile != null ) ;
message = " O n e o f ( a n d o n l y o n e o f ) s e r v i c e s . d a v i s . m a i l . d s n o r s e r v i c e s . d a v i s . m a i l . d s n F i l e m u s t b e s e t . " ;
}
] ;
services . davis . config =
{
APP_ENV = " p r o d " ;
2024-04-02 07:33:41 +00:00
APP_CACHE_DIR = " ${ cfg . dataDir } / v a r / c a c h e " ;
2024-03-25 14:32:01 +00:00
# note: we do not need the log dir (we log to stdout/journald), by davis/symfony will try to create it, and the default value is one in the nix-store
# so we set it to a path under dataDir to avoid something like: Unable to create the "logs" directory (/nix/store/5cfskz0ybbx37s1161gjn5klwb5si1zg-davis-4.4.1/var/log).
2024-04-02 07:33:41 +00:00
APP_LOG_DIR = " ${ cfg . dataDir } / v a r / l o g " ;
2024-03-25 14:32:01 +00:00
LOG_FILE_PATH = " / d e v / s t d o u t " ;
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 { MAILER_DSN . _secret = mail . dsnFile ; } )
// (
if db . createLocally then
{
DATABASE_URL =
if db . driver == " s q l i t e " then
" s q l i t e : / / / ${ cfg . dataDir } / d a v i s . d b " # 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)
2024-04-02 07:33:41 +00:00
# specifically the dummy hostname which is overriden by the host query parameter
2024-03-25 14:32:01 +00:00
then
2024-04-02 07:33:41 +00:00
" p o s t g r e s : / / ${ user } @ l o c a l h o s t / ${ db . name } ? h o s t = / r u n / p o s t g r e s q l "
2024-03-25 14:32:01 +00:00
else if mysqlLocal then
" m y s q l : / / ${ user } @ l o c a l h o s t / ${ db . name } ? s o c k e t = / r u n / m y s q l d / m y s q l d . s o c k "
else
null ;
}
else
{ DATABASE_URL . _secret = db . urlFile ; }
) ;
users = {
users = lib . mkIf ( user == " d a v i s " ) {
davis = {
description = " D a v i s s e r v i c e u s e r " ;
group = cfg . group ;
isSystemUser = true ;
home = cfg . dataDir ;
} ;
} ;
groups = lib . mkIf ( group == " d a v i s " ) { davis = { } ; } ;
} ;
systemd . tmpfiles . rules = [
" d ${ cfg . dataDir } 0 7 1 0 ${ user } ${ group } - - "
" d ${ cfg . dataDir } / v a r 0 7 0 0 ${ user } ${ group } - - "
" d ${ cfg . dataDir } / v a r / l o g 0 7 0 0 ${ user } ${ group } - - "
" d ${ cfg . dataDir } / v a r / c a c h e 0 7 0 0 ${ user } ${ group } - - "
] ;
services . phpfpm . pools . davis = {
inherit user group ;
phpOptions = ''
log_errors = on
'' ;
phpEnv = {
ENV_DIR = " ${ cfg . dataDir } " ;
2024-04-02 07:33:41 +00:00
APP_CACHE_DIR = " ${ cfg . dataDir } / v a r / c a c h e " ;
APP_LOG_DIR = " ${ cfg . dataDir } / v a r / l o g " ;
2024-03-25 14:32:01 +00:00
} ;
settings =
{
" l i s t e n . m o d e " = " 0 6 6 0 " ;
" p m " = " d y n a m i c " ;
" p m . m a x _ c h i l d r e n " = 256 ;
" p m . s t a r t _ s e r v e r s " = 10 ;
" p m . m i n _ s p a r e _ s e r v e r s " = 5 ;
" p m . m a x _ s p a r e _ s e r v e r s " = 20 ;
}
// (
if cfg . nginx != null then
{
" l i s t e n . o w n e r " = config . services . nginx . user ;
" l i s t e n . g r o u p " = config . services . nginx . group ;
}
else
{ }
)
// cfg . poolConfig ;
} ;
# Reading the user-provided secret files requires root access
systemd . services . davis-env-setup = {
description = " S e t u p d a v i s e n v i r o n m e n t " ;
before = [
" p h p f p m - d a v i s . s e r v i c e "
" d a v i s - d b - m i g r a t e . s e r v i c e "
] ;
wantedBy = [ " m u l t i - u s e r . t a r g e t " ] ;
serviceConfig = {
Type = " o n e s h o t " ;
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 } / . e n v "
# create .env.local file with the user-provided values
install - T - m 0600 - o $ { user } $ { davisEnv } " ${ cfg . dataDir } / . e n v . l o c a l "
$ { secretReplacements }
'' ;
} ;
systemd . services . davis-db-migrate = {
description = " M i g r a t e d a v i s d a t a b a s e " ;
before = [ " p h p f p m - d a v i s . s e r v i c e " ] ;
after =
lib . optional mysqlLocal " m y s q l . s e r v i c e "
++ lib . optional pgsqlLocal " p o s t g r e s q l . s e r v i c e "
++ [ " d a v i s - e n v - s e t u p . s e r v i c e " ] ;
requires =
lib . optional mysqlLocal " m y s q l . s e r v i c e "
++ lib . optional pgsqlLocal " p o s t g r e s q l . s e r v i c e "
++ [ " d a v i s - e n v - s e t u p . s e r v i c e " ] ;
wantedBy = [ " m u l t i - u s e r . t a r g e t " ] ;
serviceConfig = defaultServiceConfig // {
Type = " o n e s h o t " ;
RemainAfterExit = true ;
Environment = [
" E N V _ D I R = ${ cfg . dataDir } "
2024-04-02 07:33:41 +00:00
" A P P _ C A C H E _ D I R = ${ cfg . dataDir } / v a r / c a c h e "
" A P P _ L O G _ D I R = ${ cfg . dataDir } / v a r / l o g "
2024-03-25 14:32:01 +00:00
] ;
EnvironmentFile = " ${ cfg . dataDir } / . e n v . l o c a l " ;
} ;
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 = [
" d a v i s - e n v - s e t u p . s e r v i c e "
" d a v i s - d b - m i g r a t e . s e r v i c e "
] ;
systemd . services . phpfpm-davis . requires = [
" d a v i s - e n v - s e t u p . s e r v i c e "
" d a v i s - d b - m i g r a t e . s e r v i c e "
] ++ lib . optional mysqlLocal " m y s q l . s e r v i c e " ++ lib . optional pgsqlLocal " p o s t g r e s q l . s e r v i c e " ;
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 } / p u b l i c " ;
extraConfig = ''
charset utf-8 ;
index index . php ;
'' ;
locations = {
" / " = {
extraConfig = ''
try_files $ uri $ uri / /index.php $ is_args $ args ;
'' ;
} ;
" ~ * ^ / . w e l l - k n o w n / ( c a l d a v | c a r d d a v ) $ " = {
extraConfig = ''
return 302 $ http_x_forwarded_proto : // $ host/dav / ;
'' ;
} ;
" ~ ^ ( . + \. p h p ) ( . * ) $ " = {
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 $ http_x_forwarded_proto ;
fastcgi_param X-Forwarded-Port $ http_x_forwarded_port ;
'' ;
} ;
" ~ / ( \\ . h t ) " = {
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 } . * " = " A L L P R I V I L E G E S " ;
} ;
}
] ;
} ;
services . postgresql = lib . mkIf pgsqlLocal {
enable = true ;
ensureDatabases = [ db . name ] ;
ensureUsers = [
{
name = user ;
ensureDBOwnership = true ;
}
] ;
} ;
} ;
meta = {
doc = ./davis.md ;
maintainers = pkgs . davis . meta . maintainers ;
} ;
}