at 25.11-pre 32 kB view raw
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}