1import * as GCP from "@/GCP";
2import * as Test from "@/Test/Alchemy";
3import * as workstations from "@distilled.cloud/gcp/workstations_v1";
4import { expect } from "alchemy-test";
5import * as Effect from "effect/Effect";
6import { MinimumLogLevel } from "effect/References";
7import { GcpEnvironment } from "@/GCP/Environment";
8
9const { test } = Test.make({ providers: GCP.providers() });
10
11const logLevel = Effect.provideService(
12 MinimumLogLevel,
13 process.env.DEBUG ? "Debug" : "Info",
14);
15
16// Workstation clusters take ~20 minutes to create and delete.
17const runLifecycle = !!process.env.GCP_TEST_SLOW && !process.env.FAST;
18
19test.provider(
20 "getProjectsLocationsWorkstationClusters on a missing cluster fails with a typed tag",
21 (stack) =>
22 Effect.gen(function* () {
23 const { project } = yield* GcpEnvironment.current;
24 yield* stack.destroy();
25
26 const error = yield* Effect.flip(
27 workstations.getProjectsLocationsWorkstationClusters({
28 name: `projects/${project}/locations/us-central1/workstationClusters/alchemy-missing-cluster`,
29 }),
30 );
31 expect(error._tag).toEqual("NotFound");
32
33 const page = yield* workstations.listProjectsLocationsWorkstationClusters(
34 {
35 parent: `projects/${project}/locations/-`,
36 pageSize: 10,
37 },
38 );
39 expect(
40 (page.workstationClusters ?? []).map((item) => item.name),
41 ).not.toContain(
42 `projects/${project}/locations/us-central1/workstationClusters/alchemy-missing-cluster`,
43 );
44
45 yield* stack.destroy();
46 }).pipe(logLevel),
47 {
48 tags: ["provider:gcp", "provider:gcp:workstations", "live"],
49 timeout: 90_000,
50 },
51);
52
53test.provider.skipIf(!runLifecycle)(
54 "create against a missing network fails with GCP.OperationFailed",
55 (stack) =>
56 Effect.gen(function* () {
57 const { project } = yield* GcpEnvironment.current;
58 yield* stack.destroy();
59
60 const error = yield* Effect.flip(
61 stack.deploy(
62 Effect.gen(function* () {
63 return yield* GCP.Workstations.WorkstationCluster("Dev", {
64 network: `projects/${project}/global/networks/alchemy-missing-vpc`,
65 subnetwork: `projects/${project}/regions/us-central1/subnetworks/alchemy-missing-subnet`,
66 displayName: "alchemy-test-cluster",
67 labels: { env: "test" },
68 });
69 }),
70 ),
71 );
72 // The cluster is accepted and provisioned before the network is
73 // validated, so the create operation fails late (~50 minutes).
74 expect(error._tag).toEqual("GCP.OperationFailed");
75
76 yield* stack.destroy();
77 }).pipe(logLevel),
78 {
79 tags: ["provider:gcp", "provider:gcp:workstations", "live"],
80 // One attempt that outlasts GCP's late failure; a retry would restart
81 // the ~50-minute wait from scratch.
82 timeout: 4_500_000,
83 retry: 0,
84 },
85);
86
87test.provider.skipIf(!runLifecycle)(
88 "create, update, and delete a workstation cluster, config, and workstation",
89 (stack) =>
90 Effect.gen(function* () {
91 yield* stack.destroy();
92
93 const created = yield* stack.deploy(
94 Effect.gen(function* () {
95 const cluster = yield* GCP.Workstations.WorkstationCluster("Dev", {
96 location: "us-central1",
97 network: "default",
98 subnetwork: "default",
99 displayName: "alchemy-test-cluster",
100 labels: { env: "test" },
101 });
102 const config =
103 yield* GCP.Workstations.WorkstationClustersWorkstationConfig(
104 "Code",
105 {
106 workstationCluster: cluster.name,
107 displayName: "alchemy-test-config",
108 idleTimeout: "300s",
109 host: {
110 gceInstance: {
111 machineType: "e2-standard-2",
112 poolSize: 0,
113 bootDiskSizeGb: 30,
114 },
115 },
116 labels: { env: "test" },
117 },
118 );
119 const workstation =
120 yield* GCP.Workstations.WorkstationClustersWorkstationConfigsWorkstation(
121 "Mine",
122 {
123 workstationConfig: config.name,
124 displayName: "alchemy-test-station",
125 labels: { env: "test" },
126 },
127 );
128 return { cluster, config, workstation };
129 }),
130 );
131
132 expect(created.cluster.name).toContain("/workstationClusters/");
133 expect(created.cluster.labels).toMatchObject({ env: "test" });
134 expect(created.config.workstationCluster).toEqual(created.cluster.name);
135 expect(created.workstation.workstationConfig).toEqual(
136 created.config.name,
137 );
138
139 const fetched =
140 yield* workstations.getProjectsLocationsWorkstationClusters({
141 name: created.cluster.name,
142 });
143 expect(fetched.name).toEqual(created.cluster.name);
144 expect(fetched.labels?.env).toEqual("test");
145
146 const updated = yield* stack.deploy(
147 Effect.gen(function* () {
148 const cluster = yield* GCP.Workstations.WorkstationCluster("Dev", {
149 workstationClusterId: created.cluster.workstationClusterId,
150 location: "us-central1",
151 network: "default",
152 subnetwork: "default",
153 displayName: "alchemy-test-cluster-v2",
154 labels: { env: "prod", role: "dev" },
155 });
156 const config =
157 yield* GCP.Workstations.WorkstationClustersWorkstationConfig(
158 "Code",
159 {
160 workstationConfigId: created.config.workstationConfigId,
161 workstationCluster: cluster.name,
162 displayName: "alchemy-test-config-v2",
163 idleTimeout: "600s",
164 labels: { env: "prod" },
165 },
166 );
167 const workstation =
168 yield* GCP.Workstations.WorkstationClustersWorkstationConfigsWorkstation(
169 "Mine",
170 {
171 workstationId: created.workstation.workstationId,
172 workstationConfig: config.name,
173 displayName: "alchemy-test-station-v2",
174 labels: { env: "prod" },
175 },
176 );
177 return { cluster, config, workstation };
178 }),
179 );
180
181 expect(updated.cluster.workstationClusterId).toEqual(
182 created.cluster.workstationClusterId,
183 );
184 expect(updated.cluster.displayName).toEqual("alchemy-test-cluster-v2");
185 expect(updated.cluster.labels).toMatchObject({
186 env: "prod",
187 role: "dev",
188 });
189 expect(updated.config.displayName).toEqual("alchemy-test-config-v2");
190 expect(updated.workstation.displayName).toEqual(
191 "alchemy-test-station-v2",
192 );
193
194 yield* stack.destroy();
195
196 const gone = yield* workstations
197 .getProjectsLocationsWorkstationClusters({
198 name: created.cluster.name,
199 })
200 .pipe(
201 Effect.as("found" as const),
202 Effect.catchTag("NotFound", () => Effect.succeed("gone" as const)),
203 );
204 expect(gone).toEqual("gone");
205 }).pipe(logLevel),
206 {
207 tags: ["provider:gcp", "provider:gcp:workstations", "live"],
208 timeout: 3_600_000,
209 },
210);