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,264 @@
|
|
|
1
|
+
import { createInterface } from "readline";
|
|
2
|
+
import { phase, status, success, hint, fatal, fmt, generateAppName } from "../lib/output.js";
|
|
3
|
+
import { readLink, linkApp, resolveAppName } from "../lib/link.js";
|
|
4
|
+
import { resolveTarget } from "../lib/providers/resolve.js";
|
|
5
|
+
import { dockerBuild, dockerTag, dockerPush, dockerLogin } from "../lib/docker.js";
|
|
6
|
+
|
|
7
|
+
export async function deploy(nameOrPath, path, options) {
|
|
8
|
+
var target = await resolveTarget(options);
|
|
9
|
+
var cfg = target.cfg;
|
|
10
|
+
|
|
11
|
+
// Smart arg parsing: if first arg looks like a path, shift args
|
|
12
|
+
var name;
|
|
13
|
+
var dockerPath;
|
|
14
|
+
if (!nameOrPath) {
|
|
15
|
+
name = readLink()?.app || generateAppName();
|
|
16
|
+
dockerPath = ".";
|
|
17
|
+
} else if (nameOrPath.startsWith(".") || nameOrPath.startsWith("/") || nameOrPath.startsWith("~")) {
|
|
18
|
+
name = readLink()?.app || generateAppName();
|
|
19
|
+
dockerPath = nameOrPath;
|
|
20
|
+
} else {
|
|
21
|
+
name = nameOrPath;
|
|
22
|
+
dockerPath = path || ".";
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
var tag = options.tag || `${Date.now()}`;
|
|
26
|
+
|
|
27
|
+
// Get registry credentials and image tag
|
|
28
|
+
var registry = await target.provider("registry");
|
|
29
|
+
var remoteTag = await registry.getImageTag(cfg, name, tag);
|
|
30
|
+
var localTag = `relight-${name}:${tag}`;
|
|
31
|
+
|
|
32
|
+
// Load existing config from deployed worker (null on first deploy)
|
|
33
|
+
var appProvider = await target.provider("app");
|
|
34
|
+
var appConfig;
|
|
35
|
+
try {
|
|
36
|
+
appConfig = await appProvider.getAppConfig(cfg, name);
|
|
37
|
+
} catch {
|
|
38
|
+
appConfig = null;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
var isFirstDeploy = !appConfig;
|
|
42
|
+
|
|
43
|
+
// Get valid regions for this cloud/service
|
|
44
|
+
var validRegions = appProvider.getRegions();
|
|
45
|
+
var validCodes = validRegions.map((r) => r.code);
|
|
46
|
+
|
|
47
|
+
if (appConfig) {
|
|
48
|
+
// Existing app - update image, merge any flags
|
|
49
|
+
appConfig.image = remoteTag;
|
|
50
|
+
appConfig.deployedAt = new Date().toISOString();
|
|
51
|
+
|
|
52
|
+
if (options.env) {
|
|
53
|
+
if (!appConfig.envKeys) appConfig.envKeys = [];
|
|
54
|
+
if (!appConfig.secretKeys) appConfig.secretKeys = [];
|
|
55
|
+
if (!appConfig.env) appConfig.env = {};
|
|
56
|
+
for (var v of options.env) {
|
|
57
|
+
var eq = v.indexOf("=");
|
|
58
|
+
if (eq !== -1) {
|
|
59
|
+
var k = v.substring(0, eq);
|
|
60
|
+
appConfig.env[k] = v.substring(eq + 1);
|
|
61
|
+
appConfig.secretKeys = appConfig.secretKeys.filter((s) => s !== k);
|
|
62
|
+
if (!appConfig.envKeys.includes(k)) appConfig.envKeys.push(k);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
if (options.regions)
|
|
67
|
+
appConfig.regions = options.regions.split(",").map((r) => r.trim());
|
|
68
|
+
if (options.instances) appConfig.instances = options.instances;
|
|
69
|
+
if (options.port) appConfig.port = options.port;
|
|
70
|
+
if (options.sleep) appConfig.sleepAfter = options.sleep;
|
|
71
|
+
if (options.instanceType) appConfig.instanceType = options.instanceType;
|
|
72
|
+
if (options.vcpu) appConfig.vcpu = options.vcpu;
|
|
73
|
+
if (options.memory) appConfig.memory = options.memory;
|
|
74
|
+
if (options.disk) appConfig.disk = options.disk;
|
|
75
|
+
if (options.observability === false) appConfig.observability = false;
|
|
76
|
+
} else {
|
|
77
|
+
// First deploy - build config from flags + defaults
|
|
78
|
+
var env = {};
|
|
79
|
+
var envKeys = [];
|
|
80
|
+
if (options.env) {
|
|
81
|
+
for (var v of options.env) {
|
|
82
|
+
var eq = v.indexOf("=");
|
|
83
|
+
if (eq !== -1) {
|
|
84
|
+
var k = v.substring(0, eq);
|
|
85
|
+
env[k] = v.substring(eq + 1);
|
|
86
|
+
envKeys.push(k);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
var isService = target.kind === "service";
|
|
92
|
+
var defaultRegion = isService ? "self-hosted" : target.type === "gcp" ? "us-central1" : target.type === "aws" ? "us-east-1" : "enam";
|
|
93
|
+
var regions;
|
|
94
|
+
|
|
95
|
+
if (options.regions) {
|
|
96
|
+
regions = options.regions.split(",").map((r) => r.trim());
|
|
97
|
+
} else if (!isService && (target.type === "gcp" || target.type === "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 || (isService ? 1 : 2),
|
|
134
|
+
port: options.port || 8080,
|
|
135
|
+
sleepAfter: options.sleep || "30s",
|
|
136
|
+
instanceType: options.instanceType || (isService || target.type === "gcp" || target.type === "aws" ? 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
|
+
var newSecrets = {};
|
|
151
|
+
|
|
152
|
+
// --- Summary & confirmation ---
|
|
153
|
+
var instanceDesc = appConfig.vcpu
|
|
154
|
+
? `${appConfig.vcpu} vCPU, ${appConfig.memory || "default"} MiB`
|
|
155
|
+
: appConfig.instanceType || "lite";
|
|
156
|
+
|
|
157
|
+
process.stderr.write(`\n${fmt.bold("Deploy summary")}\n`);
|
|
158
|
+
process.stderr.write(`${fmt.dim("-".repeat(40))}\n`);
|
|
159
|
+
process.stderr.write(` ${fmt.bold("App:")} ${fmt.app(name)}${isFirstDeploy ? fmt.dim(" (new)") : ""}\n`);
|
|
160
|
+
if (target.kind === "service") {
|
|
161
|
+
process.stderr.write(` ${fmt.bold("Service:")} ${fmt.cloud(target.id)} ${fmt.dim(`(${target.type})`)}\n`);
|
|
162
|
+
} else {
|
|
163
|
+
process.stderr.write(` ${fmt.bold("Cloud:")} ${fmt.cloud(target.id)}\n`);
|
|
164
|
+
}
|
|
165
|
+
process.stderr.write(` ${fmt.bold("Path:")} ${dockerPath}\n`);
|
|
166
|
+
process.stderr.write(` ${fmt.bold("Image:")} ${remoteTag}\n`);
|
|
167
|
+
process.stderr.write(` ${fmt.bold("Regions:")} ${appConfig.regions.join(", ")}\n`);
|
|
168
|
+
process.stderr.write(` ${fmt.bold("Instances:")} ${appConfig.instances || 2} per region\n`);
|
|
169
|
+
process.stderr.write(` ${fmt.bold("Type:")} ${instanceDesc}\n`);
|
|
170
|
+
process.stderr.write(` ${fmt.bold("Port:")} ${appConfig.port || 8080}\n`);
|
|
171
|
+
process.stderr.write(` ${fmt.bold("Sleep:")} ${appConfig.sleepAfter || "30s"}\n`);
|
|
172
|
+
if (appConfig.dbId) {
|
|
173
|
+
process.stderr.write(` ${fmt.bold("Database:")} ${appConfig.dbName}\n`);
|
|
174
|
+
}
|
|
175
|
+
process.stderr.write(`${fmt.dim("-".repeat(40))}\n`);
|
|
176
|
+
|
|
177
|
+
if (!options.yes) {
|
|
178
|
+
var rl = createInterface({ input: process.stdin, output: process.stderr });
|
|
179
|
+
var answer = await new Promise((resolve) =>
|
|
180
|
+
rl.question("\nProceed? [Y/n] ", resolve)
|
|
181
|
+
);
|
|
182
|
+
rl.close();
|
|
183
|
+
if (answer && !answer.match(/^y(es)?$/i)) {
|
|
184
|
+
process.stderr.write("Deploy cancelled.\n");
|
|
185
|
+
process.exit(0);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// 1. Build Docker image
|
|
190
|
+
var platform = "linux/amd64";
|
|
191
|
+
if (target.type === "slicervm") {
|
|
192
|
+
// Match VM architecture
|
|
193
|
+
var { listNodes } = await import("../lib/clouds/slicervm.js");
|
|
194
|
+
var nodes = await listNodes(cfg);
|
|
195
|
+
var vmArch = nodes[0]?.arch;
|
|
196
|
+
if (vmArch === "arm64" || vmArch === "aarch64") platform = "linux/arm64";
|
|
197
|
+
}
|
|
198
|
+
phase("Building image");
|
|
199
|
+
status(`${localTag} for ${platform}`);
|
|
200
|
+
dockerBuild(dockerPath, localTag, { platform });
|
|
201
|
+
|
|
202
|
+
if (target.kind === "service") {
|
|
203
|
+
// Service: skip registry push - deploy extracts and uploads the image directly
|
|
204
|
+
phase("Deploying");
|
|
205
|
+
await appProvider.deploy(cfg, name, localTag, {
|
|
206
|
+
appConfig,
|
|
207
|
+
isFirstDeploy,
|
|
208
|
+
newSecrets,
|
|
209
|
+
});
|
|
210
|
+
} else {
|
|
211
|
+
// 2. Push to registry
|
|
212
|
+
phase("Pushing to registry");
|
|
213
|
+
status("Authenticating...");
|
|
214
|
+
var creds = await registry.getCredentials(cfg);
|
|
215
|
+
dockerLogin(creds.registry, creds.username, creds.password);
|
|
216
|
+
if (registry.ensureRepository) await registry.ensureRepository(cfg, name);
|
|
217
|
+
status(`Pushing ${remoteTag}...`);
|
|
218
|
+
dockerTag(localTag, remoteTag);
|
|
219
|
+
dockerPush(remoteTag);
|
|
220
|
+
|
|
221
|
+
// 3. Deploy via provider
|
|
222
|
+
phase("Deploying");
|
|
223
|
+
await appProvider.deploy(cfg, name, remoteTag, {
|
|
224
|
+
appConfig,
|
|
225
|
+
isFirstDeploy,
|
|
226
|
+
newSecrets,
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// 4. Resolve URL and report
|
|
231
|
+
var url = await appProvider.getAppUrl(cfg, name);
|
|
232
|
+
|
|
233
|
+
if (options.json) {
|
|
234
|
+
var result = {
|
|
235
|
+
name,
|
|
236
|
+
image: remoteTag,
|
|
237
|
+
url,
|
|
238
|
+
regions: appConfig.regions,
|
|
239
|
+
instances: appConfig.instances,
|
|
240
|
+
firstDeploy: isFirstDeploy,
|
|
241
|
+
};
|
|
242
|
+
if (target.kind === "service") {
|
|
243
|
+
result.service = target.id;
|
|
244
|
+
} else {
|
|
245
|
+
result.cloud = target.id;
|
|
246
|
+
}
|
|
247
|
+
console.log(JSON.stringify(result, null, 2));
|
|
248
|
+
} else {
|
|
249
|
+
success(`App ${fmt.app(name)} deployed!`);
|
|
250
|
+
process.stderr.write(` ${fmt.bold("Name:")} ${fmt.app(name)}\n`);
|
|
251
|
+
process.stderr.write(` ${fmt.bold("Image:")} ${remoteTag}\n`);
|
|
252
|
+
process.stderr.write(
|
|
253
|
+
` ${fmt.bold("URL:")} ${url ? fmt.url(url) : fmt.dim("(configure workers.dev subdomain to see URL)")}\n`
|
|
254
|
+
);
|
|
255
|
+
hint("Next", `relight open ${name}`);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// Link this directory to the app
|
|
259
|
+
if (target.kind === "service") {
|
|
260
|
+
linkApp(name, null, options.dns, null, target.id);
|
|
261
|
+
} else {
|
|
262
|
+
linkApp(name, target.id, options.dns);
|
|
263
|
+
}
|
|
264
|
+
}
|
package/src/commands/doctor.js
CHANGED
|
@@ -4,10 +4,14 @@ import {
|
|
|
4
4
|
tryGetConfig,
|
|
5
5
|
CONFIG_PATH,
|
|
6
6
|
CLOUD_NAMES,
|
|
7
|
+
SERVICE_TYPES,
|
|
8
|
+
getRegisteredServices,
|
|
9
|
+
normalizeServiceConfig,
|
|
7
10
|
} from "../lib/config.js";
|
|
8
11
|
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";
|
|
12
|
+
import { mintAccessToken, verifyProject as gcpVerifyProject, listRegions as gcpListRegions, gcpApi, AR_API, SQLADMIN_API, DNS_API } from "../lib/clouds/gcp.js";
|
|
13
|
+
import { verifyCredentials as awsVerify, checkAppRunner, awsJsonApi, awsQueryApi, awsRestXmlApi } from "../lib/clouds/aws.js";
|
|
14
|
+
import { verifyConnection as slicerVerify } from "../lib/clouds/slicervm.js";
|
|
11
15
|
import kleur from "kleur";
|
|
12
16
|
|
|
13
17
|
var PASS = kleur.green("[ok]");
|
|
@@ -16,7 +20,7 @@ var SKIP = kleur.yellow("[--]");
|
|
|
16
20
|
|
|
17
21
|
export async function doctor() {
|
|
18
22
|
process.stderr.write(`\n${kleur.bold("relight doctor")}\n`);
|
|
19
|
-
process.stderr.write(`${kleur.dim("
|
|
23
|
+
process.stderr.write(`${kleur.dim("-".repeat(50))}\n\n`);
|
|
20
24
|
var allGood = true;
|
|
21
25
|
|
|
22
26
|
// --- General checks ---
|
|
@@ -73,9 +77,29 @@ export async function doctor() {
|
|
|
73
77
|
}
|
|
74
78
|
}
|
|
75
79
|
|
|
80
|
+
// --- Services ---
|
|
81
|
+
|
|
82
|
+
var services = getRegisteredServices();
|
|
83
|
+
if (services.length > 0) {
|
|
84
|
+
process.stderr.write(`\n${kleur.bold(" Services")}\n`);
|
|
85
|
+
|
|
86
|
+
for (var service of services) {
|
|
87
|
+
var typeName = SERVICE_TYPES[service.type]?.name || service.type;
|
|
88
|
+
var endpoint = service.socketPath || service.apiUrl || "unknown";
|
|
89
|
+
|
|
90
|
+
allGood =
|
|
91
|
+
(await asyncCheck(`${service.name} (${typeName} - ${endpoint})`, async () => {
|
|
92
|
+
if (service.type === "slicervm") {
|
|
93
|
+
var cfg = normalizeServiceConfig(service);
|
|
94
|
+
await slicerVerify(cfg);
|
|
95
|
+
}
|
|
96
|
+
})) && allGood;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
76
100
|
// --- Summary ---
|
|
77
101
|
|
|
78
|
-
process.stderr.write(`\n${kleur.dim("
|
|
102
|
+
process.stderr.write(`\n${kleur.dim("-".repeat(50))}\n`);
|
|
79
103
|
if (allGood) {
|
|
80
104
|
process.stderr.write(kleur.green("All checks passed.\n\n"));
|
|
81
105
|
} else {
|
|
@@ -134,6 +158,21 @@ async function checkGCP(cfg) {
|
|
|
134
158
|
(await asyncCheck("Cloud Run API reachable", async () => {
|
|
135
159
|
await gcpListRegions(token, cfg.project);
|
|
136
160
|
})) && ok;
|
|
161
|
+
|
|
162
|
+
ok =
|
|
163
|
+
(await asyncCheck("Artifact Registry API reachable", async () => {
|
|
164
|
+
await gcpApi("GET", `${AR_API}/projects/${cfg.project}/locations/us/repositories`, null, token);
|
|
165
|
+
})) && ok;
|
|
166
|
+
|
|
167
|
+
ok =
|
|
168
|
+
(await asyncCheck("Cloud SQL Admin API reachable", async () => {
|
|
169
|
+
await gcpApi("GET", `${SQLADMIN_API}/projects/${cfg.project}/instances`, null, token);
|
|
170
|
+
})) && ok;
|
|
171
|
+
|
|
172
|
+
ok =
|
|
173
|
+
(await asyncCheck("Cloud DNS API reachable", async () => {
|
|
174
|
+
await gcpApi("GET", `${DNS_API}/projects/${cfg.project}/managedZones`, null, token);
|
|
175
|
+
})) && ok;
|
|
137
176
|
}
|
|
138
177
|
|
|
139
178
|
return ok;
|
|
@@ -143,23 +182,40 @@ async function checkGCP(cfg) {
|
|
|
143
182
|
|
|
144
183
|
async function checkAWS(cfg) {
|
|
145
184
|
var ok = true;
|
|
185
|
+
var cr = { accessKeyId: cfg.accessKeyId, secretAccessKey: cfg.secretAccessKey };
|
|
146
186
|
|
|
147
187
|
ok =
|
|
148
188
|
(await asyncCheck("Credentials valid (STS)", async () => {
|
|
149
|
-
await awsVerify(
|
|
150
|
-
{ accessKeyId: cfg.accessKeyId, secretAccessKey: cfg.secretAccessKey },
|
|
151
|
-
cfg.region
|
|
152
|
-
);
|
|
189
|
+
await awsVerify(cr, cfg.region);
|
|
153
190
|
})) && ok;
|
|
154
191
|
|
|
155
192
|
ok =
|
|
156
193
|
(await asyncCheck("App Runner accessible", async () => {
|
|
157
|
-
await checkAppRunner(
|
|
158
|
-
|
|
159
|
-
|
|
194
|
+
await checkAppRunner(cr, cfg.region);
|
|
195
|
+
})) && ok;
|
|
196
|
+
|
|
197
|
+
ok =
|
|
198
|
+
(await asyncCheck("ECR accessible", async () => {
|
|
199
|
+
await awsJsonApi(
|
|
200
|
+
"AmazonEC2ContainerRegistry_V20150921.DescribeRepositories",
|
|
201
|
+
{},
|
|
202
|
+
"ecr",
|
|
203
|
+
cr,
|
|
204
|
+
cfg.region,
|
|
205
|
+
`api.ecr.${cfg.region}.amazonaws.com`
|
|
160
206
|
);
|
|
161
207
|
})) && ok;
|
|
162
208
|
|
|
209
|
+
ok =
|
|
210
|
+
(await asyncCheck("RDS accessible", async () => {
|
|
211
|
+
await awsQueryApi("DescribeDBInstances", {}, "rds", cr, cfg.region);
|
|
212
|
+
})) && ok;
|
|
213
|
+
|
|
214
|
+
ok =
|
|
215
|
+
(await asyncCheck("Route 53 accessible", async () => {
|
|
216
|
+
await awsRestXmlApi("GET", "/2013-04-01/hostedzone", null, cr);
|
|
217
|
+
})) && ok;
|
|
218
|
+
|
|
163
219
|
return ok;
|
|
164
220
|
}
|
|
165
221
|
|
|
@@ -172,7 +228,7 @@ function check(label, fn) {
|
|
|
172
228
|
return true;
|
|
173
229
|
} catch (e) {
|
|
174
230
|
process.stderr.write(` ${FAIL} ${label}`);
|
|
175
|
-
if (e.message) process.stderr.write(kleur.dim(`
|
|
231
|
+
if (e.message) process.stderr.write(kleur.dim(` - ${e.message}`));
|
|
176
232
|
process.stderr.write("\n");
|
|
177
233
|
return false;
|
|
178
234
|
}
|
|
@@ -185,7 +241,7 @@ async function asyncCheck(label, fn) {
|
|
|
185
241
|
return true;
|
|
186
242
|
} catch (e) {
|
|
187
243
|
process.stderr.write(` ${FAIL} ${label}`);
|
|
188
|
-
if (e.message) process.stderr.write(kleur.dim(`
|
|
244
|
+
if (e.message) process.stderr.write(kleur.dim(` - ${truncate(e.message, 80)}`));
|
|
189
245
|
process.stderr.write("\n");
|
|
190
246
|
return false;
|
|
191
247
|
}
|
|
@@ -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 { resolveTarget, 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 target = await resolveTarget(options);
|
|
14
|
+
var cfg = target.cfg;
|
|
15
|
+
var dnsProvider = await target.provider("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 target = await resolveTarget(options);
|
|
62
|
+
var appCfg = target.cfg;
|
|
63
|
+
var appProvider = await target.provider("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 && (target.kind === "service" || resolveCloudId(dnsFlag) !== target.id);
|
|
68
|
+
var dnsCloud = crossCloud ? resolveCloudId(dnsFlag) : (target.kind === "cloud" ? target.id : null);
|
|
69
|
+
var dnsCfg = crossCloud ? getCloudCfg(dnsCloud) : appCfg;
|
|
70
|
+
var dnsProvider = crossCloud ? await getProvider(dnsCloud, "dns") : await target.provider("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 dnsTarget = new URL(appUrl).hostname;
|
|
151
|
+
try {
|
|
152
|
+
await dnsProvider.addDnsRecord(dnsCfg, domain, dnsTarget, zone);
|
|
153
|
+
} catch (e) {
|
|
154
|
+
fatal(e.message);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Update app config on the app cloud/service
|
|
158
|
+
status(`Updating app config...`);
|
|
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, undefined, linked.compute);
|
|
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 target = await resolveTarget(options);
|
|
197
|
+
var appCfg = target.cfg;
|
|
198
|
+
|
|
199
|
+
var dnsFlag = options.dns || resolveDns();
|
|
200
|
+
var crossCloud = dnsFlag && (target.kind === "service" || resolveCloudId(dnsFlag) !== target.id);
|
|
201
|
+
var dnsCloud = crossCloud ? resolveCloudId(dnsFlag) : (target.kind === "cloud" ? target.id : null);
|
|
202
|
+
var dnsCfg = crossCloud ? getCloudCfg(dnsCloud) : appCfg;
|
|
203
|
+
var dnsProvider = crossCloud ? await getProvider(dnsCloud, "dns") : await target.provider("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/service
|
|
209
|
+
await dnsProvider.removeDnsRecord(dnsCfg, domain);
|
|
210
|
+
|
|
211
|
+
var appProvider = await target.provider("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
|
+
}
|