drupal: init at 11.1.7, nixos/drupal: init (#407034)

This commit is contained in:
Pol Dellaiera 2025-06-03 15:26:29 +02:00 committed by GitHub
commit 1028002a94
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 552 additions and 0 deletions

View file

@ -18686,6 +18686,12 @@
github = "Oughie";
githubId = 123173954;
};
OulipianSummer = {
name = "Andrew Benbow";
github = "OulipianSummer";
githubId = 47955980;
email = "abmurrow@duck.com";
};
outfoxxed = {
name = "outfoxxed";
email = "nixpkgs@outfoxxed.me";

View file

@ -1533,6 +1533,7 @@
./services/web-apps/documize.nix
./services/web-apps/dokuwiki.nix
./services/web-apps/dolibarr.nix
./services/web-apps/drupal.nix
./services/web-apps/echoip.nix
./services/web-apps/eintopf.nix
./services/web-apps/engelsystem.nix

View file

@ -0,0 +1,478 @@
{
config,
lib,
pkgs,
...
}:
let
inherit (lib)
any
attrValues
flatten
literalExpression
mapAttrs
mapAttrs'
mapAttrsToList
mkDefault
mkEnableOption
mkIf
mkMerge
mkOption
mkPackageOption
nameValuePair
optionalAttrs
types
;
inherit (pkgs)
mariadb
stdenv
writeShellScript
;
cfg = config.services.drupal;
eachSite = cfg.sites;
user = "drupal";
webserver = config.services.${cfg.webserver};
pkg =
hostName: cfg:
stdenv.mkDerivation (finalAttrs: {
pname = "drupal-${hostName}";
name = "drupal-${hostName}";
src = cfg.package;
installPhase = ''
runHook preInstall
mkdir -p $out
cp -r * $out/
runHook postInstall
'';
postInstallPhase = ''
ln -s ${cfg.filesDir} $out/share/php/drupal/sites/default/files
ln -s ${cfg.stateDir}/sites/default/settings.php $out/share/php/drupal/sites/default/settings.php
ln -s ${cfg.modulesDir} $out/share/php/drupal/modules
ln -s ${cfg.themesDir} $out/share/php/drupal/themes
'';
});
siteOpts =
{
options,
config,
lib,
name,
...
}:
{
options = {
enable = mkEnableOption "Drupal web application";
package = mkPackageOption pkgs "drupal" { };
filesDir = mkOption {
type = types.path;
default = "/var/lib/drupal/${name}/sites/default/files";
defaultText = "/var/lib/drupal/<name>/sites/default/files";
description = ''
The location of the Drupal files directory.
'';
};
stateDir = mkOption {
type = types.path;
default = "/var/lib/drupal/${name}";
defaultText = "/var/lib/drupal/<name>";
description = "The location of the Drupal site state directory.";
};
modulesDir = mkOption {
type = types.path;
default = "/var/lib/drupal/${name}/modules";
defaultText = "/var/lib/drupal/<name>/modules";
description = "The location of Drupal modules.";
};
themesDir = mkOption {
type = types.path;
default = "/var/lib/drupal/${name}/themes";
defaultText = "/varlib/drupal/<name>/themes";
description = "The location of Drupal themes.";
};
phpOptions = mkOption {
type = types.attrsOf types.str;
default = { };
description = ''
Options for PHP's php.ini file for this Drupal site.
'';
example = literalExpression ''
{
"opcache.interned_strings_buffer" = "8";
"opcache.max_accelerated_files" = "10000";
"opcache.memory_consumption" = "128";
"opcache.revalidate_freq" = "15";
"opcache.fast_shutdown" = "1";
}
'';
};
database = {
host = mkOption {
type = types.str;
default = "localhost";
description = "Database host address.";
};
port = mkOption {
type = types.port;
default = 3306;
description = "Database host port.";
};
name = mkOption {
type = types.str;
default = "drupal";
description = "Database name.";
};
user = mkOption {
type = types.str;
default = "drupal";
description = "Database user.";
};
passwordFile = mkOption {
type = types.nullOr types.path;
default = null;
example = "/run/keys/database-dbpassword";
description = ''
A file containing the password corresponding to
{option}`database.user`.
'';
};
tablePrefix = mkOption {
type = types.str;
default = "dp_";
description = ''
The $table_prefix is the value placed in the front of your database tables.
Change the value if you want to use something other than dp_ for your database
prefix. Typically this is changed if you are installing multiple Drupal sites
in the same database.
'';
};
socket = mkOption {
type = types.nullOr types.path;
default = null;
defaultText = literalExpression "/run/mysqld/mysqld.sock";
description = "Path to the unix socket file to use for authentication.";
};
createLocally = mkOption {
type = types.bool;
default = true;
description = "Create the database and database user locally.";
};
};
virtualHost = mkOption {
type = types.submodule (import ../web-servers/apache-httpd/vhost-options.nix);
example = literalExpression ''
{
adminAddr = "webmaster@example.org";
forceSSL = true;
enableACME = true;
}
'';
description = ''
Apache configuration can be done by adapting {option}`services.httpd.virtualHosts`.
'';
};
poolConfig = mkOption {
type =
with types;
attrsOf (oneOf [
str
int
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 Drupal PHP pool. See the documentation on `php-fpm.conf`
for details on configuration directives.
'';
};
};
config.virtualHost.hostName = mkDefault name;
};
in
{
options = {
services.drupal = {
enable = mkEnableOption "drupal";
package = mkPackageOption pkgs "drupal" { };
sites = mkOption {
type = types.attrsOf (types.submodule siteOpts);
default = {
"localhost" = {
enable = true;
};
};
description = "Specification of one or more Drupal sites to serve";
};
webserver = mkOption {
type = types.enum [
"nginx"
"caddy"
];
default = "nginx";
description = ''
Whether to use nginx or caddy for virtual host management.
Further nginx configuration can be done by adapting `services.nginx.virtualHosts.<name>`.
See [](#opt-services.nginx.virtualHosts) for further information.
Further caddy configuration can be done by adapting `services.caddy.virtualHosts.<name>`.
See [](#opt-services.caddy.virtualHosts) for further information.
'';
};
};
};
config = mkIf (cfg.enable) (mkMerge [
{
assertions =
(mapAttrsToList (hostName: cfg: {
assertion = cfg.database.createLocally -> cfg.database.user == user;
message = ''services.drupal.sites."${hostName}".database.user must be ${user} if the database is to be automatically provisioned'';
}) eachSite)
++ (mapAttrsToList (hostName: cfg: {
assertion = cfg.database.createLocally -> cfg.database.passwordFile == null;
message = ''services.drupal.sites."${hostName}".database.passwordFile cannot be specified if services.drupal.sites."${hostName}".database.createLocally is set to true.'';
}) eachSite);
services.mysql = mkIf (any (v: v.database.createLocally) (attrValues eachSite)) {
enable = true;
package = mkDefault mariadb;
ensureDatabases = mapAttrsToList (hostName: cfg: cfg.database.name) eachSite;
ensureUsers = mapAttrsToList (hostName: cfg: {
name = cfg.database.user;
ensurePermissions = {
"${cfg.database.name}.*" = "ALL PRIVILEGES";
};
}) eachSite;
};
services.phpfpm.pools = mapAttrs' (
hostName: cfg:
(nameValuePair "drupal-${hostName}" {
inherit user;
group = webserver.group;
settings = {
"listen.owner" = webserver.user;
"listen.group" = webserver.group;
} // cfg.poolConfig;
})
) eachSite;
}
{
systemd.tmpfiles.rules = flatten (
mapAttrsToList (hostName: cfg: [
"d '${cfg.stateDir}' 0750 ${user} ${webserver.group} - -"
"d '${cfg.modulesDir}' 0750 ${user} ${webserver.group} - -"
"Z '${cfg.modulesDir}' 0750 ${user} ${webserver.group} - -"
"d '${cfg.themesDir}' 0750 ${user} ${webserver.group} - -"
"Z '${cfg.themesDir}' 0750 ${user} ${webserver.group} - -"
]) eachSite
);
users.users.${user} = {
group = webserver.group;
isSystemUser = true;
};
}
{
# Run a service that prepares the state directory.
systemd.services = mkMerge [
(mapAttrs' (
hostName: cfg:
(nameValuePair "drupal-state-init-${hostName}" {
wantedBy = [ "multi-user.target" ];
before = [ "nginx.service" ];
after = [ "local-fs.target" ];
serviceConfig = {
Type = "oneshot";
User = "root";
ExecStart = writeShellScript "drupal-state-init-${hostName}" ''
set -e
if [ ! -d "${cfg.stateDir}/sites" ]; then
echo "Preparing sites directory..."
cp -r "${cfg.package}/share/php/drupal/sites" "${cfg.stateDir}"
fi
if [ ! -d "${cfg.filesDir}" ]; then
echo "Preparing files directory..."
mkdir -p "${cfg.filesDir}"
chown -R ${user}:${webserver.group} ${cfg.filesDir}
fi
settings="${cfg.stateDir}/sites/default/settings.php"
defaultSettings="${cfg.package}/share/php/drupal/sites/default/default.settings.php"
if [ ! -f "$settings" ]; then
echo "Preparing settings.php for ${hostName}..."
cp "$defaultSettings" "$settings"
chmod 644 "$settings"
fi
# Set or reset file permissions so that the web user and webserver owns them.
chown -R ${user}:${webserver.group} ${cfg.stateDir}
'';
};
})
) eachSite)
(optionalAttrs (any (v: v.database.createLocally) (attrValues eachSite)) {
httpd.after = [ "mysql.service" ];
})
];
}
(mkIf (cfg.webserver == "nginx") {
services.nginx = {
enable = true;
virtualHosts = mapAttrs (hostName: cfg: {
serverName = mkDefault hostName;
root = "${pkg hostName cfg}/share/php/drupal";
extraConfig = ''
index index.php;
'';
locations = {
"~ '\.php$|^/update.php'" = {
extraConfig = ''
fastcgi_split_path_info ^(.+\.php)(/.+)$;
fastcgi_pass unix:${config.services.phpfpm.pools."drupal-${hostName}".socket};
fastcgi_index index.php;
include "${config.services.nginx.package}/conf/fastcgi.conf";
fastcgi_param PATH_INFO $fastcgi_path_info;
fastcgi_param PATH_TRANSLATED $document_root$fastcgi_path_info;
# Mitigate https://httpoxy.org/ vulnerabilities
fastcgi_param HTTP_PROXY "";
fastcgi_intercept_errors off;
fastcgi_buffer_size 16k;
fastcgi_buffers 4 16k;
fastcgi_connect_timeout 300;
fastcgi_send_timeout 300;
fastcgi_read_timeout 300;
'';
};
"= /favicon.ico" = {
extraConfig = ''
log_not_found off;
access_log off;
'';
};
"= /robots.txt" = {
extraConfig = ''
allow all;
log_not_found off;
access_log off;
'';
};
"~ \..*/.*\.php$" = {
extraConfig = ''
return 403;
'';
};
"~ ^/sites/.*/private/" = {
extraConfig = ''
return 403;
'';
};
"~ ^/sites/[^/]+/files/.*\.php$" = {
extraConfig = ''
deny all;
'';
};
"~* ^/.well-known/" = {
extraConfig = ''
allow all;
'';
};
"/" = {
extraConfig = ''
try_files $uri /index.php?$query_string;
'';
};
"@rewrite" = {
extraConfig = ''
rewrite ^ /index.php;
'';
};
"~ /vendor/.*\.php$" = {
extraConfig = ''
deny all;
return 404;
'';
};
"~* \.(js|css|png|jpg|jpeg|gif|ico|svg)$" = {
extraConfig = ''
try_files $uri @rewrite;
expires max;
log_not_found off;
'';
};
"~ ^/sites/.*/files/styles/" = {
extraConfig = ''
try_files $uri @rewrite;
'';
};
"~ ^(/[a-z\-]+)?/system/files/" = {
extraConfig = ''
try_files $uri /index.php?$query_string;
'';
};
};
}) eachSite;
};
})
(mkIf (cfg.webserver == "caddy") {
services.caddy = {
enable = true;
virtualHosts = mapAttrs' (
hostName: cfg:
(nameValuePair "http://${hostName}" {
extraConfig = ''
root * ${pkg hostName cfg}/share/php/drupal
file_server
encode zstd gzip
php_fastcgi unix/${config.services.phpfpm.pools."drupal-${hostName}".socket}
'';
})
) cfg.sites;
};
})
]);
}

View file

@ -410,6 +410,7 @@ in
drawterm = discoverTests (import ./drawterm.nix);
drbd = runTest ./drbd.nix;
druid = handleTestOn [ "x86_64-linux" ] ./druid { };
drupal = runTest ./drupal.nix;
drbd-driver = runTest ./drbd-driver.nix;
dublin-traceroute = runTest ./dublin-traceroute.nix;
earlyoom = handleTestOn [ "x86_64-linux" ] ./earlyoom.nix { };

27
nixos/tests/drupal.nix Normal file
View file

@ -0,0 +1,27 @@
{ lib, ... }:
{
name = "drupal";
nodes = {
machine_default =
{ pkgs, ... }:
{
services.drupal = {
enable = true;
};
};
};
testScript = ''
machine_default.start()
machine_default.wait_for_unit("phpfpm-drupal-localhost.service")
machine_default.wait_for_unit("nginx.service")
machine_default.wait_for_unit("mysql.service")
'';
meta.maintainers = [
lib.maintainers.drupol
lib.maintainers.OulipianSummer
];
}

View file

@ -0,0 +1,39 @@
{
lib,
fetchFromGitLab,
php,
nixosTests,
}:
php.buildComposerProject2 (finalAttrs: {
pname = "drupal";
version = "11.1.7";
src = fetchFromGitLab {
domain = "git.drupalcode.org";
owner = "project";
repo = "drupal";
tag = finalAttrs.version;
hash = "sha256-jf28r44VDP9MzShoJMFD+6xSUcKBRGYJ1/ruQ3nGTRE=";
};
vendorHash = "sha256-LUZTf/Zn8p+V2K1LjhvrgaGBiTcSmGRsG1t9vXUcbeY=";
composerNoPlugins = false;
passthru = {
tests = {
inherit (nixosTests) drupal;
};
};
meta = {
description = "Drupal CMS";
license = lib.licenses.mit;
homepage = "https://drupal.org/";
maintainers = with lib.maintainers; [
drupol
OulipianSummer
];
platforms = php.meta.platforms;
};
})