1import * as GCP from "@/GCP";
2import { GcpEnvironment } from "@/GCP/Environment";
3import * as Test from "@/Test/Alchemy";
4import * as Core from "@/Test/Core";
5import * as resourcemanager from "@distilled.cloud/gcp/cloudresourcemanager_v3";
6import * as oracle from "@distilled.cloud/gcp/oracledatabase_v1";
7import { describe, expect } from "alchemy-test";
8import * as Effect from "effect/Effect";
9import * as Schedule from "effect/Schedule";
10import { dockerAvailable, expectProbe } from "../bindingHost.ts";
11import OracleBindingsHost, {
12 AppDb,
13 Assign,
14 BaseDb,
15 Client,
16 Exa,
17 ExaVm,
18 OracleNet,
19 Replicat,
20 Src,
21 Vault,
22 Vms,
23} from "./fixtures/bindings-host.ts";
24
25const testOptions = { providers: GCP.providers() };
26const { test, beforeAll, afterAll } = Test.make(testOptions);
27const sharedStack = Core.scratchStack(testOptions, "OracleBindings");
28
29// Oracle Database@Google Cloud is not enabled in the test project
30// (ServiceDisabled) and needs an Oracle Cloud subscription. Set
31// GCP_TEST_ORACLE=1 in a project that has one.
32const runLifecycle = !!process.env.GCP_TEST_ORACLE && !process.env.FAST;
33
34let baseUrl: string;
35let hostAccount: string;
36let names: Record<
37 | "network"
38 | "subnet"
39 | "database"
40 | "infra"
41 | "vmCluster"
42 | "dbSystem"
43 | "vault"
44 | "exadb"
45 | "connection"
46 | "deployment"
47 | "assignment",
48 string
49>;
50
51const ADB_ADMIN = "roles/oracledatabase.autonomousDatabaseAdmin";
52
53/** Every role the host's bindings grant (project-level, unconditional). */
54const expectedRoles = [
55 ADB_ADMIN,
56 "roles/oracledatabase.autonomousDatabaseViewer",
57 "roles/oracledatabase.cloudExadataInfrastructureViewer",
58 "roles/oracledatabase.cloudVmClusterViewer",
59 "roles/oracledatabase.dbSystemViewer",
60 "roles/oracledatabase.exadbVmClusterViewer",
61 "roles/oracledatabase.exascaleDbStorageVaultViewer",
62 "roles/oracledatabase.goldenGateConnectionAssignmentViewer",
63 "roles/oracledatabase.goldenGateConnectionViewer",
64 "roles/oracledatabase.goldenGateDeploymentViewer",
65 "roles/oracledatabase.odbNetworkViewer",
66 "roles/oracledatabase.odbSubnetViewer",
67].sort();
68
69/**
70 * Oracle Database@Google Cloud has no resource-level IAM: each binding
71 * grants its role on the project, unconditionally.
72 */
73const expectProjectGrant = (role: string) =>
74 Effect.gen(function* () {
75 const { project } = yield* GcpEnvironment.current;
76 const policy = yield* resourcemanager.getIamPolicyProjects({
77 resource: `projects/${project}`,
78 body: { options: { requestedPolicyVersion: 3 } },
79 });
80 const grants = (policy.bindings ?? []).filter((binding) =>
81 (binding.members ?? []).includes(`serviceAccount:${hostAccount}`),
82 );
83 expect(grants.every((binding) => binding.condition === undefined)).toBe(
84 true,
85 );
86 const roles = grants.map((binding) => binding.role ?? "").sort();
87 expect(roles).toContain(role);
88 expect(roles).toEqual(expectedRoles);
89 });
90
91/** Poll the autonomous database until it reaches `state` (bounded). */
92const waitForDatabaseState = (state: string) =>
93 oracle.getProjectsLocationsAutonomousDatabases({ name: names.database }).pipe(
94 Effect.map((db) => db.properties?.state),
95 Effect.repeat({
96 schedule: Schedule.spaced("10 seconds"),
97 until: (current) => current === state,
98 times: 54,
99 }),
100 );
101
102/** A read-only `Get*` binding: probe, compare with a direct read. */
103const describeGet = <A extends { name?: string }>(options: {
104 binding: string;
105 route: string;
106 role: string;
107 name: () => string;
108 direct: (name: string) => Effect.Effect<A, { readonly _tag: string }, any>;
109}) =>
110 describe(options.binding, () => {
111 test.provider(
112 "reads the resource as the host's service account",
113 (_stack) =>
114 Effect.gen(function* () {
115 const out = yield* expectProbe<A>(baseUrl, options.route);
116 const direct = yield* options.direct(options.name());
117 expect(out.name).toEqual(options.name());
118 expect(out.name).toEqual(direct.name);
119 yield* expectProjectGrant(options.role);
120 }),
121 {
122 tags: ["provider:gcp", "provider:gcp:oracledatabase", "live"],
123 timeout: 600_000,
124 },
125 );
126 });
127
128describe.skipIf(!dockerAvailable || !runLifecycle)(
129 "OracleDatabase Bindings",
130 {
131 tags: [
132 "provider:gcp",
133 "provider:gcp:oracledatabase",
134 "provider:gcp:run",
135 "live",
136 ],
137 },
138 () => {
139 beforeAll(
140 Effect.gen(function* () {
141 yield* sharedStack.destroy();
142 const out = yield* sharedStack.deploy(
143 Effect.gen(function* () {
144 const host = yield* OracleBindingsHost;
145 return {
146 uri: host.uri,
147 serviceAccount: host.serviceAccount,
148 network: (yield* OracleNet).name,
149 subnet: (yield* Client).name,
150 database: (yield* AppDb).name,
151 infra: (yield* Exa).name,
152 vmCluster: (yield* Vms).name,
153 dbSystem: (yield* BaseDb).name,
154 vault: (yield* Vault).name,
155 exadb: (yield* ExaVm).name,
156 connection: (yield* Src).name,
157 deployment: (yield* Replicat).name,
158 assignment: (yield* Assign).name,
159 };
160 }),
161 );
162 const { uri, serviceAccount, ...rest } = out;
163 baseUrl = uri!;
164 hostAccount = serviceAccount!;
165 names = rest;
166 }),
167 { timeout: 3_600_000 },
168 );
169
170 afterAll(sharedStack.destroy(), { timeout: 3_600_000 });
171
172 describeGet({
173 binding: "GetOdbNetwork",
174 route: "getOdbNetwork",
175 role: "roles/oracledatabase.odbNetworkViewer",
176 name: () => names.network,
177 direct: (name) => oracle.getProjectsLocationsOdbNetworks({ name }),
178 });
179 describeGet({
180 binding: "GetOdbNetworksOdbSubnet",
181 route: "getOdbNetworksOdbSubnet",
182 role: "roles/oracledatabase.odbSubnetViewer",
183 name: () => names.subnet,
184 direct: (name) =>
185 oracle.getProjectsLocationsOdbNetworksOdbSubnets({ name }),
186 });
187 describeGet({
188 binding: "GetAutonomousDatabase",
189 route: "getAutonomousDatabase",
190 role: "roles/oracledatabase.autonomousDatabaseViewer",
191 name: () => names.database,
192 direct: (name) =>
193 oracle.getProjectsLocationsAutonomousDatabases({ name }),
194 });
195 describeGet({
196 binding: "GetCloudExadataInfrastructure",
197 route: "getCloudExadataInfrastructure",
198 role: "roles/oracledatabase.cloudExadataInfrastructureViewer",
199 name: () => names.infra,
200 direct: (name) =>
201 oracle.getProjectsLocationsCloudExadataInfrastructures({ name }),
202 });
203 describeGet({
204 binding: "GetCloudVmCluster",
205 route: "getCloudVmCluster",
206 role: "roles/oracledatabase.cloudVmClusterViewer",
207 name: () => names.vmCluster,
208 direct: (name) => oracle.getProjectsLocationsCloudVmClusters({ name }),
209 });
210 describeGet({
211 binding: "GetDbSystem",
212 route: "getDbSystem",
213 role: "roles/oracledatabase.dbSystemViewer",
214 name: () => names.dbSystem,
215 direct: (name) => oracle.getProjectsLocationsDbSystems({ name }),
216 });
217 describeGet({
218 binding: "GetExascaleDbStorageVault",
219 route: "getExascaleDbStorageVault",
220 role: "roles/oracledatabase.exascaleDbStorageVaultViewer",
221 name: () => names.vault,
222 direct: (name) =>
223 oracle.getProjectsLocationsExascaleDbStorageVaults({ name }),
224 });
225 describeGet({
226 binding: "GetExadbVmCluster",
227 route: "getExadbVmCluster",
228 role: "roles/oracledatabase.exadbVmClusterViewer",
229 name: () => names.exadb,
230 direct: (name) => oracle.getProjectsLocationsExadbVmClusters({ name }),
231 });
232 describeGet({
233 binding: "GetGoldengateConnection",
234 route: "getGoldengateConnection",
235 role: "roles/oracledatabase.goldenGateConnectionViewer",
236 name: () => names.connection,
237 direct: (name) =>
238 oracle.getProjectsLocationsGoldengateConnections({ name }),
239 });
240 describeGet({
241 binding: "GetGoldengateDeployment",
242 route: "getGoldengateDeployment",
243 role: "roles/oracledatabase.goldenGateDeploymentViewer",
244 name: () => names.deployment,
245 direct: (name) =>
246 oracle.getProjectsLocationsGoldengateDeployments({ name }),
247 });
248 describeGet({
249 binding: "GetGoldengateConnectionAssignment",
250 route: "getGoldengateConnectionAssignment",
251 role: "roles/oracledatabase.goldenGateConnectionAssignmentViewer",
252 name: () => names.assignment,
253 direct: (name) =>
254 oracle.getProjectsLocationsGoldengateConnectionAssignments({ name }),
255 });
256
257 // Wallet → Stop → Start → Restart act on one database: run in order.
258 describe.sequential("AutonomousDatabase actions", () => {
259 describe("GenerateWallet", () => {
260 test.provider(
261 "generates the database wallet as the host's service account",
262 (_stack) =>
263 Effect.gen(function* () {
264 const out =
265 yield* expectProbe<oracle.GenerateAutonomousDatabaseWalletResponse>(
266 baseUrl,
267 "generateWallet",
268 );
269 // The wallet is a base64 zip archive ("PK" magic).
270 const archive = Buffer.from(out.archiveContent ?? "", "base64");
271 expect(archive.subarray(0, 2).toString("latin1")).toEqual("PK");
272 yield* expectProjectGrant(ADB_ADMIN);
273 }),
274 {
275 tags: ["provider:gcp", "provider:gcp:oracledatabase", "live"],
276 timeout: 600_000,
277 },
278 );
279 });
280
281 describe("StopAutonomousDatabase", () => {
282 test.provider(
283 "stops the database as the host's service account",
284 (_stack) =>
285 Effect.gen(function* () {
286 yield* expectProbe(baseUrl, "stopAutonomousDatabase");
287 expect(yield* waitForDatabaseState("STOPPED")).toEqual("STOPPED");
288 yield* expectProjectGrant(ADB_ADMIN);
289 }),
290 {
291 tags: ["provider:gcp", "provider:gcp:oracledatabase", "live"],
292 timeout: 900_000,
293 },
294 );
295 });
296
297 describe("StartAutonomousDatabase", () => {
298 test.provider(
299 "starts the database as the host's service account",
300 (_stack) =>
301 Effect.gen(function* () {
302 yield* expectProbe(baseUrl, "startAutonomousDatabase");
303 expect(yield* waitForDatabaseState("AVAILABLE")).toEqual(
304 "AVAILABLE",
305 );
306 yield* expectProjectGrant(ADB_ADMIN);
307 }),
308 {
309 tags: ["provider:gcp", "provider:gcp:oracledatabase", "live"],
310 timeout: 900_000,
311 },
312 );
313 });
314
315 describe("RestartAutonomousDatabase", () => {
316 test.provider(
317 "restarts the database as the host's service account",
318 (_stack) =>
319 Effect.gen(function* () {
320 yield* expectProbe(baseUrl, "restartAutonomousDatabase");
321 expect(yield* waitForDatabaseState("AVAILABLE")).toEqual(
322 "AVAILABLE",
323 );
324 yield* expectProjectGrant(ADB_ADMIN);
325 }),
326 {
327 tags: ["provider:gcp", "provider:gcp:oracledatabase", "live"],
328 timeout: 900_000,
329 },
330 );
331 });
332 });
333 },
334);