@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,112 @@
1
+ import { ConfigError } from "@distilled.cloud/gcp";
2
+ import * as Effect from "effect/Effect";
3
+ import type { ArtifactRegistryRepositoryProps } from "./Repository.ts";
4
+
5
+ /**
6
+ * Plan-time validation for Artifact Registry resource props. Failing
7
+ * fast with a typed `ConfigError` is friendlier than letting the user
8
+ * wait for the API to reject the request, and unlike server-side
9
+ * errors these surface before any state mutation happens.
10
+ *
11
+ * @see https://cloud.google.com/artifact-registry/docs/reference/rest/v1/projects.locations.repositories
12
+ */
13
+
14
+ /**
15
+ * Repository ID regex per
16
+ * <https://cloud.google.com/artifact-registry/docs/repositories/create-repos#create>:
17
+ * lowercase letters, digits and hyphens; must start with a letter or
18
+ * digit; 1–63 chars total. The REST API documents up to 63; we cap at
19
+ * 63 to match.
20
+ *
21
+ * @internal
22
+ */
23
+ const REPOSITORY_NAME_RE = /^[a-z0-9][a-z0-9-]{0,62}$/;
24
+
25
+ /**
26
+ * Validate an Artifact Registry repository ID.
27
+ *
28
+ * @see https://cloud.google.com/artifact-registry/docs/repositories/create-repos#create
29
+ */
30
+ export const validateRepositoryName = (
31
+ name: string,
32
+ ): Effect.Effect<void, ConfigError> => {
33
+ if (REPOSITORY_NAME_RE.test(name)) return Effect.void;
34
+ return Effect.fail(
35
+ new ConfigError({
36
+ message: `Artifact Registry repository name ${JSON.stringify(name)} is invalid: must match /^[a-z0-9][a-z0-9-]{0,62}$/ (1–63 lowercase letters/digits/hyphens, starting with a letter or digit).`,
37
+ }),
38
+ );
39
+ };
40
+
41
+ /**
42
+ * Validate the `mode` ↔ `remoteRepositoryConfig` invariant. Artifact
43
+ * Registry rejects (with an opaque 400) any combination where:
44
+ *
45
+ * - `mode === "REMOTE_REPOSITORY"` but no `remoteRepositoryConfig`
46
+ * is supplied — the server has no upstream to point at.
47
+ * - `mode === "STANDARD_REPOSITORY"` but a `remoteRepositoryConfig`
48
+ * IS supplied — standard repositories don't have an upstream.
49
+ *
50
+ * `VIRTUAL_REPOSITORY` is not validated here; pass-through for now.
51
+ *
52
+ * @see https://cloud.google.com/artifact-registry/docs/repositories/remote-repo
53
+ */
54
+ export const validateModeConfig = (
55
+ props: ArtifactRegistryRepositoryProps,
56
+ ): Effect.Effect<void, ConfigError> => {
57
+ const mode = props.mode ?? "STANDARD_REPOSITORY";
58
+ if (mode === "REMOTE_REPOSITORY") {
59
+ const docker = props.remoteRepositoryConfig?.dockerRepository;
60
+ if (!docker) {
61
+ return Effect.fail(
62
+ new ConfigError({
63
+ message: `Artifact Registry repository has mode "REMOTE_REPOSITORY" but no \`remoteRepositoryConfig.dockerRepository\` was provided. Pass either \`{ publicRepository: "DOCKER_HUB" }\` or \`{ customRepository: { uri: "https://nvcr.io" } }\`.`,
64
+ }),
65
+ );
66
+ }
67
+ return Effect.void;
68
+ }
69
+ if (mode === "STANDARD_REPOSITORY" && props.remoteRepositoryConfig) {
70
+ return Effect.fail(
71
+ new ConfigError({
72
+ message: `Artifact Registry repository has mode "STANDARD_REPOSITORY" but \`remoteRepositoryConfig\` was supplied. Drop \`remoteRepositoryConfig\` or set \`mode: "REMOTE_REPOSITORY"\`.`,
73
+ }),
74
+ );
75
+ }
76
+ return Effect.void;
77
+ };
78
+
79
+ /**
80
+ * Reshape a `BadRequest` from an Artifact Registry create/patch/delete
81
+ * into a `ConfigError` with a remediation hint for the top failure
82
+ * modes:
83
+ *
84
+ * 1. **Upstream validation failure** — `disableUpstreamValidation` is
85
+ * `false` (default) and AR's pre-flight HEAD request against the
86
+ * remote upstream failed. Surface the underlying message; the user
87
+ * can retry with `disableUpstreamValidation: true` if the upstream
88
+ * is intentionally gated.
89
+ * 2. **KMS key not accessible** — the AR service agent does not have
90
+ * `cloudkms.cryptoKeyEncrypterDecrypter` on the supplied key.
91
+ *
92
+ * Anything else propagates verbatim — the underlying GCP message is
93
+ * usually clear enough.
94
+ */
95
+ export const reshapeBadRequest =
96
+ (op: "create" | "patch" | "delete") =>
97
+ (e: { message?: string }): Effect.Effect<never, ConfigError> => {
98
+ const underlying = e.message ?? `unknown 400 from Repository ${op}`;
99
+ let hint = "";
100
+ if (/upstream|remote.*validation|HEAD/i.test(underlying)) {
101
+ hint =
102
+ " Artifact Registry validates the upstream remote on create. If the upstream rejects unauthenticated HEAD requests (private nvcr.io paths, etc.), supply `remoteRepositoryConfig.disableUpstreamValidation: true` or attach credentials via `upstreamCredentials`.";
103
+ } else if (/kms|crypto.*key|encrypt/i.test(underlying)) {
104
+ hint =
105
+ " Grant `roles/cloudkms.cryptoKeyEncrypterDecrypter` on the supplied `kmsKeyName` to the Artifact Registry service agent (`service-{projectNumber}@gcp-sa-artifactregistry.iam.gserviceaccount.com`).";
106
+ }
107
+ return Effect.fail(
108
+ new ConfigError({
109
+ message: `Artifact Registry Repository ${op} rejected: ${underlying}.${hint}`,
110
+ }),
111
+ );
112
+ };
@@ -0,0 +1,10 @@
1
+ export {
2
+ ArtifactRegistryRepository,
3
+ ArtifactRegistryRepositoryProvider,
4
+ } from "./Repository.ts";
5
+ export type {
6
+ ArtifactRegistryDockerUpstream,
7
+ ArtifactRegistryRepositoryAttributes,
8
+ ArtifactRegistryRepositoryProps,
9
+ } from "./Repository.ts";
10
+ export type { ArtifactRegistryFormat, ArtifactRegistryMode } from "./Types.ts";
@@ -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, Profile } 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";
@@ -51,6 +51,7 @@ export const fromAuthProvider = () =>
51
51
  Layer.effect(
52
52
  Credentials,
53
53
  Effect.gen(function* () {
54
+ const profile = yield* Profile;
54
55
  const auth = yield* getAuthProvider<
55
56
  GCPAuthConfig,
56
57
  GCPResolvedCredentials
@@ -59,7 +60,7 @@ export const fromAuthProvider = () =>
59
60
  const ci = yield* Config.boolean("CI").pipe(Config.withDefault(false));
60
61
  const ctx = yield* Effect.context<never>();
61
62
 
62
- return yield* loadOrConfigure(auth, profileName, { ci }).pipe(
63
+ return yield* profile.loadOrConfigure(auth, profileName, { ci }).pipe(
63
64
  Effect.flatMap((cfg) => auth.read(profileName, cfg as GCPAuthConfig)),
64
65
  Effect.map(({ accessToken, project }) => ({ accessToken, project })),
65
66
  Effect.mapError(toConfigError),
package/src/Providers.ts CHANGED
@@ -1,5 +1,9 @@
1
1
  import * as Provider from "alchemy/Provider";
2
2
  import * as Layer from "effect/Layer";
3
+ import {
4
+ ArtifactRegistryRepository,
5
+ ArtifactRegistryRepositoryProvider,
6
+ } from "./ArtifactRegistry/Repository.ts";
3
7
  import { GCPAuth } from "./Auth/AuthProvider.ts";
4
8
  import { fromAuthProvider } from "./Auth/Credentials.ts";
5
9
  import { Project, ProjectProvider } from "./CloudResourceManager/Project.ts";
@@ -24,6 +28,9 @@ import {
24
28
  PsaConnectionProvider,
25
29
  } from "./ServiceNetworking/PsaConnection.ts";
26
30
  import { ApiEnable, ApiEnableProvider } from "./ServiceUsage/ApiEnable.ts";
31
+ import { SqlDatabase, SqlDatabaseProvider } from "./Sqladmin/Database.ts";
32
+ import { SqlInstance, SqlInstanceProvider } from "./Sqladmin/Instance.ts";
33
+ import { SqlUser, SqlUserProvider } from "./Sqladmin/User.ts";
27
34
 
28
35
  export class Providers extends Provider.ProviderCollection<Providers>()("GCP") {}
29
36
 
@@ -61,6 +68,10 @@ export const providers = () =>
61
68
  ManagedLustreInstance,
62
69
  Service,
63
70
  Job,
71
+ SqlInstance,
72
+ SqlDatabase,
73
+ SqlUser,
74
+ ArtifactRegistryRepository,
64
75
  ]),
65
76
  ).pipe(
66
77
  Layer.provide(
@@ -78,6 +89,10 @@ export const providers = () =>
78
89
  ManagedLustreInstanceProvider(),
79
90
  ServiceProvider(),
80
91
  JobProvider(),
92
+ SqlInstanceProvider(),
93
+ SqlDatabaseProvider(),
94
+ SqlUserProvider(),
95
+ ArtifactRegistryRepositoryProvider(),
81
96
  ),
82
97
  ),
83
98
  Layer.provideMerge(fromAuthProvider()),
@@ -0,0 +1,209 @@
1
+ import * as sql from "@distilled.cloud/gcp/sqladmin-v1";
2
+ import { Resource } from "alchemy";
3
+ import { isResolved, somePropsAreDifferent } from "alchemy/Diff";
4
+ import * as Provider from "alchemy/Provider";
5
+ import * as Effect from "effect/Effect";
6
+ import type * as GCP from "../Providers.ts";
7
+ import { makeAwaitOperation } from "./Operations.ts";
8
+ import { reshapeBadRequest, validateDatabaseName } from "./Validation.ts";
9
+
10
+ /**
11
+ * A logical Postgres database inside a Cloud SQL instance.
12
+ *
13
+ * Cloud SQL `databases` resources are extremely thin — name, charset,
14
+ * collation, parent instance. Use this resource to declaratively
15
+ * create a tenant database (one per app, one per team, etc.) and let
16
+ * alchemy track its lifecycle.
17
+ *
18
+ * **Adoption.** Cloud SQL databases carry NO labels. There is no
19
+ * server-side adoption marker we can stamp without polluting the
20
+ * collation field, so `read` is best-effort: it returns the observed
21
+ * shape if the database exists at the same logical address, and
22
+ * `undefined` otherwise. The caller is responsible for `--adopt` if
23
+ * they want to take over an existing physical database; we do not
24
+ * gate on labels here.
25
+ *
26
+ * **Replace triggers.** `project`, `instance`, `name`. Charset and
27
+ * collation are immutable post-create in practice — the API accepts
28
+ * a `patch` but ignores collation/charset drift, so we treat them as
29
+ * "set once, then forgotten" and only diff name/parent.
30
+ *
31
+ * @example
32
+ * ```typescript
33
+ * const db = yield* GCP.SqlDatabase("AppDb", {
34
+ * project: instance.project,
35
+ * instance: instance.name,
36
+ * name: "app",
37
+ * });
38
+ * ```
39
+ *
40
+ * @see https://cloud.google.com/sql/docs/postgres/admin-api/rest/v1/databases
41
+ */
42
+ export type SqlDatabaseProps = {
43
+ /**
44
+ * GCP project ID hosting the parent instance. Immutable — replace.
45
+ * @see https://cloud.google.com/sql/docs/postgres/admin-api/rest/v1/databases#Database
46
+ */
47
+ project: string;
48
+ /**
49
+ * Parent Cloud SQL instance name (NOT the fully-qualified resource
50
+ * name; just the bare `name`). Immutable — replace.
51
+ */
52
+ instance: string;
53
+ /**
54
+ * Database name. 1–64 chars; Postgres-specific naming applies (no
55
+ * reserved words, etc.). Immutable — replace if changed.
56
+ */
57
+ name: string;
58
+ /**
59
+ * Server-side character set. Postgres-default is `UTF8`. Set at
60
+ * create; not honoured on patch.
61
+ * @see https://cloud.google.com/sql/docs/postgres/admin-api/rest/v1/databases#Database.FIELDS.charset
62
+ */
63
+ charset?: string;
64
+ /**
65
+ * Collation. Postgres-default is `en_US.UTF8`. Set at create; not
66
+ * honoured on patch.
67
+ * @see https://cloud.google.com/sql/docs/postgres/admin-api/rest/v1/databases#Database.FIELDS.collation
68
+ */
69
+ collation?: string;
70
+ };
71
+
72
+ export type SqlDatabaseAttributes = {
73
+ /** Database name. */
74
+ name: string;
75
+ /** Parent instance name. */
76
+ instance: string;
77
+ /** Project ID. */
78
+ project: string;
79
+ /** Server-defined URL. */
80
+ selfLink: string;
81
+ /** Effective charset (server may default to `UTF8`). */
82
+ charset: string | undefined;
83
+ /** Effective collation. */
84
+ collation: string | undefined;
85
+ };
86
+
87
+ export type SqlDatabase = Resource<
88
+ "GCP.SqlDatabase",
89
+ SqlDatabaseProps,
90
+ SqlDatabaseAttributes,
91
+ never,
92
+ GCP.Providers
93
+ >;
94
+ export const SqlDatabase = Resource<SqlDatabase>("GCP.SqlDatabase");
95
+
96
+ const toAttributes = (
97
+ d: sql.Database,
98
+ parent: { project: string; instance: string; name: string },
99
+ ): SqlDatabaseAttributes => ({
100
+ name: parent.name,
101
+ instance: parent.instance,
102
+ project: parent.project,
103
+ selfLink: d.selfLink ?? "",
104
+ charset: d.charset,
105
+ collation: d.collation,
106
+ });
107
+
108
+ export const SqlDatabaseProvider = () =>
109
+ Provider.effect(
110
+ SqlDatabase,
111
+ Effect.gen(function* () {
112
+ const getDatabases = yield* sql.getDatabases;
113
+ const insertDatabases = yield* sql.insertDatabases;
114
+ const deleteDatabases = yield* sql.deleteDatabases;
115
+ const getOperations = yield* sql.getOperations;
116
+ const awaitOperation = makeAwaitOperation(getOperations);
117
+
118
+ const observe = (project: string, instance: string, database: string) =>
119
+ getDatabases({ project, instance, database }).pipe(
120
+ Effect.catchTag("NotFound", () =>
121
+ Effect.succeed(undefined as sql.Database | undefined),
122
+ ),
123
+ Effect.catchTag("Forbidden", () =>
124
+ Effect.succeed(undefined as sql.Database | undefined),
125
+ ),
126
+ );
127
+
128
+ return {
129
+ stables: ["name", "instance", "project", "selfLink"],
130
+ diff: Effect.fn(function* ({ news, olds = {} }) {
131
+ if (!isResolved(news)) return undefined;
132
+ if (
133
+ somePropsAreDifferent(olds as SqlDatabaseProps, news, [
134
+ "project",
135
+ "instance",
136
+ "name",
137
+ ])
138
+ ) {
139
+ return { action: "replace" } as const;
140
+ }
141
+ return undefined;
142
+ }),
143
+ reconcile: Effect.fn(function* ({ news, session }) {
144
+ yield* validateDatabaseName(news.name);
145
+
146
+ let observed = yield* observe(news.project, news.instance, news.name);
147
+
148
+ if (!observed) {
149
+ const op = yield* insertDatabases({
150
+ project: news.project,
151
+ instance: news.instance,
152
+ body: {
153
+ name: news.name,
154
+ ...(news.charset ? { charset: news.charset } : {}),
155
+ ...(news.collation ? { collation: news.collation } : {}),
156
+ },
157
+ }).pipe(
158
+ Effect.catchTag("Conflict", () =>
159
+ Effect.succeed(undefined as sql.Operation | undefined),
160
+ ),
161
+ Effect.catchTag(
162
+ "BadRequest",
163
+ reshapeBadRequest("Database", "insert"),
164
+ ),
165
+ );
166
+ if (op?.name) yield* awaitOperation(news.project, op.name, session);
167
+ observed = yield* getDatabases({
168
+ project: news.project,
169
+ instance: news.instance,
170
+ database: news.name,
171
+ });
172
+ }
173
+
174
+ return toAttributes(observed, {
175
+ project: news.project,
176
+ instance: news.instance,
177
+ name: news.name,
178
+ });
179
+ }),
180
+ delete: Effect.fn(function* ({ output, session }) {
181
+ yield* deleteDatabases({
182
+ project: output.project,
183
+ instance: output.instance,
184
+ database: output.name,
185
+ }).pipe(
186
+ Effect.flatMap((op) =>
187
+ op.name
188
+ ? awaitOperation(output.project, op.name, session)
189
+ : Effect.succeed(op),
190
+ ),
191
+ Effect.catchTag("NotFound", () => Effect.void),
192
+ Effect.catchTag(
193
+ "BadRequest",
194
+ reshapeBadRequest("Database", "delete"),
195
+ ),
196
+ );
197
+ }),
198
+ read: Effect.fn(function* ({ output, olds }) {
199
+ const project = output?.project ?? olds?.project;
200
+ const instance = output?.instance ?? olds?.instance;
201
+ const name = output?.name ?? olds?.name;
202
+ if (!project || !instance || !name) return undefined;
203
+ const observed = yield* observe(project, instance, name);
204
+ if (!observed) return undefined;
205
+ return toAttributes(observed, { project, instance, name });
206
+ }),
207
+ };
208
+ }),
209
+ );