From 80475b46f52a1e28a70b1866c2a8edb071208ac8 Mon Sep 17 00:00:00 2001 From: Jonas Heinrich Date: Thu, 20 Jan 2022 14:45:35 +0100 Subject: [PATCH] nixos/invoiceplane: init module and package at 1.5.11 (#146909) --- .../from_md/release-notes/rl-2205.section.xml | 8 + .../manual/release-notes/rl-2205.section.md | 2 + nixos/modules/module-list.nix | 1 + .../services/web-apps/invoiceplane.nix | 305 ++++++++++++++++++ nixos/tests/all-tests.nix | 1 + nixos/tests/invoiceplane.nix | 82 +++++ .../servers/web-apps/invoiceplane/default.nix | 32 ++ pkgs/top-level/all-packages.nix | 2 + 8 files changed, 433 insertions(+) create mode 100644 nixos/modules/services/web-apps/invoiceplane.nix create mode 100644 nixos/tests/invoiceplane.nix create mode 100644 pkgs/servers/web-apps/invoiceplane/default.nix diff --git a/nixos/doc/manual/from_md/release-notes/rl-2205.section.xml b/nixos/doc/manual/from_md/release-notes/rl-2205.section.xml index ce45b0d7977..2875ea683f8 100644 --- a/nixos/doc/manual/from_md/release-notes/rl-2205.section.xml +++ b/nixos/doc/manual/from_md/release-notes/rl-2205.section.xml @@ -108,6 +108,14 @@ services.powerdns-admin. + + + InvoicePlane, + web application for managing and creating invoices. Available + at + services.invoiceplane. + + maddy, a diff --git a/nixos/doc/manual/release-notes/rl-2205.section.md b/nixos/doc/manual/release-notes/rl-2205.section.md index 25b3ada2c56..a59513adfc9 100644 --- a/nixos/doc/manual/release-notes/rl-2205.section.md +++ b/nixos/doc/manual/release-notes/rl-2205.section.md @@ -35,6 +35,8 @@ In addition to numerous new and upgraded packages, this release has the followin - [PowerDNS-Admin](https://github.com/ngoduykhanh/PowerDNS-Admin), a web interface for the PowerDNS server. Available at [services.powerdns-admin](options.html#opt-services.powerdns-admin.enable). +- [InvoicePlane](https://invoiceplane.com), web application for managing and creating invoices. Available at [services.invoiceplane](options.html#opt-services.invoiceplane.enable). + - [maddy](https://maddy.email), a composable all-in-one mail server. Available as [services.maddy](options.html#opt-services.maddy.enable). - [mtr-exporter](https://github.com/mgumz/mtr-exporter), a Prometheus exporter for mtr metrics. Available as [services.mtr-exporter](options.html#opt-services.mtr-exporter.enable). diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix index fdf93f2e17c..4b2cb803e20 100644 --- a/nixos/modules/module-list.nix +++ b/nixos/modules/module-list.nix @@ -1022,6 +1022,7 @@ ./services/web-apps/keycloak.nix ./services/web-apps/lemmy.nix ./services/web-apps/invidious.nix + ./services/web-apps/invoiceplane.nix ./services/web-apps/limesurvey.nix ./services/web-apps/mastodon.nix ./services/web-apps/mattermost.nix diff --git a/nixos/modules/services/web-apps/invoiceplane.nix b/nixos/modules/services/web-apps/invoiceplane.nix new file mode 100644 index 00000000000..095eec36dec --- /dev/null +++ b/nixos/modules/services/web-apps/invoiceplane.nix @@ -0,0 +1,305 @@ +{ config, pkgs, lib, ... }: + +with lib; + +let + cfg = config.services.invoiceplane; + eachSite = cfg.sites; + user = "invoiceplane"; + webserver = config.services.${cfg.webserver}; + + invoiceplane-config = hostName: cfg: pkgs.writeText "ipconfig.php" '' + IP_URL=http://${hostName} + ENABLE_DEBUG=false + DISABLE_SETUP=false + REMOVE_INDEXPHP=false + DB_HOSTNAME=${cfg.database.host} + DB_USERNAME=${cfg.database.user} + # NOTE: file_get_contents adds newline at the end of returned string + DB_PASSWORD=${if cfg.database.passwordFile == null then "" else "trim(file_get_contents('${cfg.database.passwordFile}'), \"\\r\\n\")"} + DB_DATABASE=${cfg.database.name} + DB_PORT=${toString cfg.database.port} + SESS_EXPIRATION=864000 + ENABLE_INVOICE_DELETION=false + DISABLE_READ_ONLY=false + ENCRYPTION_KEY= + ENCRYPTION_CIPHER=AES-256 + SETUP_COMPLETED=false + ''; + + extraConfig = hostName: cfg: pkgs.writeText "extraConfig.php" '' + ${toString cfg.extraConfig} + ''; + + pkg = hostName: cfg: pkgs.stdenv.mkDerivation rec { + pname = "invoiceplane-${hostName}"; + version = src.version; + src = pkgs.invoiceplane; + + patchPhase = '' + # Patch index.php file to load additional config file + substituteInPlace index.php \ + --replace "require('vendor/autoload.php');" "require('vendor/autoload.php'); \$dotenv = new \Dotenv\Dotenv(__DIR__, 'extraConfig.php'); \$dotenv->load();"; + ''; + + installPhase = '' + mkdir -p $out + cp -r * $out/ + + # symlink uploads and log directories + rm -r $out/uploads $out/application/logs $out/vendor/mpdf/mpdf/tmp + ln -sf ${cfg.stateDir}/uploads $out/ + ln -sf ${cfg.stateDir}/logs $out/application/ + ln -sf ${cfg.stateDir}/tmp $out/vendor/mpdf/mpdf/ + + # symlink the InvoicePlane config + ln -s ${cfg.stateDir}/ipconfig.php $out/ipconfig.php + + # symlink the extraConfig file + ln -s ${extraConfig hostName cfg} $out/extraConfig.php + + # symlink additional templates + ${concatMapStringsSep "\n" (template: "cp -r ${template}/. $out/application/views/invoice_templates/pdf/") cfg.invoiceTemplates} + ''; + }; + + siteOpts = { lib, name, ... }: + { + options = { + + enable = mkEnableOption "InvoicePlane web application"; + + stateDir = mkOption { + type = types.path; + default = "/var/lib/invoiceplane/${name}"; + description = '' + This directory is used for uploads of attachements and cache. + The directory passed here is automatically created and permissions + adjusted as required. + ''; + }; + + database = { + host = mkOption { + type = types.str; + default = "localhost"; + description = "Database host address."; + }; + + port = mkOption { + type = types.port; + default = 3306; + description = "Database host port."; + }; + + name = mkOption { + type = types.str; + default = "invoiceplane"; + description = "Database name."; + }; + + user = mkOption { + type = types.str; + default = "invoiceplane"; + description = "Database user."; + }; + + passwordFile = mkOption { + type = types.nullOr types.path; + default = null; + example = "/run/keys/invoiceplane-dbpassword"; + description = '' + A file containing the password corresponding to + . + ''; + }; + + createLocally = mkOption { + type = types.bool; + default = true; + description = "Create the database and database user locally."; + }; + }; + + invoiceTemplates = mkOption { + type = types.listOf types.path; + default = []; + description = '' + List of path(s) to respective template(s) which are copied from the 'invoice_templates/pdf' directory. + These templates need to be packaged before use, see example. + ''; + example = literalExpression '' + let + # Let's package an example template + template-vtdirektmarketing = pkgs.stdenv.mkDerivation { + name = "vtdirektmarketing"; + # Download the template from a public repository + src = pkgs.fetchgit { + url = "https://git.project-insanity.org/onny/invoiceplane-vtdirektmarketing.git"; + sha256 = "1hh0q7wzsh8v8x03i82p6qrgbxr4v5fb05xylyrpp975l8axyg2z"; + }; + sourceRoot = "."; + # Installing simply means copying template php file to the output directory + installPhase = "" + mkdir -p $out + cp invoiceplane-vtdirektmarketing/vtdirektmarketing.php $out/ + ""; + }; + # And then pass this package to the template list like this: + in [ template-vtdirektmarketing ] + ''; + }; + + 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 = '' + Options for the InvoicePlane PHP pool. See the documentation on php-fpm.conf + for details on configuration directives. + ''; + }; + + extraConfig = mkOption { + type = types.nullOr types.lines; + default = null; + example = '' + SETUP_COMPLETED=true + DISABLE_SETUP=true + IP_URL=https://invoice.example.com + ''; + description = '' + InvoicePlane configuration. Refer to + + for details on supported values. + ''; + }; + + }; + + }; +in +{ + # interface + options = { + services.invoiceplane = mkOption { + type = types.submodule { + + options.sites = mkOption { + type = types.attrsOf (types.submodule siteOpts); + default = {}; + description = "Specification of one or more WordPress sites to serve"; + }; + + options.webserver = mkOption { + type = types.enum [ "caddy" ]; + default = "caddy"; + description = '' + Which webserver to use for virtual host management. Currently only + caddy is supported. + ''; + }; + }; + default = {}; + description = "InvoicePlane configuration."; + }; + + }; + + # implementation + config = mkIf (eachSite != {}) (mkMerge [{ + + assertions = flatten (mapAttrsToList (hostName: cfg: + [{ assertion = cfg.database.createLocally -> cfg.database.user == user; + message = ''services.invoiceplane.sites."${hostName}".database.user must be ${user} if the database is to be automatically provisioned''; + } + { assertion = cfg.database.createLocally -> cfg.database.passwordFile == null; + message = ''services.invoiceplane.sites."${hostName}".database.passwordFile cannot be specified if services.invoiceplane.sites."${hostName}".database.createLocally is set to true.''; + }] + ) eachSite); + + services.mysql = mkIf (any (v: v.database.createLocally) (attrValues eachSite)) { + enable = true; + package = mkDefault pkgs.mariadb; + ensureDatabases = mapAttrsToList (hostName: cfg: cfg.database.name) eachSite; + ensureUsers = mapAttrsToList (hostName: cfg: + { name = cfg.database.user; + ensurePermissions = { "${cfg.database.name}.*" = "ALL PRIVILEGES"; }; + } + ) eachSite; + }; + + services.phpfpm = { + phpPackage = pkgs.php74; + pools = mapAttrs' (hostName: cfg: ( + nameValuePair "invoiceplane-${hostName}" { + inherit user; + group = webserver.group; + settings = { + "listen.owner" = webserver.user; + "listen.group" = webserver.group; + } // cfg.poolConfig; + } + )) eachSite; + }; + + } + + { + systemd.tmpfiles.rules = flatten (mapAttrsToList (hostName: cfg: [ + "d ${cfg.stateDir} 0750 ${user} ${webserver.group} - -" + "f ${cfg.stateDir}/ipconfig.php 0750 ${user} ${webserver.group} - -" + "d ${cfg.stateDir}/logs 0750 ${user} ${webserver.group} - -" + "d ${cfg.stateDir}/uploads 0750 ${user} ${webserver.group} - -" + "d ${cfg.stateDir}/uploads/archive 0750 ${user} ${webserver.group} - -" + "d ${cfg.stateDir}/uploads/customer_files 0750 ${user} ${webserver.group} - -" + "d ${cfg.stateDir}/uploads/temp 0750 ${user} ${webserver.group} - -" + "d ${cfg.stateDir}/uploads/temp/mpdf 0750 ${user} ${webserver.group} - -" + "d ${cfg.stateDir}/tmp 0750 ${user} ${webserver.group} - -" + ]) eachSite); + + systemd.services.invoiceplane-config = { + serviceConfig.Type = "oneshot"; + script = concatStrings (mapAttrsToList (hostName: cfg: + '' + mkdir -p ${cfg.stateDir}/logs \ + ${cfg.stateDir}/uploads + if ! grep -q IP_URL "${cfg.stateDir}/ipconfig.php"; then + cp "${invoiceplane-config hostName cfg}" "${cfg.stateDir}/ipconfig.php" + fi + '') eachSite); + wantedBy = [ "multi-user.target" ]; + }; + + users.users.${user} = { + group = webserver.group; + isSystemUser = true; + }; + } + + (mkIf (cfg.webserver == "caddy") { + services.caddy = { + enable = true; + virtualHosts = mapAttrs' (hostName: cfg: ( + nameValuePair "http://${hostName}" { + extraConfig = '' + root * ${pkg hostName cfg} + file_server + + php_fastcgi unix/${config.services.phpfpm.pools."invoiceplane-${hostName}".socket} + ''; + } + )) eachSite; + }; + }) + + + ]); +} + diff --git a/nixos/tests/all-tests.nix b/nixos/tests/all-tests.nix index 6e66e9cbe96..940ae11ddd1 100644 --- a/nixos/tests/all-tests.nix +++ b/nixos/tests/all-tests.nix @@ -215,6 +215,7 @@ in initrd-secrets = handleTest ./initrd-secrets.nix {}; inspircd = handleTest ./inspircd.nix {}; installer = handleTest ./installer.nix {}; + invoiceplane = handleTest ./invoiceplane.nix {}; iodine = handleTest ./iodine.nix {}; ipfs = handleTest ./ipfs.nix {}; ipv6 = handleTest ./ipv6.nix {}; diff --git a/nixos/tests/invoiceplane.nix b/nixos/tests/invoiceplane.nix new file mode 100644 index 00000000000..4e63f8ac21c --- /dev/null +++ b/nixos/tests/invoiceplane.nix @@ -0,0 +1,82 @@ +import ./make-test-python.nix ({ pkgs, ... }: + +{ + name = "invoiceplane"; + meta = with pkgs.lib.maintainers; { + maintainers = [ + onny + ]; + }; + + nodes = { + invoiceplane_caddy = { ... }: { + services.invoiceplane.webserver = "caddy"; + services.invoiceplane.sites = { + "site1.local" = { + #database.name = "invoiceplane1"; + database.createLocally = true; + enable = true; + }; + "site2.local" = { + #database.name = "invoiceplane2"; + database.createLocally = true; + enable = true; + }; + }; + + networking.firewall.allowedTCPPorts = [ 80 ]; + networking.hosts."127.0.0.1" = [ "site1.local" "site2.local" ]; + }; + }; + + testScript = '' + start_all() + + invoiceplane_caddy.wait_for_unit("caddy") + invoiceplane_caddy.wait_for_open_port(80) + invoiceplane_caddy.wait_for_open_port(3306) + + site_names = ["site1.local", "site2.local"] + + for site_name in site_names: + machine.wait_for_unit(f"phpfpm-invoiceplane-{site_name}") + + with subtest("Website returns welcome screen"): + assert "Please install InvoicePlane" in machine.succeed(f"curl -L {site_name}") + + with subtest("Finish InvoicePlane setup"): + machine.succeed( + f"curl -sSfL --cookie-jar cjar {site_name}/index.php/setup/language" + ) + csrf_token = machine.succeed( + "grep ip_csrf_cookie cjar | cut -f 7 | tr -d '\n'" + ) + machine.succeed( + f"curl -sSfL --cookie cjar --cookie-jar cjar -d '_ip_csrf={csrf_token}&ip_lang=english&btn_continue=Continue' {site_name}/index.php/setup/language" + ) + csrf_token = machine.succeed( + "grep ip_csrf_cookie cjar | cut -f 7 | tr -d '\n'" + ) + machine.succeed( + f"curl -sSfL --cookie cjar --cookie-jar cjar -d '_ip_csrf={csrf_token}&btn_continue=Continue' {site_name}/index.php/setup/prerequisites" + ) + csrf_token = machine.succeed( + "grep ip_csrf_cookie cjar | cut -f 7 | tr -d '\n'" + ) + machine.succeed( + f"curl -sSfL --cookie cjar --cookie-jar cjar -d '_ip_csrf={csrf_token}&btn_continue=Continue' {site_name}/index.php/setup/configure_database" + ) + csrf_token = machine.succeed( + "grep ip_csrf_cookie cjar | cut -f 7 | tr -d '\n'" + ) + machine.succeed( + f"curl -sSfl --cookie cjar --cookie-jar cjar -d '_ip_csrf={csrf_token}&btn_continue=Continue' {site_name}/index.php/setup/install_tables" + ) + csrf_token = machine.succeed( + "grep ip_csrf_cookie cjar | cut -f 7 | tr -d '\n'" + ) + machine.succeed( + f"curl -sSfl --cookie cjar --cookie-jar cjar -d '_ip_csrf={csrf_token}&btn_continue=Continue' {site_name}/index.php/setup/upgrade_tables" + ) + ''; +}) diff --git a/pkgs/servers/web-apps/invoiceplane/default.nix b/pkgs/servers/web-apps/invoiceplane/default.nix new file mode 100644 index 00000000000..6c9ffd44b9d --- /dev/null +++ b/pkgs/servers/web-apps/invoiceplane/default.nix @@ -0,0 +1,32 @@ +{ lib, stdenv, fetchurl, writeText, unzip, nixosTests }: + +stdenv.mkDerivation rec { + pname = "invoiceplane"; + version = "1.5.11"; + + src = fetchurl { + url = "https://github.com/InvoicePlane/InvoicePlane/releases/download/v${version}/v${version}.zip"; + sha256 = "137g0xps4kb3j7f5gz84ql18iggbya6d9dnrfp05g2qcbbp8kqad"; + }; + + nativeBuildInputs = [ unzip ]; + + sourceRoot = "."; + + installPhase = '' + mkdir -p $out/ + cp -r . $out/ + ''; + + passthru.tests = { + inherit (nixosTests) invoiceplane; + }; + + meta = with lib; { + description = "Self-hosted open source application for managing your invoices, clients and payments"; + license = licenses.mit; + homepage = "https://www.invoiceplane.com"; + platforms = platforms.all; + maintainers = with maintainers; [ onny ]; + }; +} diff --git a/pkgs/top-level/all-packages.nix b/pkgs/top-level/all-packages.nix index de442328506..0244986ffb5 100644 --- a/pkgs/top-level/all-packages.nix +++ b/pkgs/top-level/all-packages.nix @@ -3240,6 +3240,8 @@ with pkgs; interlock = callPackage ../servers/interlock {}; + invoiceplane = callPackage ../servers/web-apps/invoiceplane { }; + iotools = callPackage ../tools/misc/iotools { }; jellyfin = callPackage ../servers/jellyfin { };