@microagi/alchemy-gcp 0.8.0 → 0.8.1

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 (88) hide show
  1. package/lib/ArtifactRegistry/Operations.d.ts +2 -2
  2. package/lib/ArtifactRegistry/Operations.d.ts.map +1 -1
  3. package/lib/ArtifactRegistry/Repository.d.ts +1 -1
  4. package/lib/ArtifactRegistry/Validation.d.ts.map +1 -1
  5. package/lib/Auth/AuthProvider.d.ts.map +1 -1
  6. package/lib/Auth/AuthProvider.js.map +1 -1
  7. package/lib/Auth/Credentials.d.ts +1 -1
  8. package/lib/Auth/Credentials.d.ts.map +1 -1
  9. package/lib/CloudResourceManager/Project.d.ts +1 -1
  10. package/lib/CloudResourceManager/ProjectIamMember.d.ts.map +1 -1
  11. package/lib/Compute/Address.d.ts +1 -1
  12. package/lib/Compute/Firewall.d.ts +1 -1
  13. package/lib/Compute/ForwardingRule.d.ts +1 -1
  14. package/lib/Compute/GlobalAddress.d.ts +1 -1
  15. package/lib/Compute/Network.d.ts +1 -1
  16. package/lib/Compute/Network.d.ts.map +1 -1
  17. package/lib/Compute/Operations.d.ts +4 -4
  18. package/lib/Compute/Operations.d.ts.map +1 -1
  19. package/lib/Compute/Subnetwork.d.ts +1 -1
  20. package/lib/Compute/SubnetworkIamMember.d.ts.map +1 -1
  21. package/lib/Container/Cluster.d.ts +1 -1
  22. package/lib/Container/NodePool.d.ts +11 -1
  23. package/lib/Container/NodePool.d.ts.map +1 -1
  24. package/lib/Container/NodePool.js +4 -0
  25. package/lib/Container/NodePool.js.map +1 -1
  26. package/lib/Container/Operations.d.ts +2 -2
  27. package/lib/Container/Operations.d.ts.map +1 -1
  28. package/lib/Iam/ServiceAccount.d.ts +1 -1
  29. package/lib/Iam/ServiceAccount.d.ts.map +1 -1
  30. package/lib/Iam/ServiceAccount.js +21 -2
  31. package/lib/Iam/ServiceAccount.js.map +1 -1
  32. package/lib/Iam/ServiceAccountIamMember.d.ts.map +1 -1
  33. package/lib/Iam/ServiceAccountKey.d.ts +72 -0
  34. package/lib/Iam/ServiceAccountKey.d.ts.map +1 -0
  35. package/lib/Iam/ServiceAccountKey.js +164 -0
  36. package/lib/Iam/ServiceAccountKey.js.map +1 -0
  37. package/lib/Iam/index.d.ts +2 -0
  38. package/lib/Iam/index.d.ts.map +1 -1
  39. package/lib/Iam/index.js +1 -0
  40. package/lib/Iam/index.js.map +1 -1
  41. package/lib/Kubernetes/KubernetesManifest.d.ts +1 -1
  42. package/lib/Kubernetes/Secret.d.ts +1 -1
  43. package/lib/Kubernetes/client.d.ts.map +1 -1
  44. package/lib/Kubernetes/connection.d.ts.map +1 -1
  45. package/lib/ManagedLustre/Instance.d.ts +1 -1
  46. package/lib/Providers.d.ts +1 -1
  47. package/lib/Providers.d.ts.map +1 -1
  48. package/lib/Providers.js +5 -1
  49. package/lib/Providers.js.map +1 -1
  50. package/lib/Run/IamMember.d.ts.map +1 -1
  51. package/lib/Run/IamSync.d.ts.map +1 -1
  52. package/lib/Run/Job.d.ts +1 -1
  53. package/lib/Run/Operations.d.ts +2 -2
  54. package/lib/Run/Operations.d.ts.map +1 -1
  55. package/lib/Run/Service.d.ts +1 -1
  56. package/lib/Run/Validation.d.ts.map +1 -1
  57. package/lib/Sqladmin/Instance.d.ts +1 -1
  58. package/lib/Sqladmin/Operations.d.ts +2 -2
  59. package/lib/Sqladmin/Operations.d.ts.map +1 -1
  60. package/lib/Sqladmin/Types.d.ts.map +1 -1
  61. package/lib/Sqladmin/Validation.d.ts.map +1 -1
  62. package/lib/Storage/Bucket.d.ts +117 -0
  63. package/lib/Storage/Bucket.d.ts.map +1 -0
  64. package/lib/Storage/Bucket.js +239 -0
  65. package/lib/Storage/Bucket.js.map +1 -0
  66. package/lib/Storage/BucketIamMember.d.ts +29 -0
  67. package/lib/Storage/BucketIamMember.d.ts.map +1 -0
  68. package/lib/Storage/BucketIamMember.js +26 -0
  69. package/lib/Storage/BucketIamMember.js.map +1 -0
  70. package/lib/Storage/index.d.ts +4 -0
  71. package/lib/Storage/index.d.ts.map +1 -0
  72. package/lib/Storage/index.js +3 -0
  73. package/lib/Storage/index.js.map +1 -0
  74. package/lib/Tags.d.ts.map +1 -1
  75. package/lib/index.d.ts +1 -0
  76. package/lib/index.d.ts.map +1 -1
  77. package/lib/index.js +1 -0
  78. package/lib/index.js.map +1 -1
  79. package/package.json +2 -2
  80. package/src/Container/NodePool.ts +12 -0
  81. package/src/Iam/ServiceAccount.ts +21 -2
  82. package/src/Iam/ServiceAccountKey.ts +284 -0
  83. package/src/Iam/index.ts +5 -0
  84. package/src/Providers.ts +12 -0
  85. package/src/Storage/Bucket.ts +439 -0
  86. package/src/Storage/BucketIamMember.ts +33 -0
  87. package/src/Storage/index.ts +10 -0
  88. package/src/index.ts +1 -0
@@ -0,0 +1,284 @@
1
+ import { ConfigError } from "@distilled.cloud/gcp";
2
+ import * as iam from "@distilled.cloud/gcp/unstable/iam-v1";
3
+ import { Resource } from "alchemy";
4
+ import { isResolved } from "alchemy/Diff";
5
+ import * as Provider from "alchemy/Provider";
6
+ import * as Effect from "effect/Effect";
7
+ import type * as GCP from "../Providers.ts";
8
+
9
+ /**
10
+ * A user-managed service account key (JSON credentials file). Lives
11
+ * under a {@link import("./ServiceAccount.ts").ServiceAccount} and
12
+ * produces a `privateKeyData` field (base64-encoded JSON key file)
13
+ * available ONLY on create — `get`/`list` never return it.
14
+ *
15
+ * The `privateKeyData` attribute is marked stable so the engine
16
+ * preserves it across reconciles (it cannot be re-read from the API).
17
+ * On cold recovery (state loss), if the key exists but
18
+ * `privateKeyData` is absent from state, the old key is deleted and a
19
+ * new one is created so the `privateKeyData` is re-obtained.
20
+ *
21
+ * SA keys carry no labels or description, so adoption cannot be gated
22
+ * by marker — the SA itself is alchemy-owned, and the key is managed
23
+ * exclusively through this resource.
24
+ *
25
+ * @section Creating a ServiceAccountKey
26
+ * @example Mint a JSON key for a GSA
27
+ * ```typescript
28
+ * const key = yield* GCP.ServiceAccountKey("TempBucketKey", {
29
+ * serviceAccount: sa.name, // projects/{p}/serviceAccounts/{email}
30
+ * });
31
+ * // key.privateKeyData is the base64-encoded JSON key file
32
+ * ```
33
+ */
34
+ export type ServiceAccountKeyProps = {
35
+ /**
36
+ * The resource name of the parent service account, in the format
37
+ * `projects/{PROJECT_ID}/serviceAccounts/{EMAIL_ADDRESS}`.
38
+ * Immutable — changing it triggers a replacement (the key is
39
+ * scoped to the SA).
40
+ */
41
+ serviceAccount: string;
42
+ /**
43
+ * Key algorithm. Defaults to `KEY_ALG_RSA_2048`. Immutable after
44
+ * create.
45
+ */
46
+ keyAlgorithm?: "KEY_ALG_RSA_2048" | "KEY_ALG_RSA_1024";
47
+ /**
48
+ * Output format for the private key. Defaults to
49
+ * `TYPE_GOOGLE_CREDENTIALS_FILE` (the Google Credentials JSON
50
+ * format usable by gcloud, gsutil, and the gcsfuse CSI driver).
51
+ */
52
+ privateKeyType?: "TYPE_GOOGLE_CREDENTIALS_FILE" | "TYPE_PKCS12_FILE";
53
+ };
54
+
55
+ export type ServiceAccountKey = Resource<
56
+ "GCP.ServiceAccountKey",
57
+ ServiceAccountKeyProps,
58
+ {
59
+ /** Full resource name: `projects/{p}/serviceAccounts/{email}/keys/{keyId}`. */
60
+ name: string;
61
+ /**
62
+ * Base64-encoded private key data (JSON key file). Only available
63
+ * on create — not returned by `get`/`list`. Preserved in alchemy
64
+ * state; on state loss the old key is deleted + a new one is
65
+ * created to re-obtain it.
66
+ */
67
+ privateKeyData: string | undefined;
68
+ /** Key algorithm used. */
69
+ keyAlgorithm: string | undefined;
70
+ /** Key type: `USER_MANAGED` or `SYSTEM_MANAGED`. */
71
+ keyType: string | undefined;
72
+ /** Key origin: `USER_PROVIDED` or `GOOGLE_PROVIDED`. */
73
+ keyOrigin: string | undefined;
74
+ /** Timestamp after which the key is valid (RFC 3339). */
75
+ validAfterTime: string | undefined;
76
+ /** Whether the key is disabled. */
77
+ disabled: boolean | undefined;
78
+ },
79
+ never,
80
+ GCP.Providers
81
+ >;
82
+
83
+ export const ServiceAccountKey = Resource<ServiceAccountKey>(
84
+ "GCP.ServiceAccountKey",
85
+ );
86
+
87
+ const toAttributes = (k: iam.ServiceAccountKey): ServiceAccountKey["Attributes"] => ({
88
+ name: k.name ?? "",
89
+ privateKeyData: k.privateKeyData,
90
+ keyAlgorithm: k.keyAlgorithm,
91
+ keyType: k.keyType,
92
+ keyOrigin: k.keyOrigin,
93
+ validAfterTime: k.validAfterTime,
94
+ disabled: k.disabled,
95
+ });
96
+
97
+ export const ServiceAccountKeyProvider = () =>
98
+ Provider.effect(
99
+ ServiceAccountKey,
100
+ Effect.gen(function* () {
101
+ const createProjectsServiceAccountsKeys =
102
+ yield* iam.createProjectsServiceAccountsKeys;
103
+ const deleteProjectsServiceAccountsKeys =
104
+ yield* iam.deleteProjectsServiceAccountsKeys;
105
+ const listProjectsServiceAccountsKeys =
106
+ yield* iam.listProjectsServiceAccountsKeys;
107
+ const getProjectsServiceAccountsKeys =
108
+ yield* iam.getProjectsServiceAccountsKeys;
109
+
110
+ // Find a user-managed key by name (direct probe).
111
+ // NotFound → undefined (key doesn't exist). Forbidden is NOT
112
+ // swallowed — if the caller lacks permission to read this key,
113
+ // propagating the error is safer than treating it as "missing"
114
+ // and potentially creating a duplicate on the SA.
115
+ const observeKey = (keyName: string) =>
116
+ getProjectsServiceAccountsKeys({
117
+ name: keyName,
118
+ publicKeyType: "TYPE_GOOGLE_CREDENTIALS_FILE",
119
+ }).pipe(
120
+ Effect.catchTag("NotFound", () =>
121
+ Effect.succeed(undefined as iam.ServiceAccountKey | undefined),
122
+ ),
123
+ );
124
+
125
+ // List all user-managed keys for the SA. Used for cold recovery
126
+ // when the key name is not in state. Returns the first
127
+ // user-managed key found (the SA is dedicated to this resource,
128
+ // so there should be at most one user-managed key).
129
+ const findUserManagedKey = Effect.fn(function* (
130
+ serviceAccount: string,
131
+ ) {
132
+ const page = yield* listProjectsServiceAccountsKeys({
133
+ name: serviceAccount,
134
+ keyTypes: "USER_MANAGED",
135
+ });
136
+ const keys = page.keys ?? [];
137
+ if (keys.length === 0) return undefined;
138
+ if (keys.length === 1) return keys[0];
139
+ // Multiple user-managed keys — GCP allows multiple keys per
140
+ // SA, so creating another would silently succeed (not
141
+ // Conflict as an earlier comment assumed). Surface an error
142
+ // so the user cleans up the extra keys and re-runs.
143
+ return yield* Effect.fail(
144
+ new ConfigError({
145
+ message:
146
+ `Service account ${serviceAccount} has ${keys.length} user-managed keys; ` +
147
+ `cannot determine which to adopt. Delete the extra keys and re-run.`,
148
+ }),
149
+ );
150
+ });
151
+
152
+ return {
153
+ // privateKeyData is stable: it's set on create and never
154
+ // changes. The engine preserves it across reconciles and
155
+ // does not use it for drift detection.
156
+ stables: ["name", "privateKeyData", "keyAlgorithm", "keyType"],
157
+ diff: Effect.fn(function* ({ news, olds = {} }) {
158
+ if (!isResolved(news)) return undefined;
159
+ const priorSa = olds.serviceAccount as string | undefined;
160
+ const priorKeyAlgorithm = olds.keyAlgorithm as
161
+ | string
162
+ | undefined;
163
+ const priorPrivateKeyType = olds.privateKeyType as
164
+ | string
165
+ | undefined;
166
+ if (
167
+ (priorSa && news.serviceAccount !== priorSa) ||
168
+ (priorKeyAlgorithm && news.keyAlgorithm !== priorKeyAlgorithm) ||
169
+ (priorPrivateKeyType &&
170
+ news.privateKeyType !== priorPrivateKeyType)
171
+ ) {
172
+ return { action: "replace" } as const;
173
+ }
174
+ return undefined;
175
+ }),
176
+ reconcile: Effect.fn(function* ({ id, news, output }) {
177
+ // 1. Observe — check if the key exists by name (from
178
+ // state output). Fall back to listing user-managed
179
+ // keys for cold recovery.
180
+ const keyName = output?.name;
181
+ let observed: iam.ServiceAccountKey | undefined;
182
+
183
+ if (keyName) {
184
+ observed = yield* observeKey(keyName);
185
+ }
186
+ if (!observed) {
187
+ observed = yield* findUserManagedKey(news.serviceAccount);
188
+ }
189
+
190
+ // 2. Ensure — create if missing. If the key exists but
191
+ // privateKeyData is not in state (state loss), delete
192
+ // the old key and create a new one so privateKeyData is
193
+ // re-obtained.
194
+ if (!observed) {
195
+ const created = yield* createProjectsServiceAccountsKeys({
196
+ name: news.serviceAccount,
197
+ body: {
198
+ privateKeyType:
199
+ news.privateKeyType ?? "TYPE_GOOGLE_CREDENTIALS_FILE",
200
+ keyAlgorithm: news.keyAlgorithm ?? "KEY_ALG_RSA_2048",
201
+ },
202
+ });
203
+ return toAttributes(created);
204
+ }
205
+
206
+ // Key exists. If it is the same key we created before, preserve
207
+ // privateKeyData from state. If the name differs, the key was
208
+ // swapped out-of-band — fall through to delete + recreate so
209
+ // privateKeyData matches the live key.
210
+ if (output?.privateKeyData && observed.name === output.name) {
211
+ return {
212
+ ...toAttributes(observed),
213
+ privateKeyData: output.privateKeyData,
214
+ };
215
+ }
216
+
217
+ // privateKeyData is missing from state. Only delete + recreate
218
+ // when we can PROVE the observed key is ours: output.name must
219
+ // be set and match. SA keys carry no labels, so on cold recovery
220
+ // (output undefined) a found key might be pre-existing or
221
+ // manually rotated — deleting it would destroy someone else's
222
+ // credentials. Fail instead and ask the user to clean up.
223
+ if (output?.name && observed.name === output.name) {
224
+ yield* deleteProjectsServiceAccountsKeys({
225
+ name: observed.name!,
226
+ }).pipe(Effect.catchTag("NotFound", () => Effect.void));
227
+
228
+ const created = yield* createProjectsServiceAccountsKeys({
229
+ name: news.serviceAccount,
230
+ body: {
231
+ privateKeyType:
232
+ news.privateKeyType ?? "TYPE_GOOGLE_CREDENTIALS_FILE",
233
+ keyAlgorithm: news.keyAlgorithm ?? "KEY_ALG_RSA_2048",
234
+ },
235
+ });
236
+ return toAttributes(created);
237
+ }
238
+
239
+ // Key exists but we can't prove ownership. Don't delete it.
240
+ return yield* Effect.fail(
241
+ new ConfigError({
242
+ message:
243
+ `Service account key ${observed.name} exists on ${news.serviceAccount} ` +
244
+ `but cannot be verified as ours (no matching state). ` +
245
+ `Delete it manually and re-run to create a fresh key.`,
246
+ }),
247
+ );
248
+ }),
249
+ delete: Effect.fn(function* ({ output }) {
250
+ yield* deleteProjectsServiceAccountsKeys({
251
+ name: output.name,
252
+ }).pipe(
253
+ Effect.catchTag("NotFound", () => Effect.void),
254
+ Effect.catchTag("Forbidden", () => Effect.void),
255
+ );
256
+ }),
257
+ read: Effect.fn(function* ({ id, output, olds }) {
258
+ const keyName = output?.name;
259
+ const serviceAccount = olds.serviceAccount;
260
+
261
+ let observed: iam.ServiceAccountKey | undefined;
262
+ if (keyName) {
263
+ observed = yield* observeKey(keyName);
264
+ }
265
+ if (!observed && serviceAccount) {
266
+ observed = yield* findUserManagedKey(serviceAccount);
267
+ }
268
+ if (!observed) return undefined;
269
+
270
+ // privateKeyData is not available from get/list — preserve it
271
+ // from state output only when it still describes this key.
272
+ const privateKeyData =
273
+ output?.name && observed.name === output.name
274
+ ? output.privateKeyData
275
+ : undefined;
276
+
277
+ return {
278
+ ...toAttributes(observed),
279
+ privateKeyData,
280
+ };
281
+ }),
282
+ };
283
+ }),
284
+ );
package/src/Iam/index.ts CHANGED
@@ -1,3 +1,8 @@
1
1
  export { ServiceAccount, ServiceAccountProvider } from "./ServiceAccount.ts";
2
2
  export type { ServiceAccountProps } from "./ServiceAccount.ts";
3
3
  export * from "./ServiceAccountIamMember.ts";
4
+ export {
5
+ ServiceAccountKey,
6
+ ServiceAccountKeyProvider,
7
+ } from "./ServiceAccountKey.ts";
8
+ export type { ServiceAccountKeyProps } from "./ServiceAccountKey.ts";
package/src/Providers.ts CHANGED
@@ -27,6 +27,10 @@ import {
27
27
  ServiceAccount,
28
28
  ServiceAccountProvider,
29
29
  } from "./Iam/ServiceAccount.ts";
30
+ import {
31
+ ServiceAccountKey,
32
+ ServiceAccountKeyProvider,
33
+ } from "./Iam/ServiceAccountKey.ts";
30
34
  import {
31
35
  KubernetesManifest,
32
36
  KubernetesManifestProvider,
@@ -45,6 +49,10 @@ import {
45
49
  PsaConnection,
46
50
  PsaConnectionProvider,
47
51
  } from "./ServiceNetworking/PsaConnection.ts";
52
+ import {
53
+ StorageBucket,
54
+ StorageBucketProvider,
55
+ } from "./Storage/Bucket.ts";
48
56
  import { ApiEnable, ApiEnableProvider } from "./ServiceUsage/ApiEnable.ts";
49
57
  import { SqlDatabase, SqlDatabaseProvider } from "./Sqladmin/Database.ts";
50
58
  import { SqlInstance, SqlInstanceProvider } from "./Sqladmin/Instance.ts";
@@ -87,6 +95,7 @@ export const providers = () =>
87
95
  SharedVpcServiceProject,
88
96
  PsaConnection,
89
97
  ManagedLustreInstance,
98
+ StorageBucket,
90
99
  Service,
91
100
  Job,
92
101
  SqlInstance,
@@ -94,6 +103,7 @@ export const providers = () =>
94
103
  SqlUser,
95
104
  ArtifactRegistryRepository,
96
105
  ServiceAccount,
106
+ ServiceAccountKey,
97
107
  KubernetesSecret,
98
108
  KubernetesManifest,
99
109
  ]),
@@ -114,6 +124,7 @@ export const providers = () =>
114
124
  SharedVpcServiceProjectProvider(),
115
125
  PsaConnectionProvider(),
116
126
  ManagedLustreInstanceProvider(),
127
+ StorageBucketProvider(),
117
128
  ServiceProvider(),
118
129
  JobProvider(),
119
130
  SqlInstanceProvider(),
@@ -121,6 +132,7 @@ export const providers = () =>
121
132
  SqlUserProvider(),
122
133
  ArtifactRegistryRepositoryProvider(),
123
134
  ServiceAccountProvider(),
135
+ ServiceAccountKeyProvider(),
124
136
  KubernetesSecretProvider(),
125
137
  KubernetesManifestProvider(),
126
138
  ),