diff --git a/nixos/modules/nixos/services/radarr/default.nix b/nixos/modules/nixos/services/radarr/default.nix index dc9e0c5..adf5179 100644 --- a/nixos/modules/nixos/services/radarr/default.nix +++ b/nixos/modules/nixos/services/radarr/default.nix @@ -54,147 +54,10 @@ let # Function to create instance-specific configuration mkRadarrInstance = name: instanceCfg: { - assertions = [ - { - assertion = !(instanceCfg.db.host != "" && instanceCfg.db.hostFile != ""); - message = "Specify either a direct database host via db.host or a file via db.hostFile (leave direct host empty)."; - } - { - assertion = !(instanceCfg.db.user != "radarr" && instanceCfg.db.userFile != ""); - message = "Specify either a direct database user via db.user or a file via db.userFile."; - } - { - assertion = !(instanceCfg.apiKey != "" && instanceCfg.apiKeyFile != ""); - message = "Specify either a direct API key via apiKey or a file via apiKeyFile (leave direct API key empty)."; - } - ]; - - systemd.tmpfiles.rules = [ - "d ${instanceCfg.dataDir} 0775 ${instanceCfg.user} ${instanceCfg.group}" - ]; systemd.services."radarr-${name}" = { description = "Radarr (${name})"; - after = [ - "network.target" - "nss-lookup.target" - ]; - wantedBy = [ "multi-user.target" ]; - environment = lib.mkMerge [ - { - RADARR__APP__INSTANCENAME = name; - RADARR__APP__THEME = "dark"; - RADARR__AUTH__METHOD = "External"; - RADARR__AUTH__REQUIRED = "DisabledForLocalAddresses"; - RADARR__LOG__DBENABLED = "False"; - RADARR__LOG__LEVEL = "info"; - RADARR__SERVER__PORT = toString instanceCfg.port; - RADARR__UPDATE__BRANCH = "develop"; - } - (lib.mkIf instanceCfg.db.enable { - RADARR__POSTGRES__PORT = toString instanceCfg.db.port; - RADARR__POSTGRES__MAINDB = instanceCfg.db.dbname; - }) - instanceCfg.extraEnvVars - ]; - serviceConfig = lib.mkMerge [ - { - Name = "radarr-${name}"; - Type = "simple"; - User = instanceCfg.user; - Group = instanceCfg.group; - ExecStart = utils.escapeSystemdExecArgs [ - (lib.getExe instanceCfg.package) - "-nobrowser" - "-data=${instanceCfg.dataDir}" - "-port=${toString instanceCfg.port}" - ]; - WorkingDirectory = instanceCfg.dataDir; - RuntimeDirectory = "radarr-${name}"; - LogsDirectory = "radarr-${name}"; - RuntimeDirectoryMode = "0750"; - Restart = "on-failure"; - RestartSec = 5; - } - (lib.mkIf instanceCfg.hardening { - CapabilityBoundingSet = [ "" ]; - DeviceAllow = [ "" ]; - DevicePolicy = "closed"; - LockPersonality = true; - MemoryDenyWriteExecute = false; - NoNewPrivileges = true; - PrivateDevices = true; - PrivateTmp = true; - ProtectControlGroups = true; - ProtectHome = "read-only"; - ProtectKernelModules = true; - ProtectKernelTunables = true; - ProtectSystem = "strict"; - ReadWritePaths = [ - instanceCfg.dataDir - instanceCfg.moviesDir - "/var/log/radarr-${name}" - "/eru/media" - ]; - RestrictAddressFamilies = [ - "AF_INET" - "AF_INET6" - "AF_NETLINK" - ]; - RestrictNamespaces = [ - "uts" - "ipc" - "pid" - "user" - "cgroup" - "net" - ]; - RestrictSUIDSGID = true; - SystemCallArchitectures = "native"; - SystemCallFilter = [ - "@system-service" - ]; - }) - (lib.mkIf instanceCfg.db.enable { - ExecStartPre = "+${pkgs.writeShellScript "radarr-${name}-pre-script" '' - mkdir -p /run/radarr-${name} - rm -f /run/radarr-${name}/secrets.env - - # Helper function to safely write variables - write_var() { - local var_name="$1" - local value="$2" - if [ -n "$value" ]; then - printf "%s=%s\n" "$var_name" "$value" >> /run/radarr-${name}/secrets.env - fi - } - - # API Key (direct value or file) - if [ -n "${instanceCfg.apiKey}" ]; then - write_var "RADARR__AUTH__APIKEY" "${instanceCfg.apiKey}" - else - write_var "RADARR__AUTH__APIKEY" "$(cat ${instanceCfg.apiKeyFile})" - fi - - # Database Configuration - write_var "RADARR__POSTGRES__HOST" "$([ -n "${instanceCfg.db.host}" ] && echo "${instanceCfg.db.host}" || cat "${instanceCfg.db.hostFile}")" - write_var "RADARR__POSTGRES__USER" "$([ -n "${instanceCfg.db.user}" ] && echo "${instanceCfg.db.user}" || cat "${instanceCfg.db.userFile}")" - write_var "RADARR__POSTGRES__PASSWORD" "$(cat ${instanceCfg.db.passwordFile})" - - # Final permissions - chmod 600 /run/radarr-${name}/secrets.env - chown ${instanceCfg.user}:${instanceCfg.group} /run/radarr-${name}/secrets.env - ''}"; - - EnvironmentFile = ( - [ "-/run/radarr-${name}/secrets.env" ] - ++ lib.optional ( - instanceCfg.extraEnvVarFile != null && instanceCfg.extraEnvVarFile != "" - ) instanceCfg.extraEnvVarFile - ); - }) - ]; }; networking.firewall = mkIf instanceCfg.openFirewall { @@ -317,12 +180,200 @@ in }; }; - config = mkIf cfg.enable (mkMerge [ - # Create services for each enabled instance - (mkMerge ( + config = mkIf cfg.enable { + # Add assertions for all instances + assertions = flatten ( mapAttrsToList ( - name: instanceCfg: mkIf instanceCfg.enable (mkRadarrInstance name instanceCfg) + name: instanceCfg: + if instanceCfg.enable then + [ + { + assertion = !(instanceCfg.db.host != "" && instanceCfg.db.hostFile != ""); + message = "Specify either a direct database host via db.host or a file via db.hostFile (leave direct host empty)."; + } + { + assertion = !(instanceCfg.db.user != "radarr" && instanceCfg.db.userFile != ""); + message = "Specify either a direct database user via db.user or a file via db.userFile."; + } + { + assertion = !(instanceCfg.apiKey != "" && instanceCfg.apiKeyFile != ""); + message = "Specify either a direct API key via apiKey or a file via apiKeyFile (leave direct API key empty)."; + } + ] + else + [ ] ) cfg.instances - )) - ]); + ); + + # Create systemd tmpfiles rules for each enabled instance + systemd.tmpfiles.rules = flatten ( + mapAttrsToList ( + name: instanceCfg: + if instanceCfg.enable then + [ + "d ${instanceCfg.dataDir} 0775 ${instanceCfg.user} ${instanceCfg.group}" + ] + else + [ ] + ) cfg.instances + ); + + # Create services for each enabled instance + systemd.services = mapAttrs' ( + name: instanceCfg: + nameValuePair "radarr-${name}" ( + mkIf instanceCfg.enable { + description = "Radarr (${name})"; + after = [ + "network.target" + "nss-lookup.target" + ]; + wantedBy = [ "multi-user.target" ]; + environment = lib.mkMerge [ + { + RADARR__APP__INSTANCENAME = name; + RADARR__APP__THEME = "dark"; + RADARR__AUTH__METHOD = "External"; + RADARR__AUTH__REQUIRED = "DisabledForLocalAddresses"; + RADARR__LOG__DBENABLED = "False"; + RADARR__LOG__LEVEL = "info"; + RADARR__SERVER__PORT = toString instanceCfg.port; + RADARR__UPDATE__BRANCH = "develop"; + } + (lib.mkIf instanceCfg.db.enable { + RADARR__POSTGRES__PORT = toString instanceCfg.db.port; + RADARR__POSTGRES__MAINDB = instanceCfg.db.dbname; + }) + instanceCfg.extraEnvVars + ]; + + serviceConfig = lib.mkMerge [ + { + Type = "simple"; + User = instanceCfg.user; + Group = instanceCfg.group; + ExecStart = utils.escapeSystemdExecArgs [ + (lib.getExe instanceCfg.package) + "-nobrowser" + "-data=${instanceCfg.dataDir}" + "-port=${toString instanceCfg.port}" + ]; + WorkingDirectory = instanceCfg.dataDir; + RuntimeDirectory = "radarr-${name}"; + LogsDirectory = "radarr-${name}"; + RuntimeDirectoryMode = "0750"; + Restart = "on-failure"; + RestartSec = 5; + } + (lib.mkIf instanceCfg.hardening { + CapabilityBoundingSet = [ "" ]; + DeviceAllow = [ "" ]; + DevicePolicy = "closed"; + LockPersonality = true; + MemoryDenyWriteExecute = false; + NoNewPrivileges = true; + PrivateDevices = true; + PrivateTmp = true; + ProtectControlGroups = true; + ProtectHome = "read-only"; + ProtectKernelModules = true; + ProtectKernelTunables = true; + ProtectSystem = "strict"; + ReadWritePaths = [ + instanceCfg.dataDir + instanceCfg.moviesDir + "/var/log/radarr-${name}" + "/eru/media" + ]; + RestrictAddressFamilies = [ + "AF_INET" + "AF_INET6" + "AF_NETLINK" + ]; + RestrictNamespaces = [ + "uts" + "ipc" + "pid" + "user" + "cgroup" + "net" + ]; + RestrictSUIDSGID = true; + SystemCallArchitectures = "native"; + SystemCallFilter = [ + "@system-service" + ]; + }) + (lib.mkIf instanceCfg.db.enable { + ExecStartPre = "+${pkgs.writeShellScript "radarr-${name}-pre-script" '' + mkdir -p /run/radarr-${name} + rm -f /run/radarr-${name}/secrets.env + + # Helper function to safely write variables + write_var() { + local var_name="$1" + local value="$2" + if [ -n "$value" ]; then + printf "%s=%s\n" "$var_name" "$value" >> /run/radarr-${name}/secrets.env + fi + } + + # API Key (direct value or file) + if [ -n "${instanceCfg.apiKey}" ]; then + write_var "RADARR__AUTH__APIKEY" "${instanceCfg.apiKey}" + else + write_var "RADARR__AUTH__APIKEY" "$(cat ${instanceCfg.apiKeyFile})" + fi + + # Database Configuration + write_var "RADARR__POSTGRES__HOST" "$([ -n "${instanceCfg.db.host}" ] && echo "${instanceCfg.db.host}" || cat "${instanceCfg.db.hostFile}")" + write_var "RADARR__POSTGRES__USER" "$([ -n "${instanceCfg.db.user}" ] && echo "${instanceCfg.db.user}" || cat "${instanceCfg.db.userFile}")" + write_var "RADARR__POSTGRES__PASSWORD" "$(cat ${instanceCfg.db.passwordFile})" + + # Final permissions + chmod 600 /run/radarr-${name}/secrets.env + chown ${instanceCfg.user}:${instanceCfg.group} /run/radarr-${name}/secrets.env + ''}"; + + EnvironmentFile = ( + [ "-/run/radarr-${name}/secrets.env" ] + ++ lib.optional ( + instanceCfg.extraEnvVarFile != null && instanceCfg.extraEnvVarFile != "" + ) instanceCfg.extraEnvVarFile + ); + }) + ]; + } + ) + ) cfg.instances; + + # Firewall configurations + networking.firewall = mkMerge ( + mapAttrsToList ( + name: instanceCfg: + mkIf (instanceCfg.enable && instanceCfg.openFirewall) { + allowedTCPPorts = [ instanceCfg.port ]; + } + ) cfg.instances + ); + + # Users and groups + users = mkMerge ( + mapAttrsToList ( + name: instanceCfg: + mkIf instanceCfg.enable { + groups.${instanceCfg.group} = { }; + users = mkIf (instanceCfg.user == "radarr") { + radarr = { + inherit (instanceCfg) group; + isSystemUser = true; + # home = instanceCfg.dataDir; + home = "/nahar/radarr"; + }; + }; + } + ) cfg.instances + ); + }; + }