@microagi/alchemy-gcp 0.5.0 → 0.6.0

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.
Files changed (90) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/lib/ArtifactRegistry/Repository.d.ts +1 -1
  3. package/lib/ArtifactRegistry/Repository.d.ts.map +1 -1
  4. package/lib/ArtifactRegistry/Repository.js +8 -9
  5. package/lib/ArtifactRegistry/Repository.js.map +1 -1
  6. package/lib/Auth/Credentials.d.ts +10 -3
  7. package/lib/Auth/Credentials.d.ts.map +1 -1
  8. package/lib/Auth/Credentials.js +13 -3
  9. package/lib/Auth/Credentials.js.map +1 -1
  10. package/lib/CloudResourceManager/Project.d.ts +1 -1
  11. package/lib/Compute/Address.d.ts +97 -0
  12. package/lib/Compute/Address.d.ts.map +1 -0
  13. package/lib/Compute/Address.js +143 -0
  14. package/lib/Compute/Address.js.map +1 -0
  15. package/lib/Compute/Firewall.d.ts +108 -0
  16. package/lib/Compute/Firewall.d.ts.map +1 -0
  17. package/lib/Compute/Firewall.js +163 -0
  18. package/lib/Compute/Firewall.js.map +1 -0
  19. package/lib/Compute/ForwardingRule.d.ts +106 -0
  20. package/lib/Compute/ForwardingRule.d.ts.map +1 -0
  21. package/lib/Compute/ForwardingRule.js +152 -0
  22. package/lib/Compute/ForwardingRule.js.map +1 -0
  23. package/lib/Compute/GlobalAddress.d.ts +1 -1
  24. package/lib/Compute/Network.d.ts +1 -1
  25. package/lib/Compute/SharedVpcHost.d.ts +1 -1
  26. package/lib/Compute/SharedVpcServiceProject.d.ts +1 -1
  27. package/lib/Compute/Subnetwork.d.ts +1 -1
  28. package/lib/Compute/index.d.ts +3 -0
  29. package/lib/Compute/index.d.ts.map +1 -1
  30. package/lib/Compute/index.js +3 -0
  31. package/lib/Compute/index.js.map +1 -1
  32. package/lib/Container/Cluster.d.ts +21 -1
  33. package/lib/Container/Cluster.d.ts.map +1 -1
  34. package/lib/Container/Cluster.js +5 -2
  35. package/lib/Container/Cluster.js.map +1 -1
  36. package/lib/Container/NodePool.d.ts +27 -2
  37. package/lib/Container/NodePool.d.ts.map +1 -1
  38. package/lib/Container/NodePool.js +45 -8
  39. package/lib/Container/NodePool.js.map +1 -1
  40. package/lib/Kubernetes/Secret.d.ts +89 -0
  41. package/lib/Kubernetes/Secret.d.ts.map +1 -0
  42. package/lib/Kubernetes/Secret.js +139 -0
  43. package/lib/Kubernetes/Secret.js.map +1 -0
  44. package/lib/Kubernetes/client.d.ts +58 -0
  45. package/lib/Kubernetes/client.d.ts.map +1 -0
  46. package/lib/Kubernetes/client.js +131 -0
  47. package/lib/Kubernetes/client.js.map +1 -0
  48. package/lib/Kubernetes/index.d.ts +2 -0
  49. package/lib/Kubernetes/index.d.ts.map +1 -0
  50. package/lib/Kubernetes/index.js +2 -0
  51. package/lib/Kubernetes/index.js.map +1 -0
  52. package/lib/ManagedLustre/Instance.d.ts +1 -1
  53. package/lib/ManagedLustre/Instance.d.ts.map +1 -1
  54. package/lib/ManagedLustre/Instance.js +25 -5
  55. package/lib/ManagedLustre/Instance.js.map +1 -1
  56. package/lib/Providers.d.ts +1 -1
  57. package/lib/Providers.d.ts.map +1 -1
  58. package/lib/Providers.js +9 -1
  59. package/lib/Providers.js.map +1 -1
  60. package/lib/Run/Job.d.ts +1 -1
  61. package/lib/Run/Service.d.ts +1 -1
  62. package/lib/ServiceNetworking/PsaConnection.d.ts +1 -1
  63. package/lib/ServiceUsage/ApiEnable.d.ts +1 -1
  64. package/lib/Sqladmin/Database.d.ts +1 -1
  65. package/lib/Sqladmin/Instance.d.ts +1 -1
  66. package/lib/Sqladmin/Instance.d.ts.map +1 -1
  67. package/lib/Sqladmin/Instance.js +28 -6
  68. package/lib/Sqladmin/Instance.js.map +1 -1
  69. package/lib/Sqladmin/User.d.ts +1 -1
  70. package/lib/Tags.d.ts +4 -4
  71. package/lib/index.d.ts +1 -0
  72. package/lib/index.d.ts.map +1 -1
  73. package/lib/index.js +1 -0
  74. package/lib/index.js.map +1 -1
  75. package/package.json +1 -1
  76. package/src/ArtifactRegistry/Repository.ts +13 -9
  77. package/src/Auth/Credentials.ts +15 -4
  78. package/src/Compute/Address.ts +288 -0
  79. package/src/Compute/Firewall.ts +330 -0
  80. package/src/Compute/ForwardingRule.ts +301 -0
  81. package/src/Compute/index.ts +3 -0
  82. package/src/Container/Cluster.ts +25 -2
  83. package/src/Container/NodePool.ts +87 -13
  84. package/src/Kubernetes/Secret.ts +274 -0
  85. package/src/Kubernetes/client.ts +219 -0
  86. package/src/Kubernetes/index.ts +1 -0
  87. package/src/ManagedLustre/Instance.ts +35 -6
  88. package/src/Providers.ts +18 -0
  89. package/src/Sqladmin/Instance.ts +36 -7
  90. package/src/index.ts +1 -0
@@ -503,19 +503,23 @@ export const ArtifactRegistryRepositoryProvider = () =>
503
503
  ],
504
504
  diff: Effect.fn(function* ({ news, olds = {} }) {
505
505
  if (!isResolved(news)) return undefined;
506
+ // String-typed stables are reference-comparable.
506
507
  if (
507
508
  somePropsAreDifferent(
508
509
  olds as ArtifactRegistryRepositoryProps,
509
510
  news,
510
- [
511
- "project",
512
- "location",
513
- "name",
514
- "format",
515
- "mode",
516
- "kmsKeyName",
517
- "remoteRepositoryConfig",
518
- ],
511
+ ["project", "location", "name", "format", "mode", "kmsKeyName"],
512
+ )
513
+ ) {
514
+ return { action: "replace" } as const;
515
+ }
516
+ // Object-typed stables must use deep equality — JSON-deserialized
517
+ // state never `===` a freshly-built literal even when content matches.
518
+ const oldsTyped = olds as ArtifactRegistryRepositoryProps;
519
+ if (
520
+ !deepEqual(
521
+ oldsTyped.remoteRepositoryConfig,
522
+ news.remoteRepositoryConfig,
519
523
  )
520
524
  ) {
521
525
  return { action: "replace" } as const;
@@ -1,6 +1,6 @@
1
1
  import { ConfigError, Credentials } from "@distilled.cloud/gcp";
2
2
  import { getAuthProvider } from "alchemy/Auth/AuthProvider";
3
- import { ALCHEMY_PROFILE, loadOrConfigure } from "alchemy/Auth/Profile";
3
+ import { ALCHEMY_PROFILE, AlchemyProfile } from "alchemy/Auth/Profile";
4
4
  import * as Config from "effect/Config";
5
5
  import * as Effect from "effect/Effect";
6
6
  import * as Layer from "effect/Layer";
@@ -29,11 +29,17 @@ const toConfigError = (e: { message?: string } | unknown) =>
29
29
  *
30
30
  * Prefer {@link fromAuthProvider} when the stack runs through
31
31
  * `alchemy login` / `ALCHEMY_PROFILE`.
32
+ *
33
+ * The `Credentials` service is lazy (its value is an `Effect<Config>`
34
+ * re-evaluated per SDK call), so minting happens at request time —
35
+ * `google-auth-library` caches and refreshes underneath. Failures
36
+ * become defects: the service channel carries no typed error, matching
37
+ * distilled's own `CredentialsFromEnv`.
32
38
  */
33
39
  export const fromADC = (project?: string) =>
34
- Layer.effect(
40
+ Layer.succeed(
35
41
  Credentials,
36
- mintToken({ project }).pipe(Effect.mapError(toConfigError)),
42
+ mintToken({ project }).pipe(Effect.mapError(toConfigError), Effect.orDie),
37
43
  );
38
44
 
39
45
  /**
@@ -51,6 +57,7 @@ export const fromAuthProvider = () =>
51
57
  Layer.effect(
52
58
  Credentials,
53
59
  Effect.gen(function* () {
60
+ const profile = yield* AlchemyProfile;
54
61
  const auth = yield* getAuthProvider<
55
62
  GCPAuthConfig,
56
63
  GCPResolvedCredentials
@@ -59,11 +66,15 @@ export const fromAuthProvider = () =>
59
66
  const ci = yield* Config.boolean("CI").pipe(Config.withDefault(false));
60
67
  const ctx = yield* Effect.context<never>();
61
68
 
62
- return yield* loadOrConfigure(auth, profileName, { ci }).pipe(
69
+ // The service value is itself an Effect (lazy credentials):
70
+ // profile/provider wiring resolves once at layer build, while
71
+ // loadOrConfigure + token read run per SDK call.
72
+ return profile.loadOrConfigure(auth, profileName, { ci }).pipe(
63
73
  Effect.flatMap((cfg) => auth.read(profileName, cfg as GCPAuthConfig)),
64
74
  Effect.map(({ accessToken, project }) => ({ accessToken, project })),
65
75
  Effect.mapError(toConfigError),
66
76
  Effect.provide(ctx),
77
+ Effect.orDie,
67
78
  );
68
79
  }),
69
80
  );
@@ -0,0 +1,288 @@
1
+ import * as compute from "@distilled.cloud/gcp/compute-v1";
2
+ import { Resource } from "alchemy";
3
+ import { Unowned } from "alchemy/AdoptPolicy";
4
+ import type { ScopedPlanStatusSession } from "alchemy/Cli/Cli";
5
+ import { deepEqual, isResolved, somePropsAreDifferent } from "alchemy/Diff";
6
+ import { createPhysicalName } from "alchemy/PhysicalName";
7
+ import * as Provider from "alchemy/Provider";
8
+ import * as Effect from "effect/Effect";
9
+ import { gcpInternalLabels, hasAlchemyLabels } from "../Tags.ts";
10
+ import type * as GCP from "../Providers.ts";
11
+ import { makeAwaitRegionOperation } from "./Operations.ts";
12
+
13
+ /**
14
+ * A regional compute Address. The intended use here is a **Private
15
+ * Service Connect consumer endpoint IP** — a single internal address
16
+ * reserved in a subnet (`addressType=INTERNAL`, `purpose=GCE_ENDPOINT`)
17
+ * that a `ForwardingRule` then binds to a producer service attachment
18
+ * (e.g. a Cloud SQL instance in another VPC).
19
+ *
20
+ * For global PSA *ranges* (`purpose=VPC_PEERING`) use
21
+ * {@link GlobalAddress} — that is a different API surface
22
+ * (`globalAddresses` vs regional `addresses`).
23
+ *
24
+ * Adoption uses the labels field (Address carries `labels` +
25
+ * `labelFingerprint`).
26
+ *
27
+ * @section Reserving a PSC endpoint IP
28
+ * @example Internal address inside a subnet for a PSC forwarding rule
29
+ * ```typescript
30
+ * const endpointIp = yield* GCP.Address("DbPscAddress", {
31
+ * project: hostProject.projectId,
32
+ * region: "europe-west1",
33
+ * subnetwork: pscSubnet.selfLink,
34
+ * address: "10.0.16.5",
35
+ * });
36
+ * ```
37
+ */
38
+ export type AddressProps = {
39
+ /** GCP project ID hosting the address. Immutable — replace if changed. */
40
+ project: string;
41
+ /** GCP region. Addresses are regional — a different region requires replace. */
42
+ region: string;
43
+ /**
44
+ * Address name. Defaults to `createPhysicalName({ id, lowercase: true,
45
+ * maxLength: 63 })`. Immutable — replace if changed.
46
+ */
47
+ name?: string;
48
+ /** Description (free-form). Set on create; immutable. */
49
+ description?: string;
50
+ /**
51
+ * Internal vs external. Defaults to `INTERNAL` (the only value that
52
+ * makes sense for a PSC endpoint IP). Immutable — replace.
53
+ */
54
+ addressType?: "INTERNAL" | "EXTERNAL";
55
+ /**
56
+ * Address purpose. For a PSC endpoint IP use `GCE_ENDPOINT` (the
57
+ * default GCP assigns to plain internal addresses). Immutable — replace.
58
+ */
59
+ purpose?: "GCE_ENDPOINT" | "SHARED_LOADBALANCER_VIP" | "DNS_RESOLVER";
60
+ /**
61
+ * URL of the subnetwork the address is allocated from. Pass
62
+ * `subnet.selfLink` from a `GCP.Subnetwork` to make alchemy sequence
63
+ * the address after the subnet. Required for INTERNAL addresses.
64
+ * Immutable — replace.
65
+ */
66
+ subnetwork: string;
67
+ /**
68
+ * Explicit IP to reserve inside the subnet's range. If omitted, GCP
69
+ * picks one. Immutable — replace.
70
+ */
71
+ address?: string;
72
+ /**
73
+ * Resource labels. Alchemy internal labels (`alchemy_app`,
74
+ * `alchemy_stage`, `alchemy_id`) are merged on top automatically and
75
+ * are reserved. Mutable via `setLabels`.
76
+ */
77
+ labels?: Record<string, string>;
78
+ };
79
+
80
+ export type AddressAttributes = {
81
+ /** Address name. */
82
+ name: string;
83
+ /** GCP project ID, threaded through from props for delete/read. */
84
+ project: string;
85
+ /** GCP region, threaded through from props for delete/read. */
86
+ region: string;
87
+ /** Server-defined URL. */
88
+ selfLink: string;
89
+ /** Server-assigned numeric id. */
90
+ id: string;
91
+ /** The reserved IP. */
92
+ address: string;
93
+ /** Address type. */
94
+ addressType: string | undefined;
95
+ /** Purpose. */
96
+ purpose: string | undefined;
97
+ /** Subnetwork URL. */
98
+ subnetwork: string | undefined;
99
+ /** Status: `RESERVING`, `RESERVED`, `IN_USE`. */
100
+ status: string | undefined;
101
+ /** Labels currently set, including the alchemy internals. */
102
+ labels: Record<string, string>;
103
+ };
104
+
105
+ export type Address = Resource<
106
+ "GCP.Address",
107
+ AddressProps,
108
+ AddressAttributes,
109
+ never,
110
+ GCP.Providers
111
+ >;
112
+ export const Address = Resource<Address>("GCP.Address");
113
+
114
+ const toAddressAttributes = (
115
+ a: compute.Address,
116
+ parent: { project: string; region: string },
117
+ ): AddressAttributes => ({
118
+ name: a.name ?? "",
119
+ project: parent.project,
120
+ region: parent.region,
121
+ selfLink: a.selfLink ?? "",
122
+ id: a.id ?? "",
123
+ address: a.address ?? "",
124
+ addressType: a.addressType,
125
+ purpose: a.purpose,
126
+ subnetwork: a.subnetwork,
127
+ status: a.status,
128
+ labels: { ...(a.labels ?? {}) },
129
+ });
130
+
131
+ export const AddressProvider = () =>
132
+ Provider.effect(
133
+ Address,
134
+ Effect.gen(function* () {
135
+ const getAddresses = yield* compute.getAddresses;
136
+ const insertAddresses = yield* compute.insertAddresses;
137
+ const deleteAddresses = yield* compute.deleteAddresses;
138
+ const setLabelsAddresses = yield* compute.setLabelsAddresses;
139
+ const getRegionOperations = yield* compute.getRegionOperations;
140
+ const awaitOp = makeAwaitRegionOperation(getRegionOperations);
141
+
142
+ const observe = (project: string, region: string, name: string) =>
143
+ getAddresses({ project, region, address: name }).pipe(
144
+ Effect.catchTag("NotFound", () =>
145
+ Effect.succeed(undefined as compute.Address | undefined),
146
+ ),
147
+ Effect.catchTag("Forbidden", () =>
148
+ Effect.succeed(undefined as compute.Address | undefined),
149
+ ),
150
+ );
151
+
152
+ const syncLabels = Effect.fn(function* (args: {
153
+ project: string;
154
+ region: string;
155
+ observed: compute.Address;
156
+ desired: Record<string, string>;
157
+ session: ScopedPlanStatusSession;
158
+ }) {
159
+ if (deepEqual(args.observed.labels ?? {}, args.desired)) return;
160
+ const op = yield* setLabelsAddresses({
161
+ project: args.project,
162
+ region: args.region,
163
+ resource: args.observed.name ?? "",
164
+ body: {
165
+ labels: args.desired,
166
+ labelFingerprint: args.observed.labelFingerprint,
167
+ },
168
+ });
169
+ if (op.name) {
170
+ yield* awaitOp(args.project, args.region, op.name, args.session);
171
+ }
172
+ });
173
+
174
+ return {
175
+ stables: ["name", "project", "region", "selfLink", "id", "address"],
176
+ diff: Effect.fn(function* ({ news, olds = {} }) {
177
+ if (!isResolved(news)) return undefined;
178
+ if (
179
+ somePropsAreDifferent(olds as AddressProps, news, [
180
+ "project",
181
+ "region",
182
+ "name",
183
+ "addressType",
184
+ "purpose",
185
+ "subnetwork",
186
+ "address",
187
+ ])
188
+ ) {
189
+ return { action: "replace" } as const;
190
+ }
191
+ return undefined;
192
+ }),
193
+ reconcile: Effect.fn(function* ({ id, news, session }) {
194
+ const internalLabels = yield* gcpInternalLabels(id);
195
+ const desiredName =
196
+ news.name ??
197
+ (yield* createPhysicalName({ id, maxLength: 63 })).toLowerCase();
198
+ const desiredLabels: Record<string, string> = {
199
+ ...(news.labels ?? {}),
200
+ ...internalLabels,
201
+ };
202
+
203
+ let observed = yield* observe(news.project, news.region, desiredName);
204
+
205
+ if (!observed) {
206
+ const body: compute.Address = {
207
+ name: desiredName,
208
+ ...(news.description ? { description: news.description } : {}),
209
+ addressType: news.addressType ?? "INTERNAL",
210
+ ...(news.purpose ? { purpose: news.purpose } : {}),
211
+ subnetwork: news.subnetwork,
212
+ ...(news.address ? { address: news.address } : {}),
213
+ labels: desiredLabels,
214
+ };
215
+ const op = yield* insertAddresses({
216
+ project: news.project,
217
+ region: news.region,
218
+ body,
219
+ }).pipe(
220
+ Effect.catchTag("Conflict", () =>
221
+ Effect.succeed(undefined as compute.Operation | undefined),
222
+ ),
223
+ );
224
+ if (op?.name) {
225
+ yield* awaitOp(news.project, news.region, op.name, session);
226
+ }
227
+ observed = yield* getAddresses({
228
+ project: news.project,
229
+ region: news.region,
230
+ address: desiredName,
231
+ });
232
+ }
233
+
234
+ yield* syncLabels({
235
+ project: news.project,
236
+ region: news.region,
237
+ observed,
238
+ desired: desiredLabels,
239
+ session,
240
+ });
241
+
242
+ const final = yield* getAddresses({
243
+ project: news.project,
244
+ region: news.region,
245
+ address: desiredName,
246
+ });
247
+ return toAddressAttributes(final, {
248
+ project: news.project,
249
+ region: news.region,
250
+ });
251
+ }),
252
+ delete: Effect.fn(function* ({ output, session }) {
253
+ yield* deleteAddresses({
254
+ project: output.project,
255
+ region: output.region,
256
+ address: output.name,
257
+ }).pipe(
258
+ Effect.flatMap((op) =>
259
+ op.name
260
+ ? awaitOp(output.project, output.region, op.name, session)
261
+ : Effect.succeed(op),
262
+ ),
263
+ Effect.catchTag("NotFound", () => Effect.void),
264
+ // 400 "is being used by ..." comes back while a forwarding
265
+ // rule still references the address. The rule must be
266
+ // deleted first (alchemy's dep order handles this when the
267
+ // rule consumes `address.selfLink`); surface the error
268
+ // rather than silently swallowing.
269
+ );
270
+ }),
271
+ read: Effect.fn(function* ({ id, output, olds }) {
272
+ const project = output?.project ?? olds?.project;
273
+ const region = output?.region ?? olds?.region;
274
+ if (!project || !region) return undefined;
275
+ const name =
276
+ output?.name ??
277
+ olds?.name ??
278
+ (yield* createPhysicalName({ id, maxLength: 63 })).toLowerCase();
279
+ const observed = yield* observe(project, region, name);
280
+ if (!observed) return undefined;
281
+ const attrs = toAddressAttributes(observed, { project, region });
282
+ return (yield* hasAlchemyLabels(id, observed.labels))
283
+ ? attrs
284
+ : Unowned(attrs);
285
+ }),
286
+ };
287
+ }),
288
+ );