1import * as GCP from "@/GCP";
2import * as Test from "@/Test/Alchemy";
3import * as networkservices from "@distilled.cloud/gcp/networkservices_v1";
4import { expect } from "alchemy-test";
5import * as Effect from "effect/Effect";
6import { MinimumLogLevel } from "effect/References";
7import * as Schedule from "effect/Schedule";
8import { GcpEnvironment } from "@/GCP/Environment";
9
10const { test } = Test.make({ providers: GCP.providers() });
11
12const logLevel = Effect.provideService(
13 MinimumLogLevel,
14 process.env.DEBUG ? "Debug" : "Info",
15);
16
17const waitUntilGone = (name: string) =>
18 networkservices.getProjectsLocationsLbEdgeExtensions({ name }).pipe(
19 Effect.as("found" as const),
20 Effect.catchTag("NotFound", () => Effect.succeed("gone" as const)),
21 Effect.repeat({
22 schedule: Schedule.spaced("1 second"),
23 until: (status) => status === "gone",
24 times: 10,
25 }),
26 );
27
28test.provider(
29 "getProjectsLocationsLbEdgeExtensions on a missing extension fails with a typed tag",
30 (stack) =>
31 Effect.gen(function* () {
32 yield* stack.destroy();
33
34 const { project } = yield* GcpEnvironment.current;
35 const error = yield* Effect.flip(
36 networkservices.getProjectsLocationsLbEdgeExtensions({
37 name: `projects/${project}/locations/global/lbEdgeExtensions/alchemy-missing`,
38 }),
39 );
40 expect(error._tag).toEqual("NotFound");
41
42 yield* stack.destroy();
43 }).pipe(logLevel),
44 {
45 tags: ["provider:gcp", "provider:gcp:networkservices", "live"],
46 timeout: 90_000,
47 },
48);
49
50// Edge extensions need a WasmPlugin with a main version; the fixture's
51// bare plugin makes create fail with BadRequest "MAIN_VERSION_ID_EMPTY: Main
52// version ID must not be empty." Set GCP_TEST_LB_EDGE_EXTENSION=1 once the
53// fixture publishes a plugin version.
54test.provider.skipIf(
55 !!process.env.FAST || !process.env.GCP_TEST_LB_EDGE_EXTENSION,
56)(
57 "create, update, and delete an lb edge extension",
58 (stack) =>
59 Effect.gen(function* () {
60 yield* stack.destroy();
61
62 const created = yield* stack.deploy(
63 Effect.gen(function* () {
64 const plugin = yield* GCP.NetworkServices.WasmPlugin("EdgePlugin", {
65 location: "global",
66 description: "edge wasm",
67 });
68 const map = yield* GCP.Compute.UrlMap("EdgeMap", {
69 defaultUrlRedirect: {
70 httpsRedirect: true,
71 hostRedirect: "example.com",
72 stripQuery: false,
73 },
74 });
75 const proxy = yield* GCP.Compute.TargetHttpProxy("EdgeProxy", {
76 urlMap: map.urlMapName,
77 });
78 const rule = yield* GCP.Compute.GlobalForwardingRule("EdgeFr", {
79 target: proxy.selfLink.as<string>(),
80 portRange: "80",
81 loadBalancingScheme: "EXTERNAL_MANAGED",
82 });
83 return yield* GCP.NetworkServices.LbEdgeExtension("Edge", {
84 location: "global",
85 description: "lb edge a",
86 labels: { env: "test" },
87 loadBalancingScheme: "EXTERNAL_MANAGED",
88 forwardingRules: [rule.selfLink.as<string>()],
89 extensionChains: [
90 {
91 name: "chain1",
92 matchCondition: { celExpression: "true" },
93 extensions: [
94 {
95 name: "ext1",
96 service: plugin.name,
97 supportedEvents: ["REQUEST_HEADERS"],
98 },
99 ],
100 },
101 ],
102 });
103 }),
104 );
105
106 expect(created.name).toContain("/lbEdgeExtensions/");
107 expect(created.lbEdgeExtensionId).toEqual(expect.any(String));
108 expect(created.location).toEqual("global");
109 expect(created.description).toEqual("lb edge a");
110 expect(created.labels).toMatchObject({ env: "test" });
111 expect(created.createTime).toEqual(expect.any(String));
112
113 const fetched =
114 yield* networkservices.getProjectsLocationsLbEdgeExtensions({
115 name: created.name,
116 });
117 expect(fetched.name).toEqual(created.name);
118 expect(fetched.description).toEqual("lb edge a");
119 expect(fetched.labels?.env).toEqual("test");
120 expect(
121 Object.keys(fetched.labels ?? {}).some((key) =>
122 key.startsWith("alchemy-"),
123 ),
124 ).toEqual(true);
125
126 const updated = yield* stack.deploy(
127 Effect.gen(function* () {
128 const plugin = yield* GCP.NetworkServices.WasmPlugin("EdgePlugin", {
129 location: "global",
130 description: "edge wasm",
131 });
132 const map = yield* GCP.Compute.UrlMap("EdgeMap", {
133 defaultUrlRedirect: {
134 httpsRedirect: true,
135 hostRedirect: "example.com",
136 stripQuery: false,
137 },
138 });
139 const proxy = yield* GCP.Compute.TargetHttpProxy("EdgeProxy", {
140 urlMap: map.urlMapName,
141 });
142 const rule = yield* GCP.Compute.GlobalForwardingRule("EdgeFr", {
143 target: proxy.selfLink.as<string>(),
144 portRange: "80",
145 loadBalancingScheme: "EXTERNAL_MANAGED",
146 });
147 return yield* GCP.NetworkServices.LbEdgeExtension("Edge", {
148 lbEdgeExtensionId: created.lbEdgeExtensionId,
149 location: "global",
150 description: "lb edge b",
151 labels: { env: "prod", role: "edge" },
152 loadBalancingScheme: "EXTERNAL_MANAGED",
153 forwardingRules: [rule.selfLink.as<string>()],
154 extensionChains: [
155 {
156 name: "chain1",
157 matchCondition: { celExpression: "true" },
158 extensions: [
159 {
160 name: "ext1",
161 service: plugin.name,
162 failOpen: true,
163 supportedEvents: ["REQUEST_HEADERS"],
164 },
165 ],
166 },
167 ],
168 });
169 }),
170 );
171
172 expect(updated.name).toEqual(created.name);
173 expect(updated.description).toEqual("lb edge b");
174 expect(updated.labels).toMatchObject({ env: "prod", role: "edge" });
175
176 const refetched =
177 yield* networkservices.getProjectsLocationsLbEdgeExtensions({
178 name: created.name,
179 });
180 expect(refetched.description).toEqual("lb edge b");
181 expect(refetched.labels?.env).toEqual("prod");
182
183 yield* stack.destroy();
184
185 const gone = yield* waitUntilGone(created.name);
186 expect(gone).toEqual("gone");
187 }).pipe(logLevel),
188 {
189 tags: ["provider:gcp", "provider:gcp:networkservices", "live"],
190 timeout: 120_000,
191 },
192);