| ... | @@ -7,7 +7,6 @@ import * as Effect from "effect/Effect"; | ... | @@ -7,7 +7,6 @@ import * as Effect from "effect/Effect"; |
| 7 | import * as Encoding from "effect/Encoding"; | 7 | import * as Encoding from "effect/Encoding"; |
| 8 | import * as Layer from "effect/Layer"; | 8 | import * as Layer from "effect/Layer"; |
| 9 | import * as Option from "effect/Option"; | 9 | import * as Option from "effect/Option"; |
| 10 | import * as Result from "effect/Result"; | | |
| 11 | import * as Schema from "effect/Schema"; | 10 | import * as Schema from "effect/Schema"; |
| 12 | | 11 | |
| 13 | import type { | 12 | import type { |
| ... | @@ -27,40 +26,86 @@ import * as ManagedEndpointAllocations from "./ManagedEndpointAllocations.ts"; | ... | @@ -27,40 +26,86 @@ import * as ManagedEndpointAllocations from "./ManagedEndpointAllocations.ts"; |
| 27 | | 26 | |
| 28 | export class ManagedEndpointProvisioningNotConfigured extends Schema.TaggedErrorClass<ManagedEndpointProvisioningNotConfigured>()( | 27 | export class ManagedEndpointProvisioningNotConfigured extends Schema.TaggedErrorClass<ManagedEndpointProvisioningNotConfigured>()( |
| 29 | "ManagedEndpointProvisioningNotConfigured", | 28 | "ManagedEndpointProvisioningNotConfigured", |
| 30 | {}, | 29 | { |
| | 30 | userId: Schema.String, |
| | 31 | environmentId: Schema.String, |
| | 32 | missingSettings: Schema.Array( |
| | 33 | Schema.Literals(["managedEndpointBaseDomain", "managedEndpointNamespace"]), |
| | 34 | ), |
| | 35 | }, |
| 31 | ) { | 36 | ) { |
| 32 | override get message(): string { | 37 | override get message(): string { |
| 33 | return "Managed endpoint provisioning is not configured"; | 38 | return `Managed endpoint provisioning is not configured for user '${this.userId}', environment '${this.environmentId}': missing ${this.missingSettings.join(", ")}`; |
| 34 | } | 39 | } |
| 35 | } | 40 | } |
| 36 | | 41 | |
| | 42 | const ManagedEndpointProvisioningStage = Schema.Literals([ |
| | 43 | "derive-environment-hash", |
| | 44 | "reserve-allocation", |
| | 45 | "ensure-tunnel", |
| | 46 | "validate-tunnel-response", |
| | 47 | "record-tunnel", |
| | 48 | "configure-tunnel", |
| | 49 | "ensure-dns-record", |
| | 50 | "record-dns", |
| | 51 | "get-tunnel-token", |
| | 52 | "mark-allocation-ready", |
| | 53 | ]); |
| | 54 | |
| 37 | export class ManagedEndpointProvisioningFailed extends Schema.TaggedErrorClass<ManagedEndpointProvisioningFailed>()( | 55 | export class ManagedEndpointProvisioningFailed extends Schema.TaggedErrorClass<ManagedEndpointProvisioningFailed>()( |
| 38 | "ManagedEndpointProvisioningFailed", | 56 | "ManagedEndpointProvisioningFailed", |
| 39 | { cause: Schema.Defect() }, | 57 | { |
| | 58 | stage: ManagedEndpointProvisioningStage, |
| | 59 | userId: Schema.String, |
| | 60 | environmentId: Schema.String, |
| | 61 | hostname: Schema.optionalKey(Schema.String), |
| | 62 | tunnelName: Schema.optionalKey(Schema.String), |
| | 63 | tunnelId: Schema.optionalKey(Schema.String), |
| | 64 | dnsRecordId: Schema.optionalKey(Schema.String), |
| | 65 | returnedTunnelName: Schema.optionalKey(Schema.String), |
| | 66 | returnedTunnelId: Schema.optionalKey(Schema.String), |
| | 67 | cause: Schema.optional(Schema.Defect()), |
| | 68 | }, |
| 40 | ) { | 69 | ) { |
| 41 | override get message(): string { | 70 | override get message(): string { |
| 42 | return "Managed endpoint provisioning failed"; | 71 | return `Managed endpoint provisioning failed during '${this.stage}' for user '${this.userId}', environment '${this.environmentId}'`; |
| 43 | } | 72 | } |
| 44 | } | 73 | } |
| 45 | | 74 | |
| | 75 | const ManagedEndpointDeprovisioningStage = Schema.Literals([ |
| | 76 | "load-allocation", |
| | 77 | "delete-dns-record", |
| | 78 | "delete-tunnel", |
| | 79 | "remove-allocation", |
| | 80 | ]); |
| | 81 | |
| 46 | export class ManagedEndpointDeprovisioningFailed extends Schema.TaggedErrorClass<ManagedEndpointDeprovisioningFailed>()( | 82 | export class ManagedEndpointDeprovisioningFailed extends Schema.TaggedErrorClass<ManagedEndpointDeprovisioningFailed>()( |
| 47 | "ManagedEndpointDeprovisioningFailed", | 83 | "ManagedEndpointDeprovisioningFailed", |
| 48 | { cause: Schema.Defect() }, | 84 | { |
| | 85 | stage: ManagedEndpointDeprovisioningStage, |
| | 86 | userId: Schema.String, |
| | 87 | environmentId: Schema.String, |
| | 88 | tunnelId: Schema.optionalKey(Schema.String), |
| | 89 | dnsRecordId: Schema.optionalKey(Schema.String), |
| | 90 | cause: Schema.Defect(), |
| | 91 | }, |
| 49 | ) { | 92 | ) { |
| 50 | override get message(): string { | 93 | override get message(): string { |
| 51 | return "Managed endpoint deprovisioning failed"; | 94 | return `Managed endpoint deprovisioning failed during '${this.stage}' for user '${this.userId}', environment '${this.environmentId}'`; |
| 52 | } | 95 | } |
| 53 | } | 96 | } |
| 54 | | 97 | |
| 55 | export class ManagedEndpointOriginNotAllowed extends Schema.TaggedErrorClass<ManagedEndpointOriginNotAllowed>()( | 98 | export class ManagedEndpointOriginNotAllowed extends Schema.TaggedErrorClass<ManagedEndpointOriginNotAllowed>()( |
| 56 | "ManagedEndpointOriginNotAllowed", | 99 | "ManagedEndpointOriginNotAllowed", |
| 57 | { | 100 | { |
| | 101 | userId: Schema.String, |
| | 102 | environmentId: Schema.String, |
| 58 | host: Schema.String, | 103 | host: Schema.String, |
| 59 | port: Schema.Number, | 104 | port: Schema.Number, |
| 60 | }, | 105 | }, |
| 61 | ) { | 106 | ) { |
| 62 | override get message(): string { | 107 | override get message(): string { |
| 63 | return `Managed endpoint origin '${this.host}:${this.port}' is not allowed`; | 108 | return `Managed endpoint origin '${this.host}:${this.port}' is not allowed for user '${this.userId}', environment '${this.environmentId}'`; |
| 64 | } | 109 | } |
| 65 | } | 110 | } |
| 66 | | 111 | |
| ... | @@ -94,12 +139,26 @@ interface ManagedEndpointTunnel { | ... | @@ -94,12 +139,26 @@ interface ManagedEndpointTunnel { |
| 94 | readonly name?: string | null; | 139 | readonly name?: string | null; |
| 95 | } | 140 | } |
| 96 | | 141 | |
| | 142 | const ManagedEndpointTunnelClientOperation = Schema.Literals([ |
| | 143 | "list", |
| | 144 | "create", |
| | 145 | "put-configuration", |
| | 146 | "get-token", |
| | 147 | "delete", |
| | 148 | ]); |
| | 149 | |
| 97 | export class ManagedEndpointTunnelClientError extends Schema.TaggedErrorClass<ManagedEndpointTunnelClientError>()( | 150 | export class ManagedEndpointTunnelClientError extends Schema.TaggedErrorClass<ManagedEndpointTunnelClientError>()( |
| 98 | "ManagedEndpointTunnelClientError", | 151 | "ManagedEndpointTunnelClientError", |
| 99 | { cause: Schema.Defect() }, | 152 | { |
| | 153 | operation: ManagedEndpointTunnelClientOperation, |
| | 154 | tunnelName: Schema.optionalKey(Schema.String), |
| | 155 | tunnelId: Schema.optionalKey(Schema.String), |
| | 156 | cause: Schema.Defect(), |
| | 157 | }, |
| 100 | ) { | 158 | ) { |
| 101 | override get message(): string { | 159 | override get message(): string { |
| 102 | return "Managed endpoint tunnel provider request failed"; | 160 | const target = this.tunnelId ?? this.tunnelName; |
| | 161 | return `Managed endpoint tunnel provider '${this.operation}' request failed${target === undefined ? "" : ` for '${target}'`}`; |
| 103 | } | 162 | } |
| 104 | } | 163 | } |
| 105 | | 164 | |
| ... | @@ -147,12 +206,25 @@ interface ManagedEndpointCnameRecordInput { | ... | @@ -147,12 +206,25 @@ interface ManagedEndpointCnameRecordInput { |
| 147 | readonly proxied: true; | 206 | readonly proxied: true; |
| 148 | } | 207 | } |
| 149 | | 208 | |
| | 209 | const ManagedEndpointDnsClientOperation = Schema.Literals([ |
| | 210 | "list-records", |
| | 211 | "create-record", |
| | 212 | "update-record", |
| | 213 | "delete-record", |
| | 214 | ]); |
| | 215 | |
| 150 | export class ManagedEndpointDnsClientError extends Schema.TaggedErrorClass<ManagedEndpointDnsClientError>()( | 216 | export class ManagedEndpointDnsClientError extends Schema.TaggedErrorClass<ManagedEndpointDnsClientError>()( |
| 151 | "ManagedEndpointDnsClientError", | 217 | "ManagedEndpointDnsClientError", |
| 152 | { cause: Schema.Defect() }, | 218 | { |
| | 219 | operation: ManagedEndpointDnsClientOperation, |
| | 220 | hostname: Schema.optionalKey(Schema.String), |
| | 221 | dnsRecordId: Schema.optionalKey(Schema.String), |
| | 222 | cause: Schema.Defect(), |
| | 223 | }, |
| 153 | ) { | 224 | ) { |
| 154 | override get message(): string { | 225 | override get message(): string { |
| 155 | return "Managed endpoint DNS provider request failed"; | 226 | const target = this.dnsRecordId ?? this.hostname; |
| | 227 | return `Managed endpoint DNS provider '${this.operation}' request failed${target === undefined ? "" : ` for '${target}'`}`; |
| 156 | } | 228 | } |
| 157 | } | 229 | } |
| 158 | | 230 | |
| ... | @@ -183,13 +255,26 @@ export const layerDnsClient = (client: ManagedEndpointDnsClient["Service"]) => | ... | @@ -183,13 +255,26 @@ export const layerDnsClient = (client: ManagedEndpointDnsClient["Service"]) => |
| 183 | | 255 | |
| 184 | const requireCloudflareSettings = Effect.fnUntraced(function* ( | 256 | const requireCloudflareSettings = Effect.fnUntraced(function* ( |
| 185 | settings: RelayConfiguration.RelayConfiguration["Service"], | 257 | settings: RelayConfiguration.RelayConfiguration["Service"], |
| | 258 | input: { readonly userId: string; readonly environmentId: string }, |
| 186 | ) { | 259 | ) { |
| 187 | if (!settings.managedEndpointBaseDomain || !settings.managedEndpointNamespace) { | 260 | const baseDomain = settings.managedEndpointBaseDomain; |
| 188 | return yield* new ManagedEndpointProvisioningNotConfigured(); | 261 | const namespace = settings.managedEndpointNamespace; |
| | 262 | const missingSettings: Array<"managedEndpointBaseDomain" | "managedEndpointNamespace"> = []; |
| | 263 | if (!baseDomain) { |
| | 264 | missingSettings.push("managedEndpointBaseDomain"); |
| | 265 | } |
| | 266 | if (!namespace) { |
| | 267 | missingSettings.push("managedEndpointNamespace"); |
| | 268 | } |
| | 269 | if (!baseDomain || !namespace) { |
| | 270 | return yield* new ManagedEndpointProvisioningNotConfigured({ |
| | 271 | ...input, |
| | 272 | missingSettings, |
| | 273 | }); |
| 189 | } | 274 | } |
| 190 | return { | 275 | return { |
| 191 | baseDomain: settings.managedEndpointBaseDomain, | 276 | baseDomain, |
| 192 | namespace: settings.managedEndpointNamespace, | 277 | namespace, |
| 193 | }; | 278 | }; |
| 194 | }); | 279 | }); |
| 195 | | 280 | |
| ... | @@ -231,10 +316,19 @@ function isNotFoundCause(cause: unknown): boolean { | ... | @@ -231,10 +316,19 @@ function isNotFoundCause(cause: unknown): boolean { |
| 231 | return "cause" in cause && isNotFoundCause(cause.cause); | 316 | return "cause" in cause && isNotFoundCause(cause.cause); |
| 232 | } | 317 | } |
| 233 | | 318 | |
| 234 | const ignoreNotFound = <A, E>(effect: Effect.Effect<A, E>): Effect.Effect<void, E> => | 319 | type ManagedEndpointClientError = ManagedEndpointTunnelClientError | ManagedEndpointDnsClientError; |
| | 320 | |
| | 321 | const ignoreNotFound = <A>( |
| | 322 | effect: Effect.Effect<A, ManagedEndpointClientError>, |
| | 323 | ): Effect.Effect<void, ManagedEndpointClientError> => |
| 235 | effect.pipe( | 324 | effect.pipe( |
| 236 | Effect.asVoid, | 325 | Effect.asVoid, |
| 237 | Effect.catch((cause) => (isNotFoundCause(cause) ? Effect.void : Effect.fail(cause))), | 326 | Effect.catchTags({ |
| | 327 | ManagedEndpointTunnelClientError: (error) => |
| | 328 | isNotFoundCause(error.cause) ? Effect.void : Effect.fail(error), |
| | 329 | ManagedEndpointDnsClientError: (error) => |
| | 330 | isNotFoundCause(error.cause) ? Effect.void : Effect.fail(error), |
| | 331 | }), |
| 238 | ); | 332 | ); |
| 239 | | 333 | |
| 240 | const make = Effect.gen(function* () { | 334 | const make = Effect.gen(function* () { |
| ... | @@ -289,25 +383,26 @@ const make = Effect.gen(function* () { | ... | @@ -289,25 +383,26 @@ const make = Effect.gen(function* () { |
| 289 | } | 383 | } |
| 290 | return yield* dns.createRecord(dnsRecord).pipe( | 384 | return yield* dns.createRecord(dnsRecord).pipe( |
| 291 | Effect.map((record) => record.id), | 385 | Effect.map((record) => record.id), |
| 292 | Effect.catch((createError) => | 386 | Effect.catchTags({ |
| 293 | Effect.gen(function* () { | 387 | ManagedEndpointDnsClientError: (createError) => |
| 294 | let records = yield* dns.listRecords(hostname); | 388 | Effect.gen(function* () { |
| 295 | for (let attempt = 0; records.length === 0 && attempt < 4; attempt++) { | 389 | let records = yield* dns.listRecords(hostname); |
| 296 | yield* Effect.sleep("200 millis"); | 390 | for (let attempt = 0; records.length === 0 && attempt < 4; attempt++) { |
| 297 | records = yield* dns.listRecords(hostname); | 391 | yield* Effect.sleep("200 millis"); |
| 298 | } | 392 | records = yield* dns.listRecords(hostname); |
| 299 | return records; | 393 | } |
| 300 | }).pipe( | 394 | return records; |
| 301 | Effect.flatMap((records) => | 395 | }).pipe( |
| 302 | records.length > 0 | 396 | Effect.flatMap((records) => |
| 303 | ? updateExistingDnsRecords(records, preferredDnsRecordId, dnsRecord) | 397 | records.length > 0 |
| 304 | : Effect.fail(createError), | 398 | ? updateExistingDnsRecords(records, preferredDnsRecordId, dnsRecord) |
| 305 | ), | 399 | : Effect.fail(createError), |
| 306 | Effect.flatMap((dnsRecordId) => | 400 | ), |
| 307 | dnsRecordId === null ? Effect.fail(createError) : Effect.succeed(dnsRecordId), | 401 | Effect.flatMap((dnsRecordId) => |
| | 402 | dnsRecordId === null ? Effect.fail(createError) : Effect.succeed(dnsRecordId), |
| | 403 | ), |
| 308 | ), | 404 | ), |
| 309 | ), | 405 | }), |
| 310 | ), | | |
| 311 | ); | 406 | ); |
| 312 | }); | 407 | }); |
| 313 | | 408 | |
| ... | @@ -317,25 +412,59 @@ const make = Effect.gen(function* () { | ... | @@ -317,25 +412,59 @@ const make = Effect.gen(function* () { |
| 317 | "relay.user_id": input.userId, | 412 | "relay.user_id": input.userId, |
| 318 | "relay.environment_id": input.environmentId, | 413 | "relay.environment_id": input.environmentId, |
| 319 | }); | 414 | }); |
| 320 | const allocation = yield* allocations | 415 | const allocation = yield* allocations.get(input).pipe( |
| 321 | .get(input) | 416 | Effect.mapError( |
| 322 | .pipe(Effect.mapError((cause) => new ManagedEndpointDeprovisioningFailed({ cause }))); | 417 | (cause) => |
| | 418 | new ManagedEndpointDeprovisioningFailed({ |
| | 419 | ...input, |
| | 420 | stage: "load-allocation", |
| | 421 | cause, |
| | 422 | }), |
| | 423 | ), |
| | 424 | ); |
| 323 | if (allocation === null) { | 425 | if (allocation === null) { |
| 324 | return; | 426 | return; |
| 325 | } | 427 | } |
| 326 | if (allocation.dnsRecordId !== null) { | 428 | const dnsRecordId = allocation.dnsRecordId; |
| 327 | yield* ignoreNotFound(dns.deleteRecord(allocation.dnsRecordId)).pipe( | 429 | if (dnsRecordId !== null) { |
| 328 | Effect.mapError((cause) => new ManagedEndpointDeprovisioningFailed({ cause })), | 430 | yield* ignoreNotFound(dns.deleteRecord(dnsRecordId)).pipe( |
| | 431 | Effect.mapError( |
| | 432 | (cause) => |
| | 433 | new ManagedEndpointDeprovisioningFailed({ |
| | 434 | ...input, |
| | 435 | stage: "delete-dns-record", |
| | 436 | dnsRecordId, |
| | 437 | cause, |
| | 438 | }), |
| | 439 | ), |
| 329 | ); | 440 | ); |
| 330 | } | 441 | } |
| 331 | if (allocation.tunnelId !== null) { | 442 | const tunnelId = allocation.tunnelId; |
| 332 | yield* ignoreNotFound(tunnels.delete(allocation.tunnelId)).pipe( | 443 | if (tunnelId !== null) { |
| 333 | Effect.mapError((cause) => new ManagedEndpointDeprovisioningFailed({ cause })), | 444 | yield* ignoreNotFound(tunnels.delete(tunnelId)).pipe( |
| | 445 | Effect.mapError( |
| | 446 | (cause) => |
| | 447 | new ManagedEndpointDeprovisioningFailed({ |
| | 448 | ...input, |
| | 449 | stage: "delete-tunnel", |
| | 450 | tunnelId, |
| | 451 | cause, |
| | 452 | }), |
| | 453 | ), |
| 334 | ); | 454 | ); |
| 335 | } | 455 | } |
| 336 | yield* allocations | 456 | yield* allocations.remove(input).pipe( |
| 337 | .remove(input) | 457 | Effect.mapError( |
| 338 | .pipe(Effect.mapError((cause) => new ManagedEndpointDeprovisioningFailed({ cause }))); | 458 | (cause) => |
| | 459 | new ManagedEndpointDeprovisioningFailed({ |
| | 460 | ...input, |
| | 461 | stage: "remove-allocation", |
| | 462 | ...(allocation.tunnelId === null ? {} : { tunnelId: allocation.tunnelId }), |
| | 463 | ...(allocation.dnsRecordId === null ? {} : { dnsRecordId: allocation.dnsRecordId }), |
| | 464 | cause, |
| | 465 | }), |
| | 466 | ), |
| | 467 | ); |
| 339 | }), | 468 | }), |
| 340 | provision: Effect.fn("relay.managed_endpoint_provider.provision")(function* (input) { | 469 | provision: Effect.fn("relay.managed_endpoint_provider.provision")(function* (input) { |
| 341 | yield* Effect.annotateCurrentSpan({ | 470 | yield* Effect.annotateCurrentSpan({ |
| ... | @@ -346,11 +475,13 @@ const make = Effect.gen(function* () { | ... | @@ -346,11 +475,13 @@ const make = Effect.gen(function* () { |
| 346 | }); | 475 | }); |
| 347 | if (!isLoopbackOrigin(input.origin)) { | 476 | if (!isLoopbackOrigin(input.origin)) { |
| 348 | return yield* new ManagedEndpointOriginNotAllowed({ | 477 | return yield* new ManagedEndpointOriginNotAllowed({ |
| | 478 | userId: input.userId, |
| | 479 | environmentId: input.environmentId, |
| 349 | host: input.origin.localHttpHost, | 480 | host: input.origin.localHttpHost, |
| 350 | port: input.origin.localHttpPort, | 481 | port: input.origin.localHttpPort, |
| 351 | }); | 482 | }); |
| 352 | } | 483 | } |
| 353 | const cf = yield* requireCloudflareSettings(config); | 484 | const cf = yield* requireCloudflareSettings(config, input); |
| 354 | const environmentHash = yield* crypto | 485 | const environmentHash = yield* crypto |
| 355 | .digest( | 486 | .digest( |
| 356 | "SHA-256", | 487 | "SHA-256", |
| ... | @@ -360,19 +491,45 @@ const make = Effect.gen(function* () { | ... | @@ -360,19 +491,45 @@ const make = Effect.gen(function* () { |
| 360 | ) | 491 | ) |
| 361 | .pipe( | 492 | .pipe( |
| 362 | Effect.map(Encoding.encodeHex), | 493 | Effect.map(Encoding.encodeHex), |
| 363 | Effect.mapError((cause) => new ManagedEndpointProvisioningFailed({ cause })), | 494 | Effect.mapError( |
| | 495 | (cause) => |
| | 496 | new ManagedEndpointProvisioningFailed({ |
| | 497 | userId: input.userId, |
| | 498 | environmentId: input.environmentId, |
| | 499 | stage: "derive-environment-hash", |
| | 500 | cause, |
| | 501 | }), |
| | 502 | ), |
| 364 | ); | 503 | ); |
| | 504 | const requestedHostname = managedEndpointHostname( |
| | 505 | cf.namespace, |
| | 506 | cf.baseDomain, |
| | 507 | environmentHash, |
| | 508 | ); |
| | 509 | const requestedTunnelName = managedEndpointTunnelName(cf.namespace, environmentHash); |
| 365 | const allocation = yield* allocations | 510 | const allocation = yield* allocations |
| 366 | .reserve({ | 511 | .reserve({ |
| 367 | userId: input.userId, | 512 | userId: input.userId, |
| 368 | environmentId: input.environmentId, | 513 | environmentId: input.environmentId, |
| 369 | hostname: managedEndpointHostname(cf.namespace, cf.baseDomain, environmentHash), | 514 | hostname: requestedHostname, |
| 370 | tunnelName: managedEndpointTunnelName(cf.namespace, environmentHash), | 515 | tunnelName: requestedTunnelName, |
| 371 | }) | 516 | }) |
| 372 | .pipe(Effect.mapError((cause) => new ManagedEndpointProvisioningFailed({ cause }))); | 517 | .pipe( |
| | 518 | Effect.mapError( |
| | 519 | (cause) => |
| | 520 | new ManagedEndpointProvisioningFailed({ |
| | 521 | userId: input.userId, |
| | 522 | environmentId: input.environmentId, |
| | 523 | stage: "reserve-allocation", |
| | 524 | hostname: requestedHostname, |
| | 525 | tunnelName: requestedTunnelName, |
| | 526 | cause, |
| | 527 | }), |
| | 528 | ), |
| | 529 | ); |
| 373 | const { hostname, tunnelName } = allocation; | 530 | const { hostname, tunnelName } = allocation; |
| 374 | | 531 | |
| 375 | const tunnel = yield* tunnels.list({ name: tunnelName, isDeleted: false }).pipe( | 532 | const tunnelResponse = yield* tunnels.list({ name: tunnelName, isDeleted: false }).pipe( |
| 376 | Effect.map((tunnels) => tunnels.result), | 533 | Effect.map((tunnels) => tunnels.result), |
| 377 | Effect.map(Arr.findFirst((tunnel) => tunnel.name === tunnelName)), | 534 | Effect.map(Arr.findFirst((tunnel) => tunnel.name === tunnelName)), |
| 378 | Effect.flatMap( | 535 | Effect.flatMap( |
| ... | @@ -381,20 +538,50 @@ const make = Effect.gen(function* () { | ... | @@ -381,20 +538,50 @@ const make = Effect.gen(function* () { |
| 381 | onNone: () => tunnels.create({ name: tunnelName, configSrc: "cloudflare" }), | 538 | onNone: () => tunnels.create({ name: tunnelName, configSrc: "cloudflare" }), |
| 382 | }), | 539 | }), |
| 383 | ), | 540 | ), |
| 384 | Effect.filterMapOrFail((tunnel) => | 541 | Effect.mapError( |
| 385 | tunnel.id && tunnel.name | 542 | (cause) => |
| 386 | ? Result.succeed({ id: tunnel.id, name: tunnel.name }) | 543 | new ManagedEndpointProvisioningFailed({ |
| 387 | : Result.fail(new ManagedEndpointProvisioningFailed({ cause: tunnel })), | 544 | userId: input.userId, |
| | 545 | environmentId: input.environmentId, |
| | 546 | stage: "ensure-tunnel", |
| | 547 | hostname, |
| | 548 | tunnelName, |
| | 549 | cause, |
| | 550 | }), |
| 388 | ), | 551 | ), |
| 389 | Effect.mapError((cause) => new ManagedEndpointProvisioningFailed({ cause })), | | |
| 390 | ); | 552 | ); |
| | 553 | if (!tunnelResponse.id || !tunnelResponse.name) { |
| | 554 | return yield* new ManagedEndpointProvisioningFailed({ |
| | 555 | userId: input.userId, |
| | 556 | environmentId: input.environmentId, |
| | 557 | stage: "validate-tunnel-response", |
| | 558 | hostname, |
| | 559 | tunnelName, |
| | 560 | ...(tunnelResponse.id ? { returnedTunnelId: tunnelResponse.id } : {}), |
| | 561 | ...(tunnelResponse.name ? { returnedTunnelName: tunnelResponse.name } : {}), |
| | 562 | }); |
| | 563 | } |
| | 564 | const tunnel = { id: tunnelResponse.id, name: tunnelResponse.name }; |
| 391 | yield* allocations | 565 | yield* allocations |
| 392 | .recordTunnel({ | 566 | .recordTunnel({ |
| 393 | userId: input.userId, | 567 | userId: input.userId, |
| 394 | environmentId: input.environmentId, | 568 | environmentId: input.environmentId, |
| 395 | tunnelId: tunnel.id, | 569 | tunnelId: tunnel.id, |
| 396 | }) | 570 | }) |
| 397 | .pipe(Effect.mapError((cause) => new ManagedEndpointProvisioningFailed({ cause }))); | 571 | .pipe( |
| | 572 | Effect.mapError( |
| | 573 | (cause) => |
| | 574 | new ManagedEndpointProvisioningFailed({ |
| | 575 | userId: input.userId, |
| | 576 | environmentId: input.environmentId, |
| | 577 | stage: "record-tunnel", |
| | 578 | hostname, |
| | 579 | tunnelName, |
| | 580 | tunnelId: tunnel.id, |
| | 581 | cause, |
| | 582 | }), |
| | 583 | ), |
| | 584 | ); |
| 398 | | 585 | |
| 399 | yield* tunnels | 586 | yield* tunnels |
| 400 | .putConfiguration(tunnel.id, { | 587 | .putConfiguration(tunnel.id, { |
| ... | @@ -406,7 +593,20 @@ const make = Effect.gen(function* () { | ... | @@ -406,7 +593,20 @@ const make = Effect.gen(function* () { |
| 406 | { service: "http_status:404" }, | 593 | { service: "http_status:404" }, |
| 407 | ], | 594 | ], |
| 408 | }) | 595 | }) |
| 409 | .pipe(Effect.mapError((cause) => new ManagedEndpointProvisioningFailed({ cause }))); | 596 | .pipe( |
| | 597 | Effect.mapError( |
| | 598 | (cause) => |
| | 599 | new ManagedEndpointProvisioningFailed({ |
| | 600 | userId: input.userId, |
| | 601 | environmentId: input.environmentId, |
| | 602 | stage: "configure-tunnel", |
| | 603 | hostname, |
| | 604 | tunnelName, |
| | 605 | tunnelId: tunnel.id, |
| | 606 | cause, |
| | 607 | }), |
| | 608 | ), |
| | 609 | ); |
| 410 | | 610 | |
| 411 | const dnsRecord = { | 611 | const dnsRecord = { |
| 412 | type: "CNAME", | 612 | type: "CNAME", |
| ... | @@ -417,7 +617,19 @@ const make = Effect.gen(function* () { | ... | @@ -417,7 +617,19 @@ const make = Effect.gen(function* () { |
| 417 | } as const; | 617 | } as const; |
| 418 | | 618 | |
| 419 | const dnsRecordId = yield* ensureDnsRecord(hostname, allocation.dnsRecordId, dnsRecord).pipe( | 619 | const dnsRecordId = yield* ensureDnsRecord(hostname, allocation.dnsRecordId, dnsRecord).pipe( |
| 420 | Effect.mapError((cause) => new ManagedEndpointProvisioningFailed({ cause })), | 620 | Effect.mapError( |
| | 621 | (cause) => |
| | 622 | new ManagedEndpointProvisioningFailed({ |
| | 623 | userId: input.userId, |
| | 624 | environmentId: input.environmentId, |
| | 625 | stage: "ensure-dns-record", |
| | 626 | hostname, |
| | 627 | tunnelName, |
| | 628 | tunnelId: tunnel.id, |
| | 629 | ...(allocation.dnsRecordId === null ? {} : { dnsRecordId: allocation.dnsRecordId }), |
| | 630 | cause, |
| | 631 | }), |
| | 632 | ), |
| 421 | ); | 633 | ); |
| 422 | yield* allocations | 634 | yield* allocations |
| 423 | .recordDns({ | 635 | .recordDns({ |
| ... | @@ -425,17 +637,57 @@ const make = Effect.gen(function* () { | ... | @@ -425,17 +637,57 @@ const make = Effect.gen(function* () { |
| 425 | environmentId: input.environmentId, | 637 | environmentId: input.environmentId, |
| 426 | dnsRecordId, | 638 | dnsRecordId, |
| 427 | }) | 639 | }) |
| 428 | .pipe(Effect.mapError((cause) => new ManagedEndpointProvisioningFailed({ cause }))); | 640 | .pipe( |
| | 641 | Effect.mapError( |
| | 642 | (cause) => |
| | 643 | new ManagedEndpointProvisioningFailed({ |
| | 644 | userId: input.userId, |
| | 645 | environmentId: input.environmentId, |
| | 646 | stage: "record-dns", |
| | 647 | hostname, |
| | 648 | tunnelName, |
| | 649 | tunnelId: tunnel.id, |
| | 650 | dnsRecordId, |
| | 651 | cause, |
| | 652 | }), |
| | 653 | ), |
| | 654 | ); |
| 429 | | 655 | |
| 430 | const connectorToken = yield* tunnels | 656 | const connectorToken = yield* tunnels.getToken(tunnel.id).pipe( |
| 431 | .getToken(tunnel.id) | 657 | Effect.mapError( |
| 432 | .pipe(Effect.mapError((cause) => new ManagedEndpointProvisioningFailed({ cause }))); | 658 | (cause) => |
| | 659 | new ManagedEndpointProvisioningFailed({ |
| | 660 | userId: input.userId, |
| | 661 | environmentId: input.environmentId, |
| | 662 | stage: "get-tunnel-token", |
| | 663 | hostname, |
| | 664 | tunnelName, |
| | 665 | tunnelId: tunnel.id, |
| | 666 | dnsRecordId, |
| | 667 | cause, |
| | 668 | }), |
| | 669 | ), |
| | 670 | ); |
| 433 | yield* allocations | 671 | yield* allocations |
| 434 | .markReady({ | 672 | .markReady({ |
| 435 | userId: input.userId, | 673 | userId: input.userId, |
| 436 | environmentId: input.environmentId, | 674 | environmentId: input.environmentId, |
| 437 | }) | 675 | }) |
| 438 | .pipe(Effect.mapError((cause) => new ManagedEndpointProvisioningFailed({ cause }))); | 676 | .pipe( |
| | 677 | Effect.mapError( |
| | 678 | (cause) => |
| | 679 | new ManagedEndpointProvisioningFailed({ |
| | 680 | userId: input.userId, |
| | 681 | environmentId: input.environmentId, |
| | 682 | stage: "mark-allocation-ready", |
| | 683 | hostname, |
| | 684 | tunnelName, |
| | 685 | tunnelId: tunnel.id, |
| | 686 | dnsRecordId, |
| | 687 | cause, |
| | 688 | }), |
| | 689 | ), |
| | 690 | ); |
| 439 | | 691 | |
| 440 | return { | 692 | return { |
| 441 | endpoint: managedEndpointForHostname(hostname), | 693 | endpoint: managedEndpointForHostname(hostname), |
| ... | @@ -464,27 +716,62 @@ export const layerCloudflareBindings = ( | ... | @@ -464,27 +716,62 @@ export const layerCloudflareBindings = ( |
| 464 | ManagedEndpointTunnelClient.of({ | 716 | ManagedEndpointTunnelClient.of({ |
| 465 | list: (request) => | 717 | list: (request) => |
| 466 | tunnelClient.list(request).pipe( | 718 | tunnelClient.list(request).pipe( |
| 467 | Effect.mapError((cause) => new ManagedEndpointTunnelClientError({ cause })), | 719 | Effect.mapError( |
| | 720 | (cause) => |
| | 721 | new ManagedEndpointTunnelClientError({ |
| | 722 | operation: "list", |
| | 723 | tunnelName: request.name, |
| | 724 | cause, |
| | 725 | }), |
| | 726 | ), |
| 468 | Effect.provideService(Alchemy.RuntimeContext, alchemyRuntimeContext), | 727 | Effect.provideService(Alchemy.RuntimeContext, alchemyRuntimeContext), |
| 469 | ), | 728 | ), |
| 470 | create: (request) => | 729 | create: (request) => |
| 471 | tunnelClient.create(request).pipe( | 730 | tunnelClient.create(request).pipe( |
| 472 | Effect.mapError((cause) => new ManagedEndpointTunnelClientError({ cause })), | 731 | Effect.mapError( |
| | 732 | (cause) => |
| | 733 | new ManagedEndpointTunnelClientError({ |
| | 734 | operation: "create", |
| | 735 | tunnelName: request.name, |
| | 736 | cause, |
| | 737 | }), |
| | 738 | ), |
| 473 | Effect.provideService(Alchemy.RuntimeContext, alchemyRuntimeContext), | 739 | Effect.provideService(Alchemy.RuntimeContext, alchemyRuntimeContext), |
| 474 | ), | 740 | ), |
| 475 | putConfiguration: (tunnelId, config) => | 741 | putConfiguration: (tunnelId, config) => |
| 476 | tunnelClient.putConfiguration(tunnelId, config).pipe( | 742 | tunnelClient.putConfiguration(tunnelId, config).pipe( |
| 477 | Effect.mapError((cause) => new ManagedEndpointTunnelClientError({ cause })), | 743 | Effect.mapError( |
| | 744 | (cause) => |
| | 745 | new ManagedEndpointTunnelClientError({ |
| | 746 | operation: "put-configuration", |
| | 747 | tunnelId, |
| | 748 | cause, |
| | 749 | }), |
| | 750 | ), |
| 478 | Effect.provideService(Alchemy.RuntimeContext, alchemyRuntimeContext), | 751 | Effect.provideService(Alchemy.RuntimeContext, alchemyRuntimeContext), |
| 479 | ), | 752 | ), |
| 480 | getToken: (tunnelId) => | 753 | getToken: (tunnelId) => |
| 481 | tunnelClient.getToken(tunnelId).pipe( | 754 | tunnelClient.getToken(tunnelId).pipe( |
| 482 | Effect.mapError((cause) => new ManagedEndpointTunnelClientError({ cause })), | 755 | Effect.mapError( |
| | 756 | (cause) => |
| | 757 | new ManagedEndpointTunnelClientError({ |
| | 758 | operation: "get-token", |
| | 759 | tunnelId, |
| | 760 | cause, |
| | 761 | }), |
| | 762 | ), |
| 483 | Effect.provideService(Alchemy.RuntimeContext, alchemyRuntimeContext), | 763 | Effect.provideService(Alchemy.RuntimeContext, alchemyRuntimeContext), |
| 484 | ), | 764 | ), |
| 485 | delete: (tunnelId) => | 765 | delete: (tunnelId) => |
| 486 | tunnelClient.delete(tunnelId).pipe( | 766 | tunnelClient.delete(tunnelId).pipe( |
| 487 | Effect.mapError((cause) => new ManagedEndpointTunnelClientError({ cause })), | 767 | Effect.mapError( |
| | 768 | (cause) => |
| | 769 | new ManagedEndpointTunnelClientError({ |
| | 770 | operation: "delete", |
| | 771 | tunnelId, |
| | 772 | cause, |
| | 773 | }), |
| | 774 | ), |
| 488 | Effect.provideService(Alchemy.RuntimeContext, alchemyRuntimeContext), | 775 | Effect.provideService(Alchemy.RuntimeContext, alchemyRuntimeContext), |
| 489 | ), | 776 | ), |
| 490 | }), | 777 | }), |
| ... | @@ -500,23 +787,52 @@ export const layerCloudflareBindings = ( | ... | @@ -500,23 +787,52 @@ export const layerCloudflareBindings = ( |
| 500 | normalizeHostname(record.name) === normalizeHostname(hostname), | 787 | normalizeHostname(record.name) === normalizeHostname(hostname), |
| 501 | ), | 788 | ), |
| 502 | ), | 789 | ), |
| 503 | Effect.mapError((cause) => new ManagedEndpointDnsClientError({ cause })), | 790 | Effect.mapError( |
| | 791 | (cause) => |
| | 792 | new ManagedEndpointDnsClientError({ |
| | 793 | operation: "list-records", |
| | 794 | hostname, |
| | 795 | cause, |
| | 796 | }), |
| | 797 | ), |
| 504 | Effect.provideService(Alchemy.RuntimeContext, alchemyRuntimeContext), | 798 | Effect.provideService(Alchemy.RuntimeContext, alchemyRuntimeContext), |
| 505 | ), | 799 | ), |
| 506 | createRecord: (request) => | 800 | createRecord: (request) => |
| 507 | dnsClient.createDnsRecord(request).pipe( | 801 | dnsClient.createDnsRecord(request).pipe( |
| 508 | Effect.map((response) => ({ id: response.id })), | 802 | Effect.map((response) => ({ id: response.id })), |
| 509 | Effect.mapError((cause) => new ManagedEndpointDnsClientError({ cause })), | 803 | Effect.mapError( |
| | 804 | (cause) => |
| | 805 | new ManagedEndpointDnsClientError({ |
| | 806 | operation: "create-record", |
| | 807 | hostname: request.name, |
| | 808 | cause, |
| | 809 | }), |
| | 810 | ), |
| 510 | Effect.provideService(Alchemy.RuntimeContext, alchemyRuntimeContext), | 811 | Effect.provideService(Alchemy.RuntimeContext, alchemyRuntimeContext), |
| 511 | ), | 812 | ), |
| 512 | updateRecord: (dnsRecordId, request) => | 813 | updateRecord: (dnsRecordId, request) => |
| 513 | dnsClient.updateDnsRecord(dnsRecordId, request).pipe( | 814 | dnsClient.updateDnsRecord(dnsRecordId, request).pipe( |
| 514 | Effect.mapError((cause) => new ManagedEndpointDnsClientError({ cause })), | 815 | Effect.mapError( |
| | 816 | (cause) => |
| | 817 | new ManagedEndpointDnsClientError({ |
| | 818 | operation: "update-record", |
| | 819 | hostname: request.name, |
| | 820 | dnsRecordId, |
| | 821 | cause, |
| | 822 | }), |
| | 823 | ), |
| 515 | Effect.provideService(Alchemy.RuntimeContext, alchemyRuntimeContext), | 824 | Effect.provideService(Alchemy.RuntimeContext, alchemyRuntimeContext), |
| 516 | ), | 825 | ), |
| 517 | deleteRecord: (dnsRecordId) => | 826 | deleteRecord: (dnsRecordId) => |
| 518 | dnsClient.deleteDnsRecord(dnsRecordId).pipe( | 827 | dnsClient.deleteDnsRecord(dnsRecordId).pipe( |
| 519 | Effect.mapError((cause) => new ManagedEndpointDnsClientError({ cause })), | 828 | Effect.mapError( |
| | 829 | (cause) => |
| | 830 | new ManagedEndpointDnsClientError({ |
| | 831 | operation: "delete-record", |
| | 832 | dnsRecordId, |
| | 833 | cause, |
| | 834 | }), |
| | 835 | ), |
| 520 | Effect.provideService(Alchemy.RuntimeContext, alchemyRuntimeContext), | 836 | Effect.provideService(Alchemy.RuntimeContext, alchemyRuntimeContext), |
| 521 | ), | 837 | ), |
| 522 | }), | 838 | }), |