From a5499ee5356975f4bc50a0e24fc6238f48fc4666 Mon Sep 17 00:00:00 2001 From: Ivan Trubach Date: Thu, 23 Nov 2023 13:59:27 +0300 Subject: [PATCH] nixos/pghero: init --- nixos/modules/module-list.nix | 1 + nixos/modules/services/misc/pghero.nix | 142 +++++++++++++++++++++++++ nixos/tests/all-tests.nix | 1 + nixos/tests/pghero.nix | 63 +++++++++++ pkgs/by-name/pg/pghero/package.nix | 4 + 5 files changed, 211 insertions(+) create mode 100644 nixos/modules/services/misc/pghero.nix create mode 100644 nixos/tests/pghero.nix diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix index 159696be8b6d..387c043a7d18 100644 --- a/nixos/modules/module-list.nix +++ b/nixos/modules/module-list.nix @@ -775,6 +775,7 @@ ./services/misc/paperless.nix ./services/misc/parsoid.nix ./services/misc/persistent-evdev.nix + ./services/misc/pghero.nix ./services/misc/pinnwand.nix ./services/misc/plex.nix ./services/misc/plikd.nix diff --git a/nixos/modules/services/misc/pghero.nix b/nixos/modules/services/misc/pghero.nix new file mode 100644 index 000000000000..39515f10c8e1 --- /dev/null +++ b/nixos/modules/services/misc/pghero.nix @@ -0,0 +1,142 @@ +{ config, pkgs, lib, utils, ... }: +let + cfg = config.services.pghero; + settingsFormat = pkgs.formats.yaml { }; + settingsFile = settingsFormat.generate "pghero.yaml" cfg.settings; +in +{ + options.services.pghero = { + enable = lib.mkEnableOption "PgHero service"; + package = lib.mkPackageOption pkgs "pghero" { }; + + listenAddress = lib.mkOption { + type = lib.types.str; + example = "[::1]:3000"; + description = '' + `hostname:port` to listen for HTTP traffic. + + This is bound using the systemd socket activation. + ''; + }; + + extraArgs = lib.mkOption { + type = lib.types.listOf lib.types.str; + default = [ ]; + description = '' + Additional command-line arguments for the systemd service. + + Refer to the [Puma web server documentation] for available arguments. + + [Puma web server documentation]: https://puma.io/puma#configuration + ''; + }; + + settings = lib.mkOption { + type = settingsFormat.type; + default = { }; + example = { + databases = { + primary = { + url = "<%= ENV['PRIMARY_DATABASE_URL'] %>"; + }; + }; + }; + description = '' + PgHero configuration. Refer to the [PgHero documentation] for more + details. + + [PgHero documentation]: https://github.com/ankane/pghero/blob/master/guides/Linux.md#multiple-databases + ''; + }; + + environment = lib.mkOption { + type = lib.types.attrsOf lib.types.str; + default = { }; + description = '' + Environment variables to set for the service. Secrets should be + specified using {option}`environmentFile`. + ''; + }; + + environmentFiles = lib.mkOption { + type = lib.types.listOf lib.types.path; + default = [ ]; + description = '' + File to load environment variables from. Loaded variables override + values set in {option}`environment`. + ''; + }; + + extraGroups = lib.mkOption { + type = lib.types.listOf lib.types.str; + default = [ ]; + example = [ "tlskeys" ]; + description = '' + Additional groups for the systemd service. + ''; + }; + }; + + config = lib.mkIf cfg.enable { + systemd.sockets.pghero = { + unitConfig.Description = "PgHero HTTP socket"; + wantedBy = [ "sockets.target" ]; + listenStreams = [ cfg.listenAddress ]; + }; + + systemd.services.pghero = { + description = "PgHero performance dashboard for PostgreSQL"; + wantedBy = [ "multi-user.target" ]; + requires = [ "pghero.socket" ]; + after = [ "pghero.socket" "network.target" ]; + + environment = { + RAILS_ENV = "production"; + PGHERO_CONFIG_PATH = settingsFile; + } // cfg.environment; + + serviceConfig = { + Type = "notify"; + WatchdogSec = "10"; + + ExecStart = utils.escapeSystemdExecArgs ([ + (lib.getExe cfg.package) + "--bind-to-activated-sockets" + "only" + ] ++ cfg.extraArgs); + Restart = "always"; + + WorkingDirectory = "${cfg.package}/share/pghero"; + + EnvironmentFile = cfg.environmentFiles; + SupplementaryGroups = cfg.extraGroups; + + DynamicUser = true; + UMask = "0077"; + + ProtectHome = true; + ProtectProc = "invisible"; + ProcSubset = "pid"; + ProtectClock = true; + ProtectHostname = true; + ProtectControlGroups = true; + ProtectKernelLogs = true; + ProtectKernelModules = true; + ProtectKernelTunables = true; + PrivateUsers = true; + PrivateDevices = true; + RestrictRealtime = true; + RestrictNamespaces = true; + RestrictAddressFamilies = [ "AF_INET" "AF_INET6" "AF_UNIX" ]; + DeviceAllow = [ "" ]; + DevicePolicy = "closed"; + CapabilityBoundingSet = [ "" ]; + MemoryDenyWriteExecute = true; + LockPersonality = true; + SystemCallArchitectures = "native"; + SystemCallErrorNumber = "EPERM"; + SystemCallFilter = [ "@system-service" ]; + }; + }; + }; +} diff --git a/nixos/tests/all-tests.nix b/nixos/tests/all-tests.nix index a2408a43ecc9..4a3b2ffc4f8c 100644 --- a/nixos/tests/all-tests.nix +++ b/nixos/tests/all-tests.nix @@ -717,6 +717,7 @@ in { pg_anonymizer = handleTest ./pg_anonymizer.nix {}; pgadmin4 = handleTest ./pgadmin4.nix {}; pgbouncer = handleTest ./pgbouncer.nix {}; + pghero = runTest ./pghero.nix; pgjwt = handleTest ./pgjwt.nix {}; pgmanage = handleTest ./pgmanage.nix {}; pgvecto-rs = handleTest ./pgvecto-rs.nix {}; diff --git a/nixos/tests/pghero.nix b/nixos/tests/pghero.nix new file mode 100644 index 000000000000..bce32da00886 --- /dev/null +++ b/nixos/tests/pghero.nix @@ -0,0 +1,63 @@ +let + pgheroPort = 1337; + pgheroUser = "pghero"; + pgheroPass = "pghero"; +in +{ lib, ... }: { + name = "pghero"; + meta.maintainers = [ lib.maintainers.tie ]; + + nodes.machine = { config, ... }: { + services.postgresql = { + enable = true; + # This test uses default peer authentication (socket and its directory is + # world-readably by default), so we essentially test that we can connect + # with DynamicUser= set. + ensureUsers = [{ + name = "pghero"; + ensureClauses.superuser = true; + }]; + }; + services.pghero = { + enable = true; + listenAddress = "[::]:${toString pgheroPort}"; + settings = { + databases = { + postgres.url = "<%= ENV['POSTGRES_DATABASE_URL'] %>"; + nulldb.url = "nulldb:///"; + }; + }; + environment = { + PGHERO_USERNAME = pgheroUser; + PGHERO_PASSWORD = pgheroPass; + POSTGRES_DATABASE_URL = "postgresql:///postgres?host=/run/postgresql"; + }; + }; + }; + + testScript = '' + pgheroPort = ${toString pgheroPort} + pgheroUser = "${pgheroUser}" + pgheroPass = "${pgheroPass}" + + pgheroUnauthorizedURL = f"http://localhost:{pgheroPort}" + pgheroBaseURL = f"http://{pgheroUser}:{pgheroPass}@localhost:{pgheroPort}" + + def expect_http_code(node, code, url): + http_code = node.succeed(f"curl -s -o /dev/null -w '%{{http_code}}' '{url}'") + assert http_code.split("\n")[-1].strip() == code, \ + f"expected HTTP status code {code} but got {http_code}" + + machine.wait_for_unit("postgresql.service") + machine.wait_for_unit("pghero.service") + + with subtest("requires HTTP Basic Auth credentials"): + expect_http_code(machine, "401", pgheroUnauthorizedURL) + + with subtest("works with some databases being unavailable"): + expect_http_code(machine, "500", pgheroBaseURL + "/nulldb") + + with subtest("connects to the PostgreSQL database"): + expect_http_code(machine, "200", pgheroBaseURL + "/postgres") + ''; +} diff --git a/pkgs/by-name/pg/pghero/package.nix b/pkgs/by-name/pg/pghero/package.nix index 541496946982..0f74a39add63 100644 --- a/pkgs/by-name/pg/pghero/package.nix +++ b/pkgs/by-name/pg/pghero/package.nix @@ -5,6 +5,7 @@ , buildPackages , fetchFromGitHub , makeBinaryWrapper +, nixosTests , callPackage }: stdenv.mkDerivation (finalAttrs: @@ -58,6 +59,9 @@ in passthru = { inherit bundlerEnvArgs; updateScript = callPackage ./update.nix { }; + tests = { + inherit (nixosTests) pghero; + }; }; meta = {