From 2b79c01952d67f35abfc4291a0c71cfc1f1e0413 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Sun, 25 Oct 2026 17:30:58 +0100 Subject: [PATCH] fix(offhost): judge the bucket's rules for database/ against this instance's folder The dumps now live under database//, so the rule check reads a sample key in that folder: a rule scoped to this instance's folder counts, one scoped to another instance's does not. --- .../jobs/__tests__/offhost-lifecycle.test.ts | 21 ++++++++++++++++++- src/lib/jobs/offhost-lifecycle.ts | 20 ++++++++++++++---- 2 files changed, 36 insertions(+), 5 deletions(-) diff --git a/src/lib/jobs/__tests__/offhost-lifecycle.test.ts b/src/lib/jobs/__tests__/offhost-lifecycle.test.ts index a85278266..642593e9c 100644 --- a/src/lib/jobs/__tests__/offhost-lifecycle.test.ts +++ b/src/lib/jobs/__tests__/offhost-lifecycle.test.ts @@ -3,7 +3,7 @@ * from the account copies: a rule scoped to another prefix, or to a tag, does * not touch them. */ -import { describe, expect, it } from "vitest"; +import { describe, expect, it, vi } from "vitest"; import type { LifecycleRule } from "@aws-sdk/client-s3"; import { lifecycleFromRules, ruleAppliesTo } from "../offhost-lifecycle"; @@ -29,6 +29,25 @@ describe("lifecycle rules and the database dumps", () => { ).toEqual({ state: "configured", expirationDays: 14 }); }); + it("reads the rules for this instance's own folder under database/", () => { + vi.stubEnv("APP_URL", "https://health.example.org"); + try { + expect( + lifecycleFromRules([ + rule(30, { Filter: { Prefix: "2026-" } }), + rule(9, { Filter: { Prefix: "database/health.example.org/" } }), + ]).database, + ).toEqual({ state: "configured", expirationDays: 9 }); + expect( + lifecycleFromRules([ + rule(9, { Filter: { Prefix: "database/staging.example.org/" } }), + ]).database, + ).toEqual({ state: "missing", expirationDays: null }); + } finally { + vi.unstubAllEnvs(); + } + }); + it("says database/ is not covered when the only rule is scoped elsewhere", () => { const result = lifecycleFromRules([ rule(30, { Filter: { Prefix: "2026-" } }), diff --git a/src/lib/jobs/offhost-lifecycle.ts b/src/lib/jobs/offhost-lifecycle.ts index 4638c59d5..f80364ebf 100644 --- a/src/lib/jobs/offhost-lifecycle.ts +++ b/src/lib/jobs/offhost-lifecycle.ts @@ -16,6 +16,10 @@ * without `s3:GetLifecycleConfiguration`, a target that does not implement the * call, a network failure) and says nothing either way. */ +import { + instanceIdFromEnv, + offhostObjectKey, +} from "@/lib/database-backup/dump-core.mjs"; import type { LifecycleRule } from "@aws-sdk/client-s3"; export interface LifecycleVerdict { @@ -40,9 +44,17 @@ export const LIFECYCLE_MISSING: OffhostLifecycle = { database: { state: "missing", expirationDays: null }, }; -/** A key a database dump is stored under, to test the rules against. */ -const DATABASE_SAMPLE_KEY = - "database/healthlog-20260101T013000Z-daily.dump.enc"; +/** + * A key this instance's database dumps are stored under, to test the rules + * against: inside its own folder (`database//`), so a rule + * scoped to that folder counts and one scoped to another instance's does not. + */ +function databaseSampleKey(): string { + return offhostObjectKey( + "healthlog-20260101T013000Z-daily.dump.enc", + instanceIdFromEnv(), + ); +} /** * Whether a rule's filter takes in `key`. Our objects carry no tags, so a @@ -73,7 +85,7 @@ export function lifecycleFromRules( return { ...verdict(rules), database: verdict( - rules.filter((rule) => ruleAppliesTo(rule, DATABASE_SAMPLE_KEY)), + rules.filter((rule) => ruleAppliesTo(rule, databaseSampleKey())), ), }; }