1{
2 config,
3 pkgs,
4 lib,
5 ...
6}:
7
8let
9 inherit (lib.strings)
10 hasInfix
11 hasSuffix
12 escapeURL
13 concatStringsSep
14 escapeShellArg
15 escapeShellArgs
16 versionAtLeast
17 optionalString
18 ;
19
20 inherit (lib.meta) getExe;
21
22 inherit (lib.lists) singleton;
23
24 inherit (lib.attrsets) mapAttrsToList recursiveUpdate optionalAttrs;
25
26 inherit (lib.options) mkOption mkPackageOption mkEnableOption;
27
28 inherit (lib.modules)
29 mkRenamedOptionModule
30 mkMerge
31 mkIf
32 mkDefault
33 ;
34
35 inherit (lib.trivial) warnIf throwIf;
36
37 inherit (lib) types;
38
39 cfg = config.services.mattermost;
40
41 # The directory to store mutable data within dataDir.
42 mutableDataDir = "${cfg.dataDir}/data";
43
44 # The plugin directory. Note that this is the *pre-unpack* plugin directory,
45 # since Mattermost looks in mutableDataDir for a directory called "plugins".
46 # If Mattermost is installed with plugins defined in a Nix configuration, the plugins
47 # are symlinked here. Otherwise, this is a real directory and the tarballs are uploaded here.
48 pluginTarballDir = "${mutableDataDir}/plugins";
49
50 # We need a different unpack directory for Mattermost to sync things to at launch,
51 # since the above may be a symlink to the store.
52 pluginUnpackDir = "${mutableDataDir}/.plugins";
53
54 # Mattermost uses this as a staging directory to unpack plugins, among possibly other things.
55 # Ensure that it's inside mutableDataDir since it can get rather large.
56 tempDir = "${mutableDataDir}/tmp";
57
58 # Creates a database URI.
59 mkDatabaseUri =
60 {
61 scheme,
62 user ? null,
63 password ? null,
64 escapeUserAndPassword ? true,
65 host ? null,
66 escapeHost ? true,
67 port ? null,
68 path ? null,
69 query ? { },
70 }:
71 let
72 nullToEmpty = val: if val == null then "" else toString val;
73
74 # Converts a list of URI attrs to a query string.
75 toQuery = mapAttrsToList (
76 name: value: if value == null then null else (escapeURL name) + "=" + (escapeURL (toString value))
77 );
78
79 schemePart = if scheme == null then "" else "${escapeURL scheme}://";
80 userPart =
81 let
82 realUser = if escapeUserAndPassword then escapeURL user else user;
83 realPassword = if escapeUserAndPassword then escapeURL password else password;
84 in
85 if user == null && password == null then
86 ""
87 else if user != null && password != null then
88 "${realUser}:${realPassword}"
89 else if user != null then
90 realUser
91 else
92 throw "Either user or username and password must be provided";
93 hostPart =
94 let
95 realHost = if escapeHost then escapeURL (nullToEmpty host) else nullToEmpty host;
96 in
97 if userPart == "" then realHost else "@" + realHost;
98 portPart = if port == null then "" else ":" + (toString port);
99 pathPart = if path == null then "" else "/" + path;
100 queryPart = if query == { } then "" else "?" + concatStringsSep "&" (toQuery query);
101 in
102 schemePart + userPart + hostPart + portPart + pathPart + queryPart;
103
104 database =
105 let
106 hostIsPath = hasInfix "/" cfg.database.host;
107 in
108 if cfg.database.driver == "postgres" then
109 if cfg.database.peerAuth then
110 mkDatabaseUri {
111 scheme = cfg.database.driver;
112 inherit (cfg.database) user;
113 path = escapeURL cfg.database.name;
114 query = {
115 host = cfg.database.socketPath;
116 } // cfg.database.extraConnectionOptions;
117 }
118 else
119 mkDatabaseUri {
120 scheme = cfg.database.driver;
121 inherit (cfg.database) user password;
122 host = if hostIsPath then null else cfg.database.host;
123 port = if hostIsPath then null else cfg.database.port;
124 path = escapeURL cfg.database.name;
125 query =
126 optionalAttrs hostIsPath { host = cfg.database.host; } // cfg.database.extraConnectionOptions;
127 }
128 else if cfg.database.driver == "mysql" then
129 if cfg.database.peerAuth then
130 mkDatabaseUri {
131 scheme = null;
132 inherit (cfg.database) user;
133 escapeUserAndPassword = false;
134 host = "unix(${cfg.database.socketPath})";
135 escapeHost = false;
136 path = escapeURL cfg.database.name;
137 query = cfg.database.extraConnectionOptions;
138 }
139 else
140 mkDatabaseUri {
141 scheme = null;
142 inherit (cfg.database) user password;
143 escapeUserAndPassword = false;
144 host =
145 if hostIsPath then
146 "unix(${cfg.database.host})"
147 else
148 "tcp(${cfg.database.host}:${toString cfg.database.port})";
149 escapeHost = false;
150 path = escapeURL cfg.database.name;
151 query = cfg.database.extraConnectionOptions;
152 }
153 else
154 throw "Invalid database driver: ${cfg.database.driver}";
155
156 mattermostPluginDerivations = map (
157 plugin:
158 pkgs.stdenvNoCC.mkDerivation {
159 name = "${cfg.package.name}-plugin";
160 installPhase = ''
161 runHook preInstall
162 mkdir -p $out/share
163 ln -sf ${plugin} $out/share/plugin.tar.gz
164 runHook postInstall
165 '';
166 dontUnpack = true;
167 dontPatch = true;
168 dontConfigure = true;
169 dontBuild = true;
170 preferLocalBuild = true;
171 }
172 ) cfg.plugins;
173
174 mattermostPlugins =
175 if mattermostPluginDerivations == [ ] then
176 null
177 else
178 pkgs.stdenvNoCC.mkDerivation {
179 name = "${cfg.package.name}-plugins";
180 nativeBuildInputs = [ pkgs.autoPatchelfHook ] ++ mattermostPluginDerivations;
181 buildInputs = [ cfg.package ];
182 installPhase = ''
183 runHook preInstall
184 mkdir -p $out
185 plugins=(${
186 escapeShellArgs (map (plugin: "${plugin}/share/plugin.tar.gz") mattermostPluginDerivations)
187 })
188 for plugin in "''${plugins[@]}"; do
189 hash="$(sha256sum "$plugin" | awk '{print $1}')"
190 mkdir -p "$hash"
191 tar -C "$hash" -xzf "$plugin"
192 autoPatchelf "$hash"
193 GZIP_OPT=-9 tar -C "$hash" -cvzf "$out/$hash.tar.gz" .
194 rm -rf "$hash"
195 done
196 runHook postInstall
197 '';
198
199 dontUnpack = true;
200 dontPatch = true;
201 dontConfigure = true;
202 dontBuild = true;
203 preferLocalBuild = true;
204 };
205
206 mattermostConfWithoutPlugins = recursiveUpdate {
207 ServiceSettings = {
208 SiteURL = cfg.siteUrl;
209 ListenAddress = "${cfg.host}:${toString cfg.port}";
210 LocalModeSocketLocation = cfg.socket.path;
211 EnableLocalMode = cfg.socket.enable;
212 EnableSecurityFixAlert = cfg.telemetry.enableSecurityAlerts;
213 };
214 TeamSettings.SiteName = cfg.siteName;
215 SqlSettings.DriverName = cfg.database.driver;
216 SqlSettings.DataSource =
217 if cfg.database.fromEnvironment then
218 null
219 else
220 warnIf (!cfg.database.peerAuth && cfg.database.password != null) ''
221 Database password is set in Mattermost config! This password will end up in the Nix store.
222
223 You may be able to simply set the following, if the database is on the same host
224 and peer authentication is enabled:
225
226 services.mattermost.database.peerAuth = true;
227
228 Note that this is the default if you set system.stateVersion to 25.05 or later
229 and the database host is localhost.
230
231 Alternatively, you can write the following to ${
232 if cfg.environmentFile == null then "your environment file" else cfg.environmentFile
233 }:
234
235 MM_SQLSETTINGS_DATASOURCE=${database}
236
237 Then set the following options:
238 services.mattermost.environmentFile = "<your environment file>";
239 services.mattermost.database.fromEnvironment = true;
240 '' database;
241
242 # Note that the plugin tarball directory is not configurable, and is expected to be in FileSettings.Directory/plugins.
243 FileSettings.Directory = mutableDataDir;
244 PluginSettings.Directory = "${pluginUnpackDir}/server";
245 PluginSettings.ClientDirectory = "${pluginUnpackDir}/client";
246
247 LogSettings = {
248 FileLocation = cfg.logDir;
249
250 # Reaches out to Mattermost's servers for telemetry; disable it by default.
251 # https://docs.mattermost.com/configure/environment-configuration-settings.html#enable-diagnostics-and-error-reporting
252 EnableDiagnostics = cfg.telemetry.enableDiagnostics;
253 };
254 } cfg.settings;
255
256 mattermostConf = recursiveUpdate mattermostConfWithoutPlugins (
257 if mattermostPlugins == null then
258 { }
259 else
260 {
261 PluginSettings = {
262 Enable = true;
263 };
264 }
265 );
266
267 format = pkgs.formats.json { };
268 finalConfig = format.generate "mattermost-config.json" mattermostConf;
269in
270{
271 imports = [
272 (mkRenamedOptionModule
273 [
274 "services"
275 "mattermost"
276 "listenAddress"
277 ]
278 [
279 "services"
280 "mattermost"
281 "host"
282 ]
283 )
284 (mkRenamedOptionModule
285 [
286 "services"
287 "mattermost"
288 "localDatabaseCreate"
289 ]
290 [
291 "services"
292 "mattermost"
293 "database"
294 "create"
295 ]
296 )
297 (mkRenamedOptionModule
298 [
299 "services"
300 "mattermost"
301 "localDatabasePassword"
302 ]
303 [
304 "services"
305 "mattermost"
306 "database"
307 "password"
308 ]
309 )
310 (mkRenamedOptionModule
311 [
312 "services"
313 "mattermost"
314 "localDatabaseUser"
315 ]
316 [
317 "services"
318 "mattermost"
319 "database"
320 "user"
321 ]
322 )
323 (mkRenamedOptionModule
324 [
325 "services"
326 "mattermost"
327 "localDatabaseName"
328 ]
329 [
330 "services"
331 "mattermost"
332 "database"
333 "name"
334 ]
335 )
336 (mkRenamedOptionModule
337 [
338 "services"
339 "mattermost"
340 "extraConfig"
341 ]
342 [
343 "services"
344 "mattermost"
345 "settings"
346 ]
347 )
348 (mkRenamedOptionModule
349 [
350 "services"
351 "mattermost"
352 "statePath"
353 ]
354 [
355 "services"
356 "mattermost"
357 "dataDir"
358 ]
359 )
360 ];
361
362 options = {
363 services.mattermost = {
364 enable = mkEnableOption "Mattermost chat server";
365
366 package = mkPackageOption pkgs "mattermost" { };
367
368 siteUrl = mkOption {
369 type = types.str;
370 example = "https://chat.example.com";
371 description = ''
372 URL this Mattermost instance is reachable under, without trailing slash.
373 '';
374 };
375
376 siteName = mkOption {
377 type = types.str;
378 default = "Mattermost";
379 description = "Name of this Mattermost site.";
380 };
381
382 host = mkOption {
383 type = types.str;
384 default = "127.0.0.1";
385 example = "0.0.0.0";
386 description = ''
387 Host or address that this Mattermost instance listens on.
388 '';
389 };
390
391 port = mkOption {
392 type = types.port;
393 default = 8065;
394 description = ''
395 Port for Mattermost server to listen on.
396 '';
397 };
398
399 dataDir = mkOption {
400 type = types.path;
401 default = "/var/lib/mattermost";
402 description = ''
403 Mattermost working directory.
404 '';
405 };
406
407 socket = {
408 enable = mkEnableOption "Mattermost control socket";
409
410 path = mkOption {
411 type = types.path;
412 default = "${cfg.dataDir}/mattermost.sock";
413 defaultText = ''''${config.mattermost.dataDir}/mattermost.sock'';
414 description = ''
415 Default location for the Mattermost control socket used by `mmctl`.
416 '';
417 };
418
419 export = mkEnableOption "Export socket control to system environment variables";
420 };
421
422 logDir = mkOption {
423 type = types.path;
424 default =
425 if versionAtLeast config.system.stateVersion "25.05" then
426 "/var/log/mattermost"
427 else
428 "${cfg.dataDir}/logs";
429 defaultText = ''
430 if versionAtLeast config.system.stateVersion "25.05" then "/var/log/mattermost"
431 else "''${config.services.mattermost.dataDir}/logs";
432 '';
433 description = ''
434 Mattermost log directory.
435 '';
436 };
437
438 configDir = mkOption {
439 type = types.path;
440 default =
441 if versionAtLeast config.system.stateVersion "25.05" then
442 "/etc/mattermost"
443 else
444 "${cfg.dataDir}/config";
445 defaultText = ''
446 if versionAtLeast config.system.stateVersion "25.05" then
447 "/etc/mattermost"
448 else
449 "''${config.services.mattermost.dataDir}/config";
450 '';
451 description = ''
452 Mattermost config directory.
453 '';
454 };
455
456 mutableConfig = mkOption {
457 type = types.bool;
458 default = false;
459 description = ''
460 Whether the Mattermost config.json is writeable by Mattermost.
461
462 Most of the settings can be edited in the system console of
463 Mattermost if this option is enabled. A template config using
464 the options specified in services.mattermost will be generated
465 but won't be overwritten on changes or rebuilds.
466
467 If this option is disabled, persistent changes in the system
468 console won't be possible (the default). If a config.json is
469 present, it will be overwritten at service start!
470 '';
471 };
472
473 preferNixConfig = mkOption {
474 type = types.bool;
475 default = versionAtLeast config.system.stateVersion "25.05";
476 defaultText = ''
477 versionAtLeast config.system.stateVersion "25.05";
478 '';
479 description = ''
480 If both mutableConfig and this option are set, the Nix configuration
481 will take precedence over any settings configured in the server
482 console.
483 '';
484 };
485
486 plugins = mkOption {
487 type = with types; listOf (either path package);
488 default = [ ];
489 example = "[ ./com.github.moussetc.mattermost.plugin.giphy-2.0.0.tar.gz ]";
490 description = ''
491 Plugins to add to the configuration. Overrides any installed if non-null.
492 This is a list of paths to .tar.gz files or derivations evaluating to
493 .tar.gz files. You can use `mattermost.buildPlugin` to build plugins;
494 see the NixOS documentation for more details.
495 '';
496 };
497
498 pluginsBundle = mkOption {
499 type = with types; nullOr package;
500 default = mattermostPlugins;
501 defaultText = ''
502 All entries in {config}`services.mattermost.plugins`, repacked
503 '';
504 description = ''
505 Derivation building to a directory of plugin tarballs.
506 This overrides {option}`services.mattermost.plugins` if provided.
507 '';
508 };
509
510 telemetry = {
511 enableSecurityAlerts = mkOption {
512 type = types.bool;
513 default = true;
514 description = ''
515 True if we should enable security update checking. This reaches out to Mattermost's servers:
516 https://docs.mattermost.com/manage/telemetry.html#security-update-check-feature
517 '';
518 };
519
520 enableDiagnostics = mkOption {
521 type = types.bool;
522 default = false;
523 description = ''
524 True if we should enable sending diagnostic data. This reaches out to Mattermost's servers:
525 https://docs.mattermost.com/manage/telemetry.html#error-and-diagnostics-reporting-feature
526 '';
527 };
528 };
529
530 environment = mkOption {
531 type = with types; attrsOf (either int str);
532 default = { };
533 description = ''
534 Extra environment variables to export to the Mattermost process
535 from the systemd unit configuration.
536 '';
537 example = {
538 MM_SERVICESETTINGS_SITEURL = "http://example.com";
539 };
540 };
541
542 environmentFile = mkOption {
543 type = with types; nullOr path;
544 default = null;
545 description = ''
546 Environment file (see {manpage}`systemd.exec(5)`
547 "EnvironmentFile=" section for the syntax) which sets config options
548 for mattermost (see [the Mattermost documentation](https://docs.mattermost.com/configure/configuration-settings.html#environment-variables)).
549
550 Settings defined in the environment file will overwrite settings
551 set via Nix or via the {option}`services.mattermost.extraConfig`
552 option.
553
554 Useful for setting config options without their value ending up in the
555 (world-readable) Nix store, e.g. for a database password.
556 '';
557 };
558
559 database = {
560 driver = mkOption {
561 type = types.enum [
562 "postgres"
563 "mysql"
564 ];
565 default = "postgres";
566 description = ''
567 The database driver to use (Postgres or MySQL).
568 '';
569 };
570
571 create = mkOption {
572 type = types.bool;
573 default = true;
574 description = ''
575 Create a local PostgreSQL or MySQL database for Mattermost automatically.
576 '';
577 };
578
579 peerAuth = mkOption {
580 type = types.bool;
581 default = versionAtLeast config.system.stateVersion "25.05" && cfg.database.host == "localhost";
582 defaultText = ''
583 versionAtLeast config.system.stateVersion "25.05" && config.services.mattermost.database.host == "localhost"
584 '';
585 description = ''
586 If set, will use peer auth instead of connecting to a Postgres server.
587 Use services.mattermost.database.socketPath to configure the socket path.
588 '';
589 };
590
591 socketPath = mkOption {
592 type = types.path;
593 default =
594 if cfg.database.driver == "postgres" then "/run/postgresql" else "/run/mysqld/mysqld.sock";
595 defaultText = ''
596 if config.services.mattermost.database.driver == "postgres" then "/run/postgresql" else "/run/mysqld/mysqld.sock";
597 '';
598 description = ''
599 The database (Postgres or MySQL) socket path.
600 '';
601 };
602
603 fromEnvironment = mkOption {
604 type = types.bool;
605 default = false;
606 description = ''
607 Use services.mattermost.environmentFile to configure the database instead of writing the database URI
608 to the Nix store. Useful if you use password authentication with peerAuth set to false.
609 '';
610 };
611
612 name = mkOption {
613 type = types.str;
614 default = "mattermost";
615 description = ''
616 Local Mattermost database name.
617 '';
618 };
619
620 host = mkOption {
621 type = types.str;
622 default = "localhost";
623 example = "127.0.0.1";
624 description = ''
625 Host to use for the database. Can also be set to a path if you'd like to connect
626 to a socket using a username and password.
627 '';
628 };
629
630 port = mkOption {
631 type = types.port;
632 default = if cfg.database.driver == "postgres" then 5432 else 3306;
633 defaultText = ''
634 if config.services.mattermost.database.type == "postgres" then 5432 else 3306
635 '';
636 example = 3306;
637 description = ''
638 Port to use for the database.
639 '';
640 };
641
642 user = mkOption {
643 type = types.str;
644 default = "mattermost";
645 description = ''
646 Local Mattermost database username.
647 '';
648 };
649
650 password = mkOption {
651 type = types.str;
652 default = "mmpgsecret";
653 description = ''
654 Password for local Mattermost database user. If set and peerAuth is not true,
655 will cause a warning nagging you to use environmentFile instead since it will
656 end up in the Nix store.
657 '';
658 };
659
660 extraConnectionOptions = mkOption {
661 type = with types; attrsOf (either int str);
662 default =
663 if cfg.database.driver == "postgres" then
664 {
665 sslmode = "disable";
666 connect_timeout = 60;
667 }
668 else if cfg.database.driver == "mysql" then
669 {
670 charset = "utf8mb4,utf8";
671 writeTimeout = "60s";
672 readTimeout = "60s";
673 }
674 else
675 throw "Invalid database driver ${cfg.database.driver}";
676 defaultText = ''
677 if config.mattermost.database.driver == "postgres" then
678 {
679 sslmode = "disable";
680 connect_timeout = 60;
681 }
682 else if config.mattermost.database.driver == "mysql" then
683 {
684 charset = "utf8mb4,utf8";
685 writeTimeout = "60s";
686 readTimeout = "60s";
687 }
688 else
689 throw "Invalid database driver";
690 '';
691 description = ''
692 Extra options that are placed in the connection URI's query parameters.
693 '';
694 };
695 };
696
697 user = mkOption {
698 type = types.str;
699 default = "mattermost";
700 description = ''
701 User which runs the Mattermost service.
702 '';
703 };
704
705 group = mkOption {
706 type = types.str;
707 default = "mattermost";
708 description = ''
709 Group which runs the Mattermost service.
710 '';
711 };
712
713 settings = mkOption {
714 inherit (format) type;
715 default = { };
716 description = ''
717 Additional configuration options as Nix attribute set in config.json schema.
718 '';
719 };
720
721 matterircd = {
722 enable = mkEnableOption "Mattermost IRC bridge";
723 package = mkPackageOption pkgs "matterircd" { };
724 parameters = mkOption {
725 type = types.listOf types.str;
726 default = [ ];
727 example = [
728 "-mmserver chat.example.com"
729 "-bind [::]:6667"
730 ];
731 description = ''
732 Set commandline parameters to pass to matterircd. See
733 <https://github.com/42wim/matterircd#usage> for more information.
734 '';
735 };
736 };
737 };
738 };
739
740 config = mkMerge [
741 (mkIf cfg.enable {
742 users.users = {
743 ${cfg.user} = {
744 group = cfg.group;
745 uid = mkIf (cfg.user == "mattermost") config.ids.uids.mattermost;
746 home = cfg.dataDir;
747 isSystemUser = true;
748 packages = [ cfg.package ];
749 };
750 };
751
752 users.groups = {
753 ${cfg.group} = {
754 gid = mkIf (cfg.group == "mattermost") config.ids.gids.mattermost;
755 };
756 };
757
758 services.postgresql = mkIf (cfg.database.driver == "postgres" && cfg.database.create) {
759 enable = true;
760 ensureDatabases = singleton cfg.database.name;
761 ensureUsers = singleton {
762 name =
763 throwIf
764 (cfg.database.peerAuth && (cfg.database.user != cfg.user || cfg.database.name != cfg.database.user))
765 ''
766 Mattermost database peer auth is enabled and the user, database user, or database name mismatch.
767 Peer authentication will not work.
768 ''
769 cfg.database.user;
770 ensureDBOwnership = true;
771 };
772 };
773
774 services.mysql = mkIf (cfg.database.driver == "mysql" && cfg.database.create) {
775 enable = true;
776 package = mkDefault pkgs.mariadb;
777 ensureDatabases = singleton cfg.database.name;
778 ensureUsers = singleton {
779 name = cfg.database.user;
780 ensurePermissions = {
781 "${cfg.database.name}.*" = "ALL PRIVILEGES";
782 };
783 };
784 settings = rec {
785 mysqld = {
786 collation-server = mkDefault "utf8mb4_general_ci";
787 init-connect = mkDefault "SET NAMES utf8mb4";
788 character-set-server = mkDefault "utf8mb4";
789 };
790 mysqld_safe = mysqld;
791 };
792 };
793
794 environment = {
795 variables = mkIf cfg.socket.export {
796 MMCTL_LOCAL = "true";
797 MMCTL_LOCAL_SOCKET_PATH = cfg.socket.path;
798 };
799 };
800
801 systemd.tmpfiles.rules =
802 [
803 "d ${cfg.dataDir} 0750 ${cfg.user} ${cfg.group} - -"
804 "d ${cfg.logDir} 0750 ${cfg.user} ${cfg.group} - -"
805 "d ${cfg.configDir} 0750 ${cfg.user} ${cfg.group} - -"
806 "d ${mutableDataDir} 0750 ${cfg.user} ${cfg.group} - -"
807
808 # Make sure tempDir exists and is not a symlink.
809 "R- ${tempDir} - - - - -"
810 "d= ${tempDir} 0750 ${cfg.user} ${cfg.group} - -"
811
812 # Ensure that pluginUnpackDir is a directory.
813 # Don't remove or clean it out since it should be persistent, as this is where plugins are unpacked.
814 "d= ${pluginUnpackDir} 0750 ${cfg.user} ${cfg.group} - -"
815
816 # Ensure that the plugin directories exist.
817 "d= ${mattermostConf.PluginSettings.Directory} 0750 ${cfg.user} ${cfg.group} - -"
818 "d= ${mattermostConf.PluginSettings.ClientDirectory} 0750 ${cfg.user} ${cfg.group} - -"
819
820 # Link in some of the immutable data directories.
821 "L+ ${cfg.dataDir}/bin - - - - ${cfg.package}/bin"
822 "L+ ${cfg.dataDir}/fonts - - - - ${cfg.package}/fonts"
823 "L+ ${cfg.dataDir}/i18n - - - - ${cfg.package}/i18n"
824 "L+ ${cfg.dataDir}/templates - - - - ${cfg.package}/templates"
825 "L+ ${cfg.dataDir}/client - - - - ${cfg.package}/client"
826 ]
827 ++ (
828 if cfg.pluginsBundle == null then
829 # Create the plugin tarball directory to allow plugin uploads.
830 [
831 "d= ${pluginTarballDir} 0750 ${cfg.user} ${cfg.group} - -"
832 ]
833 else
834 # Symlink the plugin tarball directory, removing anything existing, since it's managed by Nix.
835 [ "L+ ${pluginTarballDir} - - - - ${cfg.pluginsBundle}" ]
836 );
837
838 systemd.services.mattermost = rec {
839 description = "Mattermost chat service";
840 wantedBy = [ "multi-user.target" ];
841 after = mkMerge [
842 [ "network.target" ]
843 (mkIf (cfg.database.driver == "postgres" && cfg.database.create) [ "postgresql.service" ])
844 (mkIf (cfg.database.driver == "mysql" && cfg.database.create) [ "mysql.service" ])
845 ];
846 requires = after;
847
848 environment = mkMerge [
849 {
850 # Use tempDir as this can get rather large, especially if Mattermost unpacks a large number of plugins.
851 TMPDIR = tempDir;
852 }
853 cfg.environment
854 ];
855
856 preStart =
857 ''
858 dataDir=${escapeShellArg cfg.dataDir}
859 configDir=${escapeShellArg cfg.configDir}
860 logDir=${escapeShellArg cfg.logDir}
861 package=${escapeShellArg cfg.package}
862 nixConfig=${escapeShellArg finalConfig}
863 ''
864 + optionalString (versionAtLeast config.system.stateVersion "25.05") ''
865 # Migrate configs in the pre-25.05 directory structure.
866 oldConfig="$dataDir/config/config.json"
867 newConfig="$configDir/config.json"
868 if [ "$oldConfig" != "$newConfig" ] && [ -f "$oldConfig" ] && [ ! -f "$newConfig" ]; then
869 # Migrate the legacy config location to the new config location
870 echo "Moving legacy config at $oldConfig to $newConfig" >&2
871 mkdir -p "$configDir"
872 mv "$oldConfig" "$newConfig"
873 touch "$configDir/.initial-created"
874 fi
875
876 # Logs too.
877 oldLogs="$dataDir/logs"
878 newLogs="$logDir"
879 if [ "$oldLogs" != "$newLogs" ] && [ -d "$oldLogs" ] && [ ! -f "$newLogs/.initial-created" ]; then
880 # Migrate the legacy log location to the new log location.
881 # Allow this to fail if there aren't any logs to move.
882 echo "Moving legacy logs at $oldLogs to $newLogs" >&2
883 mkdir -p "$newLogs"
884 mv "$oldLogs"/* "$newLogs" || true
885 touch "$newLogs/.initial-created"
886 fi
887 ''
888 + optionalString (!cfg.mutableConfig) ''
889 ${getExe pkgs.jq} -s '.[0] * .[1]' "$package/config/config.json" "$nixConfig" > "$configDir/config.json"
890 ''
891 + optionalString cfg.mutableConfig ''
892 if [ ! -e "$configDir/.initial-created" ]; then
893 ${getExe pkgs.jq} -s '.[0] * .[1]' "$package/config/config.json" "$nixConfig" > "$configDir/config.json"
894 touch "$configDir/.initial-created"
895 fi
896 ''
897 + optionalString (cfg.mutableConfig && cfg.preferNixConfig) ''
898 echo "$(${getExe pkgs.jq} -s '.[0] * .[1]' "$configDir/config.json" "$nixConfig")" > "$configDir/config.json"
899 '';
900
901 serviceConfig = mkMerge [
902 {
903 User = cfg.user;
904 Group = cfg.group;
905 ExecStart = "${getExe cfg.package} --config ${cfg.configDir}/config.json";
906 ReadWritePaths = [
907 cfg.dataDir
908 cfg.logDir
909 cfg.configDir
910 ];
911 UMask = "0027";
912 Restart = "always";
913 RestartSec = 10;
914 LimitNOFILE = 49152;
915 LockPersonality = true;
916 NoNewPrivileges = true;
917 PrivateDevices = true;
918 PrivateTmp = true;
919 PrivateUsers = true;
920 ProtectClock = true;
921 ProtectControlGroups = true;
922 ProtectHome = true;
923 ProtectHostname = true;
924 ProtectKernelLogs = true;
925 ProtectKernelModules = true;
926 ProtectKernelTunables = true;
927 ProtectProc = "invisible";
928 ProtectSystem = "strict";
929 RestrictNamespaces = true;
930 RestrictSUIDSGID = true;
931 EnvironmentFile = cfg.environmentFile;
932 WorkingDirectory = cfg.dataDir;
933 }
934 (mkIf (cfg.dataDir == "/var/lib/mattermost") {
935 StateDirectory = baseNameOf cfg.dataDir;
936 StateDirectoryMode = "0750";
937 })
938 (mkIf (cfg.logDir == "/var/log/mattermost") {
939 LogsDirectory = baseNameOf cfg.logDir;
940 LogsDirectoryMode = "0750";
941 })
942 (mkIf (cfg.configDir == "/etc/mattermost") {
943 ConfigurationDirectory = baseNameOf cfg.configDir;
944 ConfigurationDirectoryMode = "0750";
945 })
946 ];
947
948 unitConfig.JoinsNamespaceOf = mkMerge [
949 (mkIf (cfg.database.driver == "postgres" && cfg.database.create) [ "postgresql.service" ])
950 (mkIf (cfg.database.driver == "mysql" && cfg.database.create) [ "mysql.service" ])
951 ];
952 };
953
954 assertions = [
955 {
956 # Make sure the URL doesn't have a trailing slash
957 assertion = !(hasSuffix "/" cfg.siteUrl);
958 message = ''
959 services.mattermost.siteUrl should not have a trailing "/".
960 '';
961 }
962 {
963 # Make sure this isn't a host/port pair
964 assertion = !(hasInfix ":" cfg.host && !(hasInfix "[" cfg.host) && !(hasInfix "]" cfg.host));
965 message = ''
966 services.mattermost.host should not include a port. Use services.mattermost.host for the address
967 or hostname, and services.mattermost.port to specify the port separately.
968 '';
969 }
970 ];
971 })
972 (mkIf cfg.matterircd.enable {
973 systemd.services.matterircd = {
974 description = "Mattermost IRC bridge service";
975 wantedBy = [ "multi-user.target" ];
976 serviceConfig = {
977 User = "nobody";
978 Group = "nogroup";
979 ExecStart = "${getExe cfg.matterircd.package} ${escapeShellArgs cfg.matterircd.parameters}";
980 WorkingDirectory = "/tmp";
981 PrivateTmp = true;
982 Restart = "always";
983 RestartSec = "5";
984 };
985 };
986 })
987 ];
988
989 meta.maintainers = with lib.maintainers; [ numinit ];
990}