1import { GcpEnvironment } from "@/GCP/Environment";
2import * as GCP from "@/GCP";
3import * as Test from "@/Test/Alchemy";
4import * as alloydb from "@distilled.cloud/gcp/alloydb_v1";
5import { expect } from "alchemy-test";
6import * as Effect from "effect/Effect";
7import { MinimumLogLevel } from "effect/References";
8import * as Schedule from "effect/Schedule";
9
10const { test } = Test.make({ providers: GCP.providers() });
11
12const logLevel = Effect.provideService(
13 MinimumLogLevel,
14 process.env.DEBUG ? "Debug" : "Info",
15);
16
17// AlloyDB clusters and instances take well over 5 minutes to provision.
18const runLifecycle = !!process.env.GCP_TEST_SLOW && !process.env.FAST;
19
20const waitUntilGone = (name: string) =>
21 alloydb.getProjectsLocationsClustersInstances({ name }).pipe(
22 Effect.as("found" as const),
23 Effect.catchTag("NotFound", () => Effect.succeed("gone" as const)),
24 Effect.repeat({
25 schedule: Schedule.spaced("2 seconds"),
26 until: (status) => status === "gone",
27 times: 10,
28 }),
29 );
30
31test.provider(
32 "getProjectsLocationsClustersInstances on a missing instance fails with a typed tag",
33 (stack) =>
34 Effect.gen(function* () {
35 const { project } = yield* GcpEnvironment.current;
36
37 yield* stack.destroy();
38
39 const error = yield* Effect.flip(
40 alloydb.getProjectsLocationsClustersInstances({
41 name: `projects/${project}/locations/us-central1/clusters/alchemy-alloydb-missing/instances/alchemy-alloydb-missing`,
42 }),
43 );
44 expect(error._tag).toEqual("NotFound");
45
46 const page = yield* alloydb.listProjectsLocationsClustersInstances({
47 parent: `projects/${project}/locations/-/clusters/-`,
48 pageSize: 10,
49 });
50 expect((page.instances ?? []).map((item) => item.name)).not.toContain(
51 `projects/${project}/locations/us-central1/clusters/alchemy-alloydb-missing/instances/alchemy-alloydb-missing`,
52 );
53
54 yield* stack.destroy();
55 }).pipe(logLevel),
56 { tags: ["provider:gcp", "provider:gcp:alloydb", "live"], timeout: 90_000 },
57);
58
59test.provider.skipIf(!runLifecycle)(
60 "create, update, and delete an alloydb instance",
61 (stack) =>
62 Effect.gen(function* () {
63 yield* stack.destroy();
64
65 const created = yield* stack.deploy(
66 Effect.gen(function* () {
67 const cluster = yield* GCP.AlloyDB.Cluster("AppDb", {
68 location: "us-central1",
69 pscConfig: { pscEnabled: true },
70 displayName: "alchemy-test-instance-cluster",
71 labels: { env: "test" },
72 initialUser: { user: "postgres", password: "AlchemyTest1" },
73 automatedBackupPolicy: { enabled: false },
74 continuousBackupConfig: { enabled: false },
75 });
76 const instance = yield* GCP.AlloyDB.Instance("Primary", {
77 cluster: cluster.name,
78 instanceType: "PRIMARY",
79 displayName: "alchemy-test-primary",
80 labels: { env: "test" },
81 machineConfig: { cpuCount: 2 },
82 });
83 return { cluster, instance };
84 }),
85 );
86
87 expect(created.instance.name).toContain("/instances/");
88 expect(created.instance.clusterId).toEqual(created.cluster.clusterId);
89 expect(created.instance.location).toEqual("us-central1");
90 expect(created.instance.instanceType).toEqual("PRIMARY");
91 expect(created.instance.displayName).toEqual("alchemy-test-primary");
92 expect(created.instance.labels).toMatchObject({ env: "test" });
93 expect(created.instance.machineConfig?.cpuCount).toEqual(2);
94 expect(["READY", "STOPPED"]).toContain(created.instance.state);
95
96 const fetched = yield* alloydb.getProjectsLocationsClustersInstances({
97 name: created.instance.name,
98 });
99 expect(fetched.name).toEqual(created.instance.name);
100 expect(fetched.instanceType).toEqual("PRIMARY");
101 // GCP omits displayName from instance reads; the attributes carry it.
102 expect(fetched.labels?.env).toEqual("test");
103 expect(fetched.machineConfig?.cpuCount).toEqual(2);
104
105 const updated = yield* stack.deploy(
106 Effect.gen(function* () {
107 const cluster = yield* GCP.AlloyDB.Cluster("AppDb", {
108 clusterId: created.cluster.clusterId,
109 location: "us-central1",
110 pscConfig: { pscEnabled: true },
111 displayName: "alchemy-test-instance-cluster",
112 labels: { env: "test" },
113 automatedBackupPolicy: { enabled: false },
114 continuousBackupConfig: { enabled: false },
115 });
116 const instance = yield* GCP.AlloyDB.Instance("Primary", {
117 cluster: cluster.name,
118 instanceId: created.instance.instanceId,
119 instanceType: "PRIMARY",
120 displayName: "alchemy-prod-primary",
121 labels: { env: "prod", role: "primary" },
122 machineConfig: { cpuCount: 2 },
123 });
124 return { cluster, instance };
125 }),
126 );
127
128 expect(updated.instance.name).toEqual(created.instance.name);
129 expect(updated.instance.displayName).toEqual("alchemy-prod-primary");
130 expect(updated.instance.labels).toMatchObject({
131 env: "prod",
132 role: "primary",
133 });
134
135 const refetched = yield* alloydb.getProjectsLocationsClustersInstances({
136 name: created.instance.name,
137 });
138 // GCP omits displayName from instance reads; the attributes carry it.
139 expect(refetched.labels?.env).toEqual("prod");
140 expect(refetched.labels?.role).toEqual("primary");
141
142 yield* stack.destroy();
143
144 const gone = yield* waitUntilGone(created.instance.name);
145 expect(gone).toEqual("gone");
146 }).pipe(logLevel),
147 {
148 tags: ["provider:gcp", "provider:gcp:alloydb", "live"],
149 timeout: 3_600_000,
150 },
151);