From a37d79c31cf8ff153741ad6ecb2b48bc1178526e Mon Sep 17 00:00:00 2001 From: Raito Bezarius Date: Mon, 22 Nov 2021 21:39:44 +0100 Subject: [PATCH] modules: add initial sketch of wp for nginx --- modules/default.nix | 1 + modules/web-apps/wordpress/app.nix | 128 ++++++++++++++++++ .../web-apps/wordpress/default-app-config.nix | 113 ++++++++++++++++ modules/web-apps/wordpress/default.nix | 5 + modules/web-apps/wordpress/install-wp.nix | 37 +++++ modules/web-apps/wordpress/module.nix | 79 +++++++++++ modules/web-apps/wordpress/php-config.nix | 34 +++++ modules/web-apps/wordpress/phpfpm-conf.nix | 12 ++ modules/web-apps/wordpress/plugins.nix | 14 ++ modules/web-apps/wordpress/themes.nix | 8 ++ modules/web-apps/wordpress/utils.nix | 15 ++ modules/web-apps/wordpress/wordpress.nix | 7 + 12 files changed, 453 insertions(+) create mode 100644 modules/web-apps/wordpress/app.nix create mode 100644 modules/web-apps/wordpress/default-app-config.nix create mode 100644 modules/web-apps/wordpress/default.nix create mode 100644 modules/web-apps/wordpress/install-wp.nix create mode 100644 modules/web-apps/wordpress/module.nix create mode 100644 modules/web-apps/wordpress/php-config.nix create mode 100644 modules/web-apps/wordpress/phpfpm-conf.nix create mode 100644 modules/web-apps/wordpress/plugins.nix create mode 100644 modules/web-apps/wordpress/themes.nix create mode 100644 modules/web-apps/wordpress/utils.nix create mode 100644 modules/web-apps/wordpress/wordpress.nix diff --git a/modules/default.nix b/modules/default.nix index 004d887..ea1b3d7 100644 --- a/modules/default.nix +++ b/modules/default.nix @@ -2,4 +2,5 @@ acme-dns = ./servers/acme-dns.nix; drone-server = ./servers/drone.nix; drone-exec-runner = ./servers/drone-exec-runner.nix; + wordpress = ./web-apps/wordpress; } diff --git a/modules/web-apps/wordpress/app.nix b/modules/web-apps/wordpress/app.nix new file mode 100644 index 0000000..bf25e1a --- /dev/null +++ b/modules/web-apps/wordpress/app.nix @@ -0,0 +1,128 @@ +with import ./utils.nix; +let + writeableDefault = { + appPaths = []; # list of paths in the app to make writeable + pkgPath = "_writeable"; # path to create in read-only package that stores original content of writeable paths + sysPath = required "writeable.sysPath" ''system path to store writeable data (e.g. "/var/lib/phpfpm/my-app")''; + owner = required "writeable.owner" "user which owns the writeable files"; + }; +in +{ callPackage +, lib +, runCommand +, writeScript +, writeText + +, appConfig +, writeable ? writeableDefault +, ... +}: +let + # Merge the given writeable settings with the defaults. + writeable_ = writeableDefault // writeable; + + # We only care about writeble paths if the app is mostly frozen. + writeablePaths = lib.optionals appConfig.freezeWordPress ( + ["wp-content/uploads"] + ++ lib.optional (!appConfig.freezePlugins) "wp-content/plugins" + ++ lib.optional (!appConfig.freezeThemes) "wp-content/themes" + ++ writeable_.appPaths + ); + + wordpress = callPackage appConfig.wordpress {}; + plugins = callPackage appConfig.plugins {}; + themes = callPackage appConfig.themes {}; + + # The wp-config.php file. + wpConfigFile = writeText "wp-config.php" appConfig.wpConfig.rendered; + + # Generates a list of paths in bash that can be looped over. + listOfPaths = lib.concatMapStringsSep " " (x: "'${x}'"); + + # Generates the bash command to install a path from source to destination. + # If the install is `frozen`, then we simply symlink, otherwise we copy. + installPath = isFrozen: from: to: + (if isFrozen then "ln -s" else "cp -r") + " " + ''"${from}" "${to}"''; + + # The build script for the app. + # This will install WordPress, the wp-config, plugins, and themes. + # If any writeble paths were configured, this script will copy them to a another folder in the + # package and set up symlinks in their place the given writeable path on the system. + buildPackageAt = out: '' + mkdir -p $(dirname "${out}") # Make parent directory. + + cp -r "${wordpress}" "${out}" + chmod -R +w "${out}" + + ${installPath appConfig.freezeWordPress wpConfigFile "${out}/wp-config.php"} + + # Install themes. + rm -r "${out}/wp-content/themes"/* # remove bundled themes + ${lib.concatMapStringsSep "\n" (x: installPath appConfig.freezeThemes x "${out}/wp-content/themes/${x.name}") themes} + + # Install plugins. + rm -r "${out}/wp-content/plugins"/* # remove bundled plugins + ${lib.concatMapStringsSep "\n" (x: installPath appConfig.freezePlugins x "${out}/wp-content/plugins/${x.name}") plugins} + + # TODO: Support translations. + + ${lib.optionalString (writeablePaths != []) '' + # Make symlinks to writeable directories. + writeable_orig_dir="${out}/${writeable_.pkgPath}" + mkdir -p "$writeable_orig_dir" + + for thing in ${listOfPaths writeablePaths}; do + original_thing="$writeable_orig_dir/$thing" + parent=$(dirname "$original_thing") + mkdir -p "$parent" + + # Move any existing data to the frozen writeable dir or create empty directory there. + mv "${out}/$thing" "$parent" || mkdir -p "$original_thing" + + ln -s "${writeable_.sysPath}/$thing" "${out}/$thing" + done + ''} + ''; + + # Copy the original writeable contents of the package to a writeable dir. + initWriteablePathsFor = package: '' + mkdir -p "$out" + writeable_orig_dir="${package}/${writeable_.pkgPath}" + for thing in $( ls "$writeable_orig_dir" ); do + cp -r "$writeable_orig_dir/$thing" "$out" + done + ''; + + # Takes an existing script and makes a initialization script that only runs if the output path + # has not been built yet. + mkInitScript = script: writeScript "init-writeable-paths" '' + #!/bin/sh + + out="${writeable_.sysPath}" + + if [ ! -d "$out" ]; then + + ${script} + + chown -R "${writeable_.owner}" "$out" + chmod -R 744 "$out" + + else + echo Output directory already exists. Not building path: "$out" + fi + ''; + +in if appConfig.freezeWordPress + then rec { + # For a mostly frozen app, we install it as a package and set up writeable paths on first run. + initScript = mkInitScript (initWriteablePathsFor package); + package = runCommand "wordpress-app" { + preferLocalBuild = true; + } (buildPackageAt "$out"); + } + else rec { + # For fully writeable app, we skip package installation and write the app directly to the + # writeable path on first run. + initScript = mkInitScript (buildPackageAt package); + package = writeable_.sysPath; + } diff --git a/modules/web-apps/wordpress/default-app-config.nix b/modules/web-apps/wordpress/default-app-config.nix new file mode 100644 index 0000000..3a68233 --- /dev/null +++ b/modules/web-apps/wordpress/default-app-config.nix @@ -0,0 +1,113 @@ +let + lib = (import {}).lib; +in lib.makeExtensible (self: { + domain = "wordpress-site.dev"; + + # Simple name used for directories, etc. + # WARNING: Changing this after a deployment will change the location of data directories and will + # likely result in a partial reset of your application. You must move data from the + # previous app folders to the new ones. + name = "wordpress-app"; + + description = "A Wordpress Site"; # Brief, one-line description or title + tagline = "Deployed with Nixops"; + host = "www.${self.domain}"; + adminEmail = "admin@${self.domain}"; + + siteUrl = "${if self.enableHttps then "https" else "http"}://${self.host}"; + + # Hosts that get redirected to the primary host. + hostRedirects = [self.domain]; + + # Configure timezone settings (http://php.net/manual/en/timezones.php) + timezone = "UTC"; + + # WP-CLI settings for automatic install + autoInstall = let + adminConfig = import ./wordpress-admin.keys.nix; + in lib.makeExtensible (innerSelf: { + enable = false; # set to `true` to automatically install WordPress configuration + inherit (adminConfig) adminUser adminPassword; + }); + + wordpress = import ./wordpress.nix; + plugins = import ./plugins.nix; + themes = import ./themes.nix; + + # Warning: Changing these after your site has been deployed will require manual + # work on the server. We don't want to do anything that would lose + # data so we leave that to you. + freezeWordPress = true; # Can admins upgrade WordPress in the CMS? + freezePlugins = true; # Can admins edit plugins in the CMS? + freezeThemes = true; # Can admins edit themes in the CMS? + + dbConfig = lib.makeExtensible (innerSelf: { + isLocal = true; # if `true`, MySQL will be installed on the server. + name = "wordpress"; # database name + user = "root"; + password = ""; + host = "localhost"; + charset = "utf8mb4"; + tablePrefix = "wp_"; + }); + + wpConfig = lib.makeExtensible (innerSelf: { + # Generate this file with `curl https://api.wordpress.org/secret-key/1.1/salt/ > wordpress-keys.php.secret` + secrets = builtins.readFile ./wordpress-keys.php.secret; + debugMode = false; + extraConfig = '' + define('WP_HOME', '${self.siteUrl}'); + define('WP_SITEURL', '${self.siteUrl}'); + ''; + + inherit (self) dbConfig; + + template = import ./wp-config.nix; + rendered = innerSelf.template innerSelf; + }); + + + # Server settings + enableHttps = true; + maxUploadMb = 50; + + # --- ADVANCED CONFIGURATION --- + opcache = lib.makeExtensible (innerSelf: { + enable = true; + maxMemoryMb = 128; + + # How often to invalidate timestamp cache. This is only used when the project + # has non-frozen components (see above). + # http://php.net/manual/en/opcache.configuration.php#ini.opcache.revalidate-freq + revalidateFreqSec = 60; + }); + + # PHP-FPM settings for the *dynamic* process manager: http://php.net/manual/en/install.fpm.configuration.php#pm + phpFpmProcessSettings = lib.makeExtensible (innerSelf: { + max_children = 10; + start_servers = innerSelf.min_spare_servers; # WARNING: min_spare_servers <= start_servers <= max_spare_servers + min_spare_servers = 2; + max_spare_servers = 5; + max_requests = 500; + }); + + googlePageSpeed = lib.makeExtensible (innerSelf: { + enable = true; + cachePath = "/run/nginx-pagespeed-cache"; # /run/ is tmpfs and will keep cache in memory + }); + + fastCgiCache = lib.makeExtensible (innerSelf: { + enable = true; + cachePath = "/run/nginx-fastcgi-cache"; # /run/ is tmpfs and will keep cache in memory + }); + + php = lib.makeExtensible (innerSelf: { + enableXDebug = false; + + scriptMemoryLimitMb = 128; + maxExecutionTimeSec = 300; + + # sendmail_path configuration for php.ini files + sendmailPath = "/run/wrappers/bin/sendmail -t -i"; + }); +}) diff --git a/modules/web-apps/wordpress/default.nix b/modules/web-apps/wordpress/default.nix new file mode 100644 index 0000000..8a2bda4 --- /dev/null +++ b/modules/web-apps/wordpress/default.nix @@ -0,0 +1,5 @@ +{ + imports = [ + ./module.nix + ]; +} diff --git a/modules/web-apps/wordpress/install-wp.nix b/modules/web-apps/wordpress/install-wp.nix new file mode 100644 index 0000000..6593c08 --- /dev/null +++ b/modules/web-apps/wordpress/install-wp.nix @@ -0,0 +1,37 @@ +{ pkgs +, config # system configuration +, appConfig +, appPackage +, writeableDataPath +}: +pkgs.writeScript "install-wordpress.sh" '' + #!${pkgs.stdenv.shell} -eu + + if ! $('${pkgs.wp-cli}/bin/wp' core is-installed --path='${appPackage}' --allow-root); then + echo 'Installing WordPress configuration for ${appConfig.host}' + '${pkgs.wp-cli}/bin/wp' core install \ + --url='${appConfig.siteUrl}' \ + --title='${appConfig.description}' \ + --admin_user='${appConfig.autoInstall.adminUser}' \ + --admin_password='${appConfig.autoInstall.adminPassword}' \ + --admin_email='${appConfig.adminEmail}' \ + --path='${appPackage}' \ + --allow-root; + chown -R '${config.services.nginx.user}' '${writeableDataPath}'; + else + echo 'WordPress configuration already installed for ${appConfig.host}' + fi + + '${pkgs.wp-cli}/bin/wp' option update blogname '${appConfig.description}' \ + --path='${appPackage}' \ + --allow-root; + + '${pkgs.wp-cli}/bin/wp' option update blogdescription '${appConfig.tagline}' \ + --path='${appPackage}' \ + --allow-root; + + # TODO: Provide a list of plugins to be activated from plugins.nix + '${pkgs.wp-cli}/bin/wp' plugin activate nginx-helper opcache \ + --path='${appPackage}' \ + --allow-root; +'' diff --git a/modules/web-apps/wordpress/module.nix b/modules/web-apps/wordpress/module.nix new file mode 100644 index 0000000..3b388ec --- /dev/null +++ b/modules/web-apps/wordpress/module.nix @@ -0,0 +1,79 @@ +{ config, pkgs, lib, ... }: +with lib; +let + cfg = config.services.wordpress; + appConfig = (import ./default-app-config.nix).extend (self: super: {}); + writeableDataPath = "/var/lib/phpfpm/${appConfig.name}"; + phpFpmListen = "/run/phpfpm/wordpress-pool.sock"; + phpIni = import ./php-config.nix { inherit pkgs config appConfig; }; + enablePageSpeed = pkgs.stdenv.isLinux && appConfig.googlePageSpeed.enable; + app = callPackage ./app.nix { + inherit appConfig; + writeable = { + sysPath = writeableDataPath; + owner = config.services.nginx.user; + }; + }; +in + { + options.services.wordpress = { + enable = mkEnableOption "Enable the WordPress module"; + }; + config = mkIf cfg.enable { + environment.systemPackages = [ + pkgs.wp-cli + ]; + + services.nginx = { + enable = true; + # package = pkgs.callPackage ./nginx.nix { inherit enablePageSpeed; }; + # httpConfig = nginxConfig; + # TODO: ajouter les locations pour wordpress + }; + + systemd.services.init-writeable-paths = { + description = "Initialize writeable directories for the app"; + before = [ "phpfpm.service" ]; + after = [ "network.target" ]; + wantedBy = [ "multi-user.target" "phpfpm.service" "nginx.service" ]; + serviceConfig = { + Type = "oneshot"; + ExecStart = app.initScript; + }; + }; + + + systemd.services.install-wp = let + deps = [ "init-writeable-paths.service" "mysql.service" ]; + in { + enable = appConfig.autoInstall.enable; + description = "Configure WordPress installation with WP-CLI"; + before = [ "nginx.service" ]; + after = deps; + wants = deps; + wantedBy = [ "multi-user.target" ]; + serviceConfig = { + Type = "oneshot"; + ExecStart = import ./install-wp.nix { + inherit pkgs config appConfig writeableDataPath; + appPackage = app.package; + }; + }; + environment.PHP_INI_SCAN_DIR = let + customIni = pkgs.writeTextDir "wp-cli-custom.ini" phpIni; + in "${pkgs.php}/etc:${customIni}"; + }; + + services.phpfpm = { + phpOptions = phpIni; + pools.wordpress-pool = import ./phpfpm-conf.nix { + inherit pkgs config phpFpmListen; + processSettings = appConfig.phpFpmProcessSettings; + }; + }; + + services.mysql = { + enable = true; + package = pkgs.mariadb; + }; + } diff --git a/modules/web-apps/wordpress/php-config.nix b/modules/web-apps/wordpress/php-config.nix new file mode 100644 index 0000000..5dfb1c7 --- /dev/null +++ b/modules/web-apps/wordpress/php-config.nix @@ -0,0 +1,34 @@ +{ pkgs +, config # system configuration +, appConfig +}: + +assert appConfig.php.scriptMemoryLimitMb > appConfig.maxUploadMb; + +'' + memory_limit ${toString appConfig.php.scriptMemoryLimitMb}M + + extension = "${pkgs.phpPackages.imagick}/lib/php/extensions/imagick.so" + + ${pkgs.lib.optionalString appConfig.opcache.enable '' + zend_extension = "${config.services.phpfpm.phpPackage}/lib/php/extensions/opcache.so" + ''} + + ${pkgs.lib.optionalString appConfig.php.enableXDebug '' + ; WARNING: Be sure to load opcache *before* xdebug (http://us3.php.net/manual/en/opcache.installation.php). + zend_extension = "${pkgs.phpPackages.xdebug}/lib/php/extensions/xdebug.so" + ''} + + upload_max_filesize = ${toString appConfig.maxUploadMb}M + post_max_size = ${toString appConfig.maxUploadMb}M + max_execution_time ${toString appConfig.php.maxExecutionTimeSec} + + date.timezone = "${appConfig.timezone}" + sendmail_path = ${appConfig.php.sendmailPath} + + ${import ./opcache-config.nix (appConfig.opcache // { + # Enable timestamp validation if the setup is not entirely frozen (managed by Nix). + validateTimestamps = ! builtins.all (x: x) + [appConfig.freezeWordPress appConfig.freezePlugins appConfig.freezeThemes]; + })} +'' diff --git a/modules/web-apps/wordpress/phpfpm-conf.nix b/modules/web-apps/wordpress/phpfpm-conf.nix new file mode 100644 index 0000000..77bfb38 --- /dev/null +++ b/modules/web-apps/wordpress/phpfpm-conf.nix @@ -0,0 +1,12 @@ +{ pkgs, config, phpFpmListen, processSettings }: +{ + inherit (config.services.nginx) user group; + settings = { + "listen.owner" = config.services.nginx.user; + "listen.group" = config.services.nginx.group; + "listen.mode" = 660; + "pm" = "dynamic"; + "catch_workers_output" = "yes"; + } // processSettings; + listen = phpFpmListen; +} diff --git a/modules/web-apps/wordpress/plugins.nix b/modules/web-apps/wordpress/plugins.nix new file mode 100644 index 0000000..267c4c4 --- /dev/null +++ b/modules/web-apps/wordpress/plugins.nix @@ -0,0 +1,14 @@ +# A list of your WordPress plugins. +{ callPackage, ... }: +let + utils = callPackage ./utils.nix {}; + getPlugin = utils.getPlugin; + + requiredPlugins = [ + (getPlugin "opcache" "0.3.1" "18x6fnfc7ka4ynxv4z3rf4011ivqc0qy0dsd6i4lxa113jjyqz6d") + (getPlugin "nginx-helper" "1.9.10" "1n887qz9rzs8yj069wva6cirp6y46a49wspzja4grdj2qirr4hky") + ]; +in requiredPlugins ++ [ + (getPlugin "akismet" "3.3" "02vsjnr7bs54a744p64rx7jwlbcall6nhh1mv6w54zbwj4ygqz68") + (getPlugin "jetpack" "4.8.2" "17bvkcb17dx969a30j0axb5kqzfxnx1sqkcdwwrski9gh7ihabqk") +] diff --git a/modules/web-apps/wordpress/themes.nix b/modules/web-apps/wordpress/themes.nix new file mode 100644 index 0000000..97e9a53 --- /dev/null +++ b/modules/web-apps/wordpress/themes.nix @@ -0,0 +1,8 @@ +# A list of your WordPress themes. +{ callPackage, ... }: +let + utils = callPackage ./utils.nix {}; + getTheme = utils.getTheme; +in [ + (getTheme "twentyseventeen" "1.1" "1xsdz1s68mavz9i4lhckh7rqw266jqm5mn3ql1gbz03zf6ghf982") +] diff --git a/modules/web-apps/wordpress/utils.nix b/modules/web-apps/wordpress/utils.nix new file mode 100644 index 0000000..949fba2 --- /dev/null +++ b/modules/web-apps/wordpress/utils.nix @@ -0,0 +1,15 @@ +{ + traced = x: builtins.trace x x; + required = arg: help: builtins.abort "${arg} is required: ${help}"; + + # Converts a set into a string + # pkgs is nixpkgs + # sep is a string separator to place between each field + # mapFn is (string -> string -> string) taking key and value for each attribute + # attrs is the set to process + setToString = pkgs: sep: mapFn: attrs: pkgs.lib.concatStringsSep sep ( + pkgs.lib.mapAttrsToList + (key: val: if builtins.isInt val || builtins.isString val then mapFn key val else "") + attrs + ); +} diff --git a/modules/web-apps/wordpress/wordpress.nix b/modules/web-apps/wordpress/wordpress.nix new file mode 100644 index 0000000..1ec8356 --- /dev/null +++ b/modules/web-apps/wordpress/wordpress.nix @@ -0,0 +1,7 @@ +{ fetchzip, runCommand, ... }: +let + version = "4.8"; +in fetchzip { + url = "https://wordpress.org/wordpress-${version}.tar.gz"; + sha256 = "1myflpa9pxcghnhjfd0ahqpsvgcwh3szk2k8w2x7qmvfll69n3j9"; +}