add tests for arr services and some more options (#205)
This commit is contained in:
parent
e6414dc911
commit
589e2c936f
7 changed files with 451 additions and 170 deletions
1
.github/workflows/test.yml
vendored
1
.github/workflows/test.yml
vendored
|
@ -23,4 +23,5 @@ jobs:
|
||||||
run: |
|
run: |
|
||||||
nix run github:Mic92/nix-fast-build -- \
|
nix run github:Mic92/nix-fast-build -- \
|
||||||
--skip-cached --no-nom \
|
--skip-cached --no-nom \
|
||||||
|
--max-jobs 2 \
|
||||||
--flake ".#checks.$(nix eval --raw --impure --expr builtins.currentSystem)"
|
--flake ".#checks.$(nix eval --raw --impure --expr builtins.currentSystem)"
|
||||||
|
|
|
@ -101,6 +101,7 @@
|
||||||
tests = pkgs.callPackage ./test/modules/lib.nix {};
|
tests = pkgs.callPackage ./test/modules/lib.nix {};
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
// (vm_test "arr" ./test/vm/arr.nix)
|
||||||
// (vm_test "audiobookshelf" ./test/vm/audiobookshelf.nix)
|
// (vm_test "audiobookshelf" ./test/vm/audiobookshelf.nix)
|
||||||
// (vm_test "authelia" ./test/vm/authelia.nix)
|
// (vm_test "authelia" ./test/vm/authelia.nix)
|
||||||
// (vm_test "grocy" ./test/vm/grocy.nix)
|
// (vm_test "grocy" ./test/vm/grocy.nix)
|
||||||
|
|
|
@ -21,7 +21,7 @@ rec {
|
||||||
in
|
in
|
||||||
''
|
''
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
set -x
|
|
||||||
mkdir -p $(dirname ${templatePath})
|
mkdir -p $(dirname ${templatePath})
|
||||||
ln -fs ${file} ${templatePath}
|
ln -fs ${file} ${templatePath}
|
||||||
rm -f ${resultPath}
|
rm -f ${resultPath}
|
||||||
|
|
|
@ -27,19 +27,19 @@ let
|
||||||
default = null;
|
default = null;
|
||||||
};
|
};
|
||||||
|
|
||||||
authEndpoint = lib.mkOption {
|
|
||||||
type = lib.types.str;
|
|
||||||
description = "Auth endpoint for SSO.";
|
|
||||||
default = null;
|
|
||||||
example = "https://authelia.example.com";
|
|
||||||
};
|
|
||||||
|
|
||||||
upstream = lib.mkOption {
|
upstream = lib.mkOption {
|
||||||
type = lib.types.str;
|
type = lib.types.str;
|
||||||
description = "Upstream url to be protected.";
|
description = "Upstream url to be protected.";
|
||||||
example = "http://127.0.0.1:1234";
|
example = "http://127.0.0.1:1234";
|
||||||
};
|
};
|
||||||
|
|
||||||
|
authEndpoint = lib.mkOption {
|
||||||
|
type = lib.types.nullOr lib.types.str;
|
||||||
|
description = "Optional auth endpoint for SSO.";
|
||||||
|
default = null;
|
||||||
|
example = "https://authelia.example.com";
|
||||||
|
};
|
||||||
|
|
||||||
autheliaRules = lib.mkOption {
|
autheliaRules = lib.mkOption {
|
||||||
type = lib.types.listOf (lib.types.attrsOf lib.types.anything);
|
type = lib.types.listOf (lib.types.attrsOf lib.types.anything);
|
||||||
description = "Authelia rule configuration";
|
description = "Authelia rule configuration";
|
||||||
|
@ -133,6 +133,9 @@ in
|
||||||
proxy_set_header Connection "upgrade";
|
proxy_set_header Connection "upgrade";
|
||||||
proxy_cache_bypass $http_upgrade;
|
proxy_cache_bypass $http_upgrade;
|
||||||
|
|
||||||
|
proxy_pass ${c.upstream};
|
||||||
|
''
|
||||||
|
+ lib.optionalString (!(isNull c.authEndpoint)) ''
|
||||||
auth_request /authelia;
|
auth_request /authelia;
|
||||||
auth_request_set $user $upstream_http_remote_user;
|
auth_request_set $user $upstream_http_remote_user;
|
||||||
auth_request_set $groups $upstream_http_remote_groups;
|
auth_request_set $groups $upstream_http_remote_groups;
|
||||||
|
@ -153,12 +156,10 @@ in
|
||||||
auth_request_set $redirect $scheme://$http_host$request_uri;
|
auth_request_set $redirect $scheme://$http_host$request_uri;
|
||||||
error_page 401 =302 ${c.authEndpoint}?rd=$redirect;
|
error_page 401 =302 ${c.authEndpoint}?rd=$redirect;
|
||||||
error_page 403 = ${c.authEndpoint}/error/403;
|
error_page 403 = ${c.authEndpoint}/error/403;
|
||||||
|
|
||||||
proxy_pass ${c.upstream};
|
|
||||||
'';
|
'';
|
||||||
|
|
||||||
# Virtual endpoint created by nginx to forward auth requests.
|
# Virtual endpoint created by nginx to forward auth requests.
|
||||||
locations."/authelia".extraConfig = ''
|
locations."/authelia".extraConfig = lib.mkIf (!(isNull c.authEndpoint)) ''
|
||||||
internal;
|
internal;
|
||||||
proxy_pass ${c.authEndpoint}/api/verify;
|
proxy_pass ${c.authEndpoint}/api/verify;
|
||||||
|
|
||||||
|
|
|
@ -8,7 +8,6 @@ let
|
||||||
|
|
||||||
apps = {
|
apps = {
|
||||||
radarr = {
|
radarr = {
|
||||||
defaultPort = 7001;
|
|
||||||
settingsFormat = formatXML {};
|
settingsFormat = formatXML {};
|
||||||
moreOptions = {
|
moreOptions = {
|
||||||
settings = lib.mkOption {
|
settings = lib.mkOption {
|
||||||
|
@ -17,8 +16,8 @@ let
|
||||||
type = lib.types.submodule {
|
type = lib.types.submodule {
|
||||||
freeformType = apps.radarr.settingsFormat.type;
|
freeformType = apps.radarr.settingsFormat.type;
|
||||||
options = {
|
options = {
|
||||||
APIKeyFile = lib.mkOption {
|
APIKey = lib.mkOption {
|
||||||
type = lib.types.path;
|
type = shblib.secretFileType;
|
||||||
description = "Path to api key secret file.";
|
description = "Path to api key secret file.";
|
||||||
};
|
};
|
||||||
LogLevel = lib.mkOption {
|
LogLevel = lib.mkOption {
|
||||||
|
@ -26,25 +25,173 @@ let
|
||||||
description = "Log level.";
|
description = "Log level.";
|
||||||
default = "info";
|
default = "info";
|
||||||
};
|
};
|
||||||
|
Port = lib.mkOption {
|
||||||
|
type = lib.types.port;
|
||||||
|
description = "Port on which radarr listens to incoming requests.";
|
||||||
|
default = 7878;
|
||||||
|
};
|
||||||
|
AnalyticsEnabled = lib.mkOption {
|
||||||
|
type = lib.types.bool;
|
||||||
|
description = "Wether to send anonymous data or not.";
|
||||||
|
default = false;
|
||||||
|
};
|
||||||
|
BindAddress = lib.mkOption {
|
||||||
|
type = lib.types.str;
|
||||||
|
internal = true;
|
||||||
|
default = "127.0.0.1";
|
||||||
|
};
|
||||||
|
UrlBase = lib.mkOption {
|
||||||
|
type = lib.types.str;
|
||||||
|
internal = true;
|
||||||
|
default = "";
|
||||||
|
};
|
||||||
|
EnableSsl = lib.mkOption {
|
||||||
|
type = lib.types.bool;
|
||||||
|
internal = true;
|
||||||
|
default = false;
|
||||||
|
};
|
||||||
|
AuthenticationMethod = lib.mkOption {
|
||||||
|
type = lib.types.str;
|
||||||
|
internal = true;
|
||||||
|
default = "External";
|
||||||
|
};
|
||||||
|
AuthenticationRequired = lib.mkOption {
|
||||||
|
type = lib.types.str;
|
||||||
|
internal = true;
|
||||||
|
default = "Enabled";
|
||||||
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
sonarr = {
|
sonarr = {
|
||||||
defaultPort = 8989;
|
settingsFormat = formatXML {};
|
||||||
|
moreOptions = {
|
||||||
|
settings = lib.mkOption {
|
||||||
|
description = "Specific options for sonarr.";
|
||||||
|
default = {};
|
||||||
|
type = lib.types.submodule {
|
||||||
|
freeformType = apps.sonarr.settingsFormat.type;
|
||||||
|
options = {
|
||||||
|
APIKey = lib.mkOption {
|
||||||
|
type = shblib.secretFileType;
|
||||||
|
description = "Path to api key secret file.";
|
||||||
|
};
|
||||||
|
LogLevel = lib.mkOption {
|
||||||
|
type = lib.types.enum ["debug" "info"];
|
||||||
|
description = "Log level.";
|
||||||
|
default = "info";
|
||||||
|
};
|
||||||
|
Port = lib.mkOption {
|
||||||
|
type = lib.types.port;
|
||||||
|
description = "Port on which sonarr listens to incoming requests.";
|
||||||
|
default = 8989;
|
||||||
|
};
|
||||||
|
BindAddress = lib.mkOption {
|
||||||
|
type = lib.types.str;
|
||||||
|
internal = true;
|
||||||
|
default = "127.0.0.1";
|
||||||
|
};
|
||||||
|
UrlBase = lib.mkOption {
|
||||||
|
type = lib.types.str;
|
||||||
|
internal = true;
|
||||||
|
default = "";
|
||||||
|
};
|
||||||
|
EnableSsl = lib.mkOption {
|
||||||
|
type = lib.types.bool;
|
||||||
|
internal = true;
|
||||||
|
default = false;
|
||||||
|
};
|
||||||
|
AuthenticationMethod = lib.mkOption {
|
||||||
|
type = lib.types.str;
|
||||||
|
internal = true;
|
||||||
|
default = "External";
|
||||||
|
};
|
||||||
|
AuthenticationRequired = lib.mkOption {
|
||||||
|
type = lib.types.str;
|
||||||
|
internal = true;
|
||||||
|
default = "Enabled";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
};
|
};
|
||||||
bazarr = {
|
bazarr = {
|
||||||
defaultPort = 6767;
|
settingsFormat = formatXML {};
|
||||||
|
moreOptions = {
|
||||||
|
settings = lib.mkOption {
|
||||||
|
description = "Specific options for bazarr.";
|
||||||
|
default = {};
|
||||||
|
type = lib.types.submodule {
|
||||||
|
freeformType = apps.bazarr.settingsFormat.type;
|
||||||
|
options = {
|
||||||
|
LogLevel = lib.mkOption {
|
||||||
|
type = lib.types.enum ["debug" "info"];
|
||||||
|
description = "Log level.";
|
||||||
|
default = "info";
|
||||||
|
};
|
||||||
|
Port = lib.mkOption {
|
||||||
|
type = lib.types.port;
|
||||||
|
description = "Port on which bazarr listens to incoming requests.";
|
||||||
|
default = 6767;
|
||||||
|
readOnly = true;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
};
|
};
|
||||||
readarr = {
|
readarr = {
|
||||||
defaultPort = 8787;
|
settingsFormat = formatXML {};
|
||||||
|
moreOptions = {
|
||||||
|
settings = lib.mkOption {
|
||||||
|
description = "Specific options for readarr.";
|
||||||
|
default = {};
|
||||||
|
type = lib.types.submodule {
|
||||||
|
freeformType = apps.readarr.settingsFormat.type;
|
||||||
|
options = {
|
||||||
|
LogLevel = lib.mkOption {
|
||||||
|
type = lib.types.enum ["debug" "info"];
|
||||||
|
description = "Log level.";
|
||||||
|
default = "info";
|
||||||
|
};
|
||||||
|
Port = lib.mkOption {
|
||||||
|
type = lib.types.port;
|
||||||
|
description = "Port on which readarr listens to incoming requests.";
|
||||||
|
default = 8787;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
};
|
};
|
||||||
lidarr = {
|
lidarr = {
|
||||||
defaultPort = 8686;
|
settingsFormat = formatXML {};
|
||||||
|
moreOptions = {
|
||||||
|
settings = lib.mkOption {
|
||||||
|
description = "Specific options for lidarr.";
|
||||||
|
default = {};
|
||||||
|
type = lib.types.submodule {
|
||||||
|
freeformType = apps.lidarr.settingsFormat.type;
|
||||||
|
options = {
|
||||||
|
LogLevel = lib.mkOption {
|
||||||
|
type = lib.types.enum ["debug" "info"];
|
||||||
|
description = "Log level.";
|
||||||
|
default = "info";
|
||||||
|
};
|
||||||
|
Port = lib.mkOption {
|
||||||
|
type = lib.types.port;
|
||||||
|
description = "Port on which lidarr listens to incoming requests.";
|
||||||
|
default = 8686;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
};
|
};
|
||||||
jackett = {
|
jackett = {
|
||||||
defaultPort = 9117;
|
|
||||||
settingsFormat = pkgs.formats.json {};
|
settingsFormat = pkgs.formats.json {};
|
||||||
moreOptions = {
|
moreOptions = {
|
||||||
settings = lib.mkOption {
|
settings = lib.mkOption {
|
||||||
|
@ -53,8 +200,8 @@ let
|
||||||
type = lib.types.submodule {
|
type = lib.types.submodule {
|
||||||
freeformType = apps.jackett.settingsFormat.type;
|
freeformType = apps.jackett.settingsFormat.type;
|
||||||
options = {
|
options = {
|
||||||
APIKeyFile = lib.mkOption {
|
APIKey = lib.mkOption {
|
||||||
type = lib.types.path;
|
type = shblib.secretFileType;
|
||||||
description = "Path to api key secret file.";
|
description = "Path to api key secret file.";
|
||||||
};
|
};
|
||||||
FlareSolverrUrl = lib.mkOption {
|
FlareSolverrUrl = lib.mkOption {
|
||||||
|
@ -62,8 +209,8 @@ let
|
||||||
description = "FlareSolverr endpoint.";
|
description = "FlareSolverr endpoint.";
|
||||||
default = null;
|
default = null;
|
||||||
};
|
};
|
||||||
OmdbApiKeyFile = lib.mkOption {
|
OmdbApiKey = lib.mkOption {
|
||||||
type = lib.types.nullOr lib.types.path;
|
type = lib.types.nullOr shblib.secretFileType;
|
||||||
description = "File containing the Open Movie Database API key.";
|
description = "File containing the Open Movie Database API key.";
|
||||||
default = null;
|
default = null;
|
||||||
};
|
};
|
||||||
|
@ -87,6 +234,22 @@ let
|
||||||
description = "Port of the proxy. Ignored if ProxyType is set to -1";
|
description = "Port of the proxy. Ignored if ProxyType is set to -1";
|
||||||
default = null;
|
default = null;
|
||||||
};
|
};
|
||||||
|
Port = lib.mkOption {
|
||||||
|
type = lib.types.port;
|
||||||
|
description = "Port on which jackett listens to incoming requests.";
|
||||||
|
default = 9117;
|
||||||
|
readOnly = true;
|
||||||
|
};
|
||||||
|
AllowExternal = lib.mkOption {
|
||||||
|
type = lib.types.bool;
|
||||||
|
internal = true;
|
||||||
|
default = false;
|
||||||
|
};
|
||||||
|
UpdateDisabled = lib.mkOption {
|
||||||
|
type = lib.types.bool;
|
||||||
|
internal = true;
|
||||||
|
default = true;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -94,6 +257,40 @@ let
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
autheliaProtect = { extraBypassResources ? [] }: c: {
|
||||||
|
inherit (c) subdomain domain authEndpoint ssl;
|
||||||
|
|
||||||
|
upstream = "http://127.0.0.1:${toString c.settings.Port}";
|
||||||
|
autheliaRules = lib.optionals (!(isNull c.authEndpoint)) [
|
||||||
|
{
|
||||||
|
domain = "${c.subdomain}.${c.domain}";
|
||||||
|
policy = "bypass";
|
||||||
|
resources = extraBypassResources ++ [
|
||||||
|
"^/api.*"
|
||||||
|
];
|
||||||
|
}
|
||||||
|
{
|
||||||
|
domain = "${c.subdomain}.${c.domain}";
|
||||||
|
policy = "two_factor";
|
||||||
|
subject = ["group:arr_user"];
|
||||||
|
}
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
backup = name: {
|
||||||
|
systemd.tmpfiles.rules = [
|
||||||
|
"d '${config.shb.arr.${name}.dataDir}' 0750 ${config.services.${name}.user} ${config.services.${name}.group} - -"
|
||||||
|
];
|
||||||
|
users.groups.${name} = {
|
||||||
|
members = [ "backup" ];
|
||||||
|
};
|
||||||
|
systemd.services.${name}.serviceConfig = {
|
||||||
|
# Setup permissions needed for backups, as the backup user is member of the jellyfin group.
|
||||||
|
UMask = lib.mkForce "0027";
|
||||||
|
StateDirectoryMode = lib.mkForce "0750";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
formatXML = {}: {
|
formatXML = {}: {
|
||||||
type = with lib.types; let
|
type = with lib.types; let
|
||||||
valueType = nullOr (oneOf [
|
valueType = nullOr (oneOf [
|
||||||
|
@ -109,7 +306,7 @@ let
|
||||||
};
|
};
|
||||||
in valueType;
|
in valueType;
|
||||||
|
|
||||||
generate = name: value: pkgs.callPackage ({ runCommand, python3 }: runCommand name {
|
generate = value: builtins.readFile (pkgs.callPackage ({ runCommand, python3 }: runCommand "config" {
|
||||||
value = builtins.toJSON {Config = value;};
|
value = builtins.toJSON {Config = value;};
|
||||||
passAsFile = [ "value" ];
|
passAsFile = [ "value" ];
|
||||||
} (pkgs.writers.writePython3 "dict2xml" {
|
} (pkgs.writers.writePython3 "dict2xml" {
|
||||||
|
@ -126,7 +323,7 @@ let
|
||||||
os.exit(2)
|
os.exit(2)
|
||||||
with open(os.environ["out"], "w") as out:
|
with open(os.environ["out"], "w") as out:
|
||||||
out.write(dict2xml(content))
|
out.write(dict2xml(content))
|
||||||
'')) {};
|
'')) {});
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -149,24 +346,18 @@ let
|
||||||
example = "example.com";
|
example = "example.com";
|
||||||
};
|
};
|
||||||
|
|
||||||
ssl = lib.mkOption {
|
|
||||||
description = "Path to SSL files";
|
|
||||||
type = lib.types.nullOr contracts.ssl.certs;
|
|
||||||
default = null;
|
|
||||||
};
|
|
||||||
|
|
||||||
port = lib.mkOption {
|
|
||||||
type = lib.types.port;
|
|
||||||
description = "Port on which ${name} listens to incoming requests.";
|
|
||||||
default = c.defaultPort;
|
|
||||||
};
|
|
||||||
|
|
||||||
dataDir = lib.mkOption {
|
dataDir = lib.mkOption {
|
||||||
type = lib.types.str;
|
type = lib.types.str;
|
||||||
description = "Directory where ${name} stores data.";
|
description = "Directory where ${name} stores data.";
|
||||||
default = "/var/lib/${name}";
|
default = "/var/lib/${name}";
|
||||||
};
|
};
|
||||||
|
|
||||||
|
ssl = lib.mkOption {
|
||||||
|
description = "Path to SSL files";
|
||||||
|
type = lib.types.nullOr contracts.ssl.certs;
|
||||||
|
default = null;
|
||||||
|
};
|
||||||
|
|
||||||
authEndpoint = lib.mkOption {
|
authEndpoint = lib.mkOption {
|
||||||
type = lib.types.nullOr lib.types.str;
|
type = lib.types.nullOr lib.types.str;
|
||||||
default = null;
|
default = null;
|
||||||
|
@ -191,164 +382,156 @@ in
|
||||||
options.shb.arr = lib.listToAttrs (lib.mapAttrsToList appOption apps);
|
options.shb.arr = lib.listToAttrs (lib.mapAttrsToList appOption apps);
|
||||||
|
|
||||||
config = lib.mkMerge ([
|
config = lib.mkMerge ([
|
||||||
{
|
(lib.mkIf cfg.radarr.enable ({
|
||||||
services.radarr = lib.mkIf cfg.radarr.enable {
|
services.nginx.enable = true;
|
||||||
|
|
||||||
|
services.radarr = {
|
||||||
enable = true;
|
enable = true;
|
||||||
dataDir = "/var/lib/radarr";
|
dataDir = "/var/lib/radarr";
|
||||||
};
|
};
|
||||||
users.users.radarr = lib.mkIf cfg.radarr.enable {
|
|
||||||
|
users.users.radarr = {
|
||||||
extraGroups = [ "media" ];
|
extraGroups = [ "media" ];
|
||||||
};
|
};
|
||||||
shb.arr.radarr.settings = lib.mkIf cfg.radarr.enable {
|
|
||||||
Port = config.shb.arr.radarr.port;
|
systemd.services.radarr.preStart = shblib.replaceSecrets {
|
||||||
BindAddress = "127.0.0.1";
|
userConfig = cfg.radarr.settings;
|
||||||
UrlBase = "";
|
resultPath = "${config.services.radarr.dataDir}/config.xml";
|
||||||
EnableSsl = "false";
|
generator = apps.radarr.settingsFormat.generate;
|
||||||
AuthenticationMethod = "External";
|
|
||||||
AuthenticationRequired = "Enabled";
|
|
||||||
};
|
};
|
||||||
systemd.services.radarr.preStart =
|
|
||||||
let
|
|
||||||
s = cfg.radarr.settings;
|
|
||||||
templatedfileSettings =
|
|
||||||
lib.optionalAttrs (!(isNull s.APIKeyFile)) {
|
|
||||||
ApiKey = "%APIKEY%";
|
|
||||||
};
|
|
||||||
templatedSettings = (removeAttrs s [ "APIKeyFile" ]) // templatedfileSettings;
|
|
||||||
|
|
||||||
t = shblib.template (apps.radarr.settingsFormat.generate "
|
shb.nginx.autheliaProtect = [ (autheliaProtect {} config.shb.arr.radarr) ];
|
||||||
config.xml" templatedSettings) "${config.services.radarr.dataDir}/config.xml" (
|
|
||||||
lib.optionalAttrs (!(isNull s.APIKeyFile)) {
|
|
||||||
"%APIKEY%" = "$(cat ${s.APIKeyFile})";
|
|
||||||
}
|
|
||||||
);
|
|
||||||
in
|
|
||||||
lib.mkIf cfg.radarr.enable t;
|
|
||||||
|
|
||||||
# Listens on port 8989
|
shb.backup.instances.radarr = cfg.radarr.backupCfg // {
|
||||||
services.sonarr = lib.mkIf cfg.sonarr.enable {
|
sourceDirectories = [
|
||||||
|
config.shb.arr.radarr.dataDir
|
||||||
|
];
|
||||||
|
excludePatterns = [".db-shm" ".db-wal" ".mono"];
|
||||||
|
};
|
||||||
|
} // backup "radarr"))
|
||||||
|
|
||||||
|
(lib.mkIf cfg.sonarr.enable ({
|
||||||
|
services.nginx.enable = true;
|
||||||
|
|
||||||
|
services.sonarr = {
|
||||||
enable = true;
|
enable = true;
|
||||||
dataDir = "/var/lib/sonarr";
|
dataDir = "/var/lib/sonarr";
|
||||||
};
|
};
|
||||||
users.users.sonarr = lib.mkIf cfg.sonarr.enable {
|
users.users.sonarr = {
|
||||||
extraGroups = [ "media" ];
|
extraGroups = [ "media" ];
|
||||||
};
|
};
|
||||||
|
systemd.services.sonarr.preStart = shblib.replaceSecrets {
|
||||||
|
userConfig = cfg.sonarr.settings;
|
||||||
|
resultPath = "${config.services.sonarr.dataDir}/config.xml";
|
||||||
|
generator = apps.sonarr.settingsFormat.generate;
|
||||||
|
};
|
||||||
|
|
||||||
services.bazarr = lib.mkIf cfg.bazarr.enable {
|
shb.nginx.autheliaProtect = [ (autheliaProtect {} config.shb.arr.sonarr) ];
|
||||||
|
|
||||||
|
shb.backup.instances.sonarr = cfg.sonarr.backupCfg // {
|
||||||
|
sourceDirectories = [
|
||||||
|
config.shb.arr.sonarr.dataDir
|
||||||
|
];
|
||||||
|
excludePatterns = [".db-shm" ".db-wal" ".mono"];
|
||||||
|
};
|
||||||
|
} // backup "sonarr"))
|
||||||
|
|
||||||
|
(lib.mkIf cfg.bazarr.enable ({
|
||||||
|
services.bazarr = {
|
||||||
enable = true;
|
enable = true;
|
||||||
listenPort = cfg.bazarr.port;
|
listenPort = cfg.bazarr.settings.Port;
|
||||||
};
|
};
|
||||||
users.users.bazarr = lib.mkIf cfg.bazarr.enable {
|
users.users.bazarr = {
|
||||||
extraGroups = [ "media" ];
|
extraGroups = [ "media" ];
|
||||||
};
|
};
|
||||||
|
systemd.services.bazarr.preStart = shblib.replaceSecrets {
|
||||||
|
userConfig = cfg.bazarr.settings;
|
||||||
|
resultPath = "/var/lib/${config.systemd.services.bazarr.serviceConfig.StateDirectory}/config.xml";
|
||||||
|
generator = apps.bazarr.settingsFormat.generate;
|
||||||
|
};
|
||||||
|
|
||||||
# Listens on port 8787
|
shb.nginx.autheliaProtect = [ (autheliaProtect {} config.shb.arr.bazarr) ];
|
||||||
services.readarr = lib.mkIf cfg.readarr.enable {
|
|
||||||
|
shb.backup.instances.bazarr = cfg.bazarr.backupCfg // {
|
||||||
|
sourceDirectories = [
|
||||||
|
config.shb.arr.bazarr.dataDir
|
||||||
|
];
|
||||||
|
excludePatterns = [".db-shm" ".db-wal" ".mono"];
|
||||||
|
};
|
||||||
|
} // backup "bazarr"))
|
||||||
|
|
||||||
|
(lib.mkIf cfg.readarr.enable ({
|
||||||
|
services.readarr = {
|
||||||
enable = true;
|
enable = true;
|
||||||
dataDir = "/var/lib/readarr";
|
dataDir = "/var/lib/readarr";
|
||||||
};
|
};
|
||||||
users.users.readarr = lib.mkIf cfg.readarr.enable {
|
users.users.readarr = {
|
||||||
extraGroups = [ "media" ];
|
extraGroups = [ "media" ];
|
||||||
};
|
};
|
||||||
|
systemd.services.readarr.preStart = shblib.replaceSecrets {
|
||||||
|
userConfig = cfg.readarr.settings;
|
||||||
|
resultPath = "${config.services.readarr.dataDir}/config.xml";
|
||||||
|
generator = apps.readarr.settingsFormat.generate;
|
||||||
|
};
|
||||||
|
|
||||||
# Listens on port 8686
|
shb.nginx.autheliaProtect = [ (autheliaProtect {} config.shb.arr.readarr) ];
|
||||||
services.lidarr = lib.mkIf cfg.lidarr.enable {
|
|
||||||
|
shb.backup.instances.readarr = cfg.readarr.backupCfg // {
|
||||||
|
sourceDirectories = [
|
||||||
|
config.shb.arr.readarr.dataDir
|
||||||
|
];
|
||||||
|
excludePatterns = [".db-shm" ".db-wal" ".mono"];
|
||||||
|
};
|
||||||
|
} // backup "readarr"))
|
||||||
|
|
||||||
|
(lib.mkIf cfg.lidarr.enable ({
|
||||||
|
services.lidarr = {
|
||||||
enable = true;
|
enable = true;
|
||||||
dataDir = "/var/lib/lidarr";
|
dataDir = "/var/lib/lidarr";
|
||||||
};
|
};
|
||||||
users.users.lidarr = lib.mkIf cfg.lidarr.enable {
|
users.users.lidarr = {
|
||||||
extraGroups = [ "media" ];
|
extraGroups = [ "media" ];
|
||||||
};
|
};
|
||||||
|
systemd.services.lidarr.preStart = shblib.replaceSecrets {
|
||||||
|
userConfig = cfg.lidarr.settings;
|
||||||
|
resultPath = "${config.services.lidarr.dataDir}/config.xml";
|
||||||
|
generator = apps.lidarr.settingsFormat.generate;
|
||||||
|
};
|
||||||
|
|
||||||
# Listens on port 9117
|
shb.nginx.autheliaProtect = [ (autheliaProtect {} config.shb.arr.lidarr) ];
|
||||||
services.jackett = lib.mkIf cfg.jackett.enable {
|
|
||||||
|
shb.backup.instances.lidarr = cfg.lidarr.backupCfg // {
|
||||||
|
sourceDirectories = [
|
||||||
|
config.shb.arr.lidarr.dataDir
|
||||||
|
];
|
||||||
|
excludePatterns = [".db-shm" ".db-wal" ".mono"];
|
||||||
|
};
|
||||||
|
} // backup "lidarr"))
|
||||||
|
|
||||||
|
(lib.mkIf cfg.jackett.enable ({
|
||||||
|
services.jackett = {
|
||||||
enable = true;
|
enable = true;
|
||||||
dataDir = "/var/lib/jackett";
|
dataDir = "/var/lib/jackett";
|
||||||
};
|
};
|
||||||
shb.arr.jackett.settings = lib.mkIf cfg.jackett.enable {
|
users.users.jackett = {
|
||||||
Port = config.shb.arr.jackett.port;
|
|
||||||
AllowExternal = "false";
|
|
||||||
UpdateDisabled = "true";
|
|
||||||
};
|
|
||||||
users.users.jackett = lib.mkIf cfg.jackett.enable {
|
|
||||||
extraGroups = [ "media" ];
|
extraGroups = [ "media" ];
|
||||||
};
|
};
|
||||||
systemd.services.jackett.preStart =
|
systemd.services.jackett.preStart = shblib.replaceSecrets {
|
||||||
let
|
userConfig = cfg.jackett.settings;
|
||||||
s = cfg.jackett.settings;
|
resultPath = "${config.services.jackett.dataDir}/config.xml";
|
||||||
templatedfileSettings =
|
generator = apps.jackett.settingsFormat.generate;
|
||||||
lib.optionalAttrs (!(isNull s.APIKeyFile)) {
|
};
|
||||||
APIKey = "%APIKEY%";
|
|
||||||
} // lib.optionalAttrs (!(isNull s.OmdbApiKeyFile)) {
|
|
||||||
OmdbApiKey = "%OMDBAPIKEY%";
|
|
||||||
};
|
|
||||||
templatedSettings = (removeAttrs s [ "APIKeyFile" "OmdbApiKeyFile" ]) // templatedfileSettings;
|
|
||||||
|
|
||||||
t = shblib.template (apps.jackett.settingsFormat.generate "jackett.json" templatedSettings) "${config.services.jackett.dataDir}/ServerConfig.json" (
|
shb.nginx.autheliaProtect = [ (autheliaProtect {
|
||||||
lib.optionalAttrs (!(isNull s.APIKeyFile)) {
|
extraBypassResources = [ "^/dl.*" ];
|
||||||
"%APIKEY%" = "$(cat ${s.APIKeyFile})";
|
} config.shb.arr.jackett) ];
|
||||||
} // lib.optionalAttrs (!(isNull s.OmdbApiKeyFile)) {
|
|
||||||
"%OMDBAPIKEY%" = "$(cat ${s.OmdbApiKeyFile})";
|
|
||||||
}
|
|
||||||
);
|
|
||||||
in
|
|
||||||
lib.mkIf cfg.jackett.enable t;
|
|
||||||
|
|
||||||
shb.nginx.autheliaProtect =
|
shb.backup.instances.jackett = cfg.jackett.backupCfg // {
|
||||||
let
|
sourceDirectories = [
|
||||||
appProtectConfig = name: _defaults:
|
config.shb.arr.jackett.dataDir
|
||||||
let
|
];
|
||||||
c = cfg.${name};
|
excludePatterns = [".db-shm" ".db-wal" ".mono"];
|
||||||
in
|
};
|
||||||
lib.mkIf (c.authEndpoint != null) {
|
} // backup "jackett"))
|
||||||
inherit (c) subdomain domain authEndpoint ssl;
|
]);
|
||||||
upstream = "http://127.0.0.1:${toString c.port}";
|
|
||||||
autheliaRules = [
|
|
||||||
{
|
|
||||||
domain = "${c.subdomain}.${c.domain}";
|
|
||||||
policy = "bypass";
|
|
||||||
resources = [
|
|
||||||
"^/api.*"
|
|
||||||
] ++ lib.optionals (name == "jackett") [
|
|
||||||
"^/dl.*"
|
|
||||||
];
|
|
||||||
}
|
|
||||||
{
|
|
||||||
domain = "${c.subdomain}.${c.domain}";
|
|
||||||
policy = "two_factor";
|
|
||||||
subject = ["group:arr_user"];
|
|
||||||
}
|
|
||||||
];
|
|
||||||
};
|
|
||||||
in
|
|
||||||
lib.mapAttrsToList appProtectConfig apps;
|
|
||||||
|
|
||||||
shb.backup.instances =
|
|
||||||
let
|
|
||||||
backupConfig = name: _defaults: lib.mkIf (cfg.${name}.backupCfg.enable or false) ({
|
|
||||||
${name} = (
|
|
||||||
cfg.${name}.backupCfg
|
|
||||||
// {
|
|
||||||
sourceDirectories = [
|
|
||||||
config.shb.arr.${name}.dataDir
|
|
||||||
];
|
|
||||||
excludePatterns = [".db-shm" ".db-wal" ".mono"];
|
|
||||||
});
|
|
||||||
});
|
|
||||||
in
|
|
||||||
lib.mkMerge (lib.mapAttrsToList backupConfig apps);
|
|
||||||
}
|
|
||||||
] ++ map (name: lib.mkIf cfg.${name}.enable {
|
|
||||||
systemd.tmpfiles.rules = lib.mkIf (lib.hasAttr "dataDir" config.services.${name}) [
|
|
||||||
"d '${config.services.${name}.dataDir}' 0750 ${config.services.${name}.user} ${config.services.${name}.group} - -"
|
|
||||||
];
|
|
||||||
users.groups.${name} = {
|
|
||||||
members = [ "backup" ];
|
|
||||||
};
|
|
||||||
systemd.services.${name}.serviceConfig = {
|
|
||||||
# Setup permissions needed for backups, as the backup user is member of the jellyfin group.
|
|
||||||
UMask = lib.mkForce "0027";
|
|
||||||
StateDirectoryMode = lib.mkForce "0750";
|
|
||||||
};
|
|
||||||
}) (lib.attrNames apps));
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,6 +16,7 @@ let
|
||||||
shb.backup = anyOpt {};
|
shb.backup = anyOpt {};
|
||||||
shb.nginx = anyOpt {};
|
shb.nginx = anyOpt {};
|
||||||
users = anyOpt {};
|
users = anyOpt {};
|
||||||
|
services.nginx = anyOpt {};
|
||||||
services.bazarr = anyOpt {};
|
services.bazarr = anyOpt {};
|
||||||
services.jackett = anyOpt {};
|
services.jackett = anyOpt {};
|
||||||
services.lidarr = anyOpt {};
|
services.lidarr = anyOpt {};
|
||||||
|
@ -39,11 +40,11 @@ in
|
||||||
{
|
{
|
||||||
testArrNoOptions = {
|
testArrNoOptions = {
|
||||||
expected = {
|
expected = {
|
||||||
systemd.services.radarr = {};
|
systemd = {};
|
||||||
systemd.services.jackett = {};
|
|
||||||
shb.backup = {};
|
shb.backup = {};
|
||||||
shb.nginx.autheliaProtect = [];
|
shb.nginx = {};
|
||||||
users.users = {};
|
users = {};
|
||||||
|
services.nginx = {};
|
||||||
services.bazarr = {};
|
services.bazarr = {};
|
||||||
services.jackett = {};
|
services.jackett = {};
|
||||||
services.lidarr = {};
|
services.lidarr = {};
|
||||||
|
@ -51,6 +52,7 @@ in
|
||||||
services.readarr = {};
|
services.readarr = {};
|
||||||
services.sonarr = {};
|
services.sonarr = {};
|
||||||
};
|
};
|
||||||
|
|
||||||
expr = testConfig {};
|
expr = testConfig {};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -62,11 +64,19 @@ in
|
||||||
UMask = "0027";
|
UMask = "0027";
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
systemd.services.jackett = {};
|
|
||||||
systemd.tmpfiles.rules = [
|
systemd.tmpfiles.rules = [
|
||||||
"d '/var/lib/radarr' 0750 radarr radarr - -"
|
"d '/var/lib/radarr' 0750 radarr radarr - -"
|
||||||
];
|
];
|
||||||
shb.backup = {};
|
shb.backup.instances.radarr = {
|
||||||
|
excludePatterns = [
|
||||||
|
".db-shm"
|
||||||
|
".db-wal"
|
||||||
|
".mono"
|
||||||
|
];
|
||||||
|
sourceDirectories = [
|
||||||
|
"/var/lib/radarr"
|
||||||
|
];
|
||||||
|
};
|
||||||
shb.nginx.autheliaProtect = [
|
shb.nginx.autheliaProtect = [
|
||||||
{
|
{
|
||||||
autheliaRules = [
|
autheliaRules = [
|
||||||
|
@ -88,12 +98,12 @@ in
|
||||||
domain = "example.com";
|
domain = "example.com";
|
||||||
authEndpoint = "https://oidc.example.com";
|
authEndpoint = "https://oidc.example.com";
|
||||||
subdomain = "radarr";
|
subdomain = "radarr";
|
||||||
upstream = "http://127.0.0.1:7001";
|
upstream = "http://127.0.0.1:7878";
|
||||||
ssl = null;
|
ssl = null;
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
users.users.radarr.extraGroups = [ "media" ];
|
|
||||||
users.groups.radarr.members = [ "backup" ];
|
users.groups.radarr.members = [ "backup" ];
|
||||||
|
services.nginx.enable = true;
|
||||||
services.bazarr = {};
|
services.bazarr = {};
|
||||||
services.jackett = {};
|
services.jackett = {};
|
||||||
services.lidarr = {};
|
services.lidarr = {};
|
||||||
|
@ -130,7 +140,6 @@ in
|
||||||
UMask = "0027";
|
UMask = "0027";
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
systemd.services.jackett = {};
|
|
||||||
systemd.tmpfiles.rules = [
|
systemd.tmpfiles.rules = [
|
||||||
"d '/var/lib/radarr' 0750 radarr radarr - -"
|
"d '/var/lib/radarr' 0750 radarr radarr - -"
|
||||||
];
|
];
|
||||||
|
@ -162,12 +171,12 @@ in
|
||||||
domain = "example.com";
|
domain = "example.com";
|
||||||
authEndpoint = "https://oidc.example.com";
|
authEndpoint = "https://oidc.example.com";
|
||||||
subdomain = "radarr";
|
subdomain = "radarr";
|
||||||
upstream = "http://127.0.0.1:7001";
|
upstream = "http://127.0.0.1:7878";
|
||||||
ssl = null;
|
ssl = null;
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
users.users.radarr.extraGroups = [ "media" ];
|
|
||||||
users.groups.radarr.members = [ "backup" ];
|
users.groups.radarr.members = [ "backup" ];
|
||||||
|
services.nginx.enable = true;
|
||||||
services.bazarr = {};
|
services.bazarr = {};
|
||||||
services.jackett = {};
|
services.jackett = {};
|
||||||
services.lidarr = {};
|
services.lidarr = {};
|
||||||
|
|
86
test/vm/arr.nix
Normal file
86
test/vm/arr.nix
Normal file
|
@ -0,0 +1,86 @@
|
||||||
|
{ pkgs, lib, ... }:
|
||||||
|
let
|
||||||
|
# TODO: Test login
|
||||||
|
commonTestScript = appname: { nodes, ... }:
|
||||||
|
let
|
||||||
|
shbapp = nodes.server.shb.arr.${appname};
|
||||||
|
hasSSL = !(isNull shbapp.ssl);
|
||||||
|
fqdn = if hasSSL then "https://${appname}.example.com" else "http://${appname}.example.com";
|
||||||
|
in
|
||||||
|
''
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import pathlib
|
||||||
|
|
||||||
|
start_all()
|
||||||
|
server.wait_for_unit("${appname}.service")
|
||||||
|
server.wait_for_unit("nginx.service")
|
||||||
|
server.wait_for_open_port(${builtins.toString shbapp.settings.Port})
|
||||||
|
|
||||||
|
if ${if hasSSL then "True" else "False"}:
|
||||||
|
server.copy_from_vm("/etc/ssl/certs/ca-certificates.crt")
|
||||||
|
client.succeed("rm -r /etc/ssl/certs")
|
||||||
|
client.copy_from_host(str(pathlib.Path(os.environ.get("out", os.getcwd())) / "ca-certificates.crt"), "/etc/ssl/certs/ca-certificates.crt")
|
||||||
|
|
||||||
|
def curl(target, format, endpoint, succeed=True):
|
||||||
|
return json.loads(target.succeed(
|
||||||
|
"curl -X GET --fail-with-body --silent --show-error --output /dev/null --location"
|
||||||
|
+ " --connect-to ${appname}.example.com:443:server:443"
|
||||||
|
+ " --connect-to ${appname}.example.com:80:server:80"
|
||||||
|
+ f" --write-out '{format}'"
|
||||||
|
+ " " + endpoint
|
||||||
|
))
|
||||||
|
|
||||||
|
with subtest("health"):
|
||||||
|
response = curl(client, """{"code":%{response_code}}""", "${fqdn}/health")
|
||||||
|
|
||||||
|
if response['code'] != 200:
|
||||||
|
raise Exception(f"Code is {response['code']}")
|
||||||
|
|
||||||
|
with subtest("login"):
|
||||||
|
response = curl(client, """{"code":%{response_code}}""", "${fqdn}/UI/Login")
|
||||||
|
|
||||||
|
if response['code'] != 200:
|
||||||
|
raise Exception(f"Code is {response['code']}")
|
||||||
|
'';
|
||||||
|
|
||||||
|
basic = appname: pkgs.nixosTest {
|
||||||
|
name = "arr-${appname}-basic";
|
||||||
|
|
||||||
|
nodes.server = { config, pkgs, ... }: {
|
||||||
|
imports = [
|
||||||
|
{
|
||||||
|
options = {
|
||||||
|
shb.backup = lib.mkOption { type = lib.types.anything; };
|
||||||
|
};
|
||||||
|
}
|
||||||
|
../../modules/blocks/authelia.nix
|
||||||
|
../../modules/blocks/postgresql.nix
|
||||||
|
../../modules/blocks/nginx.nix
|
||||||
|
../../modules/services/arr.nix
|
||||||
|
];
|
||||||
|
|
||||||
|
shb.arr.${appname} = {
|
||||||
|
enable = true;
|
||||||
|
domain = "example.com";
|
||||||
|
subdomain = appname;
|
||||||
|
|
||||||
|
settings.APIKey.source = pkgs.writeText "APIKey" "01234567890123456789"; # Needs to be >=20 characters.
|
||||||
|
};
|
||||||
|
# Nginx port.
|
||||||
|
networking.firewall.allowedTCPPorts = [ 80 ];
|
||||||
|
};
|
||||||
|
|
||||||
|
nodes.client = {};
|
||||||
|
|
||||||
|
testScript = commonTestScript appname;
|
||||||
|
};
|
||||||
|
in
|
||||||
|
{
|
||||||
|
radarr_basic = basic "radarr";
|
||||||
|
sonarr_basic = basic "sonarr";
|
||||||
|
bazarr_basic = basic "bazarr";
|
||||||
|
readarr_basic = basic "readarr";
|
||||||
|
lidarr_basic = basic "lidarr";
|
||||||
|
jackett_basic = basic "jackett";
|
||||||
|
}
|
Loading…
Reference in a new issue