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 lib.optionalAttrs (mattermostPlugins != null) {
90 PluginSettings = {
91 Enable = true;
92 };
93 }
94 );
95
96 mattermostConfJSON = pkgs.writeText "mattermost-config.json" (builtins.toJSON mattermostConf);
97
98in
99
100{
101 options = {
102 services.mattermost = {
103 enable = mkEnableOption "Mattermost chat server";
104
105 package = mkPackageOption pkgs "mattermost" { };
106
107 statePath = mkOption {
108 type = types.str;
109 default = "/var/lib/mattermost";
110 description = "Mattermost working directory";
111 };
112
113 siteUrl = mkOption {
114 type = types.str;
115 example = "https://chat.example.com";
116 description = ''
117 URL this Mattermost instance is reachable under, without trailing slash.
118 '';
119 };
120
121 siteName = mkOption {
122 type = types.str;
123 default = "Mattermost";
124 description = "Name of this Mattermost site.";
125 };
126
127 listenAddress = mkOption {
128 type = types.str;
129 default = ":8065";
130 example = "[::1]:8065";
131 description = ''
132 Address and port this Mattermost instance listens to.
133 '';
134 };
135
136 mutableConfig = mkOption {
137 type = types.bool;
138 default = false;
139 description = ''
140 Whether the Mattermost config.json is writeable by Mattermost.
141
142 Most of the settings can be edited in the system console of
143 Mattermost if this option is enabled. A template config using
144 the options specified in services.mattermost will be generated
145 but won't be overwritten on changes or rebuilds.
146
147 If this option is disabled, changes in the system console won't
148 be possible (default). If an config.json is present, it will be
149 overwritten!
150 '';
151 };
152
153 preferNixConfig = mkOption {
154 type = types.bool;
155 default = false;
156 description = ''
157 If both mutableConfig and this option are set, the Nix configuration
158 will take precedence over any settings configured in the server
159 console.
160 '';
161 };
162
163 extraConfig = mkOption {
164 type = types.attrs;
165 default = { };
166 description = ''
167 Additional configuration options as Nix attribute set in config.json schema.
168 '';
169 };
170
171 plugins = mkOption {
172 type = types.listOf (types.oneOf [types.path types.package]);
173 default = [];
174 example = "[ ./com.github.moussetc.mattermost.plugin.giphy-2.0.0.tar.gz ]";
175 description = ''
176 Plugins to add to the configuration. Overrides any installed if non-null.
177 This is a list of paths to .tar.gz files or derivations evaluating to
178 .tar.gz files.
179 '';
180 };
181 environmentFile = mkOption {
182 type = types.nullOr types.path;
183 default = null;
184 description = ''
185 Environment file (see {manpage}`systemd.exec(5)`
186 "EnvironmentFile=" section for the syntax) which sets config options
187 for mattermost (see [the mattermost documentation](https://docs.mattermost.com/configure/configuration-settings.html#environment-variables)).
188
189 Settings defined in the environment file will overwrite settings
190 set via nix or via the {option}`services.mattermost.extraConfig`
191 option.
192
193 Useful for setting config options without their value ending up in the
194 (world-readable) nix store, e.g. for a database password.
195 '';
196 };
197
198 localDatabaseCreate = mkOption {
199 type = types.bool;
200 default = true;
201 description = ''
202 Create a local PostgreSQL database for Mattermost automatically.
203 '';
204 };
205
206 localDatabaseName = mkOption {
207 type = types.str;
208 default = "mattermost";
209 description = ''
210 Local Mattermost database name.
211 '';
212 };
213
214 localDatabaseUser = mkOption {
215 type = types.str;
216 default = "mattermost";
217 description = ''
218 Local Mattermost database username.
219 '';
220 };
221
222 localDatabasePassword = mkOption {
223 type = types.str;
224 default = "mmpgsecret";
225 description = ''
226 Password for local Mattermost database user.
227 '';
228 };
229
230 user = mkOption {
231 type = types.str;
232 default = "mattermost";
233 description = ''
234 User which runs the Mattermost service.
235 '';
236 };
237
238 group = mkOption {
239 type = types.str;
240 default = "mattermost";
241 description = ''
242 Group which runs the Mattermost service.
243 '';
244 };
245
246 matterircd = {
247 enable = mkEnableOption "Mattermost IRC bridge";
248 package = mkPackageOption pkgs "matterircd" { };
249 parameters = mkOption {
250 type = types.listOf types.str;
251 default = [ ];
252 example = [ "-mmserver chat.example.com" "-bind [::]:6667" ];
253 description = ''
254 Set commandline parameters to pass to matterircd. See
255 https://github.com/42wim/matterircd#usage for more information.
256 '';
257 };
258 };
259 };
260 };
261
262 config = mkMerge [
263 (mkIf cfg.enable {
264 users.users = optionalAttrs (cfg.user == "mattermost") {
265 mattermost = {
266 group = cfg.group;
267 uid = config.ids.uids.mattermost;
268 home = cfg.statePath;
269 };
270 };
271
272 users.groups = optionalAttrs (cfg.group == "mattermost") {
273 mattermost.gid = config.ids.gids.mattermost;
274 };
275
276 services.postgresql.enable = cfg.localDatabaseCreate;
277
278 # The systemd service will fail to execute the preStart hook
279 # if the WorkingDirectory does not exist
280 systemd.tmpfiles.settings."10-mattermost".${cfg.statePath}.d = { };
281
282 systemd.services.mattermost = {
283 description = "Mattermost chat service";
284 wantedBy = [ "multi-user.target" ];
285 after = [ "network.target" "postgresql.service" ];
286
287 preStart = ''
288 mkdir -p "${cfg.statePath}"/{data,config,logs,plugins}
289 mkdir -p "${cfg.statePath}/plugins"/{client,server}
290 ln -sf ${cfg.package}/{bin,fonts,i18n,templates,client} "${cfg.statePath}"
291 '' + lib.optionalString (mattermostPlugins != null) ''
292 rm -rf "${cfg.statePath}/data/plugins"
293 ln -sf ${mattermostPlugins}/data/plugins "${cfg.statePath}/data"
294 '' + lib.optionalString (!cfg.mutableConfig) ''
295 rm -f "${cfg.statePath}/config/config.json"
296 ${pkgs.jq}/bin/jq -s '.[0] * .[1]' ${cfg.package}/config/config.json ${mattermostConfJSON} > "${cfg.statePath}/config/config.json"
297 '' + lib.optionalString cfg.mutableConfig ''
298 if ! test -e "${cfg.statePath}/config/.initial-created"; then
299 rm -f ${cfg.statePath}/config/config.json
300 ${pkgs.jq}/bin/jq -s '.[0] * .[1]' ${cfg.package}/config/config.json ${mattermostConfJSON} > "${cfg.statePath}/config/config.json"
301 touch "${cfg.statePath}/config/.initial-created"
302 fi
303 '' + lib.optionalString (cfg.mutableConfig && cfg.preferNixConfig) ''
304 new_config="$(${pkgs.jq}/bin/jq -s '.[0] * .[1]' "${cfg.statePath}/config/config.json" ${mattermostConfJSON})"
305
306 rm -f "${cfg.statePath}/config/config.json"
307 echo "$new_config" > "${cfg.statePath}/config/config.json"
308 '' + lib.optionalString cfg.localDatabaseCreate (createDb {}) + ''
309 # Don't change permissions recursively on the data, current, and symlinked directories (see ln -sf command above).
310 # This dramatically decreases startup times for installations with a lot of files.
311 find . -maxdepth 1 -not -name data -not -name client -not -name templates -not -name i18n -not -name fonts -not -name bin -not -name . \
312 -exec chown "${cfg.user}:${cfg.group}" -R {} \; -exec chmod u+rw,g+r,o-rwx -R {} \;
313
314 chown "${cfg.user}:${cfg.group}" "${cfg.statePath}/data" .
315 chmod u+rw,g+r,o-rwx "${cfg.statePath}/data" .
316 '';
317
318 serviceConfig = {
319 PermissionsStartOnly = true;
320 User = cfg.user;
321 Group = cfg.group;
322 ExecStart = "${cfg.package}/bin/mattermost";
323 WorkingDirectory = "${cfg.statePath}";
324 Restart = "always";
325 RestartSec = "10";
326 LimitNOFILE = "49152";
327 EnvironmentFile = cfg.environmentFile;
328 };
329 unitConfig.JoinsNamespaceOf = mkIf cfg.localDatabaseCreate "postgresql.service";
330 };
331 })
332 (mkIf cfg.matterircd.enable {
333 systemd.services.matterircd = {
334 description = "Mattermost IRC bridge service";
335 wantedBy = [ "multi-user.target" ];
336 serviceConfig = {
337 User = "nobody";
338 Group = "nogroup";
339 ExecStart = "${cfg.matterircd.package}/bin/matterircd ${escapeShellArgs cfg.matterircd.parameters}";
340 WorkingDirectory = "/tmp";
341 PrivateTmp = true;
342 Restart = "always";
343 RestartSec = "5";
344 };
345 };
346 })
347 ];
348}