From 6b42e8524dcb3a4cf3b69b66f6b6ad2491f026c4 Mon Sep 17 00:00:00 2001 From: Morgan Jones Date: Wed, 28 Dec 2022 18:38:03 -0800 Subject: [PATCH] mattermost: support plugins with refactor --- .../modules/services/web-apps/mattermost.nix | 111 +++++++++++++++++- nixos/tests/mattermost.nix | 55 +++++++-- 2 files changed, 153 insertions(+), 13 deletions(-) diff --git a/nixos/modules/services/web-apps/mattermost.nix b/nixos/modules/services/web-apps/mattermost.nix index 3a96628014cc3..846b8c0a49994 100644 --- a/nixos/modules/services/web-apps/mattermost.nix +++ b/nixos/modules/services/web-apps/mattermost.nix @@ -5,7 +5,62 @@ with lib; let cfg = config.services.mattermost; - mattermostConf = recursiveUpdate + # The directory to store mutable data within dataDir. + mutableDataDir = "${cfg.dataDir}/data"; + + # The plugin directory. Note that this is the *post-unpack* plugin directory, + # since Mattermost unpacks plugins to put them there. (Hence, mutable data.) + pluginDir = "${mutableDataDir}/plugins"; + + # Mattermost uses this as a staging directory to unpack plugins, among possibly other things. + # Ensure that it's inside mutableDataDir since it can get rather large. + tempDir = "${mutableDataDir}/tmp"; + + mattermostPluginDerivations = with pkgs; + map (plugin: stdenv.mkDerivation { + name = "mattermost-plugin"; + installPhase = '' + mkdir -p $out/share + cp ${plugin} $out/share/plugin.tar.gz + ''; + dontUnpack = true; + dontPatch = true; + dontConfigure = true; + dontBuild = true; + preferLocalBuild = true; + }) cfg.plugins; + + mattermostPlugins = with pkgs; + if mattermostPluginDerivations == [] then null + else stdenv.mkDerivation { + name = "${cfg.package.name}-plugins"; + nativeBuildInputs = [ + autoPatchelfHook + ] ++ mattermostPluginDerivations; + buildInputs = [ + cfg.package + ]; + installPhase = '' + mkdir -p $out + plugins=(${escapeShellArgs (map (plugin: "${plugin}/share/plugin.tar.gz") mattermostPluginDerivations)}) + for plugin in "''${plugins[@]}"; do + hash="$(sha256sum "$plugin" | cut -d' ' -f1)" + mkdir -p "$hash" + tar -C "$hash" -xzf "$plugin" + autoPatchelf "$hash" + GZIP_OPT=-9 tar -C "$hash" -cvzf "$out/$hash.tar.gz" . + rm -rf "$hash" + done + ''; + + dontUnpack = true; + dontPatch = true; + dontConfigure = true; + dontBuild = true; + preferLocalBuild = true; + }; + + mattermostConfWithoutPlugins = recursiveUpdate { ServiceSettings.SiteURL = cfg.siteUrl; ServiceSettings.ListenAddress = "${cfg.listenAddress}:${toString cfg.port}"; @@ -13,17 +68,27 @@ let SqlSettings.DriverName = "postgres"; SqlSettings.DataSource = "postgres:///${cfg.database.name}?host=/run/postgresql"; FileSettings.Directory = cfg.dataDir; - PluginSettings.Directory = "${cfg.dataDir}/plugins/server"; - PluginSettings.ClientDirectory = "${cfg.dataDir}/plugins/client"; + PluginSettings.Directory = "${pluginDir}/server"; + PluginSettings.ClientDirectory = "${pluginDir}/client"; LogSettings.FileLocation = cfg.logDir; } cfg.settings; + mattermostConf = recursiveUpdate + mattermostConfWithoutPlugins + ( + if mattermostPlugins == null then {} + else { + PluginSettings = { + Enable = true; + }; + } + ); + mattermostConfJSON = (pkgs.formats.json { }).generate "mattermost-config.json" mattermostConf; in { imports = [ - (mkRemovedOptionModule [ "services" "mattermost" "plugins" ] "Plugin support has been removed as its fundamentally to get it working without patchelfing and patching Mattermost's Go code.") (mkRemovedOptionModule [ "services" "mattermost" "matterircd" "enable" ] "This option has been removed as it shouldn't be part of the Mattermost module.") (mkRemovedOptionModule [ "services" "mattermost" "matterircd" "package" ] "This option has been removed as it shouldn't be part of the Mattermost module.") (mkRemovedOptionModule [ "services" "mattermost" "matterircd" "parameters" ] "This option has been removed as it shouldn't be part of the Mattermost module.") @@ -92,6 +157,17 @@ in }; }; + plugins = mkOption { + type = types.listOf (types.oneOf [types.path types.package]); + default = []; + example = "[ ./com.github.moussetc.mattermost.plugin.giphy-2.0.0.tar.gz ]"; + description = lib.mdDoc '' + Plugins to add to the configuration. Overrides any installed if non-null. + This is a list of paths to .tar.gz files or derivations evaluating to + .tar.gz files. + ''; + }; + siteUrl = mkOption { type = types.str; example = "https://chat.example.com"; @@ -116,7 +192,7 @@ in but won't be overwritten on changes or rebuilds. If this option is disabled, changes in the system console won't - be possible (default). If an config.json is present, it will be + be possible (default). If a config.json is present, it will be overwritten! ''; }; @@ -161,13 +237,33 @@ in "d ${cfg.dataDir} 0750 ${cfg.user} ${cfg.group} - -" "d ${cfg.logDir} 0750 ${cfg.user} ${cfg.group} - -" "d ${cfg.configDir} 0750 ${cfg.user} ${cfg.group} - -" + "d ${mutableDataDir} 0750 ${cfg.user} ${cfg.group} - -" + + # Remove and recreate tempDir. + "R ${tempDir} - - - - -" + "d ${tempDir} 0750 ${cfg.user} ${cfg.group} - -" + + # Ensure that pluginDir is a directory, as it could be a symlink on prior versions. + "r ${pluginDir} - - - - -" + "d ${pluginDir} 0750 ${cfg.user} ${cfg.group} - -" "d ${mattermostConf.PluginSettings.Directory} 0750 ${cfg.user} ${cfg.group} - -" "d ${mattermostConf.PluginSettings.ClientDirectory} 0750 ${cfg.user} ${cfg.group} - -" + "L+ ${cfg.dataDir}/fonts - - - - ${cfg.package}/fonts" "L+ ${cfg.dataDir}/i18n - - - - ${cfg.package}/i18n" "L+ ${cfg.dataDir}/templates - - - - ${cfg.package}/templates" "L+ ${cfg.dataDir}/client - - - - ${cfg.package}/client" - ]; + ] ++ ( + if mattermostPlugins == null then + # Create the plugin tarball directory if it's a symlink. + [ + "r ${cfg.dataDir}/plugins - - - - -" + "d ${cfg.dataDir}/plugins 0750 ${cfg.user} ${cfg.group} - -" + ] + else + # Symlink the plugin tarball directory. + ["L+ ${cfg.dataDir}/plugins - - - - ${mattermostPlugins}"] + ); systemd.services.mattermost = { description = "Mattermost chat service"; @@ -175,6 +271,9 @@ in after = [ "network.target" "postgresql.service" ]; requires = [ "network.target" "postgresql.service" ]; + # Use tempDir as this can get rather large, especially if Mattermost unpacks a large number of plugins. + environment.TMPDIR = tempDir; + preStart = lib.optionalString (!cfg.mutableConfig) '' ${pkgs.jq}/bin/jq -s '.[0] * .[1]' ${cfg.package}/config/config.json ${mattermostConfJSON} > "${cfg.configDir}/config.json" '' + lib.optionalString cfg.mutableConfig '' diff --git a/nixos/tests/mattermost.nix b/nixos/tests/mattermost.nix index b0feeb8e592a4..7e9e76d4a1bb9 100644 --- a/nixos/tests/mattermost.nix +++ b/nixos/tests/mattermost.nix @@ -38,6 +38,13 @@ import ./make-test-python.nix ({ pkgs, lib, ... }: mostlyMutable = makeMattermost { mutableConfig = true; preferNixConfig = true; + settings.PluginSettings.AutomaticPrepackagedPlugins = false; + plugins = [ + (pkgs.fetchurl { + url = "https://github.com/matterpoll/matterpoll/releases/download/v1.5.0/com.github.matterpoll.matterpoll-1.5.0.tar.gz"; + sha256 = "1nvgxfza2pfc9ggrr6l3m3hadfy93iid05h3spbhvfh9s1vnmx00"; + }) + ]; }; immutable = makeMattermost { mutableConfig = false; @@ -47,30 +54,44 @@ import ./make-test-python.nix ({ pkgs, lib, ... }: testScript = let - expectConfig = jqExpression: pkgs.writeShellScript "expect-config" '' + expectMattermostUp = pkgs.writeShellScript "expect-mattermost-up" '' set -euo pipefail - echo "Expecting config to match: "${lib.escapeShellArg jqExpression} >&2 curl ${lib.escapeShellArg url} >/dev/null + ''; + + expectConfig = jqExpression: pkgs.writeShellScript "expect-config" '' + set -euo pipefail config="$(curl ${lib.escapeShellArg "${url}/api/v4/config/client?format=old"})" - echo "Config: $(echo "$config" | ${pkgs.jq}/bin/jq)" >&2 - [[ "$(echo "$config" | ${pkgs.jq}/bin/jq -r ${lib.escapeShellArg ".SiteName == $siteName and .Version == ($mattermostName / $sep)[-1] and (${jqExpression})"} --arg siteName ${lib.escapeShellArg siteName} --arg mattermostName ${lib.escapeShellArg pkgs.mattermost.name} --arg sep '-')" = "true" ]] + [[ "$(echo "$config" | ${pkgs.jq}/bin/jq -r ${lib.escapeShellArg ".SiteName == $siteName and .Version == ($mattermostName / $sep)[-1] and (${jqExpression})"} --arg siteName ${lib.escapeShellArg siteName} --arg mattermostName ${lib.escapeShellArg pkgs.mattermost.name} --arg sep '-')" == "true" ]] ''; setConfig = jqExpression: pkgs.writeShellScript "set-config" '' set -euo pipefail mattermostConfig=/etc/mattermost/config.json newConfig="$(${pkgs.jq}/bin/jq -r ${lib.escapeShellArg jqExpression} $mattermostConfig)" - sudo -u mattermost echo "$newConfig" > "$mattermostConfig" + truncate -s 0 "$mattermostConfig" + echo "$newConfig" >> "$mattermostConfig" + ''; + + expectPlugins = jqExpressionOrStatusCode: pkgs.writeShellScript "expect-plugins" '' + set -euo pipefail + ${if builtins.isInt jqExpressionOrStatusCode then '' + code="$(curl -s -o /dev/null -w "%{http_code}" ${lib.escapeShellArg "${url}/api/v4/plugins/webapp"})" + [[ "$code" == ${lib.escapeShellArg (toString jqExpressionOrStatusCode)} ]] + '' else '' + plugins="$(curl ${lib.escapeShellArg "${url}/api/v4/plugins/webapp"})" + [[ "$(echo "$plugins" | ${pkgs.jq}/bin/jq -r ${lib.escapeShellArg "(${jqExpressionOrStatusCode})"})" == "true" ]] + ''} ''; in '' - start_all() - ## Mutable node tests ## + mutable.start() mutable.wait_for_unit("mattermost.service") mutable.wait_for_open_port(8065) # Get the initial config + mutable.succeed("${expectMattermostUp}") mutable.succeed("${expectConfig ''.AboutLink == "https://nixos.org" and .HelpLink == "https://search.nixos.org"''}") # Edit the config @@ -82,27 +103,43 @@ import ./make-test-python.nix ({ pkgs, lib, ... }: # AboutLink and HelpLink should be changed mutable.succeed("${expectConfig ''.AboutLink == "https://mattermost.com" and .HelpLink == "https://nixos.org/nixos/manual"''}") + # No plugins. + mutable.succeed("${expectPlugins ''length == 0''}") + mutable.shutdown() + ## Mostly mutable node tests ## + mostlyMutable.start() mostlyMutable.wait_for_unit("mattermost.service") mostlyMutable.wait_for_open_port(8065) # Get the initial config + mostlyMutable.succeed("${expectMattermostUp}") mostlyMutable.succeed("${expectConfig ''.AboutLink == "https://nixos.org"''}") + # No plugins. + mostlyMutable.succeed("${expectPlugins ''length == 0''}") + # Edit the config mostlyMutable.succeed("${setConfig ''.SupportSettings.AboutLink = "https://mattermost.com"''}") mostlyMutable.succeed("${setConfig ''.SupportSettings.HelpLink = "https://nixos.org/nixos/manual"''}") + mostlyMutable.succeed("${setConfig ''.PluginSettings.PluginStates."com.github.matterpoll.matterpoll".Enable = true''}") mostlyMutable.systemctl("restart mattermost.service") mostlyMutable.wait_for_open_port(8065) # AboutLink should be overridden by NixOS configuration; HelpLink should be what we set above mostlyMutable.succeed("${expectConfig ''.AboutLink == "https://nixos.org" and .HelpLink == "https://nixos.org/nixos/manual"''}") + # Single plugin that's now enabled. + mostlyMutable.succeed("${expectPlugins ''length == 1''}") + mostlyMutable.shutdown() + ## Immutable node tests ## + immutable.start() immutable.wait_for_unit("mattermost.service") immutable.wait_for_open_port(8065) # Get the initial config + immutable.succeed("${expectMattermostUp}") immutable.succeed("${expectConfig ''.AboutLink == "https://nixos.org" and .HelpLink == "https://search.nixos.org"''}") # Edit the config @@ -113,5 +150,9 @@ import ./make-test-python.nix ({ pkgs, lib, ... }: # Our edits should be ignored on restart immutable.succeed("${expectConfig ''.AboutLink == "https://nixos.org" and .HelpLink == "https://search.nixos.org"''}") + + # No plugins. + immutable.succeed("${expectPlugins ''length == 0''}") + immutable.shutdown() ''; })