1import * as Cloudflare from "@/Cloudflare";
2import { CloudflareEnvironment } from "@/Cloudflare/CloudflareEnvironment";
3import * as Test from "@/Test/Alchemy";
4import * as workers from "@distilled.cloud/cloudflare/workers";
5import { expect } from "alchemy-test";
6import * as Effect from "effect/Effect";
7import { MinimumLogLevel } from "effect/References";
8
9const { test } = Test.make({ providers: Cloudflare.providers() });
10
11const logLevel = Effect.provideService(
12 MinimumLogLevel,
13 process.env.DEBUG ? "Debug" : "Info",
14);
15
16const asyncWorkerScript = `export default {
17 async fetch() {
18 return new Response("ok");
19 },
20};
21`;
22
23// Declares the tunnel + VPC service. Yielded in both deploys (same logical
24// ids), so the first deploy creates them and the second is a no-op reconcile
25// that keeps them alive while the worker binds.
26const vpcService = Effect.gen(function* () {
27 const tunnel = yield* Cloudflare.Tunnel.Tunnel("RefTunnel", {
28 ingress: [{ service: "http://localhost:8080" }],
29 adopt: true,
30 });
31 return yield* Cloudflare.VpcService.VpcService("RefSvc", {
32 httpPort: 8080,
33 host: {
34 hostname: "localhost",
35 resolverNetwork: { tunnelId: tunnel.tunnelId },
36 },
37 adopt: true,
38 });
39});
40
41// Read the worker's live `vpc_service` bindings out-of-band from the script
42// settings API.
43const readVpcBindings = (scriptName: string) =>
44 Effect.gen(function* () {
45 const { accountId } = yield* yield* CloudflareEnvironment;
46 const settings = yield* workers.getScriptScriptAndVersionSetting({
47 accountId,
48 scriptName,
49 });
50 return (settings.bindings ?? []).filter(
51 (b): b is Extract<typeof b, { type: "vpc_service" }> =>
52 b.type === "vpc_service",
53 );
54 });
55
56test.provider(
57 "looks up a vpc service by id/name and binds it to a worker",
58 (stack) =>
59 Effect.gen(function* () {
60 yield* stack.destroy();
61
62 // Create a real VPC service to look up.
63 const svc = yield* stack.deploy(vpcService);
64
65 // Bind the service to a worker three ways — the managed resource
66 // directly, and `lookup` data sources by id and by name. All emit a
67 // `vpc_service` binding. The lookups are also returned as stack
68 // outputs, pinning plan-time Output resolution. The service and its
69 // tunnel are re-declared so they stay deployed.
70 const { byId, byName, worker } = yield* stack.deploy(
71 Effect.gen(function* () {
72 const managed = yield* vpcService;
73 const worker = yield* Cloudflare.Worker("vpc-binding-worker", {
74 script: asyncWorkerScript,
75 env: {
76 SVC_MANAGED: managed,
77 SVC_BY_ID: Cloudflare.VpcService.lookup({
78 serviceId: svc.serviceId,
79 }),
80 SVC_BY_NAME: Cloudflare.VpcService.lookup({
81 name: svc.serviceName,
82 }),
83 },
84 });
85 return {
86 byId: Cloudflare.VpcService.lookup({ serviceId: svc.serviceId }),
87 byName: Cloudflare.VpcService.lookup({ name: svc.serviceName }),
88 worker,
89 };
90 }),
91 );
92
93 // The data source resolves to the service's attributes.
94 expect(byId.serviceId).toEqual(svc.serviceId);
95 expect(byName.serviceId).toEqual(svc.serviceId);
96 expect(byName.serviceName).toEqual(svc.serviceName);
97 expect(byName.httpPort).toEqual(svc.httpPort);
98
99 const vpc = yield* readVpcBindings(worker.workerName);
100 expect(vpc.find((b) => b.name === "SVC_MANAGED")?.serviceId).toEqual(
101 svc.serviceId,
102 );
103 expect(vpc.find((b) => b.name === "SVC_BY_ID")?.serviceId).toEqual(
104 svc.serviceId,
105 );
106 expect(vpc.find((b) => b.name === "SVC_BY_NAME")?.serviceId).toEqual(
107 svc.serviceId,
108 );
109
110 yield* stack.destroy();
111 }).pipe(logLevel),
112 {
113 tags: [
114 "provider:cloudflare",
115 "provider:cloudflare:tunnel",
116 "provider:cloudflare:vpcservice",
117 "provider:cloudflare:worker",
118 "live",
119 ],
120 timeout: 120_000,
121 },
122);