at 23.05-pre 14 kB view raw
1{ config, lib, pkgs, ... }: 2 3let 4 inherit (lib) mkBefore mkDefault mkEnableOption mkIf mkOption mkRemovedOptionModule types; 5 inherit (lib) concatStringsSep literalExpression mapAttrsToList; 6 inherit (lib) optional optionalAttrs optionalString; 7 8 cfg = config.services.redmine; 9 format = pkgs.formats.yaml {}; 10 bundle = "${cfg.package}/share/redmine/bin/bundle"; 11 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 ''; 22 23 configurationYml = format.generate "configuration.yml" cfg.settings; 24 additionalEnvironment = pkgs.writeText "additional_environment.rb" cfg.extraEnv; 25 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 }); 38 39 mysqlLocal = cfg.database.createLocally && cfg.database.type == "mysql2"; 40 pgsqlLocal = cfg.database.createLocally && cfg.database.type == "postgresql"; 41 42in 43{ 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 ]; 48 49 # interface 50 options = { 51 services.redmine = { 52 enable = mkEnableOption (lib.mdDoc "Redmine"); 53 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 }; 61 62 user = mkOption { 63 type = types.str; 64 default = "redmine"; 65 description = lib.mdDoc "User under which Redmine is ran."; 66 }; 67 68 group = mkOption { 69 type = types.str; 70 default = "redmine"; 71 description = lib.mdDoc "Group under which Redmine is ran."; 72 }; 73 74 port = mkOption { 75 type = types.port; 76 default = 3000; 77 description = lib.mdDoc "Port on which Redmine is ran."; 78 }; 79 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 }; 85 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 }; 106 107 extraEnv = mkOption { 108 type = types.lines; 109 default = ""; 110 description = lib.mdDoc '' 111 Extra configuration in additional_environment.rb. 112 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 }; 120 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 }; 134 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 }; 148 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 }; 156 157 host = mkOption { 158 type = types.str; 159 default = "localhost"; 160 description = lib.mdDoc "Database host address."; 161 }; 162 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 }; 169 170 name = mkOption { 171 type = types.str; 172 default = "redmine"; 173 description = lib.mdDoc "Database name."; 174 }; 175 176 user = mkOption { 177 type = types.str; 178 default = "redmine"; 179 description = lib.mdDoc "Database user."; 180 }; 181 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 }; 191 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 }; 202 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 }; 211 212 # implementation 213 config = mkIf cfg.enable { 214 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 ]; 229 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 }; 240 241 services.redmine.extraEnv = mkBefore '' 242 config.logger = Logger.new("${cfg.stateDir}/log/production.log", 14, 1048576) 243 config.logger.level = Logger::INFO 244 ''; 245 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 }; 256 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 }; 266 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} - -" 279 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 ]; 290 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/"* 310 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/" 315 316 chmod -R u+w "${cfg.stateDir}/config" 317 318 # link in the application configuration 319 ln -fs ${configurationYml} "${cfg.stateDir}/config/configuration.yml" 320 321 # link in the additional environment configuration 322 ln -fs ${additionalEnvironment} "${cfg.stateDir}/config/additional_environment.rb" 323 324 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 329 330 # link in redmine provided themes 331 ln -sf ${cfg.package}/share/redmine/public/themes.dist/* "${cfg.stateDir}/public/themes/" 332 333 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 338 339 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" 345 346 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 352 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 ''; 358 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 }; 367 368 }; 369 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 }; 377 378 users.groups = optionalAttrs (cfg.group == "redmine") { 379 redmine.gid = config.ids.gids.redmine; 380 }; 381 382 }; 383 384}