@microagi/alchemy-gcp 0.3.0 → 0.5.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 (91) hide show
  1. package/CHANGELOG.md +124 -0
  2. package/lib/ArtifactRegistry/Operations.d.ts +47 -0
  3. package/lib/ArtifactRegistry/Operations.d.ts.map +1 -0
  4. package/lib/ArtifactRegistry/Operations.js +47 -0
  5. package/lib/ArtifactRegistry/Operations.js.map +1 -0
  6. package/lib/ArtifactRegistry/Repository.d.ts +263 -0
  7. package/lib/ArtifactRegistry/Repository.d.ts.map +1 -0
  8. package/lib/ArtifactRegistry/Repository.js +256 -0
  9. package/lib/ArtifactRegistry/Repository.js.map +1 -0
  10. package/lib/ArtifactRegistry/Types.d.ts +32 -0
  11. package/lib/ArtifactRegistry/Types.d.ts.map +1 -0
  12. package/lib/ArtifactRegistry/Types.js +12 -0
  13. package/lib/ArtifactRegistry/Types.js.map +1 -0
  14. package/lib/ArtifactRegistry/Validation.d.ts +43 -0
  15. package/lib/ArtifactRegistry/Validation.d.ts.map +1 -0
  16. package/lib/ArtifactRegistry/Validation.js +95 -0
  17. package/lib/ArtifactRegistry/Validation.js.map +1 -0
  18. package/lib/ArtifactRegistry/index.d.ts +4 -0
  19. package/lib/ArtifactRegistry/index.d.ts.map +1 -0
  20. package/lib/ArtifactRegistry/index.js +2 -0
  21. package/lib/ArtifactRegistry/index.js.map +1 -0
  22. package/lib/Auth/Credentials.d.ts +2 -1
  23. package/lib/Auth/Credentials.d.ts.map +1 -1
  24. package/lib/Auth/Credentials.js +3 -2
  25. package/lib/Auth/Credentials.js.map +1 -1
  26. package/lib/CloudResourceManager/Project.d.ts +1 -1
  27. package/lib/Compute/GlobalAddress.d.ts +1 -1
  28. package/lib/Compute/Network.d.ts +1 -1
  29. package/lib/Compute/SharedVpcHost.d.ts +1 -1
  30. package/lib/Compute/SharedVpcServiceProject.d.ts +1 -1
  31. package/lib/Compute/Subnetwork.d.ts +1 -1
  32. package/lib/Container/Cluster.d.ts +1 -1
  33. package/lib/Container/NodePool.d.ts +1 -1
  34. package/lib/ManagedLustre/Instance.d.ts +1 -1
  35. package/lib/Providers.d.ts +1 -1
  36. package/lib/Providers.d.ts.map +1 -1
  37. package/lib/Providers.js +9 -1
  38. package/lib/Providers.js.map +1 -1
  39. package/lib/Run/Job.d.ts +1 -1
  40. package/lib/Run/Service.d.ts +1 -1
  41. package/lib/ServiceNetworking/PsaConnection.d.ts +1 -1
  42. package/lib/ServiceUsage/ApiEnable.d.ts +1 -1
  43. package/lib/Sqladmin/Database.d.ts +82 -0
  44. package/lib/Sqladmin/Database.d.ts.map +1 -0
  45. package/lib/Sqladmin/Database.js +87 -0
  46. package/lib/Sqladmin/Database.js.map +1 -0
  47. package/lib/Sqladmin/Instance.d.ts +234 -0
  48. package/lib/Sqladmin/Instance.d.ts.map +1 -0
  49. package/lib/Sqladmin/Instance.js +251 -0
  50. package/lib/Sqladmin/Instance.js.map +1 -0
  51. package/lib/Sqladmin/Operations.d.ts +41 -0
  52. package/lib/Sqladmin/Operations.d.ts.map +1 -0
  53. package/lib/Sqladmin/Operations.js +42 -0
  54. package/lib/Sqladmin/Operations.js.map +1 -0
  55. package/lib/Sqladmin/Types.d.ts +82 -0
  56. package/lib/Sqladmin/Types.d.ts.map +1 -0
  57. package/lib/Sqladmin/Types.js +28 -0
  58. package/lib/Sqladmin/Types.js.map +1 -0
  59. package/lib/Sqladmin/User.d.ts +110 -0
  60. package/lib/Sqladmin/User.d.ts.map +1 -0
  61. package/lib/Sqladmin/User.js +156 -0
  62. package/lib/Sqladmin/User.js.map +1 -0
  63. package/lib/Sqladmin/Validation.d.ts +60 -0
  64. package/lib/Sqladmin/Validation.d.ts.map +1 -0
  65. package/lib/Sqladmin/Validation.js +125 -0
  66. package/lib/Sqladmin/Validation.js.map +1 -0
  67. package/lib/Sqladmin/index.d.ts +9 -0
  68. package/lib/Sqladmin/index.d.ts.map +1 -0
  69. package/lib/Sqladmin/index.js +5 -0
  70. package/lib/Sqladmin/index.js.map +1 -0
  71. package/lib/Tags.d.ts +4 -4
  72. package/lib/index.d.ts +2 -0
  73. package/lib/index.d.ts.map +1 -1
  74. package/lib/index.js +2 -0
  75. package/lib/index.js.map +1 -1
  76. package/package.json +2 -2
  77. package/src/ArtifactRegistry/Operations.ts +79 -0
  78. package/src/ArtifactRegistry/Repository.ts +628 -0
  79. package/src/ArtifactRegistry/Types.ts +49 -0
  80. package/src/ArtifactRegistry/Validation.ts +112 -0
  81. package/src/ArtifactRegistry/index.ts +10 -0
  82. package/src/Auth/Credentials.ts +3 -2
  83. package/src/Providers.ts +15 -0
  84. package/src/Sqladmin/Database.ts +209 -0
  85. package/src/Sqladmin/Instance.ts +620 -0
  86. package/src/Sqladmin/Operations.ts +73 -0
  87. package/src/Sqladmin/Types.ts +101 -0
  88. package/src/Sqladmin/User.ts +333 -0
  89. package/src/Sqladmin/Validation.ts +148 -0
  90. package/src/Sqladmin/index.ts +19 -0
  91. package/src/index.ts +2 -0
@@ -0,0 +1,628 @@
1
+ import * as ar from "@distilled.cloud/gcp/artifactregistry-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 { diffTags } from "alchemy/Tags";
9
+ import * as Duration from "effect/Duration";
10
+ import * as Effect from "effect/Effect";
11
+ import * as Schedule from "effect/Schedule";
12
+ import type * as GCP from "../Providers.ts";
13
+ import { gcpInternalLabels, hasAlchemyLabels } from "../Tags.ts";
14
+ import { makeAwaitOperation } from "./Operations.ts";
15
+ import type { ArtifactRegistryFormat, ArtifactRegistryMode } from "./Types.ts";
16
+ import {
17
+ reshapeBadRequest,
18
+ validateModeConfig,
19
+ validateRepositoryName,
20
+ } from "./Validation.ts";
21
+
22
+ /**
23
+ * Docker remote upstream — either a Google-known public repository
24
+ * (today: Docker Hub) or an arbitrary HTTPS Docker registry like
25
+ * `https://nvcr.io` or `https://quay.io`. Discriminated union — the
26
+ * server rejects bodies that supply both. Matches
27
+ * `ar.DockerRepository`.
28
+ *
29
+ * @see https://cloud.google.com/artifact-registry/docs/reference/rest/v1/projects.locations.repositories#DockerRepository
30
+ */
31
+ export type ArtifactRegistryDockerUpstream =
32
+ | {
33
+ /**
34
+ * One of Artifact Registry's built-in public-repository
35
+ * presets. Today only `DOCKER_HUB` is meaningful for the docker
36
+ * format. The string-tail is left open for forward compat with
37
+ * any future presets the server adds.
38
+ */
39
+ publicRepository: "DOCKER_HUB" | (string & {});
40
+ customRepository?: never;
41
+ }
42
+ | {
43
+ /**
44
+ * Arbitrary HTTPS Docker upstream — `https://nvcr.io`,
45
+ * `https://quay.io`, a self-hosted Harbor, etc. The server
46
+ * trims any path / pulls the manifest endpoint conventionally.
47
+ */
48
+ customRepository: { uri: string };
49
+ publicRepository?: never;
50
+ };
51
+
52
+ /**
53
+ * A Google Artifact Registry **Repository** of `format: "DOCKER"` —
54
+ * either a standard upload-target (`STANDARD_REPOSITORY`) for the
55
+ * project's own container builds, or a pull-through cache
56
+ * (`REMOTE_REPOSITORY`) pointing at Docker Hub or a custom upstream
57
+ * like `nvcr.io`.
58
+ *
59
+ * **Resource model.** A repository lives at
60
+ * `projects/{project}/locations/{location}/repositories/{name}`.
61
+ * Format and mode are immutable; the upstream config for a remote
62
+ * repository is also immutable on the GCP side (any change triggers a
63
+ * replace).
64
+ *
65
+ * **Lifecycle.** observe → ensure (create LRO; retry on the
66
+ * "API not yet enabled" 403 propagation race) → sync (patch
67
+ * description, labels, dockerConfig, cleanup policies via
68
+ * `updateMask`) → return. Mirrors `Sqladmin/Instance.ts` /
69
+ * `Run/Service.ts`.
70
+ *
71
+ * **Replace triggers.** `project`, `location`, `name`, `format`,
72
+ * `mode`, `kmsKeyName`, and any change inside `remoteRepositoryConfig`.
73
+ *
74
+ * **Adoption.** Label-gated via `alchemy_*` labels. A repository
75
+ * whose `labels` lack our keys is wrapped in `Unowned(attrs)` from
76
+ * `read`, forcing `--adopt` before takeover.
77
+ *
78
+ * @example Standard Docker repo for the project's own builds
79
+ * ```typescript
80
+ * const apps = yield* GCP.ArtifactRegistryRepository("Apps", {
81
+ * project: project.projectId,
82
+ * location: "europe-west4",
83
+ * format: "DOCKER",
84
+ * description: "App images built by CI",
85
+ * });
86
+ * ```
87
+ *
88
+ * @example Pull-through cache for Docker Hub
89
+ * ```typescript
90
+ * yield* GCP.ArtifactRegistryRepository("DockerHubMirror", {
91
+ * project: project.projectId,
92
+ * location: "europe-west4",
93
+ * format: "DOCKER",
94
+ * mode: "REMOTE_REPOSITORY",
95
+ * remoteRepositoryConfig: {
96
+ * description: "Pull-through cache for docker.io",
97
+ * dockerRepository: { publicRepository: "DOCKER_HUB" },
98
+ * },
99
+ * });
100
+ * ```
101
+ *
102
+ * @example Pull-through cache for nvcr.io
103
+ * ```typescript
104
+ * yield* GCP.ArtifactRegistryRepository("NvcrMirror", {
105
+ * project: project.projectId,
106
+ * location: "europe-west4",
107
+ * format: "DOCKER",
108
+ * mode: "REMOTE_REPOSITORY",
109
+ * remoteRepositoryConfig: {
110
+ * description: "Pull-through cache for nvcr.io",
111
+ * dockerRepository: { customRepository: { uri: "https://nvcr.io" } },
112
+ * },
113
+ * });
114
+ * ```
115
+ *
116
+ * @see https://cloud.google.com/artifact-registry/docs/reference/rest/v1/projects.locations.repositories#Repository
117
+ */
118
+ export type ArtifactRegistryRepositoryProps = {
119
+ /**
120
+ * GCP project ID hosting the repository. Immutable — replace if
121
+ * changed.
122
+ * @see https://cloud.google.com/artifact-registry/docs/reference/rest/v1/projects.locations.repositories#Repository
123
+ */
124
+ project: string;
125
+ /**
126
+ * Artifact Registry location (e.g. `europe-west4`). Regional only —
127
+ * multi-region is not supported by this resource. Immutable.
128
+ * @see https://cloud.google.com/artifact-registry/docs/repositories/repo-locations
129
+ */
130
+ location: string;
131
+ /**
132
+ * Repository short name (the trailing path segment under
133
+ * `…/repositories/`). Defaults to a sanitised
134
+ * `createPhysicalName({ id, lowercase: true, maxLength: 63 })`.
135
+ * Lowercase letters/digits/hyphens; start with a letter or digit;
136
+ * 1–63 chars. Immutable — replace if changed.
137
+ * @see https://cloud.google.com/artifact-registry/docs/repositories/create-repos#create
138
+ */
139
+ name?: string;
140
+ /**
141
+ * Package format. Today the provider exercises only `DOCKER`; other
142
+ * formats are reachable via the open-tail union but un-tested.
143
+ * Immutable — replace if changed.
144
+ * @see https://cloud.google.com/artifact-registry/docs/reference/rest/v1/projects.locations.repositories#Format
145
+ */
146
+ format: ArtifactRegistryFormat;
147
+ /**
148
+ * Repository mode. Defaults to `STANDARD_REPOSITORY`. Set to
149
+ * `REMOTE_REPOSITORY` to act as a pull-through cache for an upstream
150
+ * Docker registry; in that case `remoteRepositoryConfig` is
151
+ * required. Immutable — replace if changed (GCP does not allow
152
+ * flipping a repository between modes).
153
+ * @see https://cloud.google.com/artifact-registry/docs/reference/rest/v1/projects.locations.repositories#Mode
154
+ */
155
+ mode?: ArtifactRegistryMode;
156
+ /**
157
+ * Human-readable description. Mutable via `patch` with
158
+ * `updateMask=description`.
159
+ * @see https://cloud.google.com/artifact-registry/docs/reference/rest/v1/projects.locations.repositories#Repository.FIELDS.description
160
+ */
161
+ description?: string;
162
+ /**
163
+ * Resource labels. Alchemy internal labels (`alchemy_app`,
164
+ * `alchemy_stage`, `alchemy_id`) are merged on top automatically.
165
+ * Mutable via `patch` with `updateMask=labels`.
166
+ * @see https://cloud.google.com/artifact-registry/docs/reference/rest/v1/projects.locations.repositories#Repository.FIELDS.labels
167
+ */
168
+ labels?: Record<string, string>;
169
+ /**
170
+ * Customer-managed encryption key, fully-qualified
171
+ * (`projects/{p}/locations/{l}/keyRings/{kr}/cryptoKeys/{k}`).
172
+ * Immutable on the GCP side — we surface that as a replace trigger.
173
+ * Requires the AR service agent to hold
174
+ * `roles/cloudkms.cryptoKeyEncrypterDecrypter` on the key.
175
+ * @see https://cloud.google.com/artifact-registry/docs/cmek
176
+ */
177
+ kmsKeyName?: string;
178
+ /**
179
+ * Docker-format repository configuration. Mutable via `patch` with
180
+ * `updateMask=dockerConfig`.
181
+ * @see https://cloud.google.com/artifact-registry/docs/reference/rest/v1/projects.locations.repositories#DockerRepositoryConfig
182
+ */
183
+ dockerConfig?: {
184
+ /**
185
+ * When true, image tags inside this repository cannot be moved or
186
+ * deleted once written. Useful for supply-chain hardening.
187
+ */
188
+ immutableTags?: boolean;
189
+ };
190
+ /**
191
+ * Remote-repository configuration. Required (and only meaningful)
192
+ * when `mode === "REMOTE_REPOSITORY"`. Immutable on the GCP side —
193
+ * any change to fields under this object triggers a replace.
194
+ * @see https://cloud.google.com/artifact-registry/docs/reference/rest/v1/projects.locations.repositories#RemoteRepositoryConfig
195
+ */
196
+ remoteRepositoryConfig?: {
197
+ /**
198
+ * Description of the remote source (distinct from the top-level
199
+ * `description`).
200
+ */
201
+ description?: string;
202
+ /** Docker upstream — public preset or arbitrary HTTPS URI. */
203
+ dockerRepository?: ArtifactRegistryDockerUpstream;
204
+ /**
205
+ * If true, AR skips the create-time HEAD request against the
206
+ * upstream. Useful for upstreams that gate unauthenticated probes
207
+ * (private nvcr.io paths, etc.).
208
+ */
209
+ disableUpstreamValidation?: boolean;
210
+ /**
211
+ * Credentials for authenticated upstreams — supply a Secret
212
+ * Manager version reference for the password rather than the
213
+ * cleartext value.
214
+ */
215
+ upstreamCredentials?: {
216
+ usernamePasswordCredentials?: {
217
+ username?: string;
218
+ /**
219
+ * Secret Manager secret version path:
220
+ * `projects/{p}/secrets/{s}/versions/{v}`.
221
+ */
222
+ passwordSecretVersion?: string;
223
+ };
224
+ };
225
+ };
226
+ /**
227
+ * Cleanup policies — map of policy ID → policy spec. Pass-through
228
+ * to the API today; the policy types are accepted via the
229
+ * distilled SDK's `CleanupPolicy` shape but not specifically
230
+ * validated by this provider. Mutable via `patch` with
231
+ * `updateMask=cleanupPolicies`.
232
+ * @see https://cloud.google.com/artifact-registry/docs/repositories/cleanup-policy
233
+ */
234
+ cleanupPolicies?: Record<string, ar.CleanupPolicy>;
235
+ /**
236
+ * If true, cleanup policies are evaluated and reported but no
237
+ * versions are actually deleted. Mutable via `patch` with
238
+ * `updateMask=cleanupPolicyDryRun`.
239
+ * @see https://cloud.google.com/artifact-registry/docs/repositories/cleanup-policy#test
240
+ */
241
+ cleanupPolicyDryRun?: boolean;
242
+ };
243
+
244
+ export type ArtifactRegistryRepositoryAttributes = {
245
+ /** Bare repository name (no `projects/.../repositories/` prefix). */
246
+ name: string;
247
+ /**
248
+ * Server-side fully-qualified name:
249
+ * `projects/{project}/locations/{location}/repositories/{name}`.
250
+ */
251
+ fullyQualifiedName: string;
252
+ /** GCP project ID. */
253
+ project: string;
254
+ /** Region. */
255
+ location: string;
256
+ /** Echoed format (e.g. `DOCKER`). */
257
+ format: string;
258
+ /** Echoed mode (e.g. `STANDARD_REPOSITORY`). */
259
+ mode: string;
260
+ /**
261
+ * Repository endpoint (e.g. `europe-west4-docker.pkg.dev/proj/repo`)
262
+ * — the host:path you'd `docker pull` from. Populated by the server.
263
+ */
264
+ registryUri: string | undefined;
265
+ /** Total stored size in bytes, server-reported. */
266
+ sizeBytes: string | undefined;
267
+ /** PZS compliance flag. */
268
+ satisfiesPzs: boolean | undefined;
269
+ /** PZI compliance flag. */
270
+ satisfiesPzi: boolean | undefined;
271
+ /** Labels currently set, including alchemy internals. */
272
+ labels: Record<string, string>;
273
+ /** RFC3339 create timestamp. */
274
+ createTime: string;
275
+ /** RFC3339 last-update timestamp. */
276
+ updateTime: string;
277
+ };
278
+
279
+ export type ArtifactRegistryRepository = Resource<
280
+ "GCP.ArtifactRegistryRepository",
281
+ ArtifactRegistryRepositoryProps,
282
+ ArtifactRegistryRepositoryAttributes,
283
+ never,
284
+ GCP.Providers
285
+ >;
286
+ export const ArtifactRegistryRepository =
287
+ Resource<ArtifactRegistryRepository>("GCP.ArtifactRegistryRepository");
288
+
289
+ const fqName = (project: string, location: string, name: string): string =>
290
+ `projects/${project}/locations/${location}/repositories/${name}`;
291
+
292
+ const parent = (project: string, location: string): string =>
293
+ `projects/${project}/locations/${location}`;
294
+
295
+ const toDockerRepositoryBody = (
296
+ upstream: ArtifactRegistryDockerUpstream,
297
+ ): ar.DockerRepository => {
298
+ if (upstream.publicRepository !== undefined) {
299
+ return { publicRepository: upstream.publicRepository };
300
+ }
301
+ if (upstream.customRepository !== undefined) {
302
+ return { customRepository: { uri: upstream.customRepository.uri } };
303
+ }
304
+ // Unreachable per the discriminated union, but validation also
305
+ // rejects an empty dockerRepository at plan time.
306
+ return {};
307
+ };
308
+
309
+ const toRemoteRepositoryConfigBody = (
310
+ cfg: NonNullable<ArtifactRegistryRepositoryProps["remoteRepositoryConfig"]>,
311
+ ): ar.RemoteRepositoryConfig => ({
312
+ ...(cfg.description !== undefined ? { description: cfg.description } : {}),
313
+ ...(cfg.dockerRepository
314
+ ? { dockerRepository: toDockerRepositoryBody(cfg.dockerRepository) }
315
+ : {}),
316
+ ...(cfg.disableUpstreamValidation !== undefined
317
+ ? { disableUpstreamValidation: cfg.disableUpstreamValidation }
318
+ : {}),
319
+ ...(cfg.upstreamCredentials
320
+ ? {
321
+ upstreamCredentials: {
322
+ ...(cfg.upstreamCredentials.usernamePasswordCredentials
323
+ ? {
324
+ usernamePasswordCredentials: {
325
+ ...(cfg.upstreamCredentials.usernamePasswordCredentials
326
+ .username !== undefined
327
+ ? {
328
+ username:
329
+ cfg.upstreamCredentials.usernamePasswordCredentials
330
+ .username,
331
+ }
332
+ : {}),
333
+ ...(cfg.upstreamCredentials.usernamePasswordCredentials
334
+ .passwordSecretVersion !== undefined
335
+ ? {
336
+ passwordSecretVersion:
337
+ cfg.upstreamCredentials.usernamePasswordCredentials
338
+ .passwordSecretVersion,
339
+ }
340
+ : {}),
341
+ },
342
+ }
343
+ : {}),
344
+ },
345
+ }
346
+ : {}),
347
+ });
348
+
349
+ const toRepositoryBody = (
350
+ props: ArtifactRegistryRepositoryProps,
351
+ desiredLabels: Record<string, string>,
352
+ ): ar.Repository => ({
353
+ format: props.format,
354
+ ...(props.mode ? { mode: props.mode } : {}),
355
+ ...(props.description !== undefined
356
+ ? { description: props.description }
357
+ : {}),
358
+ labels: desiredLabels,
359
+ ...(props.kmsKeyName ? { kmsKeyName: props.kmsKeyName } : {}),
360
+ ...(props.dockerConfig
361
+ ? {
362
+ dockerConfig: {
363
+ ...(props.dockerConfig.immutableTags !== undefined
364
+ ? { immutableTags: props.dockerConfig.immutableTags }
365
+ : {}),
366
+ },
367
+ }
368
+ : {}),
369
+ ...(props.remoteRepositoryConfig
370
+ ? {
371
+ remoteRepositoryConfig: toRemoteRepositoryConfigBody(
372
+ props.remoteRepositoryConfig,
373
+ ),
374
+ }
375
+ : {}),
376
+ ...(props.cleanupPolicies
377
+ ? { cleanupPolicies: props.cleanupPolicies }
378
+ : {}),
379
+ ...(props.cleanupPolicyDryRun !== undefined
380
+ ? { cleanupPolicyDryRun: props.cleanupPolicyDryRun }
381
+ : {}),
382
+ });
383
+
384
+ const toAttributes = (
385
+ r: ar.Repository,
386
+ parentArgs: { project: string; location: string; name: string },
387
+ ): ArtifactRegistryRepositoryAttributes => ({
388
+ name: parentArgs.name,
389
+ fullyQualifiedName: fqName(
390
+ parentArgs.project,
391
+ parentArgs.location,
392
+ parentArgs.name,
393
+ ),
394
+ project: parentArgs.project,
395
+ location: parentArgs.location,
396
+ format: r.format ?? "",
397
+ mode: r.mode ?? "STANDARD_REPOSITORY",
398
+ registryUri: r.registryUri,
399
+ sizeBytes: r.sizeBytes,
400
+ satisfiesPzs: r.satisfiesPzs,
401
+ satisfiesPzi: r.satisfiesPzi,
402
+ labels: { ...(r.labels ?? {}) },
403
+ createTime: r.createTime ?? "",
404
+ updateTime: r.updateTime ?? "",
405
+ });
406
+
407
+ export const ArtifactRegistryRepositoryProvider = () =>
408
+ Provider.effect(
409
+ ArtifactRegistryRepository,
410
+ Effect.gen(function* () {
411
+ const getRepository = yield* ar.getProjectsLocationsRepositories;
412
+ const createRepository = yield* ar.createProjectsLocationsRepositories;
413
+ const patchRepository = yield* ar.patchProjectsLocationsRepositories;
414
+ const deleteRepository = yield* ar.deleteProjectsLocationsRepositories;
415
+ const getOperations = yield* ar.getProjectsLocationsOperations;
416
+ const awaitOperation = makeAwaitOperation(getOperations);
417
+
418
+ const observe = (project: string, location: string, name: string) =>
419
+ getRepository({ name: fqName(project, location, name) }).pipe(
420
+ Effect.catchTag("NotFound", () =>
421
+ Effect.succeed(undefined as ar.Repository | undefined),
422
+ ),
423
+ Effect.catchTag("Forbidden", () =>
424
+ Effect.succeed(undefined as ar.Repository | undefined),
425
+ ),
426
+ );
427
+
428
+ const syncMutable = Effect.fn(function* (args: {
429
+ name: string;
430
+ observed: ar.Repository;
431
+ news: ArtifactRegistryRepositoryProps;
432
+ desiredLabels: Record<string, string>;
433
+ session: ScopedPlanStatusSession;
434
+ }) {
435
+ const fields: string[] = [];
436
+ const body: ar.Repository = {};
437
+
438
+ if ((args.observed.description ?? "") !== (args.news.description ?? "")) {
439
+ fields.push("description");
440
+ body.description = args.news.description ?? "";
441
+ }
442
+
443
+ const labelDiff = diffTags(
444
+ { ...(args.observed.labels ?? {}) },
445
+ args.desiredLabels,
446
+ );
447
+ if (labelDiff.removed.length > 0 || labelDiff.upsert.length > 0) {
448
+ fields.push("labels");
449
+ body.labels = args.desiredLabels;
450
+ }
451
+
452
+ const desiredDockerConfig = args.news.dockerConfig
453
+ ? {
454
+ ...(args.news.dockerConfig.immutableTags !== undefined
455
+ ? { immutableTags: args.news.dockerConfig.immutableTags }
456
+ : {}),
457
+ }
458
+ : undefined;
459
+ if (!deepEqual(args.observed.dockerConfig, desiredDockerConfig)) {
460
+ fields.push("dockerConfig");
461
+ if (desiredDockerConfig) body.dockerConfig = desiredDockerConfig;
462
+ }
463
+
464
+ if (
465
+ !deepEqual(
466
+ args.observed.cleanupPolicies ?? {},
467
+ args.news.cleanupPolicies ?? {},
468
+ )
469
+ ) {
470
+ fields.push("cleanupPolicies");
471
+ if (args.news.cleanupPolicies)
472
+ body.cleanupPolicies = args.news.cleanupPolicies;
473
+ }
474
+
475
+ if (
476
+ (args.observed.cleanupPolicyDryRun ?? false) !==
477
+ (args.news.cleanupPolicyDryRun ?? false)
478
+ ) {
479
+ fields.push("cleanupPolicyDryRun");
480
+ body.cleanupPolicyDryRun = args.news.cleanupPolicyDryRun ?? false;
481
+ }
482
+
483
+ if (fields.length === 0) return;
484
+
485
+ // Patch returns the Repository synchronously (NOT an LRO per
486
+ // `PatchProjectsLocationsRepositoriesResponse = Repository`).
487
+ // No operation to await.
488
+ yield* patchRepository({
489
+ name: args.name,
490
+ updateMask: fields.join(","),
491
+ body,
492
+ }).pipe(Effect.catchTag("BadRequest", reshapeBadRequest("patch")));
493
+ });
494
+
495
+ return {
496
+ stables: [
497
+ "name",
498
+ "fullyQualifiedName",
499
+ "project",
500
+ "location",
501
+ "format",
502
+ "mode",
503
+ ],
504
+ diff: Effect.fn(function* ({ news, olds = {} }) {
505
+ if (!isResolved(news)) return undefined;
506
+ // String-typed stables are reference-comparable.
507
+ if (
508
+ somePropsAreDifferent(
509
+ olds as ArtifactRegistryRepositoryProps,
510
+ news,
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,
523
+ )
524
+ ) {
525
+ return { action: "replace" } as const;
526
+ }
527
+ return undefined;
528
+ }),
529
+ reconcile: Effect.fn(function* ({ id, news, session }) {
530
+ const internalLabels = yield* gcpInternalLabels(id);
531
+ const desiredName =
532
+ news.name ??
533
+ (yield* createPhysicalName({ id, maxLength: 63 })).toLowerCase();
534
+
535
+ yield* validateRepositoryName(desiredName);
536
+ yield* validateModeConfig(news);
537
+
538
+ const desiredLabels: Record<string, string> = {
539
+ ...(news.labels ?? {}),
540
+ ...internalLabels,
541
+ };
542
+ const name = fqName(news.project, news.location, desiredName);
543
+
544
+ // 1. Observe — collapse 403 to "missing" alongside 404.
545
+ let observed = yield* observe(news.project, news.location, desiredName);
546
+
547
+ // 2. Ensure — create LRO. Same API-enable race as Cloud Run /
548
+ // Cloud SQL: a create can fire before ApiEnable's effect
549
+ // propagates and the server returns 403 with "If you
550
+ // enabled this API recently, wait …". Retry that specific
551
+ // 403 for ~5 min; other Forbiddens propagate.
552
+ if (!observed) {
553
+ const op = yield* createRepository({
554
+ parent: parent(news.project, news.location),
555
+ repositoryId: desiredName,
556
+ body: toRepositoryBody(news, desiredLabels),
557
+ }).pipe(
558
+ Effect.retry({
559
+ while: (e: { _tag?: string; message?: string }) =>
560
+ e?._tag === "Forbidden" &&
561
+ /enabled this API recently|has not been used/i.test(
562
+ e.message ?? "",
563
+ ),
564
+ schedule: Schedule.spaced(Duration.seconds(15)).pipe(
565
+ Schedule.both(Schedule.recurs(20)),
566
+ Schedule.tapOutput(() =>
567
+ session.note(
568
+ "Waiting for Artifact Registry API enablement to propagate…",
569
+ ),
570
+ ),
571
+ ),
572
+ }),
573
+ Effect.catchTag("Conflict", () =>
574
+ Effect.succeed(undefined as ar.Operation | undefined),
575
+ ),
576
+ Effect.catchTag("BadRequest", reshapeBadRequest("create")),
577
+ );
578
+ if (op?.name) yield* awaitOperation(op.name, session);
579
+ observed = yield* getRepository({ name });
580
+ }
581
+
582
+ // 3. Sync — single patch with an updateMask of changed
583
+ // top-level fields. `remoteRepositoryConfig` is NOT
584
+ // patched here: GCP refuses updates to it, so changes
585
+ // trigger a replace via `diff` above.
586
+ yield* syncMutable({
587
+ name,
588
+ observed,
589
+ news,
590
+ desiredLabels,
591
+ session,
592
+ });
593
+
594
+ const final = yield* getRepository({ name });
595
+ return toAttributes(final, {
596
+ project: news.project,
597
+ location: news.location,
598
+ name: desiredName,
599
+ });
600
+ }),
601
+ delete: Effect.fn(function* ({ output, session }) {
602
+ const name = fqName(output.project, output.location, output.name);
603
+ yield* deleteRepository({ name }).pipe(
604
+ Effect.flatMap((op) =>
605
+ op.name ? awaitOperation(op.name, session) : Effect.succeed(op),
606
+ ),
607
+ Effect.catchTag("NotFound", () => Effect.void),
608
+ Effect.catchTag("BadRequest", reshapeBadRequest("delete")),
609
+ );
610
+ }),
611
+ read: Effect.fn(function* ({ id, output, olds }) {
612
+ const project = output?.project ?? olds?.project;
613
+ const location = output?.location ?? olds?.location;
614
+ if (!project || !location) return undefined;
615
+ const name =
616
+ output?.name ??
617
+ olds?.name ??
618
+ (yield* createPhysicalName({ id, maxLength: 63 })).toLowerCase();
619
+ const observed = yield* observe(project, location, name);
620
+ if (!observed) return undefined;
621
+ const attrs = toAttributes(observed, { project, location, name });
622
+ return (yield* hasAlchemyLabels(id, observed.labels))
623
+ ? attrs
624
+ : Unowned(attrs);
625
+ }),
626
+ };
627
+ }),
628
+ );
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Branded literal-union types for Artifact Registry repository props.
3
+ *
4
+ * Distilled's generated `format` and `mode` fields are open-tailed
5
+ * unions (`"DOCKER" | … | (string & {})`) so the GCP server can ship
6
+ * new variants without breaking SDK consumers. We mirror that shape
7
+ * here so consumers get autocomplete for the values we expect to use
8
+ * while still leaving the tail open for forward compatibility.
9
+ *
10
+ * @see https://cloud.google.com/artifact-registry/docs/reference/rest/v1/projects.locations.repositories#Repository
11
+ */
12
+
13
+ /**
14
+ * Repository package format. Artifact Registry supports many formats
15
+ * (Maven, NPM, APT, …); we keep the full enum-tail open but only
16
+ * exercise `DOCKER` in this provider today. Other formats are still
17
+ * reachable via the `(string & {})` tail.
18
+ *
19
+ * @see https://cloud.google.com/artifact-registry/docs/reference/rest/v1/projects.locations.repositories#Format
20
+ */
21
+ export type ArtifactRegistryFormat =
22
+ | "DOCKER"
23
+ | "MAVEN"
24
+ | "NPM"
25
+ | "APT"
26
+ | "YUM"
27
+ | "GOOGET"
28
+ | "PYTHON"
29
+ | "KFP"
30
+ | "GO"
31
+ | "GENERIC"
32
+ | "RUBY"
33
+ | (string & {});
34
+
35
+ /**
36
+ * Repository mode. `STANDARD_REPOSITORY` hosts artifacts uploaded by
37
+ * the user; `REMOTE_REPOSITORY` is a pull-through cache for an
38
+ * upstream registry; `VIRTUAL_REPOSITORY` aggregates multiple upstreams
39
+ * behind one URL. The provider does not type-check `VIRTUAL_REPOSITORY`
40
+ * config today — pass it through via the `remoteRepositoryConfig`
41
+ * field with a mode override, or wait for explicit support.
42
+ *
43
+ * @see https://cloud.google.com/artifact-registry/docs/reference/rest/v1/projects.locations.repositories#Mode
44
+ */
45
+ export type ArtifactRegistryMode =
46
+ | "STANDARD_REPOSITORY"
47
+ | "REMOTE_REPOSITORY"
48
+ | "VIRTUAL_REPOSITORY"
49
+ | (string & {});