modules/koillection: init
Currently fails to build as the frontend can't be built (due to using Yarn's v2 lockfile format).
This commit is contained in:
		
							
								
								
									
										318
									
								
								modules/services/web-apps/koillection.nix
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										318
									
								
								modules/services/web-apps/koillection.nix
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,318 @@ | ||||
| { config, lib, modulesPath, options, pkgs, ... }: | ||||
| let | ||||
|   inherit (lib) literalExpression mkEnableOption mkIf mkOption optionalString types; | ||||
|  | ||||
|   opt = options.services.koillection; | ||||
|   cfg = config.services.koillection; | ||||
|   db = cfg.database; | ||||
|  | ||||
|   koillection = (pkgs.koillection or pkgs.callPackage ../../../pkgs/by-name/ko/koillection/package.nix { }).override { | ||||
|     dataDir = cfg.dataDir; | ||||
|   }; | ||||
|   inherit (koillection.passthru) phpPackage; | ||||
| in | ||||
| { | ||||
|   options.services.koillection = { | ||||
|     enable = mkEnableOption "Koillection, a collection manager"; | ||||
|  | ||||
|     user = mkOption { | ||||
|       type = types.str; | ||||
|       default = "koillection"; | ||||
|       description = lib.mdDoc "User Koillection runs as."; | ||||
|     }; | ||||
|  | ||||
|     group = mkOption { | ||||
|       type = types.str; | ||||
|       default = "koillection"; | ||||
|       description = lib.mdDoc "Group Koillection runs as."; | ||||
|     }; | ||||
|  | ||||
|     hostName = lib.mkOption { | ||||
|       type = types.str; | ||||
|       default = config.networking.fqdnOrHostName; | ||||
|       defaultText = "config.networking.fqdnOrHostName"; | ||||
|       example = "koillection.example.com"; | ||||
|       description = lib.mdDoc "The hostname to serve Koillection on."; | ||||
|     }; | ||||
|  | ||||
|     dataDir = mkOption { | ||||
|       description = lib.mdDoc "Koillection data directory"; | ||||
|       default = "/var/lib/koillection"; | ||||
|       type = types.path; | ||||
|     }; | ||||
|  | ||||
|     database = { | ||||
|       host = mkOption { | ||||
|         type = types.str; | ||||
|         default = if db.createLocally then "/run/postgresql" else null; | ||||
|         defaultText = literalExpression '' | ||||
|           if config.${opt.database.createLocally} | ||||
|           then "/run/postgresql" | ||||
|           else null | ||||
|         ''; | ||||
|         example = "192.168.12.85"; | ||||
|         description = lib.mdDoc "Database host address or unix socket."; | ||||
|       }; | ||||
|       port = mkOption { | ||||
|         type = types.port; | ||||
|         default = 5432; | ||||
|         description = lib.mdDoc "Database host port."; | ||||
|       }; | ||||
|       name = mkOption { | ||||
|         type = types.str; | ||||
|         default = "koillection"; | ||||
|         description = lib.mdDoc "Database name."; | ||||
|       }; | ||||
|       user = mkOption { | ||||
|         type = types.str; | ||||
|         default = cfg.user; | ||||
|         defaultText = literalExpression "user"; | ||||
|         description = lib.mdDoc "Database username."; | ||||
|       }; | ||||
|       passwordFile = mkOption { | ||||
|         type = with types; nullOr path; | ||||
|         default = null; | ||||
|         example = "/run/keys/koillection-dbpassword"; | ||||
|         description = lib.mdDoc '' | ||||
|           A file containing the password corresponding to | ||||
|           {option}`database.user`. | ||||
|         ''; | ||||
|       }; | ||||
|       createLocally = mkOption { | ||||
|         type = types.bool; | ||||
|         default = false; | ||||
|         description = lib.mdDoc "Create the database and database user locally."; | ||||
|       }; | ||||
|     }; | ||||
|  | ||||
|     poolConfig = mkOption { | ||||
|       type = with types; attrsOf (oneOf [ str int bool ]); | ||||
|       default = { | ||||
|         "pm" = "dynamic"; | ||||
|         "pm.max_children" = 32; | ||||
|         "pm.start_servers" = 2; | ||||
|         "pm.min_spare_servers" = 2; | ||||
|         "pm.max_spare_servers" = 4; | ||||
|         "pm.max_requests" = 500; | ||||
|       }; | ||||
|       description = lib.mdDoc '' | ||||
|         Options for the bookstack PHP pool. See the documentation on `php-fpm.conf` | ||||
|         for details on configuration directives. | ||||
|       ''; | ||||
|     }; | ||||
|  | ||||
|     nginx = mkOption { | ||||
|       type = types.submodule ( | ||||
|         lib.recursiveUpdate | ||||
|           (import "${modulesPath}/services/web-servers/nginx/vhost-options.nix" { inherit config lib; }) | ||||
|           { } | ||||
|       ); | ||||
|       default = { }; | ||||
|       example = literalExpression '' | ||||
|         { | ||||
|           serverAliases = [ | ||||
|             "koillection.''${config.networking.domain}" | ||||
|           ]; | ||||
|           # To enable encryption and let let's encrypt take care of certificate | ||||
|           forceSSL = true; | ||||
|           enableACME = true; | ||||
|         } | ||||
|       ''; | ||||
|       description = lib.mdDoc '' | ||||
|         With this option, you can customize the nginx virtualHost settings. | ||||
|       ''; | ||||
|     }; | ||||
|  | ||||
|     config = mkOption { | ||||
|       type = with types; | ||||
|         attrsOf | ||||
|           (nullOr | ||||
|             (either | ||||
|               (oneOf [ bool int port path str ]) | ||||
|               (submodule { | ||||
|                 options = { | ||||
|                   _secret = mkOption { | ||||
|                     type = nullOr str; | ||||
|                     description = lib.mdDoc '' | ||||
|                       The path to a file containing the value the option should | ||||
|                       be set to in the final configuration file. | ||||
|                     ''; | ||||
|                   }; | ||||
|                 }; | ||||
|               }))); | ||||
|       default = { }; | ||||
|       example = literalExpression '' | ||||
|       ''; | ||||
|     }; | ||||
|   }; | ||||
|  | ||||
|   config = mkIf cfg.enable { | ||||
|     services.koillection.config = { | ||||
|       APP_ENV = "prod"; | ||||
|       # DB_DRIVER = "pdo_pgsql"; | ||||
|       DB_USER = db.user; | ||||
|       DB_PASSWORD._secret = db.passwordFile; | ||||
|       DB_HOST = db.host; | ||||
|       DB_PORT = db.port; | ||||
|       DB_NAME = db.name; | ||||
|       # FIXME | ||||
|       # DB_VERSION = lib.versions.major config.services.postgresql.package.version; | ||||
|       DB_VERSION = "15"; | ||||
|       PHP_TZ = config.time.timeZone; | ||||
|     }; | ||||
|  | ||||
|     services.postgresql = mkIf db.createLocally { | ||||
|       enable = true; | ||||
|       ensureDatabases = [ db.name ]; | ||||
|       ensureUsers = [{ | ||||
|         name = db.user; | ||||
|         ensureDBOwnership = true; | ||||
|       }]; | ||||
|     }; | ||||
|  | ||||
|     services.phpfpm.pools.koillection = { | ||||
|       inherit (cfg) user group; | ||||
|       inherit phpPackage; | ||||
|       # Copied from docker/php.ini | ||||
|       phpOptions = '' | ||||
|         max_execution_time = 200 | ||||
|  | ||||
|         apc.enabled = 1 | ||||
|         apc.shm_size = 64M | ||||
|         apc.ttl = 7200 | ||||
|  | ||||
|         opcache.enable = 1 | ||||
|         opcache.memory_consumption = 256 | ||||
|         opcache.max_accelerated_files = 20000 | ||||
|         opcache.max_wasted_percentage = 10 | ||||
|         opcache.validate_timestamps = 0 | ||||
|         opcache.preload = ${koillection}/share/php/koillection/config/preload.php | ||||
|         opcache.preload_user = ${cfg.user} | ||||
|         expose_php = Off | ||||
|  | ||||
|         session.cookie_httponly = 1 | ||||
|         realpath_cache_size = 4096K | ||||
|         realpath_cache_ttle = 600 | ||||
|       ''; | ||||
|       settings = { | ||||
|         "listen.mode" = "0660"; | ||||
|         "listen.owner" = cfg.user; | ||||
|         "listen.group" = cfg.group; | ||||
|       } // cfg.poolConfig; | ||||
|     }; | ||||
|  | ||||
|     services.nginx = { | ||||
|       enable = lib.mkDefault true; | ||||
|       virtualHosts."${cfg.hostName}" = lib.mkMerge [ | ||||
|         cfg.nginx | ||||
|         { | ||||
|           root = lib.mkForce "${koillection}/share/php/koillection/public"; | ||||
|           extraConfig = optionalString (cfg.nginx.addSSL || cfg.nginx.forceSSL || cfg.nginx.onlySSL || cfg.nginx.enableACME) "fastcgi_param HTTPS on;"; | ||||
|           locations = { | ||||
|             "/" = { | ||||
|               index = "index.php"; | ||||
|               extraConfig = ''try_files $uri $uri/ /index.php?query_string;''; | ||||
|             }; | ||||
|             "~ \.php$" = { | ||||
|               extraConfig = '' | ||||
|                 try_files $uri $uri/ /index.php?$query_string; | ||||
|                 include ${config.services.nginx.package}/conf/fastcgi_params; | ||||
|                 fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; | ||||
|                 fastcgi_param REDIRECT_STATUS 200; | ||||
|                 fastcgi_pass unix:${config.services.phpfpm.pools."koillection".socket}; | ||||
|                 ${optionalString (cfg.nginx.addSSL || cfg.nginx.forceSSL || cfg.nginx.onlySSL || cfg.nginx.enableACME) "fastcgi_param HTTPS on;"} | ||||
|               ''; | ||||
|             }; | ||||
|             "~ \.(js|css|gif|png|ico|jpg|jpeg)$" = { | ||||
|               extraConfig = "expires 365d;"; | ||||
|             }; | ||||
|           }; | ||||
|         } | ||||
|       ]; | ||||
|     }; | ||||
|  | ||||
|     systemd.services.koillection-setup = { | ||||
|       description = "Preparation tasks for koillection"; | ||||
|       before = [ "phpfpm-koillection.service" ]; | ||||
|       wantedBy = [ "multi-user.target" ]; | ||||
|       serviceConfig = { | ||||
|         Type = "oneshot"; | ||||
|         RemainAfterExit = true; | ||||
|         User = cfg.user; | ||||
|         WorkingDirectory = koillection; | ||||
|         RuntimeDirectory = "koillection/cache"; | ||||
|         RuntimeDirectoryMode = "0700"; | ||||
|       }; | ||||
|       path = [ pkgs.replace-secret ]; | ||||
|       script = | ||||
|         let | ||||
|           isSecret = v: lib.isAttrs v && v ? _secret && lib.isString v._secret; | ||||
|           koillectionEnvVars = lib.generators.toKeyValue { | ||||
|             mkKeyValue = lib.flip lib.generators.mkKeyValueDefault "=" { | ||||
|               mkValueString = v: with builtins; | ||||
|                 if isInt v then toString v | ||||
|                 else if lib.isString v then v | ||||
|                 else if true == v then "true" | ||||
|                 else if false == v then "false" | ||||
|                 else if isSecret v then hashString "sha256" v._secret | ||||
|                 else throw "unsupported type ${typeOf v}: ${(lib.generators.toPretty {}) v}"; | ||||
|             }; | ||||
|           }; | ||||
|           secretPaths = lib.mapAttrsToList (_: v: v._secret) (lib.filterAttrs (_: isSecret) cfg.config); | ||||
|           mkSecretReplacement = file: '' | ||||
|             replace-secret ${lib.escapeShellArgs [ ( builtins.hashString "sha256" file ) file "${cfg.dataDir}/.env" ]} | ||||
|           ''; | ||||
|           secretReplacements = lib.concatMapStrings mkSecretReplacement secretPaths; | ||||
|           filteredConfig = lib.converge (lib.filterAttrsRecursive (_: v: ! builtins.elem v [{ } null])) cfg.config; | ||||
|           koillectionEnv = pkgs.writeText "koillection.env" (koillectionEnvVars filteredConfig); | ||||
|         in | ||||
|         '' | ||||
|           # error handling | ||||
|           set -euo pipefail | ||||
|  | ||||
|           # set permissions | ||||
|           umask 077 | ||||
|  | ||||
|           # create .env file | ||||
|           install -T -m 0600 -o ${cfg.user} ${koillectionEnv} "${cfg.dataDir}/.env" | ||||
|           ${secretReplacements} | ||||
|  | ||||
|           # prepend `base64:` if it does not exist in APP_KEY | ||||
|  | ||||
|           if ! grep 'APP_KEY=base64:' "${cfg.dataDir}/.env" >/dev/null; then | ||||
|               sed -i 's/APP_KEY=/APP_KEY=base64:/' "${cfg.dataDir}/.env" | ||||
|           fi | ||||
|  | ||||
|           # migrate db | ||||
|           ${lib.getExe phpPackage} ${koillection}/share/php/koillection/bin/console doctrine:migrations:migrate --no-interaction --allow-no-migration | ||||
|  | ||||
|           # dump translations | ||||
|           # TODO: Might need pointing somewhere else. | ||||
|           ${lib.getExe phpPackage} ${koillection}/share/php/koillection/bin/console app:translations:dump | ||||
|         ''; | ||||
|     }; | ||||
|  | ||||
|     systemd.tmpfiles.rules = [ | ||||
|       "d ${cfg.dataDir}                            0710 ${cfg.user} ${cfg.group} - -" | ||||
|       "d ${cfg.dataDir}/public                     0750 ${cfg.user} ${cfg.group} - -" | ||||
|       "d ${cfg.dataDir}/public/uploads             0750 ${cfg.user} ${cfg.group} - -" | ||||
|       "d ${cfg.dataDir}/var                        0700 ${cfg.user} ${cfg.group} - -" | ||||
|       "d ${cfg.dataDir}/var/cache                  0700 ${cfg.user} ${cfg.group} - -" | ||||
|       "d ${cfg.dataDir}/var/cache/prod             0700 ${cfg.user} ${cfg.group} - -" | ||||
|       "d ${cfg.dataDir}/var/logs                   0700 ${cfg.user} ${cfg.group} - -" | ||||
|     ]; | ||||
|  | ||||
|     users = { | ||||
|       users = mkIf (cfg.user == "koillection") { | ||||
|         koillection = { | ||||
|           inherit (cfg) group; | ||||
|           isSystemUser = true; | ||||
|         }; | ||||
|         "${config.services.nginx.user}".extraGroups = [ cfg.group ]; | ||||
|       }; | ||||
|       groups = mkIf (cfg.group == "koillection") { | ||||
|         koillection = { }; | ||||
|       }; | ||||
|     }; | ||||
|   }; | ||||
| } | ||||
		Reference in New Issue
	
	Block a user