Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion infra/relay/src/environments/ManagedEndpointAllocations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down
60 changes: 57 additions & 3 deletions infra/relay/src/environments/ManagedEndpointProvider.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@ function makeDnsClient(
operation: "update-record",
hostname: request.name,
dnsRecordId,
cause: `DNS record ${dnsRecordId} does not exist.`,
cause: { _tag: "NotFound", dnsRecordId },
});
}
}),
Expand Down Expand Up @@ -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",
() => {
Expand Down Expand Up @@ -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* () {
Expand All @@ -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();
Expand Down
245 changes: 119 additions & 126 deletions infra/relay/src/environments/ManagedEndpointProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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"],
Expand Down Expand Up @@ -331,7 +325,7 @@ const ignoreNotFound = <A>(
}),
);

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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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),
),
}),
),
),
);
Loading