From a24fa6098df0116cc89a78ca49712778b304ba32 Mon Sep 17 00:00:00 2001 From: Pyrox Date: Thu, 11 Jul 2024 03:07:40 -0400 Subject: [PATCH] nixos/gotenberg: init --- .../manual/release-notes/rl-2411.section.md | 2 + nixos/modules/module-list.nix | 1 + nixos/modules/services/misc/gotenberg.nix | 258 ++++++++++++++++++ nixos/tests/all-tests.nix | 1 + nixos/tests/gotenberg.nix | 26 ++ 5 files changed, 288 insertions(+) create mode 100644 nixos/modules/services/misc/gotenberg.nix create mode 100644 nixos/tests/gotenberg.nix diff --git a/nixos/doc/manual/release-notes/rl-2411.section.md b/nixos/doc/manual/release-notes/rl-2411.section.md index 7e1127ba6cb1..23fe9774237a 100644 --- a/nixos/doc/manual/release-notes/rl-2411.section.md +++ b/nixos/doc/manual/release-notes/rl-2411.section.md @@ -34,6 +34,8 @@ - [realm](https://github.com/zhboner/realm), a simple, high performance relay server written in rust. Available as [services.realm.enable](#opt-services.realm.enable). +- [Gotenberg](https://gotenberg.dev), an API server for converting files to PDFs that can be used alongside Paperless-ngx. Available as [services.gotenberg](options.html#opt-services.gotenberg). + - [Playerctld](https://github.com/altdesktop/playerctl), a daemon to track media player activity. Available as [services.playerctld](option.html#opt-services.playerctld). - [Glance](https://github.com/glanceapp/glance), a self-hosted dashboard that puts all your feeds in one place. Available as [services.glance](option.html#opt-services.glance). diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix index 3019b23bc870..a2fe394ba607 100644 --- a/nixos/modules/module-list.nix +++ b/nixos/modules/module-list.nix @@ -741,6 +741,7 @@ ./services/misc/gitweb.nix ./services/misc/gogs.nix ./services/misc/gollum.nix + ./services/misc/gotenberg.nix ./services/misc/gpsd.nix ./services/misc/graphical-desktop.nix ./services/misc/greenclip.nix diff --git a/nixos/modules/services/misc/gotenberg.nix b/nixos/modules/services/misc/gotenberg.nix new file mode 100644 index 000000000000..57932c656d63 --- /dev/null +++ b/nixos/modules/services/misc/gotenberg.nix @@ -0,0 +1,258 @@ +{ + config, + lib, + pkgs, + ... +}: +let + cfg = config.services.gotenberg; + + args = + [ + "--api-port=${toString cfg.port}" + "--api-timeout=${cfg.timeout}" + "--api-root-path=${cfg.rootPath}" + "--log-level=${cfg.logLevel}" + "--chromium-max-queue-size=${toString cfg.chromium.maxQueueSize}" + "--libreoffice-restart-after=${toString cfg.libreoffice.restartAfter}" + "--libreoffice-max-queue-size=${toString cfg.libreoffice.maxQueueSize}" + "--pdfengines-engines=${lib.concatStringsSep "," cfg.pdfEngines}" + ] + ++ optional cfg.enableBasicAuth "--api-enable-basic-auth" + ++ optional cfg.chromium.autoStart "--chromium-auto-start" + ++ optional cfg.chromium.disableJavascript "--chromium-disable-javascript" + ++ optional cfg.chromium.disableRoutes "--chromium-disable-routes" + ++ optional cfg.libreoffice.autoStart "--libreoffice-auto-start" + ++ optional cfg.libreoffice.disableRoutes "--libreoffice-disable-routes"; + + inherit (lib) + mkEnableOption + mkPackageOption + mkOption + types + mkIf + optional + optionalAttrs + ; +in +{ + options = { + services.gotenberg = { + enable = mkEnableOption "Gotenberg, a stateless API for PDF files"; + + # Users can override only gotenberg, libreoffice and chromium if they want to (eg. ungoogled-chromium, different LO version, etc) + # Don't allow setting the qpdf, pdftk, or unoconv paths, as those are very stable + # and there's only one version of each. + package = mkPackageOption pkgs "gotenberg" { }; + + port = mkOption { + type = types.port; + default = 3000; + description = "Port on which the API should listen."; + }; + + timeout = mkOption { + type = types.nullOr types.str; + default = "30s"; + description = "Timeout for API requests."; + }; + + rootPath = mkOption { + type = types.str; + default = "/"; + description = "Root path for the Gotenberg API."; + }; + + enableBasicAuth = mkOption { + type = types.bool; + default = false; + description = '' + HTTP Basic Authentication. + + If you set this, be sure to set `GOTENBERG_API_BASIC_AUTH_USERNAME`and `GOTENBERG_API_BASIC_AUTH_PASSWORD` + in your `services.gotenberg.environmentFile` file. + ''; + }; + + extraFontPackages = mkOption { + type = types.listOf types.package; + default = [ ]; + description = "Extra fonts to make available."; + }; + + chromium = { + package = mkPackageOption pkgs "chromium" { }; + + maxQueueSize = mkOption { + type = types.int; + default = 0; + description = "Maximum queue size for chromium-based conversions. Setting to 0 disables the limit."; + }; + + autoStart = mkOption { + type = types.bool; + default = false; + description = "Automatically start chromium when Gotenberg starts. If false, Chromium will start on the first conversion request that uses it."; + }; + + disableJavascript = mkOption { + type = types.bool; + default = false; + description = "Disable Javascript execution."; + }; + + disableRoutes = mkOption { + type = types.bool; + default = false; + description = "Disable all routes allowing Chromium-based conversion."; + }; + }; + + libreoffice = { + package = mkPackageOption pkgs "libreoffice" { }; + + restartAfter = mkOption { + type = types.int; + default = 10; + description = "Restart LibreOffice after this many conversions. Setting to 0 disables this feature."; + }; + + maxQueueSize = mkOption { + type = types.int; + default = 0; + description = "Maximum queue size for LibreOffice-based conversions. Setting to 0 disables the limit."; + }; + + autoStart = mkOption { + type = types.bool; + default = false; + description = "Automatically start LibreOffice when Gotenberg starts. If false, Chromium will start on the first conversion request that uses it."; + }; + + disableRoutes = mkOption { + type = types.bool; + default = false; + description = "Disable all routes allowing LibreOffice-based conversion."; + }; + }; + + pdfEngines = mkOption { + type = types.listOf ( + types.enum [ + "pdftk" + "qpdf" + "libreoffice-pdfengine" + "exiftool" + "pdfcpu" + ] + ); + default = [ + "pdftk" + "qpdf" + "libreoffice-pdfengine" + "exiftool" + "pdfcpu" + ]; + description = '' + PDF engines to enable. Each one can be used to perform a specific task. + See [the documentation](https://gotenberg.dev/docs/configuration#pdf-engines) for more details. + Defaults to all possible PDF engines. + ''; + }; + + logLevel = mkOption { + type = types.enum [ + "error" + "warn" + "info" + "debug" + ]; + default = "info"; + description = "The logging level for Gotenberg."; + }; + + environmentFile = mkOption { + type = types.nullOr types.path; + default = null; + description = "Environment file to load extra environment variables from."; + }; + + extraArgs = mkOption { + type = types.listOf types.str; + default = [ ]; + description = "Any extra command-line flags to pass to the Gotenberg service."; + }; + }; + }; + + config = mkIf cfg.enable { + assertions = [ + { + assertion = cfg.enableBasicAuth -> cfg.environmentFile != null; + message = '' + When enabling HTTP Basic Authentication with `services.gotenberg.enableBasicAuth`, + you must provide an environment file via `services.gotenberg.environmentFile` with the appropriate environment variables set in it. + + See `services.gotenberg.enableBasicAuth` for the names of those variables. + ''; + } + ]; + + systemd.services.gotenberg = { + description = "Gotenberg API server"; + after = [ "network.target" ]; + wantedBy = [ "multi-user.target" ]; + path = [ cfg.package ]; + environment = { + LIBREOFFICE_BIN_PATH = "${cfg.libreoffice.package}/lib/libreoffice/program/soffice.bin"; + CHROMIUM_BIN_PATH = lib.getExe cfg.chromium.package; + FONTCONFIG_FILE = pkgs.makeFontsConf { + fontDirectories = [ pkgs.liberation_ttf_v2 ] ++ cfg.extraFontPackages; + }; + }; + serviceConfig = { + Type = "simple"; + DynamicUser = true; + ExecStart = "${lib.getExe cfg.package} ${lib.escapeShellArgs args}"; + + # Hardening options + PrivateDevices = true; + PrivateIPC = true; + PrivateUsers = true; + + ProtectClock = true; + ProtectControlGroups = true; + ProtectHome = true; + ProtectHostname = true; + ProtectKernelLogs = true; + ProtectKernelModules = true; + ProtectKernelTunables = true; + ProtectProc = "invisible"; + ProcSubset = "pid"; + + RestrictAddressFamilies = [ + "AF_UNIX" + "AF_INET" + "AF_INET6" + "AF_NETLINK" + ]; + RestrictNamespaces = true; + RestrictRealtime = true; + + LockPersonality = true; + MemoryDenyWriteExecute = true; + + SystemCallFilter = [ + "@system-service" + "~@resources" + "~@privileged" + ]; + SystemCallArchitectures = "native"; + + UMask = 77; + } // optionalAttrs (cfg.environmentFile != null) { EnvironmentFile = cfg.environmentFile; }; + }; + }; + + meta.maintainers = with lib.maintainers; [ pyrox0 ]; +} diff --git a/nixos/tests/all-tests.nix b/nixos/tests/all-tests.nix index c22c3e961d08..466a8e2dc21b 100644 --- a/nixos/tests/all-tests.nix +++ b/nixos/tests/all-tests.nix @@ -381,6 +381,7 @@ in { gonic = handleTest ./gonic.nix {}; google-oslogin = handleTest ./google-oslogin {}; goss = handleTest ./goss.nix {}; + gotenberg = handleTest ./gotenberg.nix {}; gotify-server = handleTest ./gotify-server.nix {}; gotosocial = runTest ./web-apps/gotosocial.nix; grafana = handleTest ./grafana {}; diff --git a/nixos/tests/gotenberg.nix b/nixos/tests/gotenberg.nix new file mode 100644 index 000000000000..aa39b2d349d7 --- /dev/null +++ b/nixos/tests/gotenberg.nix @@ -0,0 +1,26 @@ +import ./make-test-python.nix ( + { lib, ... }: + + { + name = "gotenberg"; + meta.maintainers = with lib.maintainers; [ pyrox0 ]; + + nodes.machine = { + services.gotenberg = { + enable = true; + }; + }; + + testScript = '' + start_all() + + machine.wait_for_unit("gotenberg.service") + + # Gotenberg startup + machine.wait_for_open_port(3000) + + # Ensure healthcheck endpoint succeeds + machine.succeed("curl http://localhost:3000/health") + ''; + } +)