1import * as logging from "@distilled.cloud/gcp/logging_v2";
2import * as Effect from "effect/Effect";
3import * as Stream from "effect/Stream";
4import { GcpEnvironment } from "../Environment.ts";
5import {
6 waitForOperation as waitForLongRunning,
7 type LongRunningOperation,
8} from "../Operation.ts";
9
10const getOperation = (name: string) =>
11 name.includes("/billingAccounts/") || name.startsWith("billingAccounts/")
12 ? logging.getBillingAccountsLocationsOperations({ name })
13 : name.includes("/folders/") || name.startsWith("folders/")
14 ? logging.getFoldersLocationsOperations({ name })
15 : name.includes("/organizations/") || name.startsWith("organizations/")
16 ? logging.getOrganizationsLocationsOperations({ name })
17 : name.includes("/projects/") || name.startsWith("projects/")
18 ? logging.getProjectsLocationsOperations({ name })
19 : logging.getLocationsOperations({ name });
20
21export const deleteBucketLinks = (bucketName: string) =>
22 logging.listLocationsBucketsLinks
23 .pages({ parent: bucketName, pageSize: 100 })
24 .pipe(
25 Stream.flatMap((page) => Stream.fromIterable(page.links ?? [])),
26 Stream.runCollect,
27 Effect.flatMap((links) =>
28 Effect.forEach(
29 links,
30 (link) => {
31 const name = link.name;
32 if (name === undefined) return Effect.void;
33 return logging.deleteLocationsBucketsLinks({ name }).pipe(
34 Effect.catchTag(["NotFound", "Conflict"], () => Effect.void),
35 Effect.asVoid,
36 );
37 },
38 { concurrency: 4 },
39 ),
40 ),
41 Effect.catchTag("NotFound", () => Effect.void),
42 Effect.asVoid,
43 );
44
45export const listProjectBuckets = () =>
46 Effect.gen(function* () {
47 const env = yield* GcpEnvironment.current;
48 return yield* logging.listProjectsLocationsBuckets
49 .pages({
50 parent: `projects/${env.project}/locations/-`,
51 pageSize: 1000,
52 })
53 .pipe(
54 Stream.flatMap((page) => Stream.fromIterable(page.buckets ?? [])),
55 Stream.filter((bucket) => (bucket.name ?? "").length > 0),
56 Stream.runCollect,
57 Effect.map((chunk) => Array.from(chunk)),
58 );
59 });
60
61/** Linked-dataset creation and bucket analytics upgrades take several minutes. */
62const OPERATION_BUDGET = "15 minutes";
63
64/**
65 * Wait for a long-running operation. ALREADY_EXISTS (code 6) means a
66 * concurrent create won the race; reconcile observes the resource next.
67 */
68export const waitForOperation = (operation: LongRunningOperation) =>
69 waitForLongRunning(operation, getOperation, {
70 budget: OPERATION_BUDGET,
71 }).pipe(
72 Effect.catchIf(
73 (error) => error._tag === "GCP.OperationFailed" && error.code === 6,
74 () => Effect.succeed(operation),
75 ),
76 );
77
78/**
79 * Wait for a delete operation. A vanished operation or NOT_FOUND (code 5)
80 * means the resource is already gone.
81 */
82export const waitForDeleteOperation = (operation: LongRunningOperation) =>
83 waitForOperation(operation).pipe(
84 Effect.catchIf(
85 (error) =>
86 error._tag === "NotFound" ||
87 (error._tag === "GCP.OperationFailed" && error.code === 5),
88 () => Effect.succeed(operation),
89 ),
90 );