@microagi/alchemy-gcp 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -0
- package/README.md +88 -0
- package/lib/Auth/AuthProvider.d.ts +48 -0
- package/lib/Auth/AuthProvider.d.ts.map +1 -0
- package/lib/Auth/AuthProvider.js +84 -0
- package/lib/Auth/AuthProvider.js.map +1 -0
- package/lib/Auth/Credentials.d.ts +28 -0
- package/lib/Auth/Credentials.d.ts.map +1 -0
- package/lib/Auth/Credentials.js +41 -0
- package/lib/Auth/Credentials.js.map +1 -0
- package/lib/CloudResourceManager/Project.d.ts +143 -0
- package/lib/CloudResourceManager/Project.d.ts.map +1 -0
- package/lib/CloudResourceManager/Project.js +272 -0
- package/lib/CloudResourceManager/Project.js.map +1 -0
- package/lib/CloudResourceManager/index.d.ts +3 -0
- package/lib/CloudResourceManager/index.d.ts.map +1 -0
- package/lib/CloudResourceManager/index.js +2 -0
- package/lib/CloudResourceManager/index.js.map +1 -0
- package/lib/Compute/GlobalAddress.d.ts +101 -0
- package/lib/Compute/GlobalAddress.d.ts.map +1 -0
- package/lib/Compute/GlobalAddress.js +134 -0
- package/lib/Compute/GlobalAddress.js.map +1 -0
- package/lib/Compute/Network.d.ts +114 -0
- package/lib/Compute/Network.d.ts.map +1 -0
- package/lib/Compute/Network.js +145 -0
- package/lib/Compute/Network.js.map +1 -0
- package/lib/Compute/Operations.d.ts +34 -0
- package/lib/Compute/Operations.d.ts.map +1 -0
- package/lib/Compute/Operations.js +62 -0
- package/lib/Compute/Operations.js.map +1 -0
- package/lib/Compute/SharedVpcHost.d.ts +44 -0
- package/lib/Compute/SharedVpcHost.d.ts.map +1 -0
- package/lib/Compute/SharedVpcHost.js +71 -0
- package/lib/Compute/SharedVpcHost.js.map +1 -0
- package/lib/Compute/SharedVpcServiceProject.d.ts +46 -0
- package/lib/Compute/SharedVpcServiceProject.d.ts.map +1 -0
- package/lib/Compute/SharedVpcServiceProject.js +96 -0
- package/lib/Compute/SharedVpcServiceProject.js.map +1 -0
- package/lib/Compute/Subnetwork.d.ts +108 -0
- package/lib/Compute/Subnetwork.d.ts.map +1 -0
- package/lib/Compute/Subnetwork.js +201 -0
- package/lib/Compute/Subnetwork.js.map +1 -0
- package/lib/Compute/index.d.ts +6 -0
- package/lib/Compute/index.d.ts.map +1 -0
- package/lib/Compute/index.js +6 -0
- package/lib/Compute/index.js.map +1 -0
- package/lib/Container/Cluster.d.ts +216 -0
- package/lib/Container/Cluster.d.ts.map +1 -0
- package/lib/Container/Cluster.js +326 -0
- package/lib/Container/Cluster.js.map +1 -0
- package/lib/Container/NodePool.d.ts +214 -0
- package/lib/Container/NodePool.d.ts.map +1 -0
- package/lib/Container/NodePool.js +344 -0
- package/lib/Container/NodePool.js.map +1 -0
- package/lib/Container/Operations.d.ts +50 -0
- package/lib/Container/Operations.d.ts.map +1 -0
- package/lib/Container/Operations.js +51 -0
- package/lib/Container/Operations.js.map +1 -0
- package/lib/Container/index.d.ts +5 -0
- package/lib/Container/index.d.ts.map +1 -0
- package/lib/Container/index.js +3 -0
- package/lib/Container/index.js.map +1 -0
- package/lib/Parallelstore/Instance.d.ts +119 -0
- package/lib/Parallelstore/Instance.d.ts.map +1 -0
- package/lib/Parallelstore/Instance.js +180 -0
- package/lib/Parallelstore/Instance.js.map +1 -0
- package/lib/Parallelstore/index.d.ts +2 -0
- package/lib/Parallelstore/index.d.ts.map +1 -0
- package/lib/Parallelstore/index.js +2 -0
- package/lib/Parallelstore/index.js.map +1 -0
- package/lib/Providers.d.ts +24 -0
- package/lib/Providers.d.ts.map +1 -0
- package/lib/Providers.js +51 -0
- package/lib/Providers.js.map +1 -0
- package/lib/ServiceNetworking/PsaConnection.d.ts +84 -0
- package/lib/ServiceNetworking/PsaConnection.d.ts.map +1 -0
- package/lib/ServiceNetworking/PsaConnection.js +130 -0
- package/lib/ServiceNetworking/PsaConnection.js.map +1 -0
- package/lib/ServiceNetworking/index.d.ts +2 -0
- package/lib/ServiceNetworking/index.d.ts.map +1 -0
- package/lib/ServiceNetworking/index.js +2 -0
- package/lib/ServiceNetworking/index.js.map +1 -0
- package/lib/ServiceUsage/ApiEnable.d.ts +78 -0
- package/lib/ServiceUsage/ApiEnable.d.ts.map +1 -0
- package/lib/ServiceUsage/ApiEnable.js +132 -0
- package/lib/ServiceUsage/ApiEnable.js.map +1 -0
- package/lib/ServiceUsage/index.d.ts +3 -0
- package/lib/ServiceUsage/index.d.ts.map +1 -0
- package/lib/ServiceUsage/index.js +2 -0
- package/lib/ServiceUsage/index.js.map +1 -0
- package/lib/Tags.d.ts +57 -0
- package/lib/Tags.d.ts.map +1 -0
- package/lib/Tags.js +106 -0
- package/lib/Tags.js.map +1 -0
- package/lib/index.d.ts +11 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +10 -0
- package/lib/index.js.map +1 -0
- package/package.json +70 -0
- package/src/Auth/AuthProvider.ts +180 -0
- package/src/Auth/Credentials.ts +69 -0
- package/src/CloudResourceManager/Project.ts +517 -0
- package/src/CloudResourceManager/index.ts +2 -0
- package/src/Compute/GlobalAddress.ts +283 -0
- package/src/Compute/Network.ts +306 -0
- package/src/Compute/Operations.ts +108 -0
- package/src/Compute/SharedVpcHost.ts +141 -0
- package/src/Compute/SharedVpcServiceProject.ts +185 -0
- package/src/Compute/Subnetwork.ts +375 -0
- package/src/Compute/index.ts +5 -0
- package/src/Container/Cluster.ts +599 -0
- package/src/Container/NodePool.ts +620 -0
- package/src/Container/Operations.ts +88 -0
- package/src/Container/index.ts +8 -0
- package/src/Parallelstore/Instance.ts +371 -0
- package/src/Parallelstore/index.ts +1 -0
- package/src/Providers.ts +84 -0
- package/src/ServiceNetworking/PsaConnection.ts +269 -0
- package/src/ServiceNetworking/index.ts +1 -0
- package/src/ServiceUsage/ApiEnable.ts +267 -0
- package/src/ServiceUsage/index.ts +2 -0
- package/src/Tags.ts +130 -0
- package/src/index.ts +16 -0
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { ConfigError } from "@distilled.cloud/gcp";
|
|
2
|
+
import * as cont from "@distilled.cloud/gcp/container-v1";
|
|
3
|
+
import type { ScopedPlanStatusSession } from "alchemy/Cli/Cli";
|
|
4
|
+
import * as Duration from "effect/Duration";
|
|
5
|
+
import * as Effect from "effect/Effect";
|
|
6
|
+
import * as Schedule from "effect/Schedule";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Qualify a GKE operation name. The Container API returns
|
|
10
|
+
* `Operation.name` as a bare ID like `operation-1778057673333-...`,
|
|
11
|
+
* but `getProjectsLocationsOperations` expects the fully-qualified
|
|
12
|
+
* path `projects/{p}/locations/{l}/operations/{id}`. Already-qualified
|
|
13
|
+
* names are passed through unchanged so callers can stay agnostic.
|
|
14
|
+
*
|
|
15
|
+
* (CRM operations don't need this because CRM returns names as
|
|
16
|
+
* `operations/...` and its `getOperations` takes the bare path
|
|
17
|
+
* directly.)
|
|
18
|
+
*/
|
|
19
|
+
export const qualifyOperationName = (
|
|
20
|
+
project: string,
|
|
21
|
+
location: string,
|
|
22
|
+
name: string,
|
|
23
|
+
): string =>
|
|
24
|
+
name.startsWith("projects/")
|
|
25
|
+
? name
|
|
26
|
+
: `projects/${project}/locations/${location}/operations/${name}`;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Resolved callable signature of `cont.getProjectsLocationsOperations`.
|
|
30
|
+
*
|
|
31
|
+
* `cont.getProjectsLocationsOperations` is itself an
|
|
32
|
+
* `Effect<callable, never, Credentials | HttpClient>`, so `Effect.Success`
|
|
33
|
+
* extracts the callable type — i.e. what you get back from
|
|
34
|
+
* `yield* cont.getProjectsLocationsOperations`. Using a derived alias
|
|
35
|
+
* keeps the helper aligned with the SDK without us hand-writing the
|
|
36
|
+
* input/output/error union, which would drift if the patch set changes.
|
|
37
|
+
*/
|
|
38
|
+
type GetOperations = Effect.Success<typeof cont.getProjectsLocationsOperations>;
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Build the GKE long-running-operation polling helper.
|
|
42
|
+
*
|
|
43
|
+
* Container `Operation`s carry `.status: "PENDING" | "RUNNING" | "DONE"
|
|
44
|
+
* | "ABORTING"` (NOT `.done: boolean` like Cloud Resource Manager —
|
|
45
|
+
* verified at `vendor/distilled/packages/gcp/src/services/container-v1.ts:919–976`).
|
|
46
|
+
* The doneness predicate compares `.status === "DONE"` accordingly.
|
|
47
|
+
*
|
|
48
|
+
* Schedule: exponential 2s → 1.5× growth, capped per-poll at 15s via
|
|
49
|
+
* `Schedule.either`, then a hard count cap of 80 retries via
|
|
50
|
+
* `Schedule.both(Schedule.recurs(80))` → ~20 min wall ceiling, well
|
|
51
|
+
* above the worst observed GKE op time (cluster create runs 5–10 min).
|
|
52
|
+
*
|
|
53
|
+
* The helper is a factory — the caller resolves
|
|
54
|
+
* `cont.getProjectsLocationsOperations` once at provider construction
|
|
55
|
+
* (see `Project.ts` for the same idiom) and passes the resulting
|
|
56
|
+
* callable here, so we never re-resolve services on each await.
|
|
57
|
+
*/
|
|
58
|
+
export const makeAwaitOperation = (getOperations: GetOperations) =>
|
|
59
|
+
Effect.fn(function* (
|
|
60
|
+
operationName: string,
|
|
61
|
+
session: ScopedPlanStatusSession,
|
|
62
|
+
) {
|
|
63
|
+
const op = yield* getOperations({ name: operationName }).pipe(
|
|
64
|
+
Effect.flatMap((current) =>
|
|
65
|
+
current.status === "DONE"
|
|
66
|
+
? Effect.succeed(current)
|
|
67
|
+
: Effect.fail({ _tag: "OperationPending" as const }),
|
|
68
|
+
),
|
|
69
|
+
Effect.retry({
|
|
70
|
+
while: (e: { _tag?: string }) => e?._tag === "OperationPending",
|
|
71
|
+
schedule: Schedule.exponential(Duration.seconds(2), 1.5).pipe(
|
|
72
|
+
Schedule.either(Schedule.spaced(Duration.seconds(15))),
|
|
73
|
+
Schedule.both(Schedule.recurs(80)),
|
|
74
|
+
Schedule.tapOutput(() =>
|
|
75
|
+
session.note(`Waiting for GKE operation ${operationName}…`),
|
|
76
|
+
),
|
|
77
|
+
),
|
|
78
|
+
}),
|
|
79
|
+
);
|
|
80
|
+
if (op.error) {
|
|
81
|
+
return yield* new ConfigError({
|
|
82
|
+
message: `GKE operation ${operationName} failed: ${
|
|
83
|
+
op.error.message ?? JSON.stringify(op.error)
|
|
84
|
+
}`,
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
return op;
|
|
88
|
+
});
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export { Cluster, ClusterProvider } from "./Cluster.ts";
|
|
2
|
+
export type {
|
|
3
|
+
ClusterAttributes,
|
|
4
|
+
ClusterInitialNodePool,
|
|
5
|
+
ClusterProps,
|
|
6
|
+
} from "./Cluster.ts";
|
|
7
|
+
export { NodePool, NodePoolProvider } from "./NodePool.ts";
|
|
8
|
+
export type { NodePoolAttributes, NodePoolProps } from "./NodePool.ts";
|
|
@@ -0,0 +1,371 @@
|
|
|
1
|
+
import { ConfigError } from "@distilled.cloud/gcp";
|
|
2
|
+
import * as ps from "@distilled.cloud/gcp/parallelstore-v1";
|
|
3
|
+
import { Resource } from "alchemy";
|
|
4
|
+
import { Unowned } from "alchemy/AdoptPolicy";
|
|
5
|
+
import type { ScopedPlanStatusSession } from "alchemy/Cli/Cli";
|
|
6
|
+
import { deepEqual, isResolved, somePropsAreDifferent } from "alchemy/Diff";
|
|
7
|
+
import { createPhysicalName } from "alchemy/PhysicalName";
|
|
8
|
+
import * as Provider from "alchemy/Provider";
|
|
9
|
+
import { diffTags } from "alchemy/Tags";
|
|
10
|
+
import * as Duration from "effect/Duration";
|
|
11
|
+
import * as Effect from "effect/Effect";
|
|
12
|
+
import * as Schedule from "effect/Schedule";
|
|
13
|
+
import type { NetworkRef } from "../Compute/Network.ts";
|
|
14
|
+
import { gcpInternalLabels, hasAlchemyLabels } from "../Tags.ts";
|
|
15
|
+
import type * as GCP from "../Providers.ts";
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* A Parallelstore instance — a high-throughput, POSIX-style filesystem
|
|
19
|
+
* Google sells for AI training/inference shared storage. Backed by DAOS
|
|
20
|
+
* under the hood; mounted in clusters via the Parallelstore CSI driver.
|
|
21
|
+
*
|
|
22
|
+
* **Network preconditions.** The instance attaches to a VPC over PSA
|
|
23
|
+
* (Private Services Access). The consumer VPC must already have:
|
|
24
|
+
* 1. A `GCP.GlobalAddress` reservation (`purpose=VPC_PEERING`,
|
|
25
|
+
* `addressType=INTERNAL`, ≥/24 prefix length).
|
|
26
|
+
* 2. A `GCP.PsaConnection` to `servicenetworking.googleapis.com`
|
|
27
|
+
* consuming that range.
|
|
28
|
+
* See the Shared VPC + Parallelstore wiring in the apps/cluster stack.
|
|
29
|
+
*
|
|
30
|
+
* **Capacity.** `capacityGib` must be a multiple of 12000 GiB and
|
|
31
|
+
* between 12000 (12 TiB) and 100800 (98.4 TiB). Smaller deployments
|
|
32
|
+
* may use 100 GiB increments depending on the SKU; the API surfaces
|
|
33
|
+
* the floor at create time.
|
|
34
|
+
*
|
|
35
|
+
* Parallelstore operations use a CRM-style `Operation.done: boolean`
|
|
36
|
+
* (verified at `parallelstore-v1.ts`).
|
|
37
|
+
*
|
|
38
|
+
* @section Creating a Parallelstore instance
|
|
39
|
+
* @example 20 TiB scratch instance
|
|
40
|
+
* ```typescript
|
|
41
|
+
* const fs = yield* GCP.ParallelstoreInstance("ModelsFs", {
|
|
42
|
+
* project: hostProject.projectId,
|
|
43
|
+
* location: "us-central1-a",
|
|
44
|
+
* capacityGib: "20480",
|
|
45
|
+
* network: GCP.networkRef(hostProject.projectNumber, vpc.name),
|
|
46
|
+
* reservedIpRange: psaRange.name,
|
|
47
|
+
* deploymentType: "SCRATCH",
|
|
48
|
+
* fileStripeLevel: "FILE_STRIPE_LEVEL_BALANCED",
|
|
49
|
+
* directoryStripeLevel: "DIRECTORY_STRIPE_LEVEL_BALANCED",
|
|
50
|
+
* });
|
|
51
|
+
* ```
|
|
52
|
+
*/
|
|
53
|
+
export type ParallelstoreInstanceProps = {
|
|
54
|
+
/** GCP project ID hosting the instance. Immutable — replace if changed. */
|
|
55
|
+
project: string;
|
|
56
|
+
/**
|
|
57
|
+
* Parallelstore location. **Zone, not region** — Parallelstore is a
|
|
58
|
+
* zonal product. Common: `us-central1-a`. Immutable — replace.
|
|
59
|
+
*/
|
|
60
|
+
location: string;
|
|
61
|
+
/**
|
|
62
|
+
* Instance ID. Defaults to `createPhysicalName({ id, lowercase: true,
|
|
63
|
+
* maxLength: 63 })`. Lowercase letters/numbers/hyphens, must start
|
|
64
|
+
* with a letter, end with a letter or number. Immutable — replace.
|
|
65
|
+
*/
|
|
66
|
+
instanceId?: string;
|
|
67
|
+
/** User-visible description. Mutable via `patch`. */
|
|
68
|
+
description?: string;
|
|
69
|
+
/**
|
|
70
|
+
* Capacity in **GiB** (1024-based). Sent to the API as a string per
|
|
71
|
+
* Parallelstore's int64-encoded-as-string convention. Replace-only —
|
|
72
|
+
* Parallelstore doesn't currently support online resize.
|
|
73
|
+
*/
|
|
74
|
+
capacityGib: string;
|
|
75
|
+
/**
|
|
76
|
+
* Fully-qualified consumer VPC in project-number form. Build with
|
|
77
|
+
* `networkRef(project.projectNumber, vpc.name)` — the `NetworkRef`
|
|
78
|
+
* template literal type rejects project-ID-form strings at compile
|
|
79
|
+
* time. Immutable — replace.
|
|
80
|
+
*/
|
|
81
|
+
network: NetworkRef;
|
|
82
|
+
/**
|
|
83
|
+
* Name (or self-link) of a `GCP.GlobalAddress` PSA reservation in
|
|
84
|
+
* the same VPC. Immutable — replace.
|
|
85
|
+
*/
|
|
86
|
+
reservedIpRange?: string;
|
|
87
|
+
/** `SCRATCH` (default) or `PERSISTENT`. Immutable — replace. */
|
|
88
|
+
deploymentType?: "SCRATCH" | "PERSISTENT";
|
|
89
|
+
/** File stripe level. Replace-only. */
|
|
90
|
+
fileStripeLevel?:
|
|
91
|
+
| "FILE_STRIPE_LEVEL_MIN"
|
|
92
|
+
| "FILE_STRIPE_LEVEL_BALANCED"
|
|
93
|
+
| "FILE_STRIPE_LEVEL_MAX";
|
|
94
|
+
/** Directory stripe level. Replace-only. */
|
|
95
|
+
directoryStripeLevel?:
|
|
96
|
+
| "DIRECTORY_STRIPE_LEVEL_MIN"
|
|
97
|
+
| "DIRECTORY_STRIPE_LEVEL_BALANCED"
|
|
98
|
+
| "DIRECTORY_STRIPE_LEVEL_MAX";
|
|
99
|
+
/**
|
|
100
|
+
* Resource labels. Alchemy internal labels (`alchemy_app`,
|
|
101
|
+
* `alchemy_stage`, `alchemy_id`) are merged on top automatically.
|
|
102
|
+
* Mutable via `patch`.
|
|
103
|
+
*/
|
|
104
|
+
labels?: Record<string, string>;
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
export type ParallelstoreInstanceAttributes = {
|
|
108
|
+
/** Bare instance id. */
|
|
109
|
+
instanceId: string;
|
|
110
|
+
/** Project ID. */
|
|
111
|
+
project: string;
|
|
112
|
+
/** Zone. */
|
|
113
|
+
location: string;
|
|
114
|
+
/** Resource name `projects/{p}/locations/{l}/instances/{id}`. */
|
|
115
|
+
name: string;
|
|
116
|
+
/** Description, mirroring props. */
|
|
117
|
+
description: string | undefined;
|
|
118
|
+
/** Capacity in GiB. */
|
|
119
|
+
capacityGib: string;
|
|
120
|
+
/** Consumer VPC URL (project-number form). */
|
|
121
|
+
network: NetworkRef;
|
|
122
|
+
/** Reserved IP range used (server-resolved). */
|
|
123
|
+
reservedIpRange: string | undefined;
|
|
124
|
+
/** Effective reserved range (post-allocation). */
|
|
125
|
+
effectiveReservedIpRange: string | undefined;
|
|
126
|
+
/** Lifecycle state — `ACTIVE` once ready. */
|
|
127
|
+
state: string | undefined;
|
|
128
|
+
/** Active deployment type. */
|
|
129
|
+
deploymentType: string | undefined;
|
|
130
|
+
/** DNS access points VMs mount via. */
|
|
131
|
+
accessPoints: ReadonlyArray<string>;
|
|
132
|
+
/** Labels currently set, including internals. */
|
|
133
|
+
labels: Record<string, string>;
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
export type ParallelstoreInstance = Resource<
|
|
137
|
+
"GCP.ParallelstoreInstance",
|
|
138
|
+
ParallelstoreInstanceProps,
|
|
139
|
+
ParallelstoreInstanceAttributes,
|
|
140
|
+
never,
|
|
141
|
+
GCP.Providers
|
|
142
|
+
>;
|
|
143
|
+
export const ParallelstoreInstance = Resource<ParallelstoreInstance>(
|
|
144
|
+
"GCP.ParallelstoreInstance",
|
|
145
|
+
);
|
|
146
|
+
|
|
147
|
+
const toAttributes = (
|
|
148
|
+
i: ps.Instance,
|
|
149
|
+
parent: {
|
|
150
|
+
project: string;
|
|
151
|
+
location: string;
|
|
152
|
+
instanceId: string;
|
|
153
|
+
network: NetworkRef;
|
|
154
|
+
},
|
|
155
|
+
): ParallelstoreInstanceAttributes => ({
|
|
156
|
+
instanceId: parent.instanceId,
|
|
157
|
+
project: parent.project,
|
|
158
|
+
location: parent.location,
|
|
159
|
+
name: i.name ?? `projects/${parent.project}/locations/${parent.location}/instances/${parent.instanceId}`,
|
|
160
|
+
description: i.description,
|
|
161
|
+
capacityGib: i.capacityGib ?? "",
|
|
162
|
+
// The server-echoed `i.network` matches what we sent up at create
|
|
163
|
+
// time, but we thread `parent.network` through to preserve the
|
|
164
|
+
// `NetworkRef` type without a cast — same pattern as project /
|
|
165
|
+
// location, both of which would otherwise lose their typing through
|
|
166
|
+
// the SDK's `string | undefined` fields.
|
|
167
|
+
network: parent.network,
|
|
168
|
+
reservedIpRange: i.reservedIpRange,
|
|
169
|
+
effectiveReservedIpRange: i.effectiveReservedIpRange,
|
|
170
|
+
state: i.state,
|
|
171
|
+
deploymentType: i.deploymentType,
|
|
172
|
+
accessPoints: i.accessPoints ?? [],
|
|
173
|
+
labels: { ...(i.labels ?? {}) },
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
export const ParallelstoreInstanceProvider = () =>
|
|
177
|
+
Provider.effect(
|
|
178
|
+
ParallelstoreInstance,
|
|
179
|
+
Effect.gen(function* () {
|
|
180
|
+
const getInstance = yield* ps.getProjectsLocationsInstances;
|
|
181
|
+
const createInstance = yield* ps.createProjectsLocationsInstances;
|
|
182
|
+
const patchInstance = yield* ps.patchProjectsLocationsInstances;
|
|
183
|
+
const deleteInstance = yield* ps.deleteProjectsLocationsInstances;
|
|
184
|
+
const getOperation = yield* ps.getProjectsLocationsOperations;
|
|
185
|
+
|
|
186
|
+
const awaitOperation = Effect.fn(function* (
|
|
187
|
+
operationName: string,
|
|
188
|
+
session: ScopedPlanStatusSession,
|
|
189
|
+
) {
|
|
190
|
+
const op = yield* getOperation({ name: operationName }).pipe(
|
|
191
|
+
Effect.flatMap((current) =>
|
|
192
|
+
current.done === true
|
|
193
|
+
? Effect.succeed(current)
|
|
194
|
+
: Effect.fail({ _tag: "OperationPending" as const }),
|
|
195
|
+
),
|
|
196
|
+
Effect.retry({
|
|
197
|
+
while: (e: { _tag?: string }) => e?._tag === "OperationPending",
|
|
198
|
+
schedule: Schedule.exponential(Duration.seconds(5), 1.5).pipe(
|
|
199
|
+
Schedule.either(Schedule.spaced(Duration.seconds(30))),
|
|
200
|
+
// Parallelstore creates routinely run 10–25 min for
|
|
201
|
+
// multi-TiB instances. 200 × 30s ≈ 100 min wall ceiling.
|
|
202
|
+
Schedule.both(Schedule.recurs(200)),
|
|
203
|
+
Schedule.tapOutput(() =>
|
|
204
|
+
session.note(`Waiting for Parallelstore operation ${operationName}…`),
|
|
205
|
+
),
|
|
206
|
+
),
|
|
207
|
+
}),
|
|
208
|
+
);
|
|
209
|
+
if (op.error) {
|
|
210
|
+
return yield* new ConfigError({
|
|
211
|
+
message: `Parallelstore operation ${operationName} failed: ${
|
|
212
|
+
op.error.message ?? JSON.stringify(op.error)
|
|
213
|
+
}`,
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
return op;
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
const fqName = (project: string, location: string, instanceId: string) =>
|
|
220
|
+
`projects/${project}/locations/${location}/instances/${instanceId}`;
|
|
221
|
+
|
|
222
|
+
const observe = (project: string, location: string, instanceId: string) =>
|
|
223
|
+
getInstance({ name: fqName(project, location, instanceId) }).pipe(
|
|
224
|
+
Effect.catchTag("NotFound", () =>
|
|
225
|
+
Effect.succeed(undefined as ps.Instance | undefined),
|
|
226
|
+
),
|
|
227
|
+
Effect.catchTag("Forbidden", () =>
|
|
228
|
+
Effect.succeed(undefined as ps.Instance | undefined),
|
|
229
|
+
),
|
|
230
|
+
);
|
|
231
|
+
|
|
232
|
+
const syncMutable = Effect.fn(function* (args: {
|
|
233
|
+
name: string;
|
|
234
|
+
observed: ps.Instance;
|
|
235
|
+
desired: { description: string | undefined; labels: Record<string, string> };
|
|
236
|
+
session: ScopedPlanStatusSession;
|
|
237
|
+
}) {
|
|
238
|
+
const updateMaskFields: string[] = [];
|
|
239
|
+
if ((args.observed.description ?? undefined) !== args.desired.description) {
|
|
240
|
+
updateMaskFields.push("description");
|
|
241
|
+
}
|
|
242
|
+
const observedLabels = { ...(args.observed.labels ?? {}) };
|
|
243
|
+
const labelDiff = diffTags(observedLabels, args.desired.labels);
|
|
244
|
+
if (labelDiff.removed.length > 0 || labelDiff.upsert.length > 0) {
|
|
245
|
+
updateMaskFields.push("labels");
|
|
246
|
+
}
|
|
247
|
+
if (updateMaskFields.length === 0) return;
|
|
248
|
+
|
|
249
|
+
const op = yield* patchInstance({
|
|
250
|
+
name: args.name,
|
|
251
|
+
updateMask: updateMaskFields.join(","),
|
|
252
|
+
body: {
|
|
253
|
+
description: args.desired.description,
|
|
254
|
+
labels: args.desired.labels,
|
|
255
|
+
},
|
|
256
|
+
});
|
|
257
|
+
if (op.name) yield* awaitOperation(op.name, args.session);
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
return {
|
|
261
|
+
stables: ["instanceId", "project", "location", "name"],
|
|
262
|
+
diff: Effect.fn(function* ({ news, olds = {} }) {
|
|
263
|
+
if (!isResolved(news)) return undefined;
|
|
264
|
+
if (
|
|
265
|
+
somePropsAreDifferent(olds as ParallelstoreInstanceProps, news, [
|
|
266
|
+
"project",
|
|
267
|
+
"location",
|
|
268
|
+
"instanceId",
|
|
269
|
+
"network",
|
|
270
|
+
"reservedIpRange",
|
|
271
|
+
"deploymentType",
|
|
272
|
+
"fileStripeLevel",
|
|
273
|
+
"directoryStripeLevel",
|
|
274
|
+
"capacityGib",
|
|
275
|
+
])
|
|
276
|
+
) {
|
|
277
|
+
return { action: "replace" } as const;
|
|
278
|
+
}
|
|
279
|
+
return undefined;
|
|
280
|
+
}),
|
|
281
|
+
reconcile: Effect.fn(function* ({ id, news, session }) {
|
|
282
|
+
const internalLabels = yield* gcpInternalLabels(id);
|
|
283
|
+
const desiredId =
|
|
284
|
+
news.instanceId ??
|
|
285
|
+
(yield* createPhysicalName({ id, maxLength: 63 })).toLowerCase();
|
|
286
|
+
const parent = `projects/${news.project}/locations/${news.location}`;
|
|
287
|
+
const name = fqName(news.project, news.location, desiredId);
|
|
288
|
+
const desiredLabels: Record<string, string> = {
|
|
289
|
+
...(news.labels ?? {}),
|
|
290
|
+
...internalLabels,
|
|
291
|
+
};
|
|
292
|
+
|
|
293
|
+
let observed = yield* observe(news.project, news.location, desiredId);
|
|
294
|
+
|
|
295
|
+
if (!observed) {
|
|
296
|
+
const body: ps.Instance = {
|
|
297
|
+
capacityGib: news.capacityGib,
|
|
298
|
+
network: news.network,
|
|
299
|
+
...(news.description ? { description: news.description } : {}),
|
|
300
|
+
...(news.reservedIpRange ? { reservedIpRange: news.reservedIpRange } : {}),
|
|
301
|
+
...(news.deploymentType ? { deploymentType: news.deploymentType } : {}),
|
|
302
|
+
...(news.fileStripeLevel ? { fileStripeLevel: news.fileStripeLevel } : {}),
|
|
303
|
+
...(news.directoryStripeLevel
|
|
304
|
+
? { directoryStripeLevel: news.directoryStripeLevel }
|
|
305
|
+
: {}),
|
|
306
|
+
labels: desiredLabels,
|
|
307
|
+
};
|
|
308
|
+
const op = yield* createInstance({
|
|
309
|
+
parent,
|
|
310
|
+
instanceId: desiredId,
|
|
311
|
+
body,
|
|
312
|
+
}).pipe(
|
|
313
|
+
Effect.catchTag("Conflict", () =>
|
|
314
|
+
Effect.succeed(undefined as ps.Operation | undefined),
|
|
315
|
+
),
|
|
316
|
+
);
|
|
317
|
+
if (op?.name) yield* awaitOperation(op.name, session);
|
|
318
|
+
observed = yield* getInstance({ name });
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
yield* syncMutable({
|
|
322
|
+
name,
|
|
323
|
+
observed,
|
|
324
|
+
desired: { description: news.description, labels: desiredLabels },
|
|
325
|
+
session,
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
const final = yield* getInstance({ name });
|
|
329
|
+
return toAttributes(final, {
|
|
330
|
+
project: news.project,
|
|
331
|
+
location: news.location,
|
|
332
|
+
instanceId: desiredId,
|
|
333
|
+
network: news.network,
|
|
334
|
+
});
|
|
335
|
+
}),
|
|
336
|
+
delete: Effect.fn(function* ({ output, session }) {
|
|
337
|
+
yield* deleteInstance({ name: output.name }).pipe(
|
|
338
|
+
Effect.flatMap((op) =>
|
|
339
|
+
op.name ? awaitOperation(op.name, session) : Effect.succeed(op),
|
|
340
|
+
),
|
|
341
|
+
Effect.catchTag("NotFound", () => Effect.void),
|
|
342
|
+
);
|
|
343
|
+
}),
|
|
344
|
+
read: Effect.fn(function* ({ id, output, olds }) {
|
|
345
|
+
const project = output?.project ?? olds?.project;
|
|
346
|
+
const location = output?.location ?? olds?.location;
|
|
347
|
+
// `network` only narrows to NetworkRef when it comes from
|
|
348
|
+
// post-create `output`. `olds` is the snapshot of *props*
|
|
349
|
+
// including a NetworkRef, so both feed into the same typed
|
|
350
|
+
// pipeline.
|
|
351
|
+
const network = output?.network ?? olds?.network;
|
|
352
|
+
if (!project || !location || !network) return undefined;
|
|
353
|
+
const instanceId =
|
|
354
|
+
output?.instanceId ??
|
|
355
|
+
olds?.instanceId ??
|
|
356
|
+
(yield* createPhysicalName({ id, maxLength: 63 })).toLowerCase();
|
|
357
|
+
const observed = yield* observe(project, location, instanceId);
|
|
358
|
+
if (!observed) return undefined;
|
|
359
|
+
const attrs = toAttributes(observed, {
|
|
360
|
+
project,
|
|
361
|
+
location,
|
|
362
|
+
instanceId,
|
|
363
|
+
network,
|
|
364
|
+
});
|
|
365
|
+
return (yield* hasAlchemyLabels(id, observed.labels))
|
|
366
|
+
? attrs
|
|
367
|
+
: Unowned(attrs);
|
|
368
|
+
}),
|
|
369
|
+
};
|
|
370
|
+
}),
|
|
371
|
+
);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./Instance.ts";
|
package/src/Providers.ts
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import * as Provider from "alchemy/Provider";
|
|
2
|
+
import * as Layer from "effect/Layer";
|
|
3
|
+
import { GCPAuth } from "./Auth/AuthProvider.ts";
|
|
4
|
+
import { fromAuthProvider } from "./Auth/Credentials.ts";
|
|
5
|
+
import { Project, ProjectProvider } from "./CloudResourceManager/Project.ts";
|
|
6
|
+
import { GlobalAddress, GlobalAddressProvider } from "./Compute/GlobalAddress.ts";
|
|
7
|
+
import { Network, NetworkProvider } from "./Compute/Network.ts";
|
|
8
|
+
import { SharedVpcHost, SharedVpcHostProvider } from "./Compute/SharedVpcHost.ts";
|
|
9
|
+
import {
|
|
10
|
+
SharedVpcServiceProject,
|
|
11
|
+
SharedVpcServiceProjectProvider,
|
|
12
|
+
} from "./Compute/SharedVpcServiceProject.ts";
|
|
13
|
+
import { Subnetwork, SubnetworkProvider } from "./Compute/Subnetwork.ts";
|
|
14
|
+
import { Cluster, ClusterProvider } from "./Container/Cluster.ts";
|
|
15
|
+
import { NodePool, NodePoolProvider } from "./Container/NodePool.ts";
|
|
16
|
+
import {
|
|
17
|
+
ParallelstoreInstance,
|
|
18
|
+
ParallelstoreInstanceProvider,
|
|
19
|
+
} from "./Parallelstore/Instance.ts";
|
|
20
|
+
import {
|
|
21
|
+
PsaConnection,
|
|
22
|
+
PsaConnectionProvider,
|
|
23
|
+
} from "./ServiceNetworking/PsaConnection.ts";
|
|
24
|
+
import { ApiEnable, ApiEnableProvider } from "./ServiceUsage/ApiEnable.ts";
|
|
25
|
+
|
|
26
|
+
export class Providers extends Provider.ProviderCollection<Providers>()("GCP") {}
|
|
27
|
+
|
|
28
|
+
export type ProviderRequirements = Layer.Services<ReturnType<typeof providers>>;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Bundle the GCP resource providers, AuthProvider-backed credentials, and an
|
|
32
|
+
* HTTP client into a single Layer suitable for `Alchemy.Stack({ providers })`.
|
|
33
|
+
*
|
|
34
|
+
* Credentials resolve through the Alchemy AuthProvider registry — the active
|
|
35
|
+
* profile (from `ALCHEMY_PROFILE`, default `"default"`) configures `project`
|
|
36
|
+
* and optional `keyFile` overrides; everything else (ADC discovery, env-var
|
|
37
|
+
* key files, metadata server) is delegated to `google-auth-library`. To
|
|
38
|
+
* bypass the registry, import {@link fromADC} directly.
|
|
39
|
+
*
|
|
40
|
+
* `FetchHttpClient.layer` is provided once at the Stack level (see
|
|
41
|
+
* `vendor/alchemy/packages/alchemy/src/Stack.ts`); we intentionally do NOT
|
|
42
|
+
* re-provide it here, mirroring `AWS/Providers.ts` and
|
|
43
|
+
* `Cloudflare/Providers.ts`.
|
|
44
|
+
*/
|
|
45
|
+
export const providers = () =>
|
|
46
|
+
Layer.effect(
|
|
47
|
+
Providers,
|
|
48
|
+
Provider.collection([
|
|
49
|
+
Project,
|
|
50
|
+
Cluster,
|
|
51
|
+
NodePool,
|
|
52
|
+
ApiEnable,
|
|
53
|
+
Network,
|
|
54
|
+
Subnetwork,
|
|
55
|
+
GlobalAddress,
|
|
56
|
+
SharedVpcHost,
|
|
57
|
+
SharedVpcServiceProject,
|
|
58
|
+
PsaConnection,
|
|
59
|
+
ParallelstoreInstance,
|
|
60
|
+
]),
|
|
61
|
+
).pipe(
|
|
62
|
+
Layer.provide(
|
|
63
|
+
Layer.mergeAll(
|
|
64
|
+
ProjectProvider(),
|
|
65
|
+
ClusterProvider(),
|
|
66
|
+
NodePoolProvider(),
|
|
67
|
+
ApiEnableProvider(),
|
|
68
|
+
NetworkProvider(),
|
|
69
|
+
SubnetworkProvider(),
|
|
70
|
+
GlobalAddressProvider(),
|
|
71
|
+
SharedVpcHostProvider(),
|
|
72
|
+
SharedVpcServiceProjectProvider(),
|
|
73
|
+
PsaConnectionProvider(),
|
|
74
|
+
ParallelstoreInstanceProvider(),
|
|
75
|
+
),
|
|
76
|
+
),
|
|
77
|
+
Layer.provideMerge(fromAuthProvider()),
|
|
78
|
+
Layer.provideMerge(GCPAuth),
|
|
79
|
+
// Auth/config failures (missing ADC, malformed profile) become
|
|
80
|
+
// unrecoverable defects rather than typed errors — mirrors
|
|
81
|
+
// `AWS/Providers.ts` and `Cloudflare/Providers.ts`. Resource lifecycle
|
|
82
|
+
// handlers still surface their own typed errors normally.
|
|
83
|
+
Layer.orDie,
|
|
84
|
+
);
|