Skip to content

Commit

Permalink
nixos/froide-govplan: init
Browse files Browse the repository at this point in the history
  • Loading branch information
onny committed Dec 11, 2024
1 parent f528c04 commit eadd8e4
Show file tree
Hide file tree
Showing 4 changed files with 329 additions and 0 deletions.
2 changes: 2 additions & 0 deletions nixos/doc/manual/release-notes/rl-2505.section.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@

- [Amazon CloudWatch Agent](https://github.com/aws/amazon-cloudwatch-agent), the official telemetry collector for AWS CloudWatch and AWS X-Ray. Available as [services.amazon-cloudwatch-agent](options.html#opt-services.amazon-cloudwatch-agent.enable).

- [Froide-Govplan](https://github.com/okfde/froide-govplan), a web application government planer. Available as [services.froide-govplan](#opt-services.froide-govplan.enable).

- [agorakit](https://github.com/agorakit/agorakit), an organization tool for citizens' collectives. Available with [services.agorakit](options.html#opt-services.agorakit.enable).

- [mqtt-exporter](https://github.com/kpetremann/mqtt-exporter/), a Prometheus exporter for exposing messages from MQTT. Available as [services.prometheus.exporters.mqtt](#opt-services.prometheus.exporters.mqtt.enable).
Expand Down
1 change: 1 addition & 0 deletions nixos/modules/module-list.nix
Original file line number Diff line number Diff line change
Expand Up @@ -1444,6 +1444,7 @@
./services/web-apps/flarum.nix
./services/web-apps/fluidd.nix
./services/web-apps/freshrss.nix
./services/web-apps/froide-govplan.nix
./services/web-apps/galene.nix
./services/web-apps/gancio.nix
./services/web-apps/gerrit.nix
Expand Down
217 changes: 217 additions & 0 deletions nixos/modules/services/web-apps/froide-govplan.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,217 @@
{
config,
lib,
pkgs,
...
}:
let

cfg = config.services.froide-govplan;
pythonFmt = pkgs.formats.pythonVars { };
settingsFile = pythonFmt.generate "extra_settings.py" cfg.settings;

pkg = cfg.package.overridePythonAttrs (old: {
postInstall =
old.postInstall
+ ''
ln -s ${settingsFile} $out/${pkg.python.sitePackages}/froide_govplan/project/extra_settings.py
'';
});

# Service hardening
defaultServiceConfig = {
# Secure the services
ReadWritePaths = [ cfg.dataDir ];
CacheDirectory = "froide-govplan";
CapabilityBoundingSet = "";
# ProtectClock adds DeviceAllow=char-rtc r
DeviceAllow = "";
LockPersonality = true;
MemoryDenyWriteExecute = true;
NoNewPrivileges = true;
PrivateDevices = true;
PrivateMounts = true;
# FIXME: Depends on database configuration, socket vs address
#PrivateNetwork = true;
PrivateTmp = true;
PrivateUsers = true;
ProtectClock = true;
ProtectHome = true;
ProtectHostname = true;
ProtectSystem = "strict";
ProtectControlGroups = true;
ProtectKernelLogs = true;
ProtectKernelModules = true;
ProtectKernelTunables = true;
ProtectProc = "invisible";
ProcSubset = "pid";
RestrictAddressFamilies = [
"AF_UNIX"
"AF_INET"
"AF_INET6"
];
RestrictNamespaces = true;
RestrictRealtime = true;
RestrictSUIDSGID = true;
SystemCallArchitectures = "native";
SystemCallFilter = [
"@system-service"
"~@privileged @setuid @keyring"
];
UMask = "0066";
};

in
{
options.services.froide-govplan = {

enable = lib.mkEnableOption "Gouvernment planer web app Govplan";

package = lib.mkPackageOption pkgs "froide-govplan" { };

listenAddress = lib.mkOption {
type = lib.types.str;
default = "[::1]";
description = ''
Address the server will listen on.
'';
};

port = lib.mkOption {
type = lib.types.port;
default = 8080;
description = "Web interface port.";
};

dataDir = lib.mkOption {
type = lib.types.str;
default = "/var/lib/froide-govplan";
description = "Directory to store the Froide-Govplan server data.";
};

openFirewall = lib.mkOption {
type = lib.types.bool;
default = false;
description = ''
Whether to open ports in the firewall for the server.
'';
};

settings = lib.mkOption {
description = ''
Configuration options to set in `extra_settings.py`.
'';

default = { };

type = lib.types.submodule {
freeformType = pythonFmt.type;

options = {
ALLOWED_HOSTS = lib.mkOption {
type = with lib.types; listOf str;
default = [ "*" ];
description = ''
A list of valid fully-qualified domain names (FQDNs) and/or IP
addresses that can be used to reach the Froide-Govplan service.
'';
};
};
};
};

};

config = lib.mkIf cfg.enable {

services.froide-govplan = {
settings = {
STATIC_ROOT = "${cfg.dataDir}/static";
DEBUG = false;
};
};

services.postgresql = {
enable = true;
ensureDatabases = [ "govplan" ];
ensureUsers = [
{
name = "govplan";
ensureDBOwnership = true;
}
];
extensions = ps: with ps; [ postgis ];
authentication = ''
host govplan govplan localhost trust
'';
initialScript = pkgs.writeText "backend-initScript" ''
ALTER USER govplan WITH SUPERUSER;
'';
};

systemd = {
services = {

postgresql.serviceConfig.ExecStartPost =
let
sqlFile = pkgs.writeText "immich-pgvectors-setup.sql" ''
ALTER USER govplan WITH SUPERUSER;
#CREATE EXTENSION IF NOT EXISTS postgis;
#ALTER SCHEMA govplan OWNER TO govplan;
#ALTER EXTENSION govplan UPDATE;
'';
in
[
''
${lib.getExe' config.services.postgresql.package "psql"} -d govplan -f "${sqlFile}"
''
];

froide-govplan = {
description = "Gouvernment planer Govplan";
serviceConfig = defaultServiceConfig // {
WorkingDirectory = cfg.dataDir;
StateDirectory = lib.mkIf (cfg.dataDir == "/var/lib/froide-govplan") "froide-govplan";
DynamicUser = true;
};
after = [ "postgresql.service" ];
wantedBy = [ "multi-user.target" ];
environment = {
PYTHONPATH = pkg.pythonPath;
GDAL_LIBRARY_PATH = "${pkgs.gdal}/lib/libgdal.so";
GEOS_LIBRARY_PATH = "${pkgs.geos}/lib/libgeos_c.so";
};
preStart = ''
# Auto-migrate on first run or if the package has changed
versionFile="${cfg.dataDir}/src-version"
version=$(cat "$versionFile" 2>/dev/null || echo 0)
if [[ $version != ${pkg.version} ]]; then
${lib.getExe pkg} migrate --no-input
${lib.getExe pkg} collectstatic --no-input --clear
echo ${pkg.version} > "$versionFile"
fi
'';
script =
let
python = pkg.python;
networking = "--host ${cfg.listenAddress} --port ${toString cfg.port}";
in ''
${python.pkgs.uvicorn}/bin/uvicorn ${networking} \
--app-dir ${pkg}/${pkg.python.sitePackages} \
project.asgi:application
'';
};
};

};

networking.firewall = lib.mkIf cfg.openFirewall { allowedTCPPorts = [ cfg.port ]; };

environment.systemPackages = [ pkg ];

};

meta.maintainers = with lib.maintainers; [ onny ];

}
109 changes: 109 additions & 0 deletions nixos/tests/froide-govplan.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import ./make-test-python.nix (
{ lib, ... }:
{
name = "paperless";
meta.maintainers = with lib.maintainers; [
leona
SuperSandro2000
erikarvstedt
];

nodes =
let
self = {
simple =
{ pkgs, ... }:
{
environment.systemPackages = with pkgs; [
imagemagick
jq
];
services.paperless = {
enable = true;
passwordFile = builtins.toFile "password" "admin";
};
};
postgres =
{ config, pkgs, ... }:
{
imports = [ self.simple ];
services.postgresql = {
enable = true;
ensureDatabases = [ "paperless" ];
ensureUsers = [
{
name = config.services.paperless.user;
ensureDBOwnership = true;
}
];
};
services.paperless.settings = {
PAPERLESS_DBHOST = "/run/postgresql";
PAPERLESS_OCR_LANGUAGE = "deu";
};
};
};
in
self;

testScript = ''
import json
def test_paperless(node):
node.wait_for_unit("paperless-consumer.service")
with subtest("Add a document via the file system"):
node.succeed(
"convert -size 400x40 xc:white -font 'DejaVu-Sans' -pointsize 20 -fill black "
"-annotate +5+20 'hello world 16-10-2005' /var/lib/paperless/consume/doc.png"
)
with subtest("Web interface gets ready"):
node.wait_for_unit("paperless-web.service")
# Wait until server accepts connections
node.wait_until_succeeds("curl -fs localhost:28981")
# Required for consuming documents via the web interface
with subtest("Task-queue gets ready"):
node.wait_for_unit("paperless-task-queue.service")
with subtest("Add a png document via the web interface"):
node.succeed(
"convert -size 400x40 xc:white -font 'DejaVu-Sans' -pointsize 20 -fill black "
"-annotate +5+20 'hello web 16-10-2005' /tmp/webdoc.png"
)
node.wait_until_succeeds("curl -u admin:admin -F document=@/tmp/webdoc.png -fs localhost:28981/api/documents/post_document/")
with subtest("Add a txt document via the web interface"):
node.succeed(
"echo 'hello web 16-10-2005' > /tmp/webdoc.txt"
)
node.wait_until_succeeds("curl -u admin:admin -F document=@/tmp/webdoc.txt -fs localhost:28981/api/documents/post_document/")
with subtest("Documents are consumed"):
node.wait_until_succeeds(
"(($(curl -u admin:admin -fs localhost:28981/api/documents/ | jq .count) == 3))"
)
docs = json.loads(node.succeed("curl -u admin:admin -fs localhost:28981/api/documents/"))['results']
assert "2005-10-16" in docs[0]['created']
assert "2005-10-16" in docs[1]['created']
assert "2005-10-16" in docs[2]['created']
# Detects gunicorn issues, see PR #190888
with subtest("Document metadata can be accessed"):
metadata = json.loads(node.succeed("curl -u admin:admin -fs localhost:28981/api/documents/1/metadata/"))
assert "original_checksum" in metadata
metadata = json.loads(node.succeed("curl -u admin:admin -fs localhost:28981/api/documents/2/metadata/"))
assert "original_checksum" in metadata
metadata = json.loads(node.succeed("curl -u admin:admin -fs localhost:28981/api/documents/3/metadata/"))
assert "original_checksum" in metadata
test_paperless(simple)
simple.send_monitor_command("quit")
simple.wait_for_shutdown()
test_paperless(postgres)
'';
}
)

0 comments on commit eadd8e4

Please sign in to comment.