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,232 @@
|
|
|
1
|
+
import { awsRestXmlApi, xmlList, xmlVal, xmlBlock } from "../../clouds/aws.js";
|
|
2
|
+
import { getAppConfig, pushAppConfig, getAppUrl } from "./app.js";
|
|
3
|
+
|
|
4
|
+
export async function getZones(cfg) {
|
|
5
|
+
var cr = { accessKeyId: cfg.accessKeyId, secretAccessKey: cfg.secretAccessKey };
|
|
6
|
+
var xml = await awsRestXmlApi("GET", "/2013-04-01/hostedzone", null, cr);
|
|
7
|
+
|
|
8
|
+
var zones = xmlList(xml, "HostedZone");
|
|
9
|
+
return zones.map((z) => {
|
|
10
|
+
var id = xmlVal(z, "Id");
|
|
11
|
+
var name = xmlVal(z, "Name");
|
|
12
|
+
return {
|
|
13
|
+
id: id ? id.replace("/hostedzone/", "") : null,
|
|
14
|
+
name: name ? name.replace(/\.$/, "") : null,
|
|
15
|
+
};
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function findZoneForHostname(zones, hostname) {
|
|
20
|
+
var match = null;
|
|
21
|
+
for (var zone of zones) {
|
|
22
|
+
if (hostname === zone.name || hostname.endsWith("." + zone.name)) {
|
|
23
|
+
if (!match || zone.name.length > match.name.length) {
|
|
24
|
+
match = zone;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return match;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export async function listDomains(cfg, appName) {
|
|
32
|
+
var url = await getAppUrl(cfg, appName);
|
|
33
|
+
var defaultDomain = url ? new URL(url).hostname : null;
|
|
34
|
+
|
|
35
|
+
var appConfig = await getAppConfig(cfg, appName);
|
|
36
|
+
var custom = appConfig?.domains || [];
|
|
37
|
+
|
|
38
|
+
return {
|
|
39
|
+
default: defaultDomain,
|
|
40
|
+
custom,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export async function addDomain(cfg, appName, domain, { zone }) {
|
|
45
|
+
var cr = { accessKeyId: cfg.accessKeyId, secretAccessKey: cfg.secretAccessKey };
|
|
46
|
+
|
|
47
|
+
// Get App Runner URL to use as CNAME target
|
|
48
|
+
var url = await getAppUrl(cfg, appName);
|
|
49
|
+
if (!url) throw new Error("Could not determine app URL for CNAME target.");
|
|
50
|
+
var target = new URL(url).hostname;
|
|
51
|
+
|
|
52
|
+
// FQDN with trailing dot for Route 53
|
|
53
|
+
var fqdn = domain.endsWith(".") ? domain : domain + ".";
|
|
54
|
+
var targetFqdn = target.endsWith(".") ? target : target + ".";
|
|
55
|
+
|
|
56
|
+
var xmlBody = `<?xml version="1.0" encoding="UTF-8"?>
|
|
57
|
+
<ChangeResourceRecordSetsRequest xmlns="https://route53.amazonaws.com/doc/2013-04-01/">
|
|
58
|
+
<ChangeBatch>
|
|
59
|
+
<Changes>
|
|
60
|
+
<Change>
|
|
61
|
+
<Action>UPSERT</Action>
|
|
62
|
+
<ResourceRecordSet>
|
|
63
|
+
<Name>${fqdn}</Name>
|
|
64
|
+
<Type>CNAME</Type>
|
|
65
|
+
<TTL>300</TTL>
|
|
66
|
+
<ResourceRecords>
|
|
67
|
+
<ResourceRecord>
|
|
68
|
+
<Value>${targetFqdn}</Value>
|
|
69
|
+
</ResourceRecord>
|
|
70
|
+
</ResourceRecords>
|
|
71
|
+
</ResourceRecordSet>
|
|
72
|
+
</Change>
|
|
73
|
+
</Changes>
|
|
74
|
+
</ChangeBatch>
|
|
75
|
+
</ChangeResourceRecordSetsRequest>`;
|
|
76
|
+
|
|
77
|
+
await awsRestXmlApi("POST", `/2013-04-01/hostedzone/${zone.id}/rrset`, xmlBody, cr);
|
|
78
|
+
|
|
79
|
+
// Update app config
|
|
80
|
+
var appConfig = await getAppConfig(cfg, appName);
|
|
81
|
+
if (appConfig) {
|
|
82
|
+
if (!appConfig.domains) appConfig.domains = [];
|
|
83
|
+
if (!appConfig.domains.includes(domain)) {
|
|
84
|
+
appConfig.domains.push(domain);
|
|
85
|
+
await pushAppConfig(cfg, appName, appConfig);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export async function removeDomain(cfg, appName, domain) {
|
|
91
|
+
var cr = { accessKeyId: cfg.accessKeyId, secretAccessKey: cfg.secretAccessKey };
|
|
92
|
+
var fqdn = domain.endsWith(".") ? domain : domain + ".";
|
|
93
|
+
|
|
94
|
+
// Find the zone for this domain
|
|
95
|
+
var zones = await getZones(cfg);
|
|
96
|
+
var zone = findZoneForHostname(zones, domain);
|
|
97
|
+
|
|
98
|
+
if (zone) {
|
|
99
|
+
// List record sets to get current value and TTL for DELETE
|
|
100
|
+
var xml = await awsRestXmlApi("GET", `/2013-04-01/hostedzone/${zone.id}/rrset`, null, cr);
|
|
101
|
+
var recordSets = xmlList(xml, "ResourceRecordSet");
|
|
102
|
+
|
|
103
|
+
var existing = null;
|
|
104
|
+
for (var rs of recordSets) {
|
|
105
|
+
var rsName = xmlVal(rs, "Name");
|
|
106
|
+
var rsType = xmlVal(rs, "Type");
|
|
107
|
+
if (rsName === fqdn && rsType === "CNAME") {
|
|
108
|
+
existing = rs;
|
|
109
|
+
break;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (existing) {
|
|
114
|
+
var ttl = xmlVal(existing, "TTL") || "300";
|
|
115
|
+
var value = xmlVal(xmlBlock(existing, "ResourceRecords") || "", "Value") || "";
|
|
116
|
+
|
|
117
|
+
var xmlBody = `<?xml version="1.0" encoding="UTF-8"?>
|
|
118
|
+
<ChangeResourceRecordSetsRequest xmlns="https://route53.amazonaws.com/doc/2013-04-01/">
|
|
119
|
+
<ChangeBatch>
|
|
120
|
+
<Changes>
|
|
121
|
+
<Change>
|
|
122
|
+
<Action>DELETE</Action>
|
|
123
|
+
<ResourceRecordSet>
|
|
124
|
+
<Name>${fqdn}</Name>
|
|
125
|
+
<Type>CNAME</Type>
|
|
126
|
+
<TTL>${ttl}</TTL>
|
|
127
|
+
<ResourceRecords>
|
|
128
|
+
<ResourceRecord>
|
|
129
|
+
<Value>${value}</Value>
|
|
130
|
+
</ResourceRecord>
|
|
131
|
+
</ResourceRecords>
|
|
132
|
+
</ResourceRecordSet>
|
|
133
|
+
</Change>
|
|
134
|
+
</Changes>
|
|
135
|
+
</ChangeBatch>
|
|
136
|
+
</ChangeResourceRecordSetsRequest>`;
|
|
137
|
+
|
|
138
|
+
await awsRestXmlApi("POST", `/2013-04-01/hostedzone/${zone.id}/rrset`, xmlBody, cr);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Update app config
|
|
143
|
+
var appConfig = await getAppConfig(cfg, appName);
|
|
144
|
+
if (appConfig) {
|
|
145
|
+
appConfig.domains = (appConfig.domains || []).filter((d) => d !== domain);
|
|
146
|
+
await pushAppConfig(cfg, appName, appConfig);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// --- Pure DNS record operations (for cross-cloud use) ---
|
|
151
|
+
|
|
152
|
+
export async function addDnsRecord(cfg, domain, target, zone) {
|
|
153
|
+
var cr = { accessKeyId: cfg.accessKeyId, secretAccessKey: cfg.secretAccessKey };
|
|
154
|
+
var fqdn = domain.endsWith(".") ? domain : domain + ".";
|
|
155
|
+
var targetFqdn = target.endsWith(".") ? target : target + ".";
|
|
156
|
+
|
|
157
|
+
// Check for existing CNAME
|
|
158
|
+
var xml = await awsRestXmlApi("GET", `/2013-04-01/hostedzone/${zone.id}/rrset`, null, cr);
|
|
159
|
+
var recordSets = xmlList(xml, "ResourceRecordSet");
|
|
160
|
+
for (var rs of recordSets) {
|
|
161
|
+
if (xmlVal(rs, "Name") === fqdn && xmlVal(rs, "Type") === "CNAME") {
|
|
162
|
+
var value = xmlVal(xmlBlock(rs, "ResourceRecords") || "", "Value") || "";
|
|
163
|
+
throw new Error(`CNAME record already exists for ${domain} -> ${value}`);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
var xmlBody = `<?xml version="1.0" encoding="UTF-8"?>
|
|
168
|
+
<ChangeResourceRecordSetsRequest xmlns="https://route53.amazonaws.com/doc/2013-04-01/">
|
|
169
|
+
<ChangeBatch>
|
|
170
|
+
<Changes>
|
|
171
|
+
<Change>
|
|
172
|
+
<Action>CREATE</Action>
|
|
173
|
+
<ResourceRecordSet>
|
|
174
|
+
<Name>${fqdn}</Name>
|
|
175
|
+
<Type>CNAME</Type>
|
|
176
|
+
<TTL>300</TTL>
|
|
177
|
+
<ResourceRecords>
|
|
178
|
+
<ResourceRecord>
|
|
179
|
+
<Value>${targetFqdn}</Value>
|
|
180
|
+
</ResourceRecord>
|
|
181
|
+
</ResourceRecords>
|
|
182
|
+
</ResourceRecordSet>
|
|
183
|
+
</Change>
|
|
184
|
+
</Changes>
|
|
185
|
+
</ChangeBatch>
|
|
186
|
+
</ChangeResourceRecordSetsRequest>`;
|
|
187
|
+
|
|
188
|
+
await awsRestXmlApi("POST", `/2013-04-01/hostedzone/${zone.id}/rrset`, xmlBody, cr);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export async function removeDnsRecord(cfg, domain) {
|
|
192
|
+
var cr = { accessKeyId: cfg.accessKeyId, secretAccessKey: cfg.secretAccessKey };
|
|
193
|
+
var fqdn = domain.endsWith(".") ? domain : domain + ".";
|
|
194
|
+
|
|
195
|
+
var zones = await getZones(cfg);
|
|
196
|
+
var zone = findZoneForHostname(zones, domain);
|
|
197
|
+
if (!zone) return;
|
|
198
|
+
|
|
199
|
+
var xml = await awsRestXmlApi("GET", `/2013-04-01/hostedzone/${zone.id}/rrset`, null, cr);
|
|
200
|
+
var recordSets = xmlList(xml, "ResourceRecordSet");
|
|
201
|
+
|
|
202
|
+
for (var rs of recordSets) {
|
|
203
|
+
if (xmlVal(rs, "Name") === fqdn && xmlVal(rs, "Type") === "CNAME") {
|
|
204
|
+
var ttl = xmlVal(rs, "TTL") || "300";
|
|
205
|
+
var value = xmlVal(xmlBlock(rs, "ResourceRecords") || "", "Value") || "";
|
|
206
|
+
|
|
207
|
+
var xmlBody = `<?xml version="1.0" encoding="UTF-8"?>
|
|
208
|
+
<ChangeResourceRecordSetsRequest xmlns="https://route53.amazonaws.com/doc/2013-04-01/">
|
|
209
|
+
<ChangeBatch>
|
|
210
|
+
<Changes>
|
|
211
|
+
<Change>
|
|
212
|
+
<Action>DELETE</Action>
|
|
213
|
+
<ResourceRecordSet>
|
|
214
|
+
<Name>${fqdn}</Name>
|
|
215
|
+
<Type>CNAME</Type>
|
|
216
|
+
<TTL>${ttl}</TTL>
|
|
217
|
+
<ResourceRecords>
|
|
218
|
+
<ResourceRecord>
|
|
219
|
+
<Value>${value}</Value>
|
|
220
|
+
</ResourceRecord>
|
|
221
|
+
</ResourceRecords>
|
|
222
|
+
</ResourceRecordSet>
|
|
223
|
+
</Change>
|
|
224
|
+
</Changes>
|
|
225
|
+
</ChangeBatch>
|
|
226
|
+
</ChangeResourceRecordSetsRequest>`;
|
|
227
|
+
|
|
228
|
+
await awsRestXmlApi("POST", `/2013-04-01/hostedzone/${zone.id}/rrset`, xmlBody, cr);
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { awsJsonApi, getAccountId } from "../../clouds/aws.js";
|
|
2
|
+
|
|
3
|
+
export async function getCredentials(cfg) {
|
|
4
|
+
var cr = { accessKeyId: cfg.accessKeyId, secretAccessKey: cfg.secretAccessKey };
|
|
5
|
+
|
|
6
|
+
var res = await awsJsonApi(
|
|
7
|
+
"AmazonEC2ContainerRegistry_V20150921.GetAuthorizationToken",
|
|
8
|
+
{},
|
|
9
|
+
"ecr",
|
|
10
|
+
cr,
|
|
11
|
+
cfg.region,
|
|
12
|
+
`api.ecr.${cfg.region}.amazonaws.com`
|
|
13
|
+
);
|
|
14
|
+
|
|
15
|
+
var authData = res.authorizationData?.[0];
|
|
16
|
+
if (!authData) throw new Error("No ECR authorization data returned.");
|
|
17
|
+
|
|
18
|
+
var decoded = Buffer.from(authData.authorizationToken, "base64").toString();
|
|
19
|
+
var [username, password] = decoded.split(":");
|
|
20
|
+
var registry = authData.proxyEndpoint; // https://{accountId}.dkr.ecr.{region}.amazonaws.com
|
|
21
|
+
|
|
22
|
+
return { registry, username, password };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export async function getImageTag(cfg, appName, tag) {
|
|
26
|
+
var cr = { accessKeyId: cfg.accessKeyId, secretAccessKey: cfg.secretAccessKey };
|
|
27
|
+
var accountId = await getAccountId(cr, cfg.region);
|
|
28
|
+
return `${accountId}.dkr.ecr.${cfg.region}.amazonaws.com/relight-${appName}:${tag}`;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export async function ensureRepository(cfg, appName) {
|
|
32
|
+
var cr = { accessKeyId: cfg.accessKeyId, secretAccessKey: cfg.secretAccessKey };
|
|
33
|
+
var repoName = `relight-${appName}`;
|
|
34
|
+
var host = `api.ecr.${cfg.region}.amazonaws.com`;
|
|
35
|
+
|
|
36
|
+
// Check if repository exists
|
|
37
|
+
try {
|
|
38
|
+
await awsJsonApi(
|
|
39
|
+
"AmazonEC2ContainerRegistry_V20150921.DescribeRepositories",
|
|
40
|
+
{ repositoryNames: [repoName] },
|
|
41
|
+
"ecr",
|
|
42
|
+
cr,
|
|
43
|
+
cfg.region,
|
|
44
|
+
host
|
|
45
|
+
);
|
|
46
|
+
return; // Already exists
|
|
47
|
+
} catch {
|
|
48
|
+
// Repository doesn't exist, create it
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
await awsJsonApi(
|
|
52
|
+
"AmazonEC2ContainerRegistry_V20150921.CreateRepository",
|
|
53
|
+
{ repositoryName: repoName },
|
|
54
|
+
"ecr",
|
|
55
|
+
cr,
|
|
56
|
+
cfg.region,
|
|
57
|
+
host
|
|
58
|
+
);
|
|
59
|
+
}
|