relight-cli 0.1.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +77 -34
- package/package.json +12 -4
- package/src/cli.js +350 -1
- package/src/commands/apps.js +128 -0
- package/src/commands/auth.js +13 -4
- package/src/commands/config.js +282 -0
- package/src/commands/cost.js +593 -0
- package/src/commands/db.js +775 -0
- package/src/commands/deploy.js +264 -0
- package/src/commands/doctor.js +69 -13
- package/src/commands/domains.js +223 -0
- package/src/commands/logs.js +111 -0
- package/src/commands/open.js +42 -0
- package/src/commands/ps.js +121 -0
- package/src/commands/scale.js +132 -0
- package/src/commands/service.js +227 -0
- package/src/lib/clouds/aws.js +309 -35
- package/src/lib/clouds/cf.js +401 -2
- package/src/lib/clouds/gcp.js +255 -4
- package/src/lib/clouds/neon.js +147 -0
- package/src/lib/clouds/slicervm.js +139 -0
- package/src/lib/config.js +200 -2
- package/src/lib/docker.js +34 -0
- package/src/lib/link.js +31 -5
- package/src/lib/providers/aws/app.js +481 -0
- package/src/lib/providers/aws/db.js +504 -0
- package/src/lib/providers/aws/dns.js +232 -0
- package/src/lib/providers/aws/registry.js +59 -0
- package/src/lib/providers/cf/app.js +596 -0
- package/src/lib/providers/cf/bundle.js +70 -0
- package/src/lib/providers/cf/db.js +181 -0
- package/src/lib/providers/cf/dns.js +148 -0
- package/src/lib/providers/cf/registry.js +17 -0
- package/src/lib/providers/gcp/app.js +429 -0
- package/src/lib/providers/gcp/db.js +372 -0
- package/src/lib/providers/gcp/dns.js +166 -0
- package/src/lib/providers/gcp/registry.js +30 -0
- package/src/lib/providers/neon/db.js +306 -0
- package/src/lib/providers/resolve.js +79 -0
- package/src/lib/providers/slicervm/app.js +396 -0
- package/src/lib/providers/slicervm/db.js +33 -0
- package/src/lib/providers/slicervm/dns.js +58 -0
- package/src/lib/providers/slicervm/registry.js +7 -0
- package/worker-template/package.json +10 -0
- package/worker-template/src/index.js +260 -0
|
@@ -0,0 +1,504 @@
|
|
|
1
|
+
import { randomBytes } from "crypto";
|
|
2
|
+
import { awsQueryApi, awsJsonApi, xmlVal, xmlList, xmlBlock } from "../../clouds/aws.js";
|
|
3
|
+
import { getCloudMeta, setCloudMeta } from "../../config.js";
|
|
4
|
+
|
|
5
|
+
var SHARED_INSTANCE = "relight-shared";
|
|
6
|
+
|
|
7
|
+
function userName(name) {
|
|
8
|
+
return `app_${name.replace(/-/g, "_")}`;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function dbName(name) {
|
|
12
|
+
return `relight_${name.replace(/-/g, "_")}`;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function isSharedInstance(dbId) {
|
|
16
|
+
return dbId === SHARED_INSTANCE;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
async function connectPg(connectionUrl) {
|
|
20
|
+
var pg = await import("pg");
|
|
21
|
+
var Client = pg.default?.Client || pg.Client;
|
|
22
|
+
var client = new Client({ connectionString: connectionUrl });
|
|
23
|
+
await client.connect();
|
|
24
|
+
return client;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// --- Security group for RDS public access ---
|
|
28
|
+
|
|
29
|
+
async function ensureSecurityGroup(cfg) {
|
|
30
|
+
var cr = { accessKeyId: cfg.accessKeyId, secretAccessKey: cfg.secretAccessKey };
|
|
31
|
+
var sgName = "relight-rds-public";
|
|
32
|
+
|
|
33
|
+
// Check if security group exists
|
|
34
|
+
var descXml = await awsQueryApi(
|
|
35
|
+
"DescribeSecurityGroups",
|
|
36
|
+
{ "Filter.1.Name": "group-name", "Filter.1.Value.1": sgName },
|
|
37
|
+
"ec2",
|
|
38
|
+
cr,
|
|
39
|
+
cfg.region
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
var sgBlock = xmlBlock(descXml, "securityGroupInfo");
|
|
43
|
+
var existing = sgBlock ? xmlBlock(sgBlock, "item") : null;
|
|
44
|
+
if (existing) {
|
|
45
|
+
var sgId = xmlVal(existing, "groupId");
|
|
46
|
+
if (sgId) return sgId;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Get default VPC
|
|
50
|
+
var vpcXml = await awsQueryApi(
|
|
51
|
+
"DescribeVpcs",
|
|
52
|
+
{ "Filter.1.Name": "isDefault", "Filter.1.Value.1": "true" },
|
|
53
|
+
"ec2",
|
|
54
|
+
cr,
|
|
55
|
+
cfg.region
|
|
56
|
+
);
|
|
57
|
+
var vpcId = xmlVal(vpcXml, "vpcId");
|
|
58
|
+
if (!vpcId) throw new Error("No default VPC found. Create one or specify a VPC.");
|
|
59
|
+
|
|
60
|
+
// Create security group
|
|
61
|
+
var createXml = await awsQueryApi(
|
|
62
|
+
"CreateSecurityGroup",
|
|
63
|
+
{
|
|
64
|
+
GroupName: sgName,
|
|
65
|
+
GroupDescription: "Relight RDS public access on port 5432",
|
|
66
|
+
VpcId: vpcId,
|
|
67
|
+
},
|
|
68
|
+
"ec2",
|
|
69
|
+
cr,
|
|
70
|
+
cfg.region
|
|
71
|
+
);
|
|
72
|
+
var newSgId = xmlVal(createXml, "groupId");
|
|
73
|
+
if (!newSgId) throw new Error("Failed to create security group.");
|
|
74
|
+
|
|
75
|
+
// Authorize inbound on port 5432
|
|
76
|
+
await awsQueryApi(
|
|
77
|
+
"AuthorizeSecurityGroupIngress",
|
|
78
|
+
{
|
|
79
|
+
GroupId: newSgId,
|
|
80
|
+
"IpPermissions.1.IpProtocol": "tcp",
|
|
81
|
+
"IpPermissions.1.FromPort": "5432",
|
|
82
|
+
"IpPermissions.1.ToPort": "5432",
|
|
83
|
+
"IpPermissions.1.IpRanges.1.CidrIp": "0.0.0.0/0",
|
|
84
|
+
},
|
|
85
|
+
"ec2",
|
|
86
|
+
cr,
|
|
87
|
+
cfg.region
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
return newSgId;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// --- Poll for RDS instance to become available ---
|
|
94
|
+
|
|
95
|
+
async function waitForInstance(cfg, instName) {
|
|
96
|
+
var cr = { accessKeyId: cfg.accessKeyId, secretAccessKey: cfg.secretAccessKey };
|
|
97
|
+
|
|
98
|
+
for (var i = 0; i < 180; i++) {
|
|
99
|
+
var xml = await awsQueryApi(
|
|
100
|
+
"DescribeDBInstances",
|
|
101
|
+
{ DBInstanceIdentifier: instName },
|
|
102
|
+
"rds",
|
|
103
|
+
cr,
|
|
104
|
+
cfg.region
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
var status = xmlVal(xml, "DBInstanceStatus");
|
|
108
|
+
if (status === "available") return;
|
|
109
|
+
if (status === "failed" || status === "incompatible-parameters") {
|
|
110
|
+
throw new Error(`RDS instance reached status: ${status}`);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
await new Promise((r) => setTimeout(r, 10000));
|
|
114
|
+
}
|
|
115
|
+
throw new Error("Timed out waiting for RDS instance to become available.");
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// --- Get RDS endpoint ---
|
|
119
|
+
|
|
120
|
+
function getRdsEndpoint(xml) {
|
|
121
|
+
var endpointBlock = xmlBlock(xml, "Endpoint");
|
|
122
|
+
if (!endpointBlock) return null;
|
|
123
|
+
var host = xmlVal(endpointBlock, "Address");
|
|
124
|
+
var port = xmlVal(endpointBlock, "Port") || "5432";
|
|
125
|
+
return { host, port };
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// --- Shared instance management ---
|
|
129
|
+
|
|
130
|
+
async function getOrCreateSharedInstance(cfg) {
|
|
131
|
+
var cr = { accessKeyId: cfg.accessKeyId, secretAccessKey: cfg.secretAccessKey };
|
|
132
|
+
var meta = getCloudMeta("aws", "sharedDb");
|
|
133
|
+
|
|
134
|
+
if (meta && meta.instance) {
|
|
135
|
+
// Verify instance still exists
|
|
136
|
+
try {
|
|
137
|
+
var xml = await awsQueryApi(
|
|
138
|
+
"DescribeDBInstances",
|
|
139
|
+
{ DBInstanceIdentifier: SHARED_INSTANCE },
|
|
140
|
+
"rds",
|
|
141
|
+
cr,
|
|
142
|
+
cfg.region
|
|
143
|
+
);
|
|
144
|
+
var endpoint = getRdsEndpoint(xml);
|
|
145
|
+
if (endpoint && endpoint.host !== meta.host) {
|
|
146
|
+
meta.host = endpoint.host;
|
|
147
|
+
meta.port = endpoint.port;
|
|
148
|
+
setCloudMeta("aws", "sharedDb", meta);
|
|
149
|
+
}
|
|
150
|
+
return meta;
|
|
151
|
+
} catch (e) {
|
|
152
|
+
// Instance gone, recreate
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Create shared RDS instance
|
|
157
|
+
var sgId = await ensureSecurityGroup(cfg);
|
|
158
|
+
var masterPassword = randomBytes(24).toString("base64url");
|
|
159
|
+
|
|
160
|
+
process.stderr.write(" Creating shared RDS instance (one-time, takes 5-15 minutes)...\n");
|
|
161
|
+
await awsQueryApi(
|
|
162
|
+
"CreateDBInstance",
|
|
163
|
+
{
|
|
164
|
+
DBInstanceIdentifier: SHARED_INSTANCE,
|
|
165
|
+
DBInstanceClass: "db.t4g.micro",
|
|
166
|
+
Engine: "postgres",
|
|
167
|
+
EngineVersion: "15",
|
|
168
|
+
MasterUsername: "relight_admin",
|
|
169
|
+
MasterUserPassword: masterPassword,
|
|
170
|
+
DBName: "postgres",
|
|
171
|
+
AllocatedStorage: "20",
|
|
172
|
+
PubliclyAccessible: "true",
|
|
173
|
+
"VpcSecurityGroupIds.member.1": sgId,
|
|
174
|
+
BackupRetentionPeriod: "0",
|
|
175
|
+
},
|
|
176
|
+
"rds",
|
|
177
|
+
cr,
|
|
178
|
+
cfg.region
|
|
179
|
+
);
|
|
180
|
+
|
|
181
|
+
await waitForInstance(cfg, SHARED_INSTANCE);
|
|
182
|
+
|
|
183
|
+
// Get endpoint
|
|
184
|
+
var xml = await awsQueryApi(
|
|
185
|
+
"DescribeDBInstances",
|
|
186
|
+
{ DBInstanceIdentifier: SHARED_INSTANCE },
|
|
187
|
+
"rds",
|
|
188
|
+
cr,
|
|
189
|
+
cfg.region
|
|
190
|
+
);
|
|
191
|
+
var endpoint = getRdsEndpoint(xml);
|
|
192
|
+
if (!endpoint || !endpoint.host) throw new Error("No endpoint found for shared RDS instance.");
|
|
193
|
+
|
|
194
|
+
meta = {
|
|
195
|
+
instance: SHARED_INSTANCE,
|
|
196
|
+
host: endpoint.host,
|
|
197
|
+
port: endpoint.port,
|
|
198
|
+
masterPassword,
|
|
199
|
+
};
|
|
200
|
+
setCloudMeta("aws", "sharedDb", meta);
|
|
201
|
+
|
|
202
|
+
return meta;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
async function connectAsAdmin(cfg) {
|
|
206
|
+
var meta = getCloudMeta("aws", "sharedDb");
|
|
207
|
+
if (!meta || !meta.masterPassword) {
|
|
208
|
+
throw new Error("Shared DB master credentials not found. Run `relight db create` first.");
|
|
209
|
+
}
|
|
210
|
+
var url = `postgresql://relight_admin:${encodeURIComponent(meta.masterPassword)}@${meta.host}:${meta.port}/postgres`;
|
|
211
|
+
var client = await connectPg(url);
|
|
212
|
+
return { client, meta };
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
async function destroySharedInstanceIfEmpty(cfg) {
|
|
216
|
+
var { client } = await connectAsAdmin(cfg);
|
|
217
|
+
try {
|
|
218
|
+
var res = await client.query(
|
|
219
|
+
"SELECT datname FROM pg_database WHERE datname LIKE 'relight_%'"
|
|
220
|
+
);
|
|
221
|
+
if (res.rows.length > 0) return false;
|
|
222
|
+
} finally {
|
|
223
|
+
await client.end();
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// No relight databases remain - destroy the shared instance
|
|
227
|
+
var cr = { accessKeyId: cfg.accessKeyId, secretAccessKey: cfg.secretAccessKey };
|
|
228
|
+
await awsQueryApi(
|
|
229
|
+
"DeleteDBInstance",
|
|
230
|
+
{ DBInstanceIdentifier: SHARED_INSTANCE, SkipFinalSnapshot: "true" },
|
|
231
|
+
"rds",
|
|
232
|
+
cr,
|
|
233
|
+
cfg.region
|
|
234
|
+
);
|
|
235
|
+
setCloudMeta("aws", "sharedDb", undefined);
|
|
236
|
+
return true;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// --- Public API ---
|
|
240
|
+
|
|
241
|
+
export async function createDatabase(cfg, name, opts = {}) {
|
|
242
|
+
var meta = await getOrCreateSharedInstance(cfg);
|
|
243
|
+
var database = dbName(name);
|
|
244
|
+
var user = userName(name);
|
|
245
|
+
var password = randomBytes(24).toString("base64url");
|
|
246
|
+
|
|
247
|
+
// Connect as admin to create database and user
|
|
248
|
+
var adminUrl = `postgresql://relight_admin:${encodeURIComponent(meta.masterPassword)}@${meta.host}:${meta.port}/postgres`;
|
|
249
|
+
var client = await connectPg(adminUrl);
|
|
250
|
+
try {
|
|
251
|
+
await client.query(`CREATE USER ${user} WITH PASSWORD '${password.replace(/'/g, "''")}'`);
|
|
252
|
+
await client.query(`CREATE DATABASE ${database} OWNER ${user}`);
|
|
253
|
+
} finally {
|
|
254
|
+
await client.end();
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
var connectionUrl = `postgresql://${user}:${encodeURIComponent(password)}@${meta.host}:${meta.port}/${database}`;
|
|
258
|
+
|
|
259
|
+
return {
|
|
260
|
+
dbId: SHARED_INSTANCE,
|
|
261
|
+
dbName: database,
|
|
262
|
+
dbUser: user,
|
|
263
|
+
dbToken: password,
|
|
264
|
+
connectionUrl,
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
export async function destroyDatabase(cfg, name, opts = {}) {
|
|
269
|
+
var dbId = opts.dbId;
|
|
270
|
+
if (!dbId) {
|
|
271
|
+
throw new Error("dbId is required to destroy an AWS database.");
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// Legacy per-app instance: delete the whole instance
|
|
275
|
+
if (!isSharedInstance(dbId)) {
|
|
276
|
+
var cr = { accessKeyId: cfg.accessKeyId, secretAccessKey: cfg.secretAccessKey };
|
|
277
|
+
await awsQueryApi(
|
|
278
|
+
"DeleteDBInstance",
|
|
279
|
+
{ DBInstanceIdentifier: dbId, SkipFinalSnapshot: "true" },
|
|
280
|
+
"rds",
|
|
281
|
+
cr,
|
|
282
|
+
cfg.region
|
|
283
|
+
);
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// Shared instance: drop database and user
|
|
288
|
+
var database = dbName(name);
|
|
289
|
+
var user = userName(name);
|
|
290
|
+
|
|
291
|
+
var { client } = await connectAsAdmin(cfg);
|
|
292
|
+
try {
|
|
293
|
+
// Terminate active connections to the database
|
|
294
|
+
await client.query(
|
|
295
|
+
`SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname = '${database}' AND pid <> pg_backend_pid()`
|
|
296
|
+
);
|
|
297
|
+
await client.query(`DROP DATABASE IF EXISTS ${database}`);
|
|
298
|
+
await client.query(`DROP USER IF EXISTS ${user}`);
|
|
299
|
+
} finally {
|
|
300
|
+
await client.end();
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// Check if shared instance should be destroyed
|
|
304
|
+
await destroySharedInstanceIfEmpty(cfg);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
export async function getDatabaseInfo(cfg, name, opts = {}) {
|
|
308
|
+
var dbId = opts.dbId;
|
|
309
|
+
if (!dbId) {
|
|
310
|
+
throw new Error("dbId is required to get AWS database info.");
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
var database = dbName(name);
|
|
314
|
+
var cr = { accessKeyId: cfg.accessKeyId, secretAccessKey: cfg.secretAccessKey };
|
|
315
|
+
var xml = await awsQueryApi(
|
|
316
|
+
"DescribeDBInstances",
|
|
317
|
+
{ DBInstanceIdentifier: dbId },
|
|
318
|
+
"rds",
|
|
319
|
+
cr,
|
|
320
|
+
cfg.region
|
|
321
|
+
);
|
|
322
|
+
|
|
323
|
+
var endpoint = getRdsEndpoint(xml);
|
|
324
|
+
var displayUser = isSharedInstance(dbId) ? userName(name) : "relight";
|
|
325
|
+
|
|
326
|
+
var connectionUrl = endpoint
|
|
327
|
+
? `postgresql://${displayUser}:****@${endpoint.host}:${endpoint.port}/${database}`
|
|
328
|
+
: null;
|
|
329
|
+
|
|
330
|
+
return {
|
|
331
|
+
dbId,
|
|
332
|
+
dbName: database,
|
|
333
|
+
connectionUrl,
|
|
334
|
+
size: null,
|
|
335
|
+
numTables: null,
|
|
336
|
+
createdAt: xmlVal(xml, "InstanceCreateTime") || null,
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
export async function queryDatabase(cfg, name, sql, params, opts = {}) {
|
|
341
|
+
if (!opts.connectionUrl) {
|
|
342
|
+
throw new Error("connectionUrl is required to query an AWS database.");
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
var client = await connectPg(opts.connectionUrl);
|
|
346
|
+
|
|
347
|
+
try {
|
|
348
|
+
var result = await client.query(sql, params || []);
|
|
349
|
+
return {
|
|
350
|
+
results: result.rows,
|
|
351
|
+
meta: { changes: result.rowCount, rows_read: result.rows.length },
|
|
352
|
+
};
|
|
353
|
+
} finally {
|
|
354
|
+
await client.end();
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
export async function importDatabase(cfg, name, sqlContent, opts = {}) {
|
|
359
|
+
if (!opts.connectionUrl) {
|
|
360
|
+
throw new Error("connectionUrl is required to import into an AWS database.");
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
var client = await connectPg(opts.connectionUrl);
|
|
364
|
+
|
|
365
|
+
try {
|
|
366
|
+
await client.query(sqlContent);
|
|
367
|
+
} finally {
|
|
368
|
+
await client.end();
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
export async function exportDatabase(cfg, name, opts = {}) {
|
|
373
|
+
if (!opts.connectionUrl) {
|
|
374
|
+
throw new Error("connectionUrl is required to export an AWS database.");
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
var database = dbName(name);
|
|
378
|
+
var client = await connectPg(opts.connectionUrl);
|
|
379
|
+
|
|
380
|
+
try {
|
|
381
|
+
var tablesRes = await client.query(
|
|
382
|
+
"SELECT tablename FROM pg_tables WHERE schemaname = 'public' ORDER BY tablename"
|
|
383
|
+
);
|
|
384
|
+
var tables = tablesRes.rows.map((r) => r.tablename);
|
|
385
|
+
|
|
386
|
+
var dump = [];
|
|
387
|
+
dump.push("-- PostgreSQL dump generated by relight");
|
|
388
|
+
dump.push(`-- Database: ${database}`);
|
|
389
|
+
dump.push(`-- Date: ${new Date().toISOString()}`);
|
|
390
|
+
dump.push("");
|
|
391
|
+
|
|
392
|
+
for (var t of tables) {
|
|
393
|
+
var colsRes = await client.query(
|
|
394
|
+
`SELECT column_name, data_type, is_nullable, column_default
|
|
395
|
+
FROM information_schema.columns
|
|
396
|
+
WHERE table_name = $1 AND table_schema = 'public'
|
|
397
|
+
ORDER BY ordinal_position`,
|
|
398
|
+
[t]
|
|
399
|
+
);
|
|
400
|
+
|
|
401
|
+
var cols = colsRes.rows.map((c) => {
|
|
402
|
+
var def = ` "${c.column_name}" ${c.data_type}`;
|
|
403
|
+
if (c.column_default) def += ` DEFAULT ${c.column_default}`;
|
|
404
|
+
if (c.is_nullable === "NO") def += " NOT NULL";
|
|
405
|
+
return def;
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
dump.push(`CREATE TABLE IF NOT EXISTS "${t}" (`);
|
|
409
|
+
dump.push(cols.join(",\n"));
|
|
410
|
+
dump.push(");");
|
|
411
|
+
dump.push("");
|
|
412
|
+
|
|
413
|
+
var dataRes = await client.query(`SELECT * FROM "${t}"`);
|
|
414
|
+
for (var row of dataRes.rows) {
|
|
415
|
+
var values = Object.values(row).map((v) => {
|
|
416
|
+
if (v === null) return "NULL";
|
|
417
|
+
if (typeof v === "number") return String(v);
|
|
418
|
+
if (typeof v === "boolean") return v ? "TRUE" : "FALSE";
|
|
419
|
+
return "'" + String(v).replace(/'/g, "''") + "'";
|
|
420
|
+
});
|
|
421
|
+
var colNames = Object.keys(row).map((c) => `"${c}"`).join(", ");
|
|
422
|
+
dump.push(`INSERT INTO "${t}" (${colNames}) VALUES (${values.join(", ")});`);
|
|
423
|
+
}
|
|
424
|
+
dump.push("");
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
return dump.join("\n");
|
|
428
|
+
} finally {
|
|
429
|
+
await client.end();
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
export async function rotateToken(cfg, name, opts = {}) {
|
|
434
|
+
var dbId = opts.dbId;
|
|
435
|
+
if (!dbId) {
|
|
436
|
+
throw new Error("dbId is required to rotate an AWS database token.");
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
var newPassword = randomBytes(24).toString("base64url");
|
|
440
|
+
var connectionUrl;
|
|
441
|
+
var database = dbName(name);
|
|
442
|
+
|
|
443
|
+
if (isSharedInstance(dbId)) {
|
|
444
|
+
// Update via admin connection
|
|
445
|
+
var user = userName(name);
|
|
446
|
+
var { client, meta } = await connectAsAdmin(cfg);
|
|
447
|
+
try {
|
|
448
|
+
await client.query(`ALTER USER ${user} WITH PASSWORD '${newPassword.replace(/'/g, "''")}'`);
|
|
449
|
+
} finally {
|
|
450
|
+
await client.end();
|
|
451
|
+
}
|
|
452
|
+
connectionUrl = `postgresql://${user}:${encodeURIComponent(newPassword)}@${meta.host}:${meta.port}/${database}`;
|
|
453
|
+
} else {
|
|
454
|
+
// Legacy: update RDS master password
|
|
455
|
+
var cr = { accessKeyId: cfg.accessKeyId, secretAccessKey: cfg.secretAccessKey };
|
|
456
|
+
await awsQueryApi(
|
|
457
|
+
"ModifyDBInstance",
|
|
458
|
+
{
|
|
459
|
+
DBInstanceIdentifier: dbId,
|
|
460
|
+
MasterUserPassword: newPassword,
|
|
461
|
+
},
|
|
462
|
+
"rds",
|
|
463
|
+
cr,
|
|
464
|
+
cfg.region
|
|
465
|
+
);
|
|
466
|
+
|
|
467
|
+
var xml = await awsQueryApi(
|
|
468
|
+
"DescribeDBInstances",
|
|
469
|
+
{ DBInstanceIdentifier: dbId },
|
|
470
|
+
"rds",
|
|
471
|
+
cr,
|
|
472
|
+
cfg.region
|
|
473
|
+
);
|
|
474
|
+
var endpoint = getRdsEndpoint(xml);
|
|
475
|
+
connectionUrl = endpoint
|
|
476
|
+
? `postgresql://relight:${encodeURIComponent(newPassword)}@${endpoint.host}:${endpoint.port}/${database}`
|
|
477
|
+
: null;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
return { dbToken: newPassword, connectionUrl };
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
export async function resetDatabase(cfg, name, opts = {}) {
|
|
484
|
+
if (!opts.connectionUrl) {
|
|
485
|
+
throw new Error("connectionUrl is required to reset an AWS database.");
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
var client = await connectPg(opts.connectionUrl);
|
|
489
|
+
|
|
490
|
+
try {
|
|
491
|
+
var tablesRes = await client.query(
|
|
492
|
+
"SELECT tablename FROM pg_tables WHERE schemaname = 'public'"
|
|
493
|
+
);
|
|
494
|
+
var tables = tablesRes.rows.map((r) => r.tablename);
|
|
495
|
+
|
|
496
|
+
for (var t of tables) {
|
|
497
|
+
await client.query(`DROP TABLE IF EXISTS "${t}" CASCADE`);
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
return tables;
|
|
501
|
+
} finally {
|
|
502
|
+
await client.end();
|
|
503
|
+
}
|
|
504
|
+
}
|