@microagi/alchemy-gcp 0.3.0 → 0.5.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.
- package/CHANGELOG.md +100 -0
- package/lib/ArtifactRegistry/Operations.d.ts +47 -0
- package/lib/ArtifactRegistry/Operations.d.ts.map +1 -0
- package/lib/ArtifactRegistry/Operations.js +47 -0
- package/lib/ArtifactRegistry/Operations.js.map +1 -0
- package/lib/ArtifactRegistry/Repository.d.ts +263 -0
- package/lib/ArtifactRegistry/Repository.d.ts.map +1 -0
- package/lib/ArtifactRegistry/Repository.js +257 -0
- package/lib/ArtifactRegistry/Repository.js.map +1 -0
- package/lib/ArtifactRegistry/Types.d.ts +32 -0
- package/lib/ArtifactRegistry/Types.d.ts.map +1 -0
- package/lib/ArtifactRegistry/Types.js +12 -0
- package/lib/ArtifactRegistry/Types.js.map +1 -0
- package/lib/ArtifactRegistry/Validation.d.ts +43 -0
- package/lib/ArtifactRegistry/Validation.d.ts.map +1 -0
- package/lib/ArtifactRegistry/Validation.js +95 -0
- package/lib/ArtifactRegistry/Validation.js.map +1 -0
- package/lib/ArtifactRegistry/index.d.ts +4 -0
- package/lib/ArtifactRegistry/index.d.ts.map +1 -0
- package/lib/ArtifactRegistry/index.js +2 -0
- package/lib/ArtifactRegistry/index.js.map +1 -0
- package/lib/Providers.d.ts.map +1 -1
- package/lib/Providers.js +9 -1
- package/lib/Providers.js.map +1 -1
- package/lib/Sqladmin/Database.d.ts +82 -0
- package/lib/Sqladmin/Database.d.ts.map +1 -0
- package/lib/Sqladmin/Database.js +87 -0
- package/lib/Sqladmin/Database.js.map +1 -0
- package/lib/Sqladmin/Instance.d.ts +234 -0
- package/lib/Sqladmin/Instance.d.ts.map +1 -0
- package/lib/Sqladmin/Instance.js +251 -0
- package/lib/Sqladmin/Instance.js.map +1 -0
- package/lib/Sqladmin/Operations.d.ts +41 -0
- package/lib/Sqladmin/Operations.d.ts.map +1 -0
- package/lib/Sqladmin/Operations.js +42 -0
- package/lib/Sqladmin/Operations.js.map +1 -0
- package/lib/Sqladmin/Types.d.ts +82 -0
- package/lib/Sqladmin/Types.d.ts.map +1 -0
- package/lib/Sqladmin/Types.js +28 -0
- package/lib/Sqladmin/Types.js.map +1 -0
- package/lib/Sqladmin/User.d.ts +110 -0
- package/lib/Sqladmin/User.d.ts.map +1 -0
- package/lib/Sqladmin/User.js +156 -0
- package/lib/Sqladmin/User.js.map +1 -0
- package/lib/Sqladmin/Validation.d.ts +60 -0
- package/lib/Sqladmin/Validation.d.ts.map +1 -0
- package/lib/Sqladmin/Validation.js +125 -0
- package/lib/Sqladmin/Validation.js.map +1 -0
- package/lib/Sqladmin/index.d.ts +9 -0
- package/lib/Sqladmin/index.d.ts.map +1 -0
- package/lib/Sqladmin/index.js +5 -0
- package/lib/Sqladmin/index.js.map +1 -0
- package/lib/index.d.ts +2 -0
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +2 -0
- package/lib/index.js.map +1 -1
- package/package.json +2 -2
- package/src/ArtifactRegistry/Operations.ts +79 -0
- package/src/ArtifactRegistry/Repository.ts +624 -0
- package/src/ArtifactRegistry/Types.ts +49 -0
- package/src/ArtifactRegistry/Validation.ts +112 -0
- package/src/ArtifactRegistry/index.ts +10 -0
- package/src/Providers.ts +15 -0
- package/src/Sqladmin/Database.ts +209 -0
- package/src/Sqladmin/Instance.ts +620 -0
- package/src/Sqladmin/Operations.ts +73 -0
- package/src/Sqladmin/Types.ts +101 -0
- package/src/Sqladmin/User.ts +333 -0
- package/src/Sqladmin/Validation.ts +148 -0
- package/src/Sqladmin/index.ts +19 -0
- package/src/index.ts +2 -0
|
@@ -0,0 +1,624 @@
|
|
|
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
|
+
if (
|
|
507
|
+
somePropsAreDifferent(
|
|
508
|
+
olds as ArtifactRegistryRepositoryProps,
|
|
509
|
+
news,
|
|
510
|
+
[
|
|
511
|
+
"project",
|
|
512
|
+
"location",
|
|
513
|
+
"name",
|
|
514
|
+
"format",
|
|
515
|
+
"mode",
|
|
516
|
+
"kmsKeyName",
|
|
517
|
+
"remoteRepositoryConfig",
|
|
518
|
+
],
|
|
519
|
+
)
|
|
520
|
+
) {
|
|
521
|
+
return { action: "replace" } as const;
|
|
522
|
+
}
|
|
523
|
+
return undefined;
|
|
524
|
+
}),
|
|
525
|
+
reconcile: Effect.fn(function* ({ id, news, session }) {
|
|
526
|
+
const internalLabels = yield* gcpInternalLabels(id);
|
|
527
|
+
const desiredName =
|
|
528
|
+
news.name ??
|
|
529
|
+
(yield* createPhysicalName({ id, maxLength: 63 })).toLowerCase();
|
|
530
|
+
|
|
531
|
+
yield* validateRepositoryName(desiredName);
|
|
532
|
+
yield* validateModeConfig(news);
|
|
533
|
+
|
|
534
|
+
const desiredLabels: Record<string, string> = {
|
|
535
|
+
...(news.labels ?? {}),
|
|
536
|
+
...internalLabels,
|
|
537
|
+
};
|
|
538
|
+
const name = fqName(news.project, news.location, desiredName);
|
|
539
|
+
|
|
540
|
+
// 1. Observe — collapse 403 to "missing" alongside 404.
|
|
541
|
+
let observed = yield* observe(news.project, news.location, desiredName);
|
|
542
|
+
|
|
543
|
+
// 2. Ensure — create LRO. Same API-enable race as Cloud Run /
|
|
544
|
+
// Cloud SQL: a create can fire before ApiEnable's effect
|
|
545
|
+
// propagates and the server returns 403 with "If you
|
|
546
|
+
// enabled this API recently, wait …". Retry that specific
|
|
547
|
+
// 403 for ~5 min; other Forbiddens propagate.
|
|
548
|
+
if (!observed) {
|
|
549
|
+
const op = yield* createRepository({
|
|
550
|
+
parent: parent(news.project, news.location),
|
|
551
|
+
repositoryId: desiredName,
|
|
552
|
+
body: toRepositoryBody(news, desiredLabels),
|
|
553
|
+
}).pipe(
|
|
554
|
+
Effect.retry({
|
|
555
|
+
while: (e: { _tag?: string; message?: string }) =>
|
|
556
|
+
e?._tag === "Forbidden" &&
|
|
557
|
+
/enabled this API recently|has not been used/i.test(
|
|
558
|
+
e.message ?? "",
|
|
559
|
+
),
|
|
560
|
+
schedule: Schedule.spaced(Duration.seconds(15)).pipe(
|
|
561
|
+
Schedule.both(Schedule.recurs(20)),
|
|
562
|
+
Schedule.tapOutput(() =>
|
|
563
|
+
session.note(
|
|
564
|
+
"Waiting for Artifact Registry API enablement to propagate…",
|
|
565
|
+
),
|
|
566
|
+
),
|
|
567
|
+
),
|
|
568
|
+
}),
|
|
569
|
+
Effect.catchTag("Conflict", () =>
|
|
570
|
+
Effect.succeed(undefined as ar.Operation | undefined),
|
|
571
|
+
),
|
|
572
|
+
Effect.catchTag("BadRequest", reshapeBadRequest("create")),
|
|
573
|
+
);
|
|
574
|
+
if (op?.name) yield* awaitOperation(op.name, session);
|
|
575
|
+
observed = yield* getRepository({ name });
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
// 3. Sync — single patch with an updateMask of changed
|
|
579
|
+
// top-level fields. `remoteRepositoryConfig` is NOT
|
|
580
|
+
// patched here: GCP refuses updates to it, so changes
|
|
581
|
+
// trigger a replace via `diff` above.
|
|
582
|
+
yield* syncMutable({
|
|
583
|
+
name,
|
|
584
|
+
observed,
|
|
585
|
+
news,
|
|
586
|
+
desiredLabels,
|
|
587
|
+
session,
|
|
588
|
+
});
|
|
589
|
+
|
|
590
|
+
const final = yield* getRepository({ name });
|
|
591
|
+
return toAttributes(final, {
|
|
592
|
+
project: news.project,
|
|
593
|
+
location: news.location,
|
|
594
|
+
name: desiredName,
|
|
595
|
+
});
|
|
596
|
+
}),
|
|
597
|
+
delete: Effect.fn(function* ({ output, session }) {
|
|
598
|
+
const name = fqName(output.project, output.location, output.name);
|
|
599
|
+
yield* deleteRepository({ name }).pipe(
|
|
600
|
+
Effect.flatMap((op) =>
|
|
601
|
+
op.name ? awaitOperation(op.name, session) : Effect.succeed(op),
|
|
602
|
+
),
|
|
603
|
+
Effect.catchTag("NotFound", () => Effect.void),
|
|
604
|
+
Effect.catchTag("BadRequest", reshapeBadRequest("delete")),
|
|
605
|
+
);
|
|
606
|
+
}),
|
|
607
|
+
read: Effect.fn(function* ({ id, output, olds }) {
|
|
608
|
+
const project = output?.project ?? olds?.project;
|
|
609
|
+
const location = output?.location ?? olds?.location;
|
|
610
|
+
if (!project || !location) return undefined;
|
|
611
|
+
const name =
|
|
612
|
+
output?.name ??
|
|
613
|
+
olds?.name ??
|
|
614
|
+
(yield* createPhysicalName({ id, maxLength: 63 })).toLowerCase();
|
|
615
|
+
const observed = yield* observe(project, location, name);
|
|
616
|
+
if (!observed) return undefined;
|
|
617
|
+
const attrs = toAttributes(observed, { project, location, name });
|
|
618
|
+
return (yield* hasAlchemyLabels(id, observed.labels))
|
|
619
|
+
? attrs
|
|
620
|
+
: Unowned(attrs);
|
|
621
|
+
}),
|
|
622
|
+
};
|
|
623
|
+
}),
|
|
624
|
+
);
|
|
@@ -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 & {});
|