History

[codex] Structure managed endpoint allocation failures (#3421)

Julius Marminge committed 4 months ago

Commit eb5eb0d1 parent 62161623 files · +177 −130Browse files

Unified · Split

3 files
  1. infra/relay/src/environments/ManagedEndpointAllocations.ts +1 −1
  2. infra/relay/src/environments/ManagedEndpointProvider.test.ts +57 −3
  3. infra/relay/src/environments/ManagedEndpointProvider.ts +119 −126
infra/relay/src/environments/ManagedEndpointAllocations.ts+1−1
@@ -120,7 +120,7 @@ const whereAllocation = (input: ManagedEndpointAllocationKey) => · 119 unmodified lines
120120 eq(relayManagedEndpointAllocations.environmentId, input.environmentId),
121121 );
122122
123−const make = Effect.gen(function* () {
123+export const make = Effect.gen(function* () {
124124 const db = yield* RelayDb.RelayDb;
125125
126126 return ManagedEndpointAllocations.of({
infra/relay/src/environments/ManagedEndpointProvider.test.ts+57−3
@@ -133,7 +133,7 @@ function makeDnsClient( · 132 unmodified lines
133133 operation: "update-record",
134134 hostname: request.name,
135135 dnsRecordId,
136− cause: `DNS record ${dnsRecordId} does not exist.`,
136+ cause: { _tag: "NotFound", dnsRecordId },
137137 });
138138 }
139139 }),
@@ -531,6 +531,59 @@ describe("ManagedEndpointProvider", () => { · 391 unmodified lines
531531 }).pipe(Effect.provide(layer));
532532 });
533533
534+ it.effect("does not hide non-not-found checkpoint update failures", () => {
535+ const dnsCalls: DnsCall[] = [];
536+ const failure = new ManagedEndpointProvider.ManagedEndpointDnsClientError({
537+ operation: "update-record",
538+ dnsRecordId: "created-record-id",
539+ cause: new Error("Cloudflare DNS unavailable"),
540+ });
541+ let records: ReadonlyArray<{ readonly id: string }> = [];
542+ const dnsClient = ManagedEndpointProvider.ManagedEndpointDnsClient.of({
543+ listRecords: (hostname) =>
544+ Effect.sync(() => {
545+ dnsCalls.push({ operation: "listRecords", input: hostname });
546+ return records;
547+ }),
548+ createRecord: (request) =>
549+ Effect.sync(() => {
550+ dnsCalls.push({ operation: "createRecord", input: request });
551+ const record = { id: "created-record-id" };
552+ records = [record];
553+ return record;
554+ }),
555+ updateRecord: (dnsRecordId, request) =>
556+ Effect.sync(() => {
557+ dnsCalls.push({ operation: "updateRecord", input: { dnsRecordId, request } });
558+ }).pipe(Effect.andThen(Effect.fail(failure))),
559+ deleteRecord: () => Effect.void,
560+ });
561+ const layer = providerLayer(makePersistentTunnelClient(), dnsClient, makeAllocations());
562+
563+ return Effect.gen(function* () {
564+ const provider = yield* ManagedEndpointProvider.ManagedEndpointProvider;
565+ const request = {
566+ userId: "user_ABC",
567+ environmentId: "env_ABC",
568+ origin: { localHttpHost: "127.0.0.1", localHttpPort: 3773 },
569+ } as const;
570+ yield* provider.provision(request);
571+ const error = yield* Effect.flip(provider.provision(request));
572+
573+ expect(error).toMatchObject({
574+ _tag: "ManagedEndpointProvisioningFailed",
575+ stage: "ensure-dns-record",
576+ userId: "user_ABC",
577+ environmentId: "env_ABC",
578+ });
579+ expect(dnsCalls.map((call) => call.operation)).toEqual([
580+ "listRecords",
581+ "createRecord",
582+ "updateRecord",
583+ ]);
584+ }).pipe(Effect.provide(layer));
585+ });
586+
534587 it.effect(
535588 "deprovisions checkpointed DNS and tunnel resources before removing the allocation",
536589 () => {
@@ -765,11 +818,11 @@ describe("ManagedEndpointProvider", () => { · 228 unmodified lines
765818 }).pipe(Effect.provide(providerLayer(makeTunnelClient(), dnsClient)));
766819 });
767820
768− it.effect("reports malformed tunnel responses without manufacturing a cause", () => {
821+ it.effect("reports mismatched tunnel responses without manufacturing a cause", () => {
769822 const dnsCalls: DnsCall[] = [];
770823 const tunnelClient = ManagedEndpointProvider.ManagedEndpointTunnelClient.of({
771824 ...makeTunnelClient(),
772− create: () => Effect.succeed({ id: "returned-tunnel-id", name: null }),
825+ create: () => Effect.succeed({ id: "returned-tunnel-id", name: "unexpected-tunnel" }),
773826 });
774827
775828 return Effect.gen(function* () {
@@ -790,6 +843,7 @@ describe("ManagedEndpointProvider", () => { · 14 unmodified lines
790843 hostname: expectedManagedHostname("env_ABC"),
791844 tunnelName: expectedManagedTunnelName("env_ABC"),
792845 returnedTunnelId: "returned-tunnel-id",
846+ returnedTunnelName: "unexpected-tunnel",
793847 });
794848 if (error._tag === "ManagedEndpointProvisioningFailed") {
795849 expect(error.cause).toBeUndefined();
infra/relay/src/environments/ManagedEndpointProvider.ts+119−126
@@ -192,11 +192,8 @@ export class ManagedEndpointTunnelClient extends Context.Service< · 191 unmodified lines
192192 }
193193 >()("t3code-relay/environments/ManagedEndpointProvider/ManagedEndpointTunnelClient") {}
194194
195−export const makeTunnelClient = (client: ManagedEndpointTunnelClient["Service"]) =>
196− ManagedEndpointTunnelClient.of(client);
197−
198195 export const layerTunnelClient = (client: ManagedEndpointTunnelClient["Service"]) =>
199− Layer.succeed(ManagedEndpointTunnelClient, makeTunnelClient(client));
196+ Layer.succeed(ManagedEndpointTunnelClient, client);
200197
201198 interface ManagedEndpointCnameRecordInput {
202199 readonly type: "CNAME";
@@ -247,11 +244,8 @@ export class ManagedEndpointDnsClient extends Context.Service< · 44 unmodified lines
247244 }
248245 >()("t3code-relay/environments/ManagedEndpointProvider/ManagedEndpointDnsClient") {}
249246
250−export const makeDnsClient = (client: ManagedEndpointDnsClient["Service"]) =>
251− ManagedEndpointDnsClient.of(client);
252−
253247 export const layerDnsClient = (client: ManagedEndpointDnsClient["Service"]) =>
254− Layer.succeed(ManagedEndpointDnsClient, makeDnsClient(client));
248+ Layer.succeed(ManagedEndpointDnsClient, client);
255249
256250 const requireCloudflareSettings = Effect.fnUntraced(function* (
257251 settings: RelayConfiguration.RelayConfiguration["Service"],
@@ -331,7 +325,7 @@ const ignoreNotFound = <A>( · 73 unmodified lines
331325 }),
332326 );
333327
334−const make = Effect.gen(function* () {
328+export const make = Effect.gen(function* () {
335329 const config = yield* RelayConfiguration.RelayConfiguration;
336330 const crypto = yield* Crypto.Crypto;
337331 const tunnels = yield* ManagedEndpointTunnelClient;
@@ -366,7 +360,10 @@ const make = Effect.gen(function* () { · 28 unmodified lines
366360 .updateRecord(preferredDnsRecordId, dnsRecord)
367361 .pipe(
368362 Effect.as(true),
369− Effect.orElseSucceed(() => false),
363+ Effect.catchTags({
364+ ManagedEndpointDnsClientError: (error) =>
365+ isNotFoundCause(error.cause) ? Effect.succeed(false) : Effect.fail(error),
366+ }),
370367 );
371368 if (checkpointedRecordUpdated) {
372369 return preferredDnsRecordId;
@@ -550,7 +547,7 @@ const make = Effect.gen(function* () { · 177 unmodified lines
550547 }),
551548 ),
552549 );
553− if (!tunnelResponse.id || !tunnelResponse.name) {
550+ if (!tunnelResponse.id || tunnelResponse.name !== tunnelName) {
554551 return yield* new ManagedEndpointProvisioningFailed({
555552 userId: input.userId,
556553 environmentId: input.environmentId,
@@ -712,131 +709,127 @@ export const layerCloudflareBindings = ( · 155 unmodified lines
712709 layer.pipe(
713710 Layer.provide(
714711 Layer.mergeAll(
715− layerTunnelClient(
716− ManagedEndpointTunnelClient.of({
717− list: (request) =>
718− tunnelClient.list(request).pipe(
719− Effect.mapError(
720− (cause) =>
721− new ManagedEndpointTunnelClientError({
722− operation: "list",
723− tunnelName: request.name,
724− cause,
725− }),
726− ),
727− Effect.provideService(Alchemy.RuntimeContext, alchemyRuntimeContext),
712+ layerTunnelClient({
713+ list: (request) =>
714+ tunnelClient.list(request).pipe(
715+ Effect.mapError(
716+ (cause) =>
717+ new ManagedEndpointTunnelClientError({
718+ operation: "list",
719+ tunnelName: request.name,
720+ cause,
721+ }),
728722 ),
729− create: (request) =>
730− tunnelClient.create(request).pipe(
731− Effect.mapError(
732− (cause) =>
733− new ManagedEndpointTunnelClientError({
734− operation: "create",
735− tunnelName: request.name,
736− cause,
737− }),
738− ),
739− Effect.provideService(Alchemy.RuntimeContext, alchemyRuntimeContext),
723+ Effect.provideService(Alchemy.RuntimeContext, alchemyRuntimeContext),
724+ ),
725+ create: (request) =>
726+ tunnelClient.create(request).pipe(
727+ Effect.mapError(
728+ (cause) =>
729+ new ManagedEndpointTunnelClientError({
730+ operation: "create",
731+ tunnelName: request.name,
732+ cause,
733+ }),
740734 ),
741− putConfiguration: (tunnelId, config) =>
742− tunnelClient.putConfiguration(tunnelId, config).pipe(
743− Effect.mapError(
744− (cause) =>
745− new ManagedEndpointTunnelClientError({
746− operation: "put-configuration",
747− tunnelId,
748− cause,
749− }),
750− ),
751− Effect.provideService(Alchemy.RuntimeContext, alchemyRuntimeContext),
735+ Effect.provideService(Alchemy.RuntimeContext, alchemyRuntimeContext),
736+ ),
737+ putConfiguration: (tunnelId, config) =>
738+ tunnelClient.putConfiguration(tunnelId, config).pipe(
739+ Effect.mapError(
740+ (cause) =>
741+ new ManagedEndpointTunnelClientError({
742+ operation: "put-configuration",
743+ tunnelId,
744+ cause,
745+ }),
752746 ),
753− getToken: (tunnelId) =>
754− tunnelClient.getToken(tunnelId).pipe(
755− Effect.mapError(
756− (cause) =>
757− new ManagedEndpointTunnelClientError({
758− operation: "get-token",
759− tunnelId,
760− cause,
761− }),
762− ),
763− Effect.provideService(Alchemy.RuntimeContext, alchemyRuntimeContext),
747+ Effect.provideService(Alchemy.RuntimeContext, alchemyRuntimeContext),
748+ ),
749+ getToken: (tunnelId) =>
750+ tunnelClient.getToken(tunnelId).pipe(
751+ Effect.mapError(
752+ (cause) =>
753+ new ManagedEndpointTunnelClientError({
754+ operation: "get-token",
755+ tunnelId,
756+ cause,
757+ }),
764758 ),
765− delete: (tunnelId) =>
766− tunnelClient.delete(tunnelId).pipe(
767− Effect.mapError(
768− (cause) =>
769− new ManagedEndpointTunnelClientError({
770− operation: "delete",
771− tunnelId,
772− cause,
773− }),
774− ),
775− Effect.provideService(Alchemy.RuntimeContext, alchemyRuntimeContext),
759+ Effect.provideService(Alchemy.RuntimeContext, alchemyRuntimeContext),
760+ ),
761+ delete: (tunnelId) =>
762+ tunnelClient.delete(tunnelId).pipe(
763+ Effect.mapError(
764+ (cause) =>
765+ new ManagedEndpointTunnelClientError({
766+ operation: "delete",
767+ tunnelId,
768+ cause,
769+ }),
776770 ),
777− }),
778− ),
779− layerDnsClient(
780− ManagedEndpointDnsClient.of({
781− listRecords: (hostname) =>
782− dnsClient.listDnsRecords({ search: hostname }).pipe(
783− Effect.map((response) =>
784− response.result.filter(
785− (record): record is typeof record & { readonly id: string } =>
786− typeof record.id === "string" &&
787− normalizeHostname(record.name) === normalizeHostname(hostname),
788− ),
789− ),
790− Effect.mapError(
791− (cause) =>
792− new ManagedEndpointDnsClientError({
793− operation: "list-records",
794− hostname,
795− cause,
796− }),
771+ Effect.provideService(Alchemy.RuntimeContext, alchemyRuntimeContext),
772+ ),
773+ }),
774+ layerDnsClient({
775+ listRecords: (hostname) =>
776+ dnsClient.listDnsRecords({ search: hostname }).pipe(
777+ Effect.map((response) =>
778+ response.result.filter(
779+ (record): record is typeof record & { readonly id: string } =>
780+ typeof record.id === "string" &&
781+ normalizeHostname(record.name) === normalizeHostname(hostname),
797782 ),
798− Effect.provideService(Alchemy.RuntimeContext, alchemyRuntimeContext),
799783 ),
800− createRecord: (request) =>
801− dnsClient.createDnsRecord(request).pipe(
802− Effect.map((response) => ({ id: response.id })),
803− Effect.mapError(
804− (cause) =>
805− new ManagedEndpointDnsClientError({
806− operation: "create-record",
807− hostname: request.name,
808− cause,
809− }),
810− ),
811− Effect.provideService(Alchemy.RuntimeContext, alchemyRuntimeContext),
784+ Effect.mapError(
785+ (cause) =>
786+ new ManagedEndpointDnsClientError({
787+ operation: "list-records",
788+ hostname,
789+ cause,
790+ }),
812791 ),
813− updateRecord: (dnsRecordId, request) =>
814− dnsClient.updateDnsRecord(dnsRecordId, request).pipe(
815− Effect.mapError(
816− (cause) =>
817− new ManagedEndpointDnsClientError({
818− operation: "update-record",
819− hostname: request.name,
820− dnsRecordId,
821− cause,
822− }),
823− ),
824− Effect.provideService(Alchemy.RuntimeContext, alchemyRuntimeContext),
792+ Effect.provideService(Alchemy.RuntimeContext, alchemyRuntimeContext),
793+ ),
794+ createRecord: (request) =>
795+ dnsClient.createDnsRecord(request).pipe(
796+ Effect.map((response) => ({ id: response.id })),
797+ Effect.mapError(
798+ (cause) =>
799+ new ManagedEndpointDnsClientError({
800+ operation: "create-record",
801+ hostname: request.name,
802+ cause,
803+ }),
825804 ),
826− deleteRecord: (dnsRecordId) =>
827− dnsClient.deleteDnsRecord(dnsRecordId).pipe(
828− Effect.mapError(
829− (cause) =>
830− new ManagedEndpointDnsClientError({
831− operation: "delete-record",
832− dnsRecordId,
833− cause,
834− }),
835− ),
836− Effect.provideService(Alchemy.RuntimeContext, alchemyRuntimeContext),
805+ Effect.provideService(Alchemy.RuntimeContext, alchemyRuntimeContext),
806+ ),
807+ updateRecord: (dnsRecordId, request) =>
808+ dnsClient.updateDnsRecord(dnsRecordId, request).pipe(
809+ Effect.mapError(
810+ (cause) =>
811+ new ManagedEndpointDnsClientError({
812+ operation: "update-record",
813+ hostname: request.name,
814+ dnsRecordId,
815+ cause,
816+ }),
837817 ),
838− }),
839− ),
818+ Effect.provideService(Alchemy.RuntimeContext, alchemyRuntimeContext),
819+ ),
820+ deleteRecord: (dnsRecordId) =>
821+ dnsClient.deleteDnsRecord(dnsRecordId).pipe(
822+ Effect.mapError(
823+ (cause) =>
824+ new ManagedEndpointDnsClientError({
825+ operation: "delete-record",
826+ dnsRecordId,
827+ cause,
828+ }),
829+ ),
830+ Effect.provideService(Alchemy.RuntimeContext, alchemyRuntimeContext),
831+ ),
832+ }),
840833 ),
841834 ),
842835 );

Keyboard shortcuts

General

Show keyboard shortcuts
?
Focus the current search
/

Issues and pull requests

Open filters
f

Issues, pull requests and Inbox

Select next item
j↓
Select previous item
k↑
Open selected item
Enter
Dismiss filters or clear selection
Esc

Repository code

Go to file
t