1{ config, pkgs, lib, ... }:
2
3with lib;
4
5let
6
7 cfg = config.services.mattermost;
8
9 database = "postgres://${cfg.localDatabaseUser}:${cfg.localDatabasePassword}@localhost:5432/${cfg.localDatabaseName}?sslmode=disable&connect_timeout=10";
10
11 postgresPackage = config.services.postgresql.package;
12
13 createDb = {
14 statePath ? cfg.statePath,
15 localDatabaseUser ? cfg.localDatabaseUser,
16 localDatabasePassword ? cfg.localDatabasePassword,
17 localDatabaseName ? cfg.localDatabaseName,
18 useSudo ? true
19 }: ''
20 if ! test -e ${escapeShellArg "${statePath}/.db-created"}; then
21 ${lib.optionalString useSudo "${pkgs.sudo}/bin/sudo -u ${escapeShellArg config.services.postgresql.superUser} \\"}
22 ${postgresPackage}/bin/psql postgres -c \
23 "CREATE ROLE ${localDatabaseUser} WITH LOGIN NOCREATEDB NOCREATEROLE ENCRYPTED PASSWORD '${localDatabasePassword}'"
24 ${lib.optionalString useSudo "${pkgs.sudo}/bin/sudo -u ${escapeShellArg config.services.postgresql.superUser} \\"}
25 ${postgresPackage}/bin/createdb \
26 --owner ${escapeShellArg localDatabaseUser} ${escapeShellArg localDatabaseName}
27 touch ${escapeShellArg "${statePath}/.db-created"}
28 fi
29 '';
30
31 mattermostPluginDerivations = with pkgs;
32 map (plugin: stdenv.mkDerivation {
33 name = "mattermost-plugin";
34 installPhase = ''
35 mkdir -p $out/share
36 cp ${plugin} $out/share/plugin.tar.gz
37 '';
38 dontUnpack = true;
39 dontPatch = true;
40 dontConfigure = true;
41 dontBuild = true;
42 preferLocalBuild = true;
43 }) cfg.plugins;
44
45 mattermostPlugins = with pkgs;
46 if mattermostPluginDerivations == [] then null
47 else stdenv.mkDerivation {
48 name = "${cfg.package.name}-plugins";
49 nativeBuildInputs = [
50 autoPatchelfHook
51 ] ++ mattermostPluginDerivations;
52 buildInputs = [
53 cfg.package
54 ];
55 installPhase = ''
56 mkdir -p $out/data/plugins
57 plugins=(${escapeShellArgs (map (plugin: "${plugin}/share/plugin.tar.gz") mattermostPluginDerivations)})
58 for plugin in "''${plugins[@]}"; do
59 hash="$(sha256sum "$plugin" | cut -d' ' -f1)"
60 mkdir -p "$hash"
61 tar -C "$hash" -xzf "$plugin"
62 autoPatchelf "$hash"
63 GZIP_OPT=-9 tar -C "$hash" -cvzf "$out/data/plugins/$hash.tar.gz" .
64 rm -rf "$hash"
65 done
66 '';
67
68 dontUnpack = true;
69 dontPatch = true;
70 dontConfigure = true;
71 dontBuild = true;
72 preferLocalBuild = true;
73 };
74
75 mattermostConfWithoutPlugins = recursiveUpdate
76 { ServiceSettings.SiteURL = cfg.siteUrl;
77 ServiceSettings.ListenAddress = cfg.listenAddress;
78 TeamSettings.SiteName = cfg.siteName;
79 SqlSettings.DriverName = "postgres";
80 SqlSettings.DataSource = database;
81 PluginSettings.Directory = "${cfg.statePath}/plugins/server";
82 PluginSettings.ClientDirectory = "${cfg.statePath}/plugins/client";
83 }
84 cfg.extraConfig;
85
86 mattermostConf = recursiveUpdate
87 mattermostConfWithoutPlugins
88 (
89 if mattermostPlugins == null then {}
90 else {
91 PluginSettings = {
92 Enable = true;
93 };
94 }
95 );
96
97 mattermostConfJSON = pkgs.writeText "mattermost-config.json" (builtins.toJSON mattermostConf);
98
99in
100
101{
102 options = {
103 services.mattermost = {
104 enable = mkEnableOption (lib.mdDoc "Mattermost chat server");
105
106 package = mkOption {
107 type = types.package;
108 default = pkgs.mattermost;
109 defaultText = lib.literalExpression "pkgs.mattermost";
110 description = lib.mdDoc "Mattermost derivation to use.";
111 };
112
113 statePath = mkOption {
114 type = types.str;
115 default = "/var/lib/mattermost";
116 description = lib.mdDoc "Mattermost working directory";
117 };
118
119 siteUrl = mkOption {
120 type = types.str;
121 example = "https://chat.example.com";
122 description = lib.mdDoc ''
123 URL this Mattermost instance is reachable under, without trailing slash.
124 '';
125 };
126
127 siteName = mkOption {
128 type = types.str;
129 default = "Mattermost";
130 description = lib.mdDoc "Name of this Mattermost site.";
131 };
132
133 listenAddress = mkOption {
134 type = types.str;
135 default = ":8065";
136 example = "[::1]:8065";
137 description = lib.mdDoc ''
138 Address and port this Mattermost instance listens to.
139 '';
140 };
141
142 mutableConfig = mkOption {
143 type = types.bool;
144 default = false;
145 description = lib.mdDoc ''
146 Whether the Mattermost config.json is writeable by Mattermost.
147
148 Most of the settings can be edited in the system console of
149 Mattermost if this option is enabled. A template config using
150 the options specified in services.mattermost will be generated
151 but won't be overwritten on changes or rebuilds.
152
153 If this option is disabled, changes in the system console won't
154 be possible (default). If an config.json is present, it will be
155 overwritten!
156 '';
157 };
158
159 preferNixConfig = mkOption {
160 type = types.bool;
161 default = false;
162 description = lib.mdDoc ''
163 If both mutableConfig and this option are set, the Nix configuration
164 will take precedence over any settings configured in the server
165 console.
166 '';
167 };
168
169 extraConfig = mkOption {
170 type = types.attrs;
171 default = { };
172 description = lib.mdDoc ''
173 Additional configuration options as Nix attribute set in config.json schema.
174 '';
175 };
176
177 plugins = mkOption {
178 type = types.listOf (types.oneOf [types.path types.package]);
179 default = [];
180 example = "[ ./com.github.moussetc.mattermost.plugin.giphy-2.0.0.tar.gz ]";
181 description = lib.mdDoc ''
182 Plugins to add to the configuration. Overrides any installed if non-null.
183 This is a list of paths to .tar.gz files or derivations evaluating to
184 .tar.gz files.
185 '';
186 };
187 environmentFile = mkOption {
188 type = types.nullOr types.path;
189 default = null;
190 description = lib.mdDoc ''
191 Environment file (see {manpage}`systemd.exec(5)`
192 "EnvironmentFile=" section for the syntax) which sets config options
193 for mattermost (see [the mattermost documentation](https://docs.mattermost.com/configure/configuration-settings.html#environment-variables)).
194
195 Settings defined in the environment file will overwrite settings
196 set via nix or via the {option}`services.mattermost.extraConfig`
197 option.
198
199 Useful for setting config options without their value ending up in the
200 (world-readable) nix store, e.g. for a database password.
201 '';
202 };
203
204 localDatabaseCreate = mkOption {
205 type = types.bool;
206 default = true;
207 description = lib.mdDoc ''
208 Create a local PostgreSQL database for Mattermost automatically.
209 '';
210 };
211
212 localDatabaseName = mkOption {
213 type = types.str;
214 default = "mattermost";
215 description = lib.mdDoc ''
216 Local Mattermost database name.
217 '';
218 };
219
220 localDatabaseUser = mkOption {
221 type = types.str;
222 default = "mattermost";
223 description = lib.mdDoc ''
224 Local Mattermost database username.
225 '';
226 };
227
228 localDatabasePassword = mkOption {
229 type = types.str;
230 default = "mmpgsecret";
231 description = lib.mdDoc ''
232 Password for local Mattermost database user.
233 '';
234 };
235
236 user = mkOption {
237 type = types.str;
238 default = "mattermost";
239 description = lib.mdDoc ''
240 User which runs the Mattermost service.
241 '';
242 };
243
244 group = mkOption {
245 type = types.str;
246 default = "mattermost";
247 description = lib.mdDoc ''
248 Group which runs the Mattermost service.
249 '';
250 };
251
252 matterircd = {
253 enable = mkEnableOption (lib.mdDoc "Mattermost IRC bridge");
254 package = mkOption {
255 type = types.package;
256 default = pkgs.matterircd;
257 defaultText = lib.literalExpression "pkgs.matterircd";
258 description = lib.mdDoc "matterircd derivation to use.";
259 };
260 parameters = mkOption {
261 type = types.listOf types.str;
262 default = [ ];
263 example = [ "-mmserver chat.example.com" "-bind [::]:6667" ];
264 description = lib.mdDoc ''
265 Set commandline parameters to pass to matterircd. See
266 https://github.com/42wim/matterircd#usage for more information.
267 '';
268 };
269 };
270 };
271 };
272
273 config = mkMerge [
274 (mkIf cfg.enable {
275 users.users = optionalAttrs (cfg.user == "mattermost") {
276 mattermost = {
277 group = cfg.group;
278 uid = config.ids.uids.mattermost;
279 home = cfg.statePath;
280 };
281 };
282
283 users.groups = optionalAttrs (cfg.group == "mattermost") {
284 mattermost.gid = config.ids.gids.mattermost;
285 };
286
287 services.postgresql.enable = cfg.localDatabaseCreate;
288
289 # The systemd service will fail to execute the preStart hook
290 # if the WorkingDirectory does not exist
291 system.activationScripts.mattermost = ''
292 mkdir -p "${cfg.statePath}"
293 '';
294
295 systemd.services.mattermost = {
296 description = "Mattermost chat service";
297 wantedBy = [ "multi-user.target" ];
298 after = [ "network.target" "postgresql.service" ];
299
300 preStart = ''
301 mkdir -p "${cfg.statePath}"/{data,config,logs,plugins}
302 mkdir -p "${cfg.statePath}/plugins"/{client,server}
303 ln -sf ${cfg.package}/{bin,fonts,i18n,templates,client} "${cfg.statePath}"
304 '' + lib.optionalString (mattermostPlugins != null) ''
305 rm -rf "${cfg.statePath}/data/plugins"
306 ln -sf ${mattermostPlugins}/data/plugins "${cfg.statePath}/data"
307 '' + lib.optionalString (!cfg.mutableConfig) ''
308 rm -f "${cfg.statePath}/config/config.json"
309 ${pkgs.jq}/bin/jq -s '.[0] * .[1]' ${cfg.package}/config/config.json ${mattermostConfJSON} > "${cfg.statePath}/config/config.json"
310 '' + lib.optionalString cfg.mutableConfig ''
311 if ! test -e "${cfg.statePath}/config/.initial-created"; then
312 rm -f ${cfg.statePath}/config/config.json
313 ${pkgs.jq}/bin/jq -s '.[0] * .[1]' ${cfg.package}/config/config.json ${mattermostConfJSON} > "${cfg.statePath}/config/config.json"
314 touch "${cfg.statePath}/config/.initial-created"
315 fi
316 '' + lib.optionalString (cfg.mutableConfig && cfg.preferNixConfig) ''
317 new_config="$(${pkgs.jq}/bin/jq -s '.[0] * .[1]' "${cfg.statePath}/config/config.json" ${mattermostConfJSON})"
318
319 rm -f "${cfg.statePath}/config/config.json"
320 echo "$new_config" > "${cfg.statePath}/config/config.json"
321 '' + lib.optionalString cfg.localDatabaseCreate (createDb {}) + ''
322 # Don't change permissions recursively on the data, current, and symlinked directories (see ln -sf command above).
323 # This dramatically decreases startup times for installations with a lot of files.
324 find . -maxdepth 1 -not -name data -not -name client -not -name templates -not -name i18n -not -name fonts -not -name bin -not -name . \
325 -exec chown "${cfg.user}:${cfg.group}" -R {} \; -exec chmod u+rw,g+r,o-rwx -R {} \;
326
327 chown "${cfg.user}:${cfg.group}" "${cfg.statePath}/data" .
328 chmod u+rw,g+r,o-rwx "${cfg.statePath}/data" .
329 '';
330
331 serviceConfig = {
332 PermissionsStartOnly = true;
333 User = cfg.user;
334 Group = cfg.group;
335 ExecStart = "${cfg.package}/bin/mattermost";
336 WorkingDirectory = "${cfg.statePath}";
337 Restart = "always";
338 RestartSec = "10";
339 LimitNOFILE = "49152";
340 EnvironmentFile = cfg.environmentFile;
341 };
342 unitConfig.JoinsNamespaceOf = mkIf cfg.localDatabaseCreate "postgresql.service";
343 };
344 })
345 (mkIf cfg.matterircd.enable {
346 systemd.services.matterircd = {
347 description = "Mattermost IRC bridge service";
348 wantedBy = [ "multi-user.target" ];
349 serviceConfig = {
350 User = "nobody";
351 Group = "nogroup";
352 ExecStart = "${cfg.matterircd.package}/bin/matterircd ${escapeShellArgs cfg.matterircd.parameters}";
353 WorkingDirectory = "/tmp";
354 PrivateTmp = true;
355 Restart = "always";
356 RestartSec = "5";
357 };
358 };
359 })
360 ];
361}