mirror of
https://github.com/NixOS/nixpkgs.git
synced 2025-06-10 03:23:29 +03:00
nixos/postgres-websockets: init
This commit is contained in:
parent
b2b5f8be28
commit
d62c14f5d1
6 changed files with 313 additions and 1 deletions
|
@ -111,6 +111,8 @@
|
||||||
|
|
||||||
- [PostgREST](https://postgrest.org), a standalone web server that turns your PostgreSQL database directly into a RESTful API. Available as [services.postgrest](options.html#opt-services.postgrest.enable).
|
- [PostgREST](https://postgrest.org), a standalone web server that turns your PostgreSQL database directly into a RESTful API. Available as [services.postgrest](options.html#opt-services.postgrest.enable).
|
||||||
|
|
||||||
|
- [postgres-websockets](https://github.com/diogob/postgres-websockets), a middleware that adds websockets capabilites on top of PostgreSQL's asynchronous notifications using LISTEN and NOTIFY commands. Available as [services.postgres-websockets](options.html#opt-services.postgres-websockets.enable).
|
||||||
|
|
||||||
- [µStreamer](https://github.com/pikvm/ustreamer), a lightweight MJPEG-HTTP streamer. Available as [services.ustreamer](options.html#opt-services.ustreamer).
|
- [µStreamer](https://github.com/pikvm/ustreamer), a lightweight MJPEG-HTTP streamer. Available as [services.ustreamer](options.html#opt-services.ustreamer).
|
||||||
|
|
||||||
- [Whoogle Search](https://github.com/benbusby/whoogle-search), a self-hosted, ad-free, privacy-respecting metasearch engine. Available as [services.whoogle-search](options.html#opt-services.whoogle-search.enable).
|
- [Whoogle Search](https://github.com/benbusby/whoogle-search), a self-hosted, ad-free, privacy-respecting metasearch engine. Available as [services.whoogle-search](options.html#opt-services.whoogle-search.enable).
|
||||||
|
|
|
@ -515,6 +515,7 @@
|
||||||
./services/databases/opentsdb.nix
|
./services/databases/opentsdb.nix
|
||||||
./services/databases/pgbouncer.nix
|
./services/databases/pgbouncer.nix
|
||||||
./services/databases/pgmanage.nix
|
./services/databases/pgmanage.nix
|
||||||
|
./services/databases/postgres-websockets.nix
|
||||||
./services/databases/postgresql.nix
|
./services/databases/postgresql.nix
|
||||||
./services/databases/postgrest.nix
|
./services/databases/postgrest.nix
|
||||||
./services/databases/redis.nix
|
./services/databases/redis.nix
|
||||||
|
|
221
nixos/modules/services/databases/postgres-websockets.nix
Normal file
221
nixos/modules/services/databases/postgres-websockets.nix
Normal file
|
@ -0,0 +1,221 @@
|
||||||
|
{
|
||||||
|
config,
|
||||||
|
lib,
|
||||||
|
pkgs,
|
||||||
|
...
|
||||||
|
}:
|
||||||
|
|
||||||
|
let
|
||||||
|
cfg = config.services.postgres-websockets;
|
||||||
|
|
||||||
|
# Turns an attrset of libpq connection params:
|
||||||
|
# {
|
||||||
|
# dbname = "postgres";
|
||||||
|
# user = "authenticator";
|
||||||
|
# }
|
||||||
|
# into a libpq connection string:
|
||||||
|
# dbname=postgres user=authenticator
|
||||||
|
PGWS_DB_URI = lib.pipe cfg.environment.PGWS_DB_URI [
|
||||||
|
(lib.filterAttrs (_: v: v != null))
|
||||||
|
(lib.mapAttrsToList (k: v: "${k}='${lib.escape [ "'" "\\" ] v}'"))
|
||||||
|
(lib.concatStringsSep " ")
|
||||||
|
];
|
||||||
|
in
|
||||||
|
|
||||||
|
{
|
||||||
|
meta = {
|
||||||
|
maintainers = with lib.maintainers; [ wolfgangwalther ];
|
||||||
|
};
|
||||||
|
|
||||||
|
options.services.postgres-websockets = {
|
||||||
|
enable = lib.mkEnableOption "postgres-websockets";
|
||||||
|
|
||||||
|
pgpassFile = lib.mkOption {
|
||||||
|
type =
|
||||||
|
with lib.types;
|
||||||
|
nullOr (pathWith {
|
||||||
|
inStore = false;
|
||||||
|
absolute = true;
|
||||||
|
});
|
||||||
|
default = null;
|
||||||
|
example = "/run/keys/db_password";
|
||||||
|
description = ''
|
||||||
|
The password to authenticate to PostgreSQL with.
|
||||||
|
Not needed for peer or trust based authentication.
|
||||||
|
|
||||||
|
The file must be a valid `.pgpass` file as described in:
|
||||||
|
<https://www.postgresql.org/docs/current/libpq-pgpass.html>
|
||||||
|
|
||||||
|
In most cases, the following will be enough:
|
||||||
|
```
|
||||||
|
*:*:*:*:<password>
|
||||||
|
```
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
jwtSecretFile = lib.mkOption {
|
||||||
|
type =
|
||||||
|
with lib.types;
|
||||||
|
nullOr (pathWith {
|
||||||
|
inStore = false;
|
||||||
|
absolute = true;
|
||||||
|
});
|
||||||
|
example = "/run/keys/jwt_secret";
|
||||||
|
description = ''
|
||||||
|
Secret used to sign JWT tokens used to open communications channels.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
environment = lib.mkOption {
|
||||||
|
type = lib.types.submodule {
|
||||||
|
freeformType = with lib.types; attrsOf str;
|
||||||
|
|
||||||
|
options = {
|
||||||
|
PGWS_DB_URI = lib.mkOption {
|
||||||
|
type = lib.types.submodule {
|
||||||
|
freeformType = with lib.types; attrsOf str;
|
||||||
|
|
||||||
|
# This should not be used; use pgpassFile instead.
|
||||||
|
options.password = lib.mkOption {
|
||||||
|
default = null;
|
||||||
|
readOnly = true;
|
||||||
|
internal = true;
|
||||||
|
};
|
||||||
|
# This should not be used; use pgpassFile instead.
|
||||||
|
options.passfile = lib.mkOption {
|
||||||
|
default = null;
|
||||||
|
readOnly = true;
|
||||||
|
internal = true;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
default = { };
|
||||||
|
description = ''
|
||||||
|
libpq connection parameters as documented in:
|
||||||
|
|
||||||
|
<https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-PARAMKEYWORDS>
|
||||||
|
|
||||||
|
::: {.note}
|
||||||
|
The `environment.PGWS_DB_URI.password` and `environment.PGWS_DB_URI.passfile` options are blocked.
|
||||||
|
Use [`pgpassFile`](#opt-services.postgres-websockets.pgpassFile) instead.
|
||||||
|
:::
|
||||||
|
'';
|
||||||
|
example = lib.literalExpression ''
|
||||||
|
{
|
||||||
|
host = "localhost";
|
||||||
|
dbname = "postgres";
|
||||||
|
}
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
# This should not be used; use jwtSecretFile instead.
|
||||||
|
PGWS_JWT_SECRET = lib.mkOption {
|
||||||
|
default = null;
|
||||||
|
readOnly = true;
|
||||||
|
internal = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
PGWS_HOST = lib.mkOption {
|
||||||
|
type = with lib.types; nullOr str;
|
||||||
|
default = "127.0.0.1";
|
||||||
|
description = ''
|
||||||
|
Address the server will listen for websocket connections.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
default = { };
|
||||||
|
description = ''
|
||||||
|
postgres-websockets configuration as defined in:
|
||||||
|
<https://github.com/diogob/postgres-websockets/blob/master/src/PostgresWebsockets/Config.hs#L71-L87>
|
||||||
|
|
||||||
|
`PGWS_DB_URI` is represented as an attribute set, see [`environment.PGWS_DB_URI`](#opt-services.postgres-websockets.environment.PGWS_DB_URI)
|
||||||
|
|
||||||
|
::: {.note}
|
||||||
|
The `environment.PGWS_JWT_SECRET` option is blocked.
|
||||||
|
Use [`jwtSecretFile`](#opt-services.postgres-websockets.jwtSecretFile) instead.
|
||||||
|
:::
|
||||||
|
'';
|
||||||
|
example = lib.literalExpression ''
|
||||||
|
{
|
||||||
|
PGWS_LISTEN_CHANNEL = "my_channel";
|
||||||
|
PGWS_DB_URI.dbname = "postgres";
|
||||||
|
}
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
config = lib.mkIf cfg.enable {
|
||||||
|
services.postgres-websockets.environment.PGWS_DB_URI.application_name =
|
||||||
|
with pkgs.postgres-websockets;
|
||||||
|
"${pname} ${version}";
|
||||||
|
|
||||||
|
systemd.services.postgres-websockets = {
|
||||||
|
description = "postgres-websockets";
|
||||||
|
|
||||||
|
wantedBy = [ "multi-user.target" ];
|
||||||
|
wants = [ "network-online.target" ];
|
||||||
|
after = [
|
||||||
|
"network-online.target"
|
||||||
|
"postgresql.service"
|
||||||
|
];
|
||||||
|
|
||||||
|
environment =
|
||||||
|
cfg.environment
|
||||||
|
// {
|
||||||
|
inherit PGWS_DB_URI;
|
||||||
|
PGWS_JWT_SECRET = "@%d/jwt_secret";
|
||||||
|
}
|
||||||
|
// lib.optionalAttrs (cfg.pgpassFile != null) {
|
||||||
|
PGPASSFILE = "%C/postgres-websockets/pgpass";
|
||||||
|
};
|
||||||
|
|
||||||
|
serviceConfig = {
|
||||||
|
CacheDirectory = "postgres-websockets";
|
||||||
|
CacheDirectoryMode = "0700";
|
||||||
|
LoadCredential = [
|
||||||
|
"jwt_secret:${cfg.jwtSecretFile}"
|
||||||
|
] ++ lib.optional (cfg.pgpassFile != null) "pgpass:${cfg.pgpassFile}";
|
||||||
|
Restart = "always";
|
||||||
|
User = "postgres-websockets";
|
||||||
|
|
||||||
|
# Hardening
|
||||||
|
CapabilityBoundingSet = [ "" ];
|
||||||
|
DevicePolicy = "closed";
|
||||||
|
DynamicUser = true;
|
||||||
|
LockPersonality = true;
|
||||||
|
MemoryDenyWriteExecute = true;
|
||||||
|
NoNewPrivileges = true;
|
||||||
|
PrivateDevices = true;
|
||||||
|
PrivateIPC = true;
|
||||||
|
PrivateMounts = true;
|
||||||
|
ProcSubset = "pid";
|
||||||
|
ProtectClock = true;
|
||||||
|
ProtectControlGroups = true;
|
||||||
|
ProtectHostname = true;
|
||||||
|
ProtectKernelLogs = true;
|
||||||
|
ProtectKernelModules = true;
|
||||||
|
ProtectKernelTunables = true;
|
||||||
|
ProtectProc = "invisible";
|
||||||
|
RestrictAddressFamilies = [
|
||||||
|
"AF_INET"
|
||||||
|
"AF_INET6"
|
||||||
|
"AF_UNIX"
|
||||||
|
];
|
||||||
|
RestrictNamespaces = true;
|
||||||
|
RestrictRealtime = true;
|
||||||
|
SystemCallArchitectures = "native";
|
||||||
|
SystemCallFilter = [ "" ];
|
||||||
|
UMask = "0077";
|
||||||
|
};
|
||||||
|
|
||||||
|
# Copy the pgpass file to different location, to have it report mode 0400.
|
||||||
|
# Fixes: https://github.com/systemd/systemd/issues/29435
|
||||||
|
script = ''
|
||||||
|
if [ -f "$CREDENTIALS_DIRECTORY/pgpass" ]; then
|
||||||
|
cp -f "$CREDENTIALS_DIRECTORY/pgpass" "$CACHE_DIRECTORY/pgpass"
|
||||||
|
fi
|
||||||
|
exec ${lib.getExe pkgs.postgres-websockets}
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
|
@ -1080,6 +1080,7 @@ in
|
||||||
handleTest ./postfix-raise-smtpd-tls-security-level.nix
|
handleTest ./postfix-raise-smtpd-tls-security-level.nix
|
||||||
{ };
|
{ };
|
||||||
postfixadmin = handleTest ./postfixadmin.nix { };
|
postfixadmin = handleTest ./postfixadmin.nix { };
|
||||||
|
postgres-websockets = runTest ./postgres-websockets.nix;
|
||||||
postgresql = handleTest ./postgresql { };
|
postgresql = handleTest ./postgresql { };
|
||||||
postgrest = runTest ./postgrest.nix;
|
postgrest = runTest ./postgrest.nix;
|
||||||
powerdns = handleTest ./powerdns.nix { };
|
powerdns = handleTest ./powerdns.nix { };
|
||||||
|
|
84
nixos/tests/postgres-websockets.nix
Normal file
84
nixos/tests/postgres-websockets.nix
Normal file
|
@ -0,0 +1,84 @@
|
||||||
|
{ lib, ... }:
|
||||||
|
{
|
||||||
|
name = "postgres-websockets";
|
||||||
|
|
||||||
|
meta = {
|
||||||
|
maintainers = with lib.maintainers; [ wolfgangwalther ];
|
||||||
|
};
|
||||||
|
|
||||||
|
nodes.machine =
|
||||||
|
{
|
||||||
|
config,
|
||||||
|
lib,
|
||||||
|
pkgs,
|
||||||
|
...
|
||||||
|
}:
|
||||||
|
{
|
||||||
|
environment.systemPackages = [ pkgs.websocat ];
|
||||||
|
|
||||||
|
services.postgresql = {
|
||||||
|
enable = true;
|
||||||
|
initialScript = pkgs.writeText "init.sql" ''
|
||||||
|
CREATE ROLE "postgres-websockets" LOGIN NOINHERIT;
|
||||||
|
CREATE ROLE "postgres-websockets_with_password" LOGIN NOINHERIT PASSWORD 'password';
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
services.postgres-websockets = {
|
||||||
|
enable = true;
|
||||||
|
jwtSecretFile = "/run/secrets/jwt.secret";
|
||||||
|
environment.PGWS_DB_URI.dbname = "postgres";
|
||||||
|
environment.PGWS_LISTEN_CHANNEL = "websockets-listener";
|
||||||
|
};
|
||||||
|
|
||||||
|
specialisation.withPassword.configuration = {
|
||||||
|
services.postgresql.enableTCPIP = true;
|
||||||
|
services.postgres-websockets = {
|
||||||
|
pgpassFile = "/run/secrets/.pgpass";
|
||||||
|
environment.PGWS_DB_URI.host = "localhost";
|
||||||
|
environment.PGWS_DB_URI.user = "postgres-websockets_with_password";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
extraPythonPackages = p: [ p.pyjwt ];
|
||||||
|
|
||||||
|
testScript =
|
||||||
|
{ nodes, ... }:
|
||||||
|
let
|
||||||
|
withPassword = "${nodes.machine.system.build.toplevel}/specialisation/withPassword";
|
||||||
|
in
|
||||||
|
''
|
||||||
|
machine.execute("""
|
||||||
|
mkdir -p /run/secrets
|
||||||
|
echo reallyreallyreallyreallyverysafe > /run/secrets/jwt.secret
|
||||||
|
""")
|
||||||
|
|
||||||
|
import jwt
|
||||||
|
token = jwt.encode({ "mode": "rw" }, "reallyreallyreallyreallyverysafe")
|
||||||
|
|
||||||
|
def test():
|
||||||
|
machine.wait_for_unit("postgresql.service")
|
||||||
|
machine.wait_for_unit("postgres-websockets.service")
|
||||||
|
|
||||||
|
machine.succeed(f"echo 'hi there' | websocat --no-close 'ws://localhost:3000/test/{token}' > output &")
|
||||||
|
machine.sleep(1)
|
||||||
|
machine.succeed("grep 'hi there' output")
|
||||||
|
|
||||||
|
machine.succeed("""
|
||||||
|
sudo -u postgres psql -c "SELECT pg_notify('websockets-listener', json_build_object('channel', 'test', 'event', 'message', 'payload', 'Hello World')::text);" >/dev/null
|
||||||
|
""")
|
||||||
|
machine.sleep(1)
|
||||||
|
machine.succeed("grep 'Hello World' output")
|
||||||
|
|
||||||
|
with subtest("without password"):
|
||||||
|
test()
|
||||||
|
|
||||||
|
with subtest("with password"):
|
||||||
|
machine.execute("""
|
||||||
|
echo "*:*:*:*:password" > /run/secrets/.pgpass
|
||||||
|
""")
|
||||||
|
machine.succeed("${withPassword}/bin/switch-to-configuration test >&2")
|
||||||
|
test()
|
||||||
|
'';
|
||||||
|
}
|
|
@ -481,7 +481,10 @@ builtins.intersectAttrs super {
|
||||||
hasql-transaction = dontCheck super.hasql-transaction;
|
hasql-transaction = dontCheck super.hasql-transaction;
|
||||||
|
|
||||||
# Avoid compiling twice by providing executable as a separate output (with small closure size),
|
# Avoid compiling twice by providing executable as a separate output (with small closure size),
|
||||||
postgres-websockets = enableSeparateBinOutput super.postgres-websockets;
|
postgres-websockets = lib.pipe super.postgres-websockets [
|
||||||
|
enableSeparateBinOutput
|
||||||
|
(overrideCabal { passthru.tests = pkgs.nixosTests.postgres-websockets; })
|
||||||
|
];
|
||||||
|
|
||||||
# Test suite requires a running postgresql server,
|
# Test suite requires a running postgresql server,
|
||||||
# avoid compiling twice by providing executable as a separate output (with small closure size),
|
# avoid compiling twice by providing executable as a separate output (with small closure size),
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue