at master 17 kB view raw
1{ 2 config, 3 lib, 4 pkgs, 5 ... 6}: 7 8let 9 cfg = config.services.redmine; 10 format = pkgs.formats.yaml { }; 11 bundle = "${cfg.package}/share/redmine/bin/bundle"; 12 13 databaseSettings = { 14 production = { 15 adapter = cfg.database.type; 16 database = 17 if cfg.database.type == "sqlite3" then "${cfg.stateDir}/database.sqlite3" else cfg.database.name; 18 } 19 // lib.optionalAttrs (cfg.database.type != "sqlite3") { 20 host = 21 if (cfg.database.type == "postgresql" && cfg.database.socket != null) then 22 cfg.database.socket 23 else 24 cfg.database.host; 25 port = cfg.database.port; 26 username = cfg.database.user; 27 } 28 // lib.optionalAttrs (cfg.database.type != "sqlite3" && cfg.database.passwordFile != null) { 29 password = "#dbpass#"; 30 } 31 // lib.optionalAttrs (cfg.database.type == "mysql2" && cfg.database.socket != null) { 32 socket = cfg.database.socket; 33 }; 34 }; 35 36 databaseYml = format.generate "database.yml" databaseSettings; 37 38 configurationYml = format.generate "configuration.yml" cfg.settings; 39 additionalEnvironment = pkgs.writeText "additional_environment.rb" cfg.extraEnv; 40 41 unpackTheme = unpack "theme"; 42 unpackPlugin = unpack "plugin"; 43 unpack = 44 id: 45 ( 46 name: source: 47 pkgs.stdenv.mkDerivation { 48 name = "redmine-${id}-${name}"; 49 nativeBuildInputs = [ pkgs.unzip ]; 50 buildCommand = '' 51 mkdir -p $out 52 cd $out 53 unpackFile ${source} 54 ''; 55 } 56 ); 57 58 mysqlLocal = cfg.database.createLocally && cfg.database.type == "mysql2"; 59 pgsqlLocal = cfg.database.createLocally && cfg.database.type == "postgresql"; 60 61in 62{ 63 imports = [ 64 (lib.mkRemovedOptionModule [ 65 "services" 66 "redmine" 67 "extraConfig" 68 ] "Use services.redmine.settings instead.") 69 (lib.mkRemovedOptionModule [ 70 "services" 71 "redmine" 72 "database" 73 "password" 74 ] "Use services.redmine.database.passwordFile instead.") 75 ]; 76 77 # interface 78 options = { 79 services.redmine = { 80 enable = lib.mkEnableOption "Redmine, a project management web application"; 81 82 package = lib.mkPackageOption pkgs "redmine" { 83 example = "redmine.override { ruby = pkgs.ruby_3_2; }"; 84 }; 85 86 user = lib.mkOption { 87 type = lib.types.str; 88 default = "redmine"; 89 description = "User under which Redmine is ran."; 90 }; 91 92 group = lib.mkOption { 93 type = lib.types.str; 94 default = "redmine"; 95 description = "Group under which Redmine is ran."; 96 }; 97 98 address = lib.mkOption { 99 type = lib.types.str; 100 default = "0.0.0.0"; 101 description = "IP address Redmine should bind to."; 102 }; 103 104 port = lib.mkOption { 105 type = lib.types.port; 106 default = 3000; 107 description = "Port on which Redmine is ran."; 108 }; 109 110 stateDir = lib.mkOption { 111 type = lib.types.path; 112 default = "/var/lib/redmine"; 113 description = "The state directory, logs and plugins are stored here."; 114 }; 115 116 settings = lib.mkOption { 117 type = format.type; 118 default = { }; 119 description = '' 120 Redmine configuration ({file}`configuration.yml`). Refer to 121 <https://guides.rubyonrails.org/action_mailer_basics.html#action-mailer-configuration> 122 for details. 123 ''; 124 example = lib.literalExpression '' 125 { 126 email_delivery = { 127 delivery_method = "smtp"; 128 smtp_settings = { 129 address = "mail.example.com"; 130 port = 25; 131 }; 132 }; 133 } 134 ''; 135 }; 136 137 extraEnv = lib.mkOption { 138 type = lib.types.lines; 139 default = ""; 140 description = '' 141 Extra configuration in additional_environment.rb. 142 143 See <https://svn.redmine.org/redmine/trunk/config/additional_environment.rb.example> 144 for details. 145 ''; 146 example = '' 147 config.logger.level = Logger::DEBUG 148 ''; 149 }; 150 151 themes = lib.mkOption { 152 type = lib.types.attrsOf lib.types.path; 153 default = { }; 154 description = "Set of themes."; 155 example = lib.literalExpression '' 156 { 157 dkuk-redmine_alex_skin = builtins.fetchurl { 158 url = "https://bitbucket.org/dkuk/redmine_alex_skin/get/1842ef675ef3.zip"; 159 sha256 = "0hrin9lzyi50k4w2bd2b30vrf1i4fi1c0gyas5801wn8i7kpm9yl"; 160 }; 161 } 162 ''; 163 }; 164 165 plugins = lib.mkOption { 166 type = lib.types.attrsOf lib.types.path; 167 default = { }; 168 description = "Set of plugins."; 169 example = lib.literalExpression '' 170 { 171 redmine_env_auth = builtins.fetchurl { 172 url = "https://github.com/Intera/redmine_env_auth/archive/0.6.zip"; 173 sha256 = "0yyr1yjd8gvvh832wdc8m3xfnhhxzk2pk3gm2psg5w9jdvd6skak"; 174 }; 175 } 176 ''; 177 }; 178 179 database = { 180 type = lib.mkOption { 181 type = lib.types.enum [ 182 "mysql2" 183 "postgresql" 184 "sqlite3" 185 ]; 186 example = "postgresql"; 187 default = "mysql2"; 188 description = "Database engine to use."; 189 }; 190 191 host = lib.mkOption { 192 type = lib.types.str; 193 default = "localhost"; 194 description = "Database host address."; 195 }; 196 197 port = lib.mkOption { 198 type = lib.types.port; 199 default = if cfg.database.type == "postgresql" then 5432 else 3306; 200 defaultText = lib.literalExpression "3306"; 201 description = "Database host port."; 202 }; 203 204 name = lib.mkOption { 205 type = lib.types.str; 206 default = "redmine"; 207 description = "Database name."; 208 }; 209 210 user = lib.mkOption { 211 type = lib.types.str; 212 default = "redmine"; 213 description = "Database user."; 214 }; 215 216 passwordFile = lib.mkOption { 217 type = lib.types.nullOr lib.types.path; 218 default = null; 219 example = "/run/keys/redmine-dbpassword"; 220 description = '' 221 A file containing the password corresponding to 222 {option}`database.user`. 223 ''; 224 }; 225 226 socket = lib.mkOption { 227 type = lib.types.nullOr lib.types.path; 228 default = 229 if mysqlLocal then 230 "/run/mysqld/mysqld.sock" 231 else if pgsqlLocal then 232 "/run/postgresql" 233 else 234 null; 235 defaultText = lib.literalExpression "/run/mysqld/mysqld.sock"; 236 example = "/run/mysqld/mysqld.sock"; 237 description = "Path to the unix socket file to use for authentication."; 238 }; 239 240 createLocally = lib.mkOption { 241 type = lib.types.bool; 242 default = true; 243 description = "Create the database and database user locally."; 244 }; 245 }; 246 247 components = { 248 subversion = lib.mkEnableOption "Subversion integration."; 249 250 mercurial = lib.mkEnableOption "Mercurial integration."; 251 252 git = lib.mkEnableOption "git integration."; 253 254 cvs = lib.mkEnableOption "cvs integration."; 255 256 breezy = lib.mkEnableOption "bazaar integration."; 257 258 imagemagick = lib.mkEnableOption "exporting Gant diagrams as PNG."; 259 260 ghostscript = lib.mkEnableOption "exporting Gant diagrams as PDF."; 261 262 minimagick_font_path = lib.mkOption { 263 type = lib.types.str; 264 default = ""; 265 description = "MiniMagick font path"; 266 example = "/run/current-system/sw/share/X11/fonts/LiberationSans-Regular.ttf"; 267 }; 268 }; 269 }; 270 }; 271 272 # implementation 273 config = lib.mkIf cfg.enable { 274 assertions = [ 275 { 276 assertion = 277 cfg.database.type != "sqlite3" -> cfg.database.passwordFile != null || cfg.database.socket != null; 278 message = "one of services.redmine.database.socket or services.redmine.database.passwordFile must be set"; 279 } 280 { 281 assertion = cfg.database.createLocally -> cfg.database.user == cfg.user; 282 message = "services.redmine.database.user must be set to ${cfg.user} if services.redmine.database.createLocally is set true"; 283 } 284 { 285 assertion = pgsqlLocal -> cfg.database.user == cfg.database.name; 286 message = "services.redmine.database.user and services.redmine.database.name must be the same when using a local postgresql database"; 287 } 288 { 289 assertion = 290 (cfg.database.createLocally && cfg.database.type != "sqlite3") -> cfg.database.socket != null; 291 message = "services.redmine.database.socket must be set if services.redmine.database.createLocally is set to true and no sqlite database is used"; 292 } 293 { 294 assertion = cfg.database.createLocally -> cfg.database.host == "localhost"; 295 message = "services.redmine.database.host must be set to localhost if services.redmine.database.createLocally is set to true"; 296 } 297 { 298 assertion = cfg.components.imagemagick -> cfg.components.minimagick_font_path != ""; 299 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."; 300 } 301 ]; 302 303 services.redmine.settings = { 304 production = { 305 scm_subversion_command = lib.optionalString cfg.components.subversion "${pkgs.subversion}/bin/svn"; 306 scm_mercurial_command = lib.optionalString cfg.components.mercurial "${pkgs.mercurial}/bin/hg"; 307 scm_git_command = lib.optionalString cfg.components.git "${pkgs.git}/bin/git"; 308 scm_cvs_command = lib.optionalString cfg.components.cvs "${pkgs.cvs}/bin/cvs"; 309 scm_bazaar_command = lib.optionalString cfg.components.breezy "${pkgs.breezy}/bin/bzr"; 310 imagemagick_convert_command = lib.optionalString cfg.components.imagemagick "${pkgs.imagemagick}/bin/convert"; 311 gs_command = lib.optionalString cfg.components.ghostscript "${pkgs.ghostscript}/bin/gs"; 312 minimagick_font_path = "${cfg.components.minimagick_font_path}"; 313 }; 314 }; 315 316 services.redmine.extraEnv = lib.mkBefore '' 317 config.logger = Logger.new("${cfg.stateDir}/log/production.log", 14, 1048576) 318 config.logger.level = Logger::INFO 319 ''; 320 321 services.mysql = lib.mkIf mysqlLocal { 322 enable = true; 323 package = lib.mkDefault pkgs.mariadb; 324 ensureDatabases = [ cfg.database.name ]; 325 ensureUsers = [ 326 { 327 name = cfg.database.user; 328 ensurePermissions = { 329 "${cfg.database.name}.*" = "ALL PRIVILEGES"; 330 }; 331 } 332 ]; 333 }; 334 335 services.postgresql = lib.mkIf pgsqlLocal { 336 enable = true; 337 ensureDatabases = [ cfg.database.name ]; 338 ensureUsers = [ 339 { 340 name = cfg.database.user; 341 ensureDBOwnership = true; 342 } 343 ]; 344 }; 345 346 # create symlinks for the basic directory layout the redmine package expects 347 systemd.tmpfiles.rules = [ 348 "d '${cfg.stateDir}' 0750 ${cfg.user} ${cfg.group} - -" 349 "d '${cfg.stateDir}/cache' 0750 ${cfg.user} ${cfg.group} - -" 350 "d '${cfg.stateDir}/config' 0750 ${cfg.user} ${cfg.group} - -" 351 "d '${cfg.stateDir}/files' 0750 ${cfg.user} ${cfg.group} - -" 352 "d '${cfg.stateDir}/log' 0750 ${cfg.user} ${cfg.group} - -" 353 "d '${cfg.stateDir}/plugins' 0750 ${cfg.user} ${cfg.group} - -" 354 "d '${cfg.stateDir}/public' 0750 ${cfg.user} ${cfg.group} - -" 355 "d '${cfg.stateDir}/public/assets' 0750 ${cfg.user} ${cfg.group} - -" 356 "d '${cfg.stateDir}/public/plugin_assets' 0750 ${cfg.user} ${cfg.group} - -" 357 "d '${cfg.stateDir}/themes' 0750 ${cfg.user} ${cfg.group} - -" 358 "d '${cfg.stateDir}/tmp' 0750 ${cfg.user} ${cfg.group} - -" 359 360 "d /run/redmine/public - - - - -" 361 "L+ /run/redmine/config - - - - ${cfg.stateDir}/config" 362 "L+ /run/redmine/files - - - - ${cfg.stateDir}/files" 363 "L+ /run/redmine/log - - - - ${cfg.stateDir}/log" 364 "L+ /run/redmine/plugins - - - - ${cfg.stateDir}/plugins" 365 "L+ /run/redmine/public/assets - - - - ${cfg.stateDir}/public/assets" 366 "L+ /run/redmine/public/plugin_assets - - - - ${cfg.stateDir}/public/plugin_assets" 367 "L+ /run/redmine/themes - - - - ${cfg.stateDir}/themes" 368 "L+ /run/redmine/tmp - - - - ${cfg.stateDir}/tmp" 369 ]; 370 371 systemd.services.redmine = { 372 after = [ 373 "network.target" 374 ] 375 ++ lib.optional mysqlLocal "mysql.service" 376 ++ lib.optional pgsqlLocal "postgresql.target"; 377 wantedBy = [ "multi-user.target" ]; 378 environment.RAILS_ENV = "production"; 379 environment.RAILS_CACHE = "${cfg.stateDir}/cache"; 380 environment.REDMINE_LANG = "en"; 381 environment.SCHEMA = "${cfg.stateDir}/cache/schema.db"; 382 path = 383 with pkgs; 384 [ 385 ] 386 ++ lib.optional cfg.components.subversion subversion 387 ++ lib.optional cfg.components.mercurial mercurial 388 ++ lib.optional cfg.components.git git 389 ++ lib.optional cfg.components.cvs cvs 390 ++ lib.optional cfg.components.breezy breezy 391 ++ lib.optional cfg.components.imagemagick imagemagick 392 ++ lib.optional cfg.components.ghostscript ghostscript; 393 394 preStart = '' 395 rm -rf "${cfg.stateDir}/plugins/"* 396 rm -rf "${cfg.stateDir}/themes/"* 397 398 # start with a fresh config directory 399 # the config directory is copied instead of linked as some mutable data is stored in there 400 find "${cfg.stateDir}/config" ! -name "secret_token.rb" -type f -exec rm -f {} + 401 cp -r ${cfg.package}/share/redmine/config.dist/* "${cfg.stateDir}/config/" 402 403 chmod -R u+w "${cfg.stateDir}/config" 404 405 # link in the application configuration 406 ln -fs ${configurationYml} "${cfg.stateDir}/config/configuration.yml" 407 408 # link in the additional environment configuration 409 ln -fs ${additionalEnvironment} "${cfg.stateDir}/config/additional_environment.rb" 410 411 412 # link in all user specified themes 413 for theme in ${lib.concatStringsSep " " (lib.mapAttrsToList unpackTheme cfg.themes)}; do 414 ln -fs $theme/* "${cfg.stateDir}/themes" 415 done 416 417 # link in redmine provided themes 418 ln -sf ${cfg.package}/share/redmine/themes.dist/* "${cfg.stateDir}/themes/" 419 420 421 # link in all user specified plugins 422 for plugin in ${lib.concatStringsSep " " (lib.mapAttrsToList unpackPlugin cfg.plugins)}; do 423 ln -fs $plugin/* "${cfg.stateDir}/plugins/''${plugin##*-redmine-plugin-}" 424 done 425 426 427 # handle database.passwordFile & permissions 428 cp -f ${databaseYml} "${cfg.stateDir}/config/database.yml" 429 430 ${lib.optionalString ((cfg.database.type != "sqlite3") && (cfg.database.passwordFile != null)) '' 431 DBPASS="$(head -n1 ${cfg.database.passwordFile})" 432 sed -e "s,#dbpass#,$DBPASS,g" -i "${cfg.stateDir}/config/database.yml" 433 ''} 434 435 chmod 440 "${cfg.stateDir}/config/database.yml" 436 437 438 # generate a secret token if required 439 if ! test -e "${cfg.stateDir}/config/initializers/secret_token.rb"; then 440 ${bundle} exec rake generate_secret_token 441 chmod 440 "${cfg.stateDir}/config/initializers/secret_token.rb" 442 fi 443 444 # execute redmine required commands prior to starting the application 445 ${bundle} exec rake db:migrate 446 ${bundle} exec rake redmine:plugins:migrate 447 ${bundle} exec rake redmine:load_default_data 448 ${bundle} exec rake assets:precompile 449 ''; 450 451 serviceConfig = { 452 Type = "simple"; 453 User = cfg.user; 454 Group = cfg.group; 455 TimeoutSec = "300"; 456 WorkingDirectory = "${cfg.package}/share/redmine"; 457 ExecStart = "${bundle} exec rails server -u webrick -e production -b ${toString cfg.address} -p ${toString cfg.port} -P '${cfg.stateDir}/redmine.pid'"; 458 RuntimeDirectory = "redmine"; 459 RuntimeDirectoryMode = "0750"; 460 AmbientCapabilities = ""; 461 CapabilityBoundingSet = ""; 462 LockPersonality = true; 463 MemoryDenyWriteExecute = true; 464 NoNewPrivileges = true; 465 PrivateDevices = true; 466 PrivateMounts = true; 467 PrivateTmp = true; 468 ProcSubset = "pid"; 469 ProtectClock = true; 470 ProtectControlGroups = true; 471 ProtectHome = true; 472 ProtectHostname = true; 473 ProtectKernelLogs = true; 474 ProtectKernelModules = true; 475 ProtectKernelTunables = true; 476 ProtectProc = "noaccess"; 477 ProtectSystem = "strict"; 478 ReadWritePaths = [ 479 cfg.stateDir 480 ]; 481 RemoveIPC = true; 482 RestrictAddressFamilies = [ 483 "AF_UNIX" 484 "AF_INET" 485 ]; 486 RestrictNamespaces = true; 487 RestrictRealtime = true; 488 RestrictSUIDSGID = true; 489 SystemCallArchitectures = "native"; 490 UMask = 27; 491 }; 492 }; 493 494 users.users = lib.optionalAttrs (cfg.user == "redmine") { 495 redmine = { 496 group = cfg.group; 497 home = cfg.stateDir; 498 uid = config.ids.uids.redmine; 499 }; 500 }; 501 502 users.groups = lib.optionalAttrs (cfg.group == "redmine") { 503 redmine.gid = config.ids.gids.redmine; 504 }; 505 }; 506 507 meta.maintainers = with lib.maintainers; [ felixsinger ]; 508}