feat: harden self-managed reconciliation

This commit is contained in:
2026-09-05 10:17:55 +00:00 Unverified
parent 8e9d207915
commit 321f4e807a
40 changed files with 4977 additions and 404 deletions
+36 -6
View File
@@ -138,15 +138,41 @@ export function getComposeS3Claims(compose: ComposeSpecification): S3Claim[] {
return claims;
}
function throwIfAborted(signal?: AbortSignal): void {
if (!signal?.aborted) return;
throw new Error("Workspace operation execution was cancelled");
}
async function waitForInterval(
delayMs: number,
signal?: AbortSignal,
): Promise<void> {
if (!signal) return new Promise((resolve) => setTimeout(resolve, delayMs));
throwIfAborted(signal);
await new Promise<void>((resolve, reject) => {
const timer = setTimeout(() => {
signal.removeEventListener("abort", cancelWait);
resolve();
}, delayMs);
const cancelWait = () => {
clearTimeout(timer);
reject(new Error("Workspace operation execution was cancelled"));
};
signal.addEventListener("abort", cancelWait, { once: true });
});
}
async function reconcileBuckets(
project: string,
claims: S3Claim[],
signal?: AbortSignal,
): Promise<void> {
const buckets = new Map<string, S3Claim>();
for (const claim of claims) buckets.set(claim.bucket, claim);
for (const claim of buckets.values()) {
try {
throwIfAborted(signal);
await applyResource({
apiVersion: GARAGE_API_VERSION,
kind: "GarageBucket",
@@ -177,12 +203,14 @@ async function reconcileBuckets(
async function reconcileKeys(
project: string,
claims: S3Claim[],
signal?: AbortSignal,
): Promise<void> {
const keys = new Map<string, S3Claim>();
for (const claim of claims) keys.set(claim.key, claim);
for (const claim of keys.values()) {
try {
throwIfAborted(signal);
await applyResource({
apiVersion: GARAGE_API_VERSION,
kind: "GarageKey",
@@ -227,11 +255,13 @@ async function reconcileKeys(
export async function getS3Credentials(
claim: S3Claim,
signal?: AbortSignal,
): Promise<Record<string, string>> {
const deadline = Date.now() + SECRET_WAIT_TIMEOUT_MS;
let lastPhase: string | undefined;
while (Date.now() < deadline) {
throwIfAborted(signal);
const key = await readObject<GarageKeyStatus>({
apiVersion: GARAGE_API_VERSION,
kind: "GarageKey",
@@ -274,9 +304,7 @@ export async function getS3Credentials(
}
}
await new Promise((resolve) =>
setTimeout(resolve, SECRET_WAIT_INTERVAL_MS),
);
await waitForInterval(SECRET_WAIT_INTERVAL_MS, signal);
}
throw new Error(
@@ -287,17 +315,19 @@ export async function getS3Credentials(
export async function reconcileS3Claims(
project: string,
compose: ComposeSpecification,
signal?: AbortSignal,
): Promise<Record<string, Record<string, string>>> {
const claims = getComposeS3Claims(compose);
if (claims.length === 0) return {};
await reconcileBuckets(project, claims);
await reconcileKeys(project, claims);
await reconcileBuckets(project, claims, signal);
await reconcileKeys(project, claims, signal);
const environmentsByKey = new Map<string, Record<string, string>>();
for (const claim of claims) {
if (environmentsByKey.has(claim.key)) continue;
environmentsByKey.set(claim.key, await getS3Credentials(claim));
throwIfAborted(signal);
environmentsByKey.set(claim.key, await getS3Credentials(claim, signal));
}
return Object.fromEntries(