puls-dev 0.2.6 ā 0.2.8
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/README.md +1 -1
- package/dist/core/config.d.ts +2 -0
- package/dist/core/decorators.d.ts +2 -0
- package/dist/core/decorators.js +48 -16
- package/dist/core/hooks.d.ts +21 -0
- package/dist/core/hooks.js +116 -0
- package/dist/core/hooks.test.d.ts +1 -0
- package/dist/core/hooks.test.js +194 -0
- package/dist/core/multiregion.test.d.ts +1 -0
- package/dist/core/multiregion.test.js +87 -0
- package/dist/core/output.d.ts +2 -0
- package/dist/core/output.js +9 -2
- package/dist/core/parser.d.ts +10 -0
- package/dist/core/parser.js +140 -0
- package/dist/core/parser.test.d.ts +1 -0
- package/dist/core/parser.test.js +117 -0
- package/dist/core/provisioner.d.ts +4 -0
- package/dist/core/provisioner.js +105 -0
- package/dist/core/resource.d.ts +16 -0
- package/dist/core/resource.js +44 -0
- package/dist/core/secret.d.ts +40 -0
- package/dist/core/secret.js +95 -0
- package/dist/core/secret.test.d.ts +1 -0
- package/dist/core/secret.test.js +166 -0
- package/dist/core/stack.d.ts +4 -3
- package/dist/core/stack.js +50 -9
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/providers/aws/ec2.d.ts +48 -0
- package/dist/providers/aws/ec2.js +297 -0
- package/dist/providers/aws/ec2.test.d.ts +1 -0
- package/dist/providers/aws/ec2.test.js +279 -0
- package/dist/providers/aws/index.d.ts +2 -0
- package/dist/providers/aws/index.js +2 -0
- package/dist/providers/aws/route53.d.ts +1 -0
- package/dist/providers/aws/route53.js +15 -2
- package/dist/providers/aws/route53.test.js +47 -0
- package/dist/providers/do/api.d.ts +1 -1
- package/dist/providers/do/api.js +2 -1
- package/dist/providers/do/app.d.ts +26 -0
- package/dist/providers/do/app.js +124 -0
- package/dist/providers/do/app.test.d.ts +1 -0
- package/dist/providers/do/app.test.js +268 -0
- package/dist/providers/do/database.d.ts +44 -0
- package/dist/providers/do/database.js +208 -0
- package/dist/providers/do/database.test.d.ts +1 -0
- package/dist/providers/do/database.test.js +293 -0
- package/dist/providers/do/domain.d.ts +2 -0
- package/dist/providers/do/domain.js +30 -0
- package/dist/providers/do/domain.test.js +49 -0
- package/dist/providers/do/droplet.d.ts +9 -0
- package/dist/providers/do/droplet.js +132 -8
- package/dist/providers/do/droplet.test.js +228 -1
- package/dist/providers/do/firewall.d.ts +2 -1
- package/dist/providers/do/firewall.js +23 -9
- package/dist/providers/do/firewall.test.js +54 -0
- package/dist/providers/do/index.d.ts +11 -0
- package/dist/providers/do/index.js +8 -0
- package/dist/providers/do/spaces.d.ts +27 -0
- package/dist/providers/do/spaces.js +142 -0
- package/dist/providers/do/spaces.test.d.ts +1 -0
- package/dist/providers/do/spaces.test.js +180 -0
- package/dist/providers/do/spaces_api.d.ts +2 -0
- package/dist/providers/do/spaces_api.js +20 -0
- package/dist/providers/do/vpc.d.ts +30 -0
- package/dist/providers/do/vpc.js +128 -0
- package/dist/providers/do/vpc.test.d.ts +1 -0
- package/dist/providers/do/vpc.test.js +258 -0
- package/dist/providers/gcp/clouddns.d.ts +1 -0
- package/dist/providers/gcp/clouddns.js +15 -2
- package/dist/providers/gcp/clouddns.test.js +45 -0
- package/dist/providers/gcp/index.d.ts +3 -1
- package/dist/providers/gcp/index.js +3 -1
- package/dist/providers/gcp/vm.d.ts +45 -0
- package/dist/providers/gcp/vm.js +332 -0
- package/dist/providers/gcp/vm.test.d.ts +1 -0
- package/dist/providers/gcp/vm.test.js +321 -0
- package/dist/providers/proxmox/hash.d.ts +3 -0
- package/dist/providers/proxmox/hash.js +46 -0
- package/dist/providers/proxmox/vm.d.ts +8 -7
- package/dist/providers/proxmox/vm.js +126 -106
- package/dist/providers/proxmox/vm.test.js +224 -0
- package/package.json +3 -1
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
import { test, describe, beforeEach, afterEach } from "node:test";
|
|
2
|
+
import assert from "node:assert";
|
|
3
|
+
import { AppPlatformBuilder } from "./app.js";
|
|
4
|
+
import { Config } from "../../core/config.js";
|
|
5
|
+
describe("AppPlatformBuilder Unit Tests", () => {
|
|
6
|
+
let originalFetch;
|
|
7
|
+
let fetchCalls = [];
|
|
8
|
+
let mockResponses = {};
|
|
9
|
+
beforeEach(() => {
|
|
10
|
+
Config.set({
|
|
11
|
+
dryRun: false,
|
|
12
|
+
providers: {
|
|
13
|
+
do: { token: "fake-do-token", defaultRegion: "nyc3" },
|
|
14
|
+
},
|
|
15
|
+
});
|
|
16
|
+
originalFetch = globalThis.fetch;
|
|
17
|
+
fetchCalls = [];
|
|
18
|
+
mockResponses = {};
|
|
19
|
+
globalThis.fetch = async (input, init) => {
|
|
20
|
+
const url = String(input);
|
|
21
|
+
const method = init?.method ?? "GET";
|
|
22
|
+
const body = init?.body ? JSON.parse(init.body) : undefined;
|
|
23
|
+
const headers = init?.headers;
|
|
24
|
+
fetchCalls.push({ url, method, body, headers });
|
|
25
|
+
const matchKey = Object.keys(mockResponses)
|
|
26
|
+
.filter((key) => {
|
|
27
|
+
const [mMethod, mPath] = key.split(" ");
|
|
28
|
+
return method === mMethod && url.includes(mPath);
|
|
29
|
+
})
|
|
30
|
+
.sort((a, b) => b.split(" ")[1].length - a.split(" ")[1].length)[0];
|
|
31
|
+
if (matchKey) {
|
|
32
|
+
const resp = mockResponses[matchKey];
|
|
33
|
+
return {
|
|
34
|
+
ok: resp.status >= 200 && resp.status < 300,
|
|
35
|
+
status: resp.status,
|
|
36
|
+
json: async () => resp.body,
|
|
37
|
+
text: async () => JSON.stringify(resp.body),
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
return {
|
|
41
|
+
ok: false,
|
|
42
|
+
status: 404,
|
|
43
|
+
json: async () => ({ message: "Not found" }),
|
|
44
|
+
text: async () => "Not found",
|
|
45
|
+
};
|
|
46
|
+
};
|
|
47
|
+
});
|
|
48
|
+
afterEach(() => {
|
|
49
|
+
globalThis.fetch = originalFetch;
|
|
50
|
+
});
|
|
51
|
+
test("gracefully handles discovery when App does not exist", async () => {
|
|
52
|
+
mockResponses["GET /apps"] = {
|
|
53
|
+
status: 200,
|
|
54
|
+
body: { apps: [] },
|
|
55
|
+
};
|
|
56
|
+
const builder = new AppPlatformBuilder("my-app");
|
|
57
|
+
const discoveryResult = await builder.discoveryPromise;
|
|
58
|
+
assert.strictEqual(discoveryResult, null);
|
|
59
|
+
assert.strictEqual(fetchCalls.length, 1);
|
|
60
|
+
assert.strictEqual(fetchCalls[0].method, "GET");
|
|
61
|
+
assert.ok(fetchCalls[0].url.includes("/apps"));
|
|
62
|
+
});
|
|
63
|
+
test("discovers App successfully when it exists", async () => {
|
|
64
|
+
mockResponses["GET /apps"] = {
|
|
65
|
+
status: 200,
|
|
66
|
+
body: {
|
|
67
|
+
apps: [
|
|
68
|
+
{
|
|
69
|
+
id: "app-123",
|
|
70
|
+
spec: { name: "my-app" },
|
|
71
|
+
live_url: "https://my-app.ondigitalocean.app",
|
|
72
|
+
},
|
|
73
|
+
],
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
const builder = new AppPlatformBuilder("my-app");
|
|
77
|
+
const discoveryResult = await builder.discoveryPromise;
|
|
78
|
+
assert.ok(discoveryResult);
|
|
79
|
+
assert.strictEqual(discoveryResult.id, "app-123");
|
|
80
|
+
const id = await builder.out.id.get();
|
|
81
|
+
const liveUrl = await builder.out.liveUrl.get();
|
|
82
|
+
assert.strictEqual(id, "app-123");
|
|
83
|
+
assert.strictEqual(liveUrl, "https://my-app.ondigitalocean.app");
|
|
84
|
+
});
|
|
85
|
+
test("performs clean dry-run planning without making write requests", async () => {
|
|
86
|
+
Config.set({
|
|
87
|
+
dryRun: true,
|
|
88
|
+
providers: { do: { token: "fake-token" } },
|
|
89
|
+
});
|
|
90
|
+
mockResponses["GET /apps"] = {
|
|
91
|
+
status: 200,
|
|
92
|
+
body: { apps: [] },
|
|
93
|
+
};
|
|
94
|
+
const builder = new AppPlatformBuilder("my-dry-app")
|
|
95
|
+
.spec({
|
|
96
|
+
region: "nyc",
|
|
97
|
+
services: [
|
|
98
|
+
{
|
|
99
|
+
name: "web",
|
|
100
|
+
instance_size_slug: "apps-s-1vcpu-1gb",
|
|
101
|
+
},
|
|
102
|
+
],
|
|
103
|
+
});
|
|
104
|
+
const result = await builder.deploy();
|
|
105
|
+
assert.ok(result);
|
|
106
|
+
assert.strictEqual(result.name, "my-dry-app");
|
|
107
|
+
assert.strictEqual(result.id, "PENDING");
|
|
108
|
+
// Discover should run, but no creations/updates
|
|
109
|
+
const writeCalls = fetchCalls.filter((c) => c.method !== "GET");
|
|
110
|
+
assert.strictEqual(writeCalls.length, 0);
|
|
111
|
+
const liveUrl = await builder.out.liveUrl.get();
|
|
112
|
+
assert.strictEqual(liveUrl, "https://my-dry-app.ondigitalocean.app");
|
|
113
|
+
});
|
|
114
|
+
test("deploys new App and awaits status: active with live url", async () => {
|
|
115
|
+
mockResponses["GET /apps"] = {
|
|
116
|
+
status: 200,
|
|
117
|
+
body: { apps: [] },
|
|
118
|
+
};
|
|
119
|
+
mockResponses["POST /apps"] = {
|
|
120
|
+
status: 202,
|
|
121
|
+
body: { app: { id: "new-app-id", live_url: "" } },
|
|
122
|
+
};
|
|
123
|
+
let pollCount = 0;
|
|
124
|
+
mockResponses["GET /apps/new-app-id"] = {
|
|
125
|
+
status: 200,
|
|
126
|
+
get body() {
|
|
127
|
+
pollCount++;
|
|
128
|
+
if (pollCount === 1) {
|
|
129
|
+
return { app: { id: "new-app-id", live_url: "" } };
|
|
130
|
+
}
|
|
131
|
+
return {
|
|
132
|
+
app: {
|
|
133
|
+
id: "new-app-id",
|
|
134
|
+
live_url: "https://new-app.ondigitalocean.app",
|
|
135
|
+
},
|
|
136
|
+
};
|
|
137
|
+
},
|
|
138
|
+
};
|
|
139
|
+
const builder = new AppPlatformBuilder("my-new-app")
|
|
140
|
+
.spec({
|
|
141
|
+
region: "nyc",
|
|
142
|
+
services: [
|
|
143
|
+
{
|
|
144
|
+
name: "api",
|
|
145
|
+
github: { repo: "user/repo", branch: "main" },
|
|
146
|
+
},
|
|
147
|
+
],
|
|
148
|
+
});
|
|
149
|
+
// Instantly check status
|
|
150
|
+
builder.waitFor = async (label, condition) => {
|
|
151
|
+
let done = false;
|
|
152
|
+
while (!done) {
|
|
153
|
+
done = await condition();
|
|
154
|
+
}
|
|
155
|
+
};
|
|
156
|
+
const result = await builder.deploy();
|
|
157
|
+
assert.ok(result);
|
|
158
|
+
assert.strictEqual(result.id, "new-app-id");
|
|
159
|
+
assert.strictEqual(result.liveUrl, "https://new-app.ondigitalocean.app");
|
|
160
|
+
const liveUrl = await builder.out.liveUrl.get();
|
|
161
|
+
assert.strictEqual(liveUrl, "https://new-app.ondigitalocean.app");
|
|
162
|
+
const postCall = fetchCalls.find((c) => c.method === "POST" && c.url.includes("/apps"));
|
|
163
|
+
assert.ok(postCall);
|
|
164
|
+
assert.deepStrictEqual(postCall.body, {
|
|
165
|
+
spec: {
|
|
166
|
+
name: "my-new-app",
|
|
167
|
+
region: "nyc",
|
|
168
|
+
services: [
|
|
169
|
+
{
|
|
170
|
+
name: "api",
|
|
171
|
+
github: { repo: "user/repo", branch: "main" },
|
|
172
|
+
},
|
|
173
|
+
],
|
|
174
|
+
},
|
|
175
|
+
});
|
|
176
|
+
});
|
|
177
|
+
test("updates spec on existing app if configuration differs", async () => {
|
|
178
|
+
mockResponses["GET /apps"] = {
|
|
179
|
+
status: 200,
|
|
180
|
+
body: {
|
|
181
|
+
apps: [
|
|
182
|
+
{
|
|
183
|
+
id: "app-existing-id",
|
|
184
|
+
spec: {
|
|
185
|
+
name: "my-existing-app",
|
|
186
|
+
region: "nyc",
|
|
187
|
+
},
|
|
188
|
+
live_url: "https://my-existing-app.ondigitalocean.app",
|
|
189
|
+
},
|
|
190
|
+
],
|
|
191
|
+
},
|
|
192
|
+
};
|
|
193
|
+
mockResponses["PUT /apps/app-existing-id"] = {
|
|
194
|
+
status: 200,
|
|
195
|
+
body: {
|
|
196
|
+
app: {
|
|
197
|
+
id: "app-existing-id",
|
|
198
|
+
spec: {
|
|
199
|
+
name: "my-existing-app",
|
|
200
|
+
region: "ams",
|
|
201
|
+
},
|
|
202
|
+
live_url: "https://my-existing-app.ondigitalocean.app",
|
|
203
|
+
},
|
|
204
|
+
},
|
|
205
|
+
};
|
|
206
|
+
const builder = new AppPlatformBuilder("my-existing-app")
|
|
207
|
+
.spec({
|
|
208
|
+
region: "ams", // differs from existing "nyc"
|
|
209
|
+
});
|
|
210
|
+
const result = await builder.deploy();
|
|
211
|
+
assert.ok(result);
|
|
212
|
+
const putCall = fetchCalls.find((c) => c.method === "PUT" && c.url.includes("/apps/app-existing-id"));
|
|
213
|
+
assert.ok(putCall);
|
|
214
|
+
assert.deepStrictEqual(putCall.body, {
|
|
215
|
+
spec: {
|
|
216
|
+
name: "my-existing-app",
|
|
217
|
+
region: "ams",
|
|
218
|
+
},
|
|
219
|
+
});
|
|
220
|
+
});
|
|
221
|
+
test("skips deploy on existing app if spec matches exactly", async () => {
|
|
222
|
+
mockResponses["GET /apps"] = {
|
|
223
|
+
status: 200,
|
|
224
|
+
body: {
|
|
225
|
+
apps: [
|
|
226
|
+
{
|
|
227
|
+
id: "app-existing-id",
|
|
228
|
+
spec: {
|
|
229
|
+
name: "my-existing-app",
|
|
230
|
+
region: "nyc",
|
|
231
|
+
},
|
|
232
|
+
live_url: "https://my-existing-app.ondigitalocean.app",
|
|
233
|
+
},
|
|
234
|
+
],
|
|
235
|
+
},
|
|
236
|
+
};
|
|
237
|
+
const builder = new AppPlatformBuilder("my-existing-app")
|
|
238
|
+
.spec({
|
|
239
|
+
region: "nyc", // matches exactly
|
|
240
|
+
});
|
|
241
|
+
const result = await builder.deploy();
|
|
242
|
+
assert.ok(result);
|
|
243
|
+
// Verify no PUT writes
|
|
244
|
+
const putCall = fetchCalls.find((c) => c.method === "PUT");
|
|
245
|
+
assert.ok(!putCall);
|
|
246
|
+
});
|
|
247
|
+
test("destroys App successfully", async () => {
|
|
248
|
+
mockResponses["GET /apps"] = {
|
|
249
|
+
status: 200,
|
|
250
|
+
body: {
|
|
251
|
+
apps: [
|
|
252
|
+
{ id: "app-123", spec: { name: "my-app-del" } },
|
|
253
|
+
],
|
|
254
|
+
},
|
|
255
|
+
};
|
|
256
|
+
mockResponses["DELETE /apps/app-123"] = {
|
|
257
|
+
status: 204,
|
|
258
|
+
body: {},
|
|
259
|
+
};
|
|
260
|
+
const builder = new AppPlatformBuilder("my-app-del");
|
|
261
|
+
await builder.discoveryPromise;
|
|
262
|
+
const result = await builder.destroy();
|
|
263
|
+
assert.deepStrictEqual(result, { destroyed: "my-app-del" });
|
|
264
|
+
const deleteCall = fetchCalls.find((c) => c.method === "DELETE");
|
|
265
|
+
assert.ok(deleteCall);
|
|
266
|
+
assert.ok(deleteCall.url.endsWith("/apps/app-123"));
|
|
267
|
+
});
|
|
268
|
+
});
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { BaseBuilder } from "../../core/resource.js";
|
|
2
|
+
import { Output } from "../../core/output.js";
|
|
3
|
+
export declare class DatabaseBuilder extends BaseBuilder {
|
|
4
|
+
readonly out: {
|
|
5
|
+
host: Output<string>;
|
|
6
|
+
port: Output<number>;
|
|
7
|
+
uri: Output<string>;
|
|
8
|
+
user: Output<string>;
|
|
9
|
+
password: Output<string>;
|
|
10
|
+
id: Output<string>;
|
|
11
|
+
};
|
|
12
|
+
private _engine;
|
|
13
|
+
private _version;
|
|
14
|
+
private _size;
|
|
15
|
+
private _region;
|
|
16
|
+
private _nodes;
|
|
17
|
+
private _vpcUuid?;
|
|
18
|
+
private _firewallRules;
|
|
19
|
+
constructor(name: string);
|
|
20
|
+
engine(type: "pg" | "mysql" | "redis" | "mongodb" | "valkey" | "kafka"): this;
|
|
21
|
+
version(v: string): this;
|
|
22
|
+
size(slug: string): this;
|
|
23
|
+
region(r: string): this;
|
|
24
|
+
nodes(num: number): this;
|
|
25
|
+
vpc(uuid: string): this;
|
|
26
|
+
allowIp(cidr: string): this;
|
|
27
|
+
allowDroplet(dropletId: string): this;
|
|
28
|
+
allowTag(tagName: string): this;
|
|
29
|
+
private discoverCluster;
|
|
30
|
+
deploy(): Promise<{
|
|
31
|
+
name: string;
|
|
32
|
+
id: any;
|
|
33
|
+
status: any;
|
|
34
|
+
} | {
|
|
35
|
+
name: string;
|
|
36
|
+
id: string;
|
|
37
|
+
status?: undefined;
|
|
38
|
+
}>;
|
|
39
|
+
destroy(): Promise<{
|
|
40
|
+
destroyed: boolean;
|
|
41
|
+
} | {
|
|
42
|
+
destroyed: string;
|
|
43
|
+
}>;
|
|
44
|
+
}
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import { BaseBuilder } from "../../core/resource.js";
|
|
2
|
+
import { getDoApi } from "./api.js";
|
|
3
|
+
import { Output } from "../../core/output.js";
|
|
4
|
+
export class DatabaseBuilder extends BaseBuilder {
|
|
5
|
+
out = {
|
|
6
|
+
host: new Output(),
|
|
7
|
+
port: new Output(),
|
|
8
|
+
uri: new Output(),
|
|
9
|
+
user: new Output(),
|
|
10
|
+
password: new Output(),
|
|
11
|
+
id: new Output(),
|
|
12
|
+
};
|
|
13
|
+
_engine = "pg";
|
|
14
|
+
_version = "15";
|
|
15
|
+
_size = "db-s-1vcpu-1gb";
|
|
16
|
+
_region = "nyc3";
|
|
17
|
+
_nodes = 1;
|
|
18
|
+
_vpcUuid;
|
|
19
|
+
_firewallRules = [];
|
|
20
|
+
constructor(name) {
|
|
21
|
+
super(name);
|
|
22
|
+
this.discoveryPromise = this.discoverCluster(name);
|
|
23
|
+
}
|
|
24
|
+
engine(type) {
|
|
25
|
+
this._engine = type;
|
|
26
|
+
return this;
|
|
27
|
+
}
|
|
28
|
+
version(v) {
|
|
29
|
+
this._version = v;
|
|
30
|
+
return this;
|
|
31
|
+
}
|
|
32
|
+
size(slug) {
|
|
33
|
+
this._size = slug;
|
|
34
|
+
return this;
|
|
35
|
+
}
|
|
36
|
+
region(r) {
|
|
37
|
+
this._region = r;
|
|
38
|
+
this.discoveryPromise = this.discoverCluster(this.name);
|
|
39
|
+
return this;
|
|
40
|
+
}
|
|
41
|
+
nodes(num) {
|
|
42
|
+
this._nodes = num;
|
|
43
|
+
return this;
|
|
44
|
+
}
|
|
45
|
+
vpc(uuid) {
|
|
46
|
+
this._vpcUuid = uuid;
|
|
47
|
+
return this;
|
|
48
|
+
}
|
|
49
|
+
allowIp(cidr) {
|
|
50
|
+
this._firewallRules.push({ type: "ip_addr", value: cidr });
|
|
51
|
+
return this;
|
|
52
|
+
}
|
|
53
|
+
allowDroplet(dropletId) {
|
|
54
|
+
this._firewallRules.push({ type: "droplet", value: dropletId });
|
|
55
|
+
return this;
|
|
56
|
+
}
|
|
57
|
+
allowTag(tagName) {
|
|
58
|
+
this._firewallRules.push({ type: "tag", value: tagName });
|
|
59
|
+
return this;
|
|
60
|
+
}
|
|
61
|
+
async discoverCluster(name) {
|
|
62
|
+
try {
|
|
63
|
+
const api = getDoApi();
|
|
64
|
+
const res = await api.get("/databases");
|
|
65
|
+
const match = (res.databases ?? []).find((db) => db.name === name);
|
|
66
|
+
if (match) {
|
|
67
|
+
// Resolve connection outputs immediately
|
|
68
|
+
const conn = match.private_connection ?? match.connection;
|
|
69
|
+
if (conn) {
|
|
70
|
+
this.out.host.resolve(conn.host);
|
|
71
|
+
this.out.port.resolve(conn.port);
|
|
72
|
+
this.out.uri.resolve(conn.uri);
|
|
73
|
+
this.out.user.resolve(conn.user);
|
|
74
|
+
this.out.password.resolve(conn.password);
|
|
75
|
+
}
|
|
76
|
+
this.out.id.resolve(match.id);
|
|
77
|
+
}
|
|
78
|
+
return match ?? null;
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
async deploy() {
|
|
85
|
+
const dryRun = this.isDryRunActive();
|
|
86
|
+
const existing = await this.discoveryPromise;
|
|
87
|
+
const api = getDoApi();
|
|
88
|
+
console.log(`\nšļø Finalizing DigitalOcean Database Cluster "${this.name}"...`);
|
|
89
|
+
if (existing) {
|
|
90
|
+
console.log(` ā
Database Cluster "${this.name}" already exists (id=${existing.id}, status=${existing.status}).`);
|
|
91
|
+
const conn = existing.private_connection ?? existing.connection;
|
|
92
|
+
if (conn) {
|
|
93
|
+
this.out.host.resolve(conn.host);
|
|
94
|
+
this.out.port.resolve(conn.port);
|
|
95
|
+
this.out.uri.resolve(conn.uri);
|
|
96
|
+
this.out.user.resolve(conn.user);
|
|
97
|
+
this.out.password.resolve(conn.password);
|
|
98
|
+
}
|
|
99
|
+
this.out.id.resolve(existing.id);
|
|
100
|
+
// Handle firewall rules updates
|
|
101
|
+
if (this._firewallRules.length > 0) {
|
|
102
|
+
if (dryRun) {
|
|
103
|
+
console.log(` š [PLAN] Update Database Firewall Rules (replace list):`);
|
|
104
|
+
for (const rule of this._firewallRules) {
|
|
105
|
+
console.log(` āā Rule: ${rule.type}:${rule.value}`);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
else {
|
|
109
|
+
await api.put(`/databases/${existing.id}/firewall`, {
|
|
110
|
+
rules: this._firewallRules,
|
|
111
|
+
});
|
|
112
|
+
console.log(` ā
Database Firewall Rules updated successfully.`);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
return {
|
|
116
|
+
name: this.name,
|
|
117
|
+
id: existing.id,
|
|
118
|
+
status: existing.status,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
if (dryRun) {
|
|
122
|
+
console.log(` š [PLAN] Create DigitalOcean Database Cluster "${this.name}" (${this._region})`);
|
|
123
|
+
console.log(` āā Engine: ${this._engine} (version: ${this._version})`);
|
|
124
|
+
console.log(` āā Size: ${this._size} | Nodes: ${this._nodes}`);
|
|
125
|
+
if (this._vpcUuid) {
|
|
126
|
+
console.log(` āā VPC Network: ${this._vpcUuid}`);
|
|
127
|
+
}
|
|
128
|
+
if (this._firewallRules.length > 0) {
|
|
129
|
+
console.log(` āā Firewall Rules to apply:`);
|
|
130
|
+
for (const rule of this._firewallRules) {
|
|
131
|
+
console.log(` āā ${rule.type}:${rule.value}`);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
this.out.host.resolve("127.0.0.1");
|
|
135
|
+
this.out.port.resolve(5432);
|
|
136
|
+
this.out.uri.resolve("postgresql://db:pass@127.0.0.1:5432/db");
|
|
137
|
+
this.out.user.resolve("db");
|
|
138
|
+
this.out.password.resolve("pass");
|
|
139
|
+
this.out.id.resolve("PENDING");
|
|
140
|
+
return { name: this.name, id: "PENDING" };
|
|
141
|
+
}
|
|
142
|
+
// Create the database cluster
|
|
143
|
+
console.log(`š Creating DigitalOcean Database Cluster "${this.name}" (takes several minutes)...`);
|
|
144
|
+
const body = {
|
|
145
|
+
name: this.name,
|
|
146
|
+
engine: this._engine,
|
|
147
|
+
version: this._version,
|
|
148
|
+
region: this._region,
|
|
149
|
+
size: this._size,
|
|
150
|
+
num_nodes: this._nodes,
|
|
151
|
+
};
|
|
152
|
+
if (this._vpcUuid) {
|
|
153
|
+
body.private_network_uuid = this._vpcUuid;
|
|
154
|
+
}
|
|
155
|
+
const createRes = await api.post("/databases", body);
|
|
156
|
+
const dbCluster = createRes.database;
|
|
157
|
+
console.log(`š Database Cluster created with ID: ${dbCluster.id}`);
|
|
158
|
+
// Wait for the database cluster to become active
|
|
159
|
+
await this.waitFor(`Database Cluster "${this.name}" to become active`, async () => {
|
|
160
|
+
const check = await api.get(`/databases/${dbCluster.id}`);
|
|
161
|
+
if (check.database && check.database.status === "online") {
|
|
162
|
+
const conn = check.database.private_connection ?? check.database.connection;
|
|
163
|
+
if (conn) {
|
|
164
|
+
this.out.host.resolve(conn.host);
|
|
165
|
+
this.out.port.resolve(conn.port);
|
|
166
|
+
this.out.uri.resolve(conn.uri);
|
|
167
|
+
this.out.user.resolve(conn.user);
|
|
168
|
+
this.out.password.resolve(conn.password);
|
|
169
|
+
}
|
|
170
|
+
this.out.id.resolve(check.database.id);
|
|
171
|
+
return true;
|
|
172
|
+
}
|
|
173
|
+
return false;
|
|
174
|
+
}, { intervalMs: 15_000, timeoutMs: 900_000 });
|
|
175
|
+
// Apply database firewall rules (trusted sources) if specified
|
|
176
|
+
if (this._firewallRules.length > 0) {
|
|
177
|
+
console.log(` š Applying Database Firewall Rules...`);
|
|
178
|
+
await api.put(`/databases/${dbCluster.id}/firewall`, {
|
|
179
|
+
rules: this._firewallRules,
|
|
180
|
+
});
|
|
181
|
+
console.log(` ā
Database Firewall Rules applied.`);
|
|
182
|
+
}
|
|
183
|
+
console.log(`š Database available.`);
|
|
184
|
+
return {
|
|
185
|
+
name: this.name,
|
|
186
|
+
id: dbCluster.id,
|
|
187
|
+
status: "online",
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
async destroy() {
|
|
191
|
+
const dryRun = this.isDryRunActive();
|
|
192
|
+
const existing = await this.discoveryPromise;
|
|
193
|
+
const api = getDoApi();
|
|
194
|
+
console.log(`\nšļø Destroying DigitalOcean Database Cluster "${this.name}"...`);
|
|
195
|
+
if (!existing) {
|
|
196
|
+
console.log(` ā Database Cluster "${this.name}" not found`);
|
|
197
|
+
return { destroyed: false };
|
|
198
|
+
}
|
|
199
|
+
if (dryRun) {
|
|
200
|
+
console.log(` š [PLAN] Delete Database Cluster "${this.name}" (id=${existing.id})`);
|
|
201
|
+
return { destroyed: this.name };
|
|
202
|
+
}
|
|
203
|
+
console.log(` š Deleting Database Cluster "${this.name}" (id=${existing.id})...`);
|
|
204
|
+
await api.delete(`/databases/${existing.id}`);
|
|
205
|
+
console.log(` šļø Removed Database Cluster "${this.name}"`);
|
|
206
|
+
return { destroyed: this.name };
|
|
207
|
+
}
|
|
208
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|