1import * as GCP from "@/GCP";
2import * as Test from "@/Test/Alchemy";
3import { Credentials } from "@distilled.cloud/gcp/Credentials";
4import * as networkservices from "@distilled.cloud/gcp/networkservices_v1";
5import { expect } from "alchemy-test";
6import * as Effect from "effect/Effect";
7import * as Redacted from "effect/Redacted";
8import { MinimumLogLevel } from "effect/References";
9import * as Schedule from "effect/Schedule";
10import * as HttpClient from "effect/http/HttpClient";
11import * as HttpClientRequest from "effect/http/HttpClientRequest";
12import { GcpEnvironment } from "@/GCP/Environment";
13
14const { test } = Test.make({ providers: GCP.providers() });
15
16const logLevel = Effect.provideService(
17 MinimumLogLevel,
18 process.env.DEBUG ? "Debug" : "Info",
19);
20
21const WASM = new Uint8Array([0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00]);
22
23const waitUntilGone = (name: string) =>
24 networkservices.getProjectsLocationsWasmPlugins({ name }).pipe(
25 Effect.as("found" as const),
26 Effect.catchTag("NotFound", () => Effect.succeed("gone" as const)),
27 Effect.repeat({
28 schedule: Schedule.spaced("1 second"),
29 until: (status) => status === "gone",
30 times: 10,
31 }),
32 );
33
34const imageUriOf = (repo: { name: string }) =>
35 `${repo.name}/genericArtifacts/plugin:v1`;
36
37const uploadPluginWasm = (repositoryName: string) =>
38 Effect.gen(function* () {
39 const creds = yield* yield* Credentials;
40 const client = yield* HttpClient.HttpClient;
41 const url =
42 `https://artifactregistry.googleapis.com/upload/v1/${repositoryName}/genericArtifacts:create` +
43 `?uploadType=media&filename=plugin.wasm&packageId=plugin&versionId=v1`;
44 const response = yield* client.execute(
45 HttpClientRequest.post(url).pipe(
46 HttpClientRequest.setHeader(
47 "Authorization",
48 `Bearer ${Redacted.value(creds.accessToken)}`,
49 ),
50 HttpClientRequest.bodyUint8Array(WASM, "application/octet-stream"),
51 ),
52 );
53 if (response.status === 409) return;
54 if (response.status < 200 || response.status >= 300) {
55 const body = yield* response.text.pipe(
56 Effect.catch(() => Effect.succeed("")),
57 );
58 return yield* Effect.fail(
59 new Error(`generic artifact upload failed: ${response.status} ${body}`),
60 );
61 }
62 }).pipe(
63 Effect.retry({
64 times: 5,
65 schedule: Schedule.spaced("2 seconds"),
66 }),
67 );
68
69test.provider(
70 "getProjectsLocationsWasmPlugins on a missing plugin fails with a typed tag",
71 (stack) =>
72 Effect.gen(function* () {
73 yield* stack.destroy();
74
75 const { project } = yield* GcpEnvironment.current;
76 const error = yield* Effect.flip(
77 networkservices.getProjectsLocationsWasmPlugins({
78 name: `projects/${project}/locations/global/wasmPlugins/alchemy-missing`,
79 }),
80 );
81 expect(error._tag).toEqual("NotFound");
82
83 yield* stack.destroy();
84 }).pipe(logLevel),
85 {
86 tags: ["provider:gcp", "provider:gcp:networkservices", "live"],
87 timeout: 90_000,
88 },
89);
90
91test.provider(
92 "create, update, and delete a wasm plugin",
93 (stack) =>
94 Effect.gen(function* () {
95 yield* stack.destroy();
96
97 const repo = yield* stack.deploy(
98 Effect.gen(function* () {
99 return yield* GCP.ArtifactRegistry.Repository("Wasm", {
100 location: "us-central1",
101 format: "GENERIC",
102 description: "wasm plugin artifacts",
103 });
104 }),
105 );
106
107 yield* uploadPluginWasm(repo.name);
108
109 const created = yield* stack.deploy(
110 Effect.gen(function* () {
111 const artifacts = yield* GCP.ArtifactRegistry.Repository("Wasm", {
112 repositoryId: repo.repositoryId,
113 location: "us-central1",
114 format: "GENERIC",
115 description: "wasm plugin artifacts",
116 });
117 const plugin = yield* GCP.NetworkServices.WasmPlugin("Edge", {
118 location: "global",
119 description: "wasm plugin a",
120 labels: { env: "test" },
121 mainVersionId: "v1",
122 logConfig: {
123 enable: true,
124 sampleRate: 1,
125 minLogLevel: "WARN",
126 },
127 versions: {
128 v1: {
129 description: "v1",
130 imageUri: imageUriOf(repo),
131 },
132 },
133 });
134 return { artifacts, plugin };
135 }),
136 );
137
138 expect(created.plugin.name).toContain("/wasmPlugins/");
139 expect(created.plugin.wasmPluginId).toEqual(expect.any(String));
140 expect(created.plugin.location).toEqual("global");
141 expect(created.plugin.description).toEqual("wasm plugin a");
142 expect(created.plugin.labels).toMatchObject({ env: "test" });
143 expect(created.plugin.mainVersionId).toEqual("v1");
144 expect(created.plugin.logConfig?.enable).toEqual(true);
145 expect(created.plugin.createTime).toEqual(expect.any(String));
146
147 const fetched = yield* networkservices.getProjectsLocationsWasmPlugins({
148 name: created.plugin.name,
149 view: "WASM_PLUGIN_VIEW_FULL",
150 });
151 expect(fetched.name).toEqual(created.plugin.name);
152 expect(fetched.description).toEqual("wasm plugin a");
153 expect(fetched.mainVersionId).toEqual("v1");
154 expect(fetched.labels?.env).toEqual("test");
155 expect(
156 Object.keys(fetched.labels ?? {}).some((key) =>
157 key.startsWith("alchemy-"),
158 ),
159 ).toEqual(true);
160 expect(fetched.versions?.v1).toBeDefined();
161
162 const updated = yield* stack.deploy(
163 Effect.gen(function* () {
164 const artifacts = yield* GCP.ArtifactRegistry.Repository("Wasm", {
165 repositoryId: repo.repositoryId,
166 location: "us-central1",
167 format: "GENERIC",
168 description: "wasm plugin artifacts",
169 });
170 const plugin = yield* GCP.NetworkServices.WasmPlugin("Edge", {
171 wasmPluginId: created.plugin.wasmPluginId,
172 location: "global",
173 description: "wasm plugin b",
174 labels: { env: "prod", role: "edge" },
175 mainVersionId: "v1",
176 logConfig: {
177 enable: true,
178 sampleRate: 0.5,
179 minLogLevel: "ERROR",
180 },
181 });
182 return { artifacts, plugin };
183 }),
184 );
185
186 expect(updated.plugin.name).toEqual(created.plugin.name);
187 expect(updated.plugin.description).toEqual("wasm plugin b");
188 expect(updated.plugin.labels).toMatchObject({
189 env: "prod",
190 role: "edge",
191 });
192 expect(updated.plugin.mainVersionId).toEqual("v1");
193 expect(updated.plugin.logConfig?.minLogLevel).toEqual("ERROR");
194
195 const refetched = yield* networkservices.getProjectsLocationsWasmPlugins({
196 name: created.plugin.name,
197 });
198 expect(refetched.description).toEqual("wasm plugin b");
199 expect(refetched.labels?.env).toEqual("prod");
200 expect(refetched.labels?.role).toEqual("edge");
201 expect(refetched.logConfig?.minLogLevel).toEqual("ERROR");
202
203 yield* stack.destroy();
204
205 const gone = yield* waitUntilGone(created.plugin.name);
206 expect(gone).toEqual("gone");
207 }).pipe(logLevel),
208 {
209 tags: ["provider:gcp", "provider:gcp:networkservices", "live"],
210 timeout: 120_000,
211 },
212);