homepage: init with dashboard contract
This commit is contained in:
parent
b5015b1c56
commit
41061afe5c
50 changed files with 3072 additions and 411 deletions
|
|
@ -37,13 +37,17 @@ Provided contracts are:
|
|||
- [SSL generator contract](contracts-ssl.html) to generate SSL certificates.
|
||||
Two providers are implemented: self-signed and Let's Encrypt.
|
||||
- [Backup contract][] to backup directories.
|
||||
One provider is implemented: [Restic][].
|
||||
Two providers are implemented: [BorgBackup][] and [Restic][].
|
||||
- [Database Backup contract](contracts-databasebackup.html) to backup database dumps.
|
||||
One provider is implemented: [Restic][].
|
||||
- [Secret contract](contracts-secret.html) to provide secrets that are deployed outside of the Nix store.
|
||||
One provider is implemented: [BorgBackup][] and [Restic][].
|
||||
- [Contract for Secrets](contracts-secret.html) to provide secrets that are deployed outside of the Nix store.
|
||||
One provider is implemented: [SOPS][].
|
||||
- [Dashboard contract](contracts-dashboard.html) to show services in a nice user-facing dashboard.
|
||||
One provider is implemented: [Homepage][].
|
||||
|
||||
[backup contract]: contracts-backup.html
|
||||
[borgbackup]: blocks-borgbackup.html
|
||||
[homepage]: services-homepage.html
|
||||
[restic]: blocks-restic.html
|
||||
[sops]: blocks-sops.html
|
||||
|
||||
|
|
@ -63,6 +67,10 @@ modules/contracts/databasebackup/docs/default.md
|
|||
modules/contracts/secret/docs/default.md
|
||||
```
|
||||
|
||||
```{=include=} chapters html:into-file=//contracts-dashboard.html
|
||||
modules/contracts/dashboard/docs/default.md
|
||||
```
|
||||
|
||||
## Problem Statement {#contracts-why}
|
||||
|
||||
Currently in nixpkgs, every module accessing a shared resource
|
||||
|
|
|
|||
|
|
@ -128,3 +128,29 @@ shb.sops.secret."awesome" = {
|
|||
request = config.shb.restic.instances.awesome.settings.passphrase.request;
|
||||
};
|
||||
```
|
||||
|
||||
## Impermanence {#recipes-exposeService-impermanence}
|
||||
|
||||
To save the data folder in an impermanence setup, add:
|
||||
|
||||
```nix
|
||||
{
|
||||
shb.zfs.datasets."safe/awesome".path = config.services.awesome.dataDir;
|
||||
}
|
||||
```
|
||||
|
||||
## Application Dashboard {#recipes-exposeService-applicationdashboard}
|
||||
|
||||
For example using the [Homepage](services-homepage.html) service:
|
||||
|
||||
```nix
|
||||
{
|
||||
shb.homepage.servicesGroups.MyServices.services.Awesome = {
|
||||
sortOrder = 1;
|
||||
dashboard.request = {
|
||||
externalUrl = "https://${fqdn}";
|
||||
internalUrl = "http://127.0.0.1:${toString listenPort}";
|
||||
};
|
||||
};
|
||||
}
|
||||
```
|
||||
|
|
|
|||
|
|
@ -53,9 +53,6 @@
|
|||
"blocks-authelia-forward-auth": [
|
||||
"blocks-authelia.html#blocks-authelia-forward-auth"
|
||||
],
|
||||
"blocks-authelia-global-setup": [
|
||||
"blocks-authelia.html#blocks-authelia-global-setup"
|
||||
],
|
||||
"blocks-authelia-oidc": [
|
||||
"blocks-authelia.html#blocks-authelia-oidc"
|
||||
],
|
||||
|
|
@ -65,6 +62,21 @@
|
|||
"blocks-authelia-options-shb.authelia.autheliaUser": [
|
||||
"blocks-authelia.html#blocks-authelia-options-shb.authelia.autheliaUser"
|
||||
],
|
||||
"blocks-authelia-options-shb.authelia.dashboard": [
|
||||
"blocks-authelia.html#blocks-authelia-options-shb.authelia.dashboard"
|
||||
],
|
||||
"blocks-authelia-options-shb.authelia.dashboard.request": [
|
||||
"blocks-authelia.html#blocks-authelia-options-shb.authelia.dashboard.request"
|
||||
],
|
||||
"blocks-authelia-options-shb.authelia.dashboard.request.externalUrl": [
|
||||
"blocks-authelia.html#blocks-authelia-options-shb.authelia.dashboard.request.externalUrl"
|
||||
],
|
||||
"blocks-authelia-options-shb.authelia.dashboard.request.internalUrl": [
|
||||
"blocks-authelia.html#blocks-authelia-options-shb.authelia.dashboard.request.internalUrl"
|
||||
],
|
||||
"blocks-authelia-options-shb.authelia.dashboard.result": [
|
||||
"blocks-authelia.html#blocks-authelia-options-shb.authelia.dashboard.result"
|
||||
],
|
||||
"blocks-authelia-options-shb.authelia.dcdomain": [
|
||||
"blocks-authelia.html#blocks-authelia-options-shb.authelia.dcdomain"
|
||||
],
|
||||
|
|
@ -323,6 +335,9 @@
|
|||
"blocks-authelia-troubleshooting": [
|
||||
"blocks-authelia.html#blocks-authelia-troubleshooting"
|
||||
],
|
||||
"blocks-authelia-usage-configuration": [
|
||||
"blocks-authelia.html#blocks-authelia-usage-configuration"
|
||||
],
|
||||
"blocks-borgbackup": [
|
||||
"blocks-borgbackup.html#blocks-borgbackup"
|
||||
],
|
||||
|
|
@ -578,8 +593,8 @@
|
|||
"blocks-lldap": [
|
||||
"blocks-lldap.html#blocks-lldap"
|
||||
],
|
||||
"blocks-lldap-global-setup": [
|
||||
"blocks-lldap.html#blocks-lldap-global-setup"
|
||||
"blocks-lldap-features": [
|
||||
"blocks-lldap.html#blocks-lldap-features"
|
||||
],
|
||||
"blocks-lldap-manage-groups": [
|
||||
"blocks-lldap.html#blocks-lldap-manage-groups"
|
||||
|
|
@ -623,6 +638,21 @@
|
|||
"blocks-lldap-options-shb.lldap.backup.result.restoreScript": [
|
||||
"blocks-lldap.html#blocks-lldap-options-shb.lldap.backup.result.restoreScript"
|
||||
],
|
||||
"blocks-lldap-options-shb.lldap.dashboard": [
|
||||
"blocks-lldap.html#blocks-lldap-options-shb.lldap.dashboard"
|
||||
],
|
||||
"blocks-lldap-options-shb.lldap.dashboard.request": [
|
||||
"blocks-lldap.html#blocks-lldap-options-shb.lldap.dashboard.request"
|
||||
],
|
||||
"blocks-lldap-options-shb.lldap.dashboard.request.externalUrl": [
|
||||
"blocks-lldap.html#blocks-lldap-options-shb.lldap.dashboard.request.externalUrl"
|
||||
],
|
||||
"blocks-lldap-options-shb.lldap.dashboard.request.internalUrl": [
|
||||
"blocks-lldap.html#blocks-lldap-options-shb.lldap.dashboard.request.internalUrl"
|
||||
],
|
||||
"blocks-lldap-options-shb.lldap.dashboard.result": [
|
||||
"blocks-lldap.html#blocks-lldap-options-shb.lldap.dashboard.result"
|
||||
],
|
||||
"blocks-lldap-options-shb.lldap.dcdomain": [
|
||||
"blocks-lldap.html#blocks-lldap-options-shb.lldap.dcdomain"
|
||||
],
|
||||
|
|
@ -824,18 +854,27 @@
|
|||
"blocks-lldap-options-shb.lldap.webUIListenPort": [
|
||||
"blocks-lldap.html#blocks-lldap-options-shb.lldap.webUIListenPort"
|
||||
],
|
||||
"blocks-lldap-restrict-access-by-ip": [
|
||||
"blocks-lldap.html#blocks-lldap-restrict-access-by-ip"
|
||||
],
|
||||
"blocks-lldap-ssl": [
|
||||
"blocks-lldap.html#blocks-lldap-ssl"
|
||||
],
|
||||
"blocks-lldap-tests": [
|
||||
"blocks-lldap.html#blocks-lldap-tests"
|
||||
],
|
||||
"blocks-lldap-troubleshooting": [
|
||||
"blocks-lldap.html#blocks-lldap-troubleshooting"
|
||||
],
|
||||
"blocks-lldap-usage": [
|
||||
"blocks-lldap.html#blocks-lldap-usage"
|
||||
],
|
||||
"blocks-lldap-usage-applicationdashboard": [
|
||||
"blocks-lldap.html#blocks-lldap-usage-applicationdashboard"
|
||||
],
|
||||
"blocks-lldap-usage-configuration": [
|
||||
"blocks-lldap.html#blocks-lldap-usage-configuration"
|
||||
],
|
||||
"blocks-lldap-usage-restrict-access-by-ip": [
|
||||
"blocks-lldap.html#blocks-lldap-usage-restrict-access-by-ip"
|
||||
],
|
||||
"blocks-lldap-usage-ssl": [
|
||||
"blocks-lldap.html#blocks-lldap-usage-ssl"
|
||||
],
|
||||
"blocks-mitmdump": [
|
||||
"blocks-mitmdump.html#blocks-mitmdump"
|
||||
],
|
||||
|
|
@ -953,6 +992,21 @@
|
|||
"blocks-monitoring-options-shb.monitoring.contactPoints": [
|
||||
"blocks-monitoring.html#blocks-monitoring-options-shb.monitoring.contactPoints"
|
||||
],
|
||||
"blocks-monitoring-options-shb.monitoring.dashboard": [
|
||||
"blocks-monitoring.html#blocks-monitoring-options-shb.monitoring.dashboard"
|
||||
],
|
||||
"blocks-monitoring-options-shb.monitoring.dashboard.request": [
|
||||
"blocks-monitoring.html#blocks-monitoring-options-shb.monitoring.dashboard.request"
|
||||
],
|
||||
"blocks-monitoring-options-shb.monitoring.dashboard.request.externalUrl": [
|
||||
"blocks-monitoring.html#blocks-monitoring-options-shb.monitoring.dashboard.request.externalUrl"
|
||||
],
|
||||
"blocks-monitoring-options-shb.monitoring.dashboard.request.internalUrl": [
|
||||
"blocks-monitoring.html#blocks-monitoring-options-shb.monitoring.dashboard.request.internalUrl"
|
||||
],
|
||||
"blocks-monitoring-options-shb.monitoring.dashboard.result": [
|
||||
"blocks-monitoring.html#blocks-monitoring-options-shb.monitoring.dashboard.result"
|
||||
],
|
||||
"blocks-monitoring-options-shb.monitoring.dashboards": [
|
||||
"blocks-monitoring.html#blocks-monitoring-options-shb.monitoring.dashboards"
|
||||
],
|
||||
|
|
@ -1133,6 +1187,12 @@
|
|||
"blocks-monitoring-usage": [
|
||||
"blocks-monitoring.html#blocks-monitoring-usage"
|
||||
],
|
||||
"blocks-monitoring-usage-applicationdashboard": [
|
||||
"blocks-monitoring.html#blocks-monitoring-usage-applicationdashboard"
|
||||
],
|
||||
"blocks-monitoring-usage-configuration": [
|
||||
"blocks-monitoring.html#blocks-monitoring-usage-configuration"
|
||||
],
|
||||
"blocks-monitoring-usage-log-optimization": [
|
||||
"blocks-monitoring.html#blocks-monitoring-usage-log-optimization"
|
||||
],
|
||||
|
|
@ -1712,6 +1772,15 @@
|
|||
"contract-backup-usage": [
|
||||
"contracts-backup.html#contract-backup-usage"
|
||||
],
|
||||
"contract-dashboard": [
|
||||
"contracts-dashboard.html#contract-dashboard"
|
||||
],
|
||||
"contract-dashboard-options": [
|
||||
"contracts-dashboard.html#contract-dashboard-options"
|
||||
],
|
||||
"contract-dashboard-providers": [
|
||||
"contracts-dashboard.html#contract-dashboard-providers"
|
||||
],
|
||||
"contract-databasebackup": [
|
||||
"contracts-databasebackup.html#contract-databasebackup"
|
||||
],
|
||||
|
|
@ -1805,6 +1874,27 @@
|
|||
"contracts-concept": [
|
||||
"contracts.html#contracts-concept"
|
||||
],
|
||||
"contracts-dashboard-options-shb.contracts.dashboard": [
|
||||
"contracts-dashboard.html#contracts-dashboard-options-shb.contracts.dashboard"
|
||||
],
|
||||
"contracts-dashboard-options-shb.contracts.dashboard.request": [
|
||||
"contracts-dashboard.html#contracts-dashboard-options-shb.contracts.dashboard.request"
|
||||
],
|
||||
"contracts-dashboard-options-shb.contracts.dashboard.request.externalUrl": [
|
||||
"contracts-dashboard.html#contracts-dashboard-options-shb.contracts.dashboard.request.externalUrl"
|
||||
],
|
||||
"contracts-dashboard-options-shb.contracts.dashboard.request.internalUrl": [
|
||||
"contracts-dashboard.html#contracts-dashboard-options-shb.contracts.dashboard.request.internalUrl"
|
||||
],
|
||||
"contracts-dashboard-options-shb.contracts.dashboard.result": [
|
||||
"contracts-dashboard.html#contracts-dashboard-options-shb.contracts.dashboard.result"
|
||||
],
|
||||
"contracts-dashboard-options-shb.contracts.dashboard.settings": [
|
||||
"contracts-dashboard.html#contracts-dashboard-options-shb.contracts.dashboard.settings"
|
||||
],
|
||||
"contracts-dashboard-usage": [
|
||||
"contracts-dashboard.html#contracts-dashboard-usage"
|
||||
],
|
||||
"contracts-databasebackup-options-shb.contracts.databasebackup": [
|
||||
"contracts-databasebackup.html#contracts-databasebackup-options-shb.contracts.databasebackup"
|
||||
],
|
||||
|
|
@ -2156,12 +2246,18 @@
|
|||
"recipes-exposeService": [
|
||||
"recipes-exposeService.html#recipes-exposeService"
|
||||
],
|
||||
"recipes-exposeService-applicationdashboard": [
|
||||
"recipes-exposeService.html#recipes-exposeService-applicationdashboard"
|
||||
],
|
||||
"recipes-exposeService-backup": [
|
||||
"recipes-exposeService.html#recipes-exposeService-backup"
|
||||
],
|
||||
"recipes-exposeService-debug": [
|
||||
"recipes-exposeService.html#recipes-exposeService-debug"
|
||||
],
|
||||
"recipes-exposeService-impermanence": [
|
||||
"recipes-exposeService.html#recipes-exposeService-impermanence"
|
||||
],
|
||||
"recipes-exposeService-ldap": [
|
||||
"recipes-exposeService.html#recipes-exposeService-ldap"
|
||||
],
|
||||
|
|
@ -2216,6 +2312,9 @@
|
|||
"services-arr": [
|
||||
"services-arr.html#services-arr"
|
||||
],
|
||||
"services-arr-features": [
|
||||
"services-arr.html#services-arr-features"
|
||||
],
|
||||
"services-arr-options": [
|
||||
"services-arr.html#services-arr-options"
|
||||
],
|
||||
|
|
@ -2258,6 +2357,21 @@
|
|||
"services-arr-options-shb.arr.bazarr.backup.result.restoreScript": [
|
||||
"services-arr.html#services-arr-options-shb.arr.bazarr.backup.result.restoreScript"
|
||||
],
|
||||
"services-arr-options-shb.arr.bazarr.dashboard": [
|
||||
"services-arr.html#services-arr-options-shb.arr.bazarr.dashboard"
|
||||
],
|
||||
"services-arr-options-shb.arr.bazarr.dashboard.request": [
|
||||
"services-arr.html#services-arr-options-shb.arr.bazarr.dashboard.request"
|
||||
],
|
||||
"services-arr-options-shb.arr.bazarr.dashboard.request.externalUrl": [
|
||||
"services-arr.html#services-arr-options-shb.arr.bazarr.dashboard.request.externalUrl"
|
||||
],
|
||||
"services-arr-options-shb.arr.bazarr.dashboard.request.internalUrl": [
|
||||
"services-arr.html#services-arr-options-shb.arr.bazarr.dashboard.request.internalUrl"
|
||||
],
|
||||
"services-arr-options-shb.arr.bazarr.dashboard.result": [
|
||||
"services-arr.html#services-arr-options-shb.arr.bazarr.dashboard.result"
|
||||
],
|
||||
"services-arr-options-shb.arr.bazarr.dataDir": [
|
||||
"services-arr.html#services-arr-options-shb.arr.bazarr.dataDir"
|
||||
],
|
||||
|
|
@ -2273,6 +2387,15 @@
|
|||
"services-arr-options-shb.arr.bazarr.settings": [
|
||||
"services-arr.html#services-arr-options-shb.arr.bazarr.settings"
|
||||
],
|
||||
"services-arr-options-shb.arr.bazarr.settings.ApiKey": [
|
||||
"services-arr.html#services-arr-options-shb.arr.bazarr.settings.ApiKey"
|
||||
],
|
||||
"services-arr-options-shb.arr.bazarr.settings.ApiKey.source": [
|
||||
"services-arr.html#services-arr-options-shb.arr.bazarr.settings.ApiKey.source"
|
||||
],
|
||||
"services-arr-options-shb.arr.bazarr.settings.ApiKey.transform": [
|
||||
"services-arr.html#services-arr-options-shb.arr.bazarr.settings.ApiKey.transform"
|
||||
],
|
||||
"services-arr-options-shb.arr.bazarr.settings.LogLevel": [
|
||||
"services-arr.html#services-arr-options-shb.arr.bazarr.settings.LogLevel"
|
||||
],
|
||||
|
|
@ -2336,6 +2459,21 @@
|
|||
"services-arr-options-shb.arr.jackett.backup.result.restoreScript": [
|
||||
"services-arr.html#services-arr-options-shb.arr.jackett.backup.result.restoreScript"
|
||||
],
|
||||
"services-arr-options-shb.arr.jackett.dashboard": [
|
||||
"services-arr.html#services-arr-options-shb.arr.jackett.dashboard"
|
||||
],
|
||||
"services-arr-options-shb.arr.jackett.dashboard.request": [
|
||||
"services-arr.html#services-arr-options-shb.arr.jackett.dashboard.request"
|
||||
],
|
||||
"services-arr-options-shb.arr.jackett.dashboard.request.externalUrl": [
|
||||
"services-arr.html#services-arr-options-shb.arr.jackett.dashboard.request.externalUrl"
|
||||
],
|
||||
"services-arr-options-shb.arr.jackett.dashboard.request.internalUrl": [
|
||||
"services-arr.html#services-arr-options-shb.arr.jackett.dashboard.request.internalUrl"
|
||||
],
|
||||
"services-arr-options-shb.arr.jackett.dashboard.result": [
|
||||
"services-arr.html#services-arr-options-shb.arr.jackett.dashboard.result"
|
||||
],
|
||||
"services-arr-options-shb.arr.jackett.dataDir": [
|
||||
"services-arr.html#services-arr-options-shb.arr.jackett.dataDir"
|
||||
],
|
||||
|
|
@ -2441,6 +2579,21 @@
|
|||
"services-arr-options-shb.arr.lidarr.backup.result.restoreScript": [
|
||||
"services-arr.html#services-arr-options-shb.arr.lidarr.backup.result.restoreScript"
|
||||
],
|
||||
"services-arr-options-shb.arr.lidarr.dashboard": [
|
||||
"services-arr.html#services-arr-options-shb.arr.lidarr.dashboard"
|
||||
],
|
||||
"services-arr-options-shb.arr.lidarr.dashboard.request": [
|
||||
"services-arr.html#services-arr-options-shb.arr.lidarr.dashboard.request"
|
||||
],
|
||||
"services-arr-options-shb.arr.lidarr.dashboard.request.externalUrl": [
|
||||
"services-arr.html#services-arr-options-shb.arr.lidarr.dashboard.request.externalUrl"
|
||||
],
|
||||
"services-arr-options-shb.arr.lidarr.dashboard.request.internalUrl": [
|
||||
"services-arr.html#services-arr-options-shb.arr.lidarr.dashboard.request.internalUrl"
|
||||
],
|
||||
"services-arr-options-shb.arr.lidarr.dashboard.result": [
|
||||
"services-arr.html#services-arr-options-shb.arr.lidarr.dashboard.result"
|
||||
],
|
||||
"services-arr-options-shb.arr.lidarr.dataDir": [
|
||||
"services-arr.html#services-arr-options-shb.arr.lidarr.dataDir"
|
||||
],
|
||||
|
|
@ -2456,6 +2609,15 @@
|
|||
"services-arr-options-shb.arr.lidarr.settings": [
|
||||
"services-arr.html#services-arr-options-shb.arr.lidarr.settings"
|
||||
],
|
||||
"services-arr-options-shb.arr.lidarr.settings.ApiKey": [
|
||||
"services-arr.html#services-arr-options-shb.arr.lidarr.settings.ApiKey"
|
||||
],
|
||||
"services-arr-options-shb.arr.lidarr.settings.ApiKey.source": [
|
||||
"services-arr.html#services-arr-options-shb.arr.lidarr.settings.ApiKey.source"
|
||||
],
|
||||
"services-arr-options-shb.arr.lidarr.settings.ApiKey.transform": [
|
||||
"services-arr.html#services-arr-options-shb.arr.lidarr.settings.ApiKey.transform"
|
||||
],
|
||||
"services-arr-options-shb.arr.lidarr.settings.LogLevel": [
|
||||
"services-arr.html#services-arr-options-shb.arr.lidarr.settings.LogLevel"
|
||||
],
|
||||
|
|
@ -2519,6 +2681,21 @@
|
|||
"services-arr-options-shb.arr.radarr.backup.result.restoreScript": [
|
||||
"services-arr.html#services-arr-options-shb.arr.radarr.backup.result.restoreScript"
|
||||
],
|
||||
"services-arr-options-shb.arr.radarr.dashboard": [
|
||||
"services-arr.html#services-arr-options-shb.arr.radarr.dashboard"
|
||||
],
|
||||
"services-arr-options-shb.arr.radarr.dashboard.request": [
|
||||
"services-arr.html#services-arr-options-shb.arr.radarr.dashboard.request"
|
||||
],
|
||||
"services-arr-options-shb.arr.radarr.dashboard.request.externalUrl": [
|
||||
"services-arr.html#services-arr-options-shb.arr.radarr.dashboard.request.externalUrl"
|
||||
],
|
||||
"services-arr-options-shb.arr.radarr.dashboard.request.internalUrl": [
|
||||
"services-arr.html#services-arr-options-shb.arr.radarr.dashboard.request.internalUrl"
|
||||
],
|
||||
"services-arr-options-shb.arr.radarr.dashboard.result": [
|
||||
"services-arr.html#services-arr-options-shb.arr.radarr.dashboard.result"
|
||||
],
|
||||
"services-arr-options-shb.arr.radarr.dataDir": [
|
||||
"services-arr.html#services-arr-options-shb.arr.radarr.dataDir"
|
||||
],
|
||||
|
|
@ -2609,6 +2786,21 @@
|
|||
"services-arr-options-shb.arr.readarr.backup.result.restoreScript": [
|
||||
"services-arr.html#services-arr-options-shb.arr.readarr.backup.result.restoreScript"
|
||||
],
|
||||
"services-arr-options-shb.arr.readarr.dashboard": [
|
||||
"services-arr.html#services-arr-options-shb.arr.readarr.dashboard"
|
||||
],
|
||||
"services-arr-options-shb.arr.readarr.dashboard.request": [
|
||||
"services-arr.html#services-arr-options-shb.arr.readarr.dashboard.request"
|
||||
],
|
||||
"services-arr-options-shb.arr.readarr.dashboard.request.externalUrl": [
|
||||
"services-arr.html#services-arr-options-shb.arr.readarr.dashboard.request.externalUrl"
|
||||
],
|
||||
"services-arr-options-shb.arr.readarr.dashboard.request.internalUrl": [
|
||||
"services-arr.html#services-arr-options-shb.arr.readarr.dashboard.request.internalUrl"
|
||||
],
|
||||
"services-arr-options-shb.arr.readarr.dashboard.result": [
|
||||
"services-arr.html#services-arr-options-shb.arr.readarr.dashboard.result"
|
||||
],
|
||||
"services-arr-options-shb.arr.readarr.dataDir": [
|
||||
"services-arr.html#services-arr-options-shb.arr.readarr.dataDir"
|
||||
],
|
||||
|
|
@ -2624,6 +2816,15 @@
|
|||
"services-arr-options-shb.arr.readarr.settings": [
|
||||
"services-arr.html#services-arr-options-shb.arr.readarr.settings"
|
||||
],
|
||||
"services-arr-options-shb.arr.readarr.settings.ApiKey": [
|
||||
"services-arr.html#services-arr-options-shb.arr.readarr.settings.ApiKey"
|
||||
],
|
||||
"services-arr-options-shb.arr.readarr.settings.ApiKey.source": [
|
||||
"services-arr.html#services-arr-options-shb.arr.readarr.settings.ApiKey.source"
|
||||
],
|
||||
"services-arr-options-shb.arr.readarr.settings.ApiKey.transform": [
|
||||
"services-arr.html#services-arr-options-shb.arr.readarr.settings.ApiKey.transform"
|
||||
],
|
||||
"services-arr-options-shb.arr.readarr.settings.LogLevel": [
|
||||
"services-arr.html#services-arr-options-shb.arr.readarr.settings.LogLevel"
|
||||
],
|
||||
|
|
@ -2687,6 +2888,21 @@
|
|||
"services-arr-options-shb.arr.sonarr.backup.result.restoreScript": [
|
||||
"services-arr.html#services-arr-options-shb.arr.sonarr.backup.result.restoreScript"
|
||||
],
|
||||
"services-arr-options-shb.arr.sonarr.dashboard": [
|
||||
"services-arr.html#services-arr-options-shb.arr.sonarr.dashboard"
|
||||
],
|
||||
"services-arr-options-shb.arr.sonarr.dashboard.request": [
|
||||
"services-arr.html#services-arr-options-shb.arr.sonarr.dashboard.request"
|
||||
],
|
||||
"services-arr-options-shb.arr.sonarr.dashboard.request.externalUrl": [
|
||||
"services-arr.html#services-arr-options-shb.arr.sonarr.dashboard.request.externalUrl"
|
||||
],
|
||||
"services-arr-options-shb.arr.sonarr.dashboard.request.internalUrl": [
|
||||
"services-arr.html#services-arr-options-shb.arr.sonarr.dashboard.request.internalUrl"
|
||||
],
|
||||
"services-arr-options-shb.arr.sonarr.dashboard.result": [
|
||||
"services-arr.html#services-arr-options-shb.arr.sonarr.dashboard.result"
|
||||
],
|
||||
"services-arr-options-shb.arr.sonarr.dataDir": [
|
||||
"services-arr.html#services-arr-options-shb.arr.sonarr.dataDir"
|
||||
],
|
||||
|
|
@ -2735,6 +2951,30 @@
|
|||
"services-arr-options-shb.arr.sonarr.subdomain": [
|
||||
"services-arr.html#services-arr-options-shb.arr.sonarr.subdomain"
|
||||
],
|
||||
"services-arr-usage": [
|
||||
"services-arr.html#services-arr-usage"
|
||||
],
|
||||
"services-arr-usage-apikeys": [
|
||||
"services-arr.html#services-arr-usage-apikeys"
|
||||
],
|
||||
"services-arr-usage-applicationdashboard": [
|
||||
"services-arr.html#services-arr-usage-applicationdashboard"
|
||||
],
|
||||
"services-arr-usage-configuration": [
|
||||
"services-arr.html#services-arr-usage-configuration"
|
||||
],
|
||||
"services-arr-usage-jackett-proxy": [
|
||||
"services-arr.html#services-arr-usage-jackett-proxy"
|
||||
],
|
||||
"services-authelia-features": [
|
||||
"blocks-authelia.html#services-authelia-features"
|
||||
],
|
||||
"services-authelia-usage": [
|
||||
"blocks-authelia.html#services-authelia-usage"
|
||||
],
|
||||
"services-authelia-usage-applicationdashboard": [
|
||||
"blocks-authelia.html#services-authelia-usage-applicationdashboard"
|
||||
],
|
||||
"services-category-ai": [
|
||||
"services.html#services-category-ai"
|
||||
],
|
||||
|
|
@ -2744,6 +2984,9 @@
|
|||
"services-category-code": [
|
||||
"services.html#services-category-code"
|
||||
],
|
||||
"services-category-dashboard": [
|
||||
"services.html#services-category-dashboard"
|
||||
],
|
||||
"services-category-documents": [
|
||||
"services.html#services-category-documents"
|
||||
],
|
||||
|
|
@ -2771,6 +3014,9 @@
|
|||
"services-firefly-iii-declarative-ldap": [
|
||||
"services-firefly-iii.html#services-firefly-iii-declarative-ldap"
|
||||
],
|
||||
"services-firefly-iii-features": [
|
||||
"services-firefly-iii.html#services-firefly-iii-features"
|
||||
],
|
||||
"services-firefly-iii-impermanence": [
|
||||
"services-firefly-iii.html#services-firefly-iii-impermanence"
|
||||
],
|
||||
|
|
@ -2837,6 +3083,21 @@
|
|||
"services-firefly-iii-options-shb.firefly-iii.backup.result.restoreScript": [
|
||||
"services-firefly-iii.html#services-firefly-iii-options-shb.firefly-iii.backup.result.restoreScript"
|
||||
],
|
||||
"services-firefly-iii-options-shb.firefly-iii.dashboard": [
|
||||
"services-firefly-iii.html#services-firefly-iii-options-shb.firefly-iii.dashboard"
|
||||
],
|
||||
"services-firefly-iii-options-shb.firefly-iii.dashboard.request": [
|
||||
"services-firefly-iii.html#services-firefly-iii-options-shb.firefly-iii.dashboard.request"
|
||||
],
|
||||
"services-firefly-iii-options-shb.firefly-iii.dashboard.request.externalUrl": [
|
||||
"services-firefly-iii.html#services-firefly-iii-options-shb.firefly-iii.dashboard.request.externalUrl"
|
||||
],
|
||||
"services-firefly-iii-options-shb.firefly-iii.dashboard.request.internalUrl": [
|
||||
"services-firefly-iii.html#services-firefly-iii-options-shb.firefly-iii.dashboard.request.internalUrl"
|
||||
],
|
||||
"services-firefly-iii-options-shb.firefly-iii.dashboard.result": [
|
||||
"services-firefly-iii.html#services-firefly-iii-options-shb.firefly-iii.dashboard.result"
|
||||
],
|
||||
"services-firefly-iii-options-shb.firefly-iii.dbPassword": [
|
||||
"services-firefly-iii.html#services-firefly-iii-options-shb.firefly-iii.dbPassword"
|
||||
],
|
||||
|
|
@ -3050,9 +3311,15 @@
|
|||
"services-firefly-iii-usage": [
|
||||
"services-firefly-iii.html#services-firefly-iii-usage"
|
||||
],
|
||||
"services-firefly-iii-usage-applicationdashboard": [
|
||||
"services-firefly-iii.html#services-firefly-iii-usage-applicationdashboard"
|
||||
],
|
||||
"services-firefly-iii-usage-backup": [
|
||||
"services-firefly-iii.html#services-firefly-iii-usage-backup"
|
||||
],
|
||||
"services-firefly-iii-usage-configuration": [
|
||||
"services-firefly-iii.html#services-firefly-iii-usage-configuration"
|
||||
],
|
||||
"services-forgejo": [
|
||||
"services-forgejo.html#services-forgejo"
|
||||
],
|
||||
|
|
@ -3098,6 +3365,21 @@
|
|||
"services-forgejo-options-shb.forgejo.backup.result.restoreScript": [
|
||||
"services-forgejo.html#services-forgejo-options-shb.forgejo.backup.result.restoreScript"
|
||||
],
|
||||
"services-forgejo-options-shb.forgejo.dashboard": [
|
||||
"services-forgejo.html#services-forgejo-options-shb.forgejo.dashboard"
|
||||
],
|
||||
"services-forgejo-options-shb.forgejo.dashboard.request": [
|
||||
"services-forgejo.html#services-forgejo-options-shb.forgejo.dashboard.request"
|
||||
],
|
||||
"services-forgejo-options-shb.forgejo.dashboard.request.externalUrl": [
|
||||
"services-forgejo.html#services-forgejo-options-shb.forgejo.dashboard.request.externalUrl"
|
||||
],
|
||||
"services-forgejo-options-shb.forgejo.dashboard.request.internalUrl": [
|
||||
"services-forgejo.html#services-forgejo-options-shb.forgejo.dashboard.request.internalUrl"
|
||||
],
|
||||
"services-forgejo-options-shb.forgejo.dashboard.result": [
|
||||
"services-forgejo.html#services-forgejo-options-shb.forgejo.dashboard.result"
|
||||
],
|
||||
"services-forgejo-options-shb.forgejo.databasePassword": [
|
||||
"services-forgejo.html#services-forgejo-options-shb.forgejo.databasePassword"
|
||||
],
|
||||
|
|
@ -3359,6 +3641,9 @@
|
|||
"services-forgejo-usage": [
|
||||
"services-forgejo.html#services-forgejo-usage"
|
||||
],
|
||||
"services-forgejo-usage-applicationdashboard": [
|
||||
"services-forgejo.html#services-forgejo-usage-applicationdashboard"
|
||||
],
|
||||
"services-forgejo-usage-backup": [
|
||||
"services-forgejo.html#services-forgejo-usage-backup"
|
||||
],
|
||||
|
|
@ -3446,6 +3731,21 @@
|
|||
"services-home-assistant-options-shb.home-assistant.config.unit_system": [
|
||||
"services-home-assistant.html#services-home-assistant-options-shb.home-assistant.config.unit_system"
|
||||
],
|
||||
"services-home-assistant-options-shb.home-assistant.dashboard": [
|
||||
"services-home-assistant.html#services-home-assistant-options-shb.home-assistant.dashboard"
|
||||
],
|
||||
"services-home-assistant-options-shb.home-assistant.dashboard.request": [
|
||||
"services-home-assistant.html#services-home-assistant-options-shb.home-assistant.dashboard.request"
|
||||
],
|
||||
"services-home-assistant-options-shb.home-assistant.dashboard.request.externalUrl": [
|
||||
"services-home-assistant.html#services-home-assistant-options-shb.home-assistant.dashboard.request.externalUrl"
|
||||
],
|
||||
"services-home-assistant-options-shb.home-assistant.dashboard.request.internalUrl": [
|
||||
"services-home-assistant.html#services-home-assistant-options-shb.home-assistant.dashboard.request.internalUrl"
|
||||
],
|
||||
"services-home-assistant-options-shb.home-assistant.dashboard.result": [
|
||||
"services-home-assistant.html#services-home-assistant-options-shb.home-assistant.dashboard.result"
|
||||
],
|
||||
"services-home-assistant-options-shb.home-assistant.domain": [
|
||||
"services-home-assistant.html#services-home-assistant-options-shb.home-assistant.domain"
|
||||
],
|
||||
|
|
@ -3503,6 +3803,9 @@
|
|||
"services-home-assistant-usage": [
|
||||
"services-home-assistant.html#services-home-assistant-usage"
|
||||
],
|
||||
"services-home-assistant-usage-applicationdashboard": [
|
||||
"services-home-assistant.html#services-home-assistant-usage-applicationdashboard"
|
||||
],
|
||||
"services-home-assistant-usage-backup": [
|
||||
"services-home-assistant.html#services-home-assistant-usage-backup"
|
||||
],
|
||||
|
|
@ -3539,6 +3842,132 @@
|
|||
"services-home-assistant-usage-voice": [
|
||||
"services-home-assistant.html#services-home-assistant-usage-voice"
|
||||
],
|
||||
"services-homepage": [
|
||||
"services-homepage.html#services-homepage"
|
||||
],
|
||||
"services-homepage-features": [
|
||||
"services-homepage.html#services-homepage-features"
|
||||
],
|
||||
"services-homepage-options": [
|
||||
"services-homepage.html#services-homepage-options"
|
||||
],
|
||||
"services-homepage-options-shb.homepage.domain": [
|
||||
"services-homepage.html#services-homepage-options-shb.homepage.domain"
|
||||
],
|
||||
"services-homepage-options-shb.homepage.enable": [
|
||||
"services-homepage.html#services-homepage-options-shb.homepage.enable"
|
||||
],
|
||||
"services-homepage-options-shb.homepage.ldap": [
|
||||
"services-homepage.html#services-homepage-options-shb.homepage.ldap"
|
||||
],
|
||||
"services-homepage-options-shb.homepage.ldap.userGroup": [
|
||||
"services-homepage.html#services-homepage-options-shb.homepage.ldap.userGroup"
|
||||
],
|
||||
"services-homepage-options-shb.homepage.servicesGroups": [
|
||||
"services-homepage.html#services-homepage-options-shb.homepage.servicesGroups"
|
||||
],
|
||||
"services-homepage-options-shb.homepage.servicesGroups._name_.name": [
|
||||
"services-homepage.html#services-homepage-options-shb.homepage.servicesGroups._name_.name"
|
||||
],
|
||||
"services-homepage-options-shb.homepage.servicesGroups._name_.services": [
|
||||
"services-homepage.html#services-homepage-options-shb.homepage.servicesGroups._name_.services"
|
||||
],
|
||||
"services-homepage-options-shb.homepage.servicesGroups._name_.services._name_.apiKey": [
|
||||
"services-homepage.html#services-homepage-options-shb.homepage.servicesGroups._name_.services._name_.apiKey"
|
||||
],
|
||||
"services-homepage-options-shb.homepage.servicesGroups._name_.services._name_.apiKey.request": [
|
||||
"services-homepage.html#services-homepage-options-shb.homepage.servicesGroups._name_.services._name_.apiKey.request"
|
||||
],
|
||||
"services-homepage-options-shb.homepage.servicesGroups._name_.services._name_.apiKey.request.group": [
|
||||
"services-homepage.html#services-homepage-options-shb.homepage.servicesGroups._name_.services._name_.apiKey.request.group"
|
||||
],
|
||||
"services-homepage-options-shb.homepage.servicesGroups._name_.services._name_.apiKey.request.mode": [
|
||||
"services-homepage.html#services-homepage-options-shb.homepage.servicesGroups._name_.services._name_.apiKey.request.mode"
|
||||
],
|
||||
"services-homepage-options-shb.homepage.servicesGroups._name_.services._name_.apiKey.request.owner": [
|
||||
"services-homepage.html#services-homepage-options-shb.homepage.servicesGroups._name_.services._name_.apiKey.request.owner"
|
||||
],
|
||||
"services-homepage-options-shb.homepage.servicesGroups._name_.services._name_.apiKey.request.restartUnits": [
|
||||
"services-homepage.html#services-homepage-options-shb.homepage.servicesGroups._name_.services._name_.apiKey.request.restartUnits"
|
||||
],
|
||||
"services-homepage-options-shb.homepage.servicesGroups._name_.services._name_.apiKey.result": [
|
||||
"services-homepage.html#services-homepage-options-shb.homepage.servicesGroups._name_.services._name_.apiKey.result"
|
||||
],
|
||||
"services-homepage-options-shb.homepage.servicesGroups._name_.services._name_.apiKey.result.path": [
|
||||
"services-homepage.html#services-homepage-options-shb.homepage.servicesGroups._name_.services._name_.apiKey.result.path"
|
||||
],
|
||||
"services-homepage-options-shb.homepage.servicesGroups._name_.services._name_.dashboard": [
|
||||
"services-homepage.html#services-homepage-options-shb.homepage.servicesGroups._name_.services._name_.dashboard"
|
||||
],
|
||||
"services-homepage-options-shb.homepage.servicesGroups._name_.services._name_.dashboard.request": [
|
||||
"services-homepage.html#services-homepage-options-shb.homepage.servicesGroups._name_.services._name_.dashboard.request"
|
||||
],
|
||||
"services-homepage-options-shb.homepage.servicesGroups._name_.services._name_.dashboard.request.externalUrl": [
|
||||
"services-homepage.html#services-homepage-options-shb.homepage.servicesGroups._name_.services._name_.dashboard.request.externalUrl"
|
||||
],
|
||||
"services-homepage-options-shb.homepage.servicesGroups._name_.services._name_.dashboard.request.internalUrl": [
|
||||
"services-homepage.html#services-homepage-options-shb.homepage.servicesGroups._name_.services._name_.dashboard.request.internalUrl"
|
||||
],
|
||||
"services-homepage-options-shb.homepage.servicesGroups._name_.services._name_.dashboard.result": [
|
||||
"services-homepage.html#services-homepage-options-shb.homepage.servicesGroups._name_.services._name_.dashboard.result"
|
||||
],
|
||||
"services-homepage-options-shb.homepage.servicesGroups._name_.services._name_.name": [
|
||||
"services-homepage.html#services-homepage-options-shb.homepage.servicesGroups._name_.services._name_.name"
|
||||
],
|
||||
"services-homepage-options-shb.homepage.servicesGroups._name_.services._name_.settings": [
|
||||
"services-homepage.html#services-homepage-options-shb.homepage.servicesGroups._name_.services._name_.settings"
|
||||
],
|
||||
"services-homepage-options-shb.homepage.servicesGroups._name_.services._name_.sortOrder": [
|
||||
"services-homepage.html#services-homepage-options-shb.homepage.servicesGroups._name_.services._name_.sortOrder"
|
||||
],
|
||||
"services-homepage-options-shb.homepage.servicesGroups._name_.sortOrder": [
|
||||
"services-homepage.html#services-homepage-options-shb.homepage.servicesGroups._name_.sortOrder"
|
||||
],
|
||||
"services-homepage-options-shb.homepage.ssl": [
|
||||
"services-homepage.html#services-homepage-options-shb.homepage.ssl"
|
||||
],
|
||||
"services-homepage-options-shb.homepage.ssl.paths": [
|
||||
"services-homepage.html#services-homepage-options-shb.homepage.ssl.paths"
|
||||
],
|
||||
"services-homepage-options-shb.homepage.ssl.paths.cert": [
|
||||
"services-homepage.html#services-homepage-options-shb.homepage.ssl.paths.cert"
|
||||
],
|
||||
"services-homepage-options-shb.homepage.ssl.paths.key": [
|
||||
"services-homepage.html#services-homepage-options-shb.homepage.ssl.paths.key"
|
||||
],
|
||||
"services-homepage-options-shb.homepage.ssl.systemdService": [
|
||||
"services-homepage.html#services-homepage-options-shb.homepage.ssl.systemdService"
|
||||
],
|
||||
"services-homepage-options-shb.homepage.sso": [
|
||||
"services-homepage.html#services-homepage-options-shb.homepage.sso"
|
||||
],
|
||||
"services-homepage-options-shb.homepage.sso.authEndpoint": [
|
||||
"services-homepage.html#services-homepage-options-shb.homepage.sso.authEndpoint"
|
||||
],
|
||||
"services-homepage-options-shb.homepage.sso.authorization_policy": [
|
||||
"services-homepage.html#services-homepage-options-shb.homepage.sso.authorization_policy"
|
||||
],
|
||||
"services-homepage-options-shb.homepage.sso.enable": [
|
||||
"services-homepage.html#services-homepage-options-shb.homepage.sso.enable"
|
||||
],
|
||||
"services-homepage-options-shb.homepage.subdomain": [
|
||||
"services-homepage.html#services-homepage-options-shb.homepage.subdomain"
|
||||
],
|
||||
"services-homepage-usage": [
|
||||
"services-homepage.html#services-homepage-usage"
|
||||
],
|
||||
"services-homepage-usage-custom": [
|
||||
"services-homepage.html#services-homepage-usage-custom"
|
||||
],
|
||||
"services-homepage-usage-main": [
|
||||
"services-homepage.html#services-homepage-usage-main"
|
||||
],
|
||||
"services-homepage-usage-service": [
|
||||
"services-homepage.html#services-homepage-usage-service"
|
||||
],
|
||||
"services-homepage-usage-widget": [
|
||||
"services-homepage.html#services-homepage-usage-widget"
|
||||
],
|
||||
"services-jellyfin": [
|
||||
"services-jellyfin.html#services-jellyfin"
|
||||
],
|
||||
|
|
@ -3623,6 +4052,21 @@
|
|||
"services-jellyfin-options-shb.jellyfin.backup.result.restoreScript": [
|
||||
"services-jellyfin.html#services-jellyfin-options-shb.jellyfin.backup.result.restoreScript"
|
||||
],
|
||||
"services-jellyfin-options-shb.jellyfin.dashboard": [
|
||||
"services-jellyfin.html#services-jellyfin-options-shb.jellyfin.dashboard"
|
||||
],
|
||||
"services-jellyfin-options-shb.jellyfin.dashboard.request": [
|
||||
"services-jellyfin.html#services-jellyfin-options-shb.jellyfin.dashboard.request"
|
||||
],
|
||||
"services-jellyfin-options-shb.jellyfin.dashboard.request.externalUrl": [
|
||||
"services-jellyfin.html#services-jellyfin-options-shb.jellyfin.dashboard.request.externalUrl"
|
||||
],
|
||||
"services-jellyfin-options-shb.jellyfin.dashboard.request.internalUrl": [
|
||||
"services-jellyfin.html#services-jellyfin-options-shb.jellyfin.dashboard.request.internalUrl"
|
||||
],
|
||||
"services-jellyfin-options-shb.jellyfin.dashboard.result": [
|
||||
"services-jellyfin.html#services-jellyfin-options-shb.jellyfin.dashboard.result"
|
||||
],
|
||||
"services-jellyfin-options-shb.jellyfin.debug": [
|
||||
"services-jellyfin.html#services-jellyfin-options-shb.jellyfin.debug"
|
||||
],
|
||||
|
|
@ -3776,9 +4220,15 @@
|
|||
"services-jellyfin-usage": [
|
||||
"services-jellyfin.html#services-jellyfin-usage"
|
||||
],
|
||||
"services-jellyfin-usage-applicationdashboard": [
|
||||
"services-jellyfin.html#services-jellyfin-usage-applicationdashboard"
|
||||
],
|
||||
"services-jellyfin-usage-backup": [
|
||||
"services-jellyfin.html#services-jellyfin-usage-backup"
|
||||
],
|
||||
"services-jellyfin-usage-configuration": [
|
||||
"services-jellyfin.html#services-jellyfin-usage-configuration"
|
||||
],
|
||||
"services-karakeep": [
|
||||
"services-karakeep.html#services-karakeep"
|
||||
],
|
||||
|
|
@ -3824,6 +4274,21 @@
|
|||
"services-karakeep-options-shb.karakeep.backup.result.restoreScript": [
|
||||
"services-karakeep.html#services-karakeep-options-shb.karakeep.backup.result.restoreScript"
|
||||
],
|
||||
"services-karakeep-options-shb.karakeep.dashboard": [
|
||||
"services-karakeep.html#services-karakeep-options-shb.karakeep.dashboard"
|
||||
],
|
||||
"services-karakeep-options-shb.karakeep.dashboard.request": [
|
||||
"services-karakeep.html#services-karakeep-options-shb.karakeep.dashboard.request"
|
||||
],
|
||||
"services-karakeep-options-shb.karakeep.dashboard.request.externalUrl": [
|
||||
"services-karakeep.html#services-karakeep-options-shb.karakeep.dashboard.request.externalUrl"
|
||||
],
|
||||
"services-karakeep-options-shb.karakeep.dashboard.request.internalUrl": [
|
||||
"services-karakeep.html#services-karakeep-options-shb.karakeep.dashboard.request.internalUrl"
|
||||
],
|
||||
"services-karakeep-options-shb.karakeep.dashboard.result": [
|
||||
"services-karakeep.html#services-karakeep-options-shb.karakeep.dashboard.result"
|
||||
],
|
||||
"services-karakeep-options-shb.karakeep.domain": [
|
||||
"services-karakeep.html#services-karakeep-options-shb.karakeep.domain"
|
||||
],
|
||||
|
|
@ -3974,6 +4439,12 @@
|
|||
"services-karakeep-usage": [
|
||||
"services-karakeep.html#services-karakeep-usage"
|
||||
],
|
||||
"services-karakeep-usage-applicationdashboard": [
|
||||
"services-karakeep.html#services-karakeep-usage-applicationdashboard"
|
||||
],
|
||||
"services-karakeep-usage-configuration": [
|
||||
"services-karakeep.html#services-karakeep-usage-configuration"
|
||||
],
|
||||
"services-mailserver": [
|
||||
"services-mailserver.html#services-mailserver"
|
||||
],
|
||||
|
|
@ -4583,6 +5054,21 @@
|
|||
"services-nextcloudserver-options-shb.nextcloud.backup.result.restoreScript": [
|
||||
"services-nextcloud.html#services-nextcloudserver-options-shb.nextcloud.backup.result.restoreScript"
|
||||
],
|
||||
"services-nextcloudserver-options-shb.nextcloud.dashboard": [
|
||||
"services-nextcloud.html#services-nextcloudserver-options-shb.nextcloud.dashboard"
|
||||
],
|
||||
"services-nextcloudserver-options-shb.nextcloud.dashboard.request": [
|
||||
"services-nextcloud.html#services-nextcloudserver-options-shb.nextcloud.dashboard.request"
|
||||
],
|
||||
"services-nextcloudserver-options-shb.nextcloud.dashboard.request.externalUrl": [
|
||||
"services-nextcloud.html#services-nextcloudserver-options-shb.nextcloud.dashboard.request.externalUrl"
|
||||
],
|
||||
"services-nextcloudserver-options-shb.nextcloud.dashboard.request.internalUrl": [
|
||||
"services-nextcloud.html#services-nextcloudserver-options-shb.nextcloud.dashboard.request.internalUrl"
|
||||
],
|
||||
"services-nextcloudserver-options-shb.nextcloud.dashboard.result": [
|
||||
"services-nextcloud.html#services-nextcloudserver-options-shb.nextcloud.dashboard.result"
|
||||
],
|
||||
"services-nextcloudserver-options-shb.nextcloud.dataDir": [
|
||||
"services-nextcloud.html#services-nextcloudserver-options-shb.nextcloud.dataDir"
|
||||
],
|
||||
|
|
@ -4667,6 +5153,9 @@
|
|||
"services-nextcloudserver-usage": [
|
||||
"services-nextcloud.html#services-nextcloudserver-usage"
|
||||
],
|
||||
"services-nextcloudserver-usage-applicationdashboard": [
|
||||
"services-nextcloud.html#services-nextcloudserver-usage-applicationdashboard"
|
||||
],
|
||||
"services-nextcloudserver-usage-backup": [
|
||||
"services-nextcloud.html#services-nextcloudserver-usage-backup"
|
||||
],
|
||||
|
|
@ -4754,6 +5243,21 @@
|
|||
"services-open-webui-options-shb.open-webui.backup.result.restoreScript": [
|
||||
"services-open-webui.html#services-open-webui-options-shb.open-webui.backup.result.restoreScript"
|
||||
],
|
||||
"services-open-webui-options-shb.open-webui.dashboard": [
|
||||
"services-open-webui.html#services-open-webui-options-shb.open-webui.dashboard"
|
||||
],
|
||||
"services-open-webui-options-shb.open-webui.dashboard.request": [
|
||||
"services-open-webui.html#services-open-webui-options-shb.open-webui.dashboard.request"
|
||||
],
|
||||
"services-open-webui-options-shb.open-webui.dashboard.request.externalUrl": [
|
||||
"services-open-webui.html#services-open-webui-options-shb.open-webui.dashboard.request.externalUrl"
|
||||
],
|
||||
"services-open-webui-options-shb.open-webui.dashboard.request.internalUrl": [
|
||||
"services-open-webui.html#services-open-webui-options-shb.open-webui.dashboard.request.internalUrl"
|
||||
],
|
||||
"services-open-webui-options-shb.open-webui.dashboard.result": [
|
||||
"services-open-webui.html#services-open-webui-options-shb.open-webui.dashboard.result"
|
||||
],
|
||||
"services-open-webui-options-shb.open-webui.domain": [
|
||||
"services-open-webui.html#services-open-webui-options-shb.open-webui.domain"
|
||||
],
|
||||
|
|
@ -4859,12 +5363,21 @@
|
|||
"services-open-webui-usage": [
|
||||
"services-open-webui.html#services-open-webui-usage"
|
||||
],
|
||||
"services-open-webui-usage-applicationdashboard": [
|
||||
"services-open-webui.html#services-open-webui-usage-applicationdashboard"
|
||||
],
|
||||
"services-open-webui-usage-backup": [
|
||||
"services-open-webui.html#services-open-webui-usage-backup"
|
||||
],
|
||||
"services-open-webui-usage-configuration": [
|
||||
"services-open-webui.html#services-open-webui-usage-configuration"
|
||||
],
|
||||
"services-pinchflat": [
|
||||
"services-pinchflat.html#services-pinchflat"
|
||||
],
|
||||
"services-pinchflat-features": [
|
||||
"services-pinchflat.html#services-pinchflat-features"
|
||||
],
|
||||
"services-pinchflat-options": [
|
||||
"services-pinchflat.html#services-pinchflat-options"
|
||||
],
|
||||
|
|
@ -4901,6 +5414,21 @@
|
|||
"services-pinchflat-options-shb.pinchflat.backup.result.restoreScript": [
|
||||
"services-pinchflat.html#services-pinchflat-options-shb.pinchflat.backup.result.restoreScript"
|
||||
],
|
||||
"services-pinchflat-options-shb.pinchflat.dashboard": [
|
||||
"services-pinchflat.html#services-pinchflat-options-shb.pinchflat.dashboard"
|
||||
],
|
||||
"services-pinchflat-options-shb.pinchflat.dashboard.request": [
|
||||
"services-pinchflat.html#services-pinchflat-options-shb.pinchflat.dashboard.request"
|
||||
],
|
||||
"services-pinchflat-options-shb.pinchflat.dashboard.request.externalUrl": [
|
||||
"services-pinchflat.html#services-pinchflat-options-shb.pinchflat.dashboard.request.externalUrl"
|
||||
],
|
||||
"services-pinchflat-options-shb.pinchflat.dashboard.request.internalUrl": [
|
||||
"services-pinchflat.html#services-pinchflat-options-shb.pinchflat.dashboard.request.internalUrl"
|
||||
],
|
||||
"services-pinchflat-options-shb.pinchflat.dashboard.result": [
|
||||
"services-pinchflat.html#services-pinchflat-options-shb.pinchflat.dashboard.result"
|
||||
],
|
||||
"services-pinchflat-options-shb.pinchflat.domain": [
|
||||
"services-pinchflat.html#services-pinchflat-options-shb.pinchflat.domain"
|
||||
],
|
||||
|
|
@ -4982,9 +5510,15 @@
|
|||
"services-pinchflat-usage": [
|
||||
"services-pinchflat.html#services-pinchflat-usage"
|
||||
],
|
||||
"services-pinchflat-usage-applicationdashboard": [
|
||||
"services-pinchflat.html#services-pinchflat-usage-applicationdashboard"
|
||||
],
|
||||
"services-pinchflat-usage-backup": [
|
||||
"services-pinchflat.html#services-pinchflat-usage-backup"
|
||||
],
|
||||
"services-pinchflat-usage-configuration": [
|
||||
"services-pinchflat.html#services-pinchflat-usage-configuration"
|
||||
],
|
||||
"services-vaultwarden": [
|
||||
"services-vaultwarden.html#services-vaultwarden"
|
||||
],
|
||||
|
|
@ -5039,6 +5573,21 @@
|
|||
"services-vaultwarden-options-shb.vaultwarden.backup.result.restoreScript": [
|
||||
"services-vaultwarden.html#services-vaultwarden-options-shb.vaultwarden.backup.result.restoreScript"
|
||||
],
|
||||
"services-vaultwarden-options-shb.vaultwarden.dashboard": [
|
||||
"services-vaultwarden.html#services-vaultwarden-options-shb.vaultwarden.dashboard"
|
||||
],
|
||||
"services-vaultwarden-options-shb.vaultwarden.dashboard.request": [
|
||||
"services-vaultwarden.html#services-vaultwarden-options-shb.vaultwarden.dashboard.request"
|
||||
],
|
||||
"services-vaultwarden-options-shb.vaultwarden.dashboard.request.externalUrl": [
|
||||
"services-vaultwarden.html#services-vaultwarden-options-shb.vaultwarden.dashboard.request.externalUrl"
|
||||
],
|
||||
"services-vaultwarden-options-shb.vaultwarden.dashboard.request.internalUrl": [
|
||||
"services-vaultwarden.html#services-vaultwarden-options-shb.vaultwarden.dashboard.request.internalUrl"
|
||||
],
|
||||
"services-vaultwarden-options-shb.vaultwarden.dashboard.result": [
|
||||
"services-vaultwarden.html#services-vaultwarden-options-shb.vaultwarden.dashboard.result"
|
||||
],
|
||||
"services-vaultwarden-options-shb.vaultwarden.databasePassword": [
|
||||
"services-vaultwarden.html#services-vaultwarden-options-shb.vaultwarden.databasePassword"
|
||||
],
|
||||
|
|
@ -5150,6 +5699,9 @@
|
|||
"services-vaultwarden-usage": [
|
||||
"services-vaultwarden.html#services-vaultwarden-usage"
|
||||
],
|
||||
"services-vaultwarden-usage-applicationdashboard": [
|
||||
"services-vaultwarden.html#services-vaultwarden-usage-applicationdashboard"
|
||||
],
|
||||
"services-vaultwarden-usage-configuration": [
|
||||
"services-vaultwarden.html#services-vaultwarden-usage-configuration"
|
||||
],
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
<!-- Read these docs at https://shb.skarabox.com -->
|
||||
# Services {#services}
|
||||
|
||||
Services are usually web applications that SHB help you self-host.
|
||||
Services are usually web applications that SHB help you self-host some of your data.
|
||||
Configuration of those is purposely made more opinionated than the upstream nixpkgs modules
|
||||
in exchange for requiring less options to define.
|
||||
in exchange for an uniformized configuration experience.
|
||||
That is possible thanks to the extensive use of blocks provided by SHB.
|
||||
|
||||
::: {.note}
|
||||
|
|
@ -19,6 +19,7 @@ information is provided in the respective manual sections.
|
|||
| [Firefly-iii][] | Y (1) | Y | Y | Y | Y (2) | N |
|
||||
| [Forgejo][] | Y (1) | Y | Y | Y | Y (2) | N |
|
||||
| [Home-Assistant][] | Y (1) | Y | N | Y | Y (2) | N |
|
||||
| [Homepage][] | Y (1) | Y | N | Y | Y (2) | N |
|
||||
| [Jellyfin][] | Y (1) | Y | Y | Y | Y (2) | N |
|
||||
| [Karakeep][] | Y (1) | Y | Y | Y | Y (2) | N |
|
||||
| [Nextcloud Server][] | Y (1) | Y | Y | Y | Y (2) | P (3) |
|
||||
|
|
@ -40,6 +41,7 @@ Legend: **N**: no but WIP; **P**: partial; **Y**: yes
|
|||
[Firefly-iii]: services-firefly-iii.html
|
||||
[Forgejo]: services-forgejo.html
|
||||
[Home-Assistant]: services-home-assistant.html
|
||||
[Homepage]: services-homepage.html
|
||||
[Jellyfin]: services-jellyfin.html
|
||||
[Karakeep]: services-karakeep.html
|
||||
[Nextcloud Server]: services-nextcloud.html
|
||||
|
|
@ -48,6 +50,12 @@ Legend: **N**: no but WIP; **P**: partial; **Y**: yes
|
|||
[Simple NixOS Mailserver]: services-mailserver.html
|
||||
[Vaultwarden]: services-vaultwarden.html
|
||||
|
||||
## Dashboard {#services-category-dashboard}
|
||||
|
||||
```{=include=} chapters html:into-file=//services-homepage.html
|
||||
modules/services/homepage/docs/default.md
|
||||
```
|
||||
|
||||
## Documents {#services-category-documents}
|
||||
|
||||
```{=include=} chapters html:into-file=//services-nextcloud.html
|
||||
|
|
|
|||
15
flake.nix
15
flake.nix
|
|
@ -66,6 +66,9 @@
|
|||
# The contract dummies are used to show options for contracts.
|
||||
contractDummyModules = [
|
||||
modules/contracts/backup/dummyModule.nix
|
||||
modules/contracts/dashboard/dummyModule.nix
|
||||
modules/contracts/databasebackup/dummyModule.nix
|
||||
modules/contracts/secret/dummyModule.nix
|
||||
modules/contracts/ssl/dummyModule.nix
|
||||
];
|
||||
in
|
||||
|
|
@ -110,6 +113,7 @@
|
|||
(pkgs.path + "/nixos/modules/services/misc/forgejo.nix")
|
||||
];
|
||||
"services/home-assistant" = ./modules/services/home-assistant.nix;
|
||||
"services/homepage" = ./modules/services/homepage.nix;
|
||||
"services/jellyfin" = ./modules/services/jellyfin.nix;
|
||||
"services/karakeep" = ./modules/services/karakeep.nix;
|
||||
"services/mailserver" = ./modules/services/mailserver.nix;
|
||||
|
|
@ -131,6 +135,14 @@
|
|||
"backup"
|
||||
];
|
||||
};
|
||||
"contracts/dashboard" = {
|
||||
module = ./modules/contracts/dashboard/dummyModule.nix;
|
||||
optionRoot = [
|
||||
"shb"
|
||||
"contracts"
|
||||
"dashboard"
|
||||
];
|
||||
};
|
||||
"contracts/databasebackup" = {
|
||||
module = ./modules/contracts/databasebackup/dummyModule.nix;
|
||||
optionRoot = [
|
||||
|
|
@ -298,6 +310,7 @@
|
|||
// (vm_test "hledger" ./test/services/hledger.nix)
|
||||
// (vm_test "immich" ./test/services/immich.nix)
|
||||
// (vm_test "homeassistant" ./test/services/home-assistant.nix)
|
||||
// (vm_test "homepage" ./test/services/homepage.nix)
|
||||
// (vm_test "jellyfin" ./test/services/jellyfin.nix)
|
||||
// (vm_test "karakeep" ./test/services/karakeep.nix)
|
||||
// (vm_test "nextcloud" ./test/services/nextcloud.nix)
|
||||
|
|
@ -407,6 +420,7 @@
|
|||
self.nixosModules.hledger
|
||||
self.nixosModules.immich
|
||||
self.nixosModules.home-assistant
|
||||
self.nixosModules.homepage
|
||||
self.nixosModules.jellyfin
|
||||
self.nixosModules.karakeep
|
||||
self.nixosModules.mailserver
|
||||
|
|
@ -445,6 +459,7 @@
|
|||
nixosModules.hledger = modules/services/hledger.nix;
|
||||
nixosModules.immich = modules/services/immich.nix;
|
||||
nixosModules.home-assistant = modules/services/home-assistant.nix;
|
||||
nixosModules.homepage = modules/services/homepage.nix;
|
||||
nixosModules.jellyfin = modules/services/jellyfin.nix;
|
||||
nixosModules.karakeep = modules/services/karakeep.nix;
|
||||
nixosModules.mailserver = modules/services/mailserver.nix;
|
||||
|
|
|
|||
743
lib/default.nix
743
lib/default.nix
|
|
@ -2,411 +2,420 @@
|
|||
let
|
||||
inherit (builtins) isAttrs hasAttr;
|
||||
inherit (lib) any concatMapStringsSep concatStringsSep;
|
||||
in
|
||||
rec {
|
||||
# Replace secrets in a file.
|
||||
# - userConfig is an attrset that will produce a config file.
|
||||
# - resultPath is the location the config file should have on the filesystem.
|
||||
# - generator is a function taking two arguments name and value and returning path in the nix
|
||||
# nix store where the
|
||||
replaceSecrets =
|
||||
{
|
||||
userConfig,
|
||||
resultPath,
|
||||
generator,
|
||||
user ? null,
|
||||
permissions ? "u=r,g=r,o=",
|
||||
}:
|
||||
let
|
||||
configWithTemplates = withReplacements userConfig;
|
||||
shb = rec {
|
||||
# Replace secrets in a file.
|
||||
# - userConfig is an attrset that will produce a config file.
|
||||
# - resultPath is the location the config file should have on the filesystem.
|
||||
# - generator is a function taking two arguments name and value and returning path in the nix
|
||||
# nix store where the
|
||||
replaceSecrets =
|
||||
{
|
||||
userConfig,
|
||||
resultPath,
|
||||
generator,
|
||||
user ? null,
|
||||
permissions ? "u=r,g=r,o=",
|
||||
}:
|
||||
let
|
||||
configWithTemplates = withReplacements userConfig;
|
||||
|
||||
nonSecretConfigFile = generator "template" configWithTemplates;
|
||||
nonSecretConfigFile = generator "template" configWithTemplates;
|
||||
|
||||
replacements = getReplacements userConfig;
|
||||
in
|
||||
replaceSecretsScript {
|
||||
file = nonSecretConfigFile;
|
||||
inherit resultPath replacements;
|
||||
inherit user permissions;
|
||||
};
|
||||
|
||||
replaceSecretsFormatAdapter = format: format.generate;
|
||||
replaceSecretsGeneratorAdapter =
|
||||
generator: name: value:
|
||||
pkgs.writeText "generator " (generator value);
|
||||
toEnvVar = replaceSecretsGeneratorAdapter (
|
||||
v: (lib.generators.toINIWithGlobalSection { } { globalSection = v; })
|
||||
);
|
||||
|
||||
template =
|
||||
file: newPath: replacements:
|
||||
replaceSecretsScript {
|
||||
inherit file replacements;
|
||||
resultPath = newPath;
|
||||
};
|
||||
|
||||
genReplacement =
|
||||
secret:
|
||||
let
|
||||
t =
|
||||
{
|
||||
transform ? null,
|
||||
...
|
||||
}:
|
||||
if isNull transform then x: x else transform;
|
||||
in
|
||||
lib.attrsets.nameValuePair (secretName secret.name) ((t secret) "$(cat ${toString secret.source})");
|
||||
|
||||
replaceSecretsScript =
|
||||
{
|
||||
file,
|
||||
resultPath,
|
||||
replacements,
|
||||
user ? null,
|
||||
permissions ? "u=r,g=r,o=",
|
||||
}:
|
||||
let
|
||||
templatePath = resultPath + ".template";
|
||||
|
||||
# We check that the files containing the secrets have the
|
||||
# correct permissions for us to read them in this separate
|
||||
# step. Otherwise, the $(cat ...) commands inside the sed
|
||||
# replacements could fail but not fail individually but
|
||||
# not fail the whole script.
|
||||
checkPermissions = concatMapStringsSep "\n" (
|
||||
pattern: "cat ${pattern.source} > /dev/null"
|
||||
) replacements;
|
||||
|
||||
sedPatterns = concatMapStringsSep " " (pattern: "-e \"s|${pattern.name}|${pattern.value}|\"") (
|
||||
map genReplacement replacements
|
||||
);
|
||||
|
||||
sedCmd = if replacements == [ ] then "cat" else "${pkgs.gnused}/bin/sed ${sedPatterns}";
|
||||
in
|
||||
''
|
||||
set -euo pipefail
|
||||
|
||||
${checkPermissions}
|
||||
|
||||
mkdir -p $(dirname ${templatePath})
|
||||
ln -fs ${file} ${templatePath}
|
||||
rm -f ${resultPath}
|
||||
touch ${resultPath}
|
||||
''
|
||||
+ (lib.optionalString (user != null) ''
|
||||
chown ${user} ${resultPath}
|
||||
'')
|
||||
+ ''
|
||||
${sedCmd} ${templatePath} > ${resultPath}
|
||||
chmod ${permissions} ${resultPath}
|
||||
'';
|
||||
|
||||
secretFileType = lib.types.submodule {
|
||||
options = {
|
||||
source = lib.mkOption {
|
||||
type = lib.types.path;
|
||||
description = "File containing the value.";
|
||||
replacements = getReplacements userConfig;
|
||||
in
|
||||
replaceSecretsScript {
|
||||
file = nonSecretConfigFile;
|
||||
inherit resultPath replacements;
|
||||
inherit user permissions;
|
||||
};
|
||||
|
||||
transform = lib.mkOption {
|
||||
type = lib.types.raw;
|
||||
description = "An optional function to transform the secret.";
|
||||
default = null;
|
||||
example = lib.literalExpression ''
|
||||
v: "prefix-$${v}-suffix"
|
||||
'';
|
||||
replaceSecretsFormatAdapter = format: format.generate;
|
||||
replaceSecretsGeneratorAdapter =
|
||||
generator: name: value:
|
||||
pkgs.writeText "generator " (generator value);
|
||||
toEnvVar = replaceSecretsGeneratorAdapter (
|
||||
v: (lib.generators.toINIWithGlobalSection { } { globalSection = v; })
|
||||
);
|
||||
|
||||
template =
|
||||
file: newPath: replacements:
|
||||
replaceSecretsScript {
|
||||
inherit file replacements;
|
||||
resultPath = newPath;
|
||||
};
|
||||
|
||||
genReplacement =
|
||||
secret:
|
||||
let
|
||||
t =
|
||||
{
|
||||
transform ? null,
|
||||
...
|
||||
}:
|
||||
if isNull transform then x: x else transform;
|
||||
in
|
||||
lib.attrsets.nameValuePair (secretName secret.name) ((t secret) "$(cat ${toString secret.source})");
|
||||
|
||||
replaceSecretsScript =
|
||||
{
|
||||
file,
|
||||
resultPath,
|
||||
replacements,
|
||||
user ? null,
|
||||
permissions ? "u=r,g=r,o=",
|
||||
}:
|
||||
let
|
||||
templatePath = resultPath + ".template";
|
||||
|
||||
# We check that the files containing the secrets have the
|
||||
# correct permissions for us to read them in this separate
|
||||
# step. Otherwise, the $(cat ...) commands inside the sed
|
||||
# replacements could fail but not fail individually but
|
||||
# not fail the whole script.
|
||||
checkPermissions = concatMapStringsSep "\n" (
|
||||
pattern: "cat ${pattern.source} > /dev/null"
|
||||
) replacements;
|
||||
|
||||
sedPatterns = concatMapStringsSep " " (pattern: "-e \"s|${pattern.name}|${pattern.value}|\"") (
|
||||
map genReplacement replacements
|
||||
);
|
||||
|
||||
sedCmd = if replacements == [ ] then "cat" else "${pkgs.gnused}/bin/sed ${sedPatterns}";
|
||||
in
|
||||
''
|
||||
set -euo pipefail
|
||||
|
||||
${checkPermissions}
|
||||
|
||||
mkdir -p $(dirname ${templatePath})
|
||||
ln -fs ${file} ${templatePath}
|
||||
rm -f ${resultPath}
|
||||
touch ${resultPath}
|
||||
''
|
||||
+ (lib.optionalString (user != null) ''
|
||||
chown ${user} ${resultPath}
|
||||
'')
|
||||
+ ''
|
||||
${sedCmd} ${templatePath} > ${resultPath}
|
||||
chmod ${permissions} ${resultPath}
|
||||
'';
|
||||
|
||||
secretFileType = lib.types.submodule {
|
||||
options = {
|
||||
source = lib.mkOption {
|
||||
type = lib.types.path;
|
||||
description = "File containing the value.";
|
||||
};
|
||||
|
||||
transform = lib.mkOption {
|
||||
type = lib.types.raw;
|
||||
description = "An optional function to transform the secret.";
|
||||
default = null;
|
||||
example = lib.literalExpression ''
|
||||
v: "prefix-$${v}-suffix"
|
||||
'';
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
secretName =
|
||||
names: "%SECRET${lib.strings.toUpper (lib.strings.concatMapStrings (s: "_" + s) names)}%";
|
||||
secretName =
|
||||
names: "%SECRET${lib.strings.toUpper (lib.strings.concatMapStrings (s: "_" + s) names)}%";
|
||||
|
||||
withReplacements =
|
||||
attrs:
|
||||
let
|
||||
valueOrReplacement =
|
||||
name: value: if !(builtins.isAttrs value && value ? "source") then value else secretName name;
|
||||
in
|
||||
mapAttrsRecursiveCond (v: !v ? "source") valueOrReplacement attrs;
|
||||
withReplacements =
|
||||
attrs:
|
||||
let
|
||||
valueOrReplacement =
|
||||
name: value: if !(builtins.isAttrs value && value ? "source") then value else secretName name;
|
||||
in
|
||||
mapAttrsRecursiveCond (v: !v ? "source") valueOrReplacement attrs;
|
||||
|
||||
getReplacements =
|
||||
attrs:
|
||||
let
|
||||
addNameField =
|
||||
name: value:
|
||||
if !(builtins.isAttrs value && value ? "source") then value else value // { name = name; };
|
||||
getReplacements =
|
||||
attrs:
|
||||
let
|
||||
addNameField =
|
||||
name: value:
|
||||
if !(builtins.isAttrs value && value ? "source") then value else value // { name = name; };
|
||||
|
||||
secretsWithName = mapAttrsRecursiveCond (v: !v ? "source") addNameField attrs;
|
||||
in
|
||||
collect (v: builtins.isAttrs v && v ? "source") secretsWithName;
|
||||
secretsWithName = mapAttrsRecursiveCond (v: !v ? "source") addNameField attrs;
|
||||
in
|
||||
collect (v: builtins.isAttrs v && v ? "source") secretsWithName;
|
||||
|
||||
# Inspired lib.attrsets.mapAttrsRecursiveCond but also recurses on lists.
|
||||
mapAttrsRecursiveCond =
|
||||
# A function, given the attribute set the recursion is currently at, determine if to recurse deeper into that attribute set.
|
||||
cond:
|
||||
# A function, given a list of attribute names and a value, returns a new value.
|
||||
f:
|
||||
# Attribute set or list to recursively map over.
|
||||
set:
|
||||
let
|
||||
recurse =
|
||||
path: val:
|
||||
if builtins.isAttrs val && cond val then
|
||||
lib.attrsets.mapAttrs (n: v: recurse (path ++ [ n ]) v) val
|
||||
else if builtins.isList val && cond val then
|
||||
lib.lists.imap0 (i: v: recurse (path ++ [ (builtins.toString i) ]) v) val
|
||||
else
|
||||
f path val;
|
||||
in
|
||||
recurse [ ] set;
|
||||
# Inspired lib.attrsets.mapAttrsRecursiveCond but also recurses on lists.
|
||||
mapAttrsRecursiveCond =
|
||||
# A function, given the attribute set the recursion is currently at, determine if to recurse deeper into that attribute set.
|
||||
cond:
|
||||
# A function, given a list of attribute names and a value, returns a new value.
|
||||
f:
|
||||
# Attribute set or list to recursively map over.
|
||||
set:
|
||||
let
|
||||
recurse =
|
||||
path: val:
|
||||
if builtins.isAttrs val && cond val then
|
||||
lib.attrsets.mapAttrs (n: v: recurse (path ++ [ n ]) v) val
|
||||
else if builtins.isList val && cond val then
|
||||
lib.lists.imap0 (i: v: recurse (path ++ [ (builtins.toString i) ]) v) val
|
||||
else
|
||||
f path val;
|
||||
in
|
||||
recurse [ ] set;
|
||||
|
||||
# Like lib.attrsets.collect but also recurses on lists.
|
||||
collect =
|
||||
# Given an attribute's value, determine if recursion should stop.
|
||||
pred:
|
||||
# The attribute set to recursively collect.
|
||||
attrs:
|
||||
if pred attrs then
|
||||
[ attrs ]
|
||||
else if builtins.isAttrs attrs then
|
||||
lib.lists.concatMap (collect pred) (lib.attrsets.attrValues attrs)
|
||||
else if builtins.isList attrs then
|
||||
lib.lists.concatMap (collect pred) attrs
|
||||
else
|
||||
[ ];
|
||||
# Like lib.attrsets.collect but also recurses on lists.
|
||||
collect =
|
||||
# Given an attribute's value, determine if recursion should stop.
|
||||
pred:
|
||||
# The attribute set to recursively collect.
|
||||
attrs:
|
||||
if pred attrs then
|
||||
[ attrs ]
|
||||
else if builtins.isAttrs attrs then
|
||||
lib.lists.concatMap (collect pred) (lib.attrsets.attrValues attrs)
|
||||
else if builtins.isList attrs then
|
||||
lib.lists.concatMap (collect pred) attrs
|
||||
else
|
||||
[ ];
|
||||
|
||||
indent =
|
||||
i: str:
|
||||
lib.concatMapStringsSep "\n" (x: (lib.strings.replicate i " ") + x) (lib.splitString "\n" str);
|
||||
indent =
|
||||
i: str:
|
||||
lib.concatMapStringsSep "\n" (x: (lib.strings.replicate i " ") + x) (lib.splitString "\n" str);
|
||||
|
||||
# Generator for XML
|
||||
formatXML =
|
||||
{
|
||||
enclosingRoot ? null,
|
||||
}:
|
||||
{
|
||||
type =
|
||||
with lib.types;
|
||||
let
|
||||
valueType =
|
||||
nullOr (oneOf [
|
||||
bool
|
||||
int
|
||||
float
|
||||
str
|
||||
path
|
||||
(attrsOf valueType)
|
||||
(listOf valueType)
|
||||
])
|
||||
// {
|
||||
description = "XML value";
|
||||
};
|
||||
in
|
||||
valueType;
|
||||
# Generator for XML
|
||||
formatXML =
|
||||
{
|
||||
enclosingRoot ? null,
|
||||
}:
|
||||
{
|
||||
type =
|
||||
with lib.types;
|
||||
let
|
||||
valueType =
|
||||
nullOr (oneOf [
|
||||
bool
|
||||
int
|
||||
float
|
||||
str
|
||||
path
|
||||
(attrsOf valueType)
|
||||
(listOf valueType)
|
||||
])
|
||||
// {
|
||||
description = "XML value";
|
||||
};
|
||||
in
|
||||
valueType;
|
||||
|
||||
generate =
|
||||
name: value:
|
||||
pkgs.callPackage (
|
||||
generate =
|
||||
name: value:
|
||||
pkgs.callPackage (
|
||||
{ runCommand, python3 }:
|
||||
runCommand "config"
|
||||
{
|
||||
value = builtins.toJSON (if enclosingRoot == null then value else { ${enclosingRoot} = value; });
|
||||
passAsFile = [ "value" ];
|
||||
}
|
||||
(
|
||||
pkgs.writers.writePython3 "dict2xml"
|
||||
{
|
||||
libraries = with python3.pkgs; [
|
||||
python
|
||||
dict2xml
|
||||
];
|
||||
}
|
||||
''
|
||||
import os
|
||||
import json
|
||||
from dict2xml import dict2xml
|
||||
|
||||
with open(os.environ["valuePath"]) as f:
|
||||
content = json.loads(f.read())
|
||||
if content is None:
|
||||
print("Could not parse env var valuePath as json")
|
||||
os.exit(2)
|
||||
with open(os.environ["out"], "w") as out:
|
||||
out.write(dict2xml(content))
|
||||
''
|
||||
)
|
||||
) { };
|
||||
|
||||
};
|
||||
|
||||
parseXML =
|
||||
xml:
|
||||
let
|
||||
xmlToJsonFile = pkgs.callPackage (
|
||||
{ runCommand, python3 }:
|
||||
runCommand "config"
|
||||
{
|
||||
value = builtins.toJSON (if enclosingRoot == null then value else { ${enclosingRoot} = value; });
|
||||
passAsFile = [ "value" ];
|
||||
inherit xml;
|
||||
passAsFile = [ "xml" ];
|
||||
}
|
||||
(
|
||||
pkgs.writers.writePython3 "dict2xml"
|
||||
pkgs.writers.writePython3 "xml2json"
|
||||
{
|
||||
libraries = with python3.pkgs; [
|
||||
python
|
||||
dict2xml
|
||||
];
|
||||
libraries = with python3.pkgs; [ python ];
|
||||
}
|
||||
''
|
||||
import os
|
||||
import json
|
||||
from dict2xml import dict2xml
|
||||
from collections import ChainMap
|
||||
from xml.etree import ElementTree
|
||||
|
||||
|
||||
def xml_to_dict_recursive(root):
|
||||
all_descendants = list(root)
|
||||
if len(all_descendants) == 0:
|
||||
return {root.tag: root.text}
|
||||
else:
|
||||
merged_dict = ChainMap(*map(xml_to_dict_recursive, all_descendants))
|
||||
return {root.tag: dict(merged_dict)}
|
||||
|
||||
|
||||
with open(os.environ["xmlPath"]) as f:
|
||||
root = ElementTree.XML(f.read())
|
||||
xml = xml_to_dict_recursive(root)
|
||||
j = json.dumps(xml)
|
||||
|
||||
with open(os.environ["valuePath"]) as f:
|
||||
content = json.loads(f.read())
|
||||
if content is None:
|
||||
print("Could not parse env var valuePath as json")
|
||||
os.exit(2)
|
||||
with open(os.environ["out"], "w") as out:
|
||||
out.write(dict2xml(content))
|
||||
out.write(j)
|
||||
''
|
||||
)
|
||||
) { };
|
||||
in
|
||||
builtins.fromJSON (builtins.readFile xmlToJsonFile);
|
||||
|
||||
};
|
||||
renameAttrName =
|
||||
attrset: from: to:
|
||||
(lib.attrsets.filterAttrs (name: v: name == from) attrset)
|
||||
// {
|
||||
${to} = attrset.${from};
|
||||
};
|
||||
|
||||
parseXML =
|
||||
xml:
|
||||
let
|
||||
xmlToJsonFile = pkgs.callPackage (
|
||||
{ runCommand, python3 }:
|
||||
runCommand "config"
|
||||
# Taken from https://github.com/antifuchs/nix-flake-tests/blob/main/default.nix
|
||||
# with a nicer diff display function.
|
||||
check =
|
||||
{ pkgs, tests }:
|
||||
let
|
||||
formatValue =
|
||||
val:
|
||||
if (builtins.isList val || builtins.isAttrs val) then
|
||||
builtins.toJSON val
|
||||
else
|
||||
builtins.toString val;
|
||||
|
||||
resultToString =
|
||||
{
|
||||
inherit xml;
|
||||
passAsFile = [ "xml" ];
|
||||
}
|
||||
(
|
||||
pkgs.writers.writePython3 "xml2json"
|
||||
name,
|
||||
expected,
|
||||
result,
|
||||
}:
|
||||
builtins.readFile (
|
||||
pkgs.runCommand "nix-flake-tests-error"
|
||||
{
|
||||
libraries = with python3.pkgs; [ python ];
|
||||
expected = formatValue expected;
|
||||
result = formatValue result;
|
||||
passAsFile = [
|
||||
"expected"
|
||||
"result"
|
||||
];
|
||||
}
|
||||
''
|
||||
import os
|
||||
import json
|
||||
from collections import ChainMap
|
||||
from xml.etree import ElementTree
|
||||
|
||||
|
||||
def xml_to_dict_recursive(root):
|
||||
all_descendants = list(root)
|
||||
if len(all_descendants) == 0:
|
||||
return {root.tag: root.text}
|
||||
else:
|
||||
merged_dict = ChainMap(*map(xml_to_dict_recursive, all_descendants))
|
||||
return {root.tag: dict(merged_dict)}
|
||||
|
||||
|
||||
with open(os.environ["xmlPath"]) as f:
|
||||
root = ElementTree.XML(f.read())
|
||||
xml = xml_to_dict_recursive(root)
|
||||
j = json.dumps(xml)
|
||||
|
||||
with open(os.environ["out"], "w") as out:
|
||||
out.write(j)
|
||||
echo "${name} failed (- expected, + result)" > $out
|
||||
cp ''${expectedPath} ''${expectedPath}.json
|
||||
cp ''${resultPath} ''${resultPath}.json
|
||||
${pkgs.deepdiff}/bin/deep diff ''${expectedPath}.json ''${resultPath}.json >> $out
|
||||
''
|
||||
)
|
||||
);
|
||||
|
||||
results = pkgs.lib.runTests tests;
|
||||
in
|
||||
if results != [ ] then
|
||||
builtins.throw (concatStringsSep "\n" (map resultToString (lib.traceValSeq results)))
|
||||
else
|
||||
pkgs.runCommand "nix-flake-tests-success" { } "echo > $out";
|
||||
|
||||
genConfigOutOfBandSystemd =
|
||||
{
|
||||
config,
|
||||
configLocation,
|
||||
generator,
|
||||
user ? null,
|
||||
permissions ? "u=r,g=r,o=",
|
||||
}:
|
||||
{
|
||||
loadCredentials = getLoadCredentials "source" config;
|
||||
preStart = lib.mkBefore (replaceSecrets {
|
||||
userConfig = updateToLoadCredentials "source" "$CREDENTIALS_DIRECTORY" config;
|
||||
resultPath = configLocation;
|
||||
inherit generator;
|
||||
inherit user permissions;
|
||||
});
|
||||
};
|
||||
|
||||
updateToLoadCredentials =
|
||||
sourceField: rootDir: attrs:
|
||||
let
|
||||
hasPlaceholderField = v: isAttrs v && hasAttr sourceField v;
|
||||
|
||||
valueOrLoadCredential =
|
||||
path: value:
|
||||
if !(hasPlaceholderField value) then
|
||||
value
|
||||
else
|
||||
value // { ${sourceField} = rootDir + "/" + concatStringsSep "_" path; };
|
||||
in
|
||||
mapAttrsRecursiveCond (v: !(hasPlaceholderField v)) valueOrLoadCredential attrs;
|
||||
|
||||
getLoadCredentials =
|
||||
sourceField: attrs:
|
||||
let
|
||||
hasPlaceholderField = v: isAttrs v && hasAttr sourceField v;
|
||||
|
||||
addPathField =
|
||||
path: value: if !(hasPlaceholderField value) then value else value // { inherit path; };
|
||||
|
||||
secretsWithPath = mapAttrsRecursiveCond (v: !(hasPlaceholderField v)) addPathField attrs;
|
||||
|
||||
allSecrets = collect (v: hasPlaceholderField v) secretsWithPath;
|
||||
|
||||
genLoadCredentials = secret: "${concatStringsSep "_" secret.path}:${secret.${sourceField}}";
|
||||
in
|
||||
map genLoadCredentials allSecrets;
|
||||
|
||||
anyNotNull = any (x: x != null);
|
||||
|
||||
mkJellyfinPlugin =
|
||||
{
|
||||
pname,
|
||||
version,
|
||||
hash,
|
||||
url,
|
||||
}:
|
||||
pkgs.callPackage (
|
||||
{ stdenv, fetchzip }:
|
||||
stdenv.mkDerivation (finalAttrs: {
|
||||
inherit pname version;
|
||||
|
||||
src = fetchzip {
|
||||
inherit url hash;
|
||||
stripRoot = false;
|
||||
};
|
||||
|
||||
dontBuild = true;
|
||||
|
||||
installPhase = ''
|
||||
mkdir $out
|
||||
cp -r . $out
|
||||
'';
|
||||
})
|
||||
) { };
|
||||
in
|
||||
builtins.fromJSON (builtins.readFile xmlToJsonFile);
|
||||
|
||||
renameAttrName =
|
||||
attrset: from: to:
|
||||
(lib.attrsets.filterAttrs (name: v: name == from) attrset)
|
||||
// {
|
||||
${to} = attrset.${from};
|
||||
};
|
||||
update =
|
||||
attr: fn: attrset:
|
||||
attrset // { ${attr} = fn attrset.${attr}; };
|
||||
|
||||
# Taken from https://github.com/antifuchs/nix-flake-tests/blob/main/default.nix
|
||||
# with a nicer diff display function.
|
||||
check =
|
||||
{ pkgs, tests }:
|
||||
let
|
||||
formatValue =
|
||||
val:
|
||||
if (builtins.isList val || builtins.isAttrs val) then
|
||||
builtins.toJSON val
|
||||
else
|
||||
builtins.toString val;
|
||||
|
||||
resultToString =
|
||||
{
|
||||
name,
|
||||
expected,
|
||||
result,
|
||||
}:
|
||||
builtins.readFile (
|
||||
pkgs.runCommand "nix-flake-tests-error"
|
||||
{
|
||||
expected = formatValue expected;
|
||||
result = formatValue result;
|
||||
passAsFile = [
|
||||
"expected"
|
||||
"result"
|
||||
];
|
||||
}
|
||||
''
|
||||
echo "${name} failed (- expected, + result)" > $out
|
||||
cp ''${expectedPath} ''${expectedPath}.json
|
||||
cp ''${resultPath} ''${resultPath}.json
|
||||
${pkgs.deepdiff}/bin/deep diff ''${expectedPath}.json ''${resultPath}.json >> $out
|
||||
''
|
||||
);
|
||||
|
||||
results = pkgs.lib.runTests tests;
|
||||
in
|
||||
if results != [ ] then
|
||||
builtins.throw (concatStringsSep "\n" (map resultToString (lib.traceValSeq results)))
|
||||
else
|
||||
pkgs.runCommand "nix-flake-tests-success" { } "echo > $out";
|
||||
|
||||
genConfigOutOfBandSystemd =
|
||||
{
|
||||
config,
|
||||
configLocation,
|
||||
generator,
|
||||
user ? null,
|
||||
permissions ? "u=r,g=r,o=",
|
||||
}:
|
||||
{
|
||||
loadCredentials = getLoadCredentials "source" config;
|
||||
preStart = lib.mkBefore (replaceSecrets {
|
||||
userConfig = updateToLoadCredentials "source" "$CREDENTIALS_DIRECTORY" config;
|
||||
resultPath = configLocation;
|
||||
inherit generator;
|
||||
inherit user permissions;
|
||||
});
|
||||
};
|
||||
|
||||
updateToLoadCredentials =
|
||||
sourceField: rootDir: attrs:
|
||||
let
|
||||
hasPlaceholderField = v: isAttrs v && hasAttr sourceField v;
|
||||
|
||||
valueOrLoadCredential =
|
||||
path: value:
|
||||
if !(hasPlaceholderField value) then
|
||||
value
|
||||
else
|
||||
value // { ${sourceField} = rootDir + "/" + concatStringsSep "_" path; };
|
||||
in
|
||||
mapAttrsRecursiveCond (v: !(hasPlaceholderField v)) valueOrLoadCredential attrs;
|
||||
|
||||
getLoadCredentials =
|
||||
sourceField: attrs:
|
||||
let
|
||||
hasPlaceholderField = v: isAttrs v && hasAttr sourceField v;
|
||||
|
||||
addPathField =
|
||||
path: value: if !(hasPlaceholderField value) then value else value // { inherit path; };
|
||||
|
||||
secretsWithPath = mapAttrsRecursiveCond (v: !(hasPlaceholderField v)) addPathField attrs;
|
||||
|
||||
allSecrets = collect (v: hasPlaceholderField v) secretsWithPath;
|
||||
|
||||
genLoadCredentials = secret: "${concatStringsSep "_" secret.path}:${secret.${sourceField}}";
|
||||
in
|
||||
map genLoadCredentials allSecrets;
|
||||
|
||||
anyNotNull = any (x: x != null);
|
||||
|
||||
mkJellyfinPlugin =
|
||||
{
|
||||
pname,
|
||||
version,
|
||||
hash,
|
||||
url,
|
||||
}:
|
||||
pkgs.callPackage (
|
||||
{ stdenv, fetchzip }:
|
||||
stdenv.mkDerivation (finalAttrs: {
|
||||
inherit pname version;
|
||||
|
||||
src = fetchzip {
|
||||
inherit url hash;
|
||||
stripRoot = false;
|
||||
};
|
||||
|
||||
dontBuild = true;
|
||||
|
||||
installPhase = ''
|
||||
mkdir $out
|
||||
cp -r . $out
|
||||
'';
|
||||
})
|
||||
) { };
|
||||
};
|
||||
in
|
||||
shb
|
||||
// {
|
||||
homepage = pkgs.callPackage ./homepage.nix { inherit shb; };
|
||||
}
|
||||
|
|
|
|||
92
lib/homepage.nix
Normal file
92
lib/homepage.nix
Normal file
|
|
@ -0,0 +1,92 @@
|
|||
{ lib, shb }:
|
||||
let
|
||||
sort =
|
||||
attr: vs:
|
||||
map (v: { ${v.name} = v.${attr}; }) (
|
||||
lib.sortOn (v: v.sortOrder) (lib.mapAttrsToList (n: v: v // { name = n; }) vs)
|
||||
);
|
||||
|
||||
slufigy = builtins.replaceStrings [ "-" ] [ "_" ];
|
||||
|
||||
mkService =
|
||||
groupName: serviceName:
|
||||
{
|
||||
request,
|
||||
...
|
||||
}:
|
||||
apiKey: settings:
|
||||
lib.recursiveUpdate (
|
||||
{
|
||||
href = request.externalUrl;
|
||||
siteMonitor = if (request.internalUrl == null) then null else request.internalUrl;
|
||||
icon = "sh-${lib.toLower serviceName}";
|
||||
}
|
||||
// lib.optionalAttrs (apiKey != null) {
|
||||
widget = {
|
||||
# Duplicating because widgets call the api key various names
|
||||
# and duplicating is a hacky but easy solution.
|
||||
key = "{{HOMEPAGE_FILE_${slufigy groupName}_${slufigy serviceName}}}";
|
||||
password = "{{HOMEPAGE_FILE_${slufigy groupName}_${slufigy serviceName}}}";
|
||||
type = lib.toLower serviceName;
|
||||
url = if (request.internalUrl != null) then request.internalUrl else request.externalUrl;
|
||||
};
|
||||
}
|
||||
) settings;
|
||||
|
||||
asServiceGroup =
|
||||
cfg:
|
||||
sort "services" (
|
||||
lib.mapAttrs (
|
||||
groupName: groupCfg:
|
||||
shb.update "services" (
|
||||
services:
|
||||
sort "dashboard" (
|
||||
lib.mapAttrs (
|
||||
serviceName: serviceCfg:
|
||||
shb.update "dashboard" (
|
||||
dashboard:
|
||||
(mkService groupName serviceName) dashboard serviceCfg.apiKey (serviceCfg.settings or { })
|
||||
) serviceCfg
|
||||
) services
|
||||
)
|
||||
) groupCfg
|
||||
) cfg
|
||||
);
|
||||
|
||||
allKeys =
|
||||
cfg:
|
||||
let
|
||||
flat = lib.flatten (
|
||||
lib.mapAttrsToList (
|
||||
groupName: groupCfg:
|
||||
lib.mapAttrsToList (
|
||||
serviceName: serviceCfg:
|
||||
lib.optionalAttrs (serviceCfg.apiKey != null) {
|
||||
inherit serviceName groupName;
|
||||
inherit (serviceCfg.apiKey.result) path;
|
||||
}
|
||||
) groupCfg.services
|
||||
) cfg
|
||||
);
|
||||
|
||||
flatWithApiKey = builtins.filter (v: v != { }) flat;
|
||||
in
|
||||
builtins.listToAttrs (
|
||||
map (
|
||||
{
|
||||
groupName,
|
||||
serviceName,
|
||||
path,
|
||||
}:
|
||||
lib.nameValuePair "${slufigy groupName}_${slufigy serviceName}" path
|
||||
) flatWithApiKey
|
||||
);
|
||||
in
|
||||
{
|
||||
inherit
|
||||
allKeys
|
||||
asServiceGroup
|
||||
mkService
|
||||
sort
|
||||
;
|
||||
}
|
||||
|
|
@ -390,6 +390,20 @@ in
|
|||
to see exactly what Authelia receives and sends back.
|
||||
'';
|
||||
};
|
||||
|
||||
dashboard = lib.mkOption {
|
||||
description = ''
|
||||
Dashboard contract consumer
|
||||
'';
|
||||
default = { };
|
||||
type = lib.types.submodule {
|
||||
options = shb.contracts.dashboard.mkRequester {
|
||||
externalUrl = "https://${cfg.subdomain}.${cfg.domain}";
|
||||
externalUrlText = "https://\${config.shb.authelia.subdomain}.\${config.shb.authelia.domain}";
|
||||
internalUrl = "http://127.0.0.1:${toString listenPort}";
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
config = lib.mkIf cfg.enable {
|
||||
|
|
|
|||
|
|
@ -12,7 +12,13 @@ as well as some extensive [troubleshooting](#blocks-authelia-troubleshooting) fe
|
|||
|
||||
Note that forward authentication is configured with the [nginx block](blocks-nginx.html#blocks-nginx-usage-shbforwardauth).
|
||||
|
||||
## Global Setup {#blocks-authelia-global-setup}
|
||||
## Features {#services-authelia-features}
|
||||
|
||||
- Integration with the [dashboard contract](contracts-dashboard.html) for displaying user facing application in a dashboard. [Manual](#services-authelia-usage-applicationdashboard)
|
||||
|
||||
## Usage {#services-authelia-usage}
|
||||
|
||||
### Initial Configuration {#blocks-authelia-usage-configuration}
|
||||
|
||||
Authelia cannot work without SSL and LDAP.
|
||||
So setting up the Authelia block requires to setup the [SSL block][] first
|
||||
|
|
@ -80,6 +86,22 @@ Crucially, the `shb.authelia.secrets.ldapAdminPasswordFile` must be the same
|
|||
as the `shb.lldap.ldapUserPassword` defined for the [LLDAP block][].
|
||||
This is done using Sops' `key` option.
|
||||
|
||||
### Application Dashboard {#services-authelia-usage-applicationdashboard}
|
||||
|
||||
Integration with the [dashboard contract](contracts-dashboard.html) is provided
|
||||
by the [dashboard option](#blocks-authelia-options-shb.authelia.dashboard).
|
||||
|
||||
For example using the [Homepage](services-homepage.html) service:
|
||||
|
||||
```nix
|
||||
{
|
||||
shb.homepage.servicesGroups.Admin.services.Authelia = {
|
||||
sortOrder = 2;
|
||||
dashboard.request = config.shb.authelia.dashboard.request;
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
## SHB OIDC integration {#blocks-authelia-shb-oidc}
|
||||
|
||||
For services [provided by SelfHostBlocks][services] that handle [OIDC integration][OIDC],
|
||||
|
|
|
|||
|
|
@ -333,6 +333,20 @@ in
|
|||
type = types.bool;
|
||||
default = true;
|
||||
};
|
||||
|
||||
dashboard = lib.mkOption {
|
||||
description = ''
|
||||
Dashboard contract consumer
|
||||
'';
|
||||
default = { };
|
||||
type = lib.types.submodule {
|
||||
options = shb.contracts.dashboard.mkRequester {
|
||||
externalUrl = "https://${cfg.subdomain}.${cfg.domain}";
|
||||
externalUrlText = "https://\${config.shb.lldap.subdomain}.\${config.shb.lldap.domain}";
|
||||
internalUrl = "http://127.0.0.1:${toString cfg.webUIListenPort}";
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
config = lib.mkIf cfg.enable {
|
||||
|
|
|
|||
|
|
@ -7,7 +7,13 @@ across services.
|
|||
|
||||
[LLDAP]: https://github.com/lldap/lldap
|
||||
|
||||
## Global Setup {#blocks-lldap-global-setup}
|
||||
## Features {#blocks-lldap-features}
|
||||
|
||||
- Integration with the [dashboard contract](contracts-dashboard.html) for displaying user facing application in a dashboard. [Manual](#blocks-lldap-usage-applicationdashboard)
|
||||
|
||||
## Usage {#blocks-lldap-usage}
|
||||
|
||||
### Initial Configuration {#blocks-lldap-usage-configuration}
|
||||
|
||||
```nix
|
||||
shb.lldap = {
|
||||
|
|
@ -29,7 +35,7 @@ shb.sops.secret."lldap/user_password".request = config.shb.lldap.ldapUserPasswor
|
|||
This assumes secrets are setup with SOPS
|
||||
as mentioned in [the secrets setup section](usage.html#usage-secrets) of the manual.
|
||||
|
||||
## SSL {#blocks-lldap-ssl}
|
||||
### SSL {#blocks-lldap-usage-ssl}
|
||||
|
||||
Using SSL is an important security practice, like always.
|
||||
Using the [SSL block][], the configuration to add to the one above is:
|
||||
|
|
@ -44,7 +50,7 @@ shb.certs.certs.letsencrypt.${domain}.extraDomains = [
|
|||
shb.lldap.ssl = config.shb.certs.certs.letsencrypt.${config.shb.lldap.domain};
|
||||
```
|
||||
|
||||
## Restrict Access By IP {#blocks-lldap-restrict-access-by-ip}
|
||||
### Restrict Access By IP {#blocks-lldap-usage-restrict-access-by-ip}
|
||||
|
||||
For added security, you can restrict access to the LLDAP UI
|
||||
by adding the following line:
|
||||
|
|
@ -53,6 +59,22 @@ by adding the following line:
|
|||
shb.lldap.restrictAccessIPRange = "192.168.50.0/24";
|
||||
```
|
||||
|
||||
### Application Dashboard {#blocks-lldap-usage-applicationdashboard}
|
||||
|
||||
Integration with the [dashboard contract](contracts-dashboard.html) is provided
|
||||
by the [dashboard option](#blocks-lldap-options-shb.lldap.dashboard).
|
||||
|
||||
For example using the [Homepage](services-homepage.html) service:
|
||||
|
||||
```nix
|
||||
{
|
||||
shb.homepage.servicesGroups.Admin.services.LLDAP = {
|
||||
sortOrder = 2;
|
||||
dashboard.request = config.shb.lldap.dashboard.request;
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
## Manage Groups {#blocks-lldap-manage-groups}
|
||||
|
||||
The following snippet will create group named "family" if it does not exist yet.
|
||||
|
|
|
|||
|
|
@ -247,6 +247,21 @@ in
|
|||
};
|
||||
};
|
||||
};
|
||||
|
||||
dashboard = lib.mkOption {
|
||||
description = ''
|
||||
Dashboard contract consumer
|
||||
'';
|
||||
default = { };
|
||||
type = lib.types.submodule {
|
||||
options = shb.contracts.dashboard.mkRequester {
|
||||
externalUrl = "https://${cfg.subdomain}.${cfg.domain}";
|
||||
externalUrlText = "https://\${config.shb.monitoring.subdomain}.\${config.shb.monitoring.domain}";
|
||||
internalUrl = "https://${cfg.subdomain}.${cfg.domain}";
|
||||
internalUrlText = "https://\${config.shb.monitoring.subdomain}.\${config.shb.monitoring.domain}";
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
config = lib.mkMerge [
|
||||
|
|
|
|||
|
|
@ -17,9 +17,12 @@ This block sets up the monitoring stack for Self Host Blocks. It is composed of:
|
|||
- Registration is enabled through SSO.
|
||||
- Access through [subdomain](#blocks-monitoring-options-shb.monitoring.subdomain) using reverse proxy.
|
||||
- Access through [HTTPS](#blocks-monitoring-options-shb.monitoring.ssl) using reverse proxy.
|
||||
- Integration with the [dashboard contract](contracts-dashboard.html) for displaying user facing application in a dashboard. [Manual](#blocks-monitoring-usage-applicationdashboard)
|
||||
|
||||
## Usage {#blocks-monitoring-usage}
|
||||
|
||||
### Initial Configuration {#blocks-monitoring-usage-configuration}
|
||||
|
||||
The following snippet assumes a few blocks have been setup already:
|
||||
|
||||
- the [secrets block](usage.html#usage-secrets) with SOPS,
|
||||
|
|
@ -110,6 +113,22 @@ You might for example want to update the metrics retention time with:
|
|||
services.prometheus.retentionTime = "60d";
|
||||
```
|
||||
|
||||
### Application Dashboard {#blocks-monitoring-usage-applicationdashboard}
|
||||
|
||||
Integration with the [dashboard contract](contracts-dashboard.html) is provided
|
||||
by the [dashboard option](#blocks-monitoring-options-shb.monitoring.dashboard).
|
||||
|
||||
For example using the [Homepage](services-homepage.html) service:
|
||||
|
||||
```nix
|
||||
{
|
||||
shb.homepage.servicesGroups.Admin.services.Grafana = {
|
||||
sortOrder = 10;
|
||||
dashboard.request = config.shb.monitoring.dashboard.request;
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
## Provisioning {#blocks-monitoring-provisioning}
|
||||
|
||||
Self Host Blocks will create automatically the following resources:
|
||||
|
|
|
|||
|
|
@ -33,7 +33,9 @@ Here is an example module defining such a `backup` option:
|
|||
{
|
||||
options = {
|
||||
myservice.backup = mkOption {
|
||||
type = contracts.backup.request;
|
||||
type = lib.types.submodule {
|
||||
options = contracts.backup.request;
|
||||
};
|
||||
default = {
|
||||
user = "myservice";
|
||||
sourceDirectories = [
|
||||
|
|
|
|||
77
modules/contracts/dashboard.nix
Normal file
77
modules/contracts/dashboard.nix
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
{ lib, ... }:
|
||||
let
|
||||
inherit (lib) mkOption;
|
||||
inherit (lib.types)
|
||||
nullOr
|
||||
submodule
|
||||
str
|
||||
;
|
||||
in
|
||||
{
|
||||
mkRequest =
|
||||
{
|
||||
serviceName ? "",
|
||||
externalUrl ? "",
|
||||
externalUrlText ? null,
|
||||
internalUrl ? null,
|
||||
internalUrlText ? null,
|
||||
apiKey ? null,
|
||||
}:
|
||||
mkOption {
|
||||
description = ''
|
||||
Request part of the dashboard contract.
|
||||
'';
|
||||
default = { };
|
||||
type = submodule {
|
||||
options = {
|
||||
externalUrl =
|
||||
mkOption {
|
||||
description = ''
|
||||
URL at which the service can be accessed.
|
||||
|
||||
This URL should go through the reverse proxy.
|
||||
'';
|
||||
type = str;
|
||||
default = externalUrl;
|
||||
example = "https://jellyfin.example.com";
|
||||
}
|
||||
// (lib.optionalAttrs (externalUrlText != null) {
|
||||
defaultText = externalUrlText;
|
||||
});
|
||||
|
||||
internalUrl =
|
||||
mkOption {
|
||||
description = ''
|
||||
URL at which the service can be accessed directly.
|
||||
|
||||
This URL should bypass the reverse proxy.
|
||||
It can be used for example to ping the service
|
||||
and making sure it is up and running correctly.
|
||||
'';
|
||||
type = nullOr str;
|
||||
default = internalUrl;
|
||||
example = "http://127.0.0.1:8081";
|
||||
}
|
||||
// (lib.optionalAttrs (internalUrlText != null) {
|
||||
defaultText = internalUrlText;
|
||||
});
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
mkResult =
|
||||
{
|
||||
}:
|
||||
mkOption {
|
||||
description = ''
|
||||
Result part of the dashboard contract.
|
||||
|
||||
No option is provided here.
|
||||
'';
|
||||
default = { };
|
||||
type = submodule {
|
||||
options = {
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
65
modules/contracts/dashboard/docs/default.md
Normal file
65
modules/contracts/dashboard/docs/default.md
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
# Dashboard Contract {#contract-dashboard}
|
||||
|
||||
This NixOS contract is used for user-facing services
|
||||
that want to be displayed on a dashboard.
|
||||
|
||||
It is a contract between a service that can be accessed through an URL
|
||||
and a service that wants to show a list of those services.
|
||||
|
||||
## Providers {#contract-dashboard-providers}
|
||||
|
||||
The providers of this contract in SHB are:
|
||||
|
||||
<!-- Somehow generate this list -->
|
||||
|
||||
- The homepage service under its [shb.homepage.servicesGroups.<name>.services.<name>.dashboard](#services-homepage-options-shb.homepage.servicesGroups._name_.services._name_.dashboard) option.
|
||||
|
||||
## Usage {#contracts-dashboard-usage}
|
||||
|
||||
A service that can be shown on a dashboard will provide a `dashboard` option.
|
||||
|
||||
Here is an example module defining such a requester option for this dashboard contract:
|
||||
|
||||
```nix
|
||||
{
|
||||
options = {
|
||||
myservice.dashboard = lib.mkOption {
|
||||
description = ''
|
||||
Dashboard contract consumer
|
||||
'';
|
||||
default = { };
|
||||
type = lib.types.submodule {
|
||||
options = shb.contracts.dashboard.mkRequester {
|
||||
externalUrl = "https://${config.myservice.subdomain}.${config.myservice.domain}";
|
||||
internalUrl = "http://127.0.0.1:${config.myservice.port}";
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
```
|
||||
|
||||
Then, plug both consumer and provider together in the `config`:
|
||||
|
||||
```nix
|
||||
{
|
||||
config = {
|
||||
<provider-module> = {
|
||||
dashboard.request = config.myservice.dashboard.request;
|
||||
};
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
And that's it for the contract part.
|
||||
For more specific details on each provider, go to their respective manual pages.
|
||||
|
||||
## Contract Reference {#contract-dashboard-options}
|
||||
|
||||
These are all the options that are expected to exist for this contract to be respected.
|
||||
|
||||
```{=include=} options
|
||||
id-prefix: contracts-dashboard-options-
|
||||
list-id: selfhostblocks-options
|
||||
source: @OPTIONS_JSON@
|
||||
```
|
||||
30
modules/contracts/dashboard/dummyModule.nix
Normal file
30
modules/contracts/dashboard/dummyModule.nix
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
{ lib, shb, ... }:
|
||||
let
|
||||
inherit (lib) mkOption;
|
||||
inherit (lib.types) submodule;
|
||||
in
|
||||
{
|
||||
imports = [
|
||||
../../../lib/module.nix
|
||||
];
|
||||
|
||||
options.shb.contracts.dashboard = mkOption {
|
||||
description = ''
|
||||
Contract for user-facing services that want to
|
||||
be displayed on a dashboard.
|
||||
|
||||
The requester communicates to the provider
|
||||
how to access the service
|
||||
through the `request` options.
|
||||
|
||||
The provider reads from the `request` options
|
||||
and configures what is necessary on its side
|
||||
to show the service and check its availability.
|
||||
It does not communicate back to the requester.
|
||||
'';
|
||||
|
||||
type = submodule {
|
||||
options = shb.contracts.dashboard.contract;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
|
@ -48,21 +48,28 @@ let
|
|||
importContract =
|
||||
module:
|
||||
let
|
||||
importedModule = pkgs.callPackage module { inherit shb; };
|
||||
importedModule = pkgs.callPackage module {
|
||||
shb = shb // {
|
||||
inherit contracts;
|
||||
};
|
||||
};
|
||||
in
|
||||
mkContractFunctions {
|
||||
inherit (importedModule) mkRequest mkResult;
|
||||
};
|
||||
in
|
||||
{
|
||||
databasebackup = importContract ./databasebackup.nix;
|
||||
backup = importContract ./backup.nix;
|
||||
mount = pkgs.callPackage ./mount.nix { };
|
||||
secret = importContract ./secret.nix;
|
||||
ssl = pkgs.callPackage ./ssl.nix { };
|
||||
test = {
|
||||
secret = pkgs.callPackage ./secret/test.nix { inherit shb; };
|
||||
databasebackup = pkgs.callPackage ./databasebackup/test.nix { inherit shb; };
|
||||
backup = pkgs.callPackage ./backup/test.nix { inherit shb; };
|
||||
|
||||
contracts = {
|
||||
databasebackup = importContract ./databasebackup.nix;
|
||||
dashboard = importContract ./dashboard.nix;
|
||||
backup = importContract ./backup.nix;
|
||||
mount = pkgs.callPackage ./mount.nix { };
|
||||
secret = importContract ./secret.nix;
|
||||
ssl = pkgs.callPackage ./ssl.nix { };
|
||||
test = {
|
||||
secret = pkgs.callPackage ./secret/test.nix { inherit shb; };
|
||||
databasebackup = pkgs.callPackage ./databasebackup/test.nix { inherit shb; };
|
||||
backup = pkgs.callPackage ./backup/test.nix { inherit shb; };
|
||||
};
|
||||
};
|
||||
}
|
||||
in
|
||||
contracts
|
||||
|
|
|
|||
|
|
@ -392,6 +392,20 @@ let
|
|||
};
|
||||
};
|
||||
};
|
||||
|
||||
dashboard = lib.mkOption {
|
||||
description = ''
|
||||
Dashboard contract consumer
|
||||
'';
|
||||
default = { };
|
||||
type = lib.types.submodule {
|
||||
options = shb.contracts.dashboard.mkRequester {
|
||||
externalUrl = "https://${cfg.${name}.subdomain}.${cfg.${name}.domain}";
|
||||
externalUrlText = "https://\${config.shb.arr.${name}.subdomain}.\${config.shb.arr.${name}.domain}";
|
||||
internalUrl = "http://127.0.0.1:${toString cfg.${name}.settings.Port}";
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
// (c.moreOptions or { });
|
||||
};
|
||||
|
|
|
|||
|
|
@ -3,12 +3,242 @@
|
|||
Defined in [`/modules/services/arr.nix`](@REPO@/modules/services/arr.nix).
|
||||
|
||||
This NixOS module sets up multiple [Servarr](https://wiki.servarr.com/) services.
|
||||
## Features {#services-arr-features}
|
||||
|
||||
Compared to the stock module from nixpkgs,
|
||||
this one sets up, in a fully declarative manner
|
||||
LDAP and SSO integration as well as the API key.
|
||||
|
||||
This manual page is under construction.
|
||||
## Usage {#services-arr-usage}
|
||||
|
||||
### Initial Configuration {#services-arr-usage-configuration}
|
||||
|
||||
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.certs.certs.letsencrypt.${domain}.extraDomains = [
|
||||
"moviesdl.${domain}"
|
||||
"seriesdl.${domain}"
|
||||
"subtitlesdl.${domain}"
|
||||
"booksdl.${domain}"
|
||||
"musicdl.${domain}"
|
||||
"indexer.${domain}"
|
||||
];
|
||||
|
||||
shb.arr = {
|
||||
radarr = {
|
||||
inherit domain;
|
||||
enable = true;
|
||||
ssl = config.shb.certs.certs.letsencrypt.${domain};
|
||||
authEndpoint = "https://${config.shb.authelia.subdomain}.${config.shb.authelia.domain}";
|
||||
};
|
||||
sonarr = {
|
||||
inherit domain;
|
||||
enable = true;
|
||||
ssl = config.shb.certs.certs.letsencrypt."${domain}";
|
||||
authEndpoint = "https://${config.shb.authelia.subdomain}.${config.shb.authelia.domain}";
|
||||
};
|
||||
bazarr = {
|
||||
inherit domain;
|
||||
enable = true;
|
||||
ssl = config.shb.certs.certs.letsencrypt."${domain}";
|
||||
authEndpoint = "https://${config.shb.authelia.subdomain}.${config.shb.authelia.domain}";
|
||||
};
|
||||
readarr = {
|
||||
inherit domain;
|
||||
enable = true;
|
||||
ssl = config.shb.certs.certs.letsencrypt."${domain}";
|
||||
authEndpoint = "https://${config.shb.authelia.subdomain}.${config.shb.authelia.domain}";
|
||||
};
|
||||
lidarr = {
|
||||
inherit domain;
|
||||
enable = true;
|
||||
ssl = config.shb.certs.certs.letsencrypt."${domain}";
|
||||
authEndpoint = "https://${config.shb.authelia.subdomain}.${config.shb.authelia.domain}";
|
||||
};
|
||||
jackett = {
|
||||
inherit domain;
|
||||
enable = true;
|
||||
ssl = config.shb.certs.certs.letsencrypt."${domain}";
|
||||
authEndpoint = "https://${config.shb.authelia.subdomain}.${config.shb.authelia.domain}";
|
||||
};
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
The user and admin LDAP groups are created automatically.
|
||||
|
||||
### API Keys {#services-arr-usage-apikeys}
|
||||
|
||||
The API keys for each arr service can be created declaratively.
|
||||
|
||||
First, generate one secret for each service with `nix run nixpkgs#openssl -- rand -hex 64`
|
||||
and store it in your secrets file (for example the SOPS file).
|
||||
|
||||
Then, add the API key to each service:
|
||||
|
||||
```nix
|
||||
{
|
||||
shb.arr = {
|
||||
radarr = {
|
||||
settings = {
|
||||
ApiKey.source = config.shb.sops.secret."radarr/apikey".result.path;
|
||||
};
|
||||
};
|
||||
sonarr = {
|
||||
settings = {
|
||||
ApiKey.source = config.shb.sops.secret."sonarr/apikey".result.path;
|
||||
};
|
||||
};
|
||||
bazarr = {
|
||||
settings = {
|
||||
ApiKey.source = config.shb.sops.secret."bazarr/apikey".result.path;
|
||||
};
|
||||
};
|
||||
readarr = {
|
||||
settings = {
|
||||
ApiKey.source = config.shb.sops.secret."readarr/apikey".result.path;
|
||||
};
|
||||
};
|
||||
lidarr = {
|
||||
settings = {
|
||||
ApiKey.source = config.shb.sops.secret."lidarr/apikey".result.path;
|
||||
};
|
||||
};
|
||||
jackett = {
|
||||
settings = {
|
||||
ApiKey.source = config.shb.sops.secret."jackett/apikey".result.path;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
shb.sops.secret."radarr/apikey".request = {
|
||||
mode = "0440";
|
||||
owner = "radarr";
|
||||
group = "radarr";
|
||||
restartUnits = [ "radarr.service" ];
|
||||
};
|
||||
shb.sops.secret."sonarr/apikey".request = {
|
||||
mode = "0440";
|
||||
owner = "sonarr";
|
||||
group = "sonarr";
|
||||
restartUnits = [ "sonarr.service" ];
|
||||
};
|
||||
shb.sops.secret."bazarr/apikey".request = {
|
||||
mode = "0440";
|
||||
owner = "bazarr";
|
||||
group = "bazarr";
|
||||
restartUnits = [ "bazarr.service" ];
|
||||
};
|
||||
shb.sops.secret."readarr/apikey".request = {
|
||||
mode = "0440";
|
||||
owner = "readarr";
|
||||
group = "readarr";
|
||||
restartUnits = [ "readarr.service" ];
|
||||
};
|
||||
shb.sops.secret."lidarr/apikey".request = {
|
||||
mode = "0440";
|
||||
owner = "lidarr";
|
||||
group = "lidarr";
|
||||
restartUnits = [ "lidarr.service" ];
|
||||
};
|
||||
shb.sops.secret."jackett/apikey".request = {
|
||||
mode = "0440";
|
||||
owner = "jackett";
|
||||
group = "jackett";
|
||||
restartUnits = [ "jackett.service" ];
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
### Application Dashboard {#services-arr-usage-applicationdashboard}
|
||||
|
||||
Integration with the [dashboard contract](contracts-dashboard.html) is provided
|
||||
by the various dashboard options.
|
||||
|
||||
For example using the [Homepage](services-homepage.html) service:
|
||||
|
||||
```nix
|
||||
{
|
||||
shb.homepage.servicesGroups.Media.services.Radarr = {
|
||||
sortOrder = 10;
|
||||
dashboard.request = config.shb.arr.radarr.dashboard.request;
|
||||
apiKey.result = config.shb.sops.secret."radarr/homepageApiKey".result;
|
||||
};
|
||||
shb.sops.secret."radarr/homepageApiKey" = {
|
||||
settings.key = "radarr/apikey";
|
||||
request = config.shb.homepage.servicesGroups.Media.services.Radarr.apiKey.request;
|
||||
};
|
||||
shb.homepage.servicesGroups.Media.services.Sonarr = {
|
||||
sortOrder = 11;
|
||||
dashboard.request = config.shb.arr.sonarr.dashboard.request;
|
||||
apiKey.result = config.shb.sops.secret."sonarr/homepageApiKey".result;
|
||||
};
|
||||
shb.sops.secret."sonarr/homepageApiKey" = {
|
||||
settings.key = "sonarr/apikey";
|
||||
request = config.shb.homepage.servicesGroups.Media.services.Sonarr.apiKey.request;
|
||||
};
|
||||
shb.homepage.servicesGroups.Media.services.Bazarr = {
|
||||
sortOrder = 12;
|
||||
dashboard.request = config.shb.arr.bazarr.dashboard.request;
|
||||
apiKey.result = config.shb.sops.secret."bazarr/homepageApiKey".result;
|
||||
};
|
||||
shb.sops.secret."bazarr/homepageApiKey" = {
|
||||
settings.key = "bazarr/apikey";
|
||||
request = config.shb.homepage.servicesGroups.Media.services.Bazarr.apiKey.request;
|
||||
};
|
||||
shb.homepage.servicesGroups.Media.services.Readarr = {
|
||||
sortOrder = 13;
|
||||
dashboard.request = config.shb.arr.readarr.dashboard.request;
|
||||
apiKey.result = config.shb.sops.secret."readarr/homepageApiKey".result;
|
||||
};
|
||||
shb.sops.secret."readarr/homepageApiKey" = {
|
||||
settings.key = "readarr/apikey";
|
||||
request = config.shb.homepage.servicesGroups.Media.services.Readarr.apiKey.request;
|
||||
};
|
||||
shb.homepage.servicesGroups.Media.services.Lidarr = {
|
||||
sortOrder = 14;
|
||||
dashboard.request = config.shb.arr.lidarr.dashboard.request;
|
||||
apiKey.result = config.shb.sops.secret."lidarr/homepageApiKey".result;
|
||||
};
|
||||
shb.sops.secret."lidarr/homepageApiKey" = {
|
||||
settings.key = "lidarr/apikey";
|
||||
request = config.shb.homepage.servicesGroups.Media.services.Lidarr.apiKey.request;
|
||||
};
|
||||
shb.homepage.servicesGroups.Media.services.Jackett = {
|
||||
sortOrder = 15;
|
||||
dashboard.request = config.shb.arr.jackett.dashboard.request;
|
||||
apiKey.result = config.shb.sops.secret."jackett/homepageApiKey".result;
|
||||
};
|
||||
shb.sops.secret."jackett/homepageApiKey" = {
|
||||
settings.key = "jackett/apikey";
|
||||
request = config.shb.homepage.servicesGroups.Media.services.Jackett.apiKey.request;
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
This example reuses the API keys generated declaratively from the previous section.
|
||||
|
||||
### Jackett Proxy {#services-arr-usage-jackett-proxy}
|
||||
|
||||
The Jackett service can be made to use a proxy with:
|
||||
|
||||
```nix
|
||||
{
|
||||
shb.arr.jackett = {
|
||||
settings = {
|
||||
ProxyType = "0";
|
||||
ProxyUrl = "127.0.0.1:1234";
|
||||
};
|
||||
};
|
||||
};
|
||||
```
|
||||
|
||||
## Options Reference {#services-arr-options}
|
||||
|
||||
|
|
|
|||
|
|
@ -152,6 +152,20 @@ in
|
|||
default = false;
|
||||
example = true;
|
||||
};
|
||||
|
||||
dashboard = lib.mkOption {
|
||||
description = ''
|
||||
Dashboard contract consumer
|
||||
'';
|
||||
default = { };
|
||||
type = lib.types.submodule {
|
||||
options = shb.contracts.dashboard.mkRequester {
|
||||
externalUrl = "https://${cfg.subdomain}.${cfg.domain}";
|
||||
externalUrlText = "https://\${config.shb.audiobookshelf.subdomain}.\${config.shb.audiobookshelf.domain}";
|
||||
internalUrl = "http://127.0.0.1:${toString cfg.webPort}";
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
config = lib.mkIf cfg.enable (
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ in
|
|||
options.shb.deluge = {
|
||||
enable = lib.mkEnableOption "the SHB Deluge service";
|
||||
|
||||
enableDashboard = lib.mkEnableOption "the Torrents SHB dashboard" // {
|
||||
enableDashboard = lib.mkEnableOption "the Torrents SHB monitoring dashboard" // {
|
||||
default = true;
|
||||
};
|
||||
|
||||
|
|
@ -301,6 +301,20 @@ in
|
|||
default = null;
|
||||
example = "info";
|
||||
};
|
||||
|
||||
dashboard = lib.mkOption {
|
||||
description = ''
|
||||
Dashboard contract consumer
|
||||
'';
|
||||
default = { };
|
||||
type = lib.types.submodule {
|
||||
options = shb.contracts.dashboard.mkRequester {
|
||||
externalUrl = "https://${fqdn}";
|
||||
externalUrlText = "https://\${config.shb.deluge.subdomain}.\${config.shb.deluge.domain}";
|
||||
internalUrl = "http://127.0.0.1:${toString cfg.webPort}";
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
config = lib.mkIf cfg.enable (
|
||||
|
|
|
|||
|
|
@ -10,6 +10,9 @@ let
|
|||
in
|
||||
{
|
||||
imports = [
|
||||
../blocks/nginx.nix
|
||||
../blocks/lldap.nix
|
||||
|
||||
../../lib/module.nix
|
||||
];
|
||||
|
||||
|
|
@ -293,6 +296,22 @@ in
|
|||
};
|
||||
};
|
||||
};
|
||||
|
||||
dashboard = lib.mkOption {
|
||||
description = ''
|
||||
Dashboard contract consumer
|
||||
'';
|
||||
default = { };
|
||||
type = lib.types.submodule {
|
||||
options = shb.contracts.dashboard.mkRequester {
|
||||
externalUrl = "https://${cfg.subdomain}.${cfg.domain}";
|
||||
externalUrlText = "https://\${config.shb.firefly-iii.subdomain}.\${config.shb.firefly-iii.domain}";
|
||||
# This works thanks to the Personal Access Token.
|
||||
internalUrl = "https://${cfg.subdomain}.${cfg.domain}";
|
||||
internalUrlText = "https://\${config.shb.firefly-iii.subdomain}.\${config.shb.firefly-iii.domain}";
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
config = lib.mkIf cfg.enable (
|
||||
|
|
|
|||
|
|
@ -12,8 +12,14 @@ It also sets up the Firefly-iii data importer service
|
|||
and nearly automatically links it to the Firefly-iii instance using a Personal Account Token.
|
||||
Instructions on how to do so is given in the next section.
|
||||
|
||||
## Features {#services-firefly-iii-features}
|
||||
|
||||
- Integration with the [dashboard contract](contracts-dashboard.html) for displaying user facing application in a dashboard. [Manual](#services-firefly-iii-usage-applicationdashboard)
|
||||
|
||||
## Usage {#services-firefly-iii-usage}
|
||||
|
||||
### Initial Configuration {#services-firefly-iii-usage-configuration}
|
||||
|
||||
The following snippet assumes a few blocks have been setup already:
|
||||
|
||||
- the [secrets block](usage.html#usage-secrets) with SOPS,
|
||||
|
|
@ -127,6 +133,38 @@ shb.lldap.ensureUsers.USERNAME.groups = [
|
|||
];
|
||||
```
|
||||
|
||||
### Application Dashboard {#services-firefly-iii-usage-applicationdashboard}
|
||||
|
||||
Integration with the [dashboard contract](contracts-dashboard.html) is provided
|
||||
by the [dashboard option](#services-firefly-iii-options-shb.firefly-iii.dashboard).
|
||||
|
||||
For example using the [Homepage](services-homepage.html) service:
|
||||
|
||||
```nix
|
||||
{
|
||||
shb.homepage.servicesGroups.Finance.services.Firefly-iii = {
|
||||
sortOrder = 1;
|
||||
dashboard.request = config.shb.firefly-iii.dashboard.request;
|
||||
settings.widget.type = "firefly";
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
The widget type needs to be set manually otherwise it is not displayed correctly.
|
||||
|
||||
An API key can be set to show extra info:
|
||||
|
||||
```nix
|
||||
{
|
||||
shb.homepage.servicesGroups.Finance.services.Firefly-iii = {
|
||||
apiKey.result = config.shb.sops.secret."firefly-iii/homepageApiKey".result;
|
||||
};
|
||||
|
||||
shb.sops.secret."firefly-iii/homepageApiKey".request =
|
||||
config.shb.homepage.servicesGroups.Finance.services.Firefly-iii.apiKey.request;
|
||||
}
|
||||
```
|
||||
|
||||
## Database Inspection {#services-firefly-iii-database-inspection}
|
||||
|
||||
Access the database with:
|
||||
|
|
|
|||
|
|
@ -408,6 +408,21 @@ in
|
|||
type = bool;
|
||||
default = false;
|
||||
};
|
||||
|
||||
dashboard = lib.mkOption {
|
||||
description = ''
|
||||
Dashboard contract consumer
|
||||
'';
|
||||
default = { };
|
||||
type = lib.types.submodule {
|
||||
options = shb.contracts.dashboard.mkRequester {
|
||||
externalUrl = "https://${cfg.subdomain}.${cfg.domain}";
|
||||
externalUrlText = "https://\${config.shb.forgejo.subdomain}.\${config.shb.forgejo.domain}";
|
||||
internalUrl = "https://${cfg.subdomain}.${cfg.domain}";
|
||||
internalUrlText = "https://\${config.shb.forgejo.subdomain}.\${config.shb.forgejo.domain}";
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
config = mkMerge [
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ LDAP and SSO integration as well as one local runner.
|
|||
- Access through [subdomain](#services-forgejo-options-shb.forgejo.subdomain) using reverse proxy. [Manual](#services-forgejo-usage-configuration).
|
||||
- Access through [HTTPS](#services-forgejo-options-shb.forgejo.ssl) using reverse proxy. [Manual](#services-forgejo-usage-configuration).
|
||||
- [Backup](#services-forgejo-options-shb.forgejo.sso) through the [backup block](./blocks-backup.html). [Manual](#services-forgejo-usage-backup).
|
||||
- Integration with the [dashboard contract](contracts-dashboard.html) for displaying user facing application in a dashboard. [Manual](#services-forgejo-usage-applicationdashboard)
|
||||
|
||||
## Usage {#services-forgejo-usage}
|
||||
|
||||
|
|
@ -206,6 +207,22 @@ The name `"forgejo"` in the `instances` can be anything.
|
|||
The `config.shb.forgejo.backup` option provides what directories to backup.
|
||||
You can define any number of Restic instances to backup Forgejo multiple times.
|
||||
|
||||
### Application Dashboard {#services-forgejo-usage-applicationdashboard}
|
||||
|
||||
Integration with the [dashboard contract](contracts-dashboard.html) is provided
|
||||
by the [dashboard option](#services-forgejo-options-shb.forgejo.dashboard).
|
||||
|
||||
For example using the [Homepage](services-homepage.html) service:
|
||||
|
||||
```nix
|
||||
{
|
||||
shb.homepage.servicesGroups.Admin.services.Forgejo = {
|
||||
sortOrder = 1;
|
||||
dashboard.request = config.shb.forgejo.dashboard.request;
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
### Extra Settings {#services-forgejo-usage-extra-settings}
|
||||
|
||||
Other Forgejo settings can be accessed through the nixpkgs [stock service][].
|
||||
|
|
|
|||
|
|
@ -111,6 +111,21 @@ in
|
|||
default = false;
|
||||
example = true;
|
||||
};
|
||||
|
||||
dashboard = lib.mkOption {
|
||||
description = ''
|
||||
Dashboard contract consumer
|
||||
'';
|
||||
default = { };
|
||||
type = lib.types.submodule {
|
||||
options = shb.contracts.dashboard.mkRequester {
|
||||
externalUrl = "https://${cfg.subdomain}.${cfg.domain}";
|
||||
externalUrlText = "https://\${config.shb.grocy.subdomain}.\${config.shb.grocy.domain}";
|
||||
internalUrl = "https://${cfg.subdomain}.${cfg.domain}";
|
||||
internalUrlText = "https://\${config.shb.grocy.subdomain}.\${config.shb.grocy.domain}";
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
config = lib.mkIf cfg.enable (
|
||||
|
|
|
|||
|
|
@ -82,6 +82,21 @@ in
|
|||
default = [ "--forecast" ];
|
||||
type = lib.types.listOf lib.types.str;
|
||||
};
|
||||
|
||||
dashboard = lib.mkOption {
|
||||
description = ''
|
||||
Dashboard contract consumer
|
||||
'';
|
||||
default = { };
|
||||
type = lib.types.submodule {
|
||||
options = shb.contracts.dashboard.mkRequester {
|
||||
externalUrl = "https://${cfg.subdomain}.${cfg.domain}";
|
||||
externalUrlText = "https://\${config.shb.hledger.subdomain}.\${config.shb.hledger.domain}";
|
||||
internalUrl = "http://127.0.0.1:${toString config.services.hledger-web.port}";
|
||||
internalUrlText = "http://127.0.0.1:\${config.services.hledger-web.port}";
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
config = lib.mkIf cfg.enable {
|
||||
|
|
|
|||
|
|
@ -218,6 +218,21 @@ in
|
|||
};
|
||||
};
|
||||
};
|
||||
|
||||
dashboard = lib.mkOption {
|
||||
description = ''
|
||||
Dashboard contract consumer
|
||||
'';
|
||||
default = { };
|
||||
type = lib.types.submodule {
|
||||
options = shb.contracts.dashboard.mkRequester {
|
||||
externalUrl = "https://${cfg.subdomain}.${cfg.domain}";
|
||||
externalUrlText = "https://\${config.shb.home-assistant.subdomain}.\${config.shb.home-assistant.domain}";
|
||||
internalUrl = "http://127.0.0.1:${toString config.services.home-assistant.config.http.server_port}";
|
||||
internalUrlText = "http://127.0.0.1:\${config.services.home-assistant.config.http.server_port}";
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
config = lib.mkIf cfg.enable {
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ LDAP and SSO integration.
|
|||
- Access through [subdomain](#services-home-assistant-options-shb.home-assistant.subdomain) using reverse proxy. [Manual](#services-home-assistant-usage-configuration).
|
||||
- Access through [HTTPS](#services-home-assistant-options-shb.home-assistant.ssl) using reverse proxy. [Manual](#services-home-assistant-usage-configuration).
|
||||
- [Backup](#services-home-assistant-options-shb.home-assistant.backup) through the [backup block](./blocks-backup.html). [Manual](#services-home-assistant-usage-backup).
|
||||
- Integration with the [dashboard contract](contracts-dashboard.html) for displaying user facing application in a dashboard. [Manual](#services-home-assistant-usage-applicationdashboard)
|
||||
|
||||
- Not yet: declarative SSO.
|
||||
|
||||
|
|
@ -163,6 +164,57 @@ You can define any number of Restic instances to backup Home-Assistant multiple
|
|||
You will then need to configure more options like the `repository`,
|
||||
as explained in the [restic](blocks-restic.html) documentation.
|
||||
|
||||
### Application Dashboard {#services-home-assistant-usage-applicationdashboard}
|
||||
|
||||
Integration with the [dashboard contract](contracts-dashboard.html) is provided
|
||||
by the [dashboard option](#services-home-assistant-options-shb.home-assistant.dashboard).
|
||||
|
||||
For example using the [Homepage](services-homepage.html) service:
|
||||
|
||||
```nix
|
||||
{
|
||||
shb.homepage.servicesGroups.Home.services.HomeAssistant = {
|
||||
sortOrder = 1;
|
||||
dashboard.request = config.shb.home-assistant.dashboard.request;
|
||||
settings.icon = "si-homeassistant";
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
The icon needs to be set manually otherwise it is not displayed correctly.
|
||||
|
||||
An API key can be set to show extra info:
|
||||
|
||||
```nix
|
||||
{
|
||||
shb.homepage.servicesGroups.Home.services.HomeAssistant = {
|
||||
apiKey.result = config.shb.sops.secret."home-assistant/homepageApiKey".result;
|
||||
};
|
||||
|
||||
shb.sops.secret."home-assistant/homepageApiKey".request =
|
||||
config.shb.homepage.servicesGroups.Home.services.HomeAssistant.apiKey.request;
|
||||
}
|
||||
```
|
||||
|
||||
Custom widgets can be set using Home Assistant templating:
|
||||
|
||||
```nix
|
||||
{
|
||||
shb.homepage.servicesGroups.Home.services.HomeAssistant = {
|
||||
settings.widget.custom = [
|
||||
{
|
||||
template = "{{ states('sensor.power_consumption_power_consumption', with_unit=True, rounded=True) }}";
|
||||
label = "energy now";
|
||||
}
|
||||
{
|
||||
state = "sensor.power_consumption_daily_power_consumption";
|
||||
label = "energy today";
|
||||
}
|
||||
];
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
### Extra Components {#services-home-assistant-usage-extra-components}
|
||||
|
||||
Packaged components can be found in the documentation of the corresponding option
|
||||
|
|
|
|||
286
modules/services/homepage.nix
Normal file
286
modules/services/homepage.nix
Normal file
|
|
@ -0,0 +1,286 @@
|
|||
{
|
||||
config,
|
||||
lib,
|
||||
shb,
|
||||
...
|
||||
}:
|
||||
let
|
||||
cfg = config.shb.homepage;
|
||||
|
||||
inherit (lib) types;
|
||||
in
|
||||
{
|
||||
imports = [
|
||||
../../lib/module.nix
|
||||
|
||||
../blocks/lldap.nix
|
||||
../blocks/nginx.nix
|
||||
];
|
||||
|
||||
options.shb.homepage = {
|
||||
enable = lib.mkEnableOption "the SHB homepage service";
|
||||
|
||||
subdomain = lib.mkOption {
|
||||
type = types.str;
|
||||
description = ''
|
||||
Subdomain under which homepage will be served.
|
||||
|
||||
```
|
||||
<subdomain>.<domain>
|
||||
```
|
||||
'';
|
||||
example = "homepage";
|
||||
};
|
||||
|
||||
domain = lib.mkOption {
|
||||
description = ''
|
||||
Domain under which homepage is served.
|
||||
|
||||
```
|
||||
<subdomain>.<domain>
|
||||
```
|
||||
'';
|
||||
type = types.str;
|
||||
example = "domain.com";
|
||||
};
|
||||
|
||||
ssl = lib.mkOption {
|
||||
description = "Path to SSL files";
|
||||
type = types.nullOr shb.contracts.ssl.certs;
|
||||
default = null;
|
||||
};
|
||||
|
||||
servicesGroups = lib.mkOption {
|
||||
description = "Group of services that should be showed on the dashboard.";
|
||||
default = { };
|
||||
type = types.attrsOf (
|
||||
types.submodule (
|
||||
{ name, ... }:
|
||||
{
|
||||
options = {
|
||||
name = lib.mkOption {
|
||||
type = types.str;
|
||||
description = "Display name of the group. Defaults to the attr name.";
|
||||
default = name;
|
||||
};
|
||||
sortOrder = lib.mkOption {
|
||||
description = ''
|
||||
Order in which groups will be shown.
|
||||
|
||||
The rules are:
|
||||
|
||||
- Lowest number is shown first.
|
||||
- Two groups having the same number are shown in a consistent (same across multiple deploys) but undefined order.
|
||||
- Default is null which means at the end.
|
||||
'';
|
||||
type = types.nullOr types.int;
|
||||
default = null;
|
||||
};
|
||||
services = lib.mkOption {
|
||||
description = "Services that should be showed in the group on the dashboard.";
|
||||
default = { };
|
||||
type = types.attrsOf (
|
||||
types.submodule (
|
||||
{ name, ... }:
|
||||
{
|
||||
options = {
|
||||
name = lib.mkOption {
|
||||
type = types.str;
|
||||
description = "Display name of the service. Defaults to the attr name.";
|
||||
default = name;
|
||||
};
|
||||
sortOrder = lib.mkOption {
|
||||
type = types.nullOr types.int;
|
||||
description = ''
|
||||
Order in which groups will be shown.
|
||||
|
||||
The rules are:
|
||||
|
||||
- Lowest number is shown first.
|
||||
- Two groups having the same number are shown in a consistent (same across multiple deploys) but undefined order.
|
||||
- Default is null which means at the end.
|
||||
'';
|
||||
default = null;
|
||||
};
|
||||
dashboard = lib.mkOption {
|
||||
description = ''
|
||||
Provider of the dashboard contract.
|
||||
|
||||
By default:
|
||||
|
||||
- The `serviceName` option comes from the attr name.
|
||||
- The `icon` option comes from applying `toLower` on the attr name.
|
||||
- The `siteMonitor` option is set only if `internalUrl` is set.
|
||||
'';
|
||||
type = types.submodule {
|
||||
options = shb.contracts.dashboard.mkProvider {
|
||||
resultCfg = { };
|
||||
};
|
||||
};
|
||||
};
|
||||
apiKey = lib.mkOption {
|
||||
description = ''
|
||||
API key used to access the service.
|
||||
|
||||
This can be used to get data from the service.
|
||||
'';
|
||||
default = null;
|
||||
type = types.nullOr (
|
||||
lib.types.submodule {
|
||||
options = shb.contracts.secret.mkRequester {
|
||||
owner = "root";
|
||||
restartUnits = [ "homepage-dashboard.service" ];
|
||||
};
|
||||
}
|
||||
);
|
||||
};
|
||||
settings = lib.mkOption {
|
||||
description = ''
|
||||
Extra options to pass to the homepage service.
|
||||
|
||||
Check https://gethomepage.dev/configs/services/#icons
|
||||
if the default icon is not correct.
|
||||
|
||||
And check https://gethomepage.dev/widgets
|
||||
if the default widget type is not correct.
|
||||
'';
|
||||
default = { };
|
||||
type = types.attrsOf types.anything;
|
||||
example = lib.literalExpression ''
|
||||
{
|
||||
icon = "si-homeassistant";
|
||||
widget.type = "firefly";
|
||||
widget.custom = [
|
||||
{
|
||||
template = "{{ states('sensor.total_power', with_unit=True, rounded=True) }}";
|
||||
label = "energy now";
|
||||
}
|
||||
{
|
||||
state = "sensor.total_power_today";
|
||||
label = "energy today";
|
||||
}
|
||||
];
|
||||
}
|
||||
'';
|
||||
};
|
||||
};
|
||||
}
|
||||
)
|
||||
);
|
||||
};
|
||||
};
|
||||
}
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
ldap = lib.mkOption {
|
||||
description = ''
|
||||
Setup LDAP integration.
|
||||
'';
|
||||
default = { };
|
||||
type = types.submodule {
|
||||
options = {
|
||||
userGroup = lib.mkOption {
|
||||
type = types.str;
|
||||
description = "Group users must belong to be able to login.";
|
||||
default = "homepage_user";
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
sso = lib.mkOption {
|
||||
description = ''
|
||||
Setup SSO integration.
|
||||
'';
|
||||
default = { };
|
||||
type = types.submodule {
|
||||
options = {
|
||||
enable = lib.mkEnableOption "SSO integration.";
|
||||
|
||||
authEndpoint = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
description = "Endpoint to the SSO provider.";
|
||||
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";
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
config = lib.mkIf cfg.enable {
|
||||
services.homepage-dashboard = {
|
||||
enable = true;
|
||||
|
||||
allowedHosts = "${cfg.subdomain}.${cfg.domain}";
|
||||
|
||||
settings = {
|
||||
baseUrl = "https://${cfg.subdomain}.${cfg.domain}";
|
||||
startUrl = "https://${cfg.subdomain}.${cfg.domain}";
|
||||
disableUpdateCheck = true;
|
||||
};
|
||||
|
||||
bookmarks = [ ];
|
||||
|
||||
services = shb.homepage.asServiceGroup cfg.servicesGroups;
|
||||
|
||||
widgets = [ ];
|
||||
};
|
||||
|
||||
systemd.services.homepage-dashboard.serviceConfig =
|
||||
let
|
||||
keys = shb.homepage.allKeys cfg.servicesGroups;
|
||||
in
|
||||
{
|
||||
# LoadCredential = [
|
||||
# "Media_Jellyfin:/path"
|
||||
# ];
|
||||
LoadCredential = lib.mapAttrsToList (name: path: "${name}:${path}") keys;
|
||||
# Environment = [
|
||||
# "HOMEPAGE_FILE_Media_Jellyfin=%d/Media_Jellyfin"
|
||||
# ];
|
||||
Environment = lib.mapAttrsToList (name: path: "HOMEPAGE_FILE_${name}=%d/${name}") keys;
|
||||
};
|
||||
|
||||
# This should be using a contract instead of setting the option directly.
|
||||
shb.lldap = lib.mkIf config.shb.lldap.enable {
|
||||
ensureGroups = {
|
||||
${cfg.ldap.userGroup} = { };
|
||||
};
|
||||
};
|
||||
|
||||
shb.nginx.vhosts = [
|
||||
(
|
||||
{
|
||||
inherit (cfg) subdomain domain ssl;
|
||||
|
||||
upstream = "http://127.0.0.1:${toString config.services.homepage-dashboard.listenPort}/";
|
||||
extraConfig = ''
|
||||
proxy_read_timeout 300s;
|
||||
proxy_send_timeout 300s;
|
||||
'';
|
||||
autheliaRules = lib.optionals (cfg.sso.enable) [
|
||||
{
|
||||
domain = "${cfg.subdomain}.${cfg.domain}";
|
||||
policy = cfg.sso.authorization_policy;
|
||||
subject = [ "group:${cfg.ldap.userGroup}" ];
|
||||
}
|
||||
];
|
||||
}
|
||||
// lib.optionalAttrs cfg.sso.enable {
|
||||
inherit (cfg.sso) authEndpoint;
|
||||
}
|
||||
)
|
||||
];
|
||||
};
|
||||
}
|
||||
BIN
modules/services/homepage/docs/Screenshot.png
Normal file
BIN
modules/services/homepage/docs/Screenshot.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 128 KiB |
191
modules/services/homepage/docs/default.md
Normal file
191
modules/services/homepage/docs/default.md
Normal file
|
|
@ -0,0 +1,191 @@
|
|||
# Homepage Service {#services-homepage}
|
||||
|
||||
Defined in [`/modules/services/homepage.nix`](@REPO@/modules/services/homepage.nix),
|
||||
found in the `selfhostblocks.nixosModules.homepage` module.
|
||||
See [the manual](usage.html#usage-flake) for how to import the module in your code.
|
||||
|
||||
This service sets up [Homepage Dashboard][] which provides
|
||||
a highly customizable homepage Docker and service API integrations.
|
||||
|
||||

|
||||
|
||||
[Homepage Dashboard]: https://github.com/gethomepage/homepage
|
||||
|
||||
## Features {#services-homepage-features}
|
||||
|
||||
- Declarative SSO login through forward authentication.
|
||||
Only users of the [Homepage LDAP user group][] can access the web UI.
|
||||
This is enforced using the [Authelia block][] which integrates with the LLDAP block.
|
||||
- Access through [subdomain][] using the reverse proxy.
|
||||
It is implemented with the [Nginx block][].
|
||||
- Access through [HTTPS][] using the reverse proxy.
|
||||
It is implemented with the [SSL block][].
|
||||
- Integration with [secrets contract][] to set the API key for a widget.
|
||||
|
||||
[Homepage LDAP user group]: #services-homepage-options-shb.homepage.ldap.userGroup
|
||||
[Authelia block]: blocks-authelia.html
|
||||
[subdomain]: #services-open-webui-options-shb.open-webui.subdomain
|
||||
[HTTPS]: #services-open-webui-options-shb.open-webui.ssl
|
||||
[Nginx block]: blocks-nginx.html
|
||||
[SSL block]: blocks-ssl.html
|
||||
[secrets contract]: contracts-secret.html
|
||||
|
||||
::: {.note}
|
||||
The service does not use state so no backup or impermanence integration is provided.
|
||||
:::
|
||||
|
||||
## Usage {#services-homepage-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).
|
||||
|
||||
::: {.note}
|
||||
Part of the configuration is done through the `shb.homepage` option described here
|
||||
and the rest is done through the upstream [`services.homepage-dashboard`][] option.
|
||||
:::
|
||||
|
||||
[`services.homepage-dashboard`]: https://search.nixos.org/options?query=services.homepage-dashboard
|
||||
|
||||
### Main service configuration {#services-homepage-usage-main}
|
||||
|
||||
This part sets up the web UI and its integration with the other SHB services.
|
||||
It also creates the various service groups which will hold each service.
|
||||
The names are arbitrary and you can order them as you wish through the `sortOrder` option.
|
||||
|
||||
```nix
|
||||
{
|
||||
shb.certs.certs.letsencrypt.${domain}.extraDomains = [
|
||||
"${config.shb.homepage.subdomain}.${config.shb.homepage.domain}"
|
||||
];
|
||||
shb.homepage = {
|
||||
enable = true;
|
||||
subdomain = "home";
|
||||
inherit domain;
|
||||
ssl = config.shb.certs.certs.letsencrypt.${domain};
|
||||
|
||||
sso = {
|
||||
enable = true;
|
||||
authEndpoint = "https://${config.shb.authelia.subdomain}.${config.shb.authelia.domain}";
|
||||
};
|
||||
|
||||
servicesGroups = {
|
||||
Home.sortOrder = 1;
|
||||
Documents.sortOrder = 2;
|
||||
Finance.sortOrder = 3;
|
||||
Media.sortOrder = 4;
|
||||
Admin.sortOrder = 5;
|
||||
};
|
||||
};
|
||||
|
||||
services.homepage-dashboard = {
|
||||
settings = {
|
||||
statusStyle = "dot";
|
||||
disableIndexing = true;
|
||||
};
|
||||
|
||||
widgets = [
|
||||
{
|
||||
locale = "fr";
|
||||
format = {
|
||||
dateStyle = "long";
|
||||
timeStyle = "long";
|
||||
};
|
||||
}
|
||||
];
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
The [Homepage LDAP user group][] is created automatically and users can be added declaratively to the group with:.
|
||||
|
||||
```nix
|
||||
{
|
||||
shb.lldap.ensureUsers.${user}.groups = [
|
||||
config.shb.homepage.ldap.userGroup
|
||||
];
|
||||
}
|
||||
```
|
||||
|
||||
### Display SHB service {#services-homepage-usage-service}
|
||||
|
||||
A service consumer of the dashboard contract provides a `dashboard` option that can be used like so:
|
||||
|
||||
```nix
|
||||
{
|
||||
shb.homepage.servicesGroups.Media.services.Jellyfin = {
|
||||
sortOrder = 2;
|
||||
dashboard.request = config.shb.jellyfin.dashboard.request;
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
By default:
|
||||
|
||||
- The `serviceName` option comes from the attr name, here `Jellyfin`.
|
||||
- The `icon` option comes from applying `toLower` on the attr name.
|
||||
- The `siteMonitor` option is set only if `internalUrl` is set.
|
||||
|
||||
They can be overridden by setting them in the [settings](#services-homepage-options-shb.homepage.servicesGroups._name_.services._name_.settings option) (see option documentation for examples):
|
||||
|
||||
```nix
|
||||
{
|
||||
shb.homepage.servicesGroups.Media.services.Jellyfin = {
|
||||
sortOrder = 2;
|
||||
dashboard.request = config.shb.<service>.dashboard.request;
|
||||
settings = {
|
||||
// custom options here.
|
||||
};
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
Secrets can be randomly generated with `nix run nixpkgs#openssl -- rand -hex 64`.
|
||||
|
||||
### Display custom service {#services-homepage-usage-custom}
|
||||
|
||||
To display a service that does not provide a `dashboard` option like in the previous section, set the values of the request manually:
|
||||
|
||||
```nix
|
||||
{
|
||||
shb.homepage.servicesGroups.Media.services.Jellyfin = {
|
||||
sortOrder = 2;
|
||||
dashboard.request = {
|
||||
externalUrl = "https://jellyfin.example.com";
|
||||
internalUrl = "http://127.0.0.1:8081";
|
||||
};
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
### Add API key for widget {#services-homepage-usage-widget}
|
||||
|
||||
For services [supporting a widget](https://gethomepage.dev/widgets/),
|
||||
create an API key through the service's web UI if available
|
||||
then store it securely (using SOPS for example) and provide it through the
|
||||
`apiKey` option:
|
||||
|
||||
```nix
|
||||
{
|
||||
shb.homepage.servicesGroups.Media.services.Jellyfin = {
|
||||
sortOrder = 1;
|
||||
dashboard.request = config.shb.jellyfin.dashboard.request;
|
||||
apiKey.result = config.shb.sops.secret."jellyfin/homepageApiKey".result;
|
||||
};
|
||||
shb.sops.secret."jellyfin/homepageApiKey".request =
|
||||
config.shb.homepage.servicesGroups.Media.services.Jellyfin.apiKey.request;
|
||||
}
|
||||
```
|
||||
|
||||
Unfortunately creating API keys declaratively is rarely supported by upstream services.
|
||||
|
||||
## Options Reference {#services-homepage-options}
|
||||
|
||||
```{=include=} options
|
||||
id-prefix: services-homepage-options-
|
||||
list-id: selfhostblocks-service-homepage-options
|
||||
source: @OPTIONS_JSON@
|
||||
```
|
||||
|
|
@ -465,6 +465,20 @@ in
|
|||
default = false;
|
||||
example = true;
|
||||
};
|
||||
|
||||
dashboard = lib.mkOption {
|
||||
description = ''
|
||||
Dashboard contract consumer
|
||||
'';
|
||||
default = { };
|
||||
type = lib.types.submodule {
|
||||
options = shb.contracts.dashboard.mkRequester {
|
||||
externalUrl = "https://${fqdn}";
|
||||
externalUrlText = "https://\${config.shb.immich.subdomain}.\${config.shb.immich.domain}";
|
||||
internalUrl = "http://127.0.0.1:${toString cfg.port}";
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
config = mkIf cfg.enable {
|
||||
|
|
|
|||
|
|
@ -317,6 +317,20 @@ in
|
|||
};
|
||||
};
|
||||
};
|
||||
|
||||
dashboard = lib.mkOption {
|
||||
description = ''
|
||||
Dashboard contract consumer
|
||||
'';
|
||||
default = { };
|
||||
type = types.submodule {
|
||||
options = shb.contracts.dashboard.mkRequester {
|
||||
externalUrl = "https://${fqdn}";
|
||||
externalUrlText = "https://\${config.shb.jellyfin.subdomain}.\${config.shb.jellyfin.domain}";
|
||||
internalUrl = "http://127.0.0.1:${toString cfg.port}";
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
imports = [
|
||||
|
|
|
|||
|
|
@ -20,9 +20,12 @@ this one sets up, in a fully declarative manner:
|
|||
- Declarative [LDAP](#services-jellyfin-options-shb.jellyfin.ldap) configuration.
|
||||
- Declarative [SSO](#services-jellyfin-options-shb.jellyfin.sso) configuration.
|
||||
- [Backup](#services-jellyfin-options-shb.jellyfin.backup) through the [backup block](./blocks-backup.html). [Manual](#services-jellyfin-usage-backup).
|
||||
- Integration with the [dashboard contract](contracts-dashboard.html) for displaying user facing application in a dashboard. [Manual](#services-jellyfin-usage-applicationdashboard)
|
||||
|
||||
## Usage {#services-jellyfin-usage}
|
||||
|
||||
### Initial Configuration {#services-jellyfin-usage-configuration}
|
||||
|
||||
The following snippet assumes a few blocks have been setup already:
|
||||
|
||||
- the [secrets block](usage.html#usage-secrets) with SOPS,
|
||||
|
|
@ -133,6 +136,35 @@ shb.lldap.ensureUsers.USERNAME.groups = [
|
|||
];
|
||||
```
|
||||
|
||||
### Application Dashboard {#services-jellyfin-usage-applicationdashboard}
|
||||
|
||||
Integration with the [dashboard contract](contracts-dashboard.html) is provided
|
||||
by the [dashboard option](#services-jellyfin-options-shb.jellyfin.dashboard).
|
||||
|
||||
For example using the [Homepage](services-homepage.html) service:
|
||||
|
||||
```nix
|
||||
{
|
||||
shb.homepage.servicesGroups.Media.services.Jellyfin = {
|
||||
sortOrder = 1;
|
||||
dashboard.request = config.shb.jellyfin.dashboard.request;
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
An API key can be set to show extra info:
|
||||
|
||||
```nix
|
||||
{
|
||||
shb.homepage.servicesGroups.Media.services.Jellyfin = {
|
||||
apiKey.result = config.shb.sops.secret."jellyfin/homepageApiKey".result;
|
||||
};
|
||||
|
||||
shb.sops.secret."jellyfin/homepageApiKey".request =
|
||||
config.shb.homepage.servicesGroups.Media.services.Jellyfin.apiKey.request;
|
||||
}
|
||||
```
|
||||
|
||||
## Debug {#services-jellyfin-debug}
|
||||
|
||||
In case of an issue, check the logs for systemd service `jellyfin.service`.
|
||||
|
|
|
|||
|
|
@ -175,6 +175,20 @@ in
|
|||
};
|
||||
};
|
||||
};
|
||||
|
||||
dashboard = lib.mkOption {
|
||||
description = ''
|
||||
Dashboard contract consumer
|
||||
'';
|
||||
default = { };
|
||||
type = lib.types.submodule {
|
||||
options = shb.contracts.dashboard.mkRequester {
|
||||
externalUrl = "https://${cfg.subdomain}.${cfg.domain}";
|
||||
externalUrlText = "https://\${config.shb.karakeep.subdomain}.\${config.shb.karakeep.domain}";
|
||||
internalUrl = "http://127.0.0.1:${toString cfg.port}";
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
config = (
|
||||
|
|
|
|||
|
|
@ -21,9 +21,12 @@ It integrates well with [Ollama][].
|
|||
- Access through [subdomain](#services-karakeep-options-shb.karakeep.subdomain) using reverse proxy.
|
||||
- Access through [HTTPS](#services-karakeep-options-shb.karakeep.ssl) using reverse proxy.
|
||||
- [Backup](#services-karakeep-options-shb.karakeep.sso) through the [backup block](./blocks-backup.html).
|
||||
- Integration with the [dashboard contract](contracts-dashboard.html) for displaying user facing application in a dashboard. [Manual](#services-karakeep-usage-applicationdashboard)
|
||||
|
||||
## Usage {#services-karakeep-usage}
|
||||
|
||||
### Initial Configuration {#services-karakeep-usage-configuration}
|
||||
|
||||
The following snippet assumes a few blocks have been setup already:
|
||||
|
||||
- the [secrets block](usage.html#usage-secrets) with SOPS,
|
||||
|
|
@ -66,6 +69,35 @@ The [user](#services-open-webui-options-shb.open-webui.ldap.userGroup)
|
|||
and [admin](#services-open-webui-options-shb.open-webui.ldap.adminGroup)
|
||||
LDAP groups are created automatically.
|
||||
|
||||
### Application Dashboard {#services-karakeep-usage-applicationdashboard}
|
||||
|
||||
Integration with the [dashboard contract](contracts-dashboard.html) is provided
|
||||
by the [dashboard option](#services-karakeep-options-shb.karakeep.dashboard).
|
||||
|
||||
For example using the [Homepage](services-homepage.html) service:
|
||||
|
||||
```nix
|
||||
{
|
||||
shb.homepage.servicesGroups.Documents.services.Karakeep = {
|
||||
sortOrder = 3;
|
||||
dashboard.request = config.shb.karakeep.dashboard.request;
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
An API key can be set to show extra info:
|
||||
|
||||
```nix
|
||||
{
|
||||
shb.homepage.servicesGroups.Documents.services.Karakeep = {
|
||||
apiKey.result = config.shb.sops.secret."karakeep/homepageApiKey".result;
|
||||
};
|
||||
|
||||
shb.sops.secret."karakeep/homepageApiKey".request =
|
||||
config.shb.homepage.servicesGroups.Documents.services.Karakeep.apiKey.request;
|
||||
}
|
||||
```
|
||||
|
||||
## Integration with Ollama {#services-karakeep-ollama}
|
||||
|
||||
Assuming ollama is enabled, it will be available on port `config.services.ollama.port`.
|
||||
|
|
|
|||
|
|
@ -694,6 +694,21 @@ in
|
|||
but Self Host Blocks assumes that it will not be the case for most users.
|
||||
'';
|
||||
};
|
||||
|
||||
dashboard = lib.mkOption {
|
||||
description = ''
|
||||
Dashboard contract consumer
|
||||
'';
|
||||
default = { };
|
||||
type = lib.types.submodule {
|
||||
options = shb.contracts.dashboard.mkRequester {
|
||||
externalUrl = "https://${fqdn}";
|
||||
externalUrlText = "https://\${config.shb.nextcloud.subdomain}.\${config.shb.nextcloud.domain}";
|
||||
internalUrl = "https://${fqdn}";
|
||||
internalUrlText = "https://\${config.shb.nextcloud.subdomain}.\${config.shb.nextcloud.domain}";
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
config = lib.mkMerge [
|
||||
|
|
|
|||
|
|
@ -35,8 +35,8 @@ It is based on the nixpkgs Nextcloud server and provides opinionated defaults.
|
|||
- Forces PostgreSQL as the database.
|
||||
- Forces Redis as the cache and sets good defaults.
|
||||
- Backup of the [`shb.nextcloud.dataDir`][dataDir] through the [backup block](./blocks-backup.html).
|
||||
- [Dashboard](#services-nextcloudserver-dashboard) for monitoring of reverse proxy, PHP-FPM, and database backups through the [monitoring
|
||||
block](./blocks-monitoring.html).
|
||||
- [Monitoring Dashboard](#services-nextcloudserver-dashboard) for monitoring of reverse proxy, PHP-FPM, and database backups through the [monitoring block](./blocks-monitoring.html).
|
||||
- Integration with the [dashboard contract](contracts-dashboard.html) for displaying user facing application in a dashboard.
|
||||
- [Integration Tests](@REPO@/test/services/nextcloud.nix)
|
||||
- Tests system cron job is setup correctly.
|
||||
- Tests initial admin user and password are setup correctly.
|
||||
|
|
@ -344,6 +344,22 @@ Note that this will backup the whole PostgreSQL instance,
|
|||
not just the Nextcloud database.
|
||||
This limitation will be lifted in the future.
|
||||
|
||||
### Application Dashboard {#services-nextcloudserver-usage-applicationdashboard}
|
||||
|
||||
Integration with the [dashboard contract](contracts-dashboard.html) is provided
|
||||
by the [dashboard option](#services-nextcloudserver-options-shb.nextcloud.dashboard).
|
||||
|
||||
For example using the [Homepage](services-homepage.html) service:
|
||||
|
||||
```nix
|
||||
{
|
||||
shb.homepage.servicesGroups.Documents.services.Nextcloud = {
|
||||
sortOrder = 1;
|
||||
dashboard.request = config.shb.nextcloud.dashboard.request;
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
### Enable Preview Generator App {#services-nextcloudserver-usage-previewgenerator}
|
||||
|
||||
The following snippet installs and enables the [Preview
|
||||
|
|
@ -537,7 +553,7 @@ by issuing the command `nextcloud-occ config:system:delete instanceid`.
|
|||
Head over to the [Nextcloud demo](demo-nextcloud-server.html) for a demo that installs Nextcloud with or
|
||||
without LDAP integration on a VM with minimal manual steps.
|
||||
|
||||
## Dashboard {#services-nextcloudserver-dashboard}
|
||||
## Monitoring Dashboard {#services-nextcloudserver-dashboard}
|
||||
|
||||
The dashboard is added to Grafana automatically under "Self Host Blocks > Nextcloud"
|
||||
as long as the Nextcloud service is [enabled][]
|
||||
|
|
|
|||
|
|
@ -160,6 +160,20 @@ in
|
|||
};
|
||||
};
|
||||
};
|
||||
|
||||
dashboard = lib.mkOption {
|
||||
description = ''
|
||||
Dashboard contract consumer
|
||||
'';
|
||||
default = { };
|
||||
type = lib.types.submodule {
|
||||
options = shb.contracts.dashboard.mkRequester {
|
||||
externalUrl = "https://${cfg.subdomain}.${cfg.domain}";
|
||||
externalUrlText = "https://\${config.shb.open-webui.subdomain}.\${config.shb.open-webui.domain}";
|
||||
internalUrl = "http://127.0.0.1:${toString cfg.port}";
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
config = (
|
||||
|
|
|
|||
|
|
@ -21,9 +21,12 @@ This service sets up [Open WebUI][] which provides a frontend to various LLMs.
|
|||
- Access through [subdomain](#services-open-webui-options-shb.open-webui.subdomain) using reverse proxy.
|
||||
- Access through [HTTPS](#services-open-webui-options-shb.open-webui.ssl) using reverse proxy.
|
||||
- [Backup](#services-open-webui-options-shb.open-webui.sso) through the [backup block](./blocks-backup.html).
|
||||
- Integration with the [dashboard contract](contracts-dashboard.html) for displaying user facing application in a dashboard. [Manual](#services-open-webui-usage-applicationdashboard)
|
||||
|
||||
## Usage {#services-open-webui-usage}
|
||||
|
||||
### Initial Configuration {#services-open-webui-usage-configuration}
|
||||
|
||||
The following snippet assumes a few blocks have been setup already:
|
||||
|
||||
- the [secrets block](usage.html#usage-secrets) with SOPS,
|
||||
|
|
@ -63,6 +66,25 @@ The [user](#services-open-webui-options-shb.open-webui.ldap.userGroup)
|
|||
and [admin](#services-open-webui-options-shb.open-webui.ldap.adminGroup)
|
||||
LDAP groups are created automatically.
|
||||
|
||||
### Application Dashboard {#services-open-webui-usage-applicationdashboard}
|
||||
|
||||
Integration with the [dashboard contract](contracts-dashboard.html) is provided
|
||||
by the [dashboard option](#services-open-webui-options-shb.open-webui.dashboard).
|
||||
|
||||
For example using the [Homepage](services-homepage.html) service:
|
||||
|
||||
```nix
|
||||
{
|
||||
shb.homepage.servicesGroups.Documents.services.OpenWebUI = {
|
||||
sortOrder = 1;
|
||||
dashboard.request = config.shb.home-assistant.dashboard.request;
|
||||
settings.icon = "sh-open-webui";
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
The icon needs to be set manually otherwise it is not displayed correctly.
|
||||
|
||||
## Integration with OLLAMA {#services-open-webui-ollama}
|
||||
|
||||
Assuming ollama is enabled, it will be available on port `config.services.ollama.port`.
|
||||
|
|
|
|||
|
|
@ -59,10 +59,8 @@ let
|
|||
mkIf
|
||||
lists
|
||||
mkOption
|
||||
optionals
|
||||
;
|
||||
inherit (lib.types)
|
||||
attrs
|
||||
attrsOf
|
||||
bool
|
||||
enum
|
||||
|
|
@ -346,6 +344,20 @@ in
|
|||
};
|
||||
};
|
||||
};
|
||||
|
||||
dashboard = lib.mkOption {
|
||||
description = ''
|
||||
Dashboard contract consumer
|
||||
'';
|
||||
default = { };
|
||||
type = lib.types.submodule {
|
||||
options = shb.contracts.dashboard.mkRequester {
|
||||
externalUrl = "https://${cfg.subdomain}.${cfg.domain}";
|
||||
externalUrlText = "https://\${config.shb.paperless.subdomain}.\${config.shb.paperless.domain}";
|
||||
internalUrl = "http://127.0.0.1:${toString cfg.port}";
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
config = mkIf cfg.enable {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
{
|
||||
config,
|
||||
lib,
|
||||
pkgs,
|
||||
shb,
|
||||
...
|
||||
}:
|
||||
|
|
@ -132,6 +131,20 @@ in
|
|||
};
|
||||
};
|
||||
};
|
||||
|
||||
dashboard = lib.mkOption {
|
||||
description = ''
|
||||
Dashboard contract consumer
|
||||
'';
|
||||
default = { };
|
||||
type = lib.types.submodule {
|
||||
options = shb.contracts.dashboard.mkRequester {
|
||||
externalUrl = "https://${cfg.subdomain}.${cfg.domain}";
|
||||
externalUrlText = "https://\${config.shb.pinchflat.subdomain}.\${config.shb.pinchflat.domain}";
|
||||
internalUrl = "http://127.0.0.1:${toString cfg.port}";
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
config = lib.mkIf cfg.enable {
|
||||
|
|
|
|||
|
|
@ -9,8 +9,14 @@ this one sets up, in a fully declarative manner,
|
|||
LDAP and SSO integration
|
||||
and has a nicer option for secrets.
|
||||
|
||||
## Features {#services-pinchflat-features}
|
||||
|
||||
- Integration with the [dashboard contract](contracts-dashboard.html) for displaying user facing application in a dashboard. [Manual](#services-pinchflat-usage-applicationdashboard)
|
||||
|
||||
## Usage {#services-pinchflat-usage}
|
||||
|
||||
### Initial Configuration {#services-pinchflat-usage-configuration}
|
||||
|
||||
The following snippet assumes a few blocks have been setup already:
|
||||
|
||||
- the [secrets block](usage.html#usage-secrets) with SOPS,
|
||||
|
|
@ -46,7 +52,7 @@ Secrets can be randomly generated with `nix run nixpkgs#openssl -- rand -hex 64`
|
|||
The [user](#services-pinchflat-options-shb.pinchflat.ldap.userGroup)
|
||||
LDAP group is created automatically.
|
||||
|
||||
## Backup {#services-pinchflat-usage-backup}
|
||||
### Backup {#services-pinchflat-usage-backup}
|
||||
|
||||
Backing up Pinchflat using the [Restic block](blocks-restic.html) is done like so:
|
||||
|
||||
|
|
@ -63,6 +69,22 @@ 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.
|
||||
|
||||
### Application Dashboard {#services-pinchflat-usage-applicationdashboard}
|
||||
|
||||
Integration with the [dashboard contract](contracts-dashboard.html) is provided
|
||||
by the [dashboard option](#services-pinchflat-options-shb.pinchflat.dashboard).
|
||||
|
||||
For example using the [Homepage](services-homepage.html) service:
|
||||
|
||||
```nix
|
||||
{
|
||||
shb.homepage.servicesGroups.Media.services.Pinchflat = {
|
||||
sortOrder = 2;
|
||||
dashboard.request = config.shb.pinchflat.dashboard.request;
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
## Options Reference {#services-pinchflat-options}
|
||||
|
||||
```{=include=} options
|
||||
|
|
|
|||
|
|
@ -170,6 +170,20 @@ in
|
|||
default = false;
|
||||
example = true;
|
||||
};
|
||||
|
||||
dashboard = lib.mkOption {
|
||||
description = ''
|
||||
Dashboard contract consumer
|
||||
'';
|
||||
default = { };
|
||||
type = lib.types.submodule {
|
||||
options = shb.contracts.dashboard.mkRequester {
|
||||
externalUrl = "https://${cfg.subdomain}.${cfg.domain}";
|
||||
externalUrlText = "https://\${config.shb.vaultwarden.subdomain}.\${config.shb.vaultwarden.domain}";
|
||||
internalUrl = "http://127.0.0.1:${toString cfg.port}";
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
config = lib.mkIf cfg.enable {
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ This NixOS module is a service that sets up a [Vaultwarden Server](https://githu
|
|||
- Backup of the data directory through the [backup contract](./contracts-backup.html).
|
||||
- [Integration Tests](@REPO@/test/services/vaultwarden.nix)
|
||||
- Tests /admin can only be accessed when authenticated with SSO.
|
||||
- Access to advanced options not exposed here thanks to how NixOS modules work.
|
||||
- Integration with the [dashboard contract](contracts-dashboard.html) for displaying user facing application in a dashboard.
|
||||
|
||||
## Usage {#services-vaultwarden-usage}
|
||||
|
||||
|
|
@ -120,6 +120,22 @@ The name `"vaultwarden"` in the `instances` can be anything.
|
|||
The `config.shb.vaultwarden.backup` option provides what directories to backup.
|
||||
You can define any number of Restic instances to backup Vaultwarden multiple times.
|
||||
|
||||
### Application Dashboard {#services-vaultwarden-usage-applicationdashboard}
|
||||
|
||||
Integration with the [dashboard contract](contracts-dashboard.html) is provided
|
||||
by the [dashboard option](#services-vaultwarden-options-shb.vaultwarden.dashboard).
|
||||
|
||||
For example using the [Homepage](services-homepage.html) service:
|
||||
|
||||
```nix
|
||||
{
|
||||
shb.homepage.servicesGroups.Documents.services.Vaultwarden = {
|
||||
sortOrder = 10;
|
||||
dashboard.request = config.shb.vaultwarden.dashboard.request;
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
## Maintenance {#services-vaultwarden-maintenance}
|
||||
|
||||
No command-line tool is provided to administer Vaultwarden.
|
||||
|
|
|
|||
269
test/modules/homepage.nix
Normal file
269
test/modules/homepage.nix
Normal file
|
|
@ -0,0 +1,269 @@
|
|||
{ shb }:
|
||||
{
|
||||
testHomepageAsServiceGroup = {
|
||||
expected = [
|
||||
{
|
||||
"Media" = [
|
||||
{
|
||||
"Jellyfin" = {
|
||||
"href" = "https://example.com/jellyfin";
|
||||
"icon" = "sh-jellyfin";
|
||||
"siteMonitor" = "http://127.0.0.1:8096";
|
||||
};
|
||||
}
|
||||
];
|
||||
}
|
||||
];
|
||||
|
||||
expr = shb.homepage.asServiceGroup {
|
||||
Media = {
|
||||
services = {
|
||||
Jellyfin = {
|
||||
dashboard.request = {
|
||||
externalUrl = "https://example.com/jellyfin";
|
||||
internalUrl = "http://127.0.0.1:8096";
|
||||
};
|
||||
apiKey = null;
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
testHomepageAsServiceGroupApiKey = {
|
||||
expected = [
|
||||
{
|
||||
"Media" = [
|
||||
{
|
||||
"Jellyfin" = {
|
||||
"href" = "https://example.com/jellyfin";
|
||||
"icon" = "sh-jellyfin";
|
||||
"siteMonitor" = "http://127.0.0.1:8096";
|
||||
"widget" = {
|
||||
"key" = "{{HOMEPAGE_FILE_Media_Jellyfin}}";
|
||||
"password" = "{{HOMEPAGE_FILE_Media_Jellyfin}}";
|
||||
"type" = "jellyfin";
|
||||
"url" = "http://127.0.0.1:8096";
|
||||
};
|
||||
};
|
||||
}
|
||||
];
|
||||
}
|
||||
];
|
||||
|
||||
expr = shb.homepage.asServiceGroup {
|
||||
Media = {
|
||||
services = {
|
||||
Jellyfin = {
|
||||
dashboard.request = {
|
||||
externalUrl = "https://example.com/jellyfin";
|
||||
internalUrl = "http://127.0.0.1:8096";
|
||||
};
|
||||
apiKey.result.path = "path_D";
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
testHomepageAsServiceGroupNoServiceMonitor = {
|
||||
expected = [
|
||||
{
|
||||
"Media" = [
|
||||
{
|
||||
"Jellyfin" = {
|
||||
"href" = "https://example.com/jellyfin";
|
||||
"icon" = "sh-jellyfin";
|
||||
"siteMonitor" = null;
|
||||
};
|
||||
}
|
||||
];
|
||||
}
|
||||
];
|
||||
|
||||
expr = shb.homepage.asServiceGroup {
|
||||
Media = {
|
||||
services = {
|
||||
Jellyfin = {
|
||||
dashboard.request = {
|
||||
externalUrl = "https://example.com/jellyfin";
|
||||
internalUrl = null;
|
||||
};
|
||||
apiKey = null;
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
testHomepageAsServiceGroupOverride = {
|
||||
expected = [
|
||||
{
|
||||
"Media" = [
|
||||
{
|
||||
"Jellyfin" = {
|
||||
"href" = "https://example.com/jellyfin";
|
||||
"icon" = "sh-icon";
|
||||
"siteMonitor" = "http://127.0.0.1:8096";
|
||||
};
|
||||
}
|
||||
];
|
||||
}
|
||||
];
|
||||
|
||||
expr = shb.homepage.asServiceGroup {
|
||||
Media = {
|
||||
services = {
|
||||
Jellyfin = {
|
||||
dashboard.request = {
|
||||
externalUrl = "https://example.com/jellyfin";
|
||||
internalUrl = "http://127.0.0.1:8096";
|
||||
};
|
||||
settings = {
|
||||
icon = "sh-icon";
|
||||
};
|
||||
apiKey = null;
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
testHomepageAsServiceGroupSortOrder = {
|
||||
expected = [
|
||||
{ "C" = [ ]; }
|
||||
{ "A" = [ ]; }
|
||||
{ "B" = [ ]; }
|
||||
];
|
||||
|
||||
expr = shb.homepage.asServiceGroup {
|
||||
A = {
|
||||
sortOrder = 2;
|
||||
services = { };
|
||||
};
|
||||
B = {
|
||||
sortOrder = 3;
|
||||
services = { };
|
||||
};
|
||||
C = {
|
||||
sortOrder = 1;
|
||||
services = { };
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
testHomepageAsServiceServicesSortOrder = {
|
||||
expected = [
|
||||
{
|
||||
"Media" = [
|
||||
{
|
||||
"A" = {
|
||||
"href" = "https://example.com/a";
|
||||
"icon" = "sh-a";
|
||||
"siteMonitor" = null;
|
||||
};
|
||||
}
|
||||
{
|
||||
"C" = {
|
||||
"href" = "https://example.com/c";
|
||||
"icon" = "sh-c";
|
||||
"siteMonitor" = null;
|
||||
};
|
||||
}
|
||||
{
|
||||
"B" = {
|
||||
"href" = "https://example.com/b";
|
||||
"icon" = "sh-b";
|
||||
"siteMonitor" = null;
|
||||
};
|
||||
}
|
||||
];
|
||||
}
|
||||
];
|
||||
|
||||
expr = shb.homepage.asServiceGroup {
|
||||
Media = {
|
||||
sortOrder = null;
|
||||
services = {
|
||||
A = {
|
||||
sortOrder = 1;
|
||||
dashboard.request = {
|
||||
externalUrl = "https://example.com/a";
|
||||
internalUrl = null;
|
||||
};
|
||||
apiKey = null;
|
||||
};
|
||||
B = {
|
||||
sortOrder = 3;
|
||||
dashboard.request = {
|
||||
externalUrl = "https://example.com/b";
|
||||
internalUrl = null;
|
||||
};
|
||||
apiKey = null;
|
||||
};
|
||||
C = {
|
||||
sortOrder = 2;
|
||||
dashboard.request = {
|
||||
externalUrl = "https://example.com/c";
|
||||
internalUrl = null;
|
||||
};
|
||||
apiKey = null;
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
testHomepageAllKeys = {
|
||||
expected = {
|
||||
"A_A" = "path_A";
|
||||
"A_B" = "path_B";
|
||||
"B_D" = "path_D";
|
||||
};
|
||||
|
||||
expr = shb.homepage.allKeys {
|
||||
A = {
|
||||
sortOrder = 1;
|
||||
services = {
|
||||
A = {
|
||||
sortOrder = 1;
|
||||
dashboard.request = {
|
||||
externalUrl = "https://example.com/a";
|
||||
internalUrl = null;
|
||||
};
|
||||
apiKey.result.path = "path_A";
|
||||
};
|
||||
B = {
|
||||
sortOrder = 2;
|
||||
dashboard.request = {
|
||||
externalUrl = "https://example.com/b";
|
||||
internalUrl = null;
|
||||
};
|
||||
apiKey.result.path = "path_B";
|
||||
};
|
||||
};
|
||||
};
|
||||
B = {
|
||||
sortOrder = 2;
|
||||
services = {
|
||||
C = {
|
||||
sortOrder = 1;
|
||||
dashboard.request = {
|
||||
externalUrl = "https://example.com/a";
|
||||
internalUrl = null;
|
||||
};
|
||||
apiKey = null;
|
||||
};
|
||||
D = {
|
||||
sortOrder = 2;
|
||||
dashboard.request = {
|
||||
externalUrl = "https://example.com/b";
|
||||
internalUrl = null;
|
||||
};
|
||||
apiKey.result.path = "path_D";
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
|
|
@ -131,3 +131,4 @@ in
|
|||
'';
|
||||
};
|
||||
}
|
||||
// import ./homepage.nix { inherit shb; }
|
||||
|
|
|
|||
209
test/services/homepage.nix
Normal file
209
test/services/homepage.nix
Normal file
|
|
@ -0,0 +1,209 @@
|
|||
{ shb, ... }:
|
||||
let
|
||||
commonTestScript = shb.test.mkScripts {
|
||||
hasSSL = { node, ... }: !(isNull node.config.shb.homepage.ssl);
|
||||
waitForServices =
|
||||
{ ... }:
|
||||
[
|
||||
"homepage-dashboard.service"
|
||||
"nginx.service"
|
||||
];
|
||||
waitForPorts =
|
||||
{ node, ... }:
|
||||
[
|
||||
node.config.services.homepage-dashboard.listenPort
|
||||
];
|
||||
};
|
||||
|
||||
basic =
|
||||
{ config, ... }:
|
||||
{
|
||||
imports = [
|
||||
shb.test.baseModule
|
||||
../../modules/blocks/hardcodedsecret.nix
|
||||
../../modules/services/homepage.nix
|
||||
];
|
||||
|
||||
test = {
|
||||
subdomain = "h";
|
||||
};
|
||||
|
||||
shb.homepage = {
|
||||
enable = true;
|
||||
inherit (config.test) subdomain domain;
|
||||
|
||||
servicesGroups.MyHomeGroup.services.TestService.dashboard = { };
|
||||
};
|
||||
};
|
||||
|
||||
clientLogin =
|
||||
{ config, ... }:
|
||||
{
|
||||
imports = [
|
||||
shb.test.baseModule
|
||||
shb.test.clientLoginModule
|
||||
];
|
||||
test = {
|
||||
subdomain = "h";
|
||||
};
|
||||
|
||||
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('TestService')).to_be_visible()"
|
||||
];
|
||||
}
|
||||
];
|
||||
};
|
||||
};
|
||||
|
||||
https =
|
||||
{ config, ... }:
|
||||
{
|
||||
shb.homepage = {
|
||||
ssl = config.shb.certs.certs.selfsigned.n;
|
||||
};
|
||||
};
|
||||
|
||||
ldap =
|
||||
{ config, ... }:
|
||||
{
|
||||
shb.homepage = {
|
||||
ldap = {
|
||||
userGroup = "user_group";
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
clientLoginSso =
|
||||
{ config, ... }:
|
||||
{
|
||||
imports = [
|
||||
shb.test.baseModule
|
||||
shb.test.clientLoginModule
|
||||
];
|
||||
test = {
|
||||
subdomain = "h";
|
||||
};
|
||||
|
||||
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('TestService')).to_be_visible(timeout=10000)"
|
||||
];
|
||||
}
|
||||
# Bob, with its admin role only, cannot login into Karakeep because admins do not exist in Karakeep.
|
||||
{
|
||||
username = "charlie";
|
||||
password = "NotCharliePassword";
|
||||
nextPageExpect = [
|
||||
"expect(page.get_by_text(re.compile('[Ii]ncorrect'))).to_be_visible()"
|
||||
];
|
||||
}
|
||||
{
|
||||
username = "charlie";
|
||||
password = "CharliePassword";
|
||||
nextPageExpect = [
|
||||
"expect(page).to_have_url(re.compile('.*/authenticated'))"
|
||||
];
|
||||
}
|
||||
];
|
||||
};
|
||||
};
|
||||
|
||||
sso =
|
||||
{ config, ... }:
|
||||
{
|
||||
shb.homepage = {
|
||||
sso = {
|
||||
enable = true;
|
||||
authEndpoint = "https://${config.shb.authelia.subdomain}.${config.shb.authelia.domain}";
|
||||
};
|
||||
};
|
||||
};
|
||||
in
|
||||
{
|
||||
basic = shb.test.runNixOSTest {
|
||||
name = "homepage_basic";
|
||||
|
||||
nodes.client = {
|
||||
imports = [
|
||||
clientLogin
|
||||
];
|
||||
};
|
||||
nodes.server = {
|
||||
imports = [
|
||||
basic
|
||||
];
|
||||
};
|
||||
|
||||
testScript = commonTestScript.access;
|
||||
};
|
||||
|
||||
https = shb.test.runNixOSTest {
|
||||
name = "homepage_https";
|
||||
|
||||
nodes.client = {
|
||||
imports = [
|
||||
clientLogin
|
||||
];
|
||||
};
|
||||
nodes.server = {
|
||||
imports = [
|
||||
basic
|
||||
shb.test.certs
|
||||
https
|
||||
];
|
||||
};
|
||||
|
||||
testScript = commonTestScript.access;
|
||||
};
|
||||
|
||||
sso = shb.test.runNixOSTest {
|
||||
name = "homepage_sso";
|
||||
|
||||
nodes.client = {
|
||||
imports = [
|
||||
clientLoginSso
|
||||
];
|
||||
};
|
||||
nodes.server =
|
||||
{ config, pkgs, ... }:
|
||||
{
|
||||
imports = [
|
||||
basic
|
||||
shb.test.certs
|
||||
https
|
||||
shb.test.ldap
|
||||
ldap
|
||||
(shb.test.sso config.shb.certs.certs.selfsigned.n)
|
||||
sso
|
||||
];
|
||||
};
|
||||
|
||||
testScript = commonTestScript.access.override {
|
||||
redirectSSO = true;
|
||||
};
|
||||
};
|
||||
}
|
||||
Loading…
Reference in a new issue