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,596 @@
|
|
|
1
|
+
import {
|
|
2
|
+
uploadWorker,
|
|
3
|
+
deleteWorker,
|
|
4
|
+
listWorkerScripts,
|
|
5
|
+
getWorkerSettings,
|
|
6
|
+
getDONamespaceId,
|
|
7
|
+
listContainerApps,
|
|
8
|
+
findContainerApp,
|
|
9
|
+
createContainerApp,
|
|
10
|
+
deleteContainerApp,
|
|
11
|
+
modifyContainerApp,
|
|
12
|
+
createRollout,
|
|
13
|
+
createTail,
|
|
14
|
+
deleteTail,
|
|
15
|
+
getWorkersSubdomain,
|
|
16
|
+
enableWorkerSubdomain,
|
|
17
|
+
cfGraphQL,
|
|
18
|
+
} from "../../clouds/cf.js";
|
|
19
|
+
import { getWorkerBundle, templateHash } from "./bundle.js";
|
|
20
|
+
|
|
21
|
+
var VALID_HINTS = [
|
|
22
|
+
"wnam", "enam", "sam", "weur", "eeur", "apac", "oc", "afr", "me",
|
|
23
|
+
];
|
|
24
|
+
|
|
25
|
+
export { VALID_HINTS };
|
|
26
|
+
|
|
27
|
+
// --- App config (stored in the deployed worker's RELIGHT_APP_CONFIG binding) ---
|
|
28
|
+
|
|
29
|
+
export async function getAppConfig(cfg, appName) {
|
|
30
|
+
var scriptName = `relight-${appName}`;
|
|
31
|
+
var settings = await getWorkerSettings(cfg.accountId, cfg.apiToken, scriptName);
|
|
32
|
+
var bindings = settings?.bindings || [];
|
|
33
|
+
var binding = bindings.find((b) => b.name === "RELIGHT_APP_CONFIG");
|
|
34
|
+
if (!binding) return null;
|
|
35
|
+
var appConfig = JSON.parse(binding.text);
|
|
36
|
+
|
|
37
|
+
// Migration: old format has env object with values but no envKeys/secretKeys
|
|
38
|
+
if (appConfig.env && !appConfig.envKeys) {
|
|
39
|
+
appConfig.envKeys = Object.keys(appConfig.env);
|
|
40
|
+
appConfig.secretKeys = [];
|
|
41
|
+
return appConfig;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// New format: reconstruct env from native bindings
|
|
45
|
+
if (!appConfig.env) appConfig.env = {};
|
|
46
|
+
for (var key of (appConfig.envKeys || [])) {
|
|
47
|
+
var b = bindings.find((x) => x.name === key && x.type === "plain_text");
|
|
48
|
+
if (b) appConfig.env[key] = b.text;
|
|
49
|
+
}
|
|
50
|
+
for (var key of (appConfig.secretKeys || [])) {
|
|
51
|
+
appConfig.env[key] = "[hidden]";
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return appConfig;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export async function pushAppConfig(cfg, appName, appConfig, { newSecrets } = {}) {
|
|
58
|
+
var code = getWorkerBundle();
|
|
59
|
+
var metadata = buildWorkerMetadata(appConfig, { firstDeploy: false, newSecrets });
|
|
60
|
+
await uploadWorker(cfg.accountId, cfg.apiToken, `relight-${appName}`, code, metadata);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// --- Metadata builder ---
|
|
64
|
+
|
|
65
|
+
export function buildWorkerMetadata(appConfig, { firstDeploy = false, newSecrets } = {}) {
|
|
66
|
+
var envKeys = appConfig.envKeys || [];
|
|
67
|
+
var secretKeys = appConfig.secretKeys || [];
|
|
68
|
+
var configJson = Object.assign({}, appConfig);
|
|
69
|
+
|
|
70
|
+
// Backward compat: keep env with plain_text values only
|
|
71
|
+
var backcompatEnv = {};
|
|
72
|
+
for (var key of envKeys) {
|
|
73
|
+
if (appConfig.env && appConfig.env[key] !== undefined) {
|
|
74
|
+
backcompatEnv[key] = appConfig.env[key];
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
configJson.env = backcompatEnv;
|
|
78
|
+
|
|
79
|
+
var bindings = [
|
|
80
|
+
{
|
|
81
|
+
type: "durable_object_namespace",
|
|
82
|
+
name: "APP_CONTAINER",
|
|
83
|
+
class_name: "AppContainer",
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
type: "plain_text",
|
|
87
|
+
name: "RELIGHT_APP_CONFIG",
|
|
88
|
+
text: JSON.stringify(configJson),
|
|
89
|
+
},
|
|
90
|
+
];
|
|
91
|
+
|
|
92
|
+
// D1 binding
|
|
93
|
+
if (appConfig.dbId) {
|
|
94
|
+
bindings.push({ type: "d1", name: "DB", id: appConfig.dbId });
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Emit native plain_text bindings for each envKey
|
|
98
|
+
for (var key of envKeys) {
|
|
99
|
+
if (appConfig.env && appConfig.env[key] !== undefined) {
|
|
100
|
+
bindings.push({ type: "plain_text", name: key, text: appConfig.env[key] });
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Emit native secret_text bindings only for new/updated secrets
|
|
105
|
+
for (var key of secretKeys) {
|
|
106
|
+
if (newSecrets && newSecrets[key] !== undefined) {
|
|
107
|
+
bindings.push({ type: "secret_text", name: key, text: newSecrets[key] });
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
var metadata = {
|
|
112
|
+
main_module: "index.js",
|
|
113
|
+
compatibility_date: "2025-10-08",
|
|
114
|
+
bindings,
|
|
115
|
+
observability: {
|
|
116
|
+
enabled: appConfig.observability !== false,
|
|
117
|
+
},
|
|
118
|
+
containers: [
|
|
119
|
+
{
|
|
120
|
+
class_name: "AppContainer",
|
|
121
|
+
},
|
|
122
|
+
],
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
if (firstDeploy) {
|
|
126
|
+
metadata.migrations = {
|
|
127
|
+
new_tag: "v1",
|
|
128
|
+
new_sqlite_classes: ["AppContainer"],
|
|
129
|
+
};
|
|
130
|
+
} else {
|
|
131
|
+
metadata.migrations = {
|
|
132
|
+
old_tag: "v1",
|
|
133
|
+
new_tag: "v1",
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return metadata;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// --- Container config builder ---
|
|
141
|
+
|
|
142
|
+
function buildContainerConfig(appConfig) {
|
|
143
|
+
var cfg = {
|
|
144
|
+
image: appConfig.image,
|
|
145
|
+
observability: { logs: { enabled: appConfig.observability !== false } },
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
if (appConfig.vcpu || appConfig.memory || appConfig.disk) {
|
|
149
|
+
if (appConfig.vcpu) cfg.vcpu = appConfig.vcpu;
|
|
150
|
+
if (appConfig.memory) cfg.memory_mib = appConfig.memory;
|
|
151
|
+
if (appConfig.disk) cfg.disk = { size_mb: appConfig.disk };
|
|
152
|
+
} else {
|
|
153
|
+
cfg.instance_type = appConfig.instanceType || "lite";
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return cfg;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// --- Deploy ---
|
|
160
|
+
|
|
161
|
+
export async function deploy(cfg, appName, imageTag, opts) {
|
|
162
|
+
var scriptName = `relight-${appName}`;
|
|
163
|
+
var appConfig = opts.appConfig;
|
|
164
|
+
var isFirstDeploy = opts.isFirstDeploy;
|
|
165
|
+
var newSecrets = opts.newSecrets || {};
|
|
166
|
+
|
|
167
|
+
// Upload worker
|
|
168
|
+
var currentHash = templateHash();
|
|
169
|
+
var needsWorkerUpload = isFirstDeploy || appConfig.templateHash !== currentHash;
|
|
170
|
+
|
|
171
|
+
if (needsWorkerUpload) {
|
|
172
|
+
var bundledCode = getWorkerBundle();
|
|
173
|
+
appConfig.templateHash = currentHash;
|
|
174
|
+
var metadata = buildWorkerMetadata(appConfig, { firstDeploy: isFirstDeploy, newSecrets });
|
|
175
|
+
await uploadWorker(cfg.accountId, cfg.apiToken, scriptName, bundledCode, metadata);
|
|
176
|
+
} else {
|
|
177
|
+
await pushAppConfig(cfg, appName, appConfig, { newSecrets });
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Deploy container application
|
|
181
|
+
var namespaceId = await getDONamespaceId(cfg.accountId, cfg.apiToken, scriptName, "AppContainer");
|
|
182
|
+
if (!namespaceId) {
|
|
183
|
+
throw new Error("Could not find Durable Object namespace for AppContainer. The worker upload may have failed.");
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
var existingApp = await findContainerApp(cfg.accountId, cfg.apiToken, scriptName);
|
|
187
|
+
var maxInstances = (appConfig.regions?.length || 1) * (appConfig.instances || 2);
|
|
188
|
+
|
|
189
|
+
if (existingApp) {
|
|
190
|
+
if (existingApp.max_instances !== maxInstances) {
|
|
191
|
+
await modifyContainerApp(cfg.accountId, cfg.apiToken, existingApp.id, {
|
|
192
|
+
max_instances: maxInstances,
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
await createRollout(cfg.accountId, cfg.apiToken, existingApp.id, {
|
|
196
|
+
description: `Deploy ${imageTag}`,
|
|
197
|
+
strategy: "rolling",
|
|
198
|
+
kind: "full_auto",
|
|
199
|
+
step_percentage: 100,
|
|
200
|
+
target_configuration: buildContainerConfig(appConfig),
|
|
201
|
+
});
|
|
202
|
+
} else {
|
|
203
|
+
await createContainerApp(cfg.accountId, cfg.apiToken, {
|
|
204
|
+
name: scriptName,
|
|
205
|
+
scheduling_policy: "default",
|
|
206
|
+
instances: 0,
|
|
207
|
+
max_instances: maxInstances,
|
|
208
|
+
configuration: buildContainerConfig(appConfig),
|
|
209
|
+
durable_objects: {
|
|
210
|
+
namespace_id: namespaceId,
|
|
211
|
+
},
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Enable workers.dev route
|
|
216
|
+
try {
|
|
217
|
+
await enableWorkerSubdomain(cfg.accountId, cfg.apiToken, scriptName);
|
|
218
|
+
} catch {}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// --- List apps ---
|
|
222
|
+
|
|
223
|
+
export async function listApps(cfg) {
|
|
224
|
+
var scripts = await listWorkerScripts(cfg.accountId, cfg.apiToken);
|
|
225
|
+
var apps = scripts.filter((s) => s.id.startsWith("relight-"));
|
|
226
|
+
return apps.map((s) => ({
|
|
227
|
+
name: s.id.replace("relight-", ""),
|
|
228
|
+
modified: s.modified_on || null,
|
|
229
|
+
}));
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// --- Get app info ---
|
|
233
|
+
|
|
234
|
+
export async function getAppInfo(cfg, appName) {
|
|
235
|
+
var appConfig = await getAppConfig(cfg, appName);
|
|
236
|
+
if (!appConfig) return null;
|
|
237
|
+
|
|
238
|
+
var subdomain = await getWorkersSubdomain(cfg.accountId, cfg.apiToken);
|
|
239
|
+
var url = subdomain
|
|
240
|
+
? `https://relight-${appName}.${subdomain}.workers.dev`
|
|
241
|
+
: null;
|
|
242
|
+
|
|
243
|
+
return { appConfig, url };
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// --- Destroy ---
|
|
247
|
+
|
|
248
|
+
export async function destroyApp(cfg, appName) {
|
|
249
|
+
var scriptName = `relight-${appName}`;
|
|
250
|
+
|
|
251
|
+
// Delete D1 database if attached
|
|
252
|
+
var appConfig;
|
|
253
|
+
try {
|
|
254
|
+
appConfig = await getAppConfig(cfg, appName);
|
|
255
|
+
if (appConfig && appConfig.dbId) {
|
|
256
|
+
var { deleteD1Database } = await import("../../clouds/cf.js");
|
|
257
|
+
await deleteD1Database(cfg.accountId, cfg.apiToken, appConfig.dbId);
|
|
258
|
+
}
|
|
259
|
+
} catch {}
|
|
260
|
+
|
|
261
|
+
// Delete container application
|
|
262
|
+
try {
|
|
263
|
+
var containerApp = await findContainerApp(cfg.accountId, cfg.apiToken, scriptName);
|
|
264
|
+
if (containerApp) {
|
|
265
|
+
await deleteContainerApp(cfg.accountId, cfg.apiToken, containerApp.id);
|
|
266
|
+
}
|
|
267
|
+
} catch {}
|
|
268
|
+
|
|
269
|
+
// Delete worker
|
|
270
|
+
await deleteWorker(cfg.accountId, cfg.apiToken, scriptName);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// --- Scale ---
|
|
274
|
+
|
|
275
|
+
export async function scale(cfg, appName, opts) {
|
|
276
|
+
var appConfig = opts.appConfig;
|
|
277
|
+
|
|
278
|
+
await pushAppConfig(cfg, appName, appConfig);
|
|
279
|
+
|
|
280
|
+
var scriptName = `relight-${appName}`;
|
|
281
|
+
var containerApp = await findContainerApp(cfg.accountId, cfg.apiToken, scriptName);
|
|
282
|
+
if (containerApp) {
|
|
283
|
+
var maxInstances = (appConfig.regions?.length || 1) * (appConfig.instances || 2);
|
|
284
|
+
var modification = { max_instances: maxInstances };
|
|
285
|
+
|
|
286
|
+
if (appConfig.vcpu || appConfig.memory || appConfig.disk) {
|
|
287
|
+
modification.configuration = {};
|
|
288
|
+
if (appConfig.vcpu) modification.configuration.vcpu = appConfig.vcpu;
|
|
289
|
+
if (appConfig.memory) modification.configuration.memory_mib = appConfig.memory;
|
|
290
|
+
if (appConfig.disk) modification.configuration.disk = { size_mb: appConfig.disk };
|
|
291
|
+
} else if (appConfig.instanceType) {
|
|
292
|
+
modification.configuration = { instance_type: appConfig.instanceType };
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
await modifyContainerApp(cfg.accountId, cfg.apiToken, containerApp.id, modification);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// --- Container status ---
|
|
300
|
+
|
|
301
|
+
var containerMetricsGQL = `query($accountTag: string!, $filter: AccountContainersMetricsAdaptiveGroupsFilter_InputObject!) {
|
|
302
|
+
viewer {
|
|
303
|
+
accounts(filter: { accountTag: $accountTag }) {
|
|
304
|
+
containersMetricsAdaptiveGroups(limit: 1000, filter: $filter) {
|
|
305
|
+
dimensions { applicationId region active durableObjectId }
|
|
306
|
+
avg { cpuLoad memory }
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
}`;
|
|
311
|
+
|
|
312
|
+
export async function getContainerStatus(cfg, appName) {
|
|
313
|
+
var scriptName = `relight-${appName}`;
|
|
314
|
+
var containerApp = await findContainerApp(cfg.accountId, cfg.apiToken, scriptName);
|
|
315
|
+
if (!containerApp) return [];
|
|
316
|
+
|
|
317
|
+
var now = new Date();
|
|
318
|
+
var since = new Date(now.getTime() - 15 * 60000);
|
|
319
|
+
try {
|
|
320
|
+
var data = await cfGraphQL(cfg.apiToken, containerMetricsGQL, {
|
|
321
|
+
accountTag: cfg.accountId,
|
|
322
|
+
filter: {
|
|
323
|
+
datetimeFiveMinutes_geq: since.toISOString().slice(0, 19) + "Z",
|
|
324
|
+
datetimeFiveMinutes_leq: now.toISOString().slice(0, 19) + "Z",
|
|
325
|
+
applicationId_in: [containerApp.id],
|
|
326
|
+
},
|
|
327
|
+
});
|
|
328
|
+
return data?.viewer?.accounts?.[0]?.containersMetricsAdaptiveGroups || [];
|
|
329
|
+
} catch {
|
|
330
|
+
return [];
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// --- App URL ---
|
|
335
|
+
|
|
336
|
+
export async function getAppUrl(cfg, appName) {
|
|
337
|
+
var appConfig = await getAppConfig(cfg, appName);
|
|
338
|
+
if (appConfig?.domains?.length > 0) {
|
|
339
|
+
return `https://${appConfig.domains[0]}`;
|
|
340
|
+
}
|
|
341
|
+
var subdomain = await getWorkersSubdomain(cfg.accountId, cfg.apiToken);
|
|
342
|
+
if (!subdomain) return null;
|
|
343
|
+
return `https://relight-${appName}.${subdomain}.workers.dev`;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// --- Log streaming ---
|
|
347
|
+
|
|
348
|
+
export async function streamLogs(cfg, appName) {
|
|
349
|
+
var scriptName = `relight-${appName}`;
|
|
350
|
+
var tail = await createTail(cfg.accountId, cfg.apiToken, scriptName);
|
|
351
|
+
return {
|
|
352
|
+
url: tail.url,
|
|
353
|
+
id: tail.id,
|
|
354
|
+
cleanup: async () => {
|
|
355
|
+
try {
|
|
356
|
+
await deleteTail(cfg.accountId, cfg.apiToken, scriptName, tail.id);
|
|
357
|
+
} catch {}
|
|
358
|
+
},
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// --- Cost analytics ---
|
|
363
|
+
|
|
364
|
+
var workersGQL = `query Workers($accountTag: string!, $filter: WorkersInvocationsAdaptiveFilter_InputObject!) {
|
|
365
|
+
viewer {
|
|
366
|
+
accounts(filter: { accountTag: $accountTag }) {
|
|
367
|
+
workersInvocationsAdaptive(limit: 10000, filter: $filter) {
|
|
368
|
+
dimensions { scriptName }
|
|
369
|
+
sum { requests cpuTimeUs }
|
|
370
|
+
avg { sampleInterval }
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
}`;
|
|
375
|
+
|
|
376
|
+
var doRequestsGQL = `query DORequests($accountTag: string!, $filter: DurableObjectsInvocationsAdaptiveGroupsFilter_InputObject!) {
|
|
377
|
+
viewer {
|
|
378
|
+
accounts(filter: { accountTag: $accountTag }) {
|
|
379
|
+
durableObjectsInvocationsAdaptiveGroups(limit: 10000, filter: $filter) {
|
|
380
|
+
dimensions { namespaceId }
|
|
381
|
+
sum { requests }
|
|
382
|
+
avg { sampleInterval }
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
}`;
|
|
387
|
+
|
|
388
|
+
var doDurationGQL = `query DODuration($accountTag: string!, $filter: DurableObjectsPeriodicGroupsFilter_InputObject!) {
|
|
389
|
+
viewer {
|
|
390
|
+
accounts(filter: { accountTag: $accountTag }) {
|
|
391
|
+
durableObjectsPeriodicGroups(limit: 10000, filter: $filter) {
|
|
392
|
+
dimensions { namespaceId }
|
|
393
|
+
sum { activeTime inboundWebsocketMsgCount }
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
}`;
|
|
398
|
+
|
|
399
|
+
var containersGQL = `query Containers($accountTag: string!, $filter: AccountContainersMetricsAdaptiveGroupsFilter_InputObject!) {
|
|
400
|
+
viewer {
|
|
401
|
+
accounts(filter: { accountTag: $accountTag }) {
|
|
402
|
+
containersMetricsAdaptiveGroups(limit: 10000, filter: $filter) {
|
|
403
|
+
dimensions { applicationId }
|
|
404
|
+
sum { cpuTimeSec allocatedMemory allocatedDisk txBytes }
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
}`;
|
|
409
|
+
|
|
410
|
+
export async function getCosts(cfg, appNames, dateRange) {
|
|
411
|
+
var { sinceISO, untilISO } = dateRange;
|
|
412
|
+
|
|
413
|
+
// Discover all relight scripts and container apps
|
|
414
|
+
var [allScripts, containerApps] = await Promise.all([
|
|
415
|
+
listWorkerScripts(cfg.accountId, cfg.apiToken),
|
|
416
|
+
listContainerApps(cfg.accountId, cfg.apiToken),
|
|
417
|
+
]);
|
|
418
|
+
|
|
419
|
+
var containerAppMap = {};
|
|
420
|
+
for (var ca of containerApps) {
|
|
421
|
+
containerAppMap[ca.name] = ca.id;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
// Build app list with namespace IDs
|
|
425
|
+
var targetNames = appNames || allScripts
|
|
426
|
+
.filter((s) => s.id.startsWith("relight-"))
|
|
427
|
+
.map((s) => s.id.replace(/^relight-/, ""));
|
|
428
|
+
|
|
429
|
+
var apps = [];
|
|
430
|
+
await Promise.all(
|
|
431
|
+
targetNames.map(async (appName) => {
|
|
432
|
+
var scriptName = `relight-${appName}`;
|
|
433
|
+
var [nsId, appConfig] = await Promise.all([
|
|
434
|
+
getDONamespaceId(cfg.accountId, cfg.apiToken, scriptName, "AppContainer"),
|
|
435
|
+
getAppConfig(cfg, appName),
|
|
436
|
+
]);
|
|
437
|
+
apps.push({
|
|
438
|
+
name: appName,
|
|
439
|
+
namespaceId: nsId,
|
|
440
|
+
appConfig,
|
|
441
|
+
containerAppId: containerAppMap[scriptName] || null,
|
|
442
|
+
});
|
|
443
|
+
})
|
|
444
|
+
);
|
|
445
|
+
|
|
446
|
+
apps.sort((a, b) => a.name.localeCompare(b.name));
|
|
447
|
+
|
|
448
|
+
// Fetch all analytics in parallel
|
|
449
|
+
var scriptNames = apps.map((a) => `relight-${a.name}`);
|
|
450
|
+
var namespaceIds = apps.map((a) => a.namespaceId).filter(Boolean);
|
|
451
|
+
var containerAppIds = apps.map((a) => a.containerAppId).filter(Boolean);
|
|
452
|
+
|
|
453
|
+
var queries = [];
|
|
454
|
+
|
|
455
|
+
queries.push(
|
|
456
|
+
cfGraphQL(cfg.apiToken, workersGQL, {
|
|
457
|
+
accountTag: cfg.accountId,
|
|
458
|
+
filter: {
|
|
459
|
+
datetimeHour_geq: sinceISO,
|
|
460
|
+
datetimeHour_leq: untilISO,
|
|
461
|
+
scriptName_in: scriptNames,
|
|
462
|
+
},
|
|
463
|
+
})
|
|
464
|
+
);
|
|
465
|
+
|
|
466
|
+
if (namespaceIds.length > 0) {
|
|
467
|
+
var doFilter = {
|
|
468
|
+
datetimeHour_geq: sinceISO,
|
|
469
|
+
datetimeHour_leq: untilISO,
|
|
470
|
+
namespaceId_in: namespaceIds,
|
|
471
|
+
};
|
|
472
|
+
queries.push(
|
|
473
|
+
cfGraphQL(cfg.apiToken, doRequestsGQL, { accountTag: cfg.accountId, filter: doFilter })
|
|
474
|
+
);
|
|
475
|
+
queries.push(
|
|
476
|
+
cfGraphQL(cfg.apiToken, doDurationGQL, { accountTag: cfg.accountId, filter: doFilter })
|
|
477
|
+
);
|
|
478
|
+
} else {
|
|
479
|
+
queries.push(Promise.resolve(null), Promise.resolve(null));
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
if (containerAppIds.length > 0) {
|
|
483
|
+
queries.push(
|
|
484
|
+
cfGraphQL(cfg.apiToken, containersGQL, {
|
|
485
|
+
accountTag: cfg.accountId,
|
|
486
|
+
filter: {
|
|
487
|
+
datetimeHour_geq: sinceISO,
|
|
488
|
+
datetimeHour_leq: untilISO,
|
|
489
|
+
applicationId_in: containerAppIds,
|
|
490
|
+
},
|
|
491
|
+
})
|
|
492
|
+
);
|
|
493
|
+
} else {
|
|
494
|
+
queries.push(Promise.resolve(null));
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
var [workersData, doReqData, doDurData, containersData] = await Promise.all(queries);
|
|
498
|
+
|
|
499
|
+
// Aggregate per-app usage
|
|
500
|
+
return aggregateResults(apps, { workersData, doReqData, doDurData, containersData });
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
function aggregateResults(apps, analytics) {
|
|
504
|
+
var { workersData, doReqData, doDurData, containersData } = analytics;
|
|
505
|
+
|
|
506
|
+
var workerRows =
|
|
507
|
+
workersData?.viewer?.accounts?.[0]?.workersInvocationsAdaptive || [];
|
|
508
|
+
var workersByScript = {};
|
|
509
|
+
for (var row of workerRows) {
|
|
510
|
+
var sn = row.dimensions.scriptName;
|
|
511
|
+
var si = row.avg?.sampleInterval || 1;
|
|
512
|
+
if (!workersByScript[sn]) workersByScript[sn] = { requests: 0, cpuMs: 0 };
|
|
513
|
+
workersByScript[sn].requests += (row.sum?.requests || 0) * si;
|
|
514
|
+
workersByScript[sn].cpuMs += ((row.sum?.cpuTimeUs || 0) / 1000) * si;
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
var doReqRows =
|
|
518
|
+
doReqData?.viewer?.accounts?.[0]?.durableObjectsInvocationsAdaptiveGroups || [];
|
|
519
|
+
var doReqByNs = {};
|
|
520
|
+
for (var row of doReqRows) {
|
|
521
|
+
var ns = row.dimensions.namespaceId;
|
|
522
|
+
var si = row.avg?.sampleInterval || 1;
|
|
523
|
+
if (!doReqByNs[ns]) doReqByNs[ns] = 0;
|
|
524
|
+
doReqByNs[ns] += (row.sum?.requests || 0) * si;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
var doDurRows =
|
|
528
|
+
doDurData?.viewer?.accounts?.[0]?.durableObjectsPeriodicGroups || [];
|
|
529
|
+
var doDurByNs = {};
|
|
530
|
+
for (var row of doDurRows) {
|
|
531
|
+
var ns = row.dimensions.namespaceId;
|
|
532
|
+
if (!doDurByNs[ns]) doDurByNs[ns] = { activeTime: 0, wsInbound: 0 };
|
|
533
|
+
doDurByNs[ns].activeTime += row.sum?.activeTime || 0;
|
|
534
|
+
doDurByNs[ns].wsInbound += row.sum?.inboundWebsocketMsgCount || 0;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
var containerRows =
|
|
538
|
+
containersData?.viewer?.accounts?.[0]?.containersMetricsAdaptiveGroups || [];
|
|
539
|
+
var containersByAppId = {};
|
|
540
|
+
for (var row of containerRows) {
|
|
541
|
+
var appId = row.dimensions.applicationId;
|
|
542
|
+
if (!containersByAppId[appId]) {
|
|
543
|
+
containersByAppId[appId] = { cpuTimeSec: 0, allocatedMemory: 0, allocatedDisk: 0, txBytes: 0 };
|
|
544
|
+
}
|
|
545
|
+
containersByAppId[appId].cpuTimeSec += row.sum?.cpuTimeSec || 0;
|
|
546
|
+
containersByAppId[appId].allocatedMemory += row.sum?.allocatedMemory || 0;
|
|
547
|
+
containersByAppId[appId].allocatedDisk += row.sum?.allocatedDisk || 0;
|
|
548
|
+
containersByAppId[appId].txBytes += row.sum?.txBytes || 0;
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
return apps.map((app) => {
|
|
552
|
+
var scriptName = `relight-${app.name}`;
|
|
553
|
+
var w = workersByScript[scriptName] || { requests: 0, cpuMs: 0 };
|
|
554
|
+
var nsId = app.namespaceId;
|
|
555
|
+
|
|
556
|
+
var doDuration = nsId ? doDurByNs[nsId] || {} : {};
|
|
557
|
+
var doRequests = (nsId ? doReqByNs[nsId] || 0 : 0) + (doDuration.wsInbound || 0) / 20;
|
|
558
|
+
|
|
559
|
+
var c = app.containerAppId ? containersByAppId[app.containerAppId] || {} : {};
|
|
560
|
+
var containerVcpuSec = c.cpuTimeSec || 0;
|
|
561
|
+
var containerMemGibSec = (c.allocatedMemory || 0) / (1024 * 1024 * 1024);
|
|
562
|
+
var containerDiskGbSec = (c.allocatedDisk || 0) / 1_000_000_000;
|
|
563
|
+
var containerEgressGb = (c.txBytes || 0) / 1_000_000_000;
|
|
564
|
+
|
|
565
|
+
return {
|
|
566
|
+
name: app.name,
|
|
567
|
+
usage: {
|
|
568
|
+
workerRequests: Math.round(w.requests),
|
|
569
|
+
workerCpuMs: Math.round(w.cpuMs),
|
|
570
|
+
doRequests: Math.round(doRequests),
|
|
571
|
+
doWsMsgs: Math.round(doDuration.wsInbound || 0),
|
|
572
|
+
doGbSeconds: Math.round(((doDuration.activeTime || 0) / 1_000_000) * 128 / 1024),
|
|
573
|
+
containerVcpuSec,
|
|
574
|
+
containerMemGibSec,
|
|
575
|
+
containerDiskGbSec,
|
|
576
|
+
containerEgressGb,
|
|
577
|
+
},
|
|
578
|
+
};
|
|
579
|
+
});
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
// --- Regions ---
|
|
583
|
+
|
|
584
|
+
export function getRegions() {
|
|
585
|
+
return [
|
|
586
|
+
{ code: "wnam", name: "Western North America", location: "Los Angeles, Seattle, San Francisco" },
|
|
587
|
+
{ code: "enam", name: "Eastern North America", location: "New York, Chicago, Toronto" },
|
|
588
|
+
{ code: "sam", name: "South America", location: "Sao Paulo, Buenos Aires" },
|
|
589
|
+
{ code: "weur", name: "Western Europe", location: "London, Paris, Amsterdam, Frankfurt" },
|
|
590
|
+
{ code: "eeur", name: "Eastern Europe", location: "Warsaw, Helsinki, Bucharest" },
|
|
591
|
+
{ code: "apac", name: "Asia Pacific", location: "Tokyo, Singapore, Hong Kong, Mumbai" },
|
|
592
|
+
{ code: "oc", name: "Oceania", location: "Sydney, Auckland" },
|
|
593
|
+
{ code: "afr", name: "Africa", location: "Johannesburg, Nairobi" },
|
|
594
|
+
{ code: "me", name: "Middle East", location: "Dubai, Bahrain" },
|
|
595
|
+
];
|
|
596
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { buildSync } from "esbuild";
|
|
2
|
+
import { join, dirname } from "path";
|
|
3
|
+
import {
|
|
4
|
+
readFileSync,
|
|
5
|
+
writeFileSync,
|
|
6
|
+
existsSync,
|
|
7
|
+
mkdirSync,
|
|
8
|
+
cpSync,
|
|
9
|
+
} from "fs";
|
|
10
|
+
import { execSync } from "child_process";
|
|
11
|
+
import { createHash } from "crypto";
|
|
12
|
+
import { homedir } from "os";
|
|
13
|
+
import { fileURLToPath } from "url";
|
|
14
|
+
|
|
15
|
+
var __dirname = dirname(fileURLToPath(import.meta.url));
|
|
16
|
+
var templateDir = join(__dirname, "..", "..", "..", "worker-template");
|
|
17
|
+
var cacheDir = join(homedir(), ".relight");
|
|
18
|
+
var bundlePath = join(cacheDir, "worker-bundle.js");
|
|
19
|
+
var hashPath = join(cacheDir, "worker-bundle.hash");
|
|
20
|
+
|
|
21
|
+
export function templateHash() {
|
|
22
|
+
var source = readFileSync(join(templateDir, "src", "index.js"), "utf-8");
|
|
23
|
+
var pkg = readFileSync(join(templateDir, "package.json"), "utf-8");
|
|
24
|
+
return createHash("sha256").update(source + pkg).digest("hex").slice(0, 16);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function getWorkerBundle() {
|
|
28
|
+
var hash = templateHash();
|
|
29
|
+
|
|
30
|
+
// Cache hit
|
|
31
|
+
if (existsSync(bundlePath) && existsSync(hashPath)) {
|
|
32
|
+
var cached = readFileSync(hashPath, "utf-8").trim();
|
|
33
|
+
if (cached === hash) {
|
|
34
|
+
return readFileSync(bundlePath, "utf-8");
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
console.log("Bundling worker template (first deploy, cached after this)...");
|
|
39
|
+
|
|
40
|
+
// Build in a temp work dir
|
|
41
|
+
var workDir = join(cacheDir, "bundle-work");
|
|
42
|
+
mkdirSync(join(workDir, "src"), { recursive: true });
|
|
43
|
+
cpSync(join(templateDir, "src", "index.js"), join(workDir, "src", "index.js"));
|
|
44
|
+
cpSync(join(templateDir, "package.json"), join(workDir, "package.json"));
|
|
45
|
+
|
|
46
|
+
if (!existsSync(join(workDir, "node_modules", "@cloudflare", "containers"))) {
|
|
47
|
+
execSync("npm install --production", { cwd: workDir, stdio: "inherit" });
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
var result = buildSync({
|
|
51
|
+
entryPoints: [join(workDir, "src", "index.js")],
|
|
52
|
+
bundle: true,
|
|
53
|
+
format: "esm",
|
|
54
|
+
platform: "neutral",
|
|
55
|
+
target: "es2022",
|
|
56
|
+
outfile: bundlePath,
|
|
57
|
+
external: ["cloudflare:*", "node:*"],
|
|
58
|
+
write: true,
|
|
59
|
+
minify: false,
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
if (result.errors.length > 0) {
|
|
63
|
+
throw new Error(
|
|
64
|
+
"Bundle failed: " + result.errors.map((e) => e.text).join("\n")
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
writeFileSync(hashPath, hash);
|
|
69
|
+
return readFileSync(bundlePath, "utf-8");
|
|
70
|
+
}
|