From 10793eb2d2d6148d10f7d47cee7e1fb5b5641e94 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Sat, 20 Jun 2026 11:10:10 -0700 Subject: [PATCH] fix(relay): structure managed endpoint allocation failures Co-authored-by: codex --- .../ManagedEndpointAllocations.ts | 2 +- .../ManagedEndpointProvider.test.ts | 60 ++++- .../environments/ManagedEndpointProvider.ts | 245 +++++++++--------- 3 files changed, 177 insertions(+), 130 deletions(-) diff --git a/infra/relay/src/environments/ManagedEndpointAllocations.ts b/infra/relay/src/environments/ManagedEndpointAllocations.ts index c951ee03c8d..f6cefa69071 100644 --- a/infra/relay/src/environments/ManagedEndpointAllocations.ts +++ b/infra/relay/src/environments/ManagedEndpointAllocations.ts @@ -120,7 +120,7 @@ const whereAllocation = (input: ManagedEndpointAllocationKey) => eq(relayManagedEndpointAllocations.environmentId, input.environmentId), ); -const make = Effect.gen(function* () { +export const make = Effect.gen(function* () { const db = yield* RelayDb.RelayDb; return ManagedEndpointAllocations.of({ diff --git a/infra/relay/src/environments/ManagedEndpointProvider.test.ts b/infra/relay/src/environments/ManagedEndpointProvider.test.ts index 479be412380..56bf6319d0d 100644 --- a/infra/relay/src/environments/ManagedEndpointProvider.test.ts +++ b/infra/relay/src/environments/ManagedEndpointProvider.test.ts @@ -133,7 +133,7 @@ function makeDnsClient( operation: "update-record", hostname: request.name, dnsRecordId, - cause: `DNS record ${dnsRecordId} does not exist.`, + cause: { _tag: "NotFound", dnsRecordId }, }); } }), @@ -531,6 +531,59 @@ describe("ManagedEndpointProvider", () => { }).pipe(Effect.provide(layer)); }); + it.effect("does not hide non-not-found checkpoint update failures", () => { + const dnsCalls: DnsCall[] = []; + const failure = new ManagedEndpointProvider.ManagedEndpointDnsClientError({ + operation: "update-record", + dnsRecordId: "created-record-id", + cause: new Error("Cloudflare DNS unavailable"), + }); + let records: ReadonlyArray<{ readonly id: string }> = []; + const dnsClient = ManagedEndpointProvider.ManagedEndpointDnsClient.of({ + listRecords: (hostname) => + Effect.sync(() => { + dnsCalls.push({ operation: "listRecords", input: hostname }); + return records; + }), + createRecord: (request) => + Effect.sync(() => { + dnsCalls.push({ operation: "createRecord", input: request }); + const record = { id: "created-record-id" }; + records = [record]; + return record; + }), + updateRecord: (dnsRecordId, request) => + Effect.sync(() => { + dnsCalls.push({ operation: "updateRecord", input: { dnsRecordId, request } }); + }).pipe(Effect.andThen(Effect.fail(failure))), + deleteRecord: () => Effect.void, + }); + const layer = providerLayer(makePersistentTunnelClient(), dnsClient, makeAllocations()); + + return Effect.gen(function* () { + const provider = yield* ManagedEndpointProvider.ManagedEndpointProvider; + const request = { + userId: "user_ABC", + environmentId: "env_ABC", + origin: { localHttpHost: "127.0.0.1", localHttpPort: 3773 }, + } as const; + yield* provider.provision(request); + const error = yield* Effect.flip(provider.provision(request)); + + expect(error).toMatchObject({ + _tag: "ManagedEndpointProvisioningFailed", + stage: "ensure-dns-record", + userId: "user_ABC", + environmentId: "env_ABC", + }); + expect(dnsCalls.map((call) => call.operation)).toEqual([ + "listRecords", + "createRecord", + "updateRecord", + ]); + }).pipe(Effect.provide(layer)); + }); + it.effect( "deprovisions checkpointed DNS and tunnel resources before removing the allocation", () => { @@ -765,11 +818,11 @@ describe("ManagedEndpointProvider", () => { }).pipe(Effect.provide(providerLayer(makeTunnelClient(), dnsClient))); }); - it.effect("reports malformed tunnel responses without manufacturing a cause", () => { + it.effect("reports mismatched tunnel responses without manufacturing a cause", () => { const dnsCalls: DnsCall[] = []; const tunnelClient = ManagedEndpointProvider.ManagedEndpointTunnelClient.of({ ...makeTunnelClient(), - create: () => Effect.succeed({ id: "returned-tunnel-id", name: null }), + create: () => Effect.succeed({ id: "returned-tunnel-id", name: "unexpected-tunnel" }), }); return Effect.gen(function* () { @@ -790,6 +843,7 @@ describe("ManagedEndpointProvider", () => { hostname: expectedManagedHostname("env_ABC"), tunnelName: expectedManagedTunnelName("env_ABC"), returnedTunnelId: "returned-tunnel-id", + returnedTunnelName: "unexpected-tunnel", }); if (error._tag === "ManagedEndpointProvisioningFailed") { expect(error.cause).toBeUndefined(); diff --git a/infra/relay/src/environments/ManagedEndpointProvider.ts b/infra/relay/src/environments/ManagedEndpointProvider.ts index bb2dd4b0ce9..68e93f8b17c 100644 --- a/infra/relay/src/environments/ManagedEndpointProvider.ts +++ b/infra/relay/src/environments/ManagedEndpointProvider.ts @@ -192,11 +192,8 @@ export class ManagedEndpointTunnelClient extends Context.Service< } >()("t3code-relay/environments/ManagedEndpointProvider/ManagedEndpointTunnelClient") {} -export const makeTunnelClient = (client: ManagedEndpointTunnelClient["Service"]) => - ManagedEndpointTunnelClient.of(client); - export const layerTunnelClient = (client: ManagedEndpointTunnelClient["Service"]) => - Layer.succeed(ManagedEndpointTunnelClient, makeTunnelClient(client)); + Layer.succeed(ManagedEndpointTunnelClient, client); interface ManagedEndpointCnameRecordInput { readonly type: "CNAME"; @@ -247,11 +244,8 @@ export class ManagedEndpointDnsClient extends Context.Service< } >()("t3code-relay/environments/ManagedEndpointProvider/ManagedEndpointDnsClient") {} -export const makeDnsClient = (client: ManagedEndpointDnsClient["Service"]) => - ManagedEndpointDnsClient.of(client); - export const layerDnsClient = (client: ManagedEndpointDnsClient["Service"]) => - Layer.succeed(ManagedEndpointDnsClient, makeDnsClient(client)); + Layer.succeed(ManagedEndpointDnsClient, client); const requireCloudflareSettings = Effect.fnUntraced(function* ( settings: RelayConfiguration.RelayConfiguration["Service"], @@ -331,7 +325,7 @@ const ignoreNotFound = ( }), ); -const make = Effect.gen(function* () { +export const make = Effect.gen(function* () { const config = yield* RelayConfiguration.RelayConfiguration; const crypto = yield* Crypto.Crypto; const tunnels = yield* ManagedEndpointTunnelClient; @@ -366,7 +360,10 @@ const make = Effect.gen(function* () { .updateRecord(preferredDnsRecordId, dnsRecord) .pipe( Effect.as(true), - Effect.orElseSucceed(() => false), + Effect.catchTags({ + ManagedEndpointDnsClientError: (error) => + isNotFoundCause(error.cause) ? Effect.succeed(false) : Effect.fail(error), + }), ); if (checkpointedRecordUpdated) { return preferredDnsRecordId; @@ -550,7 +547,7 @@ const make = Effect.gen(function* () { }), ), ); - if (!tunnelResponse.id || !tunnelResponse.name) { + if (!tunnelResponse.id || tunnelResponse.name !== tunnelName) { return yield* new ManagedEndpointProvisioningFailed({ userId: input.userId, environmentId: input.environmentId, @@ -712,131 +709,127 @@ export const layerCloudflareBindings = ( layer.pipe( Layer.provide( Layer.mergeAll( - layerTunnelClient( - ManagedEndpointTunnelClient.of({ - list: (request) => - tunnelClient.list(request).pipe( - Effect.mapError( - (cause) => - new ManagedEndpointTunnelClientError({ - operation: "list", - tunnelName: request.name, - cause, - }), - ), - Effect.provideService(Alchemy.RuntimeContext, alchemyRuntimeContext), + layerTunnelClient({ + list: (request) => + tunnelClient.list(request).pipe( + Effect.mapError( + (cause) => + new ManagedEndpointTunnelClientError({ + operation: "list", + tunnelName: request.name, + cause, + }), ), - create: (request) => - tunnelClient.create(request).pipe( - Effect.mapError( - (cause) => - new ManagedEndpointTunnelClientError({ - operation: "create", - tunnelName: request.name, - cause, - }), - ), - Effect.provideService(Alchemy.RuntimeContext, alchemyRuntimeContext), + Effect.provideService(Alchemy.RuntimeContext, alchemyRuntimeContext), + ), + create: (request) => + tunnelClient.create(request).pipe( + Effect.mapError( + (cause) => + new ManagedEndpointTunnelClientError({ + operation: "create", + tunnelName: request.name, + cause, + }), ), - putConfiguration: (tunnelId, config) => - tunnelClient.putConfiguration(tunnelId, config).pipe( - Effect.mapError( - (cause) => - new ManagedEndpointTunnelClientError({ - operation: "put-configuration", - tunnelId, - cause, - }), - ), - Effect.provideService(Alchemy.RuntimeContext, alchemyRuntimeContext), + Effect.provideService(Alchemy.RuntimeContext, alchemyRuntimeContext), + ), + putConfiguration: (tunnelId, config) => + tunnelClient.putConfiguration(tunnelId, config).pipe( + Effect.mapError( + (cause) => + new ManagedEndpointTunnelClientError({ + operation: "put-configuration", + tunnelId, + cause, + }), ), - getToken: (tunnelId) => - tunnelClient.getToken(tunnelId).pipe( - Effect.mapError( - (cause) => - new ManagedEndpointTunnelClientError({ - operation: "get-token", - tunnelId, - cause, - }), - ), - Effect.provideService(Alchemy.RuntimeContext, alchemyRuntimeContext), + Effect.provideService(Alchemy.RuntimeContext, alchemyRuntimeContext), + ), + getToken: (tunnelId) => + tunnelClient.getToken(tunnelId).pipe( + Effect.mapError( + (cause) => + new ManagedEndpointTunnelClientError({ + operation: "get-token", + tunnelId, + cause, + }), ), - delete: (tunnelId) => - tunnelClient.delete(tunnelId).pipe( - Effect.mapError( - (cause) => - new ManagedEndpointTunnelClientError({ - operation: "delete", - tunnelId, - cause, - }), - ), - Effect.provideService(Alchemy.RuntimeContext, alchemyRuntimeContext), + Effect.provideService(Alchemy.RuntimeContext, alchemyRuntimeContext), + ), + delete: (tunnelId) => + tunnelClient.delete(tunnelId).pipe( + Effect.mapError( + (cause) => + new ManagedEndpointTunnelClientError({ + operation: "delete", + tunnelId, + cause, + }), ), - }), - ), - layerDnsClient( - ManagedEndpointDnsClient.of({ - listRecords: (hostname) => - dnsClient.listDnsRecords({ search: hostname }).pipe( - Effect.map((response) => - response.result.filter( - (record): record is typeof record & { readonly id: string } => - typeof record.id === "string" && - normalizeHostname(record.name) === normalizeHostname(hostname), - ), - ), - Effect.mapError( - (cause) => - new ManagedEndpointDnsClientError({ - operation: "list-records", - hostname, - cause, - }), + Effect.provideService(Alchemy.RuntimeContext, alchemyRuntimeContext), + ), + }), + layerDnsClient({ + listRecords: (hostname) => + dnsClient.listDnsRecords({ search: hostname }).pipe( + Effect.map((response) => + response.result.filter( + (record): record is typeof record & { readonly id: string } => + typeof record.id === "string" && + normalizeHostname(record.name) === normalizeHostname(hostname), ), - Effect.provideService(Alchemy.RuntimeContext, alchemyRuntimeContext), ), - createRecord: (request) => - dnsClient.createDnsRecord(request).pipe( - Effect.map((response) => ({ id: response.id })), - Effect.mapError( - (cause) => - new ManagedEndpointDnsClientError({ - operation: "create-record", - hostname: request.name, - cause, - }), - ), - Effect.provideService(Alchemy.RuntimeContext, alchemyRuntimeContext), + Effect.mapError( + (cause) => + new ManagedEndpointDnsClientError({ + operation: "list-records", + hostname, + cause, + }), ), - updateRecord: (dnsRecordId, request) => - dnsClient.updateDnsRecord(dnsRecordId, request).pipe( - Effect.mapError( - (cause) => - new ManagedEndpointDnsClientError({ - operation: "update-record", - hostname: request.name, - dnsRecordId, - cause, - }), - ), - Effect.provideService(Alchemy.RuntimeContext, alchemyRuntimeContext), + Effect.provideService(Alchemy.RuntimeContext, alchemyRuntimeContext), + ), + createRecord: (request) => + dnsClient.createDnsRecord(request).pipe( + Effect.map((response) => ({ id: response.id })), + Effect.mapError( + (cause) => + new ManagedEndpointDnsClientError({ + operation: "create-record", + hostname: request.name, + cause, + }), ), - deleteRecord: (dnsRecordId) => - dnsClient.deleteDnsRecord(dnsRecordId).pipe( - Effect.mapError( - (cause) => - new ManagedEndpointDnsClientError({ - operation: "delete-record", - dnsRecordId, - cause, - }), - ), - Effect.provideService(Alchemy.RuntimeContext, alchemyRuntimeContext), + Effect.provideService(Alchemy.RuntimeContext, alchemyRuntimeContext), + ), + updateRecord: (dnsRecordId, request) => + dnsClient.updateDnsRecord(dnsRecordId, request).pipe( + Effect.mapError( + (cause) => + new ManagedEndpointDnsClientError({ + operation: "update-record", + hostname: request.name, + dnsRecordId, + cause, + }), ), - }), - ), + Effect.provideService(Alchemy.RuntimeContext, alchemyRuntimeContext), + ), + deleteRecord: (dnsRecordId) => + dnsClient.deleteDnsRecord(dnsRecordId).pipe( + Effect.mapError( + (cause) => + new ManagedEndpointDnsClientError({ + operation: "delete-record", + dnsRecordId, + cause, + }), + ), + Effect.provideService(Alchemy.RuntimeContext, alchemyRuntimeContext), + ), + }), ), ), );