@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.
- package/CHANGELOG.md +124 -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 +256 -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/Auth/Credentials.d.ts +2 -1
- package/lib/Auth/Credentials.d.ts.map +1 -1
- package/lib/Auth/Credentials.js +3 -2
- package/lib/Auth/Credentials.js.map +1 -1
- package/lib/CloudResourceManager/Project.d.ts +1 -1
- package/lib/Compute/GlobalAddress.d.ts +1 -1
- package/lib/Compute/Network.d.ts +1 -1
- package/lib/Compute/SharedVpcHost.d.ts +1 -1
- package/lib/Compute/SharedVpcServiceProject.d.ts +1 -1
- package/lib/Compute/Subnetwork.d.ts +1 -1
- package/lib/Container/Cluster.d.ts +1 -1
- package/lib/Container/NodePool.d.ts +1 -1
- package/lib/ManagedLustre/Instance.d.ts +1 -1
- package/lib/Providers.d.ts +1 -1
- package/lib/Providers.d.ts.map +1 -1
- package/lib/Providers.js +9 -1
- package/lib/Providers.js.map +1 -1
- package/lib/Run/Job.d.ts +1 -1
- package/lib/Run/Service.d.ts +1 -1
- package/lib/ServiceNetworking/PsaConnection.d.ts +1 -1
- package/lib/ServiceUsage/ApiEnable.d.ts +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/Tags.d.ts +4 -4
- 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 +628 -0
- package/src/ArtifactRegistry/Types.ts +49 -0
- package/src/ArtifactRegistry/Validation.ts +112 -0
- package/src/ArtifactRegistry/index.ts +10 -0
- package/src/Auth/Credentials.ts +3 -2
- 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,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Branded literal-union types for Cloud SQL props.
|
|
3
|
+
*
|
|
4
|
+
* Distilled's generated `databaseVersion` field is an open-tailed union
|
|
5
|
+
* (`"POSTGRES_15" | … | (string & {})`) so the GCP server can ship new
|
|
6
|
+
* versions without breaking SDK consumers. For our props we narrow to
|
|
7
|
+
* the subset we actually support — at the moment, the supported
|
|
8
|
+
* Postgres versions for greenfield instances. This gives consumers
|
|
9
|
+
* compile-time autocomplete and rejects typos like `"POSTGES_15"`.
|
|
10
|
+
*
|
|
11
|
+
* MySQL / SQL Server enum members are intentionally omitted — we only
|
|
12
|
+
* provision Postgres. Add a sibling `MySqlVersion` / `SqlServerVersion`
|
|
13
|
+
* type if/when those land as targets.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Supported Postgres major versions for new Cloud SQL instances.
|
|
18
|
+
*
|
|
19
|
+
* Cloud SQL only allows creating instances on currently-supported
|
|
20
|
+
* majors. As of 2026-06, that's 13–17 for greenfield; we expose 15+
|
|
21
|
+
* since that's what new MicroAGI stacks target. Older versions can be
|
|
22
|
+
* read/managed but should be migrated.
|
|
23
|
+
*
|
|
24
|
+
* @see https://cloud.google.com/sql/docs/postgres/db-versions
|
|
25
|
+
*/
|
|
26
|
+
export type PostgresVersion = "POSTGRES_15" | "POSTGRES_16" | "POSTGRES_17";
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Cloud SQL edition. `ENTERPRISE_PLUS` unlocks Data Cache, near-zero
|
|
30
|
+
* downtime planned maintenance, and longer log retention; `ENTERPRISE`
|
|
31
|
+
* is the standard tier.
|
|
32
|
+
*
|
|
33
|
+
* @see https://cloud.google.com/sql/docs/postgres/editions-intro
|
|
34
|
+
*/
|
|
35
|
+
export type SqlEdition = "ENTERPRISE" | "ENTERPRISE_PLUS";
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Data-disk type. PD_SSD is the default and what any new workload
|
|
39
|
+
* should pick. PD_HDD is legacy and slower; only useful for archival
|
|
40
|
+
* read replicas.
|
|
41
|
+
*
|
|
42
|
+
* @see https://cloud.google.com/sql/docs/postgres/instance-settings#storage-type-2ndgen
|
|
43
|
+
*/
|
|
44
|
+
export type SqlDataDiskType = "PD_SSD" | "PD_HDD";
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Availability type. `ZONAL` runs in one zone; `REGIONAL` runs a
|
|
48
|
+
* standby in a second zone for HA at ~2× the cost.
|
|
49
|
+
*
|
|
50
|
+
* @see https://cloud.google.com/sql/docs/postgres/high-availability
|
|
51
|
+
*/
|
|
52
|
+
export type SqlAvailabilityType = "ZONAL" | "REGIONAL";
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Activation policy. `ALWAYS` keeps the instance on; `NEVER` parks
|
|
56
|
+
* it (useful for occasional dev databases).
|
|
57
|
+
*
|
|
58
|
+
* @see https://cloud.google.com/sql/docs/postgres/start-stop-restart-instance
|
|
59
|
+
*/
|
|
60
|
+
export type SqlActivationPolicy = "ALWAYS" | "NEVER";
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Cloud SQL user auth type.
|
|
64
|
+
*
|
|
65
|
+
* - `BUILT_IN` — classic username + password Postgres role. The
|
|
66
|
+
* provider requires a `password` Redacted for this type.
|
|
67
|
+
* - `CLOUD_IAM_USER` — a human IAM principal authenticated via the
|
|
68
|
+
* Cloud SQL Auth Proxy / IAM database authn token exchange.
|
|
69
|
+
* - `CLOUD_IAM_SERVICE_ACCOUNT` — a GCP service-account principal,
|
|
70
|
+
* same auth mechanism as `CLOUD_IAM_USER`. The Postgres role name
|
|
71
|
+
* is the SA email **with the trailing `.gserviceaccount.com`
|
|
72
|
+
* stripped** (Cloud SQL caps role names at 63 chars; SA emails are
|
|
73
|
+
* longer than that).
|
|
74
|
+
* - Open tail `(string & {})` keeps forward compatibility for
|
|
75
|
+
* `CLOUD_IAM_GROUP`, `CLOUD_IAM_GROUP_SERVICE_ACCOUNT`, etc.
|
|
76
|
+
*
|
|
77
|
+
* @see https://cloud.google.com/sql/docs/postgres/iam-authentication
|
|
78
|
+
*/
|
|
79
|
+
export type SqlUserType =
|
|
80
|
+
| "BUILT_IN"
|
|
81
|
+
| "CLOUD_IAM_USER"
|
|
82
|
+
| "CLOUD_IAM_SERVICE_ACCOUNT"
|
|
83
|
+
| "CLOUD_IAM_GROUP"
|
|
84
|
+
| (string & {});
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Convenience predicate: `true` for any IAM-backed user type, where
|
|
88
|
+
* passwords are forbidden and the principal is a GCP identity.
|
|
89
|
+
*
|
|
90
|
+
* Used by `Validation.ts` and `User.ts` to fan out between the
|
|
91
|
+
* built-in password path and the IAM token-exchange path.
|
|
92
|
+
*/
|
|
93
|
+
export const isIamUserType = (
|
|
94
|
+
type: SqlUserType | undefined,
|
|
95
|
+
): boolean =>
|
|
96
|
+
type === "CLOUD_IAM_USER" ||
|
|
97
|
+
type === "CLOUD_IAM_SERVICE_ACCOUNT" ||
|
|
98
|
+
type === "CLOUD_IAM_GROUP" ||
|
|
99
|
+
type === "CLOUD_IAM_GROUP_USER" ||
|
|
100
|
+
type === "CLOUD_IAM_GROUP_SERVICE_ACCOUNT" ||
|
|
101
|
+
type === "ENTRAID_USER";
|
|
@@ -0,0 +1,333 @@
|
|
|
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 * as Redacted from "effect/Redacted";
|
|
7
|
+
import type * as GCP from "../Providers.ts";
|
|
8
|
+
import { makeAwaitOperation } from "./Operations.ts";
|
|
9
|
+
import { isIamUserType, type SqlUserType } from "./Types.ts";
|
|
10
|
+
import { reshapeBadRequest, validateUser } from "./Validation.ts";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* A Cloud SQL user (database role + auth binding).
|
|
14
|
+
*
|
|
15
|
+
* **Three auth modes:**
|
|
16
|
+
*
|
|
17
|
+
* - `BUILT_IN` — classic Postgres username/password. Requires
|
|
18
|
+
* `password`. The role exists only inside Postgres.
|
|
19
|
+
* - `CLOUD_IAM_USER` — a human IAM principal. `name` is the user's
|
|
20
|
+
* email. Authenticates via the Cloud SQL Auth Proxy / IAM token
|
|
21
|
+
* exchange; no password.
|
|
22
|
+
* - `CLOUD_IAM_SERVICE_ACCOUNT` — a GCP service account principal.
|
|
23
|
+
* `name` is the SA email with the trailing `.gserviceaccount.com`
|
|
24
|
+
* stripped (Postgres role names cap at 63 chars; full SA emails
|
|
25
|
+
* often exceed that). No password.
|
|
26
|
+
*
|
|
27
|
+
* The Postgres role itself is granted no privileges by default — wire
|
|
28
|
+
* up grants via SQL (psql / migrations) once the user exists. Cloud
|
|
29
|
+
* SQL IAM authn is independent of grant management.
|
|
30
|
+
*
|
|
31
|
+
* **Adoption.** Users carry no labels and no useful comparable shape
|
|
32
|
+
* beyond `(name, host, type)`. `read` returns the observed user if it
|
|
33
|
+
* exists at this address, `undefined` otherwise. No `Unowned` wrapper
|
|
34
|
+
* — adoption is implicit; the worst case is alchemy issuing a no-op
|
|
35
|
+
* patch / a delete on what it thinks is its own.
|
|
36
|
+
*
|
|
37
|
+
* **Replace triggers.** `project`, `instance`, `name`, `host`, `type`.
|
|
38
|
+
* Type changes need a drop/recreate because Cloud SQL stores the auth
|
|
39
|
+
* shape immutably; we surface that via replace.
|
|
40
|
+
*
|
|
41
|
+
* @example IAM service-account user (workload identity → DB role)
|
|
42
|
+
* ```typescript
|
|
43
|
+
* yield* GCP.SqlUser("AppSaUser", {
|
|
44
|
+
* project: instance.project,
|
|
45
|
+
* instance: instance.name,
|
|
46
|
+
* // App SA email is `app@my-project.iam.gserviceaccount.com` — strip
|
|
47
|
+
* // `.gserviceaccount.com` so the Postgres role name fits in 63 chars.
|
|
48
|
+
* name: "app@my-project.iam",
|
|
49
|
+
* type: "CLOUD_IAM_SERVICE_ACCOUNT",
|
|
50
|
+
* });
|
|
51
|
+
* ```
|
|
52
|
+
*
|
|
53
|
+
* @example Built-in user with a Redacted password
|
|
54
|
+
* ```typescript
|
|
55
|
+
* yield* GCP.SqlUser("MigratorUser", {
|
|
56
|
+
* project: instance.project,
|
|
57
|
+
* instance: instance.name,
|
|
58
|
+
* name: "migrator",
|
|
59
|
+
* password: Redacted.make(process.env.MIGRATOR_PASSWORD!),
|
|
60
|
+
* });
|
|
61
|
+
* ```
|
|
62
|
+
*
|
|
63
|
+
* @see https://cloud.google.com/sql/docs/postgres/admin-api/rest/v1/users
|
|
64
|
+
* @see https://cloud.google.com/sql/docs/postgres/iam-authentication
|
|
65
|
+
*/
|
|
66
|
+
export type SqlUserProps = {
|
|
67
|
+
/**
|
|
68
|
+
* GCP project ID hosting the parent instance. Immutable — replace.
|
|
69
|
+
* @see https://cloud.google.com/sql/docs/postgres/admin-api/rest/v1/users#User
|
|
70
|
+
*/
|
|
71
|
+
project: string;
|
|
72
|
+
/**
|
|
73
|
+
* Parent Cloud SQL instance name. Immutable — replace.
|
|
74
|
+
*/
|
|
75
|
+
instance: string;
|
|
76
|
+
/**
|
|
77
|
+
* User/role name. For `BUILT_IN`, any valid Postgres identifier.
|
|
78
|
+
* For `CLOUD_IAM_USER`, the principal's email. For
|
|
79
|
+
* `CLOUD_IAM_SERVICE_ACCOUNT`, the SA email **with the trailing
|
|
80
|
+
* `.gserviceaccount.com` stripped** — Postgres role names cap at
|
|
81
|
+
* 63 chars; full SA emails are typically 70+. Immutable — replace.
|
|
82
|
+
*/
|
|
83
|
+
name: string;
|
|
84
|
+
/**
|
|
85
|
+
* Host the user can connect from. PostgreSQL ignores this and
|
|
86
|
+
* defaults to `%` (any host). For MySQL it's load-bearing.
|
|
87
|
+
* Immutable — replace.
|
|
88
|
+
*/
|
|
89
|
+
host?: string;
|
|
90
|
+
/**
|
|
91
|
+
* Auth backend. Defaults to `BUILT_IN` if unset.
|
|
92
|
+
* @see https://cloud.google.com/sql/docs/postgres/iam-authentication
|
|
93
|
+
*/
|
|
94
|
+
type?: SqlUserType;
|
|
95
|
+
/**
|
|
96
|
+
* Password for `BUILT_IN` users. Required for that type; forbidden
|
|
97
|
+
* for any IAM type (validated at plan time).
|
|
98
|
+
*/
|
|
99
|
+
password?: Redacted.Redacted<string>;
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
export type SqlUserAttributes = {
|
|
103
|
+
/** User name. */
|
|
104
|
+
name: string;
|
|
105
|
+
/** Parent instance name. */
|
|
106
|
+
instance: string;
|
|
107
|
+
/** Project ID. */
|
|
108
|
+
project: string;
|
|
109
|
+
/** Connecting host (defaults to `%` for Postgres). */
|
|
110
|
+
host: string;
|
|
111
|
+
/** Auth backend in effect. */
|
|
112
|
+
type: string;
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
export type SqlUser = Resource<
|
|
116
|
+
"GCP.SqlUser",
|
|
117
|
+
SqlUserProps,
|
|
118
|
+
SqlUserAttributes,
|
|
119
|
+
never,
|
|
120
|
+
GCP.Providers
|
|
121
|
+
>;
|
|
122
|
+
export const SqlUser = Resource<SqlUser>("GCP.SqlUser");
|
|
123
|
+
|
|
124
|
+
const toAttributes = (
|
|
125
|
+
u: sql.User,
|
|
126
|
+
parent: { project: string; instance: string; name: string; host: string },
|
|
127
|
+
): SqlUserAttributes => ({
|
|
128
|
+
name: parent.name,
|
|
129
|
+
instance: parent.instance,
|
|
130
|
+
project: parent.project,
|
|
131
|
+
host: u.host ?? parent.host,
|
|
132
|
+
type: u.type ?? "BUILT_IN",
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
export const SqlUserProvider = () =>
|
|
136
|
+
Provider.effect(
|
|
137
|
+
SqlUser,
|
|
138
|
+
Effect.gen(function* () {
|
|
139
|
+
const getUsers = yield* sql.getUsers;
|
|
140
|
+
const listUsers = yield* sql.listUsers;
|
|
141
|
+
const insertUsers = yield* sql.insertUsers;
|
|
142
|
+
const updateUsers = yield* sql.updateUsers;
|
|
143
|
+
const deleteUsers = yield* sql.deleteUsers;
|
|
144
|
+
const getOperations = yield* sql.getOperations;
|
|
145
|
+
const awaitOperation = makeAwaitOperation(getOperations);
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Observe a user. Cloud SQL's `getUsers` endpoint returns 404
|
|
149
|
+
* for IAM-typed users in some Postgres minor versions — those
|
|
150
|
+
* are listable but not directly addressable by `name`. Fall
|
|
151
|
+
* back to a list filter on 404 to be safe.
|
|
152
|
+
*/
|
|
153
|
+
const observe = (
|
|
154
|
+
project: string,
|
|
155
|
+
instance: string,
|
|
156
|
+
name: string,
|
|
157
|
+
host: string | undefined,
|
|
158
|
+
) =>
|
|
159
|
+
getUsers({
|
|
160
|
+
project,
|
|
161
|
+
instance,
|
|
162
|
+
name,
|
|
163
|
+
...(host ? { host } : {}),
|
|
164
|
+
}).pipe(
|
|
165
|
+
Effect.catchTag("NotFound", () =>
|
|
166
|
+
listUsers({ project, instance }).pipe(
|
|
167
|
+
Effect.map((res) =>
|
|
168
|
+
(res.items ?? []).find(
|
|
169
|
+
(u) => u.name === name && (host ? u.host === host : true),
|
|
170
|
+
),
|
|
171
|
+
),
|
|
172
|
+
Effect.catchTag("NotFound", () =>
|
|
173
|
+
Effect.succeed(undefined as sql.User | undefined),
|
|
174
|
+
),
|
|
175
|
+
Effect.catchTag("Forbidden", () =>
|
|
176
|
+
Effect.succeed(undefined as sql.User | undefined),
|
|
177
|
+
),
|
|
178
|
+
),
|
|
179
|
+
),
|
|
180
|
+
Effect.catchTag("Forbidden", () =>
|
|
181
|
+
Effect.succeed(undefined as sql.User | undefined),
|
|
182
|
+
),
|
|
183
|
+
);
|
|
184
|
+
|
|
185
|
+
return {
|
|
186
|
+
stables: ["name", "instance", "project", "host", "type"],
|
|
187
|
+
diff: Effect.fn(function* ({ news, olds = {} }) {
|
|
188
|
+
if (!isResolved(news)) return undefined;
|
|
189
|
+
if (
|
|
190
|
+
somePropsAreDifferent(olds as SqlUserProps, news, [
|
|
191
|
+
"project",
|
|
192
|
+
"instance",
|
|
193
|
+
"name",
|
|
194
|
+
"host",
|
|
195
|
+
"type",
|
|
196
|
+
])
|
|
197
|
+
) {
|
|
198
|
+
return { action: "replace" } as const;
|
|
199
|
+
}
|
|
200
|
+
return undefined;
|
|
201
|
+
}),
|
|
202
|
+
reconcile: Effect.fn(function* ({ news, session }) {
|
|
203
|
+
const desiredType = news.type ?? "BUILT_IN";
|
|
204
|
+
const desiredHost = news.host ?? "%";
|
|
205
|
+
|
|
206
|
+
yield* validateUser({
|
|
207
|
+
name: news.name,
|
|
208
|
+
type: desiredType,
|
|
209
|
+
hasPassword: news.password !== undefined,
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
let observed = yield* observe(
|
|
213
|
+
news.project,
|
|
214
|
+
news.instance,
|
|
215
|
+
news.name,
|
|
216
|
+
news.host,
|
|
217
|
+
);
|
|
218
|
+
|
|
219
|
+
if (!observed) {
|
|
220
|
+
const body: sql.User = {
|
|
221
|
+
name: news.name,
|
|
222
|
+
...(news.host !== undefined ? { host: news.host } : {}),
|
|
223
|
+
type: desiredType,
|
|
224
|
+
...(news.password
|
|
225
|
+
? { password: Redacted.value(news.password) }
|
|
226
|
+
: {}),
|
|
227
|
+
};
|
|
228
|
+
const op = yield* insertUsers({
|
|
229
|
+
project: news.project,
|
|
230
|
+
instance: news.instance,
|
|
231
|
+
body,
|
|
232
|
+
}).pipe(
|
|
233
|
+
Effect.catchTag("Conflict", () =>
|
|
234
|
+
Effect.succeed(undefined as sql.Operation | undefined),
|
|
235
|
+
),
|
|
236
|
+
Effect.catchTag(
|
|
237
|
+
"BadRequest",
|
|
238
|
+
reshapeBadRequest("User", "insert"),
|
|
239
|
+
),
|
|
240
|
+
);
|
|
241
|
+
if (op?.name) yield* awaitOperation(news.project, op.name, session);
|
|
242
|
+
observed = yield* observe(
|
|
243
|
+
news.project,
|
|
244
|
+
news.instance,
|
|
245
|
+
news.name,
|
|
246
|
+
news.host,
|
|
247
|
+
);
|
|
248
|
+
} else if (
|
|
249
|
+
// Only BUILT_IN users have a rotatable password. For IAM
|
|
250
|
+
// users the type is fixed at insert and there's nothing
|
|
251
|
+
// mutable to sync. We don't store/compare BUILT_IN
|
|
252
|
+
// passwords (they're write-only on the API side) — if the
|
|
253
|
+
// user explicitly supplies one we re-issue the update so
|
|
254
|
+
// a rotated `Redacted` propagates, but we can't detect
|
|
255
|
+
// drift here so this is a deliberate "user is source of
|
|
256
|
+
// truth" path.
|
|
257
|
+
!isIamUserType(desiredType) &&
|
|
258
|
+
news.password
|
|
259
|
+
) {
|
|
260
|
+
const op = yield* updateUsers({
|
|
261
|
+
project: news.project,
|
|
262
|
+
instance: news.instance,
|
|
263
|
+
name: news.name,
|
|
264
|
+
...(news.host !== undefined ? { host: news.host } : {}),
|
|
265
|
+
body: {
|
|
266
|
+
name: news.name,
|
|
267
|
+
...(news.host !== undefined ? { host: news.host } : {}),
|
|
268
|
+
type: desiredType,
|
|
269
|
+
password: Redacted.value(news.password),
|
|
270
|
+
},
|
|
271
|
+
}).pipe(
|
|
272
|
+
Effect.catchTag(
|
|
273
|
+
"BadRequest",
|
|
274
|
+
reshapeBadRequest("User", "update"),
|
|
275
|
+
),
|
|
276
|
+
);
|
|
277
|
+
if (op.name) yield* awaitOperation(news.project, op.name, session);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// observed is non-undefined here: either we just created it
|
|
281
|
+
// and re-observed, or we found it pre-existing. If somehow
|
|
282
|
+
// listUsers still returns nothing (eventual consistency
|
|
283
|
+
// window), synthesise a minimal attribute set from inputs.
|
|
284
|
+
if (!observed) {
|
|
285
|
+
return {
|
|
286
|
+
name: news.name,
|
|
287
|
+
instance: news.instance,
|
|
288
|
+
project: news.project,
|
|
289
|
+
host: desiredHost,
|
|
290
|
+
type: desiredType,
|
|
291
|
+
} satisfies SqlUserAttributes;
|
|
292
|
+
}
|
|
293
|
+
return toAttributes(observed, {
|
|
294
|
+
project: news.project,
|
|
295
|
+
instance: news.instance,
|
|
296
|
+
name: news.name,
|
|
297
|
+
host: desiredHost,
|
|
298
|
+
});
|
|
299
|
+
}),
|
|
300
|
+
delete: Effect.fn(function* ({ output, session }) {
|
|
301
|
+
yield* deleteUsers({
|
|
302
|
+
project: output.project,
|
|
303
|
+
instance: output.instance,
|
|
304
|
+
name: output.name,
|
|
305
|
+
...(output.host && output.host !== "%" ? { host: output.host } : {}),
|
|
306
|
+
}).pipe(
|
|
307
|
+
Effect.flatMap((op) =>
|
|
308
|
+
op.name
|
|
309
|
+
? awaitOperation(output.project, op.name, session)
|
|
310
|
+
: Effect.succeed(op),
|
|
311
|
+
),
|
|
312
|
+
Effect.catchTag("NotFound", () => Effect.void),
|
|
313
|
+
Effect.catchTag("BadRequest", reshapeBadRequest("User", "delete")),
|
|
314
|
+
);
|
|
315
|
+
}),
|
|
316
|
+
read: Effect.fn(function* ({ output, olds }) {
|
|
317
|
+
const project = output?.project ?? olds?.project;
|
|
318
|
+
const instance = output?.instance ?? olds?.instance;
|
|
319
|
+
const name = output?.name ?? olds?.name;
|
|
320
|
+
if (!project || !instance || !name) return undefined;
|
|
321
|
+
const host = output?.host ?? olds?.host;
|
|
322
|
+
const observed = yield* observe(project, instance, name, host);
|
|
323
|
+
if (!observed) return undefined;
|
|
324
|
+
return toAttributes(observed, {
|
|
325
|
+
project,
|
|
326
|
+
instance,
|
|
327
|
+
name,
|
|
328
|
+
host: host ?? "%",
|
|
329
|
+
});
|
|
330
|
+
}),
|
|
331
|
+
};
|
|
332
|
+
}),
|
|
333
|
+
);
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { ConfigError } from "@distilled.cloud/gcp";
|
|
2
|
+
import * as Effect from "effect/Effect";
|
|
3
|
+
import { isIamUserType, type SqlUserType } from "./Types.ts";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Plan-time validation for Cloud SQL resource props. Failing fast with
|
|
7
|
+
* a typed `ConfigError` is friendlier than letting the user wait for
|
|
8
|
+
* Cloud SQL to reject the request, and unlike server-side errors these
|
|
9
|
+
* surface before any state mutation happens.
|
|
10
|
+
*
|
|
11
|
+
* @see https://cloud.google.com/sql/docs/postgres/admin-api/rest/v1/instances
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Instance name regex per
|
|
16
|
+
* <https://cloud.google.com/sql/docs/postgres/admin-api/rest/v1/instances#SqlInstance>:
|
|
17
|
+
* 1–98 lowercase letters/digits/hyphens, must begin with a letter and
|
|
18
|
+
* not end with a hyphen.
|
|
19
|
+
*
|
|
20
|
+
* @internal
|
|
21
|
+
*/
|
|
22
|
+
const INSTANCE_NAME_RE = /^[a-z]([-a-z0-9]*[a-z0-9])?$/;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Database name length cap per
|
|
26
|
+
* <https://cloud.google.com/sql/docs/postgres/admin-api/rest/v1/databases#Database>:
|
|
27
|
+
* 1–64 chars. Postgres has additional character constraints (no NUL,
|
|
28
|
+
* no embedded quotes) but the API rejects those, and they'd require
|
|
29
|
+
* dialect-specific parsing here.
|
|
30
|
+
*
|
|
31
|
+
* @internal
|
|
32
|
+
*/
|
|
33
|
+
const DATABASE_NAME_MAX = 64;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Validate a Cloud SQL instance name. Returns a `ConfigError` on the
|
|
37
|
+
* failure channel if the name is invalid, otherwise succeeds.
|
|
38
|
+
*
|
|
39
|
+
* @see https://cloud.google.com/sql/docs/postgres/admin-api/rest/v1/instances#SqlInstance
|
|
40
|
+
*/
|
|
41
|
+
export const validateInstanceName = (
|
|
42
|
+
name: string,
|
|
43
|
+
): Effect.Effect<void, ConfigError> => {
|
|
44
|
+
if (INSTANCE_NAME_RE.test(name) && name.length >= 1 && name.length <= 98) {
|
|
45
|
+
return Effect.void;
|
|
46
|
+
}
|
|
47
|
+
return Effect.fail(
|
|
48
|
+
new ConfigError({
|
|
49
|
+
message: `Cloud SQL instance name ${JSON.stringify(name)} is invalid: must match /^[a-z]([-a-z0-9]*[a-z0-9])?$/ (1–98 lowercase letters/digits/hyphens, start with letter, no trailing hyphen).`,
|
|
50
|
+
}),
|
|
51
|
+
);
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Validate a Cloud SQL database name (1–64 chars, non-empty).
|
|
56
|
+
*
|
|
57
|
+
* @see https://cloud.google.com/sql/docs/postgres/admin-api/rest/v1/databases#Database
|
|
58
|
+
*/
|
|
59
|
+
export const validateDatabaseName = (
|
|
60
|
+
name: string,
|
|
61
|
+
): Effect.Effect<void, ConfigError> => {
|
|
62
|
+
if (name.length >= 1 && name.length <= DATABASE_NAME_MAX) return Effect.void;
|
|
63
|
+
return Effect.fail(
|
|
64
|
+
new ConfigError({
|
|
65
|
+
message: `Cloud SQL database name ${JSON.stringify(name)} is invalid: must be 1–${DATABASE_NAME_MAX} characters.`,
|
|
66
|
+
}),
|
|
67
|
+
);
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Validate a Cloud SQL user shape — primarily that the password/type
|
|
72
|
+
* combination is consistent:
|
|
73
|
+
*
|
|
74
|
+
* - `BUILT_IN` users MUST carry a password.
|
|
75
|
+
* - Any IAM user type (`CLOUD_IAM_USER`, `CLOUD_IAM_SERVICE_ACCOUNT`,
|
|
76
|
+
* `CLOUD_IAM_GROUP*`, `ENTRAID_USER`) MUST NOT carry a password —
|
|
77
|
+
* the IAM principal is authenticated by token exchange.
|
|
78
|
+
*
|
|
79
|
+
* Other types from the open tail of the enum (forward-compat) skip
|
|
80
|
+
* the password check.
|
|
81
|
+
*
|
|
82
|
+
* @see https://cloud.google.com/sql/docs/postgres/users
|
|
83
|
+
* @see https://cloud.google.com/sql/docs/postgres/iam-authentication
|
|
84
|
+
*/
|
|
85
|
+
export const validateUser = (args: {
|
|
86
|
+
name: string;
|
|
87
|
+
type: SqlUserType | undefined;
|
|
88
|
+
hasPassword: boolean;
|
|
89
|
+
}): Effect.Effect<void, ConfigError> => {
|
|
90
|
+
const type = args.type ?? "BUILT_IN";
|
|
91
|
+
if (isIamUserType(type) && args.hasPassword) {
|
|
92
|
+
return Effect.fail(
|
|
93
|
+
new ConfigError({
|
|
94
|
+
message: `Cloud SQL user ${JSON.stringify(args.name)} has type ${JSON.stringify(type)} (IAM-backed) but a password was supplied. IAM users authenticate via the Cloud SQL Auth Proxy / IAM token exchange — drop the \`password\` field.`,
|
|
95
|
+
}),
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
if (type === "BUILT_IN" && !args.hasPassword) {
|
|
99
|
+
return Effect.fail(
|
|
100
|
+
new ConfigError({
|
|
101
|
+
message: `Cloud SQL user ${JSON.stringify(args.name)} has type "BUILT_IN" but no password was supplied. BUILT_IN users require a password — pass \`password: Redacted.make("...")\` or switch to an IAM type.`,
|
|
102
|
+
}),
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
return Effect.void;
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Reshape a `BadRequest` from a Cloud SQL Admin create/patch/delete
|
|
110
|
+
* into a `ConfigError` with a remediation hint for the top failure
|
|
111
|
+
* modes:
|
|
112
|
+
*
|
|
113
|
+
* 1. **Billing not enabled** — Cloud SQL refuses to provision against
|
|
114
|
+
* a project without billing. Common right after `ApiEnable` lands
|
|
115
|
+
* but before billing reconciles in a fresh project.
|
|
116
|
+
* 2. **Private network not set up** — `privateNetwork` references a
|
|
117
|
+
* VPC that has no PSA peering for `servicenetworking.googleapis.com`.
|
|
118
|
+
* Surface the precondition.
|
|
119
|
+
* 3. **Invalid tier** — typo'd `db-custom-…` or one outside the
|
|
120
|
+
* project's allow-list. Pass the underlying message through with
|
|
121
|
+
* a pointer to the tier catalogue.
|
|
122
|
+
*
|
|
123
|
+
* Anything else propagates verbatim — the underlying GCP message is
|
|
124
|
+
* usually clear enough.
|
|
125
|
+
*
|
|
126
|
+
* @see https://cloud.google.com/sql/docs/postgres/instance-settings
|
|
127
|
+
*/
|
|
128
|
+
export const reshapeBadRequest =
|
|
129
|
+
(kind: "Instance" | "Database" | "User", op: "insert" | "patch" | "update" | "delete") =>
|
|
130
|
+
(e: { message?: string }): Effect.Effect<never, ConfigError> => {
|
|
131
|
+
const underlying = e.message ?? `unknown 400 from ${kind} ${op}`;
|
|
132
|
+
let hint = "";
|
|
133
|
+
if (/billing/i.test(underlying)) {
|
|
134
|
+
hint =
|
|
135
|
+
" Cloud SQL requires billing to be enabled on the project. Attach a billing account (see `GCP.Project({ billingAccount })`) and retry.";
|
|
136
|
+
} else if (/private.*service.*access|servicenetworking|peering/i.test(underlying)) {
|
|
137
|
+
hint =
|
|
138
|
+
" The host VPC needs a Private Services Access (PSA) peering with `servicenetworking.googleapis.com` before a private-IP Cloud SQL instance can attach. Provision a `GCP.GlobalAddress` (purpose=VPC_PEERING) and a `GCP.PsaConnection` on the VPC first.";
|
|
139
|
+
} else if (/tier|machine.*type/i.test(underlying)) {
|
|
140
|
+
hint =
|
|
141
|
+
" Check the tier identifier against https://cloud.google.com/sql/docs/postgres/instance-settings — and remember that Enterprise Plus tiers (`db-perf-optimized-*`) require `edition: \"ENTERPRISE_PLUS\"`.";
|
|
142
|
+
}
|
|
143
|
+
return Effect.fail(
|
|
144
|
+
new ConfigError({
|
|
145
|
+
message: `Cloud SQL ${kind} ${op} rejected: ${underlying}.${hint}`,
|
|
146
|
+
}),
|
|
147
|
+
);
|
|
148
|
+
};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export { SqlDatabase, SqlDatabaseProvider } from "./Database.ts";
|
|
2
|
+
export type { SqlDatabaseAttributes, SqlDatabaseProps } from "./Database.ts";
|
|
3
|
+
export { SqlInstance, SqlInstanceProvider } from "./Instance.ts";
|
|
4
|
+
export type {
|
|
5
|
+
SqlInstanceAttributes,
|
|
6
|
+
SqlInstanceIpAddress,
|
|
7
|
+
SqlInstanceProps,
|
|
8
|
+
} from "./Instance.ts";
|
|
9
|
+
export { isIamUserType } from "./Types.ts";
|
|
10
|
+
export type {
|
|
11
|
+
PostgresVersion,
|
|
12
|
+
SqlActivationPolicy,
|
|
13
|
+
SqlAvailabilityType,
|
|
14
|
+
SqlDataDiskType,
|
|
15
|
+
SqlEdition,
|
|
16
|
+
SqlUserType,
|
|
17
|
+
} from "./Types.ts";
|
|
18
|
+
export { SqlUser, SqlUserProvider } from "./User.ts";
|
|
19
|
+
export type { SqlUserAttributes, SqlUserProps } from "./User.ts";
|
package/src/index.ts
CHANGED
|
@@ -7,11 +7,13 @@ export type {
|
|
|
7
7
|
GCPAuthConfig,
|
|
8
8
|
GCPResolvedCredentials,
|
|
9
9
|
} from "./Auth/AuthProvider.ts";
|
|
10
|
+
export * from "./ArtifactRegistry/index.ts";
|
|
10
11
|
export * from "./CloudResourceManager/index.ts";
|
|
11
12
|
export * from "./Compute/index.ts";
|
|
12
13
|
export * from "./Container/index.ts";
|
|
13
14
|
export * from "./ManagedLustre/index.ts";
|
|
14
15
|
export * from "./Run/index.ts";
|
|
15
16
|
export * from "./ServiceNetworking/index.ts";
|
|
17
|
+
export * from "./Sqladmin/index.ts";
|
|
16
18
|
export * from "./ServiceUsage/index.ts";
|
|
17
19
|
export * from "./Providers.ts";
|