diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f0825bf4..21c2a821 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -7,14 +7,60 @@ on: - main jobs: - check: + prepare: + name: Find Checks 🔍 runs-on: ubuntu-latest + outputs: + checks: ${{ steps.checks.outputs.checks }} steps: - uses: actions/checkout@v4 - - uses: DeterminateSystems/nix-installer-action@main + - name: Install Nix + uses: cachix/install-nix-action@v31 + with: + nix_path: nixpkgs=channel:nixos-unstable - - uses: DeterminateSystems/magic-nix-cache-action@main + - name: Setup Cachix + uses: cachix/cachix-action@v16 + with: + name: kiriwalawren + authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}" - - name: Run flake checks - run: nix flake check --all-systems + - name: Find Checks 🔍 + id: checks + run: | + nix eval --json '.#checks.x86_64-linux' --apply builtins.attrNames | perl -pe 's|(.*)|checks=\1|' >> $GITHUB_OUTPUT + + checks: + name: ${{ matrix.check }} + needs: prepare + runs-on: ubuntu-latest + timeout-minutes: 20 + strategy: + fail-fast: false + matrix: + check: ${{ fromJSON(needs.prepare.outputs.checks) }} + steps: + - uses: actions/checkout@v4 + + - name: Install Nix + uses: cachix/install-nix-action@v31 + with: + nix_path: nixpkgs=channel:nixos-unstable + + - name: Setup Cachix + uses: cachix/cachix-action@v16 + with: + name: kiriwalawren + authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}" + + # Enable KVM for VM tests + - name: Enable KVM group perms + run: | + echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules + sudo udevadm control --reload-rules + sudo udevadm trigger --name-match=kvm + + - name: Run Check 📋 + run: | + nix build -L .#checks.x86_64-linux.${{ matrix.check }} diff --git a/.github/workflows/update-flake.yml b/.github/workflows/update-flake.yml index 89901c23..96bb5413 100644 --- a/.github/workflows/update-flake.yml +++ b/.github/workflows/update-flake.yml @@ -12,9 +12,16 @@ jobs: steps: - uses: actions/checkout@v4 - - uses: DeterminateSystems/nix-installer-action@main + - name: Install Nix + uses: cachix/install-nix-action@v31 + with: + nix_path: nixpkgs=channel:nixos-unstable - - uses: DeterminateSystems/magic-nix-cache-action@main + - name: Setup Cachix + uses: cachix/cachix-action@v16 + with: + name: kiriwalawren + authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}" - name: Update flake.lock run: nix flake update diff --git a/flake.nix b/flake.nix index 4b551c29..89341ed4 100644 --- a/flake.nix +++ b/flake.nix @@ -25,17 +25,24 @@ checks = forAllSystems (system: let pkgs = nixpkgs.legacyPackages.${system}; - in { - format = pkgs.runCommand "check-format" {} '' - ${pkgs.alejandra}/bin/alejandra --check ${./.} - touch $out - ''; + tests = import ./tests { + inherit system pkgs; + nixosModules = self.nixosModules.default; + }; + in + { + format = pkgs.runCommand "check-format" {} '' + ${pkgs.alejandra}/bin/alejandra --check ${./.} + touch $out + ''; - statix = pkgs.runCommand "check-statix" {} '' - ${pkgs.statix}/bin/statix check ${./.} - touch $out - ''; - }); + statix = pkgs.runCommand "check-statix" {} '' + ${pkgs.statix}/bin/statix check ${./.} + touch $out + ''; + } + // tests.vm-tests + // tests.unit-tests); devShells = forAllSystems (system: let pkgs = nixpkgs.legacyPackages.${system}; diff --git a/modules/arr-common/configModule.nix b/modules/arr-common/configModule.nix index 33a714fd..4eecfab6 100644 --- a/modules/arr-common/configModule.nix +++ b/modules/arr-common/configModule.nix @@ -1,4 +1,4 @@ -extraConfigOptions: {lib}: +{lib}: extraConfigOptions: with lib; types.submodule { options = @@ -20,18 +20,6 @@ with lib; default = {}; description = "Host configuration options that will be set via the API /config/host endpoint"; }; - - rootFolders = mkOption { - type = types.listOf types.attrs; - default = []; - description = '' - List of root folders to create via the API /rootfolder endpoint. - Each folder is an attribute set that will be converted to JSON and sent to the API. - - For Sonarr/Radarr, a simple path is sufficient: {path = "/path/to/folder";} - For Lidarr, additional fields are required like defaultQualityProfileId, etc. - ''; - }; } // extraConfigOptions; } diff --git a/modules/arr-common/default.nix b/modules/arr-common/default.nix new file mode 100644 index 00000000..3dffb362 --- /dev/null +++ b/modules/arr-common/default.nix @@ -0,0 +1,12 @@ +{ + config, + lib, + pkgs, + usesDynamicUser ? false, + ... +}: { + arrConfigModule = import ./configModule.nix {inherit lib;}; + mkArrHostConfigService = import ./hostConfigService.nix {inherit lib pkgs;}; + mkArrRootFoldersService = import ./rootFoldersService.nix {inherit lib pkgs;}; + mkArrServiceModule = import ../arr-common/mkArrServiceModule.nix {inherit config lib pkgs usesDynamicUser;}; +} diff --git a/modules/arr-common/hostConfigService.nix b/modules/arr-common/hostConfigService.nix index c2d50e98..53895cca 100644 --- a/modules/arr-common/hostConfigService.nix +++ b/modules/arr-common/hostConfigService.nix @@ -4,7 +4,10 @@ }: # Helper function to create a systemd service that configures *arr basic settings via API serviceName: serviceConfig: -with lib; { +with lib; let + mkWaitForApiScript = import ./mkWaitForApiScript.nix {inherit lib pkgs;}; + capitalizedName = lib.toUpper (builtins.substring 0 1 serviceName) + builtins.substring 1 (-1) serviceName; +in { description = "Configure ${serviceName} via API"; after = ["${serviceName}.service"]; wantedBy = ["multi-user.target"]; @@ -12,11 +15,10 @@ with lib; { serviceConfig = { Type = "oneshot"; RemainAfterExit = true; + ExecStartPre = mkWaitForApiScript serviceName serviceConfig; }; - script = let - capitalizedName = lib.toUpper (builtins.substring 0 1 serviceName) + builtins.substring 1 (-1) serviceName; - in '' + script = '' set -eu # Read secrets @@ -25,17 +27,6 @@ with lib; { BASE_URL="http://127.0.0.1:${builtins.toString serviceConfig.hostConfig.port}${serviceConfig.hostConfig.urlBase}/api/${serviceConfig.apiVersion}" - # Wait for API to be available (up to 60 seconds) - echo "Waiting for ${capitalizedName} API to be available..." - for i in {1..60}; do - if ${pkgs.curl}/bin/curl -s -f "$BASE_URL/system/status?apiKey=$API_KEY" >/dev/null 2>&1; then - echo "${capitalizedName} API is available" - break - fi - echo "Waiting for ${capitalizedName} API... ($i/60)" - sleep 1 - done - # Get current host configuration echo "Fetching current host configuration..." HOST_CONFIG=$(${pkgs.curl}/bin/curl -s -f -H "X-Api-Key: $API_KEY" "$BASE_URL/config/host" 2>/dev/null) diff --git a/modules/arr-common/mkArrServiceModule.nix b/modules/arr-common/mkArrServiceModule.nix index 5f23be7d..d28d326e 100644 --- a/modules/arr-common/mkArrServiceModule.nix +++ b/modules/arr-common/mkArrServiceModule.nix @@ -1,230 +1,294 @@ -serviceName: extraConfigOptions: { +{ config, lib, pkgs, ... -}: +}: serviceName: extraConfigOptions: with lib; let - arrConfigModule = import ./configModule.nix extraConfigOptions {inherit lib;}; + inherit (config) nixflix; + inherit (nixflix) globals; + cfg = config.nixflix.${serviceName}; + stateDir = "${nixflix.stateDir}/${serviceName}"; + + arrConfigModule = import ./configModule.nix {inherit lib;}; mkArrHostConfigService = import ./hostConfigService.nix {inherit lib pkgs;}; mkArrRootFoldersService = import ./rootFoldersService.nix {inherit lib pkgs;}; capitalizedName = toUpper (substring 0 1 serviceName) + substring 1 (-1) serviceName; + usesDynamicUser = elem serviceName ["prowlarr"]; + usesMediaDirs = !(elem serviceName ["prowlarr"]); + effectiveUser = + if usesDynamicUser + then serviceName + else cfg.user; in { - options.nixflix.${serviceName} = { - enable = mkEnableOption "${capitalizedName}"; - - usesDynamicUser = mkOption { - type = types.bool; - default = false; - description = "Whether the service uses systemd DynamicUser"; - }; + options.nixflix.${serviceName} = + { + enable = mkEnableOption "${capitalizedName}"; - group = mkOption { - type = types.str; - default = serviceName; - description = "Group under which the service runs"; - }; + config = mkOption { + type = + arrConfigModule + (extraConfigOptions + // optionalAttrs usesMediaDirs { + rootFolders = mkOption { + type = types.listOf types.attrs; + default = []; + description = '' + List of root folders to create via the API /rootfolder endpoint. + Each folder is an attribute set that will be converted to JSON and sent to the API. - user = mkOption { - type = types.str; - default = serviceName; - description = "User under which the service runs"; - }; + For Sonarr/Radarr, a simple path is sufficient: {path = "/path/to/folder";} + For Lidarr, additional fields are required like defaultQualityProfileId, etc. + ''; + }; + }); + default = {}; + description = "${capitalizedName} configuration options that will be set via the API."; + }; + } + // optionalAttrs (!usesDynamicUser) { + group = mkOption { + type = types.str; + default = serviceName; + description = "Group under which the service runs"; + }; - mediaDirs = mkOption { - type = types.listOf (types.submodule { - options = { - dir = mkOption { - type = types.str; - description = "Directory path"; - }; - owner = mkOption { - type = types.str; - default = "root"; - description = "Directory owner"; + user = mkOption { + type = types.str; + default = serviceName; + description = "User under which the service runs"; + }; + } + // optionalAttrs usesMediaDirs { + mediaDirs = mkOption { + type = types.listOf (types.submodule { + options = { + dir = mkOption { + type = types.str; + description = "Directory path"; + }; + owner = mkOption { + type = types.str; + default = "root"; + description = "Directory owner"; + }; }; - }; - }); - default = []; - description = "List of media directories to create and manage"; - }; - - config = mkOption { - type = arrConfigModule; - default = {}; - description = "${capitalizedName} configuration options that will be set via the API."; + }); + default = []; + description = "List of media directories to create and manage"; + }; }; - }; - config = let - inherit (config) nixflix; - inherit (nixflix) globals; - cfg = config.nixflix.${serviceName}; - stateDir = "${nixflix.stateDir}/${serviceName}"; - in - mkIf (nixflix.enable && cfg.enable) { - # Set pattern-based defaults - nixflix.${serviceName}.config = { - apiKeyPath = mkDefault null; - hostConfig = { - username = mkDefault serviceName; - passwordPath = mkDefault null; - instanceName = mkDefault capitalizedName; - urlBase = mkDefault ( - if nixflix.serviceNameIsUrlBase - then "/${serviceName}" - else "" - ); - }; + config = mkIf (nixflix.enable && cfg.enable) { + # Set pattern-based defaults + nixflix.${serviceName}.config = { + apiKeyPath = mkDefault null; + hostConfig = { + username = mkDefault serviceName; + passwordPath = mkDefault null; + instanceName = mkDefault capitalizedName; + urlBase = mkDefault ( + if nixflix.serviceNameIsUrlBase + then "/${serviceName}" + else "" + ); }; + }; - # Register directories to be created - nixflix.dirRegistrations = - [ - ( - if cfg.usesDynamicUser - then { - dir = stateDir; - owner = "root"; - group = "root"; - mode = "0700"; - } - else { - inherit (cfg) group; - dir = stateDir; - owner = cfg.user; - } - ) - ] - ++ (map (mediaDir: { + # Register directories to be created + nixflix.dirRegistrations = + [ + ( + if usesDynamicUser + then { + dir = stateDir; + owner = "root"; + group = "root"; + mode = "0700"; + } + else { inherit (cfg) group; - inherit (mediaDir) dir owner; - }) - cfg.mediaDirs); - - services = { - ${serviceName} = - { - inherit (cfg) enable; - dataDir = stateDir; - settings = - { - auth = { - required = "Enabled"; - method = "Forms"; - }; - server = {inherit (cfg.config.hostConfig) port urlBase;}; - } - // optionalAttrs config.services.postgresql.enable { - postgres = { - inherit (cfg) user; - host = "/run/postgresql"; - port = 5432; - mainDb = cfg.user; - logDb = cfg.user; - }; - }; + dir = stateDir; + owner = cfg.user; } - // optionalAttrs (!cfg.usesDynamicUser) { - inherit (cfg) user group; - }; + ) + ] + ++ optionals usesMediaDirs (map (mediaDir: { + inherit (cfg) group; + inherit (mediaDir) dir owner; + }) + cfg.mediaDirs); - postgresql = mkIf config.services.postgresql.enable { - ensureDatabases = [cfg.user]; - ensureUsers = [ + services = { + ${serviceName} = + { + inherit (cfg) enable; + dataDir = stateDir; + settings = { - name = cfg.user; - ensureDBOwnership = true; + auth = { + required = "Enabled"; + method = "Forms"; + }; + server = {inherit (cfg.config.hostConfig) port urlBase;}; } - ]; + // optionalAttrs config.services.postgresql.enable { + log.dbEnabled = true; + postgres = { + user = effectiveUser; + host = "/run/postgresql"; + port = 5432; + mainDb = effectiveUser; + logDb = effectiveUser; + }; + }; + } + // optionalAttrs (!usesDynamicUser) { + inherit (cfg) user group; }; - nginx = mkIf nixflix.nginx.enable { - virtualHosts.localhost.locations."${ - if cfg.config.hostConfig.urlBase == "" - then "/" - else cfg.config.hostConfig.urlBase - }" = { - proxyPass = "http://127.0.0.1:${builtins.toString cfg.config.hostConfig.port}"; - recommendedProxySettings = true; - extraConfig = '' - proxy_set_header X-Forwarded-Host $host; - proxy_set_header X-Forwarded-Server $host; - proxy_set_header X-Forwarded-Proto $scheme; - proxy_redirect off; - ''; - }; - }; + postgresql = mkIf config.services.postgresql.enable { + ensureDatabases = [effectiveUser]; + ensureUsers = [ + { + name = effectiveUser; + ensureDBOwnership = true; + } + ]; }; - users = mkIf (!cfg.usesDynamicUser) { - groups.${cfg.group} = optionalAttrs (globals.gids ? ${cfg.group}) { - gid = globals.gids.${cfg.group}; + nginx = mkIf nixflix.nginx.enable { + virtualHosts.localhost.locations."${ + if cfg.config.hostConfig.urlBase == "" + then "/" + else cfg.config.hostConfig.urlBase + }" = { + proxyPass = "http://127.0.0.1:${builtins.toString cfg.config.hostConfig.port}"; + recommendedProxySettings = true; + extraConfig = '' + proxy_set_header X-Forwarded-Host $host; + proxy_set_header X-Forwarded-Server $host; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_redirect off; + ''; }; - users.${cfg.user} = - { - inherit (cfg) group; - isSystemUser = true; - } - // optionalAttrs (globals.uids ? ${cfg.user}) { - uid = globals.uids.${cfg.user}; - }; }; + }; - systemd.services = + users = mkIf (!usesDynamicUser) { + groups.${cfg.group} = optionalAttrs (globals.gids ? ${cfg.group}) { + gid = globals.gids.${cfg.group}; + }; + users.${cfg.user} = { - # Ensure main service (radarr.service, etc.) starts after - # directories are created and configured dependencies - ${serviceName} = { - after = - ["nixflix-setup-dirs.service"] - ++ (optional config.services.postgresql.enable "postgresql.service") - ++ (optional config.nixflix.mullvad.enable "mullvad-config.service"); - requires = - ["nixflix-setup-dirs.service"] - ++ (optional config.services.postgresql.enable "postgresql.service"); - wants = optional config.nixflix.mullvad.enable "mullvad-config.service"; - }; + inherit (cfg) group; + isSystemUser = true; } - # Only create config and rootfolders services if apiKeyPath is configured - // optionalAttrs (cfg.config.apiKeyPath != null && cfg.config.hostConfig.passwordPath != null) { - # Create environment file setup service - "${serviceName}-env" = { - description = "Setup ${capitalizedName} environment file"; - wantedBy = ["${serviceName}.service"]; - before = ["${serviceName}.service"]; - - serviceConfig = { + // optionalAttrs (globals.uids ? ${cfg.user}) { + uid = globals.uids.${cfg.user}; + }; + }; + + systemd.services = + { + # Service that waits for PostgreSQL database role to be ready + "${serviceName}-wait-for-db" = mkIf config.services.postgresql.enable { + description = "Wait for ${capitalizedName} PostgreSQL database role to be ready"; + after = ["postgresql.service" "postgresql-setup.service"]; + before = ["postgresql-ready.target"]; + requiredBy = ["postgresql-ready.target"]; + + serviceConfig = + { Type = "oneshot"; RemainAfterExit = true; + TimeoutStartSec = "5min"; + } + // optionalAttrs (!usesDynamicUser) { + User = cfg.user; + Group = cfg.group; }; - script = let - envVar = toUpper serviceName + "__AUTH__APIKEY"; - in '' - mkdir -p /run/${serviceName} - echo "${envVar}=$(cat ${cfg.config.apiKeyPath})" > /run/${serviceName}/env - ${optionalString (!cfg.usesDynamicUser) "chown ${cfg.user}:${cfg.group} /run/${serviceName}/env"} - chmod 0${ - if cfg.usesDynamicUser - then "444" - else "400" - } /run/${serviceName}/env - ''; - }; + script = let + dbUser = effectiveUser; + psqlCmd = + if usesDynamicUser + then "${pkgs.sudo}/bin/sudo -u postgres ${pkgs.postgresql}/bin/psql" + else "${pkgs.postgresql}/bin/psql -h /run/postgresql"; + checkCmd = + if usesDynamicUser + then "SELECT 1 FROM pg_database WHERE datname='${dbUser}'" + else "SELECT 1"; + in '' + while true; do + if ${psqlCmd} -d ${dbUser} -c "${checkCmd}" > /dev/null 2>&1; then + echo "${capitalizedName} PostgreSQL database is ready" + exit 0 + fi + echo "Waiting for PostgreSQL role ${capitalizedName}..." + sleep 1 + done + ''; + }; + + # Ensure main service (radarr.service, etc.) starts after + # directories are created and configured dependencies + ${serviceName} = { + after = + ["nixflix-setup-dirs.service"] + ++ (optional config.services.postgresql.enable "postgresql-ready.target") + ++ (optional config.nixflix.mullvad.enable "mullvad-config.service"); + requires = + ["nixflix-setup-dirs.service"] + ++ (optional config.services.postgresql.enable "postgresql-ready.target"); + wants = optional config.nixflix.mullvad.enable "mullvad-config.service"; + }; + } + # Only create config and rootfolders services if apiKeyPath is configured + // optionalAttrs (cfg.config.apiKeyPath != null && cfg.config.hostConfig.passwordPath != null) { + # Create environment file setup service + "${serviceName}-env" = { + description = "Setup ${capitalizedName} environment file"; + wantedBy = ["${serviceName}.service"]; + before = ["${serviceName}.service"]; - ${serviceName} = { - after = ["${serviceName}-env.service"]; - requires = ["${serviceName}-env.service"]; - serviceConfig.EnvironmentFile = "/run/${serviceName}/env"; + serviceConfig = { + Type = "oneshot"; + RemainAfterExit = true; }; - # Configure service via API - "${serviceName}-config" = mkArrHostConfigService serviceName cfg.config; - } - # Only create root folders service if rootFolders is not empty - // optionalAttrs (cfg.config.apiKeyPath != null && cfg.config.rootFolders != []) { - "${serviceName}-rootfolders" = mkArrRootFoldersService serviceName cfg.config; + script = let + envVar = toUpper serviceName + "__AUTH__APIKEY"; + in '' + mkdir -p /run/${serviceName} + echo "${envVar}=$(cat ${cfg.config.apiKeyPath})" > /run/${serviceName}/env + ${optionalString (!usesDynamicUser) "chown ${cfg.user}:${cfg.group} /run/${serviceName}/env"} + chmod 0${ + if usesDynamicUser + then "444" + else "400" + } /run/${serviceName}/env + ''; }; - }; + + ${serviceName} = { + after = + ["${serviceName}-env.service" "nixflix-setup-dirs.service"] + ++ (optional config.services.postgresql.enable "postgresql-ready.target") + ++ (optional config.nixflix.mullvad.enable "mullvad-config.service"); + requires = + ["${serviceName}-env.service" "nixflix-setup-dirs.service"] + ++ (optional config.services.postgresql.enable "postgresql-ready.target"); + wants = optional config.nixflix.mullvad.enable "mullvad-config.service"; + serviceConfig.EnvironmentFile = "/run/${serviceName}/env"; + }; + + # Configure service via API + "${serviceName}-config" = mkArrHostConfigService serviceName cfg.config; + } + # Only create root folders service if rootFolders is not empty + // optionalAttrs (usesMediaDirs && cfg.config.apiKeyPath != null && cfg.config.rootFolders != []) { + "${serviceName}-rootfolders" = mkArrRootFoldersService serviceName cfg.config; + }; + }; } diff --git a/modules/arr-common/mkWaitForApiScript.nix b/modules/arr-common/mkWaitForApiScript.nix new file mode 100644 index 00000000..d80dc971 --- /dev/null +++ b/modules/arr-common/mkWaitForApiScript.nix @@ -0,0 +1,25 @@ +{ + lib, + pkgs, +}: +# Helper function to create a script that waits for an *arr service API to be ready +serviceName: serviceConfig: +pkgs.writeShellScript "${serviceName}-wait-for-api" (let + capitalizedName = lib.toUpper (builtins.substring 0 1 serviceName) + builtins.substring 1 (-1) serviceName; +in '' + API_KEY=$(cat ${serviceConfig.apiKeyPath}) + BASE_URL="http://127.0.0.1:${builtins.toString serviceConfig.hostConfig.port}${serviceConfig.hostConfig.urlBase}/api/${serviceConfig.apiVersion}" + + echo "Waiting for ${capitalizedName} API to be available..." + for i in {1..90}; do + if ${pkgs.curl}/bin/curl -s -f "$BASE_URL/system/status?apiKey=$API_KEY" >/dev/null 2>&1; then + echo "${capitalizedName} API is available" + exit 0 + fi + echo "Waiting for ${capitalizedName} API... ($i/90)" + sleep 1 + done + + echo "${capitalizedName} API not available after 90 seconds" >&2 + exit 1 +'') diff --git a/modules/arr-common/rootFoldersService.nix b/modules/arr-common/rootFoldersService.nix index 048bce9c..b39a3b91 100644 --- a/modules/arr-common/rootFoldersService.nix +++ b/modules/arr-common/rootFoldersService.nix @@ -4,7 +4,10 @@ }: # Helper function to create a systemd service that configures *arr root folders via API serviceName: serviceConfig: -with lib; { +with lib; let + mkWaitForApiScript = import ./mkWaitForApiScript.nix {inherit lib pkgs;}; + capitalizedName = lib.toUpper (builtins.substring 0 1 serviceName) + builtins.substring 1 (-1) serviceName; +in { description = "Configure ${serviceName} root folders via API"; after = ["${serviceName}-config.service"]; wantedBy = ["multi-user.target"]; @@ -12,11 +15,10 @@ with lib; { serviceConfig = { Type = "oneshot"; RemainAfterExit = true; + ExecStartPre = mkWaitForApiScript serviceName serviceConfig; }; - script = let - capitalizedName = lib.toUpper (builtins.substring 0 1 serviceName) + builtins.substring 1 (-1) serviceName; - in '' + script = '' set -eu # Read API key secret @@ -24,17 +26,6 @@ with lib; { BASE_URL="http://127.0.0.1:${builtins.toString serviceConfig.hostConfig.port}${serviceConfig.hostConfig.urlBase}/api/${serviceConfig.apiVersion}" - # Wait for API to be available (up to 60 seconds) - echo "Waiting for ${capitalizedName} API to be available..." - for i in {1..60}; do - if ${pkgs.curl}/bin/curl -sSf "$BASE_URL/system/status?apiKey=$API_KEY" >/dev/null 2>&1; then - echo "${capitalizedName} API is available" - break - fi - echo "Waiting for ${capitalizedName} API... ($i/60)" - sleep 1 - done - # Create root folders if they don't exist echo "Checking for root folders..." ROOT_FOLDERS=$(${pkgs.curl}/bin/curl -sSf -H "X-Api-Key: $API_KEY" "$BASE_URL/rootfolder" 2>/dev/null) diff --git a/modules/lidarr/default.nix b/modules/lidarr/default.nix index 2cc78f3c..851a67d5 100644 --- a/modules/lidarr/default.nix +++ b/modules/lidarr/default.nix @@ -6,8 +6,9 @@ }: let inherit (config) nixflix; cfg = config.nixflix.lidarr; + arrCommon = import ../arr-common {inherit config lib pkgs;}; in { - imports = [(import ../arr-common/mkArrServiceModule.nix "lidarr" {} {inherit config lib pkgs;})]; + imports = [(arrCommon.mkArrServiceModule "lidarr" {})]; config.nixflix.lidarr = { group = lib.mkDefault "media"; diff --git a/modules/nixflix.nix b/modules/nixflix.nix index c6838d25..d123c39e 100644 --- a/modules/nixflix.nix +++ b/modules/nixflix.nix @@ -73,8 +73,8 @@ in { stateDir = mkOption { type = types.path; - default = "/data/.state/services"; - example = "/data/.state/services"; + default = "/data/.state"; + example = "/data/.state"; description = '' The location of the state directory for the services. diff --git a/modules/postgres.nix b/modules/postgres.nix index 34fceb45..33d6c0b6 100644 --- a/modules/postgres.nix +++ b/modules/postgres.nix @@ -39,5 +39,11 @@ in { after = ["nixflix-setup-dirs.service"]; requires = ["nixflix-setup-dirs.service"]; }; + + systemd.targets.postgresql-ready = { + after = ["postgresql.service" "postgresql-setup.service"]; + requires = ["postgresql.service" "postgresql-setup.service"]; + wantedBy = ["multi-user.target"]; + }; }; } diff --git a/modules/prowlarr/default.nix b/modules/prowlarr/default.nix index 436cfefb..ddb229da 100644 --- a/modules/prowlarr/default.nix +++ b/modules/prowlarr/default.nix @@ -6,6 +6,9 @@ }: with lib; let mkProwlarrIndexersService = import ./indexersService.nix {inherit lib pkgs;}; + arrCommon = import ../arr-common { + inherit config lib pkgs; + }; extraConfigOptions = { indexers = mkOption { @@ -35,11 +38,10 @@ with lib; let }; }; in { - imports = [(import ../arr-common/mkArrServiceModule.nix "prowlarr" extraConfigOptions {inherit config lib pkgs;})]; + imports = [(arrCommon.mkArrServiceModule "prowlarr" extraConfigOptions)]; config = { nixflix.prowlarr = { - usesDynamicUser = true; config = { apiVersion = lib.mkDefault "v1"; hostConfig = { diff --git a/modules/prowlarr/indexersService.nix b/modules/prowlarr/indexersService.nix index cbc6aca6..5d5d91b0 100644 --- a/modules/prowlarr/indexersService.nix +++ b/modules/prowlarr/indexersService.nix @@ -4,19 +4,22 @@ }: # Helper function to create a systemd service that configures Prowlarr indexers via API serviceName: serviceConfig: -with lib; { +with lib; let + mkWaitForApiScript = import ../arr-common/mkWaitForApiScript.nix {inherit lib pkgs;}; + capitalizedName = lib.toUpper (builtins.substring 0 1 serviceName) + builtins.substring 1 (-1) serviceName; +in { description = "Configure ${serviceName} indexers via API"; after = ["${serviceName}-config.service"]; + requires = ["${serviceName}-config.service"]; wantedBy = ["multi-user.target"]; serviceConfig = { Type = "oneshot"; RemainAfterExit = true; + ExecStartPre = mkWaitForApiScript serviceName serviceConfig; }; - script = let - capitalizedName = lib.toUpper (builtins.substring 0 1 serviceName) + builtins.substring 1 (-1) serviceName; - in '' + script = '' set -eu # Read API key secret @@ -24,17 +27,6 @@ with lib; { BASE_URL="http://127.0.0.1:${builtins.toString serviceConfig.hostConfig.port}${serviceConfig.hostConfig.urlBase}/api/${serviceConfig.apiVersion}" - # Wait for API to be available (up to 60 seconds) - echo "Waiting for ${capitalizedName} API to be available..." - for i in {1..60}; do - if ${pkgs.curl}/bin/curl -sSf "$BASE_URL/system/status?apiKey=$API_KEY" >/dev/null 2>&1; then - echo "${capitalizedName} API is available" - break - fi - echo "Waiting for ${capitalizedName} API... ($i/60)" - sleep 1 - done - # Fetch all indexer schemas echo "Fetching indexer schemas..." SCHEMAS=$(${pkgs.curl}/bin/curl -sS -H "X-Api-Key: $API_KEY" "$BASE_URL/indexer/schema") diff --git a/modules/radarr/default.nix b/modules/radarr/default.nix index a80c216d..c172ecc2 100644 --- a/modules/radarr/default.nix +++ b/modules/radarr/default.nix @@ -6,8 +6,9 @@ }: let inherit (config) nixflix; cfg = config.nixflix.radarr; + arrCommon = import ../arr-common {inherit config lib pkgs;}; in { - imports = [(import ../arr-common/mkArrServiceModule.nix "radarr" {} {inherit config lib pkgs;})]; + imports = [(arrCommon.mkArrServiceModule "radarr" {})]; config.nixflix.radarr = { group = lib.mkDefault "media"; diff --git a/modules/sonarr/default.nix b/modules/sonarr/default.nix index ba1dc4be..fecb15ce 100644 --- a/modules/sonarr/default.nix +++ b/modules/sonarr/default.nix @@ -6,8 +6,9 @@ }: let inherit (config) nixflix; cfg = config.nixflix.sonarr; + arrCommon = import ../arr-common {inherit config lib pkgs;}; in { - imports = [(import ../arr-common/mkArrServiceModule.nix "sonarr" {} {inherit config lib pkgs;})]; + imports = [(arrCommon.mkArrServiceModule "sonarr" {})]; config.nixflix.sonarr = { group = lib.mkDefault "media"; diff --git a/tests/README.md b/tests/README.md new file mode 100644 index 00000000..03fb008a --- /dev/null +++ b/tests/README.md @@ -0,0 +1,185 @@ +# Testing Guide for Nixflix + +This directory contains tests for the Nixflix NixOS modules that configure \*arr applications (Sonarr, Radarr, Lidarr, Prowlarr). + +## Test Structure + +``` +tests/ +├── README.md # This file +├── default.nix # Main test entry point +├── vm-tests/ # NixOS VM integration tests +│ ├── default.nix +│ └── *.nix # Individual VM test files +└── unit-tests/ # Configuration generation tests + └── default.nix +``` + +## Test Types + +### 1. VM Tests (Integration Tests) + +VM tests spin up actual NixOS virtual machines and test the full service stack, including: + +- Service startup and availability +- API connectivity with configured API keys +- Configuration service execution +- Multi-service integration + +### 2. Unit Tests (Configuration Tests) + +Unit tests verify that NixOS module options generate correct systemd service definitions without actually running the services. They validate: + +- Service generation from module options +- Correct systemd unit dependencies +- Default value application + +## Running Tests + +### List Available Tests + +```bash +nix eval --json '.#checks.x86_64-linux' --apply builtins.attrNames +``` + +### Run Individual Tests + +```bash +# Run a specific VM test (replace with actual test name) +nix build .#checks.x86_64-linux. -L + +# Example: +nix build .#checks.x86_64-linux.sonarr-basic -L +``` + +The `-L` flag shows detailed logs during the build/test process. + +### Run Tests Locally with Interactive VM + +For debugging, you can run VM tests interactively: + +```bash +nix build .#checks.x86_64-linux.sonarr-basic.driverInteractive +./result/bin/nixos-test-driver +``` + +This opens a Python REPL where you can interact with the VM: + +```python +>>> start_all() +>>> machine.wait_for_unit("sonarr.service") +>>> machine.succeed("curl http://127.0.0.1:8989") +>>> machine.screenshot("screenshot.png") +``` + +## Continuous Integration + +Tests run automatically on GitHub Actions for every push and pull request. + +When you add new tests to `tests/vm-tests/default.nix` or `tests/unit-tests/default.nix`, they are automatically included in CI - no workflow updates needed! + +## Writing New Tests + +### Adding a New VM Test + +1. Create a new file in `tests/vm-tests/`, e.g., `my-test.nix`: + +```nix +{ + system ? builtins.currentSystem, + pkgs ? import {inherit system;}, + nixosModules, +}: +pkgs.testers.runNixOSTest { + name = "my-test"; + + nodes.machine = {config, pkgs, ...}: { + imports = [nixosModules]; + + services.nixflix = { + enable = true; + user = "testuser"; + # ... your configuration + }; + + users.users.testuser = { + isNormalUser = true; + createHome = true; + }; + }; + + testScript = '' + start_all() + machine.wait_for_unit("sonarr.service") + # ... your test assertions + ''; +} +``` + +2. Add it to `tests/vm-tests/default.nix`: + +```nix +{ + # ... existing tests + my-test = import ./my-test.nix {inherit system pkgs nixosModules;}; +} +``` + +3. The test will automatically appear in `nix flake check` and GitHub Actions! + +### Adding a New Unit Test + +1. Add a new test to `tests/unit-tests/default.nix`: + +```nix +{ + # ... existing tests + my-unit-test = let + config = evalConfig [ + { + services.nixflix = { + # ... your config + }; + } + ]; + # ... assertions + in + assertTest "my-unit-test" (/* condition */); +} +``` + +## Debugging Failed Tests + +### VM Test Failures + +1. Run the test with `-L` flag for detailed logs: + + ```bash + nix build .#checks.x86_64-linux.sonarr-basic -L + ``` + +2. Use the interactive driver to explore: + + ```bash + nix build .#checks.x86_64-linux.sonarr-basic.driverInteractive + ./result/bin/nixos-test-driver + ``` + +3. Check service logs in the VM: + ```python + >>> machine.succeed("journalctl -u sonarr.service") + ``` + +### Unit Test Failures + +Unit tests will show Nix evaluation errors. Check: + +- Module syntax errors +- Missing or incorrect options +- Type mismatches in configuration + +## Resources + +- [NixOS VM Tests Documentation](https://nixos.org/manual/nixos/stable/#sec-nixos-tests) +- [Testing NixOS Modules](https://nix.dev/tutorials/nixos/integration-testing-using-virtual-machines) +- [GitHub Actions for Nix](https://github.com/DeterminateSystems/nix-installer) diff --git a/tests/default.nix b/tests/default.nix new file mode 100644 index 00000000..3f646412 --- /dev/null +++ b/tests/default.nix @@ -0,0 +1,9 @@ +{ + system ? builtins.currentSystem, + pkgs ? import {inherit system;}, + nixosModules, +}: { + # Import all test modules + vm-tests = import ./vm-tests {inherit system pkgs nixosModules;}; + unit-tests = import ./unit-tests {inherit system pkgs nixosModules;}; +} diff --git a/tests/unit-tests/default.nix b/tests/unit-tests/default.nix new file mode 100644 index 00000000..f1f3247f --- /dev/null +++ b/tests/unit-tests/default.nix @@ -0,0 +1,114 @@ +{ + system ? builtins.currentSystem, + pkgs ? import {inherit system;}, + nixosModules, +}: let + inherit (pkgs) lib; + + # Helper to evaluate a NixOS configuration without building + evalConfig = modules: + import "${pkgs.path}/nixos/lib/eval-config.nix" { + inherit system; + modules = + [ + nixosModules + { + # Minimal NixOS config stubs needed for evaluation + nixpkgs.hostPlatform = system; + } + ] + ++ modules; + }; + + # Test helper to assert conditions + assertTest = name: cond: + pkgs.runCommand "unit-test-${name}" {} '' + ${lib.optionalString (!cond) "echo 'FAIL: ${name}' && exit 1"} + echo 'PASS: ${name}' > $out + ''; +in { + # Test that nixflix.sonarr options generate correct systemd units + sonarr-service-generation = let + config = evalConfig [ + { + nixflix = { + enable = true; + sonarr = { + enable = true; + user = "testuser"; + config = { + hostConfig = { + port = 8989; + username = "admin"; + passwordPath = "/run/secrets/sonarr-pass"; + }; + apiKeyPath = "/run/secrets/sonarr-api"; + rootFolders = [{path = "/media/tv";}]; + }; + }; + }; + } + ]; + systemdUnits = config.config.systemd.services; + hasAllServices = systemdUnits ? sonarr && systemdUnits ? sonarr-config && systemdUnits ? sonarr-rootfolders; + in + assertTest "sonarr-service-generation" hasAllServices; + + # Test that radarr options generate correct systemd units + radarr-service-generation = let + config = evalConfig [ + { + nixflix = { + enable = true; + radarr = { + enable = true; + user = "testuser"; + config = { + hostConfig = { + port = 7878; + username = "admin"; + passwordPath = "/run/secrets/radarr-pass"; + }; + apiKeyPath = "/run/secrets/radarr-api"; + rootFolders = [{path = "/media/movies";}]; + }; + }; + }; + } + ]; + systemdUnits = config.config.systemd.services; + hasAllServices = systemdUnits ? radarr && systemdUnits ? radarr-config && systemdUnits ? radarr-rootfolders; + in + assertTest "radarr-service-generation" hasAllServices; + + # Test that prowlarr with indexers generates correct systemd units + prowlarr-service-generation = let + config = evalConfig [ + { + nixflix = { + enable = true; + prowlarr = { + enable = true; + config = { + hostConfig = { + port = 9696; + username = "admin"; + passwordPath = "/run/secrets/prowlarr-pass"; + }; + apiKeyPath = "/run/secrets/prowlarr-api"; + indexers = [ + { + name = "1337x"; + apiKeyPath = "/run/secrets/1337x-api"; + } + ]; + }; + }; + }; + } + ]; + systemdUnits = config.config.systemd.services; + hasAllServices = systemdUnits ? prowlarr && systemdUnits ? prowlarr-config && systemdUnits ? prowlarr-indexers; + in + assertTest "prowlarr-service-generation" hasAllServices; +} diff --git a/tests/vm-tests/default.nix b/tests/vm-tests/default.nix new file mode 100644 index 00000000..1cec6b7c --- /dev/null +++ b/tests/vm-tests/default.nix @@ -0,0 +1,14 @@ +{ + system ? builtins.currentSystem, + pkgs ? import {inherit system;}, + nixosModules, +}: { + sonarr-basic = import ./sonarr-basic.nix {inherit system pkgs nixosModules;}; + radarr-basic = import ./radarr-basic.nix {inherit system pkgs nixosModules;}; + lidarr-basic = import ./lidarr-basic.nix {inherit system pkgs nixosModules;}; + prowlarr-basic = import ./prowlarr-basic.nix {inherit system pkgs nixosModules;}; + full-stack = import ./full-stack.nix {inherit system pkgs nixosModules;}; + nginx-integration = import ./nginx-integration.nix {inherit system pkgs nixosModules;}; + postgresql-integration = import ./postgresql-integration.nix {inherit system pkgs nixosModules;}; + mullvad-integration = import ./mullvad-integration.nix {inherit system pkgs nixosModules;}; +} diff --git a/tests/vm-tests/full-stack.nix b/tests/vm-tests/full-stack.nix new file mode 100644 index 00000000..09d7601b --- /dev/null +++ b/tests/vm-tests/full-stack.nix @@ -0,0 +1,138 @@ +{ + system ? builtins.currentSystem, + pkgs ? import {inherit system;}, + nixosModules, +}: +pkgs.testers.runNixOSTest { + name = "full-stack-test"; + + nodes.machine = { + config, + pkgs, + ... + }: { + imports = [nixosModules]; + + nixflix = { + enable = true; + + prowlarr = { + enable = true; + config = { + hostConfig = { + port = 9696; + username = "admin"; + passwordPath = "${pkgs.writeText "prowlarr-password" "testpass"}"; + }; + apiKeyPath = "${pkgs.writeText "prowlarr-apikey" "prowlarr11111111111111111111111111"}"; + }; + }; + + sonarr = { + enable = true; + user = "mediauser"; + mediaDirs = [{dir = "/media/tv";}]; + config = { + hostConfig = { + port = 8989; + username = "admin"; + passwordPath = "${pkgs.writeText "sonarr-password" "testpass"}"; + }; + apiKeyPath = "${pkgs.writeText "sonarr-apikey" "sonarr222222222222222222222222222"}"; + }; + }; + + radarr = { + enable = true; + user = "mediauser"; + mediaDirs = [{dir = "/media/movies";}]; + config = { + hostConfig = { + port = 7878; + username = "admin"; + passwordPath = "${pkgs.writeText "radarr-password" "testpass"}"; + }; + apiKeyPath = "${pkgs.writeText "radarr-apikey" "radarr333333333333333333333333333"}"; + }; + }; + + lidarr = { + enable = true; + user = "mediauser"; + mediaDirs = [{dir = "/media/music";}]; + config = { + hostConfig = { + port = 8686; + username = "admin"; + passwordPath = "${pkgs.writeText "lidarr-password" "testpass"}"; + }; + apiKeyPath = "${pkgs.writeText "lidarr-apikey" "lidarr444444444444444444444444444"}"; + }; + }; + }; + }; + + testScript = '' + start_all() + + # Wait for all services to start + machine.wait_for_unit("prowlarr.service", timeout=60) + machine.wait_for_unit("sonarr.service", timeout=60) + machine.wait_for_unit("radarr.service", timeout=60) + machine.wait_for_unit("lidarr.service", timeout=60) + + # Wait for ports + machine.wait_for_open_port(9696, timeout=60) + machine.wait_for_open_port(8989, timeout=60) + machine.wait_for_open_port(7878, timeout=60) + machine.wait_for_open_port(8686, timeout=60) + + # Wait for all configuration services + machine.wait_for_unit("prowlarr-config.service", timeout=60) + machine.wait_for_unit("sonarr-config.service", timeout=60) + machine.wait_for_unit("radarr-config.service", timeout=60) + machine.wait_for_unit("lidarr-config.service", timeout=60) + + # Wait for all services to come back up after restart + machine.wait_for_unit("prowlarr.service", timeout=60) + machine.wait_for_unit("sonarr.service", timeout=60) + machine.wait_for_unit("radarr.service", timeout=60) + machine.wait_for_unit("lidarr.service", timeout=60) + machine.wait_for_open_port(9696, timeout=60) + machine.wait_for_open_port(8989, timeout=60) + machine.wait_for_open_port(7878, timeout=60) + machine.wait_for_open_port(8686, timeout=60) + + # Test all APIs are accessible + machine.succeed( + "curl -f -H 'X-Api-Key: prowlarr11111111111111111111111111' " + "http://127.0.0.1:9696/api/v1/system/status" + ) + machine.succeed( + "curl -f -H 'X-Api-Key: sonarr222222222222222222222222222' " + "http://127.0.0.1:8989/api/v3/system/status" + ) + machine.succeed( + "curl -f -H 'X-Api-Key: radarr333333333333333333333333333' " + "http://127.0.0.1:7878/api/v3/system/status" + ) + machine.succeed( + "curl -f -H 'X-Api-Key: lidarr444444444444444444444444444' " + "http://127.0.0.1:8686/api/v1/system/status" + ) + + # Wait for additional services (root folders only - indexers omitted) + machine.wait_for_unit("sonarr-rootfolders.service", timeout=60) + machine.wait_for_unit("radarr-rootfolders.service", timeout=60) + machine.wait_for_unit("lidarr-rootfolders.service", timeout=60) + machine.wait_for_unit("prowlarr-indexers.service", timeout=60) + + # Verify all processes running under correct user + machine.succeed("pgrep Prowlarr") + machine.succeed("pgrep -u mediauser Sonarr") + machine.succeed("pgrep -u mediauser Radarr") + machine.succeed("pgrep -u mediauser dotnet") + + print("All services are running successfully!") + ''; +} diff --git a/tests/vm-tests/lidarr-basic.nix b/tests/vm-tests/lidarr-basic.nix new file mode 100644 index 00000000..d2feb798 --- /dev/null +++ b/tests/vm-tests/lidarr-basic.nix @@ -0,0 +1,67 @@ +{ + system ? builtins.currentSystem, + pkgs ? import {inherit system;}, + nixosModules, +}: +pkgs.testers.runNixOSTest { + name = "lidarr-basic-test"; + + nodes.machine = { + config, + pkgs, + ... + }: { + imports = [nixosModules]; + + nixflix = { + enable = true; + + lidarr = { + enable = true; + user = "testuser"; + mediaDirs = [ + {dir = "/media/music";} + ]; + config = { + hostConfig = { + port = 8686; + username = "admin"; + passwordPath = "${pkgs.writeText "lidarr-password" "testpassword123"}"; + }; + apiKeyPath = "${pkgs.writeText "lidarr-apikey" "5678efgh5678efgh5678efgh5678efgh"}"; + }; + }; + }; + }; + + testScript = '' + start_all() + + machine.wait_for_unit("lidarr.service", timeout=60) + machine.wait_for_open_port(8686, timeout=60) + machine.wait_for_unit("lidarr-config.service", timeout=60) + + # Wait for lidarr to come back up after restart + machine.wait_for_unit("lidarr.service", timeout=60) + machine.wait_for_open_port(8686, timeout=60) + + # Test API connectivity + machine.succeed( + "curl -f -H 'X-Api-Key: 5678efgh5678efgh5678efgh5678efgh' " + "http://127.0.0.1:8686/api/v1/system/status" + ) + + # Wait for root folders service + machine.wait_for_unit("lidarr-rootfolders.service", timeout=60) + + # Check root folder + folders = machine.succeed( + "curl -s -H 'X-Api-Key: 5678efgh5678efgh5678efgh5678efgh' " + "http://127.0.0.1:8686/api/v1/rootfolder" + ) + print(f"Root folders: {folders}") + assert "/media/music" in folders, "Root folder not created" + + machine.succeed("pgrep -u testuser dotnet") + ''; +} diff --git a/tests/vm-tests/mullvad-integration.nix b/tests/vm-tests/mullvad-integration.nix new file mode 100644 index 00000000..6f3f03ae --- /dev/null +++ b/tests/vm-tests/mullvad-integration.nix @@ -0,0 +1,71 @@ +{ + system ? builtins.currentSystem, + pkgs ? import {inherit system;}, + nixosModules, +}: +pkgs.testers.runNixOSTest { + name = "mullvad-integration-test"; + + nodes.machine = { + config, + pkgs, + ... + }: { + imports = [nixosModules]; + + nixflix = { + enable = true; + mullvad = { + enable = true; + autoConnect = false; # Don't auto-connect in tests + killSwitch = { + enable = true; + allowLan = true; + }; + dns = ["1.1.1.1" "1.0.0.1"]; + }; + }; + }; + + testScript = '' + start_all() + + # Wait for Mullvad daemon to start + machine.wait_for_unit("mullvad-daemon.service", timeout=60) + + # Wait for Mullvad configuration service + machine.wait_for_unit("mullvad-config.service", timeout=60) + + # Verify Mullvad daemon is running + print("Checking Mullvad daemon status...") + machine.succeed("mullvad status") + + # Verify lockdown mode (kill switch) is enabled + print("Verifying kill switch is enabled...") + status = machine.succeed("mullvad lockdown-mode get") + assert "on" in status.lower(), f"Kill switch not enabled: {status}" + + # Verify LAN access is allowed + print("Verifying LAN access is allowed...") + lan_status = machine.succeed("mullvad lan get") + assert "allow" in lan_status.lower(), f"LAN access not allowed: {lan_status}" + + # Verify DNS settings + print("Verifying DNS settings...") + dns_status = machine.succeed("mullvad dns get") + assert "custom" in dns_status.lower(), f"Custom DNS not configured: {dns_status}" + assert "1.1.1.1" in dns_status, f"DNS 1.1.1.1 not found in configuration: {dns_status}" + assert "1.0.0.1" in dns_status, f"DNS 1.0.0.1 not found in configuration: {dns_status}" + + # Verify auto-connect is off (as configured) + print("Verifying auto-connect is disabled...") + autoconnect_status = machine.succeed("mullvad auto-connect get") + assert "off" in autoconnect_status.lower(), f"Auto-connect not disabled: {autoconnect_status}" + + # Test that we can disconnect (service should handle this gracefully even when not connected) + print("Testing disconnect command...") + machine.succeed("mullvad disconnect || true") + + print("Mullvad integration test successful! Kill switch configured correctly.") + ''; +} diff --git a/tests/vm-tests/nginx-integration.nix b/tests/vm-tests/nginx-integration.nix new file mode 100644 index 00000000..0a0ac6bf --- /dev/null +++ b/tests/vm-tests/nginx-integration.nix @@ -0,0 +1,140 @@ +{ + system ? builtins.currentSystem, + pkgs ? import {inherit system;}, + nixosModules, +}: +pkgs.testers.runNixOSTest { + name = "nginx-integration-test"; + + nodes.machine = { + config, + pkgs, + ... + }: { + imports = [nixosModules]; + + nixflix = { + enable = true; + nginx.enable = true; + serviceNameIsUrlBase = true; + + prowlarr = { + enable = true; + config = { + hostConfig = { + port = 9696; + username = "admin"; + passwordPath = "${pkgs.writeText "prowlarr-password" "testpass"}"; + }; + apiKeyPath = "${pkgs.writeText "prowlarr-apikey" "prowlarr11111111111111111111111111"}"; + }; + }; + + sonarr = { + enable = true; + user = "sonarr"; + mediaDirs = [{dir = "/media/tv";}]; + config = { + hostConfig = { + port = 8989; + username = "admin"; + passwordPath = "${pkgs.writeText "sonarr-password" "testpass"}"; + }; + apiKeyPath = "${pkgs.writeText "sonarr-apikey" "sonarr222222222222222222222222222"}"; + }; + }; + + radarr = { + enable = true; + user = "radarr"; + mediaDirs = [{dir = "/media/movies";}]; + config = { + hostConfig = { + port = 7878; + username = "admin"; + passwordPath = "${pkgs.writeText "radarr-password" "testpass"}"; + }; + apiKeyPath = "${pkgs.writeText "radarr-apikey" "radarr333333333333333333333333333"}"; + }; + }; + + lidarr = { + enable = true; + user = "lidarr"; + mediaDirs = [{dir = "/media/music";}]; + config = { + hostConfig = { + port = 8686; + username = "admin"; + passwordPath = "${pkgs.writeText "lidarr-password" "testpass"}"; + }; + apiKeyPath = "${pkgs.writeText "lidarr-apikey" "lidarr444444444444444444444444444"}"; + }; + }; + }; + }; + + testScript = '' + start_all() + + # Wait for nginx + machine.wait_for_unit("nginx.service", timeout=60) + machine.wait_for_open_port(80, timeout=60) + + # Wait for all services + machine.wait_for_unit("prowlarr.service", timeout=60) + machine.wait_for_unit("sonarr.service", timeout=60) + machine.wait_for_unit("radarr.service", timeout=60) + machine.wait_for_unit("lidarr.service", timeout=60) + machine.wait_for_open_port(9696, timeout=60) + machine.wait_for_open_port(8989, timeout=60) + machine.wait_for_open_port(7878, timeout=60) + machine.wait_for_open_port(8686, timeout=60) + + # Wait for configuration services + machine.wait_for_unit("prowlarr-config.service", timeout=60) + machine.wait_for_unit("sonarr-config.service", timeout=60) + machine.wait_for_unit("radarr-config.service", timeout=60) + machine.wait_for_unit("lidarr-config.service", timeout=60) + + # Wait for services to come back up after restart + machine.wait_for_unit("prowlarr.service", timeout=60) + machine.wait_for_unit("sonarr.service", timeout=60) + machine.wait_for_unit("radarr.service", timeout=60) + machine.wait_for_unit("lidarr.service", timeout=60) + machine.wait_for_open_port(9696, timeout=60) + machine.wait_for_open_port(8989, timeout=60) + machine.wait_for_open_port(7878, timeout=60) + machine.wait_for_open_port(8686, timeout=60) + + # Test nginx is proxying to Prowlarr + print("Testing Prowlarr via nginx...") + machine.succeed( + "curl -f http://localhost/prowlarr/api/v1/system/status " + "-H 'X-Api-Key: prowlarr11111111111111111111111111'" + ) + + # Test nginx is proxying to Sonarr + print("Testing Sonarr via nginx...") + machine.succeed( + "curl -f http://localhost/sonarr/api/v3/system/status " + "-H 'X-Api-Key: sonarr222222222222222222222222222'" + ) + + # Test nginx is proxying to Radarr + print("Testing Radarr via nginx...") + machine.succeed( + "curl -f http://localhost/radarr/api/v3/system/status " + "-H 'X-Api-Key: radarr333333333333333333333333333'" + ) + + # Test nginx is proxying to Lidarr + print("Testing Lidarr via nginx...") + machine.succeed( + "curl -f http://localhost/lidarr/api/v1/system/status " + "-H 'X-Api-Key: lidarr444444444444444444444444444'" + ) + + print("Nginx integration test successful! All services accessible via reverse proxy.") + ''; +} diff --git a/tests/vm-tests/postgresql-integration.nix b/tests/vm-tests/postgresql-integration.nix new file mode 100644 index 00000000..a26f3b19 --- /dev/null +++ b/tests/vm-tests/postgresql-integration.nix @@ -0,0 +1,183 @@ +{ + system ? builtins.currentSystem, + pkgs ? import {inherit system;}, + nixosModules, +}: +pkgs.testers.runNixOSTest { + name = "postgresql-integration-test"; + + nodes.machine = { + config, + pkgs, + ... + }: { + imports = [nixosModules]; + + nixflix = { + enable = true; + postgres.enable = true; + stateDir = "/var/lib/nixflix"; + + prowlarr = { + enable = true; + config = { + hostConfig = { + port = 9696; + username = "admin"; + passwordPath = "${pkgs.writeText "prowlarr-password" "testpass"}"; + }; + apiKeyPath = "${pkgs.writeText "prowlarr-apikey" "prowlarr11111111111111111111111111"}"; + }; + }; + + sonarr = { + enable = true; + user = "sonarr"; + mediaDirs = [{dir = "/media/tv";}]; + config = { + hostConfig = { + port = 8989; + username = "admin"; + passwordPath = "${pkgs.writeText "sonarr-password" "testpass"}"; + }; + apiKeyPath = "${pkgs.writeText "sonarr-apikey" "sonarr222222222222222222222222222"}"; + }; + }; + + radarr = { + enable = true; + user = "radarr"; + mediaDirs = [{dir = "/media/movies";}]; + config = { + hostConfig = { + port = 7878; + username = "admin"; + passwordPath = "${pkgs.writeText "radarr-password" "testpass"}"; + }; + apiKeyPath = "${pkgs.writeText "radarr-apikey" "radarr333333333333333333333333333"}"; + }; + }; + + lidarr = { + enable = true; + user = "lidarr"; + mediaDirs = [{dir = "/media/music";}]; + config = { + hostConfig = { + port = 8686; + username = "admin"; + passwordPath = "${pkgs.writeText "lidarr-password" "testpass"}"; + }; + apiKeyPath = "${pkgs.writeText "lidarr-apikey" "lidarr444444444444444444444444444"}"; + }; + }; + }; + }; + + testScript = '' + import json + + start_all() + + # Wait for PostgreSQL + machine.wait_for_unit("postgresql.service", timeout=60) + machine.wait_for_open_port(5432, timeout=60) + machine.wait_for_unit("postgresql-setup.service", timeout=60) + + # Wait for database roles to be ready + machine.wait_for_unit("prowlarr-wait-for-db.service", timeout=60) + machine.wait_for_unit("sonarr-wait-for-db.service", timeout=60) + machine.wait_for_unit("radarr-wait-for-db.service", timeout=60) + machine.wait_for_unit("lidarr-wait-for-db.service", timeout=60) + + # Wait for all services + machine.wait_for_unit("prowlarr.service", timeout=60) + machine.wait_for_unit("sonarr.service", timeout=60) + machine.wait_for_unit("radarr.service", timeout=60) + machine.wait_for_unit("lidarr.service", timeout=60) + machine.wait_for_open_port(9696, timeout=60) + machine.wait_for_open_port(8989, timeout=60) + machine.wait_for_open_port(7878, timeout=60) + machine.wait_for_open_port(8686, timeout=60) + + # Wait for configuration services + machine.wait_for_unit("prowlarr-config.service", timeout=60) + machine.wait_for_unit("sonarr-config.service", timeout=60) + machine.wait_for_unit("radarr-config.service", timeout=60) + machine.wait_for_unit("lidarr-config.service", timeout=60) + + # Wait for services to come back up after restart + machine.wait_for_unit("prowlarr.service", timeout=60) + machine.wait_for_unit("sonarr.service", timeout=60) + machine.wait_for_unit("radarr.service", timeout=60) + machine.wait_for_unit("lidarr.service", timeout=60) + machine.wait_for_open_port(9696, timeout=60) + machine.wait_for_open_port(8989, timeout=60) + machine.wait_for_open_port(7878, timeout=60) + machine.wait_for_open_port(8686, timeout=60) + + # Verify PostgreSQL is running and accessible + print("Testing PostgreSQL is running...") + machine.succeed("sudo -u postgres psql -c 'SELECT version();'") + + # Verify PostgreSQL dataDir configuration + print("Verifying PostgreSQL data directory...") + datadir = machine.succeed("sudo -u postgres psql -t -c 'SHOW data_directory;'").strip() + assert "/var/lib/nixflix/postgres" in datadir, f"PostgreSQL dataDir incorrect: {datadir}" + + # Test Prowlarr API and verify PostgreSQL database type + print("Testing Prowlarr API and database type...") + prowlarr_status = machine.succeed( + "curl -f http://localhost:9696/api/v1/system/status " + "-H 'X-Api-Key: prowlarr11111111111111111111111111'" + ) + prowlarr_data = json.loads(prowlarr_status) + assert prowlarr_data.get("databaseType") == "postgreSQL", \ + f"Prowlarr not using PostgreSQL: {prowlarr_data.get('databaseType')}" + + # Test Sonarr API and verify PostgreSQL database type + print("Testing Sonarr API and database type...") + sonarr_status = machine.succeed( + "curl -f http://localhost:8989/api/v3/system/status " + "-H 'X-Api-Key: sonarr222222222222222222222222222'" + ) + sonarr_data = json.loads(sonarr_status) + assert sonarr_data.get("databaseType") == "postgreSQL", \ + f"Sonarr not using PostgreSQL: {sonarr_data.get('databaseType')}" + + # Test Radarr API and verify PostgreSQL database type + print("Testing Radarr API and database type...") + radarr_status = machine.succeed( + "curl -f http://localhost:7878/api/v3/system/status " + "-H 'X-Api-Key: radarr333333333333333333333333333'" + ) + radarr_data = json.loads(radarr_status) + assert radarr_data.get("databaseType") == "postgreSQL", \ + f"Radarr not using PostgreSQL: {radarr_data.get('databaseType')}" + + # Test Lidarr API and verify PostgreSQL database type + print("Testing Lidarr API and database type...") + lidarr_status = machine.succeed( + "curl -f http://localhost:8686/api/v1/system/status " + "-H 'X-Api-Key: lidarr444444444444444444444444444'" + ) + lidarr_data = json.loads(lidarr_status) + assert lidarr_data.get("databaseType") == "postgreSQL", \ + f"Lidarr not using PostgreSQL: {lidarr_data.get('databaseType')}" + + # Verify PostgreSQL directory has correct permissions + print("Verifying PostgreSQL directory permissions...") + stat_output = machine.succeed("stat -c '%a %U %G' /var/lib/nixflix/postgres") + perms, owner, group = stat_output.strip().split() + assert perms == "700", f"PostgreSQL directory permissions incorrect: {perms}" + assert owner == "postgres", f"PostgreSQL directory owner incorrect: {owner}" + assert group == "postgres", f"PostgreSQL directory group incorrect: {group}" + + # Verify PostgreSQL is using version 16 + print("Verifying PostgreSQL version...") + version_output = machine.succeed("sudo -u postgres psql -t -c 'SHOW server_version;'") + assert "16." in version_output, f"PostgreSQL version incorrect: {version_output}" + + print("PostgreSQL integration test successful! All services running with PostgreSQL enabled.") + ''; +} diff --git a/tests/vm-tests/prowlarr-basic.nix b/tests/vm-tests/prowlarr-basic.nix new file mode 100644 index 00000000..b725889a --- /dev/null +++ b/tests/vm-tests/prowlarr-basic.nix @@ -0,0 +1,58 @@ +{ + system ? builtins.currentSystem, + pkgs ? import {inherit system;}, + nixosModules, +}: +pkgs.testers.runNixOSTest { + name = "prowlarr-basic-test"; + + nodes.machine = { + config, + pkgs, + ... + }: { + imports = [nixosModules]; + + nixflix = { + enable = true; + + prowlarr = { + enable = true; + config = { + hostConfig = { + port = 9696; + username = "admin"; + passwordPath = "${pkgs.writeText "prowlarr-password" "testpassword123"}"; + }; + apiKeyPath = "${pkgs.writeText "prowlarr-apikey" "fedcba9876543210fedcba9876543210"}"; + }; + }; + }; + }; + + testScript = '' + start_all() + + # Wait for Prowlarr to start + machine.wait_for_unit("prowlarr.service", timeout=60) + machine.wait_for_open_port(9696, timeout=60) + + # Wait for configuration service (which restarts prowlarr) + machine.wait_for_unit("prowlarr-config.service", timeout=60) + + # Wait for prowlarr to come back up after restart + machine.wait_for_unit("prowlarr.service", timeout=60) + machine.wait_for_open_port(9696, timeout=60) + + # Test API connectivity + machine.succeed( + "curl -f -H 'X-Api-Key: fedcba9876543210fedcba9876543210' " + "http://127.0.0.1:9696/api/v1/system/status" + ) + + # Verify the service is running + machine.succeed("pgrep Prowlarr") + + print("Prowlarr is running successfully!") + ''; +} diff --git a/tests/vm-tests/radarr-basic.nix b/tests/vm-tests/radarr-basic.nix new file mode 100644 index 00000000..efa19353 --- /dev/null +++ b/tests/vm-tests/radarr-basic.nix @@ -0,0 +1,67 @@ +{ + system ? builtins.currentSystem, + pkgs ? import {inherit system;}, + nixosModules, +}: +pkgs.testers.runNixOSTest { + name = "radarr-basic-test"; + + nodes.machine = { + config, + pkgs, + ... + }: { + imports = [nixosModules]; + + nixflix = { + enable = true; + + radarr = { + enable = true; + user = "testuser"; + mediaDirs = [ + {dir = "/media/movies";} + ]; + config = { + hostConfig = { + port = 7878; + username = "admin"; + passwordPath = "${pkgs.writeText "radarr-password" "testpassword123"}"; + }; + apiKeyPath = "${pkgs.writeText "radarr-apikey" "abcd1234abcd1234abcd1234abcd1234"}"; + }; + }; + }; + }; + + testScript = '' + start_all() + + machine.wait_for_unit("radarr.service", timeout=60) + machine.wait_for_open_port(7878, timeout=60) + machine.wait_for_unit("radarr-config.service", timeout=60) + + # Wait for radarr to come back up after restart + machine.wait_for_unit("radarr.service", timeout=60) + machine.wait_for_open_port(7878, timeout=60) + + # Test API connectivity + machine.succeed( + "curl -f -H 'X-Api-Key: abcd1234abcd1234abcd1234abcd1234' " + "http://127.0.0.1:7878/api/v3/system/status" + ) + + # Wait for root folders service + machine.wait_for_unit("radarr-rootfolders.service", timeout=60) + + # Check root folder + folders = machine.succeed( + "curl -s -H 'X-Api-Key: abcd1234abcd1234abcd1234abcd1234' " + "http://127.0.0.1:7878/api/v3/rootfolder" + ) + print(f"Root folders: {folders}") + assert "/media/movies" in folders, "Root folder not created" + + machine.succeed("pgrep -u testuser Radarr") + ''; +} diff --git a/tests/vm-tests/sonarr-basic.nix b/tests/vm-tests/sonarr-basic.nix new file mode 100644 index 00000000..05357d93 --- /dev/null +++ b/tests/vm-tests/sonarr-basic.nix @@ -0,0 +1,81 @@ +{ + system ? builtins.currentSystem, + pkgs ? import {inherit system;}, + nixosModules, +}: +pkgs.testers.runNixOSTest { + name = "sonarr-basic-test"; + + nodes.machine = { + config, + pkgs, + ... + }: { + imports = [nixosModules]; + + nixflix = { + enable = true; + + sonarr = { + enable = true; + user = "testuser"; + mediaDirs = [ + {dir = "/media/tv";} + ]; + config = { + hostConfig = { + port = 8989; + username = "admin"; + passwordPath = "${pkgs.writeText "sonarr-password" "testpassword123"}"; + }; + apiKeyPath = "${pkgs.writeText "sonarr-apikey" "0123456789abcdef0123456789abcdef"}"; + }; + }; + }; + }; + + testScript = '' + start_all() + + # Wait for Sonarr to start + machine.wait_for_unit("sonarr.service", timeout=60) + machine.wait_for_open_port(8989, timeout=60) + + # Wait for configuration service to complete (which restarts sonarr) + machine.wait_for_unit("sonarr-config.service", timeout=60) + + # Wait for sonarr to come back up after restart + machine.wait_for_unit("sonarr.service", timeout=60) + machine.wait_for_open_port(8989, timeout=60) + + # Test API connectivity with the configured API key + machine.succeed( + "curl -f -H 'X-Api-Key: 0123456789abcdef0123456789abcdef' " + "http://127.0.0.1:8989/api/v3/system/status" + ) + + # Check that host configuration was applied + result = machine.succeed( + "curl -s -H 'X-Api-Key: 0123456789abcdef0123456789abcdef' " + "http://127.0.0.1:8989/api/v3/config/host" + ) + print(f"Host config: {result}") + + # Verify username is set correctly + assert "admin" in result, "Username not configured correctly" + + # Wait for root folders service to complete + machine.wait_for_unit("sonarr-rootfolders.service", timeout=60) + + # Check that root folder was created + folders = machine.succeed( + "curl -s -H 'X-Api-Key: 0123456789abcdef0123456789abcdef' " + "http://127.0.0.1:8989/api/v3/rootfolder" + ) + print(f"Root folders: {folders}") + assert "/media/tv" in folders, "Root folder not created" + + # Verify the service is running under the correct user + machine.succeed("pgrep -u testuser Sonarr") + ''; +}