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 (lib.versions.majorMinor lib.version); 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; }; telemetry.enableSecurityAlerts = false; 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 (lib.versions.majorMinor lib.version); }; } ) 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; preferNixConfig = false; settings.SupportSettings.HelpLink = "https://search.nixos.org"; } { }; 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" ]; }; }) ]; } { # Last version to support the "old" config layout. system.stateVersion = lib.mkForce "24.11"; # Supports the "new" config layout. specialisation.upgrade.configuration.system.stateVersion = lib.mkVMOverride ( lib.versions.majorMinor lib.version ); }; 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 fi # Make sure it's on the filesystem in the expected place fsPath="$(find /var/lib/mattermost/data -name "$(basename -- "$postAttachment")" -print -quit)" if [ -z "$fsPath" ] || [ ! -f "$fsPath" ]; then echo "Attachment didn't exist on the filesystem!" >&2 exit 1 fi # And that the hash matches. actualFsAttachmentHash="$(sha256sum "$fsPath" | awk '{print $1}')" if [ "$actualFsAttachmentHash" == "$postAttachmentHash" ]; then echo "Post attachment hash was OK!" >&2 exit 0 else echo "Attachment hash mismatched on disk!" >&2 exit 1 fi else echo "Post didn't exist when it should have!" >&2 exit 1 fi ''; in '' import sys import shlex import threading import queue def wait_mattermost_up(node, site_name="${siteName}"): print(f"wait_mattermost_up({node.name!r}, site_name={site_name!r})", file=sys.stderr) node.wait_for_unit("multi-user.target") 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}"): print(f"restart_mattermost({node.name!r}, site_name={site_name!r})", file=sys.stderr) node.systemctl("restart mattermost.service") wait_mattermost_up(node, site_name) def expect_config(node, mattermost_version, *configs): print(f"expect_config({node.name!r}, {mattermost_version!r}, *{configs!r})", file=sys.stderr) for config in configs: node.succeed(f"${expectConfig} {shlex.quote(config)} {shlex.quote(mattermost_version)}") def expect_plugins(node, jq_or_code): print(f"expect_plugins({node.name!r}, {jq_or_code!r})", file=sys.stderr) node.succeed(f"${expectPlugins} {shlex.quote(str(jq_or_code))}") def ensure_post(node, fail_if_not_found=False): print(f"ensure_post({node.name!r}, fail_if_not_found={fail_if_not_found!r})", file=sys.stderr) node.succeed(f"${ensurePost} {shlex.quote('${url}')} {1 if fail_if_not_found else 0}") def set_config(node, *configs, nixos_version='${lib.versions.majorMinor lib.version}'): print(f"set_config({node.name!r}, *{configs!r}, nixos_version={nixos_version!r})", file=sys.stderr) 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 switch_to_specialisation(node, toplevel: str, specialisation: str): print(f"switch_to_specialisation({node.name!r}, {toplevel!r}, {specialisation!r})", file=sys.stderr) node.succeed(f"{toplevel}/specialisation/{specialisation}/bin/switch-to-configuration switch || true") def run_mattermost_tests(shutdown_queue: queue.Queue, mutableToplevel: str, mutable, mostlyMutableToplevel: str, mostlyMutablePlugins: 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"' ) 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 latest Mattermost version switch_to_specialisation(mutable, mutableToplevel, "latest") 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) shutdown_queue.put(mutable) ## 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"', nixos_version='24.11' # Default 'mostlyMutable' config is an old version ) ensure_post(mostlyMutable) restart_mattermost(mostlyMutable) # HelpLink should be changed but AboutLink should not, and the post should exist expect_config(mostlyMutable, esr, '.AboutLink == "https://nixos.org" and .HelpLink == "https://nixos.org/nixos/manual"') ensure_post(mostlyMutable, fail_if_not_found=True) # Switch to the newer config and make sure the plugins directory is replaced with a directory, # since it could have been a symlink on previous versions. mostlyMutable.systemctl("stop mattermost.service") mostlyMutable.succeed('[ -L /var/lib/mattermost/data/plugins ] && [ -d /var/lib/mattermost/data/plugins ]') switch_to_specialisation(mostlyMutable, mostlyMutableToplevel, "upgrade") wait_mattermost_up(mostlyMutable) # HelpLink should be changed, still, and the post should still exist expect_config(mostlyMutable, esr, '.AboutLink == "https://nixos.org" and .HelpLink == "https://nixos.org/nixos/manual"') ensure_post(mostlyMutable, fail_if_not_found=True) # Edit the config and make a post set_config( mostlyMutable, '.SupportSettings.AboutLink = "https://mattermost.com/foo"', '.SupportSettings.HelpLink = "https://nixos.org/nixos/manual/bar"', '.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/bar"') # 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 switch_to_specialisation(mostlyMutable, mostlyMutableToplevel, "latest") 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/bar"') ensure_post(mostlyMutable, fail_if_not_found=True) shutdown_queue.put(mostlyMutable) ## 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 switch_to_specialisation(immutable, immutableToplevel, "latest") 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) shutdown_queue.put(immutable) ## 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 switch_to_specialisation(environmentFile, environmentFileToplevel, "latest") 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) shutdown_queue.put(environmentFile) # Run shutdowns asynchronously so we can pipeline them. shutdown_queue: queue.Queue = queue.Queue() def shutdown_worker(): while True: node = shutdown_queue.get() print(f"Shutting down node {node.name!r} asynchronously", file=sys.stderr) node.shutdown() shutdown_queue.task_done() threading.Thread(target=shutdown_worker, daemon=True).start() ${pkgs.lib.optionalString pkgs.stdenv.hostPlatform.isx86_64 '' # Only run the MySQL tests on x86_64 so we don't have to debug MySQL ARM issues. run_mattermost_tests( shutdown_queue, "${nodes.mysqlMutable.system.build.toplevel}", mysqlMutable, "${nodes.mysqlMostlyMutable.system.build.toplevel}", "${nodes.mysqlMostlyMutable.services.mattermost.pluginsBundle}", mysqlMostlyMutable, "${nodes.mysqlImmutable.system.build.toplevel}", mysqlImmutable, "${nodes.mysqlEnvironmentFile.system.build.toplevel}", mysqlEnvironmentFile ) ''} run_mattermost_tests( shutdown_queue, "${nodes.postgresMutable.system.build.toplevel}", postgresMutable, "${nodes.postgresMostlyMutable.system.build.toplevel}", "${nodes.postgresMostlyMutable.services.mattermost.pluginsBundle}", postgresMostlyMutable, "${nodes.postgresImmutable.system.build.toplevel}", postgresImmutable, "${nodes.postgresEnvironmentFile.system.build.toplevel}", postgresEnvironmentFile ) # Drain the queue shutdown_queue.join() ''; } )