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 = "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 Addtional 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
188 localDatabaseCreate = mkOption {
189 type = types.bool;
190 default = true;
191 description = lib.mdDoc ''
192 Create a local PostgreSQL database for Mattermost automatically.
193 '';
194 };
195
196 localDatabaseName = mkOption {
197 type = types.str;
198 default = "mattermost";
199 description = lib.mdDoc ''
200 Local Mattermost database name.
201 '';
202 };
203
204 localDatabaseUser = mkOption {
205 type = types.str;
206 default = "mattermost";
207 description = lib.mdDoc ''
208 Local Mattermost database username.
209 '';
210 };
211
212 localDatabasePassword = mkOption {
213 type = types.str;
214 default = "mmpgsecret";
215 description = lib.mdDoc ''
216 Password for local Mattermost database user.
217 '';
218 };
219
220 user = mkOption {
221 type = types.str;
222 default = "mattermost";
223 description = lib.mdDoc ''
224 User which runs the Mattermost service.
225 '';
226 };
227
228 group = mkOption {
229 type = types.str;
230 default = "mattermost";
231 description = lib.mdDoc ''
232 Group which runs the Mattermost service.
233 '';
234 };
235
236 matterircd = {
237 enable = mkEnableOption (lib.mdDoc "Mattermost IRC bridge");
238 package = mkOption {
239 type = types.package;
240 default = pkgs.matterircd;
241 defaultText = "pkgs.matterircd";
242 description = lib.mdDoc "matterircd derivation to use.";
243 };
244 parameters = mkOption {
245 type = types.listOf types.str;
246 default = [ ];
247 example = [ "-mmserver chat.example.com" "-bind [::]:6667" ];
248 description = lib.mdDoc ''
249 Set commandline parameters to pass to matterircd. See
250 https://github.com/42wim/matterircd#usage for more information.
251 '';
252 };
253 };
254 };
255 };
256
257 config = mkMerge [
258 (mkIf cfg.enable {
259 users.users = optionalAttrs (cfg.user == "mattermost") {
260 mattermost = {
261 group = cfg.group;
262 uid = config.ids.uids.mattermost;
263 home = cfg.statePath;
264 };
265 };
266
267 users.groups = optionalAttrs (cfg.group == "mattermost") {
268 mattermost.gid = config.ids.gids.mattermost;
269 };
270
271 services.postgresql.enable = cfg.localDatabaseCreate;
272
273 # The systemd service will fail to execute the preStart hook
274 # if the WorkingDirectory does not exist
275 system.activationScripts.mattermost = ''
276 mkdir -p "${cfg.statePath}"
277 '';
278
279 systemd.services.mattermost = {
280 description = "Mattermost chat service";
281 wantedBy = [ "multi-user.target" ];
282 after = [ "network.target" "postgresql.service" ];
283
284 preStart = ''
285 mkdir -p "${cfg.statePath}"/{data,config,logs,plugins}
286 mkdir -p "${cfg.statePath}/plugins"/{client,server}
287 ln -sf ${cfg.package}/{bin,fonts,i18n,templates,client} "${cfg.statePath}"
288 '' + lib.optionalString (mattermostPlugins != null) ''
289 rm -rf "${cfg.statePath}/data/plugins"
290 ln -sf ${mattermostPlugins}/data/plugins "${cfg.statePath}/data"
291 '' + lib.optionalString (!cfg.mutableConfig) ''
292 rm -f "${cfg.statePath}/config/config.json"
293 ${pkgs.jq}/bin/jq -s '.[0] * .[1]' ${cfg.package}/config/config.json ${mattermostConfJSON} > "${cfg.statePath}/config/config.json"
294 '' + lib.optionalString cfg.mutableConfig ''
295 if ! test -e "${cfg.statePath}/config/.initial-created"; then
296 rm -f ${cfg.statePath}/config/config.json
297 ${pkgs.jq}/bin/jq -s '.[0] * .[1]' ${cfg.package}/config/config.json ${mattermostConfJSON} > "${cfg.statePath}/config/config.json"
298 touch "${cfg.statePath}/config/.initial-created"
299 fi
300 '' + lib.optionalString (cfg.mutableConfig && cfg.preferNixConfig) ''
301 new_config="$(${pkgs.jq}/bin/jq -s '.[0] * .[1]' "${cfg.statePath}/config/config.json" ${mattermostConfJSON})"
302
303 rm -f "${cfg.statePath}/config/config.json"
304 echo "$new_config" > "${cfg.statePath}/config/config.json"
305 '' + lib.optionalString cfg.localDatabaseCreate (createDb {}) + ''
306 # Don't change permissions recursively on the data, current, and symlinked directories (see ln -sf command above).
307 # This dramatically decreases startup times for installations with a lot of files.
308 find . -maxdepth 1 -not -name data -not -name client -not -name templates -not -name i18n -not -name fonts -not -name bin -not -name . \
309 -exec chown "${cfg.user}:${cfg.group}" -R {} \; -exec chmod u+rw,g+r,o-rwx -R {} \;
310
311 chown "${cfg.user}:${cfg.group}" "${cfg.statePath}/data" .
312 chmod u+rw,g+r,o-rwx "${cfg.statePath}/data" .
313 '';
314
315 serviceConfig = {
316 PermissionsStartOnly = true;
317 User = cfg.user;
318 Group = cfg.group;
319 ExecStart = "${cfg.package}/bin/mattermost";
320 WorkingDirectory = "${cfg.statePath}";
321 Restart = "always";
322 RestartSec = "10";
323 LimitNOFILE = "49152";
324 };
325 unitConfig.JoinsNamespaceOf = mkIf cfg.localDatabaseCreate "postgresql.service";
326 };
327 })
328 (mkIf cfg.matterircd.enable {
329 systemd.services.matterircd = {
330 description = "Mattermost IRC bridge service";
331 wantedBy = [ "multi-user.target" ];
332 serviceConfig = {
333 User = "nobody";
334 Group = "nogroup";
335 ExecStart = "${cfg.matterircd.package}/bin/matterircd ${escapeShellArgs cfg.matterircd.parameters}";
336 WorkingDirectory = "/tmp";
337 PrivateTmp = true;
338 Restart = "always";
339 RestartSec = "5";
340 };
341 };
342 })
343 ];
344}