diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 570f122..2c832a4 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -23,4 +23,5 @@ jobs: run: | nix run github:Mic92/nix-fast-build -- \ --skip-cached --no-nom \ + --max-jobs 2 \ --flake ".#checks.$(nix eval --raw --impure --expr builtins.currentSystem)" diff --git a/flake.nix b/flake.nix index daf887d..3c82ff2 100644 --- a/flake.nix +++ b/flake.nix @@ -101,6 +101,7 @@ tests = pkgs.callPackage ./test/modules/lib.nix {}; }; } + // (vm_test "arr" ./test/vm/arr.nix) // (vm_test "audiobookshelf" ./test/vm/audiobookshelf.nix) // (vm_test "authelia" ./test/vm/authelia.nix) // (vm_test "grocy" ./test/vm/grocy.nix) diff --git a/lib/default.nix b/lib/default.nix index 4846a3e..611e0a8 100644 --- a/lib/default.nix +++ b/lib/default.nix @@ -21,7 +21,7 @@ rec { in '' set -euo pipefail - set -x + mkdir -p $(dirname ${templatePath}) ln -fs ${file} ${templatePath} rm -f ${resultPath} diff --git a/modules/blocks/nginx.nix b/modules/blocks/nginx.nix index fb8f076..5d613e5 100644 --- a/modules/blocks/nginx.nix +++ b/modules/blocks/nginx.nix @@ -27,19 +27,19 @@ let default = null; }; - authEndpoint = lib.mkOption { - type = lib.types.str; - description = "Auth endpoint for SSO."; - default = null; - example = "https://authelia.example.com"; - }; - upstream = lib.mkOption { type = lib.types.str; description = "Upstream url to be protected."; 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 { type = lib.types.listOf (lib.types.attrsOf lib.types.anything); description = "Authelia rule configuration"; @@ -133,6 +133,9 @@ in proxy_set_header Connection "upgrade"; proxy_cache_bypass $http_upgrade; + proxy_pass ${c.upstream}; + '' + + lib.optionalString (!(isNull c.authEndpoint)) '' auth_request /authelia; auth_request_set $user $upstream_http_remote_user; auth_request_set $groups $upstream_http_remote_groups; @@ -153,12 +156,10 @@ in auth_request_set $redirect $scheme://$http_host$request_uri; error_page 401 =302 ${c.authEndpoint}?rd=$redirect; error_page 403 = ${c.authEndpoint}/error/403; - - proxy_pass ${c.upstream}; ''; # Virtual endpoint created by nginx to forward auth requests. - locations."/authelia".extraConfig = '' + locations."/authelia".extraConfig = lib.mkIf (!(isNull c.authEndpoint)) '' internal; proxy_pass ${c.authEndpoint}/api/verify; diff --git a/modules/services/arr.nix b/modules/services/arr.nix index c55f669..caf61cc 100644 --- a/modules/services/arr.nix +++ b/modules/services/arr.nix @@ -8,7 +8,6 @@ let apps = { radarr = { - defaultPort = 7001; settingsFormat = formatXML {}; moreOptions = { settings = lib.mkOption { @@ -17,8 +16,8 @@ let type = lib.types.submodule { freeformType = apps.radarr.settingsFormat.type; options = { - APIKeyFile = lib.mkOption { - type = lib.types.path; + APIKey = lib.mkOption { + type = shblib.secretFileType; description = "Path to api key secret file."; }; LogLevel = lib.mkOption { @@ -26,25 +25,173 @@ let description = "Log level."; 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 = { - 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 = { - 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 = { - 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 = { - 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 = { - defaultPort = 9117; settingsFormat = pkgs.formats.json {}; moreOptions = { settings = lib.mkOption { @@ -53,8 +200,8 @@ let type = lib.types.submodule { freeformType = apps.jackett.settingsFormat.type; options = { - APIKeyFile = lib.mkOption { - type = lib.types.path; + APIKey = lib.mkOption { + type = shblib.secretFileType; description = "Path to api key secret file."; }; FlareSolverrUrl = lib.mkOption { @@ -62,8 +209,8 @@ let description = "FlareSolverr endpoint."; default = null; }; - OmdbApiKeyFile = lib.mkOption { - type = lib.types.nullOr lib.types.path; + OmdbApiKey = lib.mkOption { + type = lib.types.nullOr shblib.secretFileType; description = "File containing the Open Movie Database API key."; default = null; }; @@ -87,6 +234,22 @@ let description = "Port of the proxy. Ignored if ProxyType is set to -1"; 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 = {}: { type = with lib.types; let valueType = nullOr (oneOf [ @@ -109,7 +306,7 @@ let }; 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;}; passAsFile = [ "value" ]; } (pkgs.writers.writePython3 "dict2xml" { @@ -126,7 +323,7 @@ let os.exit(2) with open(os.environ["out"], "w") as out: out.write(dict2xml(content)) - '')) {}; + '')) {}); }; @@ -149,24 +346,18 @@ let 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 { type = lib.types.str; description = "Directory where ${name} stores data."; default = "/var/lib/${name}"; }; + ssl = lib.mkOption { + description = "Path to SSL files"; + type = lib.types.nullOr contracts.ssl.certs; + default = null; + }; + authEndpoint = lib.mkOption { type = lib.types.nullOr lib.types.str; default = null; @@ -191,164 +382,156 @@ in options.shb.arr = lib.listToAttrs (lib.mapAttrsToList appOption apps); config = lib.mkMerge ([ - { - services.radarr = lib.mkIf cfg.radarr.enable { + (lib.mkIf cfg.radarr.enable ({ + services.nginx.enable = true; + + services.radarr = { enable = true; dataDir = "/var/lib/radarr"; }; - users.users.radarr = lib.mkIf cfg.radarr.enable { + + users.users.radarr = { extraGroups = [ "media" ]; }; - shb.arr.radarr.settings = lib.mkIf cfg.radarr.enable { - Port = config.shb.arr.radarr.port; - BindAddress = "127.0.0.1"; - UrlBase = ""; - EnableSsl = "false"; - AuthenticationMethod = "External"; - AuthenticationRequired = "Enabled"; + + systemd.services.radarr.preStart = shblib.replaceSecrets { + userConfig = cfg.radarr.settings; + resultPath = "${config.services.radarr.dataDir}/config.xml"; + generator = apps.radarr.settingsFormat.generate; }; - 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 " -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; + shb.nginx.autheliaProtect = [ (autheliaProtect {} config.shb.arr.radarr) ]; - # Listens on port 8989 - services.sonarr = lib.mkIf cfg.sonarr.enable { + shb.backup.instances.radarr = cfg.radarr.backupCfg // { + 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; dataDir = "/var/lib/sonarr"; }; - users.users.sonarr = lib.mkIf cfg.sonarr.enable { + users.users.sonarr = { 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; - listenPort = cfg.bazarr.port; + listenPort = cfg.bazarr.settings.Port; }; - users.users.bazarr = lib.mkIf cfg.bazarr.enable { + users.users.bazarr = { 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 - services.readarr = lib.mkIf cfg.readarr.enable { + shb.nginx.autheliaProtect = [ (autheliaProtect {} config.shb.arr.bazarr) ]; + + 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; dataDir = "/var/lib/readarr"; }; - users.users.readarr = lib.mkIf cfg.readarr.enable { + users.users.readarr = { 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 - services.lidarr = lib.mkIf cfg.lidarr.enable { + shb.nginx.autheliaProtect = [ (autheliaProtect {} config.shb.arr.readarr) ]; + + 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; dataDir = "/var/lib/lidarr"; }; - users.users.lidarr = lib.mkIf cfg.lidarr.enable { + users.users.lidarr = { 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 - services.jackett = lib.mkIf cfg.jackett.enable { + shb.nginx.autheliaProtect = [ (autheliaProtect {} config.shb.arr.lidarr) ]; + + 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; dataDir = "/var/lib/jackett"; }; - shb.arr.jackett.settings = lib.mkIf cfg.jackett.enable { - Port = config.shb.arr.jackett.port; - AllowExternal = "false"; - UpdateDisabled = "true"; - }; - users.users.jackett = lib.mkIf cfg.jackett.enable { + users.users.jackett = { extraGroups = [ "media" ]; }; - systemd.services.jackett.preStart = - let - s = cfg.jackett.settings; - templatedfileSettings = - lib.optionalAttrs (!(isNull s.APIKeyFile)) { - APIKey = "%APIKEY%"; - } // lib.optionalAttrs (!(isNull s.OmdbApiKeyFile)) { - OmdbApiKey = "%OMDBAPIKEY%"; - }; - templatedSettings = (removeAttrs s [ "APIKeyFile" "OmdbApiKeyFile" ]) // templatedfileSettings; + systemd.services.jackett.preStart = shblib.replaceSecrets { + userConfig = cfg.jackett.settings; + resultPath = "${config.services.jackett.dataDir}/config.xml"; + generator = apps.jackett.settingsFormat.generate; + }; - t = shblib.template (apps.jackett.settingsFormat.generate "jackett.json" templatedSettings) "${config.services.jackett.dataDir}/ServerConfig.json" ( - lib.optionalAttrs (!(isNull s.APIKeyFile)) { - "%APIKEY%" = "$(cat ${s.APIKeyFile})"; - } // lib.optionalAttrs (!(isNull s.OmdbApiKeyFile)) { - "%OMDBAPIKEY%" = "$(cat ${s.OmdbApiKeyFile})"; - } - ); - in - lib.mkIf cfg.jackett.enable t; + shb.nginx.autheliaProtect = [ (autheliaProtect { + extraBypassResources = [ "^/dl.*" ]; + } config.shb.arr.jackett) ]; - shb.nginx.autheliaProtect = - let - appProtectConfig = name: _defaults: - let - c = cfg.${name}; - in - lib.mkIf (c.authEndpoint != null) { - 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)); + shb.backup.instances.jackett = cfg.jackett.backupCfg // { + sourceDirectories = [ + config.shb.arr.jackett.dataDir + ]; + excludePatterns = [".db-shm" ".db-wal" ".mono"]; + }; + } // backup "jackett")) + ]); } diff --git a/test/modules/arr.nix b/test/modules/arr.nix index 4095992..cb4ea2f 100644 --- a/test/modules/arr.nix +++ b/test/modules/arr.nix @@ -16,6 +16,7 @@ let shb.backup = anyOpt {}; shb.nginx = anyOpt {}; users = anyOpt {}; + services.nginx = anyOpt {}; services.bazarr = anyOpt {}; services.jackett = anyOpt {}; services.lidarr = anyOpt {}; @@ -39,11 +40,11 @@ in { testArrNoOptions = { expected = { - systemd.services.radarr = {}; - systemd.services.jackett = {}; + systemd = {}; shb.backup = {}; - shb.nginx.autheliaProtect = []; - users.users = {}; + shb.nginx = {}; + users = {}; + services.nginx = {}; services.bazarr = {}; services.jackett = {}; services.lidarr = {}; @@ -51,6 +52,7 @@ in services.readarr = {}; services.sonarr = {}; }; + expr = testConfig {}; }; @@ -62,11 +64,19 @@ in UMask = "0027"; }; }; - systemd.services.jackett = {}; systemd.tmpfiles.rules = [ "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 = [ { autheliaRules = [ @@ -88,12 +98,12 @@ in domain = "example.com"; authEndpoint = "https://oidc.example.com"; subdomain = "radarr"; - upstream = "http://127.0.0.1:7001"; + upstream = "http://127.0.0.1:7878"; ssl = null; } ]; - users.users.radarr.extraGroups = [ "media" ]; users.groups.radarr.members = [ "backup" ]; + services.nginx.enable = true; services.bazarr = {}; services.jackett = {}; services.lidarr = {}; @@ -130,7 +140,6 @@ in UMask = "0027"; }; }; - systemd.services.jackett = {}; systemd.tmpfiles.rules = [ "d '/var/lib/radarr' 0750 radarr radarr - -" ]; @@ -162,12 +171,12 @@ in domain = "example.com"; authEndpoint = "https://oidc.example.com"; subdomain = "radarr"; - upstream = "http://127.0.0.1:7001"; + upstream = "http://127.0.0.1:7878"; ssl = null; } ]; - users.users.radarr.extraGroups = [ "media" ]; users.groups.radarr.members = [ "backup" ]; + services.nginx.enable = true; services.bazarr = {}; services.jackett = {}; services.lidarr = {}; diff --git a/test/vm/arr.nix b/test/vm/arr.nix new file mode 100644 index 0000000..d1c4678 --- /dev/null +++ b/test/vm/arr.nix @@ -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"; +}