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,298 @@
|
|
|
1
|
+
import { createInterface } from "readline";
|
|
2
|
+
import { randomBytes } from "crypto";
|
|
3
|
+
import { phase, status, success, hint, fatal, fmt, generateAppName } from "../lib/output.js";
|
|
4
|
+
import { readLink, linkApp, resolveAppName } from "../lib/link.js";
|
|
5
|
+
import { resolveCloudId, getCloudCfg, getProvider } from "../lib/providers/resolve.js";
|
|
6
|
+
import { dockerBuild, dockerTag, dockerPush, dockerLogin } from "../lib/docker.js";
|
|
7
|
+
|
|
8
|
+
export async function deploy(nameOrPath, path, options) {
|
|
9
|
+
var cloud = resolveCloudId(options.cloud);
|
|
10
|
+
var cfg = getCloudCfg(cloud);
|
|
11
|
+
|
|
12
|
+
// Smart arg parsing: if first arg looks like a path, shift args
|
|
13
|
+
var name;
|
|
14
|
+
var dockerPath;
|
|
15
|
+
if (!nameOrPath) {
|
|
16
|
+
name = readLink()?.app || generateAppName();
|
|
17
|
+
dockerPath = ".";
|
|
18
|
+
} else if (nameOrPath.startsWith(".") || nameOrPath.startsWith("/") || nameOrPath.startsWith("~")) {
|
|
19
|
+
name = readLink()?.app || generateAppName();
|
|
20
|
+
dockerPath = nameOrPath;
|
|
21
|
+
} else {
|
|
22
|
+
name = nameOrPath;
|
|
23
|
+
dockerPath = path || ".";
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
var tag = options.tag || `${Date.now()}`;
|
|
27
|
+
|
|
28
|
+
// Get registry credentials and image tag
|
|
29
|
+
var registry = await getProvider(cloud, "registry");
|
|
30
|
+
var remoteTag = await registry.getImageTag(cfg, name, tag);
|
|
31
|
+
var localTag = `relight-${name}:${tag}`;
|
|
32
|
+
|
|
33
|
+
// Load existing config from deployed worker (null on first deploy)
|
|
34
|
+
var appProvider = await getProvider(cloud, "app");
|
|
35
|
+
var appConfig;
|
|
36
|
+
try {
|
|
37
|
+
appConfig = await appProvider.getAppConfig(cfg, name);
|
|
38
|
+
} catch {
|
|
39
|
+
appConfig = null;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
var isFirstDeploy = !appConfig;
|
|
43
|
+
|
|
44
|
+
// Get valid regions for this cloud
|
|
45
|
+
var validRegions = appProvider.getRegions();
|
|
46
|
+
var validCodes = validRegions.map((r) => r.code);
|
|
47
|
+
|
|
48
|
+
if (appConfig) {
|
|
49
|
+
// Existing app - update image, merge any flags
|
|
50
|
+
appConfig.image = remoteTag;
|
|
51
|
+
appConfig.deployedAt = new Date().toISOString();
|
|
52
|
+
|
|
53
|
+
if (options.env) {
|
|
54
|
+
if (!appConfig.envKeys) appConfig.envKeys = [];
|
|
55
|
+
if (!appConfig.secretKeys) appConfig.secretKeys = [];
|
|
56
|
+
if (!appConfig.env) appConfig.env = {};
|
|
57
|
+
for (var v of options.env) {
|
|
58
|
+
var eq = v.indexOf("=");
|
|
59
|
+
if (eq !== -1) {
|
|
60
|
+
var k = v.substring(0, eq);
|
|
61
|
+
appConfig.env[k] = v.substring(eq + 1);
|
|
62
|
+
appConfig.secretKeys = appConfig.secretKeys.filter((s) => s !== k);
|
|
63
|
+
if (!appConfig.envKeys.includes(k)) appConfig.envKeys.push(k);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
if (options.regions)
|
|
68
|
+
appConfig.regions = options.regions.split(",").map((r) => r.trim());
|
|
69
|
+
if (options.instances) appConfig.instances = options.instances;
|
|
70
|
+
if (options.port) appConfig.port = options.port;
|
|
71
|
+
if (options.sleep) appConfig.sleepAfter = options.sleep;
|
|
72
|
+
if (options.instanceType) appConfig.instanceType = options.instanceType;
|
|
73
|
+
if (options.vcpu) appConfig.vcpu = options.vcpu;
|
|
74
|
+
if (options.memory) appConfig.memory = options.memory;
|
|
75
|
+
if (options.disk) appConfig.disk = options.disk;
|
|
76
|
+
if (options.observability === false) appConfig.observability = false;
|
|
77
|
+
} else {
|
|
78
|
+
// First deploy - build config from flags + defaults
|
|
79
|
+
var env = {};
|
|
80
|
+
var envKeys = [];
|
|
81
|
+
if (options.env) {
|
|
82
|
+
for (var v of options.env) {
|
|
83
|
+
var eq = v.indexOf("=");
|
|
84
|
+
if (eq !== -1) {
|
|
85
|
+
var k = v.substring(0, eq);
|
|
86
|
+
env[k] = v.substring(eq + 1);
|
|
87
|
+
envKeys.push(k);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
var defaultRegion = cloud === "gcp" ? "us-central1" : cloud === "aws" ? "us-east-1" : cloud === "slicervm" ? "self-hosted" : "enam";
|
|
93
|
+
var regions;
|
|
94
|
+
|
|
95
|
+
if (options.regions) {
|
|
96
|
+
regions = options.regions.split(",").map((r) => r.trim());
|
|
97
|
+
} else if ((cloud === "gcp" || cloud === "aws") && process.stdin.isTTY) {
|
|
98
|
+
// Interactive region picker for GCP/AWS first deploy
|
|
99
|
+
var { createInterface: createRL } = await import("readline");
|
|
100
|
+
var rl = createRL({ input: process.stdin, output: process.stderr });
|
|
101
|
+
process.stderr.write(`\n${fmt.bold("Select a region:")}\n\n`);
|
|
102
|
+
for (var i = 0; i < validRegions.length; i++) {
|
|
103
|
+
process.stderr.write(
|
|
104
|
+
` ${fmt.bold(`[${i + 1}]`)} ${validRegions[i].code} ${fmt.dim(`(${validRegions[i].name})`)}\n`
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
process.stderr.write("\n");
|
|
108
|
+
var choice = await new Promise((resolve) =>
|
|
109
|
+
rl.question(`Region [1-${validRegions.length}] (default: 1): `, resolve)
|
|
110
|
+
);
|
|
111
|
+
rl.close();
|
|
112
|
+
var idx = choice.trim() ? parseInt(choice, 10) - 1 : 0;
|
|
113
|
+
if (isNaN(idx) || idx < 0 || idx >= validRegions.length) {
|
|
114
|
+
fatal("Invalid region selection.");
|
|
115
|
+
}
|
|
116
|
+
regions = [validRegions[idx].code];
|
|
117
|
+
} else {
|
|
118
|
+
regions = [defaultRegion];
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
for (var r of regions) {
|
|
122
|
+
if (!validCodes.includes(r)) {
|
|
123
|
+
fatal(
|
|
124
|
+
`Invalid region '${r}'.`,
|
|
125
|
+
`Valid regions: ${validCodes.join(", ")}`
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
appConfig = {
|
|
131
|
+
name,
|
|
132
|
+
regions,
|
|
133
|
+
instances: options.instances || (cloud === "slicervm" ? 1 : 2),
|
|
134
|
+
port: options.port || 8080,
|
|
135
|
+
sleepAfter: options.sleep || "30s",
|
|
136
|
+
instanceType: options.instanceType || (cloud === "gcp" || cloud === "aws" || cloud === "slicervm" ? undefined : "lite"),
|
|
137
|
+
vcpu: options.vcpu || undefined,
|
|
138
|
+
memory: options.memory || undefined,
|
|
139
|
+
disk: options.disk || undefined,
|
|
140
|
+
env,
|
|
141
|
+
envKeys,
|
|
142
|
+
secretKeys: [],
|
|
143
|
+
domains: [],
|
|
144
|
+
image: remoteTag,
|
|
145
|
+
createdAt: new Date().toISOString(),
|
|
146
|
+
deployedAt: new Date().toISOString(),
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// --- D1 database (--db flag, CF only) ---
|
|
151
|
+
var newSecrets = {};
|
|
152
|
+
if (options.db && !appConfig.dbId) {
|
|
153
|
+
if (cloud === "gcp") {
|
|
154
|
+
process.stderr.write(
|
|
155
|
+
`\n${fmt.dim("Cloud SQL provisioning takes 3-10 minutes. Use 'relight db create' separately after deploy.")}\n\n`
|
|
156
|
+
);
|
|
157
|
+
} else if (cloud === "aws") {
|
|
158
|
+
process.stderr.write(
|
|
159
|
+
`\n${fmt.dim("RDS provisioning takes 5-15 minutes. Use 'relight db create' separately after deploy.")}\n\n`
|
|
160
|
+
);
|
|
161
|
+
} else if (cloud === "cf") {
|
|
162
|
+
var { createD1Database, getWorkersSubdomain } = await import("../lib/clouds/cf.js");
|
|
163
|
+
var dbResult = await createD1Database(cfg.accountId, cfg.apiToken, `relight-${name}`, {
|
|
164
|
+
locationHint: options.dbLocation,
|
|
165
|
+
jurisdiction: options.dbJurisdiction,
|
|
166
|
+
});
|
|
167
|
+
appConfig.dbId = dbResult.uuid;
|
|
168
|
+
appConfig.dbName = `relight-${name}`;
|
|
169
|
+
|
|
170
|
+
if (!appConfig.envKeys) appConfig.envKeys = [];
|
|
171
|
+
if (!appConfig.secretKeys) appConfig.secretKeys = [];
|
|
172
|
+
if (!appConfig.env) appConfig.env = {};
|
|
173
|
+
|
|
174
|
+
var subdomain = await getWorkersSubdomain(cfg.accountId, cfg.apiToken);
|
|
175
|
+
var dbUrl = subdomain
|
|
176
|
+
? `https://relight-${name}.${subdomain}.workers.dev`
|
|
177
|
+
: null;
|
|
178
|
+
|
|
179
|
+
if (dbUrl) {
|
|
180
|
+
appConfig.env["DB_URL"] = dbUrl;
|
|
181
|
+
if (!appConfig.envKeys.includes("DB_URL")) appConfig.envKeys.push("DB_URL");
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
appConfig.env["DB_TOKEN"] = "[hidden]";
|
|
185
|
+
appConfig.secretKeys = appConfig.secretKeys.filter((k) => k !== "DB_TOKEN");
|
|
186
|
+
appConfig.secretKeys.push("DB_TOKEN");
|
|
187
|
+
appConfig.envKeys = appConfig.envKeys.filter((k) => k !== "DB_TOKEN");
|
|
188
|
+
|
|
189
|
+
newSecrets.DB_TOKEN = randomBytes(32).toString("hex");
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// --- Summary & confirmation ---
|
|
194
|
+
var instanceDesc = appConfig.vcpu
|
|
195
|
+
? `${appConfig.vcpu} vCPU, ${appConfig.memory || "default"} MiB`
|
|
196
|
+
: appConfig.instanceType || "lite";
|
|
197
|
+
|
|
198
|
+
process.stderr.write(`\n${fmt.bold("Deploy summary")}\n`);
|
|
199
|
+
process.stderr.write(`${fmt.dim("-".repeat(40))}\n`);
|
|
200
|
+
process.stderr.write(` ${fmt.bold("App:")} ${fmt.app(name)}${isFirstDeploy ? fmt.dim(" (new)") : ""}\n`);
|
|
201
|
+
process.stderr.write(` ${fmt.bold("Cloud:")} ${fmt.cloud(cloud)}\n`);
|
|
202
|
+
process.stderr.write(` ${fmt.bold("Path:")} ${dockerPath}\n`);
|
|
203
|
+
process.stderr.write(` ${fmt.bold("Image:")} ${remoteTag}\n`);
|
|
204
|
+
process.stderr.write(` ${fmt.bold("Regions:")} ${appConfig.regions.join(", ")}\n`);
|
|
205
|
+
process.stderr.write(` ${fmt.bold("Instances:")} ${appConfig.instances || 2} per region\n`);
|
|
206
|
+
process.stderr.write(` ${fmt.bold("Type:")} ${instanceDesc}\n`);
|
|
207
|
+
process.stderr.write(` ${fmt.bold("Port:")} ${appConfig.port || 8080}\n`);
|
|
208
|
+
process.stderr.write(` ${fmt.bold("Sleep:")} ${appConfig.sleepAfter || "30s"}\n`);
|
|
209
|
+
if (appConfig.dbId) {
|
|
210
|
+
process.stderr.write(` ${fmt.bold("Database:")} ${appConfig.dbName}\n`);
|
|
211
|
+
}
|
|
212
|
+
process.stderr.write(`${fmt.dim("-".repeat(40))}\n`);
|
|
213
|
+
|
|
214
|
+
if (!options.yes) {
|
|
215
|
+
var rl = createInterface({ input: process.stdin, output: process.stderr });
|
|
216
|
+
var answer = await new Promise((resolve) =>
|
|
217
|
+
rl.question("\nProceed? [Y/n] ", resolve)
|
|
218
|
+
);
|
|
219
|
+
rl.close();
|
|
220
|
+
if (answer && !answer.match(/^y(es)?$/i)) {
|
|
221
|
+
process.stderr.write("Deploy cancelled.\n");
|
|
222
|
+
process.exit(0);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// 1. Build Docker image
|
|
227
|
+
var platform = "linux/amd64";
|
|
228
|
+
if (cloud === "slicervm") {
|
|
229
|
+
// Match VM architecture
|
|
230
|
+
var { listNodes } = await import("../lib/clouds/slicervm.js");
|
|
231
|
+
var nodes = await listNodes(cfg);
|
|
232
|
+
var vmArch = nodes[0]?.arch;
|
|
233
|
+
if (vmArch === "arm64" || vmArch === "aarch64") platform = "linux/arm64";
|
|
234
|
+
}
|
|
235
|
+
phase("Building image");
|
|
236
|
+
status(`${localTag} for ${platform}`);
|
|
237
|
+
dockerBuild(dockerPath, localTag, { platform });
|
|
238
|
+
|
|
239
|
+
if (cloud === "slicervm") {
|
|
240
|
+
// SlicerVM: skip registry push - deploy extracts and uploads the image directly
|
|
241
|
+
phase("Deploying");
|
|
242
|
+
await appProvider.deploy(cfg, name, localTag, {
|
|
243
|
+
appConfig,
|
|
244
|
+
isFirstDeploy,
|
|
245
|
+
newSecrets,
|
|
246
|
+
});
|
|
247
|
+
} else {
|
|
248
|
+
// 2. Push to registry
|
|
249
|
+
phase("Pushing to registry");
|
|
250
|
+
status("Authenticating...");
|
|
251
|
+
var creds = await registry.getCredentials(cfg);
|
|
252
|
+
dockerLogin(creds.registry, creds.username, creds.password);
|
|
253
|
+
if (registry.ensureRepository) await registry.ensureRepository(cfg, name);
|
|
254
|
+
status(`Pushing ${remoteTag}...`);
|
|
255
|
+
dockerTag(localTag, remoteTag);
|
|
256
|
+
dockerPush(remoteTag);
|
|
257
|
+
|
|
258
|
+
// 3. Deploy via provider
|
|
259
|
+
phase("Deploying");
|
|
260
|
+
await appProvider.deploy(cfg, name, remoteTag, {
|
|
261
|
+
appConfig,
|
|
262
|
+
isFirstDeploy,
|
|
263
|
+
newSecrets,
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// 4. Resolve URL and report
|
|
268
|
+
var url = await appProvider.getAppUrl(cfg, name);
|
|
269
|
+
|
|
270
|
+
if (options.json) {
|
|
271
|
+
console.log(
|
|
272
|
+
JSON.stringify(
|
|
273
|
+
{
|
|
274
|
+
name,
|
|
275
|
+
cloud,
|
|
276
|
+
image: remoteTag,
|
|
277
|
+
url,
|
|
278
|
+
regions: appConfig.regions,
|
|
279
|
+
instances: appConfig.instances,
|
|
280
|
+
firstDeploy: isFirstDeploy,
|
|
281
|
+
},
|
|
282
|
+
null,
|
|
283
|
+
2
|
|
284
|
+
)
|
|
285
|
+
);
|
|
286
|
+
} else {
|
|
287
|
+
success(`App ${fmt.app(name)} deployed!`);
|
|
288
|
+
process.stderr.write(` ${fmt.bold("Name:")} ${fmt.app(name)}\n`);
|
|
289
|
+
process.stderr.write(` ${fmt.bold("Image:")} ${remoteTag}\n`);
|
|
290
|
+
process.stderr.write(
|
|
291
|
+
` ${fmt.bold("URL:")} ${url ? fmt.url(url) : fmt.dim("(configure workers.dev subdomain to see URL)")}\n`
|
|
292
|
+
);
|
|
293
|
+
hint("Next", `relight open ${name}`);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// Link this directory to the app
|
|
297
|
+
linkApp(name, cloud, options.dns, options.dbCloud);
|
|
298
|
+
}
|
package/src/commands/doctor.js
CHANGED
|
@@ -6,8 +6,8 @@ import {
|
|
|
6
6
|
CLOUD_NAMES,
|
|
7
7
|
} from "../lib/config.js";
|
|
8
8
|
import { verifyToken as cfVerify, getWorkersSubdomain } from "../lib/clouds/cf.js";
|
|
9
|
-
import { mintAccessToken, verifyProject as gcpVerifyProject, listRegions as gcpListRegions } from "../lib/clouds/gcp.js";
|
|
10
|
-
import { verifyCredentials as awsVerify, checkAppRunner } from "../lib/clouds/aws.js";
|
|
9
|
+
import { mintAccessToken, verifyProject as gcpVerifyProject, listRegions as gcpListRegions, gcpApi, AR_API, SQLADMIN_API, DNS_API } from "../lib/clouds/gcp.js";
|
|
10
|
+
import { verifyCredentials as awsVerify, checkAppRunner, awsJsonApi, awsQueryApi, awsRestXmlApi } from "../lib/clouds/aws.js";
|
|
11
11
|
import kleur from "kleur";
|
|
12
12
|
|
|
13
13
|
var PASS = kleur.green("[ok]");
|
|
@@ -134,6 +134,21 @@ async function checkGCP(cfg) {
|
|
|
134
134
|
(await asyncCheck("Cloud Run API reachable", async () => {
|
|
135
135
|
await gcpListRegions(token, cfg.project);
|
|
136
136
|
})) && ok;
|
|
137
|
+
|
|
138
|
+
ok =
|
|
139
|
+
(await asyncCheck("Artifact Registry API reachable", async () => {
|
|
140
|
+
await gcpApi("GET", `${AR_API}/projects/${cfg.project}/locations/us/repositories`, null, token);
|
|
141
|
+
})) && ok;
|
|
142
|
+
|
|
143
|
+
ok =
|
|
144
|
+
(await asyncCheck("Cloud SQL Admin API reachable", async () => {
|
|
145
|
+
await gcpApi("GET", `${SQLADMIN_API}/projects/${cfg.project}/instances`, null, token);
|
|
146
|
+
})) && ok;
|
|
147
|
+
|
|
148
|
+
ok =
|
|
149
|
+
(await asyncCheck("Cloud DNS API reachable", async () => {
|
|
150
|
+
await gcpApi("GET", `${DNS_API}/projects/${cfg.project}/managedZones`, null, token);
|
|
151
|
+
})) && ok;
|
|
137
152
|
}
|
|
138
153
|
|
|
139
154
|
return ok;
|
|
@@ -143,23 +158,40 @@ async function checkGCP(cfg) {
|
|
|
143
158
|
|
|
144
159
|
async function checkAWS(cfg) {
|
|
145
160
|
var ok = true;
|
|
161
|
+
var cr = { accessKeyId: cfg.accessKeyId, secretAccessKey: cfg.secretAccessKey };
|
|
146
162
|
|
|
147
163
|
ok =
|
|
148
164
|
(await asyncCheck("Credentials valid (STS)", async () => {
|
|
149
|
-
await awsVerify(
|
|
150
|
-
{ accessKeyId: cfg.accessKeyId, secretAccessKey: cfg.secretAccessKey },
|
|
151
|
-
cfg.region
|
|
152
|
-
);
|
|
165
|
+
await awsVerify(cr, cfg.region);
|
|
153
166
|
})) && ok;
|
|
154
167
|
|
|
155
168
|
ok =
|
|
156
169
|
(await asyncCheck("App Runner accessible", async () => {
|
|
157
|
-
await checkAppRunner(
|
|
158
|
-
|
|
159
|
-
|
|
170
|
+
await checkAppRunner(cr, cfg.region);
|
|
171
|
+
})) && ok;
|
|
172
|
+
|
|
173
|
+
ok =
|
|
174
|
+
(await asyncCheck("ECR accessible", async () => {
|
|
175
|
+
await awsJsonApi(
|
|
176
|
+
"AmazonEC2ContainerRegistry_V20150921.DescribeRepositories",
|
|
177
|
+
{},
|
|
178
|
+
"ecr",
|
|
179
|
+
cr,
|
|
180
|
+
cfg.region,
|
|
181
|
+
`api.ecr.${cfg.region}.amazonaws.com`
|
|
160
182
|
);
|
|
161
183
|
})) && ok;
|
|
162
184
|
|
|
185
|
+
ok =
|
|
186
|
+
(await asyncCheck("RDS accessible", async () => {
|
|
187
|
+
await awsQueryApi("DescribeDBInstances", {}, "rds", cr, cfg.region);
|
|
188
|
+
})) && ok;
|
|
189
|
+
|
|
190
|
+
ok =
|
|
191
|
+
(await asyncCheck("Route 53 accessible", async () => {
|
|
192
|
+
await awsRestXmlApi("GET", "/2013-04-01/hostedzone", null, cr);
|
|
193
|
+
})) && ok;
|
|
194
|
+
|
|
163
195
|
return ok;
|
|
164
196
|
}
|
|
165
197
|
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
import { createInterface } from "readline";
|
|
2
|
+
import { phase, status, success, fatal, hint, fmt } from "../lib/output.js";
|
|
3
|
+
import { resolveAppName, resolveDns, readLink, linkApp } from "../lib/link.js";
|
|
4
|
+
import { resolveCloudId, getCloudCfg, getProvider } from "../lib/providers/resolve.js";
|
|
5
|
+
import kleur from "kleur";
|
|
6
|
+
|
|
7
|
+
function prompt(rl, question) {
|
|
8
|
+
return new Promise((resolve) => rl.question(question, resolve));
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export async function domainsList(name, options) {
|
|
12
|
+
name = resolveAppName(name);
|
|
13
|
+
var cloud = resolveCloudId(options.cloud);
|
|
14
|
+
var cfg = getCloudCfg(cloud);
|
|
15
|
+
var dnsProvider = await getProvider(cloud, "dns");
|
|
16
|
+
|
|
17
|
+
var result = await dnsProvider.listDomains(cfg, name);
|
|
18
|
+
|
|
19
|
+
if (options.json) {
|
|
20
|
+
console.log(JSON.stringify(result, null, 2));
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (result.default) {
|
|
25
|
+
console.log(
|
|
26
|
+
`\n${fmt.bold("Default:")} ${fmt.url(`https://${result.default}`)}`
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (result.custom.length === 0) {
|
|
31
|
+
console.log(`${fmt.bold("Custom:")} ${fmt.dim("(none)")}`);
|
|
32
|
+
hint("Add", `relight domains add ${name}`);
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
console.log(`\n${fmt.bold("Custom domains:")}`);
|
|
37
|
+
for (var d of result.custom) {
|
|
38
|
+
console.log(` ${d}`);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export async function domainsAdd(args, options) {
|
|
43
|
+
var name, domain;
|
|
44
|
+
|
|
45
|
+
if (args.length === 2) {
|
|
46
|
+
name = args[0];
|
|
47
|
+
domain = args[1];
|
|
48
|
+
} else if (args.length === 1) {
|
|
49
|
+
if (args[0].includes(".")) {
|
|
50
|
+
name = resolveAppName(null);
|
|
51
|
+
domain = args[0];
|
|
52
|
+
} else {
|
|
53
|
+
name = args[0];
|
|
54
|
+
domain = null;
|
|
55
|
+
}
|
|
56
|
+
} else {
|
|
57
|
+
name = resolveAppName(null);
|
|
58
|
+
domain = null;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
var appCloud = resolveCloudId(options.cloud);
|
|
62
|
+
var appCfg = getCloudCfg(appCloud);
|
|
63
|
+
var appProvider = await getProvider(appCloud, "app");
|
|
64
|
+
|
|
65
|
+
// Cross-cloud DNS: --dns flag or .relight dns field specifies a different cloud for DNS records
|
|
66
|
+
var dnsFlag = options.dns || resolveDns();
|
|
67
|
+
var crossCloud = dnsFlag && resolveCloudId(dnsFlag) !== appCloud;
|
|
68
|
+
var dnsCloud = crossCloud ? resolveCloudId(dnsFlag) : appCloud;
|
|
69
|
+
var dnsCfg = crossCloud ? getCloudCfg(dnsCloud) : appCfg;
|
|
70
|
+
var dnsProvider = await getProvider(dnsCloud, "dns");
|
|
71
|
+
|
|
72
|
+
var appConfig = await appProvider.getAppConfig(appCfg, name);
|
|
73
|
+
if (!appConfig) {
|
|
74
|
+
fatal(
|
|
75
|
+
`App ${fmt.app(name)} not found.`,
|
|
76
|
+
`Run ${fmt.cmd(`relight deploy ${name} .`)} first.`
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Fetch zones from the DNS provider
|
|
81
|
+
status("Loading zones...");
|
|
82
|
+
var zones = await dnsProvider.getZones(dnsCfg);
|
|
83
|
+
|
|
84
|
+
if (zones.length === 0) {
|
|
85
|
+
fatal(
|
|
86
|
+
"No active zones found in the DNS account.",
|
|
87
|
+
"Add a domain to your DNS provider first."
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
var rl = createInterface({ input: process.stdin, output: process.stderr });
|
|
92
|
+
var zone;
|
|
93
|
+
|
|
94
|
+
if (domain) {
|
|
95
|
+
zone = dnsProvider.findZoneForHostname(zones, domain);
|
|
96
|
+
if (!zone) {
|
|
97
|
+
rl.close();
|
|
98
|
+
var zoneList = zones.map((z) => ` ${z.name}`).join("\n");
|
|
99
|
+
fatal(
|
|
100
|
+
`No zone found for '${domain}'.`,
|
|
101
|
+
`Available zones:\n${zoneList}`
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
} else {
|
|
105
|
+
process.stderr.write(`\n${kleur.bold("Available zones:")}\n\n`);
|
|
106
|
+
for (var i = 0; i < zones.length; i++) {
|
|
107
|
+
process.stderr.write(
|
|
108
|
+
` ${kleur.bold(`[${i + 1}]`)} ${zones[i].name}\n`
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
process.stderr.write("\n");
|
|
112
|
+
|
|
113
|
+
var zoneChoice = await prompt(rl, `Select zone [1-${zones.length}]: `);
|
|
114
|
+
var zoneIdx = parseInt(zoneChoice, 10) - 1;
|
|
115
|
+
if (isNaN(zoneIdx) || zoneIdx < 0 || zoneIdx >= zones.length) {
|
|
116
|
+
rl.close();
|
|
117
|
+
fatal("Invalid selection.");
|
|
118
|
+
}
|
|
119
|
+
zone = zones[zoneIdx];
|
|
120
|
+
|
|
121
|
+
process.stderr.write(`\n${kleur.bold("Route type:")}\n\n`);
|
|
122
|
+
process.stderr.write(` ${kleur.bold("[1]")} Root domain (${zone.name})\n`);
|
|
123
|
+
process.stderr.write(` ${kleur.bold("[2]")} Subdomain (*.${zone.name})\n`);
|
|
124
|
+
process.stderr.write("\n");
|
|
125
|
+
|
|
126
|
+
var routeChoice = await prompt(rl, "Select [1-2]: ");
|
|
127
|
+
|
|
128
|
+
if (routeChoice.trim() === "1") {
|
|
129
|
+
domain = zone.name;
|
|
130
|
+
} else if (routeChoice.trim() === "2") {
|
|
131
|
+
var sub = await prompt(rl, `Subdomain: ${fmt.dim("___." + zone.name + " -> ")} `);
|
|
132
|
+
sub = (sub || "").trim();
|
|
133
|
+
if (!sub) {
|
|
134
|
+
rl.close();
|
|
135
|
+
fatal("No subdomain provided.");
|
|
136
|
+
}
|
|
137
|
+
domain = `${sub}.${zone.name}`;
|
|
138
|
+
} else {
|
|
139
|
+
rl.close();
|
|
140
|
+
fatal("Invalid selection.");
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
rl.close();
|
|
145
|
+
|
|
146
|
+
if (crossCloud) {
|
|
147
|
+
// Cross-cloud: DNS record on one cloud, app config on another
|
|
148
|
+
status(`Creating DNS record for ${domain}...`);
|
|
149
|
+
var appUrl = await appProvider.getAppUrl(appCfg, name);
|
|
150
|
+
var target = new URL(appUrl).hostname;
|
|
151
|
+
try {
|
|
152
|
+
await dnsProvider.addDnsRecord(dnsCfg, domain, target, zone);
|
|
153
|
+
} catch (e) {
|
|
154
|
+
fatal(e.message);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Update app config on the app cloud
|
|
158
|
+
status(`Updating app config on ${appCloud}...`);
|
|
159
|
+
if (!appConfig.domains) appConfig.domains = [];
|
|
160
|
+
if (!appConfig.domains.includes(domain)) {
|
|
161
|
+
appConfig.domains.push(domain);
|
|
162
|
+
await appProvider.pushAppConfig(appCfg, name, appConfig);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Persist dns cloud in .relight so future commands don't need --dns
|
|
166
|
+
var linked = readLink();
|
|
167
|
+
if (linked && !linked.dns) {
|
|
168
|
+
linkApp(linked.app, linked.cloud, dnsCloud);
|
|
169
|
+
}
|
|
170
|
+
} else {
|
|
171
|
+
// Same-cloud: existing flow
|
|
172
|
+
status(`Attaching ${domain} to relight-${name}...`);
|
|
173
|
+
try {
|
|
174
|
+
await dnsProvider.addDomain(dnsCfg, name, domain, { zone, zones });
|
|
175
|
+
} catch (e) {
|
|
176
|
+
fatal(e.message);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
success(`Domain ${fmt.bold(domain)} added to ${fmt.app(name)}.`);
|
|
181
|
+
process.stderr.write(` ${fmt.url(`https://${domain}`)}\n`);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
export async function domainsRemove(args, options) {
|
|
185
|
+
var name, domain;
|
|
186
|
+
if (args.length === 2) {
|
|
187
|
+
name = args[0];
|
|
188
|
+
domain = args[1];
|
|
189
|
+
} else if (args.length === 1) {
|
|
190
|
+
name = resolveAppName(null);
|
|
191
|
+
domain = args[0];
|
|
192
|
+
} else {
|
|
193
|
+
fatal("Usage: relight domains remove [name] <domain>");
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
var appCloud = resolveCloudId(options.cloud);
|
|
197
|
+
var appCfg = getCloudCfg(appCloud);
|
|
198
|
+
|
|
199
|
+
var dnsFlag = options.dns || resolveDns();
|
|
200
|
+
var crossCloud = dnsFlag && resolveCloudId(dnsFlag) !== appCloud;
|
|
201
|
+
var dnsCloud = crossCloud ? resolveCloudId(dnsFlag) : appCloud;
|
|
202
|
+
var dnsCfg = crossCloud ? getCloudCfg(dnsCloud) : appCfg;
|
|
203
|
+
var dnsProvider = await getProvider(dnsCloud, "dns");
|
|
204
|
+
|
|
205
|
+
status(`Removing ${domain}...`);
|
|
206
|
+
|
|
207
|
+
if (crossCloud) {
|
|
208
|
+
// Cross-cloud: remove DNS record from dns cloud, update app config on app cloud
|
|
209
|
+
await dnsProvider.removeDnsRecord(dnsCfg, domain);
|
|
210
|
+
|
|
211
|
+
var appProvider = await getProvider(appCloud, "app");
|
|
212
|
+
var appConfig = await appProvider.getAppConfig(appCfg, name);
|
|
213
|
+
if (appConfig) {
|
|
214
|
+
appConfig.domains = (appConfig.domains || []).filter((d) => d !== domain);
|
|
215
|
+
await appProvider.pushAppConfig(appCfg, name, appConfig);
|
|
216
|
+
}
|
|
217
|
+
} else {
|
|
218
|
+
// Same-cloud: existing flow
|
|
219
|
+
await dnsProvider.removeDomain(dnsCfg, name, domain);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
success(`Domain ${fmt.bold(domain)} removed from ${fmt.app(name)}.`);
|
|
223
|
+
}
|