nixos/mattermost: modernize, support MySQL and mmctl

Based on #198040. Prioritizes backwards compatibility, including
database and plugin compatibility, while adding more sensible
defaults like database peer authentication.

Expand the scope of tests to include plugins (including building
from source) and testing that a piece of media uploads and downloads
to make sure the storage directory doesn't vanish.
This commit is contained in:
Morgan Jones 2024-10-21 23:05:06 -07:00 committed by Valentin Gagarin
parent 8944a4259c
commit f8eac009ee
5 changed files with 1248 additions and 323 deletions

View File

@ -5,35 +5,147 @@
...
}:
with lib;
let
inherit (lib.strings)
hasInfix
hasSuffix
escapeURL
concatStringsSep
escapeShellArg
escapeShellArgs
versionAtLeast
optionalString
;
inherit (lib.meta) getExe;
inherit (lib.lists) singleton;
inherit (lib.attrsets) mapAttrsToList recursiveUpdate optionalAttrs;
inherit (lib.options) mkOption mkPackageOption mkEnableOption;
inherit (lib.modules)
mkRenamedOptionModule
mkMerge
mkIf
mkDefault
;
inherit (lib.trivial) warnIf throwIf;
inherit (lib) types;
cfg = config.services.mattermost;
database = "postgres://${cfg.localDatabaseUser}:${cfg.localDatabasePassword}@localhost:5432/${cfg.localDatabaseName}?sslmode=disable&connect_timeout=10";
# The directory to store mutable data within dataDir.
mutableDataDir = "${cfg.dataDir}/data";
postgresPackage = config.services.postgresql.package;
# 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";
createDb =
# 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";
# Creates a database URI.
mkDatabaseUri =
{
statePath ? cfg.statePath,
localDatabaseUser ? cfg.localDatabaseUser,
localDatabasePassword ? cfg.localDatabasePassword,
localDatabaseName ? cfg.localDatabaseName,
useSudo ? true,
scheme,
user ? null,
password ? null,
escapeUserAndPassword ? true,
host ? null,
escapeHost ? true,
port ? null,
path ? null,
query ? { },
}:
''
if ! test -e ${escapeShellArg "${statePath}/.db-created"}; then
${lib.optionalString useSudo "${pkgs.sudo}/bin/sudo -u ${escapeShellArg config.services.postgresql.superUser} \\"}
${postgresPackage}/bin/psql postgres -c \
"CREATE ROLE ${localDatabaseUser} WITH LOGIN NOCREATEDB NOCREATEROLE ENCRYPTED PASSWORD '${localDatabasePassword}'"
${lib.optionalString useSudo "${pkgs.sudo}/bin/sudo -u ${escapeShellArg config.services.postgresql.superUser} \\"}
${postgresPackage}/bin/createdb \
--owner ${escapeShellArg localDatabaseUser} ${escapeShellArg localDatabaseName}
touch ${escapeShellArg "${statePath}/.db-created"}
fi
'';
let
nullToEmpty = val: if val == null then "" else toString val;
# Converts a list of URI attrs to a query string.
toQuery = mapAttrsToList (
name: value: if value == null then null else (escapeURL name) + "=" + (escapeURL (toString value))
);
schemePart = if scheme == null then "" else "${escapeURL scheme}://";
userPart =
let
realUser = if escapeUserAndPassword then escapeURL user else user;
realPassword = if escapeUserAndPassword then escapeURL password else password;
in
if user == null && password == null then
""
else if user != null && password != null then
"${realUser}:${realPassword}"
else if user != null then
realUser
else
throw "Either user or username and password must be provided";
hostPart =
let
realHost = if escapeHost then escapeURL (nullToEmpty host) else nullToEmpty host;
in
if userPart == "" then realHost else "@" + realHost;
portPart = if port == null then "" else ":" + (toString port);
pathPart = if path == null then "" else "/" + path;
queryPart = if query == { } then "" else "?" + concatStringsSep "&" (toQuery query);
in
schemePart + userPart + hostPart + portPart + pathPart + queryPart;
database =
let
hostIsPath = hasInfix "/" cfg.database.host;
in
if cfg.database.driver == "postgres" then
if cfg.database.peerAuth then
mkDatabaseUri {
scheme = cfg.database.driver;
inherit (cfg.database) user;
path = escapeURL cfg.database.name;
query = {
host = cfg.database.socketPath;
} // cfg.database.extraConnectionOptions;
}
else
mkDatabaseUri {
scheme = cfg.database.driver;
inherit (cfg.database) user password;
host = if hostIsPath then null else cfg.database.host;
port = if hostIsPath then null else cfg.database.port;
path = escapeURL cfg.database.name;
query =
optionalAttrs hostIsPath { host = cfg.database.host; } // cfg.database.extraConnectionOptions;
}
else if cfg.database.driver == "mysql" then
if cfg.database.peerAuth then
mkDatabaseUri {
scheme = null;
inherit (cfg.database) user;
escapeUserAndPassword = false;
host = "unix(${cfg.database.socketPath})";
escapeHost = false;
path = escapeURL cfg.database.name;
query = cfg.database.extraConnectionOptions;
}
else
mkDatabaseUri {
scheme = null;
inherit (cfg.database) user password;
escapeUserAndPassword = false;
host =
if hostIsPath then
"unix(${cfg.database.host})"
else
"tcp(${cfg.database.host}:${toString cfg.database.port})";
escapeHost = false;
path = escapeURL cfg.database.name;
query = cfg.database.extraConnectionOptions;
}
else
throw "Invalid database driver: ${cfg.database.driver}";
mattermostPluginDerivations =
with pkgs;
@ -60,23 +172,19 @@ let
else
stdenv.mkDerivation {
name = "${cfg.package.name}-plugins";
nativeBuildInputs = [
autoPatchelfHook
] ++ mattermostPluginDerivations;
buildInputs = [
cfg.package
];
nativeBuildInputs = [ autoPatchelfHook ] ++ mattermostPluginDerivations;
buildInputs = [ cfg.package ];
installPhase = ''
mkdir -p $out/data/plugins
mkdir -p $out
plugins=(${
escapeShellArgs (map (plugin: "${plugin}/share/plugin.tar.gz") mattermostPluginDerivations)
})
for plugin in "''${plugins[@]}"; do
hash="$(sha256sum "$plugin" | cut -d' ' -f1)"
hash="$(sha256sum "$plugin" | awk '{print $1}')"
mkdir -p "$hash"
tar -C "$hash" -xzf "$plugin"
autoPatchelf "$hash"
GZIP_OPT=-9 tar -C "$hash" -cvzf "$out/data/plugins/$hash.tar.gz" .
GZIP_OPT=-9 tar -C "$hash" -cvzf "$out/$hash.tar.gz" .
rm -rf "$hash"
done
'';
@ -89,40 +197,157 @@ let
};
mattermostConfWithoutPlugins = recursiveUpdate {
ServiceSettings.SiteURL = cfg.siteUrl;
ServiceSettings.ListenAddress = cfg.listenAddress;
ServiceSettings = {
SiteURL = cfg.siteUrl;
ListenAddress = "${cfg.host}:${toString cfg.port}";
LocalModeSocketLocation = cfg.socket.path;
EnableLocalMode = cfg.socket.enable;
};
TeamSettings.SiteName = cfg.siteName;
SqlSettings.DriverName = "postgres";
SqlSettings.DataSource = database;
PluginSettings.Directory = "${cfg.statePath}/plugins/server";
PluginSettings.ClientDirectory = "${cfg.statePath}/plugins/client";
} cfg.extraConfig;
SqlSettings.DriverName = cfg.database.driver;
SqlSettings.DataSource =
if cfg.database.fromEnvironment then
null
else
warnIf (!cfg.database.peerAuth && cfg.database.password != null) ''
Database password is set in Mattermost config! This password will end up in the Nix store.
You may be able to simply set the following, if the database is on the same host
and peer authentication is enabled:
services.mattermost.database.peerAuth = true;
Note that this is the default if you set system.stateVersion to 25.05 or later
and the database host is localhost.
Alternatively, you can write the following to ${
if cfg.environmentFile == null then "your environment file" else cfg.environmentFile
}:
MM_SQLSETTINGS_DATASOURCE=${database}
Then set the following options:
services.mattermost.environmentFile = "<your environment file>";
services.mattermost.database.fromEnvironment = true;
'' database;
FileSettings.Directory = cfg.dataDir;
PluginSettings.Directory = "${pluginDir}/server";
PluginSettings.ClientDirectory = "${pluginDir}/client";
LogSettings.FileLocation = cfg.logDir;
} cfg.settings;
mattermostConf = recursiveUpdate mattermostConfWithoutPlugins (
lib.optionalAttrs (mattermostPlugins != null) {
PluginSettings = {
Enable = true;
};
}
if mattermostPlugins == null then
{ }
else
{
PluginSettings = {
Enable = true;
};
}
);
mattermostConfJSON = pkgs.writeText "mattermost-config.json" (builtins.toJSON mattermostConf);
in
{
imports = [
(mkRenamedOptionModule
[
"services"
"mattermost"
"listenAddress"
]
[
"services"
"mattermost"
"host"
]
)
(mkRenamedOptionModule
[
"services"
"mattermost"
"localDatabaseCreate"
]
[
"services"
"mattermost"
"database"
"create"
]
)
(mkRenamedOptionModule
[
"services"
"mattermost"
"localDatabasePassword"
]
[
"services"
"mattermost"
"database"
"password"
]
)
(mkRenamedOptionModule
[
"services"
"mattermost"
"localDatabaseUser"
]
[
"services"
"mattermost"
"database"
"user"
]
)
(mkRenamedOptionModule
[
"services"
"mattermost"
"localDatabaseName"
]
[
"services"
"mattermost"
"database"
"name"
]
)
(mkRenamedOptionModule
[
"services"
"mattermost"
"extraConfig"
]
[
"services"
"mattermost"
"settings"
]
)
(mkRenamedOptionModule
[
"services"
"mattermost"
"statePath"
]
[
"services"
"mattermost"
"dataDir"
]
)
];
options = {
services.mattermost = {
enable = mkEnableOption "Mattermost chat server";
package = mkPackageOption pkgs "mattermost" { };
statePath = mkOption {
type = types.str;
default = "/var/lib/mattermost";
description = "Mattermost working directory";
};
siteUrl = mkOption {
type = types.str;
example = "https://chat.example.com";
@ -137,12 +362,77 @@ in
description = "Name of this Mattermost site.";
};
listenAddress = mkOption {
host = mkOption {
type = types.str;
default = ":8065";
example = "[::1]:8065";
default = "127.0.0.1";
example = "0.0.0.0";
description = ''
Address and port this Mattermost instance listens to.
Host or address that this Mattermost instance listens on.
'';
};
port = mkOption {
type = types.port;
default = 8065;
description = ''
Port for Mattermost server to listen on.
'';
};
dataDir = mkOption {
type = types.path;
default = "/var/lib/mattermost";
description = ''
Mattermost working directory.
'';
};
socket = {
enable = mkEnableOption "Mattermost control socket";
path = mkOption {
type = types.path;
default = "${cfg.dataDir}/mattermost.sock";
defaultText = ''''${config.mattermost.dataDir}/mattermost.sock'';
description = ''
Default location for the Mattermost control socket used by `mmctl`.
'';
};
export = mkEnableOption "Export socket control to system environment variables";
};
logDir = mkOption {
type = types.path;
default =
if versionAtLeast config.system.stateVersion "25.05" then
"/var/log/mattermost"
else
"${cfg.dataDir}/logs";
defaultText = ''
if versionAtLeast config.system.stateVersion "25.05" then "/var/log/mattermost"
else "''${config.services.mattermost.dataDir}/logs";
'';
description = ''
Mattermost log directory.
'';
};
configDir = mkOption {
type = types.path;
default =
if versionAtLeast config.system.stateVersion "25.05" then
"/etc/mattermost"
else
"${cfg.dataDir}/config";
defaultText = ''
if versionAtLeast config.system.stateVersion "25.05" then
"/etc/mattermost"
else
"''${config.services.mattermost.dataDir}/config";
'';
description = ''
Mattermost config directory.
'';
};
@ -173,21 +463,8 @@ in
'';
};
extraConfig = mkOption {
type = types.attrs;
default = { };
description = ''
Additional configuration options as Nix attribute set in config.json schema.
'';
};
plugins = mkOption {
type = types.listOf (
types.oneOf [
types.path
types.package
]
);
type = with types; listOf (either path package);
default = [ ];
example = "[ ./com.github.moussetc.mattermost.plugin.giphy-2.0.0.tar.gz ]";
description = ''
@ -196,13 +473,25 @@ in
.tar.gz files.
'';
};
environment = mkOption {
type = with types; attrsOf (either int str);
default = { };
description = ''
Extra environment variables to export to the Mattermost process, in the systemd unit.
'';
example = {
MM_SERVICESETTINGS_SITEURL = "http://example.com";
};
};
environmentFile = mkOption {
type = types.nullOr types.path;
type = with types; nullOr path;
default = null;
description = ''
Environment file (see {manpage}`systemd.exec(5)`
"EnvironmentFile=" section for the syntax) which sets config options
for mattermost (see [the mattermost documentation](https://docs.mattermost.com/configure/configuration-settings.html#environment-variables)).
for mattermost (see [the Mattermost documentation](https://docs.mattermost.com/configure/configuration-settings.html#environment-variables)).
Settings defined in the environment file will overwrite settings
set via nix or via the {option}`services.mattermost.extraConfig`
@ -213,36 +502,142 @@ in
'';
};
localDatabaseCreate = mkOption {
type = types.bool;
default = true;
description = ''
Create a local PostgreSQL database for Mattermost automatically.
'';
};
database = {
driver = mkOption {
type = types.enum [
"postgres"
"mysql"
];
default = "postgres";
description = ''
The database driver to use (Postgres or MySQL).
'';
};
localDatabaseName = mkOption {
type = types.str;
default = "mattermost";
description = ''
Local Mattermost database name.
'';
};
create = mkOption {
type = types.bool;
default = true;
description = ''
Create a local PostgreSQL or MySQL database for Mattermost automatically.
'';
};
localDatabaseUser = mkOption {
type = types.str;
default = "mattermost";
description = ''
Local Mattermost database username.
'';
};
peerAuth = mkOption {
type = types.bool;
default = versionAtLeast config.system.stateVersion "25.05" && cfg.database.host == "localhost";
defaultText = ''
versionAtLeast config.system.stateVersion "25.05" && config.services.mattermost.database.host == "localhost"
'';
description = ''
If set, will use peer auth instead of connecting to a Postgres server.
Use services.mattermost.database.socketPath to configure the socket path.
'';
};
localDatabasePassword = mkOption {
type = types.str;
default = "mmpgsecret";
description = ''
Password for local Mattermost database user.
'';
socketPath = mkOption {
type = types.path;
default =
if cfg.database.driver == "postgres" then "/run/postgresql" else "/run/mysqld/mysqld.sock";
defaultText = ''
if config.services.mattermost.database.driver == "postgres" then "/run/postgresql" else "/run/mysqld/mysqld.sock";
'';
description = ''
The database (Postgres or MySQL) socket path.
'';
};
fromEnvironment = mkOption {
type = types.bool;
default = false;
description = ''
Use services.mattermost.environmentFile to configure the database instead of writing the database URI
to the Nix store. Useful if you use password authentication with peerAuth set to false.
'';
};
name = mkOption {
type = types.str;
default = "mattermost";
description = ''
Local Mattermost database name.
'';
};
host = mkOption {
type = types.str;
default = "localhost";
example = "127.0.0.1";
description = ''
Host to use for the database. Can also be set to a path if you'd like to connect
to a socket using a username and password.
'';
};
port = mkOption {
type = types.port;
default = if cfg.database.driver == "postgres" then 5432 else 3306;
defaultText = ''
if config.services.mattermost.database.type == "postgres" then 5432 else 3306
'';
example = 3306;
description = ''
Port to use for the database.
'';
};
user = mkOption {
type = types.str;
default = "mattermost";
description = ''
Local Mattermost database username.
'';
};
password = mkOption {
type = types.str;
default = "mmpgsecret";
description = ''
Password for local Mattermost database user. If set and peerAuth is not true,
will cause a warning nagging you to use environmentFile instead since it will
end up in the Nix store.
'';
};
extraConnectionOptions = mkOption {
type = with types; attrsOf (either int str);
default =
if cfg.database.driver == "postgres" then
{
sslmode = "disable";
connect_timeout = 30;
}
else if cfg.database.driver == "mysql" then
{
charset = "utf8mb4,utf8";
writeTimeout = "30s";
readTimeout = "30s";
}
else
throw "Invalid database driver ${cfg.database.driver}";
defaultText = ''
if config.mattermost.database.driver == "postgres" then
{
sslmode = "disable";
connect_timeout = 30;
}
else if config.mattermost.database.driver == "mysql" then
{
charset = "utf8mb4,utf8";
writeTimeout = "30s";
readTimeout = "30s";
}
else
throw "Invalid database driver";
'';
description = ''
Extra options that are placed in the connection URI's query parameters.
'';
};
};
user = mkOption {
@ -261,6 +656,14 @@ in
'';
};
settings = mkOption {
type = types.attrs;
default = { };
description = ''
Additional configuration options as Nix attribute set in config.json schema.
'';
};
matterircd = {
enable = mkEnableOption "Mattermost IRC bridge";
package = mkPackageOption pkgs "matterircd" { };
@ -282,83 +685,235 @@ in
config = mkMerge [
(mkIf cfg.enable {
users.users = optionalAttrs (cfg.user == "mattermost") {
mattermost = {
users.users = {
${cfg.user} = {
group = cfg.group;
uid = config.ids.uids.mattermost;
home = cfg.statePath;
uid = mkIf (cfg.user == "mattermost") config.ids.uids.mattermost;
home = cfg.dataDir;
isSystemUser = true;
packages = [ cfg.package ];
};
};
users.groups = optionalAttrs (cfg.group == "mattermost") {
mattermost.gid = config.ids.gids.mattermost;
users.groups = {
${cfg.group} = {
gid = mkIf (cfg.group == "mattermost") config.ids.gids.mattermost;
};
};
services.postgresql.enable = cfg.localDatabaseCreate;
services.postgresql = mkIf (cfg.database.driver == "postgres" && cfg.database.create) {
enable = true;
ensureDatabases = singleton cfg.database.name;
ensureUsers = singleton {
name =
throwIf
(cfg.database.peerAuth && (cfg.database.user != cfg.user || cfg.database.name != cfg.database.user))
''
Mattermost database peer auth is enabled and the user, database user, or database name mismatch.
Peer authentication will not work.
''
cfg.database.user;
ensureDBOwnership = true;
};
};
# The systemd service will fail to execute the preStart hook
# if the WorkingDirectory does not exist
systemd.tmpfiles.settings."10-mattermost".${cfg.statePath}.d = { };
services.mysql = mkIf (cfg.database.driver == "mysql" && cfg.database.create) {
enable = true;
package = mkDefault pkgs.mariadb;
ensureDatabases = singleton cfg.database.name;
ensureUsers = singleton {
name = cfg.database.user;
ensurePermissions = {
"${cfg.database.name}.*" = "ALL PRIVILEGES";
};
};
settings = rec {
mysqld = {
collation-server = mkDefault "utf8mb4_general_ci";
init-connect = mkDefault "SET NAMES utf8mb4";
character-set-server = mkDefault "utf8mb4";
};
mysqld_safe = mysqld;
};
};
systemd.services.mattermost = {
environment = {
variables = mkIf cfg.socket.export {
MMCTL_LOCAL = "true";
MMCTL_LOCAL_SOCKET_PATH = cfg.socket.path;
};
};
systemd.tmpfiles.rules =
[
"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} - -"
# Make sure tempDir exists and is not a symlink.
"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} - -"
# Ensure that the plugin directories exist.
"d= ${mattermostConf.PluginSettings.Directory} 0750 ${cfg.user} ${cfg.group} - -"
"d= ${mattermostConf.PluginSettings.ClientDirectory} 0750 ${cfg.user} ${cfg.group} - -"
# Link in some of the immutable data directories.
"L+ ${cfg.dataDir}/bin - - - - ${cfg.package}/bin"
"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, removing anything existing.
[ "L+ ${cfg.dataDir}/plugins - - - - ${mattermostPlugins}" ]
);
systemd.services.mattermost = rec {
description = "Mattermost chat service";
wantedBy = [ "multi-user.target" ];
after = [
"network.target"
"postgresql.service"
after = mkMerge [
[ "network.target" ]
(mkIf (cfg.database.driver == "postgres" && cfg.database.create) [ "postgresql.service" ])
(mkIf (cfg.database.driver == "mysql" && cfg.database.create) [ "mysql.service" ])
];
requires = after;
environment = mkMerge [
{
# Use tempDir as this can get rather large, especially if Mattermost unpacks a large number of plugins.
TMPDIR = tempDir;
}
cfg.environment
];
preStart =
''
mkdir -p "${cfg.statePath}"/{data,config,logs,plugins}
mkdir -p "${cfg.statePath}/plugins"/{client,server}
ln -sf ${cfg.package}/{bin,fonts,i18n,templates,client} "${cfg.statePath}"
dataDir=${escapeShellArg cfg.dataDir}
configDir=${escapeShellArg cfg.configDir}
logDir=${escapeShellArg cfg.logDir}
package=${escapeShellArg cfg.package}
nixConfig=${escapeShellArg mattermostConfJSON}
''
+ lib.optionalString (mattermostPlugins != null) ''
rm -rf "${cfg.statePath}/data/plugins"
ln -sf ${mattermostPlugins}/data/plugins "${cfg.statePath}/data"
''
+ lib.optionalString (!cfg.mutableConfig) ''
rm -f "${cfg.statePath}/config/config.json"
${pkgs.jq}/bin/jq -s '.[0] * .[1]' ${cfg.package}/config/config.json ${mattermostConfJSON} > "${cfg.statePath}/config/config.json"
''
+ lib.optionalString cfg.mutableConfig ''
if ! test -e "${cfg.statePath}/config/.initial-created"; then
rm -f ${cfg.statePath}/config/config.json
${pkgs.jq}/bin/jq -s '.[0] * .[1]' ${cfg.package}/config/config.json ${mattermostConfJSON} > "${cfg.statePath}/config/config.json"
touch "${cfg.statePath}/config/.initial-created"
+ optionalString (versionAtLeast config.system.stateVersion "25.05") ''
# Migrate configs in the pre-25.05 directory structure.
oldConfig="$dataDir/config/config.json"
newConfig="$configDir/config.json"
if [ "$oldConfig" != "$newConfig" ] && [ -f "$oldConfig" ] && [ ! -f "$newConfig" ]; then
# Migrate the legacy config location to the new config location
echo "Moving legacy config at $oldConfig to $newConfig" >&2
mkdir -p "$configDir"
mv "$oldConfig" "$newConfig"
touch "$configDir/.initial-created"
fi
# Logs too.
oldLogs="$dataDir/logs"
newLogs="$logDir"
if [ "$oldLogs" != "$newLogs" ] && [ -d "$oldLogs" ]; then
# Migrate the legacy log location to the new log location.
# Allow this to fail if there aren't any logs to move.
echo "Moving legacy logs at $oldLogs to $newLogs" >&2
mkdir -p "$newLogs"
mv "$oldLogs"/* "$newLogs" || true
fi
''
+ lib.optionalString (cfg.mutableConfig && cfg.preferNixConfig) ''
new_config="$(${pkgs.jq}/bin/jq -s '.[0] * .[1]' "${cfg.statePath}/config/config.json" ${mattermostConfJSON})"
rm -f "${cfg.statePath}/config/config.json"
echo "$new_config" > "${cfg.statePath}/config/config.json"
+ optionalString (!cfg.mutableConfig) ''
${getExe pkgs.jq} -s '.[0] * .[1]' "$package/config/config.json" "$nixConfig" > "$configDir/config.json"
''
+ lib.optionalString cfg.localDatabaseCreate (createDb { })
+ ''
# Don't change permissions recursively on the data, current, and symlinked directories (see ln -sf command above).
# This dramatically decreases startup times for installations with a lot of files.
find . -maxdepth 1 -not -name data -not -name client -not -name templates -not -name i18n -not -name fonts -not -name bin -not -name . \
-exec chown "${cfg.user}:${cfg.group}" -R {} \; -exec chmod u+rw,g+r,o-rwx -R {} \;
chown "${cfg.user}:${cfg.group}" "${cfg.statePath}/data" .
chmod u+rw,g+r,o-rwx "${cfg.statePath}/data" .
+ optionalString cfg.mutableConfig ''
if [ ! -e "$configDir/.initial-created" ]; then
${getExe pkgs.jq} -s '.[0] * .[1]' "$package/config/config.json" "$nixConfig" > "$configDir/config.json"
touch "$configDir/.initial-created"
fi
''
+ optionalString (cfg.mutableConfig && cfg.preferNixConfig) ''
echo "$(${getExe pkgs.jq} -s '.[0] * .[1]' "$configDir/config.json" "$nixConfig")" > "$configDir/config.json"
'';
serviceConfig = {
PermissionsStartOnly = true;
User = cfg.user;
Group = cfg.group;
ExecStart = "${cfg.package}/bin/mattermost";
WorkingDirectory = "${cfg.statePath}";
Restart = "always";
RestartSec = "10";
LimitNOFILE = "49152";
EnvironmentFile = cfg.environmentFile;
};
unitConfig.JoinsNamespaceOf = mkIf cfg.localDatabaseCreate "postgresql.service";
serviceConfig = mkMerge [
{
User = cfg.user;
Group = cfg.group;
ExecStart = "${getExe cfg.package} --config ${cfg.configDir}/config.json";
ReadWritePaths = [
cfg.dataDir
cfg.logDir
cfg.configDir
];
UMask = "0027";
Restart = "always";
RestartSec = 10;
LimitNOFILE = 49152;
LockPersonality = true;
NoNewPrivileges = true;
PrivateDevices = true;
PrivateTmp = true;
PrivateUsers = true;
ProtectClock = true;
ProtectControlGroups = true;
ProtectHome = true;
ProtectHostname = true;
ProtectKernelLogs = true;
ProtectKernelModules = true;
ProtectKernelTunables = true;
ProtectProc = "invisible";
ProtectSystem = "strict";
RestrictNamespaces = true;
RestrictSUIDSGID = true;
EnvironmentFile = cfg.environmentFile;
WorkingDirectory = cfg.dataDir;
}
(mkIf (cfg.dataDir == "/var/lib/mattermost") {
StateDirectory = baseNameOf cfg.dataDir;
StateDirectoryMode = "0750";
})
(mkIf (cfg.logDir == "/var/log/mattermost") {
LogsDirectory = baseNameOf cfg.logDir;
LogsDirectoryMode = "0750";
})
(mkIf (cfg.configDir == "/etc/mattermost") {
ConfigurationDirectory = baseNameOf cfg.configDir;
ConfigurationDirectoryMode = "0750";
})
];
unitConfig.JoinsNamespaceOf = mkMerge [
(mkIf (cfg.database.driver == "postgres" && cfg.database.create) [ "postgresql.service" ])
(mkIf (cfg.database.driver == "mysql" && cfg.database.create) [ "mysql.service" ])
];
};
assertions = [
{
# Make sure the URL doesn't have a trailing slash
assertion = !(hasSuffix "/" cfg.siteUrl);
message = ''
services.mattermost.siteUrl should not have a trailing "/".
'';
}
{
# Make sure this isn't a host/port pair
assertion = !(hasInfix ":" cfg.host && !(hasInfix "[" cfg.host) && !(hasInfix "]" cfg.host));
message = ''
services.mattermost.host should not include a port. Use services.mattermost.host for the address
or hostname, and services.mattermost.port to specify the port separately.
'';
}
];
})
(mkIf cfg.matterircd.enable {
systemd.services.matterircd = {
@ -367,7 +922,7 @@ in
serviceConfig = {
User = "nobody";
Group = "nogroup";
ExecStart = "${cfg.matterircd.package}/bin/matterircd ${escapeShellArgs cfg.matterircd.parameters}";
ExecStart = "${getExe cfg.matterircd.package} ${escapeShellArgs cfg.matterircd.parameters}";
WorkingDirectory = "/tmp";
PrivateTmp = true;
Restart = "always";
@ -376,4 +931,6 @@ in
};
})
];
meta.maintainers = with lib.maintainers; [ numinit ];
}

View File

@ -593,7 +593,7 @@ in {
matrix-synapse-workers = handleTest ./matrix/synapse-workers.nix {};
mautrix-meta-postgres = handleTest ./matrix/mautrix-meta-postgres.nix {};
mautrix-meta-sqlite = handleTest ./matrix/mautrix-meta-sqlite.nix {};
mattermost = handleTest ./mattermost.nix {};
mattermost = handleTest ./mattermost {};
mealie = handleTest ./mealie.nix {};
mediamtx = handleTest ./mediamtx.nix {};
mediatomb = handleTest ./mediatomb.nix {};

View File

@ -1,169 +0,0 @@
import ./make-test-python.nix (
{ pkgs, lib, ... }:
let
host = "smoke.test";
port = "8065";
url = "http://${host}:${port}";
siteName = "NixOS Smoke Tests, Inc.";
makeMattermost =
mattermostConfig:
{ config, ... }:
{
environment.systemPackages = [
pkgs.mattermost
pkgs.curl
pkgs.jq
];
networking.hosts = {
"127.0.0.1" = [ host ];
};
services.mattermost = lib.recursiveUpdate {
enable = true;
inherit siteName;
listenAddress = "0.0.0.0:${port}";
siteUrl = url;
extraConfig = {
SupportSettings.AboutLink = "https://nixos.org";
};
} mattermostConfig;
};
in
{
name = "mattermost";
nodes = {
mutable = makeMattermost {
mutableConfig = true;
extraConfig.SupportSettings.HelpLink = "https://search.nixos.org";
};
mostlyMutable = makeMattermost {
mutableConfig = true;
preferNixConfig = true;
plugins =
let
mattermostDemoPlugin = pkgs.fetchurl {
url = "https://github.com/mattermost/mattermost-plugin-demo/releases/download/v0.9.0/com.mattermost.demo-plugin-0.9.0.tar.gz";
sha256 = "1h4qi34gcxcx63z8wiqcf2aaywmvv8lys5g8gvsk13kkqhlmag25";
};
in
[
mattermostDemoPlugin
];
};
immutable = makeMattermost {
package = pkgs.mattermost.overrideAttrs (prev: {
webapp = prev.webapp.overrideAttrs (prevWebapp: {
# Ensure that users can add patches.
postPatch =
prevWebapp.postPatch or ""
+ ''
substituteInPlace channels/src/root.html --replace-fail "Mattermost" "Patched Mattermost"
'';
});
});
mutableConfig = false;
extraConfig.SupportSettings.HelpLink = "https://search.nixos.org";
};
environmentFile = makeMattermost {
mutableConfig = false;
extraConfig.SupportSettings.AboutLink = "https://example.org";
environmentFile = pkgs.writeText "mattermost-env" ''
MM_SUPPORTSETTINGS_ABOUTLINK=https://nixos.org
'';
};
};
testScript =
let
expectConfig =
jqExpression:
pkgs.writeShellScript "expect-config" ''
set -euo pipefail
echo "Expecting config to match: "${lib.escapeShellArg jqExpression} >&2
curl ${lib.escapeShellArg url} >/dev/null
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" ]]
'';
setConfig =
jqExpression:
pkgs.writeShellScript "set-config" ''
set -euo pipefail
mattermostConfig=/var/lib/mattermost/config/config.json
newConfig="$(${pkgs.jq}/bin/jq -r ${lib.escapeShellArg jqExpression} $mattermostConfig)"
rm -f $mattermostConfig
echo "$newConfig" > "$mattermostConfig"
'';
in
''
start_all()
## Mutable node tests ##
mutable.wait_for_unit("mattermost.service")
mutable.wait_for_open_port(8065)
mutable.succeed("curl -L http://localhost:8065/index.html | grep '${siteName}'")
mutable.succeed("curl -L http://localhost:8065/index.html | grep 'Mattermost'")
# Get the initial config
mutable.succeed("${expectConfig ''.AboutLink == "https://nixos.org" and .HelpLink == "https://search.nixos.org"''}")
# Edit the config
mutable.succeed("${setConfig ''.SupportSettings.AboutLink = "https://mattermost.com"''}")
mutable.succeed("${setConfig ''.SupportSettings.HelpLink = "https://nixos.org/nixos/manual"''}")
mutable.systemctl("restart mattermost.service")
mutable.wait_for_open_port(8065)
# AboutLink and HelpLink should be changed
mutable.succeed("${expectConfig ''.AboutLink == "https://mattermost.com" and .HelpLink == "https://nixos.org/nixos/manual"''}")
## Mostly mutable node tests ##
mostlyMutable.wait_for_unit("mattermost.service")
mostlyMutable.wait_for_open_port(8065)
mostlyMutable.succeed("curl -L http://localhost:8065/index.html | grep '${siteName}'")
mostlyMutable.succeed("curl -L http://localhost:8065/index.html | grep 'Mattermost'")
# Get the initial config
mostlyMutable.succeed("${expectConfig ''.AboutLink == "https://nixos.org"''}")
# Edit the config
mostlyMutable.succeed("${setConfig ''.SupportSettings.AboutLink = "https://mattermost.com"''}")
mostlyMutable.succeed("${setConfig ''.SupportSettings.HelpLink = "https://nixos.org/nixos/manual"''}")
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"''}")
## Immutable node tests ##
immutable.wait_for_unit("mattermost.service")
immutable.wait_for_open_port(8065)
# Since we patched it, it doesn't replace the site name at runtime anymore
immutable.succeed("curl -L http://localhost:8065/index.html | grep 'Patched Mattermost'")
# Get the initial config
immutable.succeed("${expectConfig ''.AboutLink == "https://nixos.org" and .HelpLink == "https://search.nixos.org"''}")
# Edit the config
immutable.succeed("${setConfig ''.SupportSettings.AboutLink = "https://mattermost.com"''}")
immutable.succeed("${setConfig ''.SupportSettings.HelpLink = "https://nixos.org/nixos/manual"''}")
immutable.systemctl("restart mattermost.service")
immutable.wait_for_open_port(8065)
# Our edits should be ignored on restart
immutable.succeed("${expectConfig ''.AboutLink == "https://nixos.org" and .HelpLink == "https://search.nixos.org"''}")
## Environment File node tests ##
environmentFile.wait_for_unit("mattermost.service")
environmentFile.wait_for_open_port(8065)
environmentFile.succeed("curl -L http://localhost:8065/index.html | grep '${siteName}'")
environmentFile.succeed("curl -L http://localhost:8065/index.html | grep 'Mattermost'")
# Settings in the environment file should override settings set otherwise
environmentFile.succeed("${expectConfig ''.AboutLink == "https://nixos.org"''}")
'';
}
)

View File

@ -0,0 +1,537 @@
import ../make-test-python.nix (
{ pkgs, lib, ... }:
let
host = "smoke.test";
port = 8065;
url = "http://${host}:${toString port}";
siteName = "NixOS Smoke Tests, Inc.";
makeMattermost =
mattermostConfig: extraConfig:
lib.mkMerge [
(
{ config, ... }:
{
environment = {
systemPackages = [
pkgs.mattermost
pkgs.curl
pkgs.jq
];
};
networking.hosts = {
"127.0.0.1" = [ host ];
};
# Assume that Postgres won't update across stateVersion.
services.postgresql = {
package = lib.mkForce pkgs.postgresql;
initialScript = lib.mkIf (!config.services.mattermost.database.peerAuth) (
pkgs.writeText "init.sql" ''
create role ${config.services.mattermost.database.user} with login nocreatedb nocreaterole encrypted password '${config.services.mattermost.database.password}';
''
);
};
system.stateVersion = lib.mkDefault "25.05";
services.mattermost = lib.recursiveUpdate {
enable = true;
inherit siteName;
host = "0.0.0.0";
inherit port;
siteUrl = url;
socket = {
enable = true;
export = true;
};
database = {
peerAuth = lib.mkDefault true;
};
settings = {
SupportSettings.AboutLink = "https://nixos.org";
PluginSettings.AutomaticPrepackagedPlugins = false;
AnnouncementSettings = {
# Disable this since it doesn't work in the sandbox and causes a timeout.
AdminNoticesEnabled = false;
UserNoticesEnabled = false;
};
};
} mattermostConfig;
# Upgrade to the latest Mattermost.
specialisation.latest.configuration = {
services.mattermost.package = lib.mkForce pkgs.mattermostLatest;
system.stateVersion = lib.mkVMOverride "25.05";
};
}
)
extraConfig
];
makeMysql =
mattermostConfig: extraConfig:
lib.mkMerge [
mattermostConfig
(
{ pkgs, config, ... }:
{
services.mattermost.database = {
driver = lib.mkForce "mysql";
peerAuth = lib.mkForce true;
};
}
)
extraConfig
];
in
{
name = "mattermost";
nodes = rec {
postgresMutable =
makeMattermost
{
mutableConfig = true;
settings.SupportSettings.HelpLink = "https://search.nixos.org";
}
{
# Last version to support the "old" config layout.
system.stateVersion = lib.mkForce "24.11";
# First version to support the "new" config layout.
specialisation.upgrade.configuration.system.stateVersion = lib.mkVMOverride "25.05";
};
postgresMostlyMutable = makeMattermost {
mutableConfig = true;
preferNixConfig = true;
plugins = with pkgs; [
# Build the demo plugin.
(mattermost.buildPlugin {
pname = "mattermost-plugin-starter-template";
version = "0.1.0";
src = fetchFromGitHub {
owner = "mattermost";
repo = "mattermost-plugin-starter-template";
# Newer versions have issues with their dependency lockfile.
rev = "7c98e89ac1a268ce8614bc665571b7bbc9a70df2";
hash = "sha256-uyfxB0GZ45qL9ssWUord0eKQC6S0TlCTtjTOXWtK4H0=";
};
vendorHash = "sha256-Jl4F9YkHNqiFP9/yeyi4vTntqxMk/J1zhEP6QLSvJQA=";
npmDepsHash = "sha256-z08nc4XwT+uQjQlZiUydJyh8mqeJoYdPFWuZpw9k99s=";
})
# Build the todos plugin.
(mattermost.buildPlugin {
pname = "mattermost-plugin-todo";
version = "0.8-pre";
src = fetchFromGitHub {
owner = "mattermost-community";
repo = "mattermost-plugin-todo";
# 0.7.1 didn't work, seems to use an older set of node dependencies.
rev = "f25dc91ea401c9f0dcd4abcebaff10eb8b9836e5";
hash = "sha256-OM+m4rTqVtolvL5tUE8RKfclqzoe0Y38jLU60Pz7+HI=";
};
vendorHash = "sha256-5KpechSp3z/Nq713PXYruyNxveo6CwrCSKf2JaErbgg=";
npmDepsHash = "sha256-o2UOEkwb8Vx2lDWayNYgng0GXvmS6lp/ExfOq3peyMY=";
extraGoModuleAttrs = {
npmFlags = [ "--legacy-peer-deps" ];
};
})
];
} { };
postgresImmutable = makeMattermost {
package = pkgs.mattermost.overrideAttrs (prev: {
webapp = prev.webapp.overrideAttrs (prevWebapp: {
# Ensure that users can add patches.
postPatch =
prevWebapp.postPatch or ""
+ ''
substituteInPlace channels/src/root.html --replace-fail "Mattermost" "Patched Mattermost"
'';
});
});
mutableConfig = false;
# Make sure something other than the default works.
user = "mmuser";
group = "mmgroup";
database = {
# Ensure that this gets tested on Postgres.
peerAuth = false;
};
settings.SupportSettings.HelpLink = "https://search.nixos.org";
} { };
postgresEnvironmentFile = makeMattermost {
mutableConfig = false;
database.fromEnvironment = true;
settings.SupportSettings.AboutLink = "https://example.org";
environmentFile = pkgs.writeText "mattermost-env" ''
MM_SQLSETTINGS_DATASOURCE=postgres:///mattermost?host=/run/postgresql
MM_SUPPORTSETTINGS_ABOUTLINK=https://nixos.org
'';
} { };
mysqlMutable = makeMysql postgresMutable { };
mysqlMostlyMutable = makeMysql postgresMostlyMutable { };
mysqlImmutable = makeMysql postgresImmutable {
# Let's try to use this on MySQL.
services.mattermost.database = {
peerAuth = lib.mkForce true;
user = lib.mkForce "mmuser";
name = lib.mkForce "mmuser";
};
};
mysqlEnvironmentFile = makeMysql postgresEnvironmentFile {
services.mattermost.environmentFile = lib.mkForce (
pkgs.writeText "mattermost-env" ''
MM_SQLSETTINGS_DATASOURCE=mattermost@unix(/run/mysqld/mysqld.sock)/mattermost?charset=utf8mb4,utf8&writeTimeout=30s
MM_SUPPORTSETTINGS_ABOUTLINK=https://nixos.org
''
);
};
};
testScript =
{ nodes, ... }:
let
expectConfig = 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 == $mattermostVersion and "}"($1)" --arg siteName ${lib.escapeShellArg siteName} --arg mattermostVersion "$2" --arg sep '-')" = "true" ]]
'';
setConfig = pkgs.writeShellScript "set-config" ''
set -eo pipefail
mattermostConfig=/etc/mattermost/config.json
nixosVersion="$2"
if [ -z "$nixosVersion" ]; then
nixosVersion="$(nixos-version)"
fi
nixosVersion="$(echo "$nixosVersion" | sed -nr 's/^([0-9]{2})\.([0-9]{2}).*/\1\2/p')"
echo "NixOS version: $nixosVersion" >&2
if [ "$nixosVersion" -lt 2505 ]; then
mattermostConfig=/var/lib/mattermost/config/config.json
fi
newConfig="$(${pkgs.jq}/bin/jq -r "$1" "$mattermostConfig")"
echo "New config @ $mattermostConfig: $(echo "$newConfig" | ${pkgs.jq}/bin/jq)" >&2
truncate -s 0 "$mattermostConfig"
echo "$newConfig" >> "$mattermostConfig"
'';
expectPlugins = pkgs.writeShellScript "expect-plugins" ''
set -euo pipefail
case "$1" in
""|*[!0-9]*)
plugins="$(curl ${lib.escapeShellArg "${url}/api/v4/plugins/webapp"})"
echo "Plugins: $(echo "$plugins" | ${pkgs.jq}/bin/jq)" >&2
[[ "$(echo "$plugins" | ${pkgs.jq}/bin/jq -r "$1")" == "true" ]]
;;
*)
code="$(curl -s -o /dev/null -w "%{http_code}" ${lib.escapeShellArg "${url}/api/v4/plugins/webapp"})"
[[ "$code" == "$1" ]]
;;
esac
'';
ensurePost = pkgs.writeShellScript "ensure-post" ''
set -euo pipefail
url="$1"
failIfNotFound="$2"
# Make sure the user exists
thingExists='(type == "array" and length > 0)'
userExists="($thingExists and ("'.[0].username == "nixos"))'
if mmctl user list --json | jq | tee /dev/stderr | jq -e "$userExists | not"; then
if [ "$failIfNotFound" -ne 0 ]; then
echo "User didn't exist!" >&2
exit 1
else
mmctl user create \
--email tests@nixos.org \
--username nixos --password nixosrules --system-admin --email-verified >&2
# Make sure the user exists.
while mmctl user list --json | jq | tee /dev/stderr | jq -e "$userExists | not"; do
sleep 1
done
fi
fi
# Auth.
mmctl auth login "$url" --name nixos --username nixos --password nixosrules
# Make sure the team exists
teamExists="($thingExists and ("'.[0].display_name == "NixOS Smoke Tests, Inc."))'
if mmctl team list --json | jq | tee /dev/stderr | jq -e "$teamExists | not"; then
if [ "$failIfNotFound" -ne 0 ]; then
echo "Team didn't exist!" >&2
exit 1
else
mmctl team create \
--name nixos \
--display-name "NixOS Smoke Tests, Inc."
# Teams take a second to create.
while mmctl team list --json | jq | tee /dev/stderr | jq -e "$teamExists | not"; do
sleep 1
done
# Add the user.
mmctl team users add nixos tests@nixos.org
fi
fi
authToken="$(cat ~/.config/mmctl/config | jq -r '.nixos.authToken')"
authHeader="Authorization: Bearer $authToken"
acceptHeader="Accept: application/json; charset=UTF-8"
# Make sure the test post exists.
postContents="pls enjoy this NixOS meme I made"
postAttachment=${./test.jpg}
postAttachmentSize="$(stat -c%s $postAttachment)"
postAttachmentHash="$(sha256sum $postAttachment | awk '{print $1}')"
postAttachmentId=""
postPredicate='select(.message == $message and (.file_ids | length) > 0 and (.metadata.files[0].size | tonumber) == ($size | tonumber))'
postExists="($thingExists and ("'(.[] | '"$postPredicate"' | length) > 0))'
if mmctl post list nixos:off-topic --json | jq | tee /dev/stderr | jq --arg message "$postContents" --arg size "$postAttachmentSize" -e "$postExists | not"; then
if [ "$failIfNotFound" -ne 0 ]; then
echo "Post didn't exist!" >&2
exit 1
else
# Can't use mmcli for this seemingly.
channelId="$(mmctl channel list nixos --json | jq | tee /dev/stderr | jq -r '.[] | select(.name == "off-topic") | .id')"
echo "Channel ID: $channelId" >&2
# Upload the file.
echo "Uploading file at $postAttachment (size: $postAttachmentSize)..." >&2
postAttachmentId="$(curl "$url/api/v4/files" -X POST -H "$acceptHeader" -H "$authHeader" \
-F "files=@$postAttachment" -F "channel_id=$channelId" -F "client_ids=test" | jq | tee /dev/stderr | jq -r '.file_infos[0].id')"
# Create the post with it attached.
postJson="$(echo '{}' | jq -c --arg channelId "$channelId" --arg message "$postContents" --arg fileId "$postAttachmentId" \
'{channel_id: $channelId, message: $message, file_ids: [$fileId]}')"
echo "Creating post with contents $postJson..." >&2
curl "$url/api/v4/posts" -X POST -H "$acceptHeader" -H "$authHeader" --json "$postJson" | jq >&2
fi
fi
if mmctl post list nixos:off-topic --json | jq | tee /dev/stderr | jq --arg message "$postContents" --arg size "$postAttachmentSize" -e "$postExists"; then
# Get the attachment ID.
getPostAttachmentId=".[] | $postPredicate | .file_ids[0]"
postAttachmentId="$(mmctl post list nixos:off-topic --json | jq | tee /dev/stderr | \
jq --arg message "$postContents" --arg size "$postAttachmentSize" -r "$getPostAttachmentId")"
echo "Expected post attachment hash: $postAttachmentHash" >&2
actualPostAttachmentHash="$(curl "$url/api/v4/files/$postAttachmentId?download=1" -H "$authHeader" | sha256sum | awk '{print $1}')"
echo "Actual post attachment hash: $postAttachmentHash" >&2
if [ "$actualPostAttachmentHash" != "$postAttachmentHash" ]; then
echo "Post attachment hash mismatched!" >&2
exit 1
else
echo "Post attachment hash was OK!" >&2
exit 0
fi
else
echo "Post didn't exist when it should have!" >&2
exit 1
fi
'';
in
''
import shlex
def wait_mattermost_up(node, site_name="${siteName}"):
node.systemctl("start mattermost.service")
node.wait_for_unit("mattermost.service")
node.wait_for_open_port(8065)
node.succeed(f"curl {shlex.quote('${url}')} >/dev/null")
node.succeed(f"curl {shlex.quote('${url}')}/index.html | grep {shlex.quote(site_name)}")
def restart_mattermost(node, site_name="${siteName}"):
node.systemctl("restart mattermost.service")
wait_mattermost_up(node, site_name)
def expect_config(node, mattermost_version, *configs):
for config in configs:
node.succeed(f"${expectConfig} {shlex.quote(config)} {shlex.quote(mattermost_version)}")
def expect_plugins(node, jq_or_code):
node.succeed(f"${expectPlugins} {shlex.quote(str(jq_or_code))}")
def ensure_post(node, fail_if_not_found=False):
node.succeed(f"${ensurePost} {shlex.quote('${url}')} {1 if fail_if_not_found else 0}")
def set_config(node, *configs, nixos_version='25.05'):
for config in configs:
args = [shlex.quote("${setConfig}")]
args.append(shlex.quote(config))
if nixos_version:
args.append(shlex.quote(str(nixos_version)))
node.succeed(' '.join(args))
def run_mattermost_tests(mutableToplevel: str, mutable,
mostlyMutableToplevel: str, mostlyMutable,
immutableToplevel: str, immutable,
environmentFileToplevel: str, environmentFile):
esr, latest = '${pkgs.mattermost.version}', '${pkgs.mattermostLatest.version}'
## Mutable node tests ##
mutable.start()
wait_mattermost_up(mutable)
# Get the initial config
expect_config(mutable, esr, '.AboutLink == "https://nixos.org" and .HelpLink == "https://search.nixos.org"')
# Edit the config and make a post
set_config(
mutable,
'.SupportSettings.AboutLink = "https://mattermost.com"',
'.SupportSettings.HelpLink = "https://nixos.org/nixos/manual"',
nixos_version='24.11' # Default 'mutable' config is an old version
)
ensure_post(mutable)
restart_mattermost(mutable)
# AboutLink and HelpLink should be changed, and the post should exist
expect_config(mutable, esr, '.AboutLink == "https://mattermost.com" and .HelpLink == "https://nixos.org/nixos/manual"')
ensure_post(mutable, fail_if_not_found=True)
# Switch to the newer config
mutable.succeed(f"{mutableToplevel}/specialisation/upgrade/bin/switch-to-configuration switch")
wait_mattermost_up(mutable)
# AboutLink and HelpLink should be changed, still, and the post should still exist
expect_config(mutable, esr, '.AboutLink == "https://mattermost.com" and .HelpLink == "https://nixos.org/nixos/manual"')
ensure_post(mutable, fail_if_not_found=True)
# Switch to the latest Mattermost version
mutable.succeed(f"{mutableToplevel}/specialisation/latest/bin/switch-to-configuration switch")
wait_mattermost_up(mutable)
# AboutLink and HelpLink should be changed, still, and the post should still exist
expect_config(mutable, latest, '.AboutLink == "https://mattermost.com" and .HelpLink == "https://nixos.org/nixos/manual"')
ensure_post(mutable, fail_if_not_found=True)
mutable.shutdown()
## Mostly mutable node tests ##
mostlyMutable.start()
wait_mattermost_up(mostlyMutable)
# Get the initial config
expect_config(mostlyMutable, esr, '.AboutLink == "https://nixos.org"')
# No plugins.
expect_plugins(mostlyMutable, 'length == 0')
# Edit the config and make a post
set_config(
mostlyMutable,
'.SupportSettings.AboutLink = "https://mattermost.com"',
'.SupportSettings.HelpLink = "https://nixos.org/nixos/manual"',
'.PluginSettings.PluginStates."com.mattermost.plugin-todo".Enable = true'
)
ensure_post(mostlyMutable)
restart_mattermost(mostlyMutable)
# AboutLink should be overridden by NixOS configuration; HelpLink should be what we set above
expect_config(mostlyMutable, esr, '.AboutLink == "https://nixos.org" and .HelpLink == "https://nixos.org/nixos/manual"')
# Single plugin that's now enabled.
expect_plugins(mostlyMutable, 'length == 1')
# Post should exist.
ensure_post(mostlyMutable, fail_if_not_found=True)
# Switch to the latest Mattermost version
mostlyMutable.succeed(f"{mostlyMutableToplevel}/specialisation/latest/bin/switch-to-configuration switch")
wait_mattermost_up(mostlyMutable)
# AboutLink should be overridden and the post should still exist
expect_config(mostlyMutable, latest, '.AboutLink == "https://nixos.org" and .HelpLink == "https://nixos.org/nixos/manual"')
ensure_post(mostlyMutable, fail_if_not_found=True)
mostlyMutable.shutdown()
## Immutable node tests ##
immutable.start()
wait_mattermost_up(immutable, "Patched Mattermost")
# Get the initial config
expect_config(immutable, esr, '.AboutLink == "https://nixos.org" and .HelpLink == "https://search.nixos.org"')
# Edit the config and make a post
set_config(
immutable,
'.SupportSettings.AboutLink = "https://mattermost.com"',
'.SupportSettings.HelpLink = "https://nixos.org/nixos/manual"'
)
ensure_post(immutable)
restart_mattermost(immutable, "Patched Mattermost")
# Our edits should be ignored on restart
expect_config(immutable, esr, '.AboutLink == "https://nixos.org" and .HelpLink == "https://search.nixos.org"')
# No plugins.
expect_plugins(immutable, 'length == 0')
# Post should exist.
ensure_post(immutable, fail_if_not_found=True)
# Switch to the latest Mattermost version
immutable.succeed(f"{immutableToplevel}/specialisation/latest/bin/switch-to-configuration switch")
wait_mattermost_up(immutable)
# AboutLink and HelpLink should be changed, still, and the post should still exist
expect_config(immutable, latest, '.AboutLink == "https://nixos.org" and .HelpLink == "https://search.nixos.org"')
ensure_post(immutable, fail_if_not_found=True)
immutable.shutdown()
## Environment File node tests ##
environmentFile.start()
wait_mattermost_up(environmentFile)
ensure_post(environmentFile)
# Settings in the environment file should override settings set otherwise, and the post should exist
expect_config(environmentFile, esr, '.AboutLink == "https://nixos.org"')
ensure_post(environmentFile, fail_if_not_found=True)
# Switch to the latest Mattermost version
environmentFile.succeed(f"{environmentFileToplevel}/specialisation/latest/bin/switch-to-configuration switch")
wait_mattermost_up(environmentFile)
# AboutLink should be changed still, and the post should still exist
expect_config(environmentFile, latest, '.AboutLink == "https://nixos.org"')
ensure_post(environmentFile, fail_if_not_found=True)
environmentFile.shutdown()
run_mattermost_tests(
"${nodes.mysqlMutable.system.build.toplevel}",
mysqlMutable,
"${nodes.mysqlMostlyMutable.system.build.toplevel}",
mysqlMostlyMutable,
"${nodes.mysqlImmutable.system.build.toplevel}",
mysqlImmutable,
"${nodes.mysqlEnvironmentFile.system.build.toplevel}",
mysqlEnvironmentFile
)
run_mattermost_tests(
"${nodes.postgresMutable.system.build.toplevel}",
postgresMutable,
"${nodes.postgresMostlyMutable.system.build.toplevel}",
postgresMostlyMutable,
"${nodes.postgresImmutable.system.build.toplevel}",
postgresImmutable,
"${nodes.postgresEnvironmentFile.system.build.toplevel}",
postgresEnvironmentFile
)
'';
}
)

Binary file not shown.

After

Width:  |  Height:  |  Size: 119 KiB