1import * as GCP from "@/GCP";
2import * as Test from "@/Test/Alchemy";
3import * as compute from "@distilled.cloud/gcp/compute_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 zone = "us-central1-a";
18const vmName = "alchemy-ti-backend";
19
20const lastSegment = (value: string | undefined): string => {
21 if (value === undefined || value.length === 0) return "";
22 const parts = value.split("/").filter((part) => part.length > 0);
23 return parts[parts.length - 1] ?? "";
24};
25
26const waitZoneOp = (operation: compute.Operation) => {
27 if (operation.status === "DONE") return Effect.succeed(operation);
28 const name = lastSegment(operation.name);
29 return GcpEnvironment.current.pipe(
30 Effect.flatMap(({ project }) =>
31 compute.getZoneOperations({ project, zone, operation: name }).pipe(
32 Effect.repeat({
33 schedule: Schedule.spaced("2 seconds"),
34 until: (op) => op.status === "DONE",
35 times: 20,
36 }),
37 ),
38 ),
39 );
40};
41
42const waitUntilGone = (targetInstance: string) =>
43 GcpEnvironment.current.pipe(
44 Effect.flatMap(({ project }) =>
45 compute.getTargetInstances({ project, zone, targetInstance }).pipe(
46 Effect.as("found" as const),
47 Effect.catchTag("NotFound", () => Effect.succeed("gone" as const)),
48 Effect.repeat({
49 schedule: Schedule.spaced("2 seconds"),
50 until: (status) => status === "gone",
51 times: 10,
52 }),
53 ),
54 ),
55 );
56
57const waitVmGone = () =>
58 GcpEnvironment.current.pipe(
59 Effect.flatMap(({ project }) =>
60 compute.getInstances({ project, zone, instance: vmName }).pipe(
61 Effect.as("found" as const),
62 Effect.catchTag("NotFound", () => Effect.succeed("gone" as const)),
63 Effect.repeat({
64 schedule: Schedule.spaced("3 seconds"),
65 until: (status) => status === "gone",
66 times: 16,
67 }),
68 ),
69 ),
70 );
71
72const getVm = () =>
73 GcpEnvironment.current.pipe(
74 Effect.flatMap(({ project }) =>
75 compute
76 .getInstances({ project, zone, instance: vmName })
77 .pipe(Effect.catchTag("NotFound", () => Effect.succeed(undefined))),
78 ),
79 );
80
81const ensureVm = () =>
82 Effect.gen(function* () {
83 const { project } = yield* GcpEnvironment.current;
84 const existing = yield* getVm();
85 if (existing !== undefined) return existing;
86 const operation = yield* compute
87 .insertInstances({
88 project,
89 zone,
90 body: {
91 name: vmName,
92 machineType: `zones/${zone}/machineTypes/e2-micro`,
93 canIpForward: true,
94 disks: [
95 {
96 boot: true,
97 autoDelete: true,
98 type: "PERSISTENT",
99 initializeParams: {
100 sourceImage:
101 "projects/debian-cloud/global/images/family/debian-12",
102 diskSizeGb: "10",
103 },
104 },
105 ],
106 networkInterfaces: [{ network: "global/networks/default" }],
107 },
108 })
109 .pipe(Effect.catchTag("Conflict", () => Effect.succeed(undefined)));
110 if (operation !== undefined) {
111 yield* waitZoneOp(operation);
112 }
113 return yield* compute
114 .getInstances({ project, zone, instance: vmName })
115 .pipe(
116 Effect.retry({
117 while: (error) => error._tag === "NotFound",
118 schedule: Schedule.spaced("2 seconds"),
119 times: 10,
120 }),
121 );
122 });
123
124const deleteVm = () =>
125 Effect.gen(function* () {
126 const { project } = yield* GcpEnvironment.current;
127 const operation = yield* compute
128 .deleteInstances({ project, zone, instance: vmName })
129 .pipe(Effect.catchTag("NotFound", () => Effect.succeed(undefined)));
130 if (operation !== undefined) {
131 yield* waitZoneOp(operation).pipe(
132 Effect.catchTag("NotFound", () => Effect.void),
133 );
134 }
135 yield* waitVmGone();
136 });
137
138test.provider.skipIf(!!process.env.FAST)(
139 "create, update, and delete a target instance",
140 (stack) =>
141 Effect.gen(function* () {
142 const { project } = yield* GcpEnvironment.current;
143 yield* stack.destroy();
144 yield* deleteVm().pipe(Effect.ignore);
145
146 const vm = yield* ensureVm();
147
148 const created = yield* stack.deploy(
149 Effect.gen(function* () {
150 return yield* GCP.Compute.TargetInstance("Target", {
151 description: "protocol frontend",
152 instance: vmName,
153 zone,
154 });
155 }),
156 );
157
158 expect(created.targetInstanceName).toEqual(expect.any(String));
159 expect(created.zone).toEqual(zone);
160 expect(created.description).toEqual("protocol frontend");
161 expect(lastSegment(created.instance)).toEqual(vmName);
162 expect(created.natPolicy).toEqual("NO_NAT");
163
164 const fetched = yield* compute.getTargetInstances({
165 project,
166 zone,
167 targetInstance: created.targetInstanceName,
168 });
169 expect(fetched.name).toEqual(created.targetInstanceName);
170 expect(lastSegment(fetched.instance)).toEqual(vmName);
171 expect(fetched.description).toContain("[alchemy ");
172 expect(fetched.description).toContain("protocol frontend");
173 expect(fetched.natPolicy).toEqual("NO_NAT");
174 expect(lastSegment(vm.name)).toEqual(vmName);
175
176 const updated = yield* stack.deploy(
177 Effect.gen(function* () {
178 return yield* GCP.Compute.TargetInstance("Target", {
179 targetInstanceName: created.targetInstanceName,
180 description: "updated protocol frontend",
181 instance: vmName,
182 zone,
183 });
184 }),
185 );
186
187 expect(updated.targetInstanceName).toEqual(created.targetInstanceName);
188 expect(updated.description).toEqual("updated protocol frontend");
189 expect(lastSegment(updated.instance)).toEqual(vmName);
190
191 const refetched = yield* compute.getTargetInstances({
192 project,
193 zone,
194 targetInstance: updated.targetInstanceName,
195 });
196 expect(refetched.description).toContain("updated protocol frontend");
197 expect(lastSegment(refetched.instance)).toEqual(vmName);
198
199 yield* stack.destroy();
200
201 const gone = yield* waitUntilGone(created.targetInstanceName);
202 expect(gone).toEqual("gone");
203
204 yield* deleteVm();
205 }).pipe(logLevel),
206 { tags: ["provider:gcp", "provider:gcp:compute", "live"], timeout: 180_000 },
207);