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