python3Packages.orjson: Disable failing tests on 32 bit
[NixPkgs.git] / nixos / modules / services / misc / redmine.nix
blob75c5a4e26e0f68ef0c4285dca2a41611c48d6b21
1 { config, lib, pkgs, ... }:
3 let
4   inherit (lib) mkBefore mkDefault mkEnableOption mkIf mkOption mkRemovedOptionModule types;
5   inherit (lib) concatStringsSep literalExpression mapAttrsToList;
6   inherit (lib) optional optionalAttrs optionalString;
8   cfg = config.services.redmine;
9   format = pkgs.formats.yaml {};
10   bundle = "${cfg.package}/share/redmine/bin/bundle";
12   databaseYml = pkgs.writeText "database.yml" ''
13     production:
14       adapter: ${cfg.database.type}
15       database: ${cfg.database.name}
16       host: ${if (cfg.database.type == "postgresql" && cfg.database.socket != null) then cfg.database.socket else cfg.database.host}
17       port: ${toString cfg.database.port}
18       username: ${cfg.database.user}
19       password: #dbpass#
20       ${optionalString (cfg.database.type == "mysql2" && cfg.database.socket != null) "socket: ${cfg.database.socket}"}
21   '';
23   configurationYml = format.generate "configuration.yml" cfg.settings;
24   additionalEnvironment = pkgs.writeText "additional_environment.rb" cfg.extraEnv;
26   unpackTheme = unpack "theme";
27   unpackPlugin = unpack "plugin";
28   unpack = id: (name: source:
29     pkgs.stdenv.mkDerivation {
30       name = "redmine-${id}-${name}";
31       nativeBuildInputs = [ pkgs.unzip ];
32       buildCommand = ''
33         mkdir -p $out
34         cd $out
35         unpackFile ${source}
36       '';
37   });
39   mysqlLocal = cfg.database.createLocally && cfg.database.type == "mysql2";
40   pgsqlLocal = cfg.database.createLocally && cfg.database.type == "postgresql";
44   imports = [
45     (mkRemovedOptionModule [ "services" "redmine" "extraConfig" ] "Use services.redmine.settings instead.")
46     (mkRemovedOptionModule [ "services" "redmine" "database" "password" ] "Use services.redmine.database.passwordFile instead.")
47   ];
49   # interface
50   options = {
51     services.redmine = {
52       enable = mkEnableOption (lib.mdDoc "Redmine");
54       package = mkOption {
55         type = types.package;
56         default = pkgs.redmine;
57         defaultText = literalExpression "pkgs.redmine";
58         description = lib.mdDoc "Which Redmine package to use.";
59         example = literalExpression "pkgs.redmine.override { ruby = pkgs.ruby_2_7; }";
60       };
62       user = mkOption {
63         type = types.str;
64         default = "redmine";
65         description = lib.mdDoc "User under which Redmine is ran.";
66       };
68       group = mkOption {
69         type = types.str;
70         default = "redmine";
71         description = lib.mdDoc "Group under which Redmine is ran.";
72       };
74       port = mkOption {
75         type = types.port;
76         default = 3000;
77         description = lib.mdDoc "Port on which Redmine is ran.";
78       };
80       stateDir = mkOption {
81         type = types.str;
82         default = "/var/lib/redmine";
83         description = lib.mdDoc "The state directory, logs and plugins are stored here.";
84       };
86       settings = mkOption {
87         type = format.type;
88         default = {};
89         description = lib.mdDoc ''
90           Redmine configuration ({file}`configuration.yml`). Refer to
91           <https://guides.rubyonrails.org/action_mailer_basics.html#action-mailer-configuration>
92           for details.
93         '';
94         example = literalExpression ''
95           {
96             email_delivery = {
97               delivery_method = "smtp";
98               smtp_settings = {
99                 address = "mail.example.com";
100                 port = 25;
101               };
102             };
103           }
104         '';
105       };
107       extraEnv = mkOption {
108         type = types.lines;
109         default = "";
110         description = lib.mdDoc ''
111           Extra configuration in additional_environment.rb.
113           See <https://svn.redmine.org/redmine/trunk/config/additional_environment.rb.example>
114           for details.
115         '';
116         example = ''
117           config.logger.level = Logger::DEBUG
118         '';
119       };
121       themes = mkOption {
122         type = types.attrsOf types.path;
123         default = {};
124         description = lib.mdDoc "Set of themes.";
125         example = literalExpression ''
126           {
127             dkuk-redmine_alex_skin = builtins.fetchurl {
128               url = "https://bitbucket.org/dkuk/redmine_alex_skin/get/1842ef675ef3.zip";
129               sha256 = "0hrin9lzyi50k4w2bd2b30vrf1i4fi1c0gyas5801wn8i7kpm9yl";
130             };
131           }
132         '';
133       };
135       plugins = mkOption {
136         type = types.attrsOf types.path;
137         default = {};
138         description = lib.mdDoc "Set of plugins.";
139         example = literalExpression ''
140           {
141             redmine_env_auth = builtins.fetchurl {
142               url = "https://github.com/Intera/redmine_env_auth/archive/0.6.zip";
143               sha256 = "0yyr1yjd8gvvh832wdc8m3xfnhhxzk2pk3gm2psg5w9jdvd6skak";
144             };
145           }
146         '';
147       };
149       database = {
150         type = mkOption {
151           type = types.enum [ "mysql2" "postgresql" ];
152           example = "postgresql";
153           default = "mysql2";
154           description = lib.mdDoc "Database engine to use.";
155         };
157         host = mkOption {
158           type = types.str;
159           default = "localhost";
160           description = lib.mdDoc "Database host address.";
161         };
163         port = mkOption {
164           type = types.port;
165           default = if cfg.database.type == "postgresql" then 5432 else 3306;
166           defaultText = literalExpression "3306";
167           description = lib.mdDoc "Database host port.";
168         };
170         name = mkOption {
171           type = types.str;
172           default = "redmine";
173           description = lib.mdDoc "Database name.";
174         };
176         user = mkOption {
177           type = types.str;
178           default = "redmine";
179           description = lib.mdDoc "Database user.";
180         };
182         passwordFile = mkOption {
183           type = types.nullOr types.path;
184           default = null;
185           example = "/run/keys/redmine-dbpassword";
186           description = lib.mdDoc ''
187             A file containing the password corresponding to
188             {option}`database.user`.
189           '';
190         };
192         socket = mkOption {
193           type = types.nullOr types.path;
194           default =
195             if mysqlLocal then "/run/mysqld/mysqld.sock"
196             else if pgsqlLocal then "/run/postgresql"
197             else null;
198           defaultText = literalExpression "/run/mysqld/mysqld.sock";
199           example = "/run/mysqld/mysqld.sock";
200           description = lib.mdDoc "Path to the unix socket file to use for authentication.";
201         };
203         createLocally = mkOption {
204           type = types.bool;
205           default = true;
206           description = lib.mdDoc "Create the database and database user locally.";
207         };
208       };
209     };
210   };
212   # implementation
213   config = mkIf cfg.enable {
215     assertions = [
216       { assertion = cfg.database.passwordFile != null || cfg.database.socket != null;
217         message = "one of services.redmine.database.socket or services.redmine.database.passwordFile must be set";
218       }
219       { assertion = cfg.database.createLocally -> cfg.database.user == cfg.user;
220         message = "services.redmine.database.user must be set to ${cfg.user} if services.redmine.database.createLocally is set true";
221       }
222       { assertion = cfg.database.createLocally -> cfg.database.socket != null;
223         message = "services.redmine.database.socket must be set if services.redmine.database.createLocally is set to true";
224       }
225       { assertion = cfg.database.createLocally -> cfg.database.host == "localhost";
226         message = "services.redmine.database.host must be set to localhost if services.redmine.database.createLocally is set to true";
227       }
228     ];
230     services.redmine.settings = {
231       production = {
232         scm_subversion_command = "${pkgs.subversion}/bin/svn";
233         scm_mercurial_command = "${pkgs.mercurial}/bin/hg";
234         scm_git_command = "${pkgs.git}/bin/git";
235         scm_cvs_command = "${pkgs.cvs}/bin/cvs";
236         scm_bazaar_command = "${pkgs.breezy}/bin/bzr";
237         scm_darcs_command = "${pkgs.darcs}/bin/darcs";
238       };
239     };
241     services.redmine.extraEnv = mkBefore ''
242       config.logger = Logger.new("${cfg.stateDir}/log/production.log", 14, 1048576)
243       config.logger.level = Logger::INFO
244     '';
246     services.mysql = mkIf mysqlLocal {
247       enable = true;
248       package = mkDefault pkgs.mariadb;
249       ensureDatabases = [ cfg.database.name ];
250       ensureUsers = [
251         { name = cfg.database.user;
252           ensurePermissions = { "${cfg.database.name}.*" = "ALL PRIVILEGES"; };
253         }
254       ];
255     };
257     services.postgresql = mkIf pgsqlLocal {
258       enable = true;
259       ensureDatabases = [ cfg.database.name ];
260       ensureUsers = [
261         { name = cfg.database.user;
262           ensurePermissions = { "DATABASE ${cfg.database.name}" = "ALL PRIVILEGES"; };
263         }
264       ];
265     };
267     # create symlinks for the basic directory layout the redmine package expects
268     systemd.tmpfiles.rules = [
269       "d '${cfg.stateDir}' 0750 ${cfg.user} ${cfg.group} - -"
270       "d '${cfg.stateDir}/cache' 0750 ${cfg.user} ${cfg.group} - -"
271       "d '${cfg.stateDir}/config' 0750 ${cfg.user} ${cfg.group} - -"
272       "d '${cfg.stateDir}/files' 0750 ${cfg.user} ${cfg.group} - -"
273       "d '${cfg.stateDir}/log' 0750 ${cfg.user} ${cfg.group} - -"
274       "d '${cfg.stateDir}/plugins' 0750 ${cfg.user} ${cfg.group} - -"
275       "d '${cfg.stateDir}/public' 0750 ${cfg.user} ${cfg.group} - -"
276       "d '${cfg.stateDir}/public/plugin_assets' 0750 ${cfg.user} ${cfg.group} - -"
277       "d '${cfg.stateDir}/public/themes' 0750 ${cfg.user} ${cfg.group} - -"
278       "d '${cfg.stateDir}/tmp' 0750 ${cfg.user} ${cfg.group} - -"
280       "d /run/redmine - - - - -"
281       "d /run/redmine/public - - - - -"
282       "L+ /run/redmine/config - - - - ${cfg.stateDir}/config"
283       "L+ /run/redmine/files - - - - ${cfg.stateDir}/files"
284       "L+ /run/redmine/log - - - - ${cfg.stateDir}/log"
285       "L+ /run/redmine/plugins - - - - ${cfg.stateDir}/plugins"
286       "L+ /run/redmine/public/plugin_assets - - - - ${cfg.stateDir}/public/plugin_assets"
287       "L+ /run/redmine/public/themes - - - - ${cfg.stateDir}/public/themes"
288       "L+ /run/redmine/tmp - - - - ${cfg.stateDir}/tmp"
289     ];
291     systemd.services.redmine = {
292       after = [ "network.target" ] ++ optional mysqlLocal "mysql.service" ++ optional pgsqlLocal "postgresql.service";
293       wantedBy = [ "multi-user.target" ];
294       environment.RAILS_ENV = "production";
295       environment.RAILS_CACHE = "${cfg.stateDir}/cache";
296       environment.REDMINE_LANG = "en";
297       environment.SCHEMA = "${cfg.stateDir}/cache/schema.db";
298       path = with pkgs; [
299         imagemagick
300         breezy
301         cvs
302         darcs
303         git
304         mercurial
305         subversion
306       ];
307       preStart = ''
308         rm -rf "${cfg.stateDir}/plugins/"*
309         rm -rf "${cfg.stateDir}/public/themes/"*
311         # start with a fresh config directory
312         # the config directory is copied instead of linked as some mutable data is stored in there
313         find "${cfg.stateDir}/config" ! -name "secret_token.rb" -type f -exec rm -f {} +
314         cp -r ${cfg.package}/share/redmine/config.dist/* "${cfg.stateDir}/config/"
316         chmod -R u+w "${cfg.stateDir}/config"
318         # link in the application configuration
319         ln -fs ${configurationYml} "${cfg.stateDir}/config/configuration.yml"
321         # link in the additional environment configuration
322         ln -fs ${additionalEnvironment} "${cfg.stateDir}/config/additional_environment.rb"
325         # link in all user specified themes
326         for theme in ${concatStringsSep " " (mapAttrsToList unpackTheme cfg.themes)}; do
327           ln -fs $theme/* "${cfg.stateDir}/public/themes"
328         done
330         # link in redmine provided themes
331         ln -sf ${cfg.package}/share/redmine/public/themes.dist/* "${cfg.stateDir}/public/themes/"
334         # link in all user specified plugins
335         for plugin in ${concatStringsSep " " (mapAttrsToList unpackPlugin cfg.plugins)}; do
336           ln -fs $plugin/* "${cfg.stateDir}/plugins/''${plugin##*-redmine-plugin-}"
337         done
340         # handle database.passwordFile & permissions
341         DBPASS=${optionalString (cfg.database.passwordFile != null) "$(head -n1 ${cfg.database.passwordFile})"}
342         cp -f ${databaseYml} "${cfg.stateDir}/config/database.yml"
343         sed -e "s,#dbpass#,$DBPASS,g" -i "${cfg.stateDir}/config/database.yml"
344         chmod 440 "${cfg.stateDir}/config/database.yml"
347         # generate a secret token if required
348         if ! test -e "${cfg.stateDir}/config/initializers/secret_token.rb"; then
349           ${bundle} exec rake generate_secret_token
350           chmod 440 "${cfg.stateDir}/config/initializers/secret_token.rb"
351         fi
353         # execute redmine required commands prior to starting the application
354         ${bundle} exec rake db:migrate
355         ${bundle} exec rake redmine:plugins:migrate
356         ${bundle} exec rake redmine:load_default_data
357       '';
359       serviceConfig = {
360         Type = "simple";
361         User = cfg.user;
362         Group = cfg.group;
363         TimeoutSec = "300";
364         WorkingDirectory = "${cfg.package}/share/redmine";
365         ExecStart="${bundle} exec rails server webrick -e production -p ${toString cfg.port} -P '${cfg.stateDir}/redmine.pid'";
366       };
368     };
370     users.users = optionalAttrs (cfg.user == "redmine") {
371       redmine = {
372         group = cfg.group;
373         home = cfg.stateDir;
374         uid = config.ids.uids.redmine;
375       };
376     };
378     users.groups = optionalAttrs (cfg.group == "redmine") {
379       redmine.gid = config.ids.gids.redmine;
380     };
382   };