relight-cli 0.2.0 → 0.4.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/package.json +1 -1
- package/src/cli.js +94 -49
- package/src/commands/apps.js +13 -10
- package/src/commands/auth.js +3 -63
- package/src/commands/config.js +16 -16
- package/src/commands/cost.js +6 -6
- package/src/commands/db.js +434 -190
- package/src/commands/deploy.js +38 -72
- package/src/commands/doctor.js +30 -6
- package/src/commands/domains.js +41 -24
- package/src/commands/logs.js +4 -4
- package/src/commands/open.js +4 -4
- package/src/commands/ps.js +20 -15
- package/src/commands/scale.js +4 -4
- package/src/commands/service.js +227 -0
- package/src/lib/clouds/gcp.js +97 -1
- package/src/lib/clouds/neon.js +147 -0
- package/src/lib/config.js +169 -11
- package/src/lib/link.js +13 -2
- package/src/lib/providers/aws/db.js +217 -226
- package/src/lib/providers/aws/dns.js +1 -1
- package/src/lib/providers/cf/db.js +33 -131
- package/src/lib/providers/cf/dns.js +3 -3
- package/src/lib/providers/gcp/app.js +82 -7
- package/src/lib/providers/gcp/db.js +169 -254
- package/src/lib/providers/gcp/dns.js +9 -7
- package/src/lib/providers/neon/db.js +306 -0
- package/src/lib/providers/resolve.js +33 -3
|
@@ -1,13 +1,19 @@
|
|
|
1
1
|
import { randomBytes } from "crypto";
|
|
2
2
|
import { awsQueryApi, awsJsonApi, xmlVal, xmlList, xmlBlock } from "../../clouds/aws.js";
|
|
3
|
-
import {
|
|
3
|
+
import { getCloudMeta, setCloudMeta } from "../../config.js";
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
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, "_")}`;
|
|
7
13
|
}
|
|
8
14
|
|
|
9
|
-
function
|
|
10
|
-
return
|
|
15
|
+
function isSharedInstance(dbId) {
|
|
16
|
+
return dbId === SHARED_INSTANCE;
|
|
11
17
|
}
|
|
12
18
|
|
|
13
19
|
async function connectPg(connectionUrl) {
|
|
@@ -84,63 +90,6 @@ async function ensureSecurityGroup(cfg) {
|
|
|
84
90
|
return newSgId;
|
|
85
91
|
}
|
|
86
92
|
|
|
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
93
|
// --- Poll for RDS instance to become available ---
|
|
145
94
|
|
|
146
95
|
async function waitForInstance(cfg, instName) {
|
|
@@ -166,40 +115,59 @@ async function waitForInstance(cfg, instName) {
|
|
|
166
115
|
throw new Error("Timed out waiting for RDS instance to become available.");
|
|
167
116
|
}
|
|
168
117
|
|
|
169
|
-
// ---
|
|
118
|
+
// --- Get RDS endpoint ---
|
|
170
119
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
throw new Error(`App ${appName} already has a database: ${appConfig.dbId}`);
|
|
179
|
-
}
|
|
180
|
-
}
|
|
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
|
+
}
|
|
181
127
|
|
|
128
|
+
// --- Shared instance management ---
|
|
129
|
+
|
|
130
|
+
async function getOrCreateSharedInstance(cfg) {
|
|
182
131
|
var cr = { accessKeyId: cfg.accessKeyId, secretAccessKey: cfg.secretAccessKey };
|
|
183
|
-
var
|
|
184
|
-
|
|
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
|
+
}
|
|
185
155
|
|
|
186
|
-
//
|
|
156
|
+
// Create shared RDS instance
|
|
187
157
|
var sgId = await ensureSecurityGroup(cfg);
|
|
158
|
+
var masterPassword = randomBytes(24).toString("base64url");
|
|
188
159
|
|
|
189
|
-
|
|
190
|
-
var password = randomBytes(24).toString("base64url");
|
|
191
|
-
|
|
192
|
-
// Create RDS instance
|
|
160
|
+
process.stderr.write(" Creating shared RDS instance (one-time, takes 5-15 minutes)...\n");
|
|
193
161
|
await awsQueryApi(
|
|
194
162
|
"CreateDBInstance",
|
|
195
163
|
{
|
|
196
|
-
DBInstanceIdentifier:
|
|
164
|
+
DBInstanceIdentifier: SHARED_INSTANCE,
|
|
197
165
|
DBInstanceClass: "db.t4g.micro",
|
|
198
166
|
Engine: "postgres",
|
|
199
167
|
EngineVersion: "15",
|
|
200
|
-
MasterUsername: "
|
|
201
|
-
MasterUserPassword:
|
|
202
|
-
DBName:
|
|
168
|
+
MasterUsername: "relight_admin",
|
|
169
|
+
MasterUserPassword: masterPassword,
|
|
170
|
+
DBName: "postgres",
|
|
203
171
|
AllocatedStorage: "20",
|
|
204
172
|
PubliclyAccessible: "true",
|
|
205
173
|
"VpcSecurityGroupIds.member.1": sgId,
|
|
@@ -210,91 +178,139 @@ export async function createDatabase(cfg, appName, opts = {}) {
|
|
|
210
178
|
cfg.region
|
|
211
179
|
);
|
|
212
180
|
|
|
213
|
-
|
|
214
|
-
process.stderr.write(" Waiting for RDS instance (this takes 5-15 minutes)...\n");
|
|
215
|
-
await waitForInstance(cfg, instName);
|
|
181
|
+
await waitForInstance(cfg, SHARED_INSTANCE);
|
|
216
182
|
|
|
217
|
-
// Get
|
|
218
|
-
var
|
|
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);
|
|
219
201
|
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
appConfig.dbId = instName;
|
|
223
|
-
appConfig.dbName = database;
|
|
202
|
+
return meta;
|
|
203
|
+
}
|
|
224
204
|
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
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
|
+
}
|
|
228
214
|
|
|
229
|
-
|
|
230
|
-
|
|
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
|
+
}
|
|
231
225
|
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
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 ---
|
|
236
240
|
|
|
237
|
-
|
|
238
|
-
|
|
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();
|
|
239
255
|
}
|
|
240
256
|
|
|
257
|
+
var connectionUrl = `postgresql://${user}:${encodeURIComponent(password)}@${meta.host}:${meta.port}/${database}`;
|
|
258
|
+
|
|
241
259
|
return {
|
|
242
|
-
dbId:
|
|
260
|
+
dbId: SHARED_INSTANCE,
|
|
243
261
|
dbName: database,
|
|
262
|
+
dbUser: user,
|
|
244
263
|
dbToken: password,
|
|
245
264
|
connectionUrl,
|
|
246
265
|
};
|
|
247
266
|
}
|
|
248
267
|
|
|
249
|
-
export async function destroyDatabase(cfg,
|
|
268
|
+
export async function destroyDatabase(cfg, name, opts = {}) {
|
|
250
269
|
var dbId = opts.dbId;
|
|
251
270
|
if (!dbId) {
|
|
252
|
-
|
|
253
|
-
if (!appConfig || !appConfig.dbId) {
|
|
254
|
-
throw new Error(`App ${appName} does not have a database.`);
|
|
255
|
-
}
|
|
256
|
-
dbId = appConfig.dbId;
|
|
271
|
+
throw new Error("dbId is required to destroy an AWS database.");
|
|
257
272
|
}
|
|
258
273
|
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
delete appConfig.dbName;
|
|
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
|
+
}
|
|
272
286
|
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
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");
|
|
287
|
+
// Shared instance: drop database and user
|
|
288
|
+
var database = dbName(name);
|
|
289
|
+
var user = userName(name);
|
|
279
290
|
|
|
280
|
-
|
|
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();
|
|
281
301
|
}
|
|
302
|
+
|
|
303
|
+
// Check if shared instance should be destroyed
|
|
304
|
+
await destroySharedInstanceIfEmpty(cfg);
|
|
282
305
|
}
|
|
283
306
|
|
|
284
|
-
export async function getDatabaseInfo(cfg,
|
|
307
|
+
export async function getDatabaseInfo(cfg, name, opts = {}) {
|
|
285
308
|
var dbId = opts.dbId;
|
|
286
|
-
var dbNameVal;
|
|
287
309
|
if (!dbId) {
|
|
288
|
-
|
|
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);
|
|
310
|
+
throw new Error("dbId is required to get AWS database info.");
|
|
296
311
|
}
|
|
297
312
|
|
|
313
|
+
var database = dbName(name);
|
|
298
314
|
var cr = { accessKeyId: cfg.accessKeyId, secretAccessKey: cfg.secretAccessKey };
|
|
299
315
|
var xml = await awsQueryApi(
|
|
300
316
|
"DescribeDBInstances",
|
|
@@ -304,17 +320,16 @@ export async function getDatabaseInfo(cfg, appName, opts = {}) {
|
|
|
304
320
|
cfg.region
|
|
305
321
|
);
|
|
306
322
|
|
|
307
|
-
var
|
|
308
|
-
var
|
|
309
|
-
var port = endpointBlock ? (xmlVal(endpointBlock, "Port") || "5432") : "5432";
|
|
323
|
+
var endpoint = getRdsEndpoint(xml);
|
|
324
|
+
var displayUser = isSharedInstance(dbId) ? userName(name) : "relight";
|
|
310
325
|
|
|
311
|
-
var connectionUrl =
|
|
312
|
-
? `postgresql
|
|
326
|
+
var connectionUrl = endpoint
|
|
327
|
+
? `postgresql://${displayUser}:****@${endpoint.host}:${endpoint.port}/${database}`
|
|
313
328
|
: null;
|
|
314
329
|
|
|
315
330
|
return {
|
|
316
331
|
dbId,
|
|
317
|
-
dbName:
|
|
332
|
+
dbName: database,
|
|
318
333
|
connectionUrl,
|
|
319
334
|
size: null,
|
|
320
335
|
numTables: null,
|
|
@@ -322,17 +337,12 @@ export async function getDatabaseInfo(cfg, appName, opts = {}) {
|
|
|
322
337
|
};
|
|
323
338
|
}
|
|
324
339
|
|
|
325
|
-
export async function queryDatabase(cfg,
|
|
326
|
-
if (!opts.
|
|
327
|
-
|
|
328
|
-
if (!appConfig || !appConfig.dbId) {
|
|
329
|
-
throw new Error(`App ${appName} does not have a database.`);
|
|
330
|
-
}
|
|
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.");
|
|
331
343
|
}
|
|
332
344
|
|
|
333
|
-
var
|
|
334
|
-
var connectionUrl = await getConnectionUrl(cfg, appName, password);
|
|
335
|
-
var client = await connectPg(connectionUrl);
|
|
345
|
+
var client = await connectPg(opts.connectionUrl);
|
|
336
346
|
|
|
337
347
|
try {
|
|
338
348
|
var result = await client.query(sql, params || []);
|
|
@@ -345,17 +355,12 @@ export async function queryDatabase(cfg, appName, sql, params, opts = {}) {
|
|
|
345
355
|
}
|
|
346
356
|
}
|
|
347
357
|
|
|
348
|
-
export async function importDatabase(cfg,
|
|
349
|
-
if (!opts.
|
|
350
|
-
|
|
351
|
-
if (!appConfig || !appConfig.dbId) {
|
|
352
|
-
throw new Error(`App ${appName} does not have a database.`);
|
|
353
|
-
}
|
|
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.");
|
|
354
361
|
}
|
|
355
362
|
|
|
356
|
-
var
|
|
357
|
-
var connectionUrl = await getConnectionUrl(cfg, appName, password);
|
|
358
|
-
var client = await connectPg(connectionUrl);
|
|
363
|
+
var client = await connectPg(opts.connectionUrl);
|
|
359
364
|
|
|
360
365
|
try {
|
|
361
366
|
await client.query(sqlContent);
|
|
@@ -364,21 +369,13 @@ export async function importDatabase(cfg, appName, sqlContent, opts = {}) {
|
|
|
364
369
|
}
|
|
365
370
|
}
|
|
366
371
|
|
|
367
|
-
export async function exportDatabase(cfg,
|
|
368
|
-
|
|
369
|
-
|
|
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);
|
|
372
|
+
export async function exportDatabase(cfg, name, opts = {}) {
|
|
373
|
+
if (!opts.connectionUrl) {
|
|
374
|
+
throw new Error("connectionUrl is required to export an AWS database.");
|
|
377
375
|
}
|
|
378
376
|
|
|
379
|
-
var
|
|
380
|
-
var
|
|
381
|
-
var client = await connectPg(connectionUrl);
|
|
377
|
+
var database = dbName(name);
|
|
378
|
+
var client = await connectPg(opts.connectionUrl);
|
|
382
379
|
|
|
383
380
|
try {
|
|
384
381
|
var tablesRes = await client.query(
|
|
@@ -433,68 +430,62 @@ export async function exportDatabase(cfg, appName, opts = {}) {
|
|
|
433
430
|
}
|
|
434
431
|
}
|
|
435
432
|
|
|
436
|
-
export async function rotateToken(cfg,
|
|
433
|
+
export async function rotateToken(cfg, name, opts = {}) {
|
|
437
434
|
var dbId = opts.dbId;
|
|
438
435
|
if (!dbId) {
|
|
439
|
-
|
|
440
|
-
if (!appConfig || !appConfig.dbId) {
|
|
441
|
-
throw new Error(`App ${appName} does not have a database.`);
|
|
442
|
-
}
|
|
443
|
-
dbId = appConfig.dbId;
|
|
436
|
+
throw new Error("dbId is required to rotate an AWS database token.");
|
|
444
437
|
}
|
|
445
438
|
|
|
446
|
-
var cr = { accessKeyId: cfg.accessKeyId, secretAccessKey: cfg.secretAccessKey };
|
|
447
439
|
var newPassword = randomBytes(24).toString("base64url");
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
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);
|
|
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();
|
|
467
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
|
+
);
|
|
468
466
|
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
await pushAppConfig(cfg, appName, appConfig, { newSecrets: { DB_TOKEN: newPassword } });
|
|
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;
|
|
482
478
|
}
|
|
483
479
|
|
|
484
480
|
return { dbToken: newPassword, connectionUrl };
|
|
485
481
|
}
|
|
486
482
|
|
|
487
|
-
export async function resetDatabase(cfg,
|
|
488
|
-
if (!opts.
|
|
489
|
-
|
|
490
|
-
if (!appConfig || !appConfig.dbId) {
|
|
491
|
-
throw new Error(`App ${appName} does not have a database.`);
|
|
492
|
-
}
|
|
483
|
+
export async function resetDatabase(cfg, name, opts = {}) {
|
|
484
|
+
if (!opts.connectionUrl) {
|
|
485
|
+
throw new Error("connectionUrl is required to reset an AWS database.");
|
|
493
486
|
}
|
|
494
487
|
|
|
495
|
-
var
|
|
496
|
-
var connectionUrl = await getConnectionUrl(cfg, appName, password);
|
|
497
|
-
var client = await connectPg(connectionUrl);
|
|
488
|
+
var client = await connectPg(opts.connectionUrl);
|
|
498
489
|
|
|
499
490
|
try {
|
|
500
491
|
var tablesRes = await client.query(
|
|
@@ -149,7 +149,7 @@ export async function removeDomain(cfg, appName, domain) {
|
|
|
149
149
|
|
|
150
150
|
// --- Pure DNS record operations (for cross-cloud use) ---
|
|
151
151
|
|
|
152
|
-
export async function addDnsRecord(cfg, domain, target, zone) {
|
|
152
|
+
export async function addDnsRecord(cfg, domain, target, zone, opts = {}) {
|
|
153
153
|
var cr = { accessKeyId: cfg.accessKeyId, secretAccessKey: cfg.secretAccessKey };
|
|
154
154
|
var fqdn = domain.endsWith(".") ? domain : domain + ".";
|
|
155
155
|
var targetFqdn = target.endsWith(".") ? target : target + ".";
|