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
package/src/lib/clouds/gcp.js
CHANGED
|
@@ -1,11 +1,17 @@
|
|
|
1
1
|
import { createSign } from "crypto";
|
|
2
2
|
import { readFileSync } from "fs";
|
|
3
3
|
|
|
4
|
-
var RUN_API = "https://run.googleapis.com/v2";
|
|
4
|
+
export var RUN_API = "https://run.googleapis.com/v2";
|
|
5
5
|
var CRM_API = "https://cloudresourcemanager.googleapis.com/v1";
|
|
6
6
|
var TOKEN_URI = "https://oauth2.googleapis.com/token";
|
|
7
7
|
var SCOPE = "https://www.googleapis.com/auth/cloud-platform";
|
|
8
8
|
|
|
9
|
+
export var AR_API = "https://artifactregistry.googleapis.com/v1";
|
|
10
|
+
export var SQLADMIN_API = "https://sqladmin.googleapis.com/v1";
|
|
11
|
+
export var DNS_API = "https://dns.googleapis.com/dns/v1";
|
|
12
|
+
export var LOGGING_API = "https://logging.googleapis.com/v2";
|
|
13
|
+
export var MONITORING_API = "https://monitoring.googleapis.com/v3";
|
|
14
|
+
|
|
9
15
|
// --- Service account key file ---
|
|
10
16
|
|
|
11
17
|
export function readKeyFile(path) {
|
|
@@ -25,7 +31,7 @@ export function readKeyFile(path) {
|
|
|
25
31
|
};
|
|
26
32
|
}
|
|
27
33
|
|
|
28
|
-
// --- JWT
|
|
34
|
+
// --- JWT -> access token ---
|
|
29
35
|
|
|
30
36
|
export async function mintAccessToken(clientEmail, privateKey) {
|
|
31
37
|
var now = Math.floor(Date.now() / 1000);
|
|
@@ -53,7 +59,7 @@ export async function mintAccessToken(clientEmail, privateKey) {
|
|
|
53
59
|
var res = await fetch(TOKEN_URI, {
|
|
54
60
|
method: "POST",
|
|
55
61
|
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
56
|
-
body: `grant_type=${encodeURIComponent("urn:ietf:params:oauth:
|
|
62
|
+
body: `grant_type=${encodeURIComponent("urn:ietf:params:oauth:grant-type:jwt-bearer")}&assertion=${encodeURIComponent(jwt)}`,
|
|
57
63
|
});
|
|
58
64
|
|
|
59
65
|
if (!res.ok) {
|
|
@@ -95,10 +101,51 @@ export async function gcpApi(method, url, body, token) {
|
|
|
95
101
|
return res.json();
|
|
96
102
|
}
|
|
97
103
|
|
|
104
|
+
// --- LRO polling ---
|
|
105
|
+
|
|
106
|
+
export async function waitForOperation(token, operationName, apiBase) {
|
|
107
|
+
var url = apiBase
|
|
108
|
+
? `${apiBase}/${operationName}`
|
|
109
|
+
: `https://run.googleapis.com/v2/${operationName}`;
|
|
110
|
+
while (true) {
|
|
111
|
+
var op = await gcpApi("GET", url, null, token);
|
|
112
|
+
if (op.done) {
|
|
113
|
+
if (op.error) {
|
|
114
|
+
throw new Error(`Operation failed: ${op.error.message || JSON.stringify(op.error)}`);
|
|
115
|
+
}
|
|
116
|
+
return op.response || op;
|
|
117
|
+
}
|
|
118
|
+
await new Promise((r) => setTimeout(r, 2000));
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export async function waitForSqlOperation(token, project, opName) {
|
|
123
|
+
while (true) {
|
|
124
|
+
var op = await gcpApi(
|
|
125
|
+
"GET",
|
|
126
|
+
`${SQLADMIN_API}/projects/${project}/operations/${opName}`,
|
|
127
|
+
null,
|
|
128
|
+
token
|
|
129
|
+
);
|
|
130
|
+
if (op.status === "DONE") {
|
|
131
|
+
if (op.error) {
|
|
132
|
+
var msgs = (op.error.errors || []).map((e) => e.message).join(", ");
|
|
133
|
+
throw new Error(`SQL operation failed: ${msgs || JSON.stringify(op.error)}`);
|
|
134
|
+
}
|
|
135
|
+
return op;
|
|
136
|
+
}
|
|
137
|
+
await new Promise((r) => setTimeout(r, 3000));
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// --- Project verification ---
|
|
142
|
+
|
|
98
143
|
export async function verifyProject(token, project) {
|
|
99
144
|
return gcpApi("GET", `${CRM_API}/projects/${project}`, null, token);
|
|
100
145
|
}
|
|
101
146
|
|
|
147
|
+
// --- Cloud Run ---
|
|
148
|
+
|
|
102
149
|
export async function listRegions(token, project) {
|
|
103
150
|
var res = await gcpApi(
|
|
104
151
|
"GET",
|
|
@@ -117,4 +164,208 @@ export async function listServices(token, project, region) {
|
|
|
117
164
|
token
|
|
118
165
|
);
|
|
119
166
|
return res.services || [];
|
|
120
|
-
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export async function listAllServices(token, project) {
|
|
170
|
+
var res = await gcpApi(
|
|
171
|
+
"GET",
|
|
172
|
+
`${RUN_API}/projects/${project}/locations/-/services`,
|
|
173
|
+
null,
|
|
174
|
+
token
|
|
175
|
+
);
|
|
176
|
+
return res.services || [];
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export async function getService(token, serviceName) {
|
|
180
|
+
return gcpApi("GET", `${RUN_API}/${serviceName}`, null, token);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export async function createService(token, project, region, serviceId, body) {
|
|
184
|
+
var op = await gcpApi(
|
|
185
|
+
"POST",
|
|
186
|
+
`${RUN_API}/projects/${project}/locations/${region}/services?serviceId=${serviceId}`,
|
|
187
|
+
body,
|
|
188
|
+
token
|
|
189
|
+
);
|
|
190
|
+
return waitForOperation(token, op.name);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export async function updateService(token, serviceName, body) {
|
|
194
|
+
var op = await gcpApi("PATCH", `${RUN_API}/${serviceName}`, body, token);
|
|
195
|
+
return waitForOperation(token, op.name);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
export async function deleteService(token, serviceName) {
|
|
199
|
+
var op = await gcpApi("DELETE", `${RUN_API}/${serviceName}`, null, token);
|
|
200
|
+
return waitForOperation(token, op.name);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
export async function setIamPolicy(token, serviceName, policy) {
|
|
204
|
+
return gcpApi(
|
|
205
|
+
"POST",
|
|
206
|
+
`${RUN_API}/${serviceName}:setIamPolicy`,
|
|
207
|
+
{ policy },
|
|
208
|
+
token
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// --- Artifact Registry ---
|
|
213
|
+
|
|
214
|
+
export async function getRepository(token, project, location, repoName) {
|
|
215
|
+
return gcpApi(
|
|
216
|
+
"GET",
|
|
217
|
+
`${AR_API}/projects/${project}/locations/${location}/repositories/${repoName}`,
|
|
218
|
+
null,
|
|
219
|
+
token
|
|
220
|
+
);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
export async function createRepository(token, project, location, repoName) {
|
|
224
|
+
var op = await gcpApi(
|
|
225
|
+
"POST",
|
|
226
|
+
`${AR_API}/projects/${project}/locations/${location}/repositories?repositoryId=${repoName}`,
|
|
227
|
+
{ format: "DOCKER" },
|
|
228
|
+
token
|
|
229
|
+
);
|
|
230
|
+
if (op.done) return op.response || op;
|
|
231
|
+
return waitForOperation(token, op.name, AR_API);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// --- Cloud SQL ---
|
|
235
|
+
|
|
236
|
+
export async function createSqlInstance(token, project, body) {
|
|
237
|
+
var op = await gcpApi(
|
|
238
|
+
"POST",
|
|
239
|
+
`${SQLADMIN_API}/projects/${project}/instances`,
|
|
240
|
+
body,
|
|
241
|
+
token
|
|
242
|
+
);
|
|
243
|
+
return waitForSqlOperation(token, project, op.name);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
export async function getSqlInstance(token, project, instanceName) {
|
|
247
|
+
return gcpApi(
|
|
248
|
+
"GET",
|
|
249
|
+
`${SQLADMIN_API}/projects/${project}/instances/${instanceName}`,
|
|
250
|
+
null,
|
|
251
|
+
token
|
|
252
|
+
);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
export async function deleteSqlInstance(token, project, instanceName) {
|
|
256
|
+
var op = await gcpApi(
|
|
257
|
+
"DELETE",
|
|
258
|
+
`${SQLADMIN_API}/projects/${project}/instances/${instanceName}`,
|
|
259
|
+
null,
|
|
260
|
+
token
|
|
261
|
+
);
|
|
262
|
+
return waitForSqlOperation(token, project, op.name);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
export async function createSqlDatabase(token, project, instanceName, dbName) {
|
|
266
|
+
var op = await gcpApi(
|
|
267
|
+
"POST",
|
|
268
|
+
`${SQLADMIN_API}/projects/${project}/instances/${instanceName}/databases`,
|
|
269
|
+
{ name: dbName },
|
|
270
|
+
token
|
|
271
|
+
);
|
|
272
|
+
return waitForSqlOperation(token, project, op.name);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
export async function deleteSqlDatabase(token, project, instanceName, dbName) {
|
|
276
|
+
var op = await gcpApi(
|
|
277
|
+
"DELETE",
|
|
278
|
+
`${SQLADMIN_API}/projects/${project}/instances/${instanceName}/databases/${dbName}`,
|
|
279
|
+
null,
|
|
280
|
+
token
|
|
281
|
+
);
|
|
282
|
+
return waitForSqlOperation(token, project, op.name);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
export async function createSqlUser(token, project, instanceName, userName, password) {
|
|
286
|
+
var op = await gcpApi(
|
|
287
|
+
"POST",
|
|
288
|
+
`${SQLADMIN_API}/projects/${project}/instances/${instanceName}/users`,
|
|
289
|
+
{ name: userName, password },
|
|
290
|
+
token
|
|
291
|
+
);
|
|
292
|
+
return waitForSqlOperation(token, project, op.name);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
export async function updateSqlUser(token, project, instanceName, userName, password) {
|
|
296
|
+
var op = await gcpApi(
|
|
297
|
+
"PUT",
|
|
298
|
+
`${SQLADMIN_API}/projects/${project}/instances/${instanceName}/users?name=${encodeURIComponent(userName)}`,
|
|
299
|
+
{ name: userName, password },
|
|
300
|
+
token
|
|
301
|
+
);
|
|
302
|
+
return waitForSqlOperation(token, project, op.name);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
export async function deleteSqlUser(token, project, instanceName, userName) {
|
|
306
|
+
var op = await gcpApi(
|
|
307
|
+
"DELETE",
|
|
308
|
+
`${SQLADMIN_API}/projects/${project}/instances/${instanceName}/users?name=${encodeURIComponent(userName)}`,
|
|
309
|
+
null,
|
|
310
|
+
token
|
|
311
|
+
);
|
|
312
|
+
return waitForSqlOperation(token, project, op.name);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
export async function listSqlDatabases(token, project, instanceName) {
|
|
316
|
+
var res = await gcpApi(
|
|
317
|
+
"GET",
|
|
318
|
+
`${SQLADMIN_API}/projects/${project}/instances/${instanceName}/databases`,
|
|
319
|
+
null,
|
|
320
|
+
token
|
|
321
|
+
);
|
|
322
|
+
return res.items || [];
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// --- Cloud DNS ---
|
|
326
|
+
|
|
327
|
+
export async function listManagedZones(token, project) {
|
|
328
|
+
var res = await gcpApi(
|
|
329
|
+
"GET",
|
|
330
|
+
`${DNS_API}/projects/${project}/managedZones`,
|
|
331
|
+
null,
|
|
332
|
+
token
|
|
333
|
+
);
|
|
334
|
+
return res.managedZones || [];
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
export async function createDnsChange(token, project, zoneName, change) {
|
|
338
|
+
return gcpApi(
|
|
339
|
+
"POST",
|
|
340
|
+
`${DNS_API}/projects/${project}/managedZones/${zoneName}/changes`,
|
|
341
|
+
change,
|
|
342
|
+
token
|
|
343
|
+
);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
export async function listResourceRecordSets(token, project, zoneName) {
|
|
347
|
+
var res = await gcpApi(
|
|
348
|
+
"GET",
|
|
349
|
+
`${DNS_API}/projects/${project}/managedZones/${zoneName}/rrsets`,
|
|
350
|
+
null,
|
|
351
|
+
token
|
|
352
|
+
);
|
|
353
|
+
return res.rrsets || [];
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// --- Cloud Logging ---
|
|
357
|
+
|
|
358
|
+
export async function listLogEntries(token, body) {
|
|
359
|
+
return gcpApi("POST", `${LOGGING_API}/entries:list`, body, token);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// --- Cloud Monitoring ---
|
|
363
|
+
|
|
364
|
+
export async function queryTimeSeries(token, project, body) {
|
|
365
|
+
return gcpApi(
|
|
366
|
+
"POST",
|
|
367
|
+
`${MONITORING_API}/projects/${project}/timeSeries:query`,
|
|
368
|
+
body,
|
|
369
|
+
token
|
|
370
|
+
);
|
|
371
|
+
}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
var API_BASE = "https://console.neon.tech/api/v2";
|
|
2
|
+
|
|
3
|
+
export async function neonApi(apiKey, method, path, body) {
|
|
4
|
+
var headers = {
|
|
5
|
+
Authorization: `Bearer ${apiKey}`,
|
|
6
|
+
Accept: "application/json",
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
if (body && typeof body === "object") {
|
|
10
|
+
headers["Content-Type"] = "application/json";
|
|
11
|
+
body = JSON.stringify(body);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
var res = await fetch(`${API_BASE}${path}`, {
|
|
15
|
+
method,
|
|
16
|
+
headers,
|
|
17
|
+
body: method === "GET" || method === "DELETE" ? undefined : body,
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
if (!res.ok) {
|
|
21
|
+
var text = await res.text();
|
|
22
|
+
throw new Error(`Neon API ${method} ${path}: ${res.status} ${text}`);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (res.status === 204) return null;
|
|
26
|
+
return res.json();
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// --- Projects ---
|
|
30
|
+
|
|
31
|
+
export async function listProjects(apiKey) {
|
|
32
|
+
var data = await neonApi(apiKey, "GET", "/projects");
|
|
33
|
+
return data.projects || [];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export async function createProject(apiKey, opts = {}) {
|
|
37
|
+
var body = {
|
|
38
|
+
project: {
|
|
39
|
+
name: opts.name || "relight",
|
|
40
|
+
pg_version: opts.pgVersion || 16,
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
if (opts.regionId) body.project.region_id = opts.regionId;
|
|
44
|
+
return neonApi(apiKey, "POST", "/projects", body);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export async function getProject(apiKey, projectId) {
|
|
48
|
+
return neonApi(apiKey, "GET", `/projects/${projectId}`);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export async function deleteProject(apiKey, projectId) {
|
|
52
|
+
return neonApi(apiKey, "DELETE", `/projects/${projectId}`);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// --- Branches ---
|
|
56
|
+
|
|
57
|
+
export async function listBranches(apiKey, projectId) {
|
|
58
|
+
var data = await neonApi(apiKey, "GET", `/projects/${projectId}/branches`);
|
|
59
|
+
return data.branches || [];
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// --- Databases ---
|
|
63
|
+
|
|
64
|
+
export async function listDatabases(apiKey, projectId, branchId) {
|
|
65
|
+
var data = await neonApi(
|
|
66
|
+
apiKey,
|
|
67
|
+
"GET",
|
|
68
|
+
`/projects/${projectId}/branches/${branchId}/databases`
|
|
69
|
+
);
|
|
70
|
+
return data.databases || [];
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export async function createDatabase(apiKey, projectId, branchId, dbName, ownerName) {
|
|
74
|
+
return neonApi(
|
|
75
|
+
apiKey,
|
|
76
|
+
"POST",
|
|
77
|
+
`/projects/${projectId}/branches/${branchId}/databases`,
|
|
78
|
+
{ database: { name: dbName, owner_name: ownerName } }
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export async function deleteDatabase(apiKey, projectId, branchId, dbName) {
|
|
83
|
+
return neonApi(
|
|
84
|
+
apiKey,
|
|
85
|
+
"DELETE",
|
|
86
|
+
`/projects/${projectId}/branches/${branchId}/databases/${dbName}`
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// --- Roles ---
|
|
91
|
+
|
|
92
|
+
export async function createRole(apiKey, projectId, branchId, roleName) {
|
|
93
|
+
return neonApi(
|
|
94
|
+
apiKey,
|
|
95
|
+
"POST",
|
|
96
|
+
`/projects/${projectId}/branches/${branchId}/roles`,
|
|
97
|
+
{ role: { name: roleName } }
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export async function deleteRole(apiKey, projectId, branchId, roleName) {
|
|
102
|
+
return neonApi(
|
|
103
|
+
apiKey,
|
|
104
|
+
"DELETE",
|
|
105
|
+
`/projects/${projectId}/branches/${branchId}/roles/${roleName}`
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export async function getRolePassword(apiKey, projectId, branchId, roleName) {
|
|
110
|
+
var data = await neonApi(
|
|
111
|
+
apiKey,
|
|
112
|
+
"GET",
|
|
113
|
+
`/projects/${projectId}/branches/${branchId}/roles/${roleName}/reveal_password`
|
|
114
|
+
);
|
|
115
|
+
return data.password;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export async function resetRolePassword(apiKey, projectId, branchId, roleName) {
|
|
119
|
+
var data = await neonApi(
|
|
120
|
+
apiKey,
|
|
121
|
+
"POST",
|
|
122
|
+
`/projects/${projectId}/branches/${branchId}/roles/${roleName}/reset_password`
|
|
123
|
+
);
|
|
124
|
+
return data.password;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// --- Connection URI ---
|
|
128
|
+
|
|
129
|
+
export async function getConnectionUri(apiKey, projectId, dbName, roleName) {
|
|
130
|
+
var params = new URLSearchParams({
|
|
131
|
+
database_name: dbName,
|
|
132
|
+
role_name: roleName,
|
|
133
|
+
});
|
|
134
|
+
var data = await neonApi(
|
|
135
|
+
apiKey,
|
|
136
|
+
"GET",
|
|
137
|
+
`/projects/${projectId}/connection_uri?${params}`
|
|
138
|
+
);
|
|
139
|
+
return data.uri;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// --- Verification ---
|
|
143
|
+
|
|
144
|
+
export async function verifyApiKey(apiKey) {
|
|
145
|
+
var projects = await listProjects(apiKey);
|
|
146
|
+
return projects;
|
|
147
|
+
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
// SlicerVM API client
|
|
2
|
+
// Supports two connection modes:
|
|
3
|
+
// - Unix socket: cfg.socketPath (local dev, no auth)
|
|
4
|
+
// - HTTP: cfg.apiUrl + cfg.apiToken (remote, bearer token auth)
|
|
5
|
+
|
|
6
|
+
import http from "node:http";
|
|
7
|
+
|
|
8
|
+
function socketRequest(socketPath, method, path, body, headers) {
|
|
9
|
+
return new Promise((resolve, reject) => {
|
|
10
|
+
var opts = { socketPath, method, path, headers: headers || {} };
|
|
11
|
+
var req = http.request(opts, (res) => {
|
|
12
|
+
var chunks = [];
|
|
13
|
+
res.on("data", (c) => chunks.push(c));
|
|
14
|
+
res.on("end", () => {
|
|
15
|
+
resolve({
|
|
16
|
+
ok: res.statusCode >= 200 && res.statusCode < 300,
|
|
17
|
+
status: res.statusCode,
|
|
18
|
+
headers: res.headers,
|
|
19
|
+
text: () => Promise.resolve(Buffer.concat(chunks).toString()),
|
|
20
|
+
json: () => Promise.resolve(JSON.parse(Buffer.concat(chunks).toString())),
|
|
21
|
+
body: null,
|
|
22
|
+
});
|
|
23
|
+
});
|
|
24
|
+
});
|
|
25
|
+
req.on("error", reject);
|
|
26
|
+
if (body) req.write(body);
|
|
27
|
+
req.end();
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export async function slicerFetch(cfg, method, path, body, opts = {}) {
|
|
32
|
+
var headers = {};
|
|
33
|
+
|
|
34
|
+
if (cfg.apiToken) {
|
|
35
|
+
headers.Authorization = `Bearer ${cfg.apiToken}`;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
var rawBody = body;
|
|
39
|
+
if (opts.contentType) {
|
|
40
|
+
headers["Content-Type"] = opts.contentType;
|
|
41
|
+
} else if (body && typeof body === "object" && !Buffer.isBuffer(body)) {
|
|
42
|
+
headers["Content-Type"] = "application/json";
|
|
43
|
+
rawBody = JSON.stringify(body);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
var res;
|
|
47
|
+
if (cfg.socketPath) {
|
|
48
|
+
res = await socketRequest(cfg.socketPath, method, path, method === "GET" ? undefined : rawBody, headers);
|
|
49
|
+
} else {
|
|
50
|
+
res = await fetch(`${cfg.apiUrl}${path}`, {
|
|
51
|
+
method,
|
|
52
|
+
headers,
|
|
53
|
+
body: method === "GET" ? undefined : rawBody,
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (!res.ok) {
|
|
58
|
+
var text = await res.text();
|
|
59
|
+
throw new Error(`Slicer API ${method} ${path}: ${res.status} ${text}`);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
var ct = (res.headers instanceof Headers)
|
|
63
|
+
? res.headers.get("content-type") || ""
|
|
64
|
+
: res.headers["content-type"] || "";
|
|
65
|
+
|
|
66
|
+
if (ct.includes("application/json")) {
|
|
67
|
+
return res.json();
|
|
68
|
+
}
|
|
69
|
+
if (opts.stream) {
|
|
70
|
+
return res;
|
|
71
|
+
}
|
|
72
|
+
return res.text();
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// --- Nodes ---
|
|
76
|
+
|
|
77
|
+
export async function listNodes(cfg) {
|
|
78
|
+
return slicerFetch(cfg, "GET", "/nodes");
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export async function createNode(cfg, hostGroup, opts = {}) {
|
|
82
|
+
return slicerFetch(cfg, "POST", `/hostgroup/${hostGroup}/nodes`, {
|
|
83
|
+
tags: opts.tags || [],
|
|
84
|
+
vcpu: opts.vcpu,
|
|
85
|
+
memory: opts.memory,
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export async function deleteNode(cfg, hostGroup, hostname) {
|
|
90
|
+
return slicerFetch(cfg, "DELETE", `/hostgroup/${hostGroup}/nodes/${hostname}`);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// --- VM lifecycle ---
|
|
94
|
+
|
|
95
|
+
export async function resumeVM(cfg, hostname) {
|
|
96
|
+
return slicerFetch(cfg, "POST", `/vm/${hostname}/resume`);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export async function pauseVM(cfg, hostname) {
|
|
100
|
+
return slicerFetch(cfg, "POST", `/vm/${hostname}/pause`);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export async function healthCheck(cfg, hostname) {
|
|
104
|
+
return slicerFetch(cfg, "GET", `/vm/${hostname}/health`);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// --- Exec ---
|
|
108
|
+
|
|
109
|
+
export async function execInVM(cfg, hostname, cmd, args, opts = {}) {
|
|
110
|
+
var qs = new URLSearchParams();
|
|
111
|
+
qs.set("cmd", cmd);
|
|
112
|
+
for (var arg of (args || [])) {
|
|
113
|
+
qs.append("args", arg);
|
|
114
|
+
}
|
|
115
|
+
if (opts.uid !== undefined) qs.set("uid", String(opts.uid));
|
|
116
|
+
if (opts.gid !== undefined) qs.set("gid", String(opts.gid));
|
|
117
|
+
if (opts.workdir) qs.set("cwd", opts.workdir);
|
|
118
|
+
return slicerFetch(cfg, "POST", `/vm/${hostname}/exec?${qs}`, null, {
|
|
119
|
+
stream: opts.stream,
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// --- File upload ---
|
|
124
|
+
|
|
125
|
+
export async function uploadToVM(cfg, hostname, path, tarBuffer, opts = {}) {
|
|
126
|
+
var qs = new URLSearchParams({ path });
|
|
127
|
+
if (opts.uid !== undefined) qs.set("uid", String(opts.uid));
|
|
128
|
+
if (opts.gid !== undefined) qs.set("gid", String(opts.gid));
|
|
129
|
+
if (opts.mode) qs.set("mode", opts.mode);
|
|
130
|
+
return slicerFetch(cfg, "POST", `/vm/${hostname}/cp?${qs}`, tarBuffer, {
|
|
131
|
+
contentType: "application/x-tar",
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// --- Auth verification ---
|
|
136
|
+
|
|
137
|
+
export async function verifyConnection(cfg) {
|
|
138
|
+
return listNodes(cfg);
|
|
139
|
+
}
|