new pds module

karitham.dev 865501d7 01c25e63

verified
+5 -1
modules/default.nix
··· 25 25 26 26 wakuna-image = self.lib.sdImageFromSystem self.nixosConfigurations.wakuna; 27 27 }; 28 + checks = { 29 + pds-simple = pkgs.callPackage ./pds/pds-recovery-simple.nix { inherit (inputs) nixpkgs; }; 30 + pds-full = pkgs.callPackage ./pds/pds-recovery-full.nix { inherit (inputs) nixpkgs; }; 31 + }; 28 32 formatter = pkgs.nixfmt-rfc-style; 29 33 devShells.default = pkgs.mkShell { packages = with pkgs; [ sops ]; }; 30 34 }; ··· 73 77 dev = import ./dev/nixos.nix; 74 78 desktop = import ./desktop/nixos.nix; 75 79 multi-scrobbler = import ./services/multi-scrobbler.nix; 76 - pds-backup = import ./services/pds.nix; 80 + pds = import ./pds/nixos.nix; 77 81 }; 78 82 }; 79 83 }
+365
modules/pds/nixos.nix
··· 1 + { 2 + config, 3 + lib, 4 + pkgs, 5 + ... 6 + }: 7 + let 8 + inherit (lib) 9 + mkOption 10 + mkIf 11 + mkMerge 12 + mkEnableOption 13 + mkDefault 14 + types 15 + ; 16 + 17 + cfg = config.services.pds-with-backups; 18 + 19 + pdsUser = "pds"; 20 + pdsGroup = "pds"; 21 + 22 + secretsFiles = cfg.secretsFiles; 23 + 24 + litestreamConfig = pkgs.writeText "litestream-pds-config.yml" '' 25 + dbs: 26 + - dir: ${cfg.pdsDataDir} 27 + pattern: "*.sqlite" 28 + recursive: true 29 + watch: true 30 + replica: 31 + type: s3 32 + path: ${cfg.s3Prefix} 33 + bucket: ''${S3_BUCKET} 34 + ''; 35 + 36 + restoreScript = pkgs.writeShellApplication { 37 + name = "pds-litestream-restore"; 38 + runtimeInputs = with pkgs; [ 39 + awscli2 40 + litestream 41 + gnugrep 42 + gnused 43 + coreutils 44 + findutils 45 + gawk 46 + ]; 47 + excludeShellChecks = [ 48 + "SC1091" 49 + "SC2046" 50 + "SC2168" 51 + "SC1090" 52 + "SC2043" 53 + ]; 54 + text = '' 55 + main() { 56 + set -euo pipefail 57 + 58 + echo "[PDS Restore] Starting automatic restore from S3..." 59 + 60 + for f in ${toString secretsFiles}; do 61 + if [ -f "$f" ]; then 62 + set -a 63 + source "$f" 64 + set +a 65 + else 66 + echo "[PDS Restore] Error: Secrets file not found: $f" 67 + exit 1 68 + fi 69 + done 70 + 71 + if [ -z "''${S3_BUCKET:-}" ]; then 72 + echo "[PDS Restore] Error: S3_BUCKET not set in secrets file" 73 + exit 1 74 + fi 75 + 76 + s3Bucket="''${S3_BUCKET}" 77 + s3Prefix="${cfg.s3Prefix}" 78 + 79 + run_aws() { 80 + local envArgs=() 81 + if [ -n "''${AWS_ENDPOINT_URL:-}" ]; then 82 + envArgs+=("AWS_ENDPOINT_URL=''${AWS_ENDPOINT_URL}") 83 + fi 84 + env "''${envArgs[@]}" aws "$@" 85 + } 86 + 87 + run_litestream() { 88 + local envArgs=() 89 + if [ -n "''${AWS_ENDPOINT_URL:-}" ]; then 90 + envArgs+=("AWS_ENDPOINT_URL=''${AWS_ENDPOINT_URL}") 91 + fi 92 + env "''${envArgs[@]}" litestream "$@" 93 + } 94 + 95 + echo "[PDS Restore] Verifying S3 connectivity..." 96 + local retries=5 97 + local connected=false 98 + for i in $(seq 1 $retries); do 99 + if run_aws s3 ls "s3://$s3Bucket/" &>/dev/null; then 100 + connected=true 101 + break 102 + fi 103 + echo "[PDS Restore] Waiting for S3 bucket (attempt $i/$retries)..." 104 + sleep 2 105 + done 106 + 107 + if [ "$connected" = false ]; then 108 + echo "[PDS Restore] Error: Cannot connect to S3 bucket: $s3Bucket" 109 + exit 1 110 + fi 111 + 112 + echo "[PDS Restore] S3 connection verified" 113 + 114 + local objects 115 + objects=$(run_aws s3 ls "s3://$s3Bucket/$s3Prefix" --recursive 2>/dev/null | awk '{print $4}' || true) 116 + 117 + local databases 118 + databases=$(echo "$objects" | grep '\.sqlite/' | sed 's|.*/\([^/]*\.sqlite\).*|\1|' | sort -u || true) 119 + 120 + if [ -z "$databases" ]; then 121 + echo "[PDS Restore] No databases found in S3 at s3://$s3Bucket/$s3Prefix" 122 + echo "[PDS Restore] New deployment - skipping restore." 123 + exit 0 124 + fi 125 + 126 + echo "[PDS Restore] Found databases to restore:" 127 + echo "$databases" 128 + echo "" 129 + 130 + mkdir -p "${cfg.pdsDataDir}" 131 + 132 + local restoredCount=0 133 + for db in $databases; do 134 + local localPath="${cfg.pdsDataDir}/$db" 135 + local s3DbPath="$s3Prefix/$db" 136 + local s3DbUrl="s3://$s3Bucket/$s3DbPath" 137 + 138 + if [ -f "$localPath" ]; then 139 + echo "[PDS Restore] Database already exists locally: $db (skipping)" 140 + continue 141 + fi 142 + 143 + echo "[PDS Restore] Restoring database: $db" 144 + mkdir -p "$(dirname "$localPath")" 145 + 146 + if run_litestream restore -if-db-not-exists -if-replica-exists -o "$localPath" "$s3DbUrl"; then 147 + echo "[PDS Restore] Successfully restored: $db" 148 + restoredCount=$((restoredCount + 1)) 149 + 150 + if [ -f "$localPath" ]; then 151 + chown ${pdsUser}:${pdsGroup} "$localPath" 152 + chmod 644 "$localPath" 153 + fi 154 + else 155 + echo "[PDS Restore] Warning: Failed to restore $db" 156 + fi 157 + done 158 + 159 + echo "" 160 + echo "[PDS Restore] Restore completed. Restored $restoredCount database(s)." 161 + 162 + if [ $restoredCount -eq 0 ]; then 163 + echo "[PDS Restore] No new databases restored. PDS will start with fresh state." 164 + fi 165 + } 166 + 167 + main 168 + ''; 169 + }; 170 + 171 + healthCheckScript = pkgs.writeShellScript "pds-healthcheck" '' 172 + set -euo pipefail 173 + 174 + if ! systemctl is-active --quiet bluesky-pds; then 175 + echo "[PDS HealthCheck] PDS service is not running" 176 + exit 1 177 + fi 178 + 179 + if [ -f "${cfg.pdsDataDir}/primary.sqlite" ]; then 180 + if ! systemctl is-active --quiet litestream-pds; then 181 + echo "[PDS HealthCheck] Litestream service is not running" 182 + exit 1 183 + fi 184 + fi 185 + 186 + echo "[PDS HealthCheck] All services healthy" 187 + exit 0 188 + ''; 189 + in 190 + { 191 + options.services.pds-with-backups = { 192 + enable = mkEnableOption "Zero-Touch Recovery PDS with Litestream and S3 blob storage"; 193 + 194 + domain = mkOption { 195 + type = types.str; 196 + description = "PDS domain name (e.g., bsky.example.com)."; 197 + example = "bsky.example.com"; 198 + }; 199 + 200 + pdsDataDir = mkOption { 201 + type = types.str; 202 + default = "/var/lib/pds"; 203 + description = "PDS data directory for SQLite databases."; 204 + }; 205 + 206 + secretsFiles = mkOption { 207 + type = types.listOf types.path; 208 + description = '' 209 + List of paths to secrets files in dotenv format. 210 + All files will be sourced to load credentials. 211 + Required variables: PDS_JWT_SECRET, PDS_ADMIN_PASSWORD, PDS_PLC_ROTATION_KEY_K256_PRIVATE_KEY_HEX, 212 + AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, S3_BUCKET. 213 + Optional: AWS_ENDPOINT_URL. 214 + ''; 215 + example = [ "/run/secrets/pds.env" ]; 216 + }; 217 + 218 + s3Prefix = mkOption { 219 + type = types.strMatching "[^/].*[^/]"; 220 + default = "pds"; 221 + description = "S3 directory prefix for Litestream replicas."; 222 + example = "pds-backups"; 223 + }; 224 + 225 + pdsSettings = mkOption { 226 + type = types.attrs; 227 + default = { }; 228 + description = "Additional settings to pass to bluesky-pds."; 229 + example = { 230 + PDS_PORT = 3000; 231 + PDS_DISABLE_PHONE_VERIFICATION = "true"; 232 + }; 233 + }; 234 + 235 + backupLogDir = mkOption { 236 + type = types.path; 237 + default = "/var/log/pds-backup"; 238 + description = "Directory for backup and restore logs."; 239 + }; 240 + }; 241 + 242 + config = mkIf cfg.enable { 243 + services.bluesky-pds = { 244 + enable = mkDefault true; 245 + settings = mkMerge [ 246 + { 247 + PDS_HOSTNAME = cfg.domain; 248 + PDS_SQLITE_DISABLE_WAL_AUTO_CHECKPOINT = "true"; 249 + PDS_DATA_DIRECTORY = cfg.pdsDataDir; 250 + } 251 + cfg.pdsSettings 252 + ]; 253 + environmentFiles = secretsFiles; 254 + }; 255 + 256 + users.users.${pdsUser} = { 257 + isSystemUser = true; 258 + group = pdsGroup; 259 + description = "Bluesky PDS service user"; 260 + }; 261 + users.groups.${pdsGroup} = { }; 262 + 263 + systemd.tmpfiles.rules = [ 264 + "d ${cfg.pdsDataDir} 0755 ${pdsUser} ${pdsGroup} -" 265 + "d ${cfg.backupLogDir} 0755 ${pdsUser} ${pdsGroup} -" 266 + ]; 267 + 268 + systemd.services.bluesky-pds = { 269 + after = [ 270 + "network.target" 271 + "pds-restore.service" 272 + ]; 273 + wants = [ "pds-restore.service" ]; 274 + serviceConfig.Restart = lib.mkDefault "on-failure"; 275 + serviceConfig.RestartSec = "10s"; 276 + }; 277 + 278 + systemd.services.pds-restore = { 279 + description = "PDS Automatic Restore from S3"; 280 + wantedBy = [ "multi-user.target" ]; 281 + before = [ "bluesky-pds.service" ]; 282 + 283 + serviceConfig = { 284 + Type = "oneshot"; 285 + ExecStart = "${restoreScript}/bin/pds-litestream-restore"; 286 + EnvironmentFile = secretsFiles; 287 + User = "root"; 288 + Group = "root"; 289 + RemainAfterExit = true; 290 + 291 + NoNewPrivileges = true; 292 + ProtectSystem = "strict"; 293 + ProtectHome = true; 294 + PrivateTmp = true; 295 + RestrictRealtime = true; 296 + }; 297 + }; 298 + 299 + systemd.services.litestream-pds = { 300 + description = "Litestream real-time replication for PDS databases"; 301 + after = [ 302 + "network.target" 303 + "pds-restore.service" 304 + "bluesky-pds.service" 305 + ]; 306 + requires = [ "bluesky-pds.service" ]; 307 + wantedBy = [ "multi-user.target" ]; 308 + 309 + serviceConfig = { 310 + ExecStart = "${pkgs.litestream}/bin/litestream replicate -config ${litestreamConfig}"; 311 + EnvironmentFile = secretsFiles; 312 + User = pdsUser; 313 + Group = pdsGroup; 314 + Restart = "on-failure"; 315 + RestartSec = "5s"; 316 + 317 + NoNewPrivileges = true; 318 + ProtectSystem = "strict"; 319 + ProtectHome = true; 320 + ReadWritePaths = [ 321 + cfg.pdsDataDir 322 + cfg.backupLogDir 323 + ]; 324 + RestrictRealtime = true; 325 + MemoryDenyWriteExecute = true; 326 + }; 327 + }; 328 + 329 + systemd.services.pds-healthcheck = { 330 + description = "PDS Health Check"; 331 + after = [ 332 + "bluesky-pds.service" 333 + "litestream-pds.service" 334 + ]; 335 + requires = [ "bluesky-pds.service" ]; 336 + 337 + serviceConfig = { 338 + Type = "oneshot"; 339 + ExecStart = healthCheckScript; 340 + User = "root"; 341 + Group = "root"; 342 + 343 + NoNewPrivileges = true; 344 + ProtectSystem = "strict"; 345 + ProtectHome = true; 346 + }; 347 + 348 + startAt = "hourly"; 349 + }; 350 + 351 + systemd.timers.pds-healthcheck = { 352 + wantedBy = [ "timers.target" ]; 353 + timerConfig = { 354 + OnCalendar = "hourly"; 355 + Persistent = true; 356 + }; 357 + }; 358 + 359 + environment.systemPackages = with pkgs; [ 360 + litestream 361 + restoreScript 362 + awscli2 363 + ]; 364 + }; 365 + }
+163
modules/pds/pds-recovery-full.nix
··· 1 + { nixpkgs, pkgs }: 2 + let 3 + _pkgs = import nixpkgs { 4 + config = { }; 5 + system = pkgs.stdenv.hostPlatform.system; 6 + overlays = [ (import ../overlays) ]; 7 + }; 8 + in 9 + _pkgs.testers.runNixOSTest { 10 + name = "pds-full"; 11 + meta.maintainers = [ ]; 12 + 13 + nodes.machine = 14 + { pkgs, ... }: 15 + { 16 + imports = [ ./default.nix ]; 17 + 18 + services.minio = { 19 + enable = true; 20 + rootCredentialsFile = "/tmp/minio-credentials"; 21 + }; 22 + 23 + systemd.tmpfiles.rules = [ 24 + "f /tmp/minio-credentials 0600 root root - MINIO_ROOT_USER=minioadmin\\nMINIO_ROOT_PASSWORD=minioadmin123" 25 + "f /run/secrets/s3.env 0600 root root - AWS_ACCESS_KEY_ID=minioadmin\\nAWS_SECRET_ACCESS_KEY=minioadmin123\\nAWS_ENDPOINT_URL=http://127.0.0.1:9000\\nS3_BUCKET=pds-test-bucket" 26 + "f /run/secrets/pds.env 0600 root root - PDS_JWT_SECRET=test-jwt-secret-for-full-testing\\nPDS_ADMIN_PASSWORD=test-admin-password\\nPDS_PLC_ROTATION_KEY_K256_PRIVATE_KEY_HEX=1111111111111111111111111111111111111111111111111111111111111111" 27 + ]; 28 + 29 + services.pds-with-backups = { 30 + enable = true; 31 + domain = "test.example.com"; 32 + pdsDataDir = "/var/lib/pds"; 33 + secretsFiles = [ 34 + "/run/secrets/pds.env" 35 + "/run/secrets/s3.env" 36 + ]; 37 + s3Bucket = "pds-test-bucket"; 38 + s3Prefix = "pds-replica"; 39 + enableStatelessBlobs = false; 40 + pdsSettings = { 41 + PDS_PORT = 3000; 42 + PDS_DISABLE_PHONE_VERIFICATION = "true"; 43 + }; 44 + }; 45 + 46 + environment.systemPackages = with pkgs; [ 47 + sqlite 48 + curl 49 + jq 50 + minio-client 51 + awscli2 52 + ]; 53 + }; 54 + 55 + testScript = '' 56 + import json 57 + 58 + machine.start() 59 + 60 + print("=== PDS Recovery Full Integration Test ===") 61 + 62 + print("\n--- Setting up MinIO ---") 63 + machine.wait_for_unit("minio.service") 64 + machine.wait_for_open_port(9000) 65 + machine.succeed("sleep 5") 66 + 67 + machine.succeed("test -f /tmp/minio-credentials") 68 + machine.succeed("test -f /run/secrets/s3.env") 69 + machine.succeed("test -f /run/secrets/pds.env") 70 + 71 + machine.succeed("mc alias set local http://127.0.0.1:9000 minioadmin minioadmin123") 72 + machine.succeed("mc mb local/pds-test-bucket --ignore-existing") 73 + print(" [PASS] MinIO bucket created") 74 + 75 + print("\n--- Test 1: PDS Initialization ---") 76 + machine.succeed("systemctl start pds-restore") 77 + machine.wait_for_unit("pds-restore.service") 78 + 79 + machine.succeed("systemctl start bluesky-pds") 80 + machine.wait_for_unit("bluesky-pds.service") 81 + machine.wait_for_open_port(3000) 82 + 83 + machine.succeed("sleep 30") 84 + 85 + health_response = machine.succeed("curl -s http://127.0.0.1:3000/xrpc/_health") 86 + try: 87 + health_data = json.loads(health_response) 88 + assert "version" in health_data or "status" in health_data, f"Unexpected health response: {health_response}" 89 + print(f" [PASS] PDS is running and healthy: {health_response}") 90 + except json.JSONDecodeError: 91 + assert health_response.strip() in ["", "OK"], f"Unexpected health response: {health_response}" 92 + print(" [PASS] PDS is running and healthy (empty response)") 93 + 94 + print("\n--- Test 2: Litestream replication ---") 95 + machine.wait_for_unit("litestream-pds.service") 96 + machine.succeed("systemctl status litestream-pds.service") 97 + print(" [PASS] Litestream service is running") 98 + 99 + print("\n--- Test 3: Database creation ---") 100 + machine.succeed("sleep 30") 101 + pds_files = machine.succeed("ls -la /var/lib/pds/") 102 + print(f" Files in /var/lib/pds: {pds_files}") 103 + 104 + sqlite_files = machine.succeed("find /var/lib/pds -name '*.sqlite' 2>/dev/null || true") 105 + assert sqlite_files.strip() != "", f"No sqlite files found in /var/lib/pds. Output: {sqlite_files}" 106 + print(f" Found sqlite files: {sqlite_files.strip()}") 107 + print(" [PASS] Database files created") 108 + 109 + print("\n--- Test 4: Simulating disaster (data loss) ---") 110 + machine.succeed("systemctl stop bluesky-pds litestream-pds") 111 + 112 + machine.succeed("rm -rf /var/lib/pds/*") 113 + remaining = machine.succeed("find /var/lib/pds -name '*.sqlite' 2>/dev/null || true") 114 + assert remaining.strip() == "", "Should have no sqlite files after deletion" 115 + print(" [PASS] All data deleted - simulating complete server failure") 116 + 117 + print("\n--- Test 5: Automatic restore from S3 ---") 118 + restore_result = machine.succeed("pds-litestream-restore 2>&1") 119 + print(f" Restore output: {restore_result}") 120 + 121 + restored_sqlite = machine.succeed("find /var/lib/pds -name '*.sqlite' 2>/dev/null || true") 122 + assert restored_sqlite.strip() != "", "Expected databases to be restored from S3" 123 + print(f" Restored sqlite files: {restored_sqlite.strip()}") 124 + print(" [PASS] Databases restored from S3") 125 + 126 + print("\n--- Test 6: Data integrity verification ---") 127 + machine.succeed("systemctl start bluesky-pds") 128 + machine.wait_for_unit("bluesky-pds.service") 129 + machine.wait_for_open_port(3000) 130 + machine.succeed("sleep 30") 131 + 132 + health_response2 = machine.succeed("curl -s http://127.0.0.1:3000/xrpc/_health") 133 + try: 134 + health_data2 = json.loads(health_response2) 135 + assert "version" in health_data2, f"Unexpected health response: {health_response2}" 136 + print(" [PASS] PDS is healthy after recovery") 137 + except json.JSONDecodeError: 138 + pass 139 + 140 + print("\n--- Test 7: Litestream resumption ---") 141 + machine.succeed("systemctl start litestream-pds") 142 + machine.wait_for_unit("litestream-pds.service") 143 + machine.succeed("sleep 10") 144 + 145 + final_minio = machine.succeed("mc ls local/pds-test-bucket/pds-replica/ --recursive 2>/dev/null") 146 + assert ".sqlite" in final_minio, "Expected ongoing replication" 147 + print(" [PASS] Litestream resumed replication after recovery") 148 + 149 + print("\n--- Test 8: Health check service ---") 150 + machine.succeed("systemctl start pds-healthcheck") 151 + machine.succeed("sleep 2") 152 + health_log = machine.succeed("journalctl -u pds-healthcheck.service -o cat 2>/dev/null || true") 153 + assert "All services healthy" in health_log, f"Expected health check message, got: {health_log}" 154 + print(" [PASS] Health check service working") 155 + 156 + print("\n" + "="*60) 157 + print("ALL FULL INTEGRATION TESTS PASSED") 158 + print("Zero-touch disaster recovery verified successfully") 159 + print("Data integrity maintained through backup/restore cycle") 160 + print("Multiple secrets files work correctly") 161 + print("="*60) 162 + ''; 163 + }
+118
modules/pds/pds-recovery-simple.nix
··· 1 + { nixpkgs, pkgs }: 2 + let 3 + _pkgs = import nixpkgs { 4 + config = { }; 5 + system = pkgs.stdenv.hostPlatform.system; 6 + overlays = [ (import ../overlays) ]; 7 + }; 8 + in 9 + _pkgs.testers.runNixOSTest { 10 + name = "pds-simple"; 11 + meta.maintainers = [ ]; 12 + 13 + nodes.machine = 14 + { pkgs, lib, ... }: 15 + { 16 + imports = [ ./default.nix ]; 17 + 18 + services.pds-with-backups = { 19 + enable = true; 20 + domain = "test.example.com"; 21 + pdsDataDir = "/var/lib/pds"; 22 + secretsFiles = [ 23 + "/run/secrets/pds.env" 24 + "/run/secrets/s3.env" 25 + ]; 26 + s3Bucket = "test-bucket"; 27 + s3Prefix = "test-pds"; 28 + enableStatelessBlobs = false; 29 + pdsSettings = { 30 + PDS_PORT = 3000; 31 + PDS_DISABLE_PHONE_VERIFICATION = "true"; 32 + }; 33 + }; 34 + 35 + systemd.tmpfiles.rules = [ 36 + "f /run/secrets/pds.env 0644 root root - PDS_JWT_SECRET=test-jwt-secret\nPDS_ADMIN_PASSWORD=test-password\nPDS_PLC_ROTATION_KEY_K256_PRIVATE_KEY_HEX=0000000000000000000000000000000000000000000000000000000000000000" 37 + "f /run/secrets/s3.env 0644 root root - AWS_ACCESS_KEY_ID=test-key\nAWS_SECRET_ACCESS_KEY=test-secret\nAWS_ENDPOINT_URL=https://s3.test.example.com\nS3_BUCKET=test-bucket" 38 + ]; 39 + 40 + services.bluesky-pds.enable = true; 41 + systemd.services.bluesky-pds.serviceConfig.ExecStart = 42 + lib.mkForce "${pkgs.coreutils}/bin/echo 'PDS mocked for testing'"; 43 + 44 + environment.systemPackages = with pkgs; [ 45 + sqlite 46 + coreutils 47 + ]; 48 + }; 49 + 50 + testScript = '' 51 + machine.start() 52 + machine.wait_for_unit("multi-user.target") 53 + 54 + print("=== PDS Recovery Simple Configuration Tests ===") 55 + 56 + print("\n--- Test 1: Systemd service creation ---") 57 + services_to_check = [ 58 + "/etc/systemd/system/pds-restore.service", 59 + "/etc/systemd/system/litestream-pds.service", 60 + "/etc/systemd/system/pds-healthcheck.timer" 61 + ] 62 + 63 + for service in services_to_check: 64 + machine.succeed(f"test -f {service}") 65 + print(f" [PASS] {service} exists") 66 + 67 + print("\n--- Test 2: User and group creation ---") 68 + machine.succeed("getent passwd pds") 69 + machine.succeed("getent group pds") 70 + print(" [PASS] PDS user and group exist") 71 + 72 + print("\n--- Test 3: Directory creation ---") 73 + machine.succeed("test -d /var/lib/pds") 74 + machine.succeed("test -d /var/log/pds-backup") 75 + pds_dir_info = machine.succeed("stat -c '%U:%G %a' /var/lib/pds") 76 + assert "pds:pds" in pds_dir_info, f"Unexpected ownership: {pds_dir_info}" 77 + print(" [PASS] Directories created with correct permissions") 78 + 79 + print("\n--- Test 4: Secrets files ---") 80 + machine.succeed("test -f /run/secrets/pds.env") 81 + machine.succeed("test -f /run/secrets/s3.env") 82 + print(" [PASS] Secrets files exist") 83 + 84 + print("\n--- Test 5: Restore script availability ---") 85 + machine.succeed("which pds-litestream-restore") 86 + print(" [PASS] Restore script is available") 87 + 88 + print("\n--- Test 6: pds-restore service configuration ---") 89 + restore_config = machine.succeed("systemctl cat pds-restore.service") 90 + assert "Type=oneshot" in restore_config, "pds-restore should be oneshot" 91 + assert "RemainAfterExit=true" in restore_config, "pds-restore should remain after exit" 92 + assert "/run/secrets/pds.env" in restore_config, "Restore should use pds.env" 93 + assert "/run/secrets/s3.env" in restore_config, "Restore should use s3.env" 94 + print(" [PASS] pds-restore configured correctly") 95 + 96 + print("\n--- Test 7: litestream-pds service configuration ---") 97 + litestream_config = machine.succeed("systemctl cat litestream-pds.service") 98 + assert "User=pds" in litestream_config, "litestream should run as pds user" 99 + assert "Restart=on-failure" in litestream_config, "litestream should restart on failure" 100 + assert "/run/secrets/pds.env" in litestream_config, "Litestream should use pds.env" 101 + assert "/run/secrets/s3.env" in litestream_config, "Litestream should use s3.env" 102 + print(" [PASS] litestream-pds configured correctly") 103 + 104 + print("\n--- Test 8: PDS service dependencies ---") 105 + pds_config = machine.succeed("systemctl cat bluesky-pds.service") 106 + assert "pds-restore.service" in pds_config, "PDS should depend on restore service" 107 + print(" [PASS] PDS service correctly depends on restore") 108 + 109 + print("\n--- Test 9: Package dependencies ---") 110 + machine.succeed("which litestream") 111 + machine.succeed("which aws") 112 + print(" [PASS] Required packages are available") 113 + 114 + print("\n" + "="*60) 115 + print("ALL SIMPLE CONFIGURATION TESTS PASSED") 116 + print("="*60) 117 + ''; 118 + }
-319
modules/services/pds.nix
··· 1 - { 2 - config, 3 - lib, 4 - pkgs, 5 - ... 6 - }: 7 - let 8 - cfg = config.services.pds-backup; 9 - 10 - pdsUser = config.systemd.services.bluesky-pds.serviceConfig.User or "pds"; 11 - pdsGroup = config.systemd.services.bluesky-pds.serviceConfig.Group or "pds"; 12 - 13 - restoreScript = pkgs.writeShellApplication { 14 - name = "pds-restore"; 15 - runtimeInputs = with pkgs; [ 16 - awscli2 17 - gnutar 18 - coreutils 19 - ]; 20 - excludeShellChecks = [ "SC1091" ]; 21 - text = '' 22 - echo "Starting PDS restore..." 23 - 24 - if [ -f "${cfg.s3CredentialsFile}" ]; then 25 - set -a; source "${cfg.s3CredentialsFile}"; set +a 26 - else 27 - echo "Error: Credentials file not found at ${cfg.s3CredentialsFile}" 28 - exit 1 29 - fi 30 - 31 - backups=$(aws s3 ls "s3://$S3_BUCKET/backups/") 32 - if [ -z "$backups" ]; then 33 - echo "Error: No backups found in S3" 34 - exit 1 35 - fi 36 - 37 - LATEST=$(echo "$backups" | sort | tail -1 | awk '{print $4}') 38 - echo "Latest backup: $LATEST" 39 - 40 - local_file="/tmp/$LATEST" 41 - echo "Downloading backup..." 42 - if ! aws s3 cp "s3://$S3_BUCKET/backups/$LATEST" "$local_file"; then 43 - echo "Error: Failed to download backup" 44 - exit 1 45 - fi 46 - 47 - echo "Stopping PDS service..." 48 - systemctl stop bluesky-pds 49 - 50 - echo "Clearing existing data..." 51 - rm -rf ${cfg.pdsDataDir}/* 52 - 53 - echo "Extracting backup..." 54 - tar -xzf "$local_file" -C ${cfg.pdsDataDir} 55 - rm -f "$local_file" 56 - 57 - echo "Setting ownership..." 58 - chown -R ${pdsUser}:${pdsGroup} ${cfg.pdsDataDir} 59 - 60 - echo "Starting PDS service..." 61 - systemctl start bluesky-pds 62 - 63 - echo "Restore completed successfully." 64 - ''; 65 - }; 66 - 67 - backupScript = pkgs.writeShellApplication { 68 - name = "pds-backup-script"; 69 - runtimeInputs = with pkgs; [ 70 - awscli2 71 - gnutar 72 - gzip 73 - coreutils 74 - ]; 75 - bashOptions = [ "errexit" ]; 76 - text = '' 77 - log() { 78 - echo "$(date): $1" | tee -a "$LOG_FILE" 79 - } 80 - 81 - fail() { 82 - log "ERROR: $1" 83 - systemctl restart bluesky-pds 2>/dev/null || log "WARNING: Failed to restart PDS service" 84 - exit 1 85 - } 86 - 87 - cleanup_old_logs() { 88 - find "$LOG_DIR" -name "*.log" -mtime +90 -delete 89 - if [ "$(find "$LOG_FILE" -mtime +30 2>/dev/null)" ]; then 90 - mv "$LOG_FILE" "$LOG_FILE.old" && touch "$LOG_FILE" 91 - fi 92 - if [ "$(wc -l < "$LOG_FILE" 2>/dev/null)" -gt 1000 ]; then 93 - mv "$LOG_FILE" "$LOG_FILE.old" && touch "$LOG_FILE" 94 - fi 95 - } 96 - 97 - mkdir -p "$LOG_DIR" 98 - DATE_LABEL=$(date +"%Y%m%d-%H%M") 99 - LOG_FILE="$LOG_DIR/$DATE_LABEL.log" 100 - ARCHIVE_FILE="/tmp/pds-backup-$DATE_LABEL.tar.gz" 101 - ARCHIVE_NAME="$DATE_LABEL.tar.gz" 102 - 103 - log "Starting backup..." 104 - 105 - if ! systemctl list-units --full -all | grep -Fq "bluesky-pds.service"; then 106 - fail "PDS service not found" 107 - fi 108 - 109 - log "Stopping PDS service..." 110 - if ! systemctl stop bluesky-pds 2>/dev/null; then 111 - log "Failed to stop PDS service" 112 - fi 113 - 114 - if [ ! -d "$PDS_DATA_DIR" ]; then 115 - fail "Source directory $PDS_DATA_DIR does not exist" 116 - fi 117 - 118 - log "Creating archive..." 119 - if ! tar -czf "$ARCHIVE_FILE" -C "$PDS_DATA_DIR" . 2>> "$LOG_FILE"; then 120 - fail "Failed to create archive" 121 - fi 122 - 123 - log "Uploading to S3..." 124 - attempt=1 125 - while [ "$attempt" -le "$MAX_RETRIES" ]; do 126 - if aws s3 cp "$ARCHIVE_FILE" "s3://$S3_BUCKET/backups/$ARCHIVE_NAME" 2>> "$LOG_FILE"; then 127 - log "Upload successful" 128 - break 129 - else 130 - if [ "$attempt" -lt "$MAX_RETRIES" ]; then 131 - log "Upload failed, retrying in $RETRY_INTERVAL seconds..." 132 - sleep "$RETRY_INTERVAL" 133 - else 134 - fail "Upload failed after $MAX_RETRIES attempts" 135 - fi 136 - fi 137 - ((attempt++)) 138 - done 139 - 140 - rm -f "$ARCHIVE_FILE" 141 - 142 - log "Starting PDS service..." 143 - if ! systemctl start bluesky-pds 2>/dev/null; then 144 - fail "Failed to start PDS service" 145 - fi 146 - 147 - log "Cleaning up old logs..." 148 - cleanup_old_logs 149 - 150 - log "Backup completed successfully" 151 - ''; 152 - }; 153 - 154 - litestreamConfigFile = pkgs.writeText "litestream-pds-config.yml" '' 155 - dbs: 156 - - dir: ${cfg.pdsDataDir} 157 - pattern: "*.sqlite" 158 - recursive: true 159 - watch: true 160 - replica: 161 - type: s3 162 - path: ${cfg.s3Prefix} 163 - endpoint: ''${AWS_ENDPOINT_URL} 164 - bucket: ''${S3_BUCKET} 165 - access-key-id: ''${AWS_ACCESS_KEY_ID} 166 - secret-access-key: ''${AWS_SECRET_ACCESS_KEY} 167 - ''; 168 - 169 - litestreamRestore = pkgs.writeShellApplication { 170 - name = "pds-litestream-restore"; 171 - runtimeInputs = with pkgs; [ 172 - awscli2 173 - litestream 174 - gnugrep 175 - coreutils 176 - ]; 177 - excludeShellChecks = [ "SC1091" ]; 178 - text = '' 179 - set -e 180 - 181 - if [ -f "${cfg.s3CredentialsFile}" ]; then 182 - set -a; source "${cfg.s3CredentialsFile}"; set +a 183 - else 184 - echo "Error: Credentials file not found at ${cfg.s3CredentialsFile}" 185 - exit 1 186 - fi 187 - 188 - systemctl stop bluesky-pds 189 - 190 - S3_PREFIX="${cfg.s3Prefix}/" 191 - S3_URI="s3://$S3_BUCKET/$S3_PREFIX" 192 - MAP=$(aws s3 ls "$S3_URI" --recursive --endpoint-url "$AWS_ENDPOINT_URL" | grep -oE "$S3_PREFIX.+\.sqlite/" | sort -u) 193 - 194 - if [ -z "$MAP" ]; then 195 - echo "No databases found in S3." 196 - exit 2 197 - fi 198 - 199 - for S3_DB_PATH in $MAP; do 200 - REL_PATH=''${S3_DB_PATH#"$S3_PREFIX"} 201 - REL_PATH=''${REL_PATH%/} 202 - S3_DB_REPLICA_URL="s3://$S3_BUCKET/$S3_DB_PATH?endpoint=$AWS_ENDPOINT_URL" 203 - S3_DB_REPLICA_URL=''${S3_DB_REPLICA_URL%/} 204 - 205 - litestream restore -if-db-not-exists -if-replica-exists -o "${cfg.pdsDataDir}/$REL_PATH" "$S3_DB_REPLICA_URL" 206 - 207 - chown ${pdsUser}:${pdsGroup} "${cfg.pdsDataDir}/$REL_PATH" 208 - done 209 - 210 - systemctl start bluesky-pds 211 - ''; 212 - }; 213 - in 214 - { 215 - options.services.pds-backup = { 216 - enable = lib.mkEnableOption "PDS backup with Litestream and S3 archive"; 217 - pdsDataDir = lib.mkOption { 218 - type = lib.types.path; 219 - default = "/var/lib/pds"; 220 - description = "PDS data directory."; 221 - }; 222 - pdsSecretsFile = lib.mkOption { 223 - type = lib.types.path; 224 - description = "Path to PDS secrets file (dotenv format)."; 225 - }; 226 - s3Prefix = lib.mkOption { 227 - type = lib.types.strMatching "[^/].*[^/]"; 228 - default = "pds"; 229 - description = "S3 directory subpath."; 230 - }; 231 - s3CredentialsFile = lib.mkOption { 232 - type = lib.types.path; 233 - description = "Path to S3 credentials file (containing AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, S3_BUCKET, etc)."; 234 - }; 235 - pdsSettings = lib.mkOption { 236 - type = lib.types.attrs; 237 - default = { }; 238 - description = "Additional settings to pass to bluesky-pds."; 239 - }; 240 - backupLogDir = lib.mkOption { 241 - type = lib.types.path; 242 - default = "/var/log/pds-backup"; 243 - description = "Directory for backup logs."; 244 - }; 245 - maxRetries = lib.mkOption { 246 - type = lib.types.int; 247 - default = 3; 248 - description = "Maximum number of retry attempts for S3 upload."; 249 - }; 250 - retryInterval = lib.mkOption { 251 - type = lib.types.int; 252 - default = 60; 253 - description = "Seconds to wait between retry attempts."; 254 - }; 255 - }; 256 - 257 - config = lib.mkIf cfg.enable { 258 - services.bluesky-pds = { 259 - enable = true; 260 - settings = lib.mkMerge [ 261 - { PDS_SQLITE_DISABLE_WAL_AUTO_CHECKPOINT = "true"; } 262 - cfg.pdsSettings 263 - ]; 264 - environmentFiles = [ cfg.pdsSecretsFile ]; 265 - }; 266 - 267 - systemd.services.litestream-pds = { 268 - description = "Litestream backup for PDS databases"; 269 - after = [ 270 - "network.target" 271 - "bluesky-pds.service" 272 - ]; 273 - requires = [ "bluesky-pds.service" ]; 274 - wantedBy = [ "multi-user.target" ]; 275 - 276 - serviceConfig = { 277 - ExecStart = "${pkgs.litestream}/bin/litestream replicate -config ${litestreamConfigFile}"; 278 - EnvironmentFile = cfg.s3CredentialsFile; 279 - User = pdsUser; 280 - Group = pdsGroup; 281 - Restart = "on-failure"; 282 - RestartSec = "5s"; 283 - NoNewPrivileges = true; 284 - ProtectSystem = "full"; 285 - RestrictRealtime = true; 286 - }; 287 - }; 288 - 289 - systemd.services.pds-backup = { 290 - description = "Backup PDS data to S3"; 291 - serviceConfig = { 292 - ExecStart = "${backupScript}/bin/pds-backup-script"; 293 - Environment = [ 294 - "PDS_DATA_DIR=${cfg.pdsDataDir}" 295 - "LOG_DIR=${cfg.backupLogDir}" 296 - "MAX_RETRIES=${toString cfg.maxRetries}" 297 - "RETRY_INTERVAL=${toString cfg.retryInterval}" 298 - ]; 299 - EnvironmentFile = [ cfg.s3CredentialsFile ]; 300 - User = "root"; 301 - Type = "oneshot"; 302 - }; 303 - }; 304 - 305 - systemd.timers.pds-backup = { 306 - wantedBy = [ "timers.target" ]; 307 - timerConfig = { 308 - OnCalendar = "daily"; 309 - Persistent = true; 310 - }; 311 - }; 312 - 313 - environment.systemPackages = [ 314 - restoreScript 315 - litestreamRestore 316 - pkgs.litestream 317 - ]; 318 - }; 319 - }
+2 -6
secrets/pds-backup-s3.env
··· 1 - AWS_ACCESS_KEY_ID=ENC[AES256_GCM,data:5oKU0U+FeR666BTNL7gdb8Wnts/nvhCYDlGtetcq+Dc=,iv:i3i4ykyAR3tpfpDoke6renys9tZp/a6/JpRe7ajAnsg=,tag:S3yDGCIl+wG9tedXbcPbmw==,type:str] 2 - AWS_SECRET_ACCESS_KEY=ENC[AES256_GCM,data:3UEACvF1q20QRdSxVcTrMJNsjZmSsGHmX0fq98KMvnJJffjYO6h+ClNT+38aDhgh8Dd2VRujYMVTI6RXPn28DQ==,iv:bBEs9lMhDMoeH3HymUQEOT8Y13457xcrevKI89nXBeg=,tag:eSekCTOKUu2u1S7/Mcy9sg==,type:str] 3 - AWS_ENDPOINT_URL=ENC[AES256_GCM,data:bqp2c1pPg1ZWG2w8XF9dm+3wKMYFucxzNLmvlhoKYuIxJX5CZwUSrmDXsx4FXZtq0l2fIWUIMCRurE49Jq8QtL8M6GQ=,iv:j4uL5y6E3qd6KS7DALc/GdL2/HgpheQj2J/uuLZ7XZU=,tag:dWVbavob4IC02bY+EqfYGA==,type:str] 4 - S3_BUCKET=ENC[AES256_GCM,data:1q3k8mjG6kM=,iv:3nktB4AvrGmH6MfIijV3mPajO9088UWfuBOeM99hwCQ=,tag:cfLCokyYHWqvtCeokIGrtg==,type:str] 5 1 sops_age__list_0__map_enc=-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB2SkJUdWdYclR0U1pPUHpn\nc3pWSml5b3FOR3cvaVhNczJrbHA4NG83U1JZClVibTgyc3JBWC9LSXVJVmhZckxT\nSk1sN040MmRKd3BodUdFOVRxMTdGcUEKLS0tIHhjZUs5TXhhMm1nK3RQc3BJMTJG\nZmQ5cGlRbXNiczBwUnd1aTFaWk94bUkKjQEr03lQRuWxzQ6uTCRgpTj3C/FwBFwz\nQoYYAyqN5RBAJvN+7TFewgGgSBu+bE2RFazAxOizdXQXAmgceZnT5A==\n-----END AGE ENCRYPTED FILE-----\n 6 2 sops_age__list_0__map_recipient=age17cxj5zwkkxjkjvmpskpkyh6yt4xj4l8h6jyjxez3nmq6y9tvhqjsdp0m5j 7 3 sops_age__list_1__map_enc=-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBpdEo3Nlp6RmgvZER2ZWtS\nU2dFazU0Q2FndnZBL1g2Ky9lcjNKRDBoaGlBCmlIVG9iUXVmazkzb2NTeDhXKzhT\nc3JQZjhhb0h3VjI2b2tpcGxiVG9NQmMKLS0tIG9tTjlPLzVQSjRLSkw2djM5QzQ2\nenJ6RFJUazlmQStJRStudlhuV1Rsc1UKMaAFJszBkgONafeLGMYO1zzS+dHzX3Uh\n8wwni17QDZTTE5Q2P8KPBquVWyzh7UUI3GhbxuZhANfs/RA5AQlg2w==\n-----END AGE ENCRYPTED FILE-----\n 8 4 sops_age__list_1__map_recipient=age1j6j2ldpsj7jmchstwl3nktvatut9hzxnemmy6py84rrga5eaf93q5w8s39 9 - sops_lastmodified=2025-10-24T22:20:59Z 10 - sops_mac=ENC[AES256_GCM,data:h7telhUinjAmbijnepEv3NXjvtYuCvQzCfQ6PA5vqr9QY8kAUlqICLofOP9+SVocUR0LlXsoKfmLcdIMMpZG1Jb6D0PQ8RuG4sTm5BSrWvvPj+U/Cm8JuPAqtOie0vWPBsL8vHg9BQGrUWEVtiK9fBLVmEvafVj9mmKzbwBZNOg=,iv:hzi5UOVUAdQW2wY8fAxLysfaLUertVLYo1D4H9Ubz+4=,tag:fRB9Lerke0aDkmcu+OzIfw==,type:str] 5 + sops_lastmodified=2026-01-04T15:43:03Z 6 + sops_mac=ENC[AES256_GCM,data:Icr7CvMbO79L0m+nzWVE3anuzOP8cO86apYB5Ax9OEtS6kZG/V94YR+hP8Zles+65brIux9cVcQuwiJUBddxt3ifMxqatjvj3ARAsKisBCTTEHgAaLos6Np7jHt/Av37ejIb90jU7bE3pFRjkBqimYPhqkVtlM00XmNzuZZFAvI=,iv:yduWTYvLtTG6Qc/rzbzyRNmchQ1OdnxziNd0DHwyYDM=,tag:6f0uFuLzj9yIovVrtomD9g==,type:str] 11 7 sops_unencrypted_suffix=_unencrypted 12 8 sops_version=3.11.0
+13 -2
secrets/pds.env
··· 3 3 PDS_PLC_ROTATION_KEY_K256_PRIVATE_KEY_HEX=ENC[AES256_GCM,data:OjAyEohVNTmWKapzJX1e/ed6ZbWL3I5+SgM93Bm89qymJWGX90ssca+fAcny38g2sZJkiJWjfUulE5sTMAbGog==,iv:vquXtXGhDMEFNJ6tqE76HTv1PZtGjtIL3+K05eY7KxE=,tag:sVDuGtkxfYL6csJ4d80+dQ==,type:str] 4 4 PDS_EMAIL_SMTP_URL=ENC[AES256_GCM,data:u2ylh4YK4aR5WhOFTazwLk3i2CU71Yakcb3pyoc2D+i94KMX0xEWKLb0xKpBnKyVBEI9CLMpiLUOPR1CuJRVv/aOR4yBIA==,iv:/NuzdQxYq0r4z9iTLGRi4aa/qBxFp0NeyNIOxIoEjbY=,tag:3dhfx9BbMdTahW02mYwKjA==,type:str] 5 5 PDS_EMAIL_FROM_ADDRESS=ENC[AES256_GCM,data:Xqly+3nqg8VpOQ==,iv:Dx0YD81azbMt2iT6EaqmJRXCXxhycM/pyW9eFH1cHGE=,tag:zi6bE3BNu443IFaTZDe3Wg==,type:str] 6 + PDS_BLOBSTORE_S3_BUCKET=ENC[AES256_GCM,data:JPb/0f2Q/6U=,iv:LBjZhWB1gF6G+z+clxqmP9BPvD8zixH9wnT2Wt7ezFo=,tag:EM5oM36aIPW5Iyv+6P3L0w==,type:str] 7 + PDS_BLOBSTORE_S3_REGION=ENC[AES256_GCM,data:ze3/cw==,iv:lQH76YOv/cA2NADm0h9h2m60Q9KPIayVU2Vo9IQV3Ho=,tag:GulJcJ2eJ5Dn3RwaRnpR8g==,type:str] 8 + PDS_BLOBSTORE_S3_ENDPOINT=ENC[AES256_GCM,data:eYdOT/bHeURLI6gvxfVmsb68MrZ/m0hwfotxbekpLDu7nM6zVykTrUoF0gyr0tnh0JZFNdqMjfjfijiM7HNBoQSCYqs=,iv:sq987mZrdj1d6ELyoKuVPl+ZB82nRAfGwtDEhw+pUqU=,tag:/nilWfeOiy2Gjgep1RkUuQ==,type:str] 9 + PDS_BLOBSTORE_S3_FORCE_PATH_STYLE=ENC[AES256_GCM,data:UKfn+o4=,iv:cWjc04TNNaiOap7NH4gVgmV6/EzNZUmXQuk4xjyJcxA=,tag:hRXEc7wbGC7kMN4KQKAmzQ==,type:str] 10 + PDS_BLOBSTORE_S3_ACCESS_KEY_ID=ENC[AES256_GCM,data:1lCFrKcGFWIwka4fsxw4N2HrYG1EEBvNTf3NbgFMa5k=,iv:+CjSrZKsiHt2mdfaXLD/fPlckuidUhMfvIMOT1y18Ds=,tag:vxFX1CEPXP51vo/ANC74VQ==,type:str] 11 + PDS_BLOBSTORE_S3_SECRET_ACCESS_KEY=ENC[AES256_GCM,data:6WdWC+7H5p8+tQ10G27oksww/y0Qa0iQN/UsgFmac/FOEf5+jnFGYMXRKgKzLpEzsCyqqZ906zHsyEiTV+2iCA==,iv:Dz7Mcr5qr9iRkykSQUmnmk7rjLmQgiKZxOLJq5Z0sJs=,tag:xRZ1N2snHB0NrERBFlUb9w==,type:str] 12 + PDS_BLOBSTORE_S3_UPLOAD_TIMEOUT_MS=ENC[AES256_GCM,data:H9XeBJJ0,iv:c5q14JkgHA0hmFRskinfVZyfIeB7hiT7lA0LD5TDTZQ=,tag:neUysh5VmF0GFYb9L0ujGA==,type:str] 13 + AWS_ACCESS_KEY_ID=ENC[AES256_GCM,data:TkeTnnIwu/kzaE0DZzzAM6zTSWVd9Qlrf0PrtKFIb+s=,iv:vDpULwiHD4zkZJdLZkNYUNttnV4w89/1JREt808BHUE=,tag:jGT9sAozIIGOwq3D0Xycvg==,type:str] 14 + AWS_SECRET_ACCESS_KEY=ENC[AES256_GCM,data:e1dHdjB23FbUObOrmqP3+CsRVHTKleQDLktdiIyLUmsM53uL6RGtX/+1eVUXj9kszjkZCnKG2AOGKrsWWUgnqw==,iv:I/a5Ayj5RYZC63s92pX84c6sMXVgLL7oL5xwG6o6x/0=,tag:vDD9WeN5+w+0TAiu4Ssz/Q==,type:str] 15 + AWS_ENDPOINT_URL=ENC[AES256_GCM,data:3t0VPGK36rV9V37kdMZYLamVDN3vh9zCgl7oJ6EnsPJwlYrH1GQ9zfOATk5w3RqICuqkRLAW+xmPOVUfdOedArfSY74=,iv:p2AHzQhmHA+hmVQohS5Go+f4W4skO1kjXBRsXkgNgyE=,tag:AEgdxUt9J0SWdL8MZc6h9A==,type:str] 16 + S3_BUCKET=ENC[AES256_GCM,data:tJJxdFgiuXA=,iv:yEOuQZWSsN3zAdXSHJPhKTBIIEN4y8E2X+Q/XwstZ7E=,tag:HEHaY+I2XlzdOEfXYBtTWw==,type:str] 6 17 sops_age__list_0__map_enc=-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBwS0dHSU9pd2tWVzZPZDlQ\nYVd6NjF0SkFrSGNUUUprUGN0c1Yzdk5BYXpZCnBiajU0cGVDQ0UwUVl6dHg5SjFO\nU3RScFhiTlZuYUJ6aC84K1F6YnEza2cKLS0tIDNFZ21BdHRTQTFGMkR3MDBxOHhG\nOVByYktJdjlsbTlNSDMvOTkvK0FyeU0KYBMndJPeIMpnqSrAUs0Em5Pbm7GBo/0e\nsaBKyYhn/pIPtJCyOiISqfXwMFHsiWCtd3dEejunG0x9eEkjWzDKqw==\n-----END AGE ENCRYPTED FILE-----\n 7 18 sops_age__list_0__map_recipient=age17cxj5zwkkxjkjvmpskpkyh6yt4xj4l8h6jyjxez3nmq6y9tvhqjsdp0m5j 8 19 sops_age__list_1__map_enc=-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSA2WW9pWHVZSWl4eWdCUGUv\nT3VSdVVPdVh0bENRanFjNnJVdlhQS2JvbzNRCmY2YnZmVlNhSDFYZ2VDWnN1MHp5\nS3lyMEpOa0hmTVdFeUtIQ1lrMWZVODgKLS0tIC93Q2hzb0pxdUNySEYwa1VyUjFN\nNk9tekkvUC9JT3NpcENqTHF5TUIyWXMKJ082QrvQ6QRfJ1RZqL9sSyKCmTLi+I9R\nxuo8E1SfP/204OQaihP6+9cCbLh3yYtESaAFw2Alisnoe4PvSR2I9g==\n-----END AGE ENCRYPTED FILE-----\n 9 20 sops_age__list_1__map_recipient=age1j6j2ldpsj7jmchstwl3nktvatut9hzxnemmy6py84rrga5eaf93q5w8s39 10 - sops_lastmodified=2025-10-24T21:30:51Z 11 - sops_mac=ENC[AES256_GCM,data:T7pttRlFl7HQwRY+AAXIX00wsABEyiHJsKPxXatl50Auaa+S3JJss56tVhphp2iMch7Z0nxnVT9kS03eAU1RUV0wHgCyu0QWNAib7PbUQIQvVq7pqF8SXORNuvwHJj2S9niASIDQqIenLYzUCRyq6tBnJ/XkW9JbiaOxs6w75G4=,iv:nvo7iAVjYkN45RZsqV3nGWzoaPcg74NRdhzy44lIi+Y=,tag:nIPrJy1srFn/nFTRjtdLbg==,type:str] 21 + sops_lastmodified=2026-01-04T15:13:30Z 22 + sops_mac=ENC[AES256_GCM,data:C+FIoCt7i/dMOwGs2BA0/t4XS41MkIO87D5lVXfwRPXihewG73574Xp5VeyQ5/+SH1OHrsXDTIE+TBrbzlnOr6LCbfqHaoRnYnhT0E8Xk84+z3Ihky2ziSViArYJjWOHMhKHyu+6XiaOD7UV/Cuimp/79PVNwk8VGD9dyUuvXVg=,iv:d11RcB3KVt9Me6I8gVVjs80jHQE5ss1j3iRwjvSdhmw=,tag:VhmTUP+M5cGeYaE0AAZUfA==,type:str] 12 23 sops_unencrypted_suffix=_unencrypted 13 24 sops_version=3.11.0
+4 -1
systems/default.nix
··· 4 4 5 5 config.easy-hosts = { 6 6 shared = { 7 - modules = [ ../modules/core.nix ]; 7 + modules = [ 8 + ../modules/core.nix 9 + { nixpkgs.overlays = [ self.overlays.default ]; } 10 + ]; 8 11 9 12 specialArgs = { inherit inputs self; }; 10 13 };
+7 -9
systems/reg/pds.nix
··· 1 1 { config, self, ... }: 2 2 { 3 3 imports = [ 4 - self.nixosModules.pds-backup 4 + self.nixosModules.pds 5 5 ../../modules/services/acme-nginx.nix 6 6 ]; 7 7 ··· 15 15 format = "dotenv"; 16 16 sopsFile = ../../secrets/cloudflare-api.env; 17 17 }; 18 - secrets.pds-s3 = { 19 - format = "dotenv"; 20 - sopsFile = ../../secrets/pds-backup-s3.env; 21 - }; 22 18 }; 23 19 24 - services.pds-backup = { 20 + services.pds-with-backups = { 25 21 enable = true; 26 - pdsSecretsFile = config.sops.secrets.pds.path; 27 - s3CredentialsFile = config.sops.secrets.pds-s3.path; 22 + domain = "0xf.fr"; 23 + secretsFiles = [ config.sops.secrets.pds.path ]; 24 + s3Prefix = "backups"; 28 25 29 26 pdsSettings = { 30 - PDS_HOSTNAME = "0xf.fr"; 27 + PDS_PORT = 3000; 28 + PDS_BLOBSTORE_DISK_LOCATION = null; 31 29 }; 32 30 }; 33 31