puls-dev 0.2.0 → 0.2.2

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 (64) hide show
  1. package/README.md +1 -1
  2. package/dist/core/config.d.ts +5 -0
  3. package/dist/providers/aws/api.d.ts +4 -0
  4. package/dist/providers/aws/api.js +4 -0
  5. package/dist/providers/aws/cloudwatch.d.ts +44 -0
  6. package/dist/providers/aws/cloudwatch.js +205 -0
  7. package/dist/providers/aws/cloudwatch.test.d.ts +1 -0
  8. package/dist/providers/aws/cloudwatch.test.js +224 -0
  9. package/dist/providers/aws/fargate.d.ts +2 -0
  10. package/dist/providers/aws/fargate.js +6 -0
  11. package/dist/providers/aws/iam.d.ts +52 -0
  12. package/dist/providers/aws/iam.js +307 -0
  13. package/dist/providers/aws/iam.test.d.ts +1 -0
  14. package/dist/providers/aws/iam.test.js +367 -0
  15. package/dist/providers/aws/index.d.ts +7 -0
  16. package/dist/providers/aws/index.js +7 -0
  17. package/dist/providers/aws/lambda.d.ts +3 -1
  18. package/dist/providers/aws/lambda.js +11 -2
  19. package/dist/providers/aws/rds.d.ts +1 -0
  20. package/dist/providers/aws/rds.js +4 -1
  21. package/dist/providers/aws/sns.d.ts +22 -0
  22. package/dist/providers/aws/sns.js +146 -0
  23. package/dist/providers/aws/sns.test.d.ts +1 -0
  24. package/dist/providers/aws/sns.test.js +162 -0
  25. package/dist/providers/firebase/appcheck.d.ts +15 -0
  26. package/dist/providers/firebase/appcheck.js +109 -0
  27. package/dist/providers/firebase/appcheck.test.d.ts +1 -0
  28. package/dist/providers/firebase/appcheck.test.js +141 -0
  29. package/dist/providers/firebase/index.d.ts +2 -0
  30. package/dist/providers/firebase/index.js +2 -0
  31. package/dist/providers/gcp/api.d.ts +10 -0
  32. package/dist/providers/gcp/api.js +111 -0
  33. package/dist/providers/gcp/clouddns.d.ts +37 -0
  34. package/dist/providers/gcp/clouddns.js +284 -0
  35. package/dist/providers/gcp/clouddns.test.d.ts +1 -0
  36. package/dist/providers/gcp/clouddns.test.js +259 -0
  37. package/dist/providers/gcp/cloudrun.d.ts +31 -0
  38. package/dist/providers/gcp/cloudrun.js +240 -0
  39. package/dist/providers/gcp/cloudrun.test.d.ts +1 -0
  40. package/dist/providers/gcp/cloudrun.test.js +281 -0
  41. package/dist/providers/gcp/cloudsql.d.ts +37 -0
  42. package/dist/providers/gcp/cloudsql.js +262 -0
  43. package/dist/providers/gcp/cloudsql.test.d.ts +1 -0
  44. package/dist/providers/gcp/cloudsql.test.js +295 -0
  45. package/dist/providers/gcp/iam.d.ts +38 -0
  46. package/dist/providers/gcp/iam.js +309 -0
  47. package/dist/providers/gcp/iam.test.d.ts +1 -0
  48. package/dist/providers/gcp/iam.test.js +305 -0
  49. package/dist/providers/gcp/index.d.ts +19 -0
  50. package/dist/providers/gcp/index.js +19 -0
  51. package/dist/providers/gcp/pubsub.d.ts +31 -0
  52. package/dist/providers/gcp/pubsub.js +227 -0
  53. package/dist/providers/gcp/pubsub.test.d.ts +1 -0
  54. package/dist/providers/gcp/pubsub.test.js +244 -0
  55. package/dist/providers/gcp/secrets.d.ts +21 -0
  56. package/dist/providers/gcp/secrets.js +187 -0
  57. package/dist/providers/gcp/secrets.test.d.ts +1 -0
  58. package/dist/providers/gcp/secrets.test.js +264 -0
  59. package/dist/providers/proxmox/vm.d.ts +2 -0
  60. package/dist/providers/proxmox/vm.js +35 -3
  61. package/dist/providers/proxmox/vm.test.d.ts +1 -0
  62. package/dist/providers/proxmox/vm.test.js +155 -0
  63. package/dist/types/aws.d.ts +11 -0
  64. package/package.json +32 -2
@@ -0,0 +1,281 @@
1
+ import { test, describe, beforeEach, afterEach, mock } from "node:test";
2
+ import assert from "node:assert";
3
+ import { GoogleAuth } from "google-auth-library";
4
+ import { GCPCloudRunBuilder } from "./cloudrun.js";
5
+ import { Config } from "../../core/config.js";
6
+ describe("GCPCloudRunBuilder Unit Tests", () => {
7
+ let originalFetch;
8
+ let fetchCalls = [];
9
+ let mockResponses = {};
10
+ beforeEach(() => {
11
+ Config.set({
12
+ dryRun: false,
13
+ providers: {
14
+ gcp: {
15
+ projectId: "my-gcp-project",
16
+ serviceAccountPath: "/fake/sa.json",
17
+ region: "us-east1",
18
+ },
19
+ },
20
+ });
21
+ originalFetch = globalThis.fetch;
22
+ fetchCalls = [];
23
+ mockResponses = {};
24
+ globalThis.fetch = async (input, init) => {
25
+ const url = String(input);
26
+ const method = init?.method ?? "GET";
27
+ let body;
28
+ if (init?.body) {
29
+ if (typeof init.body === "string") {
30
+ try {
31
+ body = JSON.parse(init.body);
32
+ }
33
+ catch {
34
+ body = init.body;
35
+ }
36
+ }
37
+ else {
38
+ body = "[Binary/Buffer Body]";
39
+ }
40
+ }
41
+ const headers = init?.headers;
42
+ fetchCalls.push({ url, method, body, headers });
43
+ const matchKey = Object.keys(mockResponses)
44
+ .filter((key) => {
45
+ const [mMethod, mPath] = key.split(" ");
46
+ return method === mMethod && url.includes(mPath);
47
+ })
48
+ .sort((a, b) => b.split(" ")[1].length - a.split(" ")[1].length)[0];
49
+ if (matchKey) {
50
+ const resp = mockResponses[matchKey];
51
+ return {
52
+ ok: resp.status >= 200 && resp.status < 300,
53
+ status: resp.status,
54
+ json: async () => resp.body,
55
+ text: async () => JSON.stringify(resp.body),
56
+ };
57
+ }
58
+ return {
59
+ ok: false,
60
+ status: 404,
61
+ json: async () => ({ message: `Endpoint not mocked: ${method} ${url}` }),
62
+ text: async () => `Endpoint not mocked: ${method} ${url}`,
63
+ };
64
+ };
65
+ mock.method(GoogleAuth.prototype, "getClient", async () => {
66
+ return {
67
+ getAccessToken: async () => ({ token: "fake-gcp-token" }),
68
+ };
69
+ });
70
+ });
71
+ afterEach(() => {
72
+ globalThis.fetch = originalFetch;
73
+ mock.restoreAll();
74
+ });
75
+ test("fluent builder api sets properties correctly", () => {
76
+ const builder = new GCPCloudRunBuilder("my-service")
77
+ .image("gcr.io/my-proj/my-image:v1")
78
+ .port(3000)
79
+ .cpu(2)
80
+ .memory(1024)
81
+ .minInstances(2)
82
+ .maxInstances(20)
83
+ .env({ DB_HOST: "localhost", PORT: "3000" })
84
+ .region("europe-west1")
85
+ .public(false);
86
+ assert.strictEqual(builder._image, "gcr.io/my-proj/my-image:v1");
87
+ assert.strictEqual(builder._port, 3000);
88
+ assert.strictEqual(builder._cpu, 2);
89
+ assert.strictEqual(builder._memory, 1024);
90
+ assert.strictEqual(builder._minInstances, 2);
91
+ assert.strictEqual(builder._maxInstances, 20);
92
+ assert.deepStrictEqual(builder._env, { DB_HOST: "localhost", PORT: "3000" });
93
+ assert.strictEqual(builder._region, "europe-west1");
94
+ assert.strictEqual(builder._public, false);
95
+ });
96
+ test("runs in dry-run mode safely and logs plans", async () => {
97
+ Config.set({
98
+ dryRun: true,
99
+ providers: {
100
+ gcp: {
101
+ projectId: "my-gcp-project",
102
+ serviceAccountPath: "/fake/sa.json",
103
+ region: "us-central1",
104
+ },
105
+ },
106
+ });
107
+ const builder = new GCPCloudRunBuilder("dry-run-service")
108
+ .image("gcr.io/my-proj/my-image:v1")
109
+ .minInstances(1)
110
+ .maxInstances(5);
111
+ const result = await builder.deploy();
112
+ assert.strictEqual(result.serviceId, "dry-run-service");
113
+ assert.strictEqual(result.url, "https://dry-run-service-dryrun.a.run.app");
114
+ const writeCalls = fetchCalls.filter((c) => c.method !== "GET");
115
+ assert.strictEqual(writeCalls.length, 0); // No write requests in dry-run
116
+ });
117
+ test("creates a new service when it does not exist", async () => {
118
+ // 1. Mock GET returning 404 (discovery)
119
+ mockResponses["GET /services/new-service"] = {
120
+ status: 404,
121
+ body: { message: "Not found" },
122
+ };
123
+ // 2. Mock POST (create)
124
+ mockResponses["POST /services?serviceId=new-service"] = {
125
+ status: 201,
126
+ body: {
127
+ name: "projects/my-gcp-project/locations/us-east1/services/new-service",
128
+ uri: "https://new-service-xyz.run.app",
129
+ },
130
+ };
131
+ // 3. Mock POST (setIamPolicy)
132
+ mockResponses["POST /services/new-service:setIamPolicy"] = {
133
+ status: 200,
134
+ body: {},
135
+ };
136
+ const builder = new GCPCloudRunBuilder("new-service")
137
+ .image("gcr.io/my-proj/my-image:v1")
138
+ .port(8080)
139
+ .cpu("1")
140
+ .memory("512Mi")
141
+ .minInstances(0)
142
+ .maxInstances(10)
143
+ .public(true);
144
+ const result = await builder.deploy();
145
+ assert.strictEqual(result.serviceId, "new-service");
146
+ assert.strictEqual(result.url, "https://new-service-xyz.run.app");
147
+ // Assert fetch calls occurred
148
+ const postCalls = fetchCalls.filter((c) => c.method === "POST" && c.url.includes("serviceId="));
149
+ assert.strictEqual(postCalls.length, 1);
150
+ assert.deepStrictEqual(postCalls[0].body.template.containers[0].image, "gcr.io/my-proj/my-image:v1");
151
+ const iamCalls = fetchCalls.filter((c) => c.method === "POST" && c.url.includes(":setIamPolicy"));
152
+ assert.strictEqual(iamCalls.length, 1);
153
+ assert.deepStrictEqual(iamCalls[0].body.policy.bindings[0].role, "roles/run.invoker");
154
+ assert.deepStrictEqual(iamCalls[0].body.policy.bindings[0].members, ["allUsers"]);
155
+ });
156
+ test("updates an existing service if configuration differs", async () => {
157
+ // 1. Mock GET returning existing service config with different image
158
+ mockResponses["GET /services/existing-service"] = {
159
+ status: 200,
160
+ body: {
161
+ name: "projects/my-gcp-project/locations/us-east1/services/existing-service",
162
+ uri: "https://existing-service-xyz.run.app",
163
+ template: {
164
+ containers: [
165
+ {
166
+ image: "gcr.io/my-proj/old-image:v1",
167
+ ports: [{ containerPort: 8080 }],
168
+ resources: { limits: { cpu: "1", memory: "512Mi" } },
169
+ },
170
+ ],
171
+ scaling: { minInstanceCount: 0, maxInstanceCount: 10 },
172
+ },
173
+ ingress: "INGRESS_TRAFFIC_ALL",
174
+ },
175
+ };
176
+ // 2. Mock PATCH (update)
177
+ mockResponses["PATCH /services/existing-service"] = {
178
+ status: 200,
179
+ body: {
180
+ name: "projects/my-gcp-project/locations/us-east1/services/existing-service",
181
+ uri: "https://existing-service-xyz.run.app",
182
+ },
183
+ };
184
+ // 3. Mock POST (setIamPolicy)
185
+ mockResponses["POST /services/existing-service:setIamPolicy"] = {
186
+ status: 200,
187
+ body: {},
188
+ };
189
+ const builder = new GCPCloudRunBuilder("existing-service")
190
+ .image("gcr.io/my-proj/new-image:v1") // Image changed!
191
+ .minInstances(0)
192
+ .maxInstances(10)
193
+ .public(true);
194
+ const result = await builder.deploy();
195
+ assert.strictEqual(result.serviceId, "existing-service");
196
+ assert.strictEqual(result.url, "https://existing-service-xyz.run.app");
197
+ // Verify PATCH was called
198
+ const patchCalls = fetchCalls.filter((c) => c.method === "PATCH");
199
+ assert.strictEqual(patchCalls.length, 1);
200
+ assert.deepStrictEqual(patchCalls[0].body.template.containers[0].image, "gcr.io/my-proj/new-image:v1");
201
+ const iamCalls = fetchCalls.filter((c) => c.method === "POST" && c.url.includes(":setIamPolicy"));
202
+ assert.strictEqual(iamCalls.length, 1);
203
+ });
204
+ test("skips updating an existing service if configuration is identical", async () => {
205
+ // 1. Mock GET returning exact same config
206
+ mockResponses["GET /services/identical-service"] = {
207
+ status: 200,
208
+ body: {
209
+ name: "projects/my-gcp-project/locations/us-east1/services/identical-service",
210
+ uri: "https://identical-service-xyz.run.app",
211
+ template: {
212
+ containers: [
213
+ {
214
+ image: "gcr.io/my-proj/image:v1",
215
+ ports: [{ containerPort: 8080 }],
216
+ resources: { limits: { cpu: "1", memory: "512Mi" } },
217
+ env: [{ name: "NODE_ENV", value: "production" }],
218
+ },
219
+ ],
220
+ scaling: { minInstanceCount: 0, maxInstanceCount: 10 },
221
+ },
222
+ ingress: "INGRESS_TRAFFIC_ALL",
223
+ },
224
+ };
225
+ // 2. Mock POST (setIamPolicy)
226
+ mockResponses["POST /services/identical-service:setIamPolicy"] = {
227
+ status: 200,
228
+ body: {},
229
+ };
230
+ const builder = new GCPCloudRunBuilder("identical-service")
231
+ .image("gcr.io/my-proj/image:v1")
232
+ .port(8080)
233
+ .cpu("1")
234
+ .memory("512Mi")
235
+ .minInstances(0)
236
+ .maxInstances(10)
237
+ .env({ NODE_ENV: "production" })
238
+ .public(true);
239
+ const result = await builder.deploy();
240
+ assert.strictEqual(result.serviceId, "identical-service");
241
+ assert.strictEqual(result.url, "https://identical-service-xyz.run.app");
242
+ // Verify PATCH or POST for services was NOT called
243
+ const writeCalls = fetchCalls.filter((c) => (c.method === "PATCH" || (c.method === "POST" && c.url.includes("serviceId="))));
244
+ assert.strictEqual(writeCalls.length, 0);
245
+ // Verify setIamPolicy was still called
246
+ const iamCalls = fetchCalls.filter((c) => c.method === "POST" && c.url.includes(":setIamPolicy"));
247
+ assert.strictEqual(iamCalls.length, 1);
248
+ });
249
+ test("destroys an existing service", async () => {
250
+ // 1. Mock GET returning existing service config (discovery on destroy)
251
+ mockResponses["GET /services/to-delete"] = {
252
+ status: 200,
253
+ body: { name: "projects/my-gcp-project/locations/us-east1/services/to-delete" },
254
+ };
255
+ // 2. Mock DELETE
256
+ mockResponses["DELETE /services/to-delete"] = {
257
+ status: 200,
258
+ body: {},
259
+ };
260
+ const builder = new GCPCloudRunBuilder("to-delete");
261
+ const result = await builder.destroy();
262
+ assert.deepStrictEqual(result, { destroyed: "to-delete" });
263
+ // Verify DELETE was called
264
+ const deleteCalls = fetchCalls.filter((c) => c.method === "DELETE");
265
+ assert.strictEqual(deleteCalls.length, 1);
266
+ assert.strictEqual(deleteCalls[0].url.includes("/services/to-delete"), true);
267
+ });
268
+ test("destroy does nothing when service does not exist", async () => {
269
+ // 1. Mock GET returning 404
270
+ mockResponses["GET /services/not-exist"] = {
271
+ status: 404,
272
+ body: { message: "Not found" },
273
+ };
274
+ const builder = new GCPCloudRunBuilder("not-exist");
275
+ const result = await builder.destroy();
276
+ assert.deepStrictEqual(result, { destroyed: "not-exist" });
277
+ // Verify DELETE was NOT called
278
+ const deleteCalls = fetchCalls.filter((c) => c.method === "DELETE");
279
+ assert.strictEqual(deleteCalls.length, 0);
280
+ });
281
+ });
@@ -0,0 +1,37 @@
1
+ import { BaseBuilder } from "../../core/resource.js";
2
+ export declare class GCPCloudSQLBuilder extends BaseBuilder {
3
+ private _engine;
4
+ private _engineVersion;
5
+ private _tier;
6
+ private _storage;
7
+ private _username?;
8
+ private _password?;
9
+ private _dbName?;
10
+ private _publicAccess;
11
+ private _region?;
12
+ resolvedEndpoint: string | null;
13
+ resolvedPort: number | null;
14
+ resolvedConnectionName: string | null;
15
+ constructor(instanceId: string);
16
+ engine(e: {
17
+ engine: "postgres" | "mysql";
18
+ version: string;
19
+ }): this;
20
+ size(tier: string): this;
21
+ storage(gb: number): this;
22
+ credentials(username: string, password: string): this;
23
+ database(name: string): this;
24
+ publicAccess(enabled?: boolean): this;
25
+ region(reg: string): this;
26
+ private discoverInstance;
27
+ private waitForOperation;
28
+ deploy(): Promise<{
29
+ name: string;
30
+ endpoint: string | null;
31
+ port: number;
32
+ connectionName: string | null;
33
+ }>;
34
+ destroy(): Promise<{
35
+ destroyed: string;
36
+ }>;
37
+ }
@@ -0,0 +1,262 @@
1
+ import { BaseBuilder } from "../../core/resource.js";
2
+ import { gcpFetch, getProjectId, getRegion } from "./api.js";
3
+ const SQL_BASE = "https://sqladmin.googleapis.com";
4
+ const DB_PORT = {
5
+ postgres: 5432,
6
+ postgresql: 5432,
7
+ mysql: 3306,
8
+ };
9
+ function formatDatabaseVersion(engine, version) {
10
+ const eng = engine.toLowerCase();
11
+ if (eng === "postgres" || eng === "postgresql") {
12
+ return `POSTGRES_${version.split(".")[0]}`;
13
+ }
14
+ if (eng === "mysql") {
15
+ return `MYSQL_${version.replace(/\./g, "_")}`;
16
+ }
17
+ return `${engine.toUpperCase()}_${version}`;
18
+ }
19
+ export class GCPCloudSQLBuilder extends BaseBuilder {
20
+ _engine = "postgres";
21
+ _engineVersion = "16";
22
+ _tier = "db-f1-micro";
23
+ _storage = 10;
24
+ _username;
25
+ _password;
26
+ _dbName;
27
+ _publicAccess = false;
28
+ _region;
29
+ resolvedEndpoint = null;
30
+ resolvedPort = null;
31
+ resolvedConnectionName = null;
32
+ constructor(instanceId) {
33
+ super(instanceId);
34
+ this.discoveryPromise = this.discoverInstance();
35
+ }
36
+ engine(e) {
37
+ this._engine = e.engine;
38
+ this._engineVersion = e.version;
39
+ return this;
40
+ }
41
+ size(tier) {
42
+ this._tier = tier;
43
+ return this;
44
+ }
45
+ storage(gb) {
46
+ this._storage = gb;
47
+ return this;
48
+ }
49
+ credentials(username, password) {
50
+ this._username = username;
51
+ this._password = password;
52
+ return this;
53
+ }
54
+ database(name) {
55
+ this._dbName = name;
56
+ return this;
57
+ }
58
+ publicAccess(enabled = true) {
59
+ this._publicAccess = enabled;
60
+ return this;
61
+ }
62
+ region(reg) {
63
+ this._region = reg;
64
+ this.discoveryPromise = this.discoverInstance();
65
+ return this;
66
+ }
67
+ async discoverInstance() {
68
+ try {
69
+ const project = getProjectId();
70
+ const instanceId = this.name;
71
+ const res = await gcpFetch(SQL_BASE, `/v1/projects/${project}/instances/${instanceId}`);
72
+ if (res.state === "DELETED")
73
+ return null;
74
+ const primaryIp = res.ipAddresses?.find((ip) => ip.type === "PRIMARY")?.ipAddress;
75
+ this.resolvedEndpoint = primaryIp ?? null;
76
+ this.resolvedPort = DB_PORT[this._engine] ?? 5432;
77
+ this.resolvedConnectionName = res.connectionName ?? null;
78
+ return res;
79
+ }
80
+ catch (e) {
81
+ if (e.message?.includes("404") ||
82
+ e.message?.includes("403") ||
83
+ e.message?.includes("credentials not configured")) {
84
+ return null;
85
+ }
86
+ throw e;
87
+ }
88
+ }
89
+ async waitForOperation(opName, label) {
90
+ const project = getProjectId();
91
+ await this.waitFor(label, async () => {
92
+ const op = await gcpFetch(SQL_BASE, `/v1/projects/${project}/operations/${opName}`);
93
+ if (op.status === "DONE") {
94
+ if (op.error) {
95
+ throw new Error(`Cloud SQL Operation failed: ${JSON.stringify(op.error)}`);
96
+ }
97
+ return true;
98
+ }
99
+ return false;
100
+ }, { intervalMs: 10_000, timeoutMs: 900_000 });
101
+ }
102
+ async deploy() {
103
+ const dryRun = this.isDryRunActive();
104
+ const project = getProjectId();
105
+ const location = this._region ?? getRegion();
106
+ const instanceId = this.name;
107
+ const port = DB_PORT[this._engine] ?? 5432;
108
+ console.log(`\n⚡ Finalizing GCP Cloud SQL Instance "${instanceId}"...`);
109
+ if (!this._username || !this._password) {
110
+ throw new Error(`[GCP.CloudSQL:${instanceId}] .credentials(username, password) is required`);
111
+ }
112
+ const existing = await this.discoveryPromise;
113
+ const targetDbVersion = formatDatabaseVersion(this._engine, this._engineVersion);
114
+ const targetAuthorizedNetworks = this._publicAccess
115
+ ? [{ value: "0.0.0.0/0", name: "internet" }]
116
+ : [];
117
+ if (dryRun) {
118
+ console.log(` 📝 [PLAN] ${existing ? "Update" : "Create"} Cloud SQL instance "${instanceId}" in ${location}`);
119
+ console.log(` └─ Engine: ${this._engine} (${targetDbVersion})`);
120
+ console.log(` └─ Tier: ${this._tier} | Disk: ${this._storage}GB`);
121
+ console.log(` └─ Public Access: ${this._publicAccess ? "enabled" : "disabled"}`);
122
+ if (this._dbName) {
123
+ console.log(` └─ Database to create: "${this._dbName}"`);
124
+ }
125
+ if (this._username && this._username !== "postgres" && this._username !== "root") {
126
+ console.log(` └─ Custom user to create: "${this._username}"`);
127
+ }
128
+ this.resolvedEndpoint = "127.0.0.1";
129
+ this.resolvedPort = port;
130
+ this.resolvedConnectionName = `${project}:${location}:${instanceId}`;
131
+ return {
132
+ name: instanceId,
133
+ endpoint: this.resolvedEndpoint,
134
+ port: this.resolvedPort,
135
+ connectionName: this.resolvedConnectionName,
136
+ };
137
+ }
138
+ let needsUpdate = !existing;
139
+ if (existing) {
140
+ const existingSettings = existing.settings ?? {};
141
+ const existingNetworks = existingSettings.ipConfiguration?.authorizedNetworks ?? [];
142
+ const hasTierChange = existingSettings.tier !== this._tier;
143
+ const hasStorageChange = Number(existingSettings.dataDiskSizeGb ?? 0) < this._storage;
144
+ const hasNetworkChange = JSON.stringify(existingNetworks) !== JSON.stringify(targetAuthorizedNetworks);
145
+ needsUpdate = hasTierChange || hasStorageChange || hasNetworkChange;
146
+ }
147
+ const settings = {
148
+ tier: this._tier,
149
+ dataDiskSizeGb: String(this._storage),
150
+ ipConfiguration: {
151
+ ipv4Enabled: true,
152
+ authorizedNetworks: targetAuthorizedNetworks,
153
+ },
154
+ };
155
+ let currentInstance;
156
+ if (!existing) {
157
+ console.log(`🚀 Creating GCP Cloud SQL instance "${instanceId}" (takes several minutes)...`);
158
+ const op = await gcpFetch(SQL_BASE, `/v1/projects/${project}/instances`, {
159
+ method: "POST",
160
+ body: JSON.stringify({
161
+ name: instanceId,
162
+ databaseVersion: targetDbVersion,
163
+ region: location,
164
+ rootPassword: this._password,
165
+ settings,
166
+ }),
167
+ });
168
+ await this.waitForOperation(op.name, `Instance creation "${instanceId}"`);
169
+ currentInstance = await gcpFetch(SQL_BASE, `/v1/projects/${project}/instances/${instanceId}`);
170
+ }
171
+ else if (needsUpdate) {
172
+ console.log(`🔄 Updating GCP Cloud SQL instance "${instanceId}"...`);
173
+ const op = await gcpFetch(SQL_BASE, `/v1/projects/${project}/instances/${instanceId}`, {
174
+ method: "PATCH",
175
+ body: JSON.stringify({ settings }),
176
+ });
177
+ await this.waitForOperation(op.name, `Instance update "${instanceId}"`);
178
+ currentInstance = await gcpFetch(SQL_BASE, `/v1/projects/${project}/instances/${instanceId}`);
179
+ }
180
+ else {
181
+ console.log(`✅ GCP Cloud SQL instance "${instanceId}" is up to date.`);
182
+ currentInstance = existing;
183
+ }
184
+ const primaryIp = currentInstance.ipAddresses?.find((ip) => ip.type === "PRIMARY")?.ipAddress;
185
+ this.resolvedEndpoint = primaryIp ?? null;
186
+ this.resolvedPort = port;
187
+ this.resolvedConnectionName = currentInstance.connectionName ?? null;
188
+ if (this._dbName) {
189
+ console.log(` 🗄️ Ensuring database "${this._dbName}" exists...`);
190
+ try {
191
+ await gcpFetch(SQL_BASE, `/v1/projects/${project}/instances/${instanceId}/databases`, {
192
+ method: "POST",
193
+ body: JSON.stringify({
194
+ name: this._dbName,
195
+ instance: instanceId,
196
+ project,
197
+ }),
198
+ });
199
+ console.log(` ✅ Database "${this._dbName}" created.`);
200
+ }
201
+ catch (e) {
202
+ if (!e.message?.includes("409")) {
203
+ throw e;
204
+ }
205
+ console.log(` ✅ Database "${this._dbName}" already exists.`);
206
+ }
207
+ }
208
+ const defaultAdmin = this._engine === "postgres" ? "postgres" : "root";
209
+ if (this._username && this._username !== defaultAdmin) {
210
+ console.log(` 👤 Ensuring custom user "${this._username}" exists...`);
211
+ try {
212
+ await gcpFetch(SQL_BASE, `/v1/projects/${project}/instances/${instanceId}/users`, {
213
+ method: "POST",
214
+ body: JSON.stringify({
215
+ name: this._username,
216
+ password: this._password,
217
+ instance: instanceId,
218
+ project,
219
+ }),
220
+ });
221
+ console.log(` ✅ Custom user "${this._username}" created.`);
222
+ }
223
+ catch (e) {
224
+ if (!e.message?.includes("409")) {
225
+ throw e;
226
+ }
227
+ console.log(` ✅ Custom user "${this._username}" already exists.`);
228
+ }
229
+ }
230
+ await this.deploySidecars();
231
+ console.log(`🚀 Database available → ${this.resolvedEndpoint}:${this.resolvedPort}`);
232
+ return {
233
+ name: instanceId,
234
+ endpoint: this.resolvedEndpoint,
235
+ port: this.resolvedPort,
236
+ connectionName: this.resolvedConnectionName,
237
+ };
238
+ }
239
+ async destroy() {
240
+ const dryRun = this.isDryRunActive();
241
+ const project = getProjectId();
242
+ const instanceId = this.name;
243
+ console.log(`\n🗑️ Destroying GCP Cloud SQL Instance "${instanceId}"...`);
244
+ const existing = await this.discoverInstance();
245
+ if (!existing) {
246
+ console.log(` ✅ Instance "${instanceId}" does not exist - nothing to do.`);
247
+ return { destroyed: instanceId };
248
+ }
249
+ if (dryRun) {
250
+ console.log(` 📝 [PLAN] Delete Cloud SQL instance "${instanceId}"`);
251
+ return { destroyed: instanceId };
252
+ }
253
+ console.log(` 🔄 Deleting Cloud SQL instance "${instanceId}"...`);
254
+ const op = await gcpFetch(SQL_BASE, `/v1/projects/${project}/instances/${instanceId}`, {
255
+ method: "DELETE",
256
+ });
257
+ await this.waitForOperation(op.name, `Instance deletion "${instanceId}"`);
258
+ console.log(` ✅ Instance "${instanceId}" deleted.`);
259
+ await this.destroySidecars();
260
+ return { destroyed: instanceId };
261
+ }
262
+ }
@@ -0,0 +1 @@
1
+ export {};