1{
2 config,
3 pkgs,
4 lib,
5 utils,
6 ...
7}:
8let
9 cfg = config.services.pghero;
10 settingsFormat = pkgs.formats.yaml { };
11 settingsFile = settingsFormat.generate "pghero.yaml" cfg.settings;
12in
13{
14 options.services.pghero = {
15 enable = lib.mkEnableOption "PgHero service";
16 package = lib.mkPackageOption pkgs "pghero" { };
17
18 listenAddress = lib.mkOption {
19 type = lib.types.str;
20 example = "[::1]:3000";
21 description = ''
22 `hostname:port` to listen for HTTP traffic.
23
24 This is bound using the systemd socket activation.
25 '';
26 };
27
28 extraArgs = lib.mkOption {
29 type = lib.types.listOf lib.types.str;
30 default = [ ];
31 description = ''
32 Additional command-line arguments for the systemd service.
33
34 Refer to the [Puma web server documentation] for available arguments.
35
36 [Puma web server documentation]: https://puma.io/puma#configuration
37 '';
38 };
39
40 settings = lib.mkOption {
41 type = settingsFormat.type;
42 default = { };
43 example = {
44 databases = {
45 primary = {
46 url = "<%= ENV['PRIMARY_DATABASE_URL'] %>";
47 };
48 };
49 };
50 description = ''
51 PgHero configuration. Refer to the [PgHero documentation] for more
52 details.
53
54 [PgHero documentation]: https://github.com/ankane/pghero/blob/master/guides/Linux.md#multiple-databases
55 '';
56 };
57
58 environment = lib.mkOption {
59 type = lib.types.attrsOf lib.types.str;
60 default = { };
61 description = ''
62 Environment variables to set for the service. Secrets should be
63 specified using {option}`environmentFile`.
64 '';
65 };
66
67 environmentFiles = lib.mkOption {
68 type = lib.types.listOf lib.types.path;
69 default = [ ];
70 description = ''
71 File to load environment variables from. Loaded variables override
72 values set in {option}`environment`.
73 '';
74 };
75
76 extraGroups = lib.mkOption {
77 type = lib.types.listOf lib.types.str;
78 default = [ ];
79 example = [ "tlskeys" ];
80 description = ''
81 Additional groups for the systemd service.
82 '';
83 };
84 };
85
86 config = lib.mkIf cfg.enable {
87 systemd.sockets.pghero = {
88 unitConfig.Description = "PgHero HTTP socket";
89 wantedBy = [ "sockets.target" ];
90 listenStreams = [ cfg.listenAddress ];
91 };
92
93 systemd.services.pghero = {
94 description = "PgHero performance dashboard for PostgreSQL";
95 wantedBy = [ "multi-user.target" ];
96 requires = [ "pghero.socket" ];
97 after = [
98 "pghero.socket"
99 "network.target"
100 ];
101
102 environment = {
103 RAILS_ENV = "production";
104 PGHERO_CONFIG_PATH = settingsFile;
105 }
106 // cfg.environment;
107
108 serviceConfig = {
109 Type = "notify";
110 WatchdogSec = "10";
111
112 ExecStart = utils.escapeSystemdExecArgs (
113 [
114 (lib.getExe cfg.package)
115 "--bind-to-activated-sockets"
116 "only"
117 ]
118 ++ cfg.extraArgs
119 );
120 Restart = "always";
121
122 WorkingDirectory = "${cfg.package}/share/pghero";
123
124 EnvironmentFile = cfg.environmentFiles;
125 SupplementaryGroups = cfg.extraGroups;
126
127 DynamicUser = true;
128 UMask = "0077";
129
130 ProtectHome = true;
131 ProtectProc = "invisible";
132 ProcSubset = "pid";
133 ProtectClock = true;
134 ProtectHostname = true;
135 ProtectControlGroups = true;
136 ProtectKernelLogs = true;
137 ProtectKernelModules = true;
138 ProtectKernelTunables = true;
139 PrivateUsers = true;
140 PrivateDevices = true;
141 RestrictRealtime = true;
142 RestrictNamespaces = true;
143 RestrictAddressFamilies = [
144 "AF_INET"
145 "AF_INET6"
146 "AF_UNIX"
147 ];
148 DeviceAllow = [ "" ];
149 DevicePolicy = "closed";
150 CapabilityBoundingSet = [ "" ];
151 MemoryDenyWriteExecute = true;
152 LockPersonality = true;
153 SystemCallArchitectures = "native";
154 SystemCallErrorNumber = "EPERM";
155 SystemCallFilter = [ "@system-service" ];
156 };
157 };
158 };
159}