at 23.11-beta 16 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 components = { 211 subversion = mkOption { 212 type = types.bool; 213 default = false; 214 description = lib.mdDoc "Subversion integration."; 215 }; 216 217 mercurial = mkOption { 218 type = types.bool; 219 default = false; 220 description = lib.mdDoc "Mercurial integration."; 221 }; 222 223 git = mkOption { 224 type = types.bool; 225 default = false; 226 description = lib.mdDoc "git integration."; 227 }; 228 229 cvs = mkOption { 230 type = types.bool; 231 default = false; 232 description = lib.mdDoc "cvs integration."; 233 }; 234 235 breezy = mkOption { 236 type = types.bool; 237 default = false; 238 description = lib.mdDoc "bazaar integration."; 239 }; 240 241 imagemagick = mkOption { 242 type = types.bool; 243 default = false; 244 description = lib.mdDoc "Allows exporting Gant diagrams as PNG."; 245 }; 246 247 ghostscript = mkOption { 248 type = types.bool; 249 default = false; 250 description = lib.mdDoc "Allows exporting Gant diagrams as PDF."; 251 }; 252 253 minimagick_font_path = mkOption { 254 type = types.str; 255 default = ""; 256 description = lib.mdDoc "MiniMagick font path"; 257 example = "/run/current-system/sw/share/X11/fonts/LiberationSans-Regular.ttf"; 258 }; 259 }; 260 }; 261 }; 262 263 # implementation 264 config = mkIf cfg.enable { 265 266 assertions = [ 267 { assertion = cfg.database.passwordFile != null || cfg.database.socket != null; 268 message = "one of services.redmine.database.socket or services.redmine.database.passwordFile must be set"; 269 } 270 { assertion = cfg.database.createLocally -> cfg.database.user == cfg.user && cfg.database.user == cfg.database.name; 271 message = "services.redmine.database.user must be set to ${cfg.user} if services.redmine.database.createLocally is set true"; 272 } 273 { assertion = cfg.database.createLocally -> cfg.database.socket != null; 274 message = "services.redmine.database.socket must be set if services.redmine.database.createLocally is set to true"; 275 } 276 { assertion = cfg.database.createLocally -> cfg.database.host == "localhost"; 277 message = "services.redmine.database.host must be set to localhost if services.redmine.database.createLocally is set to true"; 278 } 279 { assertion = cfg.components.imagemagick -> cfg.components.minimagick_font_path != ""; 280 message = "services.redmine.components.minimagick_font_path must be configured with a path to a font file if services.redmine.components.imagemagick is set to true."; 281 } 282 ]; 283 284 services.redmine.settings = { 285 production = { 286 scm_subversion_command = optionalString cfg.components.subversion "${pkgs.subversion}/bin/svn"; 287 scm_mercurial_command = optionalString cfg.components.mercurial "${pkgs.mercurial}/bin/hg"; 288 scm_git_command = optionalString cfg.components.git "${pkgs.git}/bin/git"; 289 scm_cvs_command = optionalString cfg.components.cvs "${pkgs.cvs}/bin/cvs"; 290 scm_bazaar_command = optionalString cfg.components.breezy "${pkgs.breezy}/bin/bzr"; 291 imagemagick_convert_command = optionalString cfg.components.imagemagick "${pkgs.imagemagick}/bin/convert"; 292 gs_command = optionalString cfg.components.ghostscript "${pkgs.ghostscript}/bin/gs"; 293 minimagick_font_path = "${cfg.components.minimagick_font_path}"; 294 }; 295 }; 296 297 services.redmine.extraEnv = mkBefore '' 298 config.logger = Logger.new("${cfg.stateDir}/log/production.log", 14, 1048576) 299 config.logger.level = Logger::INFO 300 ''; 301 302 services.mysql = mkIf mysqlLocal { 303 enable = true; 304 package = mkDefault pkgs.mariadb; 305 ensureDatabases = [ cfg.database.name ]; 306 ensureUsers = [ 307 { name = cfg.database.user; 308 ensurePermissions = { "${cfg.database.name}.*" = "ALL PRIVILEGES"; }; 309 } 310 ]; 311 }; 312 313 services.postgresql = mkIf pgsqlLocal { 314 enable = true; 315 ensureDatabases = [ cfg.database.name ]; 316 ensureUsers = [ 317 { name = cfg.database.user; 318 ensureDBOwnership = true; 319 } 320 ]; 321 }; 322 323 # create symlinks for the basic directory layout the redmine package expects 324 systemd.tmpfiles.rules = [ 325 "d '${cfg.stateDir}' 0750 ${cfg.user} ${cfg.group} - -" 326 "d '${cfg.stateDir}/cache' 0750 ${cfg.user} ${cfg.group} - -" 327 "d '${cfg.stateDir}/config' 0750 ${cfg.user} ${cfg.group} - -" 328 "d '${cfg.stateDir}/files' 0750 ${cfg.user} ${cfg.group} - -" 329 "d '${cfg.stateDir}/log' 0750 ${cfg.user} ${cfg.group} - -" 330 "d '${cfg.stateDir}/plugins' 0750 ${cfg.user} ${cfg.group} - -" 331 "d '${cfg.stateDir}/public' 0750 ${cfg.user} ${cfg.group} - -" 332 "d '${cfg.stateDir}/public/plugin_assets' 0750 ${cfg.user} ${cfg.group} - -" 333 "d '${cfg.stateDir}/public/themes' 0750 ${cfg.user} ${cfg.group} - -" 334 "d '${cfg.stateDir}/tmp' 0750 ${cfg.user} ${cfg.group} - -" 335 336 "d /run/redmine - - - - -" 337 "d /run/redmine/public - - - - -" 338 "L+ /run/redmine/config - - - - ${cfg.stateDir}/config" 339 "L+ /run/redmine/files - - - - ${cfg.stateDir}/files" 340 "L+ /run/redmine/log - - - - ${cfg.stateDir}/log" 341 "L+ /run/redmine/plugins - - - - ${cfg.stateDir}/plugins" 342 "L+ /run/redmine/public/plugin_assets - - - - ${cfg.stateDir}/public/plugin_assets" 343 "L+ /run/redmine/public/themes - - - - ${cfg.stateDir}/public/themes" 344 "L+ /run/redmine/tmp - - - - ${cfg.stateDir}/tmp" 345 ]; 346 347 systemd.services.redmine = { 348 after = [ "network.target" ] ++ optional mysqlLocal "mysql.service" ++ optional pgsqlLocal "postgresql.service"; 349 wantedBy = [ "multi-user.target" ]; 350 environment.RAILS_ENV = "production"; 351 environment.RAILS_CACHE = "${cfg.stateDir}/cache"; 352 environment.REDMINE_LANG = "en"; 353 environment.SCHEMA = "${cfg.stateDir}/cache/schema.db"; 354 path = with pkgs; [ 355 ] 356 ++ optional cfg.components.subversion subversion 357 ++ optional cfg.components.mercurial mercurial 358 ++ optional cfg.components.git git 359 ++ optional cfg.components.cvs cvs 360 ++ optional cfg.components.breezy breezy 361 ++ optional cfg.components.imagemagick imagemagick 362 ++ optional cfg.components.ghostscript ghostscript; 363 364 preStart = '' 365 rm -rf "${cfg.stateDir}/plugins/"* 366 rm -rf "${cfg.stateDir}/public/themes/"* 367 368 # start with a fresh config directory 369 # the config directory is copied instead of linked as some mutable data is stored in there 370 find "${cfg.stateDir}/config" ! -name "secret_token.rb" -type f -exec rm -f {} + 371 cp -r ${cfg.package}/share/redmine/config.dist/* "${cfg.stateDir}/config/" 372 373 chmod -R u+w "${cfg.stateDir}/config" 374 375 # link in the application configuration 376 ln -fs ${configurationYml} "${cfg.stateDir}/config/configuration.yml" 377 378 # link in the additional environment configuration 379 ln -fs ${additionalEnvironment} "${cfg.stateDir}/config/additional_environment.rb" 380 381 382 # link in all user specified themes 383 for theme in ${concatStringsSep " " (mapAttrsToList unpackTheme cfg.themes)}; do 384 ln -fs $theme/* "${cfg.stateDir}/public/themes" 385 done 386 387 # link in redmine provided themes 388 ln -sf ${cfg.package}/share/redmine/public/themes.dist/* "${cfg.stateDir}/public/themes/" 389 390 391 # link in all user specified plugins 392 for plugin in ${concatStringsSep " " (mapAttrsToList unpackPlugin cfg.plugins)}; do 393 ln -fs $plugin/* "${cfg.stateDir}/plugins/''${plugin##*-redmine-plugin-}" 394 done 395 396 397 # handle database.passwordFile & permissions 398 DBPASS=${optionalString (cfg.database.passwordFile != null) "$(head -n1 ${cfg.database.passwordFile})"} 399 cp -f ${databaseYml} "${cfg.stateDir}/config/database.yml" 400 sed -e "s,#dbpass#,$DBPASS,g" -i "${cfg.stateDir}/config/database.yml" 401 chmod 440 "${cfg.stateDir}/config/database.yml" 402 403 404 # generate a secret token if required 405 if ! test -e "${cfg.stateDir}/config/initializers/secret_token.rb"; then 406 ${bundle} exec rake generate_secret_token 407 chmod 440 "${cfg.stateDir}/config/initializers/secret_token.rb" 408 fi 409 410 # execute redmine required commands prior to starting the application 411 ${bundle} exec rake db:migrate 412 ${bundle} exec rake redmine:plugins:migrate 413 ${bundle} exec rake redmine:load_default_data 414 ''; 415 416 serviceConfig = { 417 Type = "simple"; 418 User = cfg.user; 419 Group = cfg.group; 420 TimeoutSec = "300"; 421 WorkingDirectory = "${cfg.package}/share/redmine"; 422 ExecStart="${bundle} exec rails server -u webrick -e production -p ${toString cfg.port} -P '${cfg.stateDir}/redmine.pid'"; 423 }; 424 425 }; 426 427 users.users = optionalAttrs (cfg.user == "redmine") { 428 redmine = { 429 group = cfg.group; 430 home = cfg.stateDir; 431 uid = config.ids.uids.redmine; 432 }; 433 }; 434 435 users.groups = optionalAttrs (cfg.group == "redmine") { 436 redmine.gid = config.ids.gids.redmine; 437 }; 438 439 }; 440 441}