pinchflat: init

This commit is contained in:
ibizaman 2025-09-16 21:17:46 +02:00 committed by Pierre Penninckx
parent 3f74b4ecc4
commit 98b4cc32b6
7 changed files with 610 additions and 8 deletions

View file

@ -163,6 +163,7 @@ Also, the stack fits together nicely thanks to [contracts](#contracts).
- Hledger
- Home-Assistant
- Jellyfin
- Pinchflat
- Vaultwarden
Like explained above, those services all benefit from

View file

@ -3320,6 +3320,129 @@
"services-nextcloudserver-usage-version": [
"services-nextcloud.html#services-nextcloudserver-usage-version"
],
"services-pinchflat": [
"services-pinchflat.html#services-pinchflat"
],
"services-pinchflat-options": [
"services-pinchflat.html#services-pinchflat-options"
],
"services-pinchflat-options-shb.pinchflat.backup": [
"services-pinchflat.html#services-pinchflat-options-shb.pinchflat.backup"
],
"services-pinchflat-options-shb.pinchflat.backup.request": [
"services-pinchflat.html#services-pinchflat-options-shb.pinchflat.backup.request"
],
"services-pinchflat-options-shb.pinchflat.backup.request.excludePatterns": [
"services-pinchflat.html#services-pinchflat-options-shb.pinchflat.backup.request.excludePatterns"
],
"services-pinchflat-options-shb.pinchflat.backup.request.hooks": [
"services-pinchflat.html#services-pinchflat-options-shb.pinchflat.backup.request.hooks"
],
"services-pinchflat-options-shb.pinchflat.backup.request.hooks.afterBackup": [
"services-pinchflat.html#services-pinchflat-options-shb.pinchflat.backup.request.hooks.afterBackup"
],
"services-pinchflat-options-shb.pinchflat.backup.request.hooks.beforeBackup": [
"services-pinchflat.html#services-pinchflat-options-shb.pinchflat.backup.request.hooks.beforeBackup"
],
"services-pinchflat-options-shb.pinchflat.backup.request.sourceDirectories": [
"services-pinchflat.html#services-pinchflat-options-shb.pinchflat.backup.request.sourceDirectories"
],
"services-pinchflat-options-shb.pinchflat.backup.request.user": [
"services-pinchflat.html#services-pinchflat-options-shb.pinchflat.backup.request.user"
],
"services-pinchflat-options-shb.pinchflat.backup.result": [
"services-pinchflat.html#services-pinchflat-options-shb.pinchflat.backup.result"
],
"services-pinchflat-options-shb.pinchflat.backup.result.backupService": [
"services-pinchflat.html#services-pinchflat-options-shb.pinchflat.backup.result.backupService"
],
"services-pinchflat-options-shb.pinchflat.backup.result.restoreScript": [
"services-pinchflat.html#services-pinchflat-options-shb.pinchflat.backup.result.restoreScript"
],
"services-pinchflat-options-shb.pinchflat.domain": [
"services-pinchflat.html#services-pinchflat-options-shb.pinchflat.domain"
],
"services-pinchflat-options-shb.pinchflat.enable": [
"services-pinchflat.html#services-pinchflat-options-shb.pinchflat.enable"
],
"services-pinchflat-options-shb.pinchflat.ldap": [
"services-pinchflat.html#services-pinchflat-options-shb.pinchflat.ldap"
],
"services-pinchflat-options-shb.pinchflat.ldap.enable": [
"services-pinchflat.html#services-pinchflat-options-shb.pinchflat.ldap.enable"
],
"services-pinchflat-options-shb.pinchflat.ldap.userGroup": [
"services-pinchflat.html#services-pinchflat-options-shb.pinchflat.ldap.userGroup"
],
"services-pinchflat-options-shb.pinchflat.mediaDir": [
"services-pinchflat.html#services-pinchflat-options-shb.pinchflat.mediaDir"
],
"services-pinchflat-options-shb.pinchflat.port": [
"services-pinchflat.html#services-pinchflat-options-shb.pinchflat.port"
],
"services-pinchflat-options-shb.pinchflat.secretKeyBase": [
"services-pinchflat.html#services-pinchflat-options-shb.pinchflat.secretKeyBase"
],
"services-pinchflat-options-shb.pinchflat.secretKeyBase.request": [
"services-pinchflat.html#services-pinchflat-options-shb.pinchflat.secretKeyBase.request"
],
"services-pinchflat-options-shb.pinchflat.secretKeyBase.request.group": [
"services-pinchflat.html#services-pinchflat-options-shb.pinchflat.secretKeyBase.request.group"
],
"services-pinchflat-options-shb.pinchflat.secretKeyBase.request.mode": [
"services-pinchflat.html#services-pinchflat-options-shb.pinchflat.secretKeyBase.request.mode"
],
"services-pinchflat-options-shb.pinchflat.secretKeyBase.request.owner": [
"services-pinchflat.html#services-pinchflat-options-shb.pinchflat.secretKeyBase.request.owner"
],
"services-pinchflat-options-shb.pinchflat.secretKeyBase.request.restartUnits": [
"services-pinchflat.html#services-pinchflat-options-shb.pinchflat.secretKeyBase.request.restartUnits"
],
"services-pinchflat-options-shb.pinchflat.secretKeyBase.result": [
"services-pinchflat.html#services-pinchflat-options-shb.pinchflat.secretKeyBase.result"
],
"services-pinchflat-options-shb.pinchflat.secretKeyBase.result.path": [
"services-pinchflat.html#services-pinchflat-options-shb.pinchflat.secretKeyBase.result.path"
],
"services-pinchflat-options-shb.pinchflat.ssl": [
"services-pinchflat.html#services-pinchflat-options-shb.pinchflat.ssl"
],
"services-pinchflat-options-shb.pinchflat.ssl.paths": [
"services-pinchflat.html#services-pinchflat-options-shb.pinchflat.ssl.paths"
],
"services-pinchflat-options-shb.pinchflat.ssl.paths.cert": [
"services-pinchflat.html#services-pinchflat-options-shb.pinchflat.ssl.paths.cert"
],
"services-pinchflat-options-shb.pinchflat.ssl.paths.key": [
"services-pinchflat.html#services-pinchflat-options-shb.pinchflat.ssl.paths.key"
],
"services-pinchflat-options-shb.pinchflat.ssl.systemdService": [
"services-pinchflat.html#services-pinchflat-options-shb.pinchflat.ssl.systemdService"
],
"services-pinchflat-options-shb.pinchflat.sso": [
"services-pinchflat.html#services-pinchflat-options-shb.pinchflat.sso"
],
"services-pinchflat-options-shb.pinchflat.sso.authEndpoint": [
"services-pinchflat.html#services-pinchflat-options-shb.pinchflat.sso.authEndpoint"
],
"services-pinchflat-options-shb.pinchflat.sso.authorization_policy": [
"services-pinchflat.html#services-pinchflat-options-shb.pinchflat.sso.authorization_policy"
],
"services-pinchflat-options-shb.pinchflat.sso.enable": [
"services-pinchflat.html#services-pinchflat-options-shb.pinchflat.sso.enable"
],
"services-pinchflat-options-shb.pinchflat.subdomain": [
"services-pinchflat.html#services-pinchflat-options-shb.pinchflat.subdomain"
],
"services-pinchflat-options-shb.pinchflat.timeZone": [
"services-pinchflat.html#services-pinchflat-options-shb.pinchflat.timeZone"
],
"services-pinchflat-usage": [
"services-pinchflat.html#services-pinchflat-usage"
],
"services-pinchflat-usage-backup": [
"services-pinchflat.html#services-pinchflat-usage-backup"
],
"services-vaultwarden": [
"services-vaultwarden.html#services-vaultwarden"
],

View file

@ -11,14 +11,15 @@ Not all services are yet documented. You can find all available services [in the
The following table summarizes for each documented service what features it provides. More
information is provided in the respective manual sections.
| Service | Backup | Reverse Proxy | SSO | LDAP | Monitoring | Profiling |
|-----------------------|--------|---------------|-----|-------|------------|-----------|
| [*Arr][] | Y (1) | Y | Y | Y (4) | Y (2) | N |
| [Forgejo][] | Y (1) | Y | Y | Y | Y (2) | N |
| [Jellyfin][] | Y (1) | Y | Y | Y | Y (2) | N |
| [Home-Assistant][] | Y (1) | Y | N | Y | Y (2) | N |
| [Nextcloud Server][] | Y (1) | Y | Y | Y | Y (2) | P (3) |
| [Vaultwarden][] | Y (1) | Y | Y | Y | Y (2) | N |
| Service | Backup | Reverse Proxy | SSO | LDAP | Monitoring | Profiling |
|----------------------|--------|---------------|-----|-------|------------|-----------|
| [*Arr][] | Y (1) | Y | Y | Y (4) | Y (2) | N |
| [Forgejo][] | Y (1) | Y | Y | Y | Y (2) | N |
| [Jellyfin][] | Y (1) | Y | Y | Y | Y (2) | N |
| [Home-Assistant][] | Y (1) | Y | N | Y | Y (2) | N |
| [Nextcloud Server][] | Y (1) | Y | Y | Y | Y (2) | P (3) |
| [Pinchflat][] | Y | Y | Y | Y (4) | Y (5) | N |
| [Vaultwarden][] | Y (1) | Y | Y | Y | Y (2) | N |
Legend: **N**: no but WIP; **P**: partial; **Y**: yes
@ -34,6 +35,7 @@ Legend: **N**: no but WIP; **P**: partial; **Y**: yes
[Home-Assistant]: services-home-assistant.html
[Jellyfin]: services-jellyfin.html
[Nextcloud Server]: services-nextcloud.html
[Pinchflat]: services-pinchflat.html
[Vaultwarden]: services-vaultwarden.html
```{=include=} chapters html:into-file=//services-arr.html
@ -56,6 +58,10 @@ modules/services/home-assistant/docs/default.md
modules/services/nextcloud-server/docs/default.md
```
```{=include=} chapters html:into-file=//services-pinchflat.html
modules/services/pinchflat/docs/default.md
```
```{=include=} chapters html:into-file=//services-vaultwarden.html
modules/services/vaultwarden/docs/default.md
```

View file

@ -81,6 +81,7 @@
self.nixosModules.${system}.assistant
self.nixosModules.${system}.jellyfin
self.nixosModules.${system}.nextcloud-server
self.nixosModules.${system}.pinchflat
self.nixosModules.${system}.vaultwarden
];
};
@ -114,6 +115,7 @@
nixosModules.assistant = modules/services/home-assistant.nix;
nixosModules.jellyfin = modules/services/jellyfin.nix;
nixosModules.nextcloud-server = modules/services/nextcloud-server.nix;
nixosModules.pinchflat = modules/services/pinchflat.nix;
nixosModules.vaultwarden = modules/services/vaultwarden.nix;
packages.manualHtml = pkgs.callPackage ./docs {
@ -142,6 +144,7 @@
"services/home-assistant" = ./modules/services/home-assistant.nix;
"services/jellyfin" = ./modules/services/jellyfin.nix;
"services/nextcloud-server" = ./modules/services/nextcloud-server.nix;
"services/pinchflat" = ./modules/services/pinchflat.nix;
"services/vaultwarden" = ./modules/services/vaultwarden.nix;
"contracts/backup" = ./modules/contracts/backup/dummyModule.nix;
"contracts/databasebackup" = ./modules/contracts/databasebackup/dummyModule.nix;
@ -257,6 +260,7 @@
// (vm_test "jellyfin" ./test/services/jellyfin.nix)
// (vm_test "monitoring" ./test/services/monitoring.nix)
// (vm_test "nextcloud" ./test/services/nextcloud.nix)
// (vm_test "pinchflat" ./test/services/pinchflat.nix)
// (vm_test "vaultwarden" ./test/services/vaultwarden.nix)
// (vm_test "authelia" ./test/blocks/authelia.nix)

View file

@ -0,0 +1,191 @@
{ config, lib, pkgs, ... }:
let
cfg = config.shb.pinchflat;
inherit (lib) types;
contracts = pkgs.callPackage ../contracts {};
shblib = pkgs.callPackage ../../lib {};
in
{
options.shb.pinchflat = {
enable = lib.mkEnableOption "the Pinchflat service.";
subdomain = lib.mkOption {
type = lib.types.str;
description = "Subdomain under which Pinchflat will be served.";
default = "pinchflat";
};
domain = lib.mkOption {
type = lib.types.str;
description = "domain under which Pinchflat will be served.";
example = "mydomain.com";
};
ssl = lib.mkOption {
description = "Path to SSL files";
type = lib.types.nullOr contracts.ssl.certs;
default = null;
};
port = lib.mkOption {
type = lib.types.port;
description = "Port Pinchflat listens to incoming requests.";
default = 8945;
};
secretKeyBase = lib.mkOption {
description = ''
Used to sign/encrypt cookies and other secrets.
Make sure the secret is at least 64 characters long.
'';
type = types.submodule {
options = contracts.secret.mkRequester {
restartUnits = [ "pinchflat.service" ];
};
};
};
mediaDir = lib.mkOption {
description = "Path where videos are stored.";
type = lib.types.str;
};
timeZone = lib.mkOption {
type = lib.types.oneOf [ lib.types.str shblib.secretFileType ];
description = "Timezone of this instance.";
example = "America/Los_Angeles";
};
ldap = lib.mkOption {
description = ''
Setup LDAP integration.
'';
default = {};
type = types.submodule {
options = {
enable = lib.mkEnableOption "LDAP integration." // {
default = cfg.sso.enable;
};
userGroup = lib.mkOption {
type = types.str;
description = "Group users must belong to be able to login.";
default = "pinchflat_user";
};
};
};
};
sso = lib.mkOption {
description = ''
Setup SSO integration.
'';
default = {};
type = types.submodule {
options = {
enable = lib.mkEnableOption "SSO integration.";
authEndpoint = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = null;
description = "Endpoint to the SSO provider. Leave null to not have SSO configured.";
example = "https://authelia.example.com";
};
authorization_policy = lib.mkOption {
type = types.enum [ "one_factor" "two_factor" ];
description = "Require one factor (password) or two factor (device) authentication.";
default = "one_factor";
};
};
};
};
backup = lib.mkOption {
description = ''
Backup media directory `shb.mediaDir`.
'';
default = {};
type = lib.types.submodule {
options = contracts.backup.mkRequester {
user = "pinchflat";
sourceDirectories = [
cfg.mediaDir
];
sourceDirectoriesText = "[ config.shb.pinchflat.mediaDir ]";
};
};
};
};
config = lib.mkIf cfg.enable {
systemd.tmpfiles.rules = [
"d '/etc/pinchflat' 0750 root root - -"
];
# Pinchflat relies on the global value so for now this is the only way to pass the option in.
time.timeZone = lib.mkDefault cfg.timeZone;
services.pinchflat = {
inherit (cfg) enable port mediaDir;
secretsFile = "/etc/pinchflat/secrets.env";
extraConfig = {
ENABLE_PROMETHEUS = true;
# TZ = "as"; # I consider where you live to be sensible so it should be passed as a secret.
};
};
# This should be using a contract instead of setting the option directly.
shb.lldap = lib.mkIf config.shb.ldap.enable {
ensureGroups = { ${cfg.ldap.userGroup} = {}; };
};
systemd.services.pinchflat-pre = {
script = shblib.replaceSecrets {
userConfig = {
SECRET_KEY_BASE.source = cfg.secretKeyBase.result.path;
# TZ = cfg.secretKeyBase.result.path; # Uncomment when PR is merged.
};
resultPath = "/etc/pinchflat/secrets.env";
generator = shblib.toEnvVar;
};
serviceConfig.Type = "oneshot";
wantedBy = [ "multi-user.target" ];
before = [ "pinchflat.service" ];
requiredBy = [ "pinchflat.service" ];
};
shb.nginx.vhosts = [
{
inherit (cfg) subdomain domain ssl;
inherit (cfg.sso) authEndpoint;
upstream = "http://127.0.0.1:${toString cfg.port}";
autheliaRules = lib.optionals (cfg.sso.enable) [
{
domain = "${cfg.subdomain}.${cfg.domain}";
policy = cfg.sso.authorization_policy;
subject = [ "group:${cfg.ldap.userGroup}" ];
}
];
}
];
services.prometheus.scrapeConfigs = [
{
job_name = "pinchflat";
static_configs = [
{
targets = ["127.0.0.1:${toString cfg.port}"];
labels = {
"hostname" = config.networking.hostName;
"domain" = cfg.domain;
};
}
];
}
];
};
}

View file

@ -0,0 +1,70 @@
# Pinchflat Service {#services-pinchflat}
Defined in [`/modules/services/pinchflat.nix`](@REPO@/modules/services/pinchflat.nix).
This NixOS module is a service that sets up a [Pinchflat](https://github.com/kieraneglin/pinchflat) instance.
Compared to the stock module from nixpkgs,
this one sets up, in a fully declarative manner,
LDAP and SSO integration
and has a nicer option for secrets.
## Usage {#services-pinchflat-usage}
The following snippet assumes a few blocks have been setup already:
- the [secrets block](usage.html#usage-secrets) with SOPS,
- the [`shb.ssl` block](blocks-ssl.html#usage),
- the [`shb.lldap` block](blocks-lldap.html#blocks-lldap-global-setup).
- the [`shb.authelia` block](blocks-authelia.html#blocks-sso-global-setup).
```nix
shb.pinchflat = {
enable = true;
secretKeyBase.result = config.shb.sops.secret."pinchflat/secretKeyBase".result;
timeZone = "Europe/Brussels";
mediaDir = "/srv/pinchflat";
domain = "example.com";
subdomain = "pinchflat";
ssl = config.shb.certs.certs.letsencrypt.${domain};
ldap = {
enable = true;
userGroup = "pinchflat_user";
};
sso = {
enable = true;
authEndpoint = "https://${config.shb.authelia.subdomain}.${config.shb.authelia.domain}";
};
};
shb.sops.secret."pinchflat/secretKeyBase".request = config.shb.pinchflat.secretKeyBase.request;
```
Secrets can be randomly generated with `nix run nixpkgs#openssl -- rand -hex 64`.
## Backup {#services-pinchflat-usage-backup}
Backing up Pinchflat using the [Restic block](blocks-restic.html) is done like so:
```nix
shb.restic.instances."pinchflat" = {
request = config.shb.pinchflat.backup;
settings = {
enable = true;
};
};
```
The name `"pinchflat"` in the `instances` can be anything.
The `config.shb.pinchflat.backup` option provides what directories to backup.
You can define any number of Restic instances to backup Pinchflat multiple times.
## Options Reference {#services-pinchflat-options}
```{=include=} options
id-prefix: services-pinchflat-options-
list-id: selfhostblocks-service-pinchflat-options
source: @OPTIONS_JSON@
```

207
test/services/pinchflat.nix Normal file
View file

@ -0,0 +1,207 @@
{ pkgs, ... }:
let
testLib = pkgs.callPackage ../common.nix {};
commonTestScript = testLib.mkScripts {
hasSSL = { node, ... }: !(isNull node.config.shb.pinchflat.ssl);
waitForServices = { ... }: [
"pinchflat.service"
"nginx.service"
];
waitForPorts = { node, ... }: [
node.config.shb.pinchflat.port
];
};
basic = { config, ... }: {
imports = [
testLib.baseModule
../../modules/blocks/hardcodedsecret.nix
../../modules/blocks/lldap.nix
../../modules/services/pinchflat.nix
];
test = {
subdomain = "p";
};
shb.pinchflat = {
enable = true;
inherit (config.test) subdomain domain;
mediaDir = "/src/pinchflat";
timeZone = "America/Los_Angeles";
secretKeyBase.result = config.shb.hardcodedsecret.secretKeyBase.result;
};
systemd.tmpfiles.rules = [
"d '/src/pinchflat' 0750 pinchflat pinchflat - -"
];
# Needed for gitea-runner-local to be able to ping pinchflat.
networking.hosts = {
"127.0.0.1" = [ "${config.test.subdomain}.${config.test.domain}" ];
};
shb.hardcodedsecret.secretKeyBase = {
request = config.shb.pinchflat.secretKeyBase.request;
settings.content = pkgs.lib.strings.replicate 64 "Z";
};
};
clientLogin = { config, ... }: {
imports = [
testLib.baseModule
testLib.clientLoginModule
];
test = {
subdomain = "p";
};
test.login = {
startUrl = "http://${config.test.fqdn}";
# There is no login without SSO integration.
testLoginWith = [
{ username = null; password = null; nextPageExpect = [
"expect(page.get_by_text('Create a media profile')).to_be_visible()"
]; }
];
};
};
https = { config, ... }: {
shb.pinchflat = {
ssl = config.shb.certs.certs.selfsigned.n;
};
};
ldap = { config, ... }: {
shb.pinchflat = {
ldap = {
enable = true;
userGroup = "user_group";
};
};
};
clientLoginSso = { config, ... }: {
imports = [
testLib.baseModule
testLib.clientLoginModule
];
test = {
subdomain = "p";
};
test.login = {
startUrl = "https://${config.test.fqdn}";
usernameFieldLabelRegex = "Username";
passwordFieldLabelRegex = "Password";
loginButtonNameRegex = "[sS]ign [iI]n";
testLoginWith = [
{ username = "alice"; password = "NotAlicePassword"; nextPageExpect = [
"expect(page.get_by_text(re.compile('[Ii]ncorrect'))).to_be_visible()"
]; }
{ username = "alice"; password = "AlicePassword"; nextPageExpect = [
"expect(page.get_by_text(re.compile('[Ii]ncorrect'))).not_to_be_visible()"
"expect(page.get_by_role('button', name=re.compile('Sign In'))).not_to_be_visible()"
"expect(page.get_by_text('Create a media profile')).to_be_visible()"
]; }
{ username = "bob"; password = "NotBobPassword"; nextPageExpect = [
"expect(page.get_by_text(re.compile('[Ii]ncorrect'))).to_be_visible()"
]; }
{ username = "bob"; password = "BobPassword"; nextPageExpect = [
"expect(page.get_by_text(re.compile('[Ii]ncorrect'))).not_to_be_visible()"
"expect(page.get_by_role('button', name=re.compile('Sign In'))).not_to_be_visible()"
"expect(page.get_by_text('Create a media profile')).to_be_visible()"
]; }
];
};
};
sso = { config, ... }: {
shb.pinchflat = {
sso = {
enable = true;
authEndpoint = "https://${config.shb.authelia.subdomain}.${config.shb.authelia.domain}";
};
};
};
in
{
basic = pkgs.testers.runNixOSTest {
name = "pinchflat_basic";
nodes.client = {
imports = [
clientLogin
];
};
nodes.server = {
imports = [
basic
];
};
testScript = commonTestScript.access;
};
backup = pkgs.testers.runNixOSTest {
name = "pinchflat_backup";
nodes.server = { config, ... }: {
imports = [
basic
(testLib.backup config.shb.pinchflat.backup)
];
};
nodes.client = {};
testScript = commonTestScript.backup;
};
https = pkgs.testers.runNixOSTest {
name = "pinchflat_https";
nodes.client = {
imports = [
clientLogin
];
};
nodes.server = {
imports = [
basic
testLib.certs
https
];
};
testScript = commonTestScript.access;
};
sso = pkgs.testers.runNixOSTest {
name = "pinchflat_sso";
nodes.client = {
imports = [
clientLoginSso
];
};
nodes.server = { config, pkgs, ... }: {
imports = [
basic
testLib.certs
https
testLib.ldap
ldap
(testLib.sso config.shb.certs.certs.selfsigned.n)
sso
];
};
testScript = commonTestScript.access.override {
redirectSSO = true;
};
};
}