@tokenbuddy/tb-admin 1.0.14 → 1.0.15
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/dist/src/bootstrap-registry.d.ts +1 -0
- package/dist/src/bootstrap-registry.d.ts.map +1 -1
- package/dist/src/bootstrap-registry.js.map +1 -1
- package/dist/src/cli.d.ts.map +1 -1
- package/dist/src/cli.js +8 -0
- package/dist/src/cli.js.map +1 -1
- package/dist/src/server-cmd.d.ts +22 -1
- package/dist/src/server-cmd.d.ts.map +1 -1
- package/dist/src/server-cmd.js +93 -16
- package/dist/src/server-cmd.js.map +1 -1
- package/dist/src/ui-actions.d.ts +88 -0
- package/dist/src/ui-actions.d.ts.map +1 -0
- package/dist/src/ui-actions.js +763 -0
- package/dist/src/ui-actions.js.map +1 -0
- package/dist/src/ui-command.d.ts +4 -0
- package/dist/src/ui-command.d.ts.map +1 -0
- package/dist/src/ui-command.js +37 -0
- package/dist/src/ui-command.js.map +1 -0
- package/dist/src/ui-server.d.ts +23 -0
- package/dist/src/ui-server.d.ts.map +1 -0
- package/dist/src/ui-server.js +245 -0
- package/dist/src/ui-server.js.map +1 -0
- package/dist/src/ui-state.d.ts +134 -0
- package/dist/src/ui-state.d.ts.map +1 -0
- package/dist/src/ui-state.js +407 -0
- package/dist/src/ui-state.js.map +1 -0
- package/dist/src/ui-static.d.ts +2 -0
- package/dist/src/ui-static.d.ts.map +1 -0
- package/dist/src/ui-static.js +144 -0
- package/dist/src/ui-static.js.map +1 -0
- package/dist/src/upstream-balance-probe.d.ts +41 -0
- package/dist/src/upstream-balance-probe.d.ts.map +1 -0
- package/dist/src/upstream-balance-probe.js +379 -0
- package/dist/src/upstream-balance-probe.js.map +1 -0
- package/package.json +1 -1
- package/src/bootstrap-registry.ts +1 -0
- package/src/cli.ts +9 -0
- package/src/server-cmd.ts +118 -19
- package/src/ui-actions.ts +901 -0
- package/src/ui-command.ts +39 -0
- package/src/ui-server.ts +308 -0
- package/src/ui-state.ts +575 -0
- package/src/ui-static.ts +144 -0
- package/src/upstream-balance-probe.ts +505 -0
- package/tests/admin.test.ts +871 -1
package/src/server-cmd.ts
CHANGED
|
@@ -1,6 +1,25 @@
|
|
|
1
|
-
import { execSync } from "child_process";
|
|
1
|
+
import { execSync, spawnSync, type SpawnSyncReturns } from "child_process";
|
|
2
|
+
import * as fs from "fs";
|
|
2
3
|
import { SellerProviderConfig } from "./config.js";
|
|
3
4
|
|
|
5
|
+
type ExecRunner = (command: string, options?: Parameters<typeof execSync>[1]) => string | Buffer;
|
|
6
|
+
type SpawnRunner = (command: string, args?: string[], options?: Parameters<typeof spawnSync>[2]) => SpawnSyncReturns<string | Buffer>;
|
|
7
|
+
|
|
8
|
+
export interface DockerImageInspection {
|
|
9
|
+
ok: boolean;
|
|
10
|
+
error?: string;
|
|
11
|
+
exitCode?: number;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export type ImageInspectRunner = (image: string) => DockerImageInspection;
|
|
15
|
+
|
|
16
|
+
export interface FlyProviderRuntime {
|
|
17
|
+
checkFlyctlInstalled?: (flyctlPath?: string) => boolean;
|
|
18
|
+
execSync?: ExecRunner;
|
|
19
|
+
spawnSync?: SpawnRunner;
|
|
20
|
+
imageInspector?: ImageInspectRunner;
|
|
21
|
+
}
|
|
22
|
+
|
|
4
23
|
/**
|
|
5
24
|
* 检查 flyctl 是否在 PATH 中(或在指定路径)。
|
|
6
25
|
*
|
|
@@ -16,6 +35,41 @@ export function checkFlyctlInstalled(flyctlPath?: string): boolean {
|
|
|
16
35
|
}
|
|
17
36
|
}
|
|
18
37
|
|
|
38
|
+
export function inspectDockerImage(image: string): DockerImageInspection {
|
|
39
|
+
const result = spawnSync("docker", ["buildx", "imagetools", "inspect", image], {
|
|
40
|
+
encoding: "utf8",
|
|
41
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
42
|
+
});
|
|
43
|
+
return dockerImageInspectionFromResult(result);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function requirePublishedDockerImage(image: string, inspectImage: ImageInspectRunner = inspectDockerImage): void {
|
|
47
|
+
const inspection = inspectImage(image);
|
|
48
|
+
if (inspection.ok) {
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
const detail = inspection.error ? ` Detail: ${inspection.error.trim()}` : "";
|
|
52
|
+
throw new Error(
|
|
53
|
+
`seller image is not published or is not accessible: ${image}. ` +
|
|
54
|
+
`Publish it first with RELEASE_VERSION=<v> bash scripts/release/all.sh, or pass an existing registry.fly.io/tb-seller:<v> tag. ` +
|
|
55
|
+
`No Fly app was created.${detail}`
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function dockerImageInspectionFromResult(result: SpawnSyncReturns<string>): DockerImageInspection {
|
|
60
|
+
if (result.error) {
|
|
61
|
+
return { ok: false, error: result.error.message };
|
|
62
|
+
}
|
|
63
|
+
if (result.status !== 0) {
|
|
64
|
+
return {
|
|
65
|
+
ok: false,
|
|
66
|
+
exitCode: result.status ?? undefined,
|
|
67
|
+
error: (result.stderr || result.stdout || "").toString()
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
return { ok: true };
|
|
71
|
+
}
|
|
72
|
+
|
|
19
73
|
export function parseFlyMachineIds(json: string, app: string): string[] {
|
|
20
74
|
let parsed: unknown;
|
|
21
75
|
try {
|
|
@@ -60,6 +114,7 @@ export interface SellerCreateOptions {
|
|
|
60
114
|
volumeSizeGb?: number;
|
|
61
115
|
volumeId?: string;
|
|
62
116
|
volumeSnapshotRetentionDays?: number;
|
|
117
|
+
initialConfigPath?: string;
|
|
63
118
|
dryRun?: boolean;
|
|
64
119
|
}
|
|
65
120
|
|
|
@@ -69,9 +124,17 @@ export interface SellerCreateOptions {
|
|
|
69
124
|
*/
|
|
70
125
|
export class FlyProvider {
|
|
71
126
|
private providerConfig?: SellerProviderConfig;
|
|
127
|
+
private readonly runtime: Required<FlyProviderRuntime>;
|
|
72
128
|
|
|
73
|
-
constructor(providerConfig?: SellerProviderConfig) {
|
|
129
|
+
constructor(providerConfig?: SellerProviderConfig, runtime?: FlyProviderRuntime) {
|
|
74
130
|
this.providerConfig = providerConfig;
|
|
131
|
+
this.runtime = {
|
|
132
|
+
checkFlyctlInstalled,
|
|
133
|
+
execSync,
|
|
134
|
+
spawnSync,
|
|
135
|
+
imageInspector: inspectDockerImage,
|
|
136
|
+
...runtime
|
|
137
|
+
};
|
|
75
138
|
}
|
|
76
139
|
|
|
77
140
|
private get flyctl(): string {
|
|
@@ -82,10 +145,10 @@ export class FlyProvider {
|
|
|
82
145
|
* List apps on Fly.io
|
|
83
146
|
*/
|
|
84
147
|
public listApps(): string {
|
|
85
|
-
if (!checkFlyctlInstalled(this.flyctl)) {
|
|
148
|
+
if (!this.runtime.checkFlyctlInstalled(this.flyctl)) {
|
|
86
149
|
throw new Error(`\`${this.flyctl}\` is not installed on your system PATH.`);
|
|
87
150
|
}
|
|
88
|
-
return execSync(`${this.flyctl} apps list`, { encoding: "utf8" });
|
|
151
|
+
return this.runtime.execSync(`${this.flyctl} apps list`, { encoding: "utf8" }) as string;
|
|
89
152
|
}
|
|
90
153
|
|
|
91
154
|
/**
|
|
@@ -112,6 +175,7 @@ export class FlyProvider {
|
|
|
112
175
|
|| 1;
|
|
113
176
|
const volumeSnapshotRetentionDays = options.volumeSnapshotRetentionDays;
|
|
114
177
|
const volumeId = options.volumeId;
|
|
178
|
+
const initialConfigPath = options.initialConfigPath;
|
|
115
179
|
|
|
116
180
|
if (!targetImage) {
|
|
117
181
|
throw new Error("seller create requires --image registry.fly.io/tb-seller:<v>");
|
|
@@ -129,35 +193,59 @@ export class FlyProvider {
|
|
|
129
193
|
` Volume: ${volumeName} (${volumeSizeGb}GB)`,
|
|
130
194
|
];
|
|
131
195
|
if (flyConfig) lines.push(` Fly config: ${flyConfig}`);
|
|
196
|
+
if (initialConfigPath) lines.push(` Initial config secret: TOKENBUDDY_SELLER_CONFIG_B64 from ${initialConfigPath}`);
|
|
132
197
|
if (volumeId) lines.push(` Volume ID: ${volumeId}`);
|
|
133
198
|
if (volumeSnapshotRetentionDays !== undefined) lines.push(` Volume snapshot retention: ${volumeSnapshotRetentionDays} days`);
|
|
134
199
|
return lines.join("\n");
|
|
135
200
|
}
|
|
136
201
|
|
|
137
|
-
if (!checkFlyctlInstalled(this.flyctl)) {
|
|
202
|
+
if (!this.runtime.checkFlyctlInstalled(this.flyctl)) {
|
|
138
203
|
throw new Error(`\`${this.flyctl}\` is not installed on PATH.`);
|
|
139
204
|
}
|
|
140
205
|
|
|
141
206
|
if (!operatorSecret) {
|
|
142
207
|
throw new Error("operator_secret is required. Provide --operator-secret or configure seller_providers.fly.operator_secret");
|
|
143
208
|
}
|
|
209
|
+
requireReadableFile(flyConfig, "Fly config");
|
|
210
|
+
if (initialConfigPath) {
|
|
211
|
+
requireReadableFile(initialConfigPath, "Initial seller config");
|
|
212
|
+
}
|
|
213
|
+
requirePublishedDockerImage(targetImage, this.runtime.imageInspector);
|
|
144
214
|
|
|
145
215
|
console.log(`[Fly.io] Creating app ${appName}...`);
|
|
146
|
-
execSync(`${this.flyctl} apps create ${appName} --machines`, { stdio: "inherit" });
|
|
216
|
+
this.runtime.execSync(`${this.flyctl} apps create ${appName} --machines`, { stdio: "inherit" });
|
|
147
217
|
|
|
148
218
|
console.log(`[Fly.io] Setting secrets...`);
|
|
149
|
-
|
|
150
|
-
`${this.flyctl} secrets set ALLOW_MOCK=false OPERATOR_SECRET=${operatorSecret} --app ${appName}`,
|
|
151
|
-
{ stdio: "inherit" }
|
|
152
|
-
);
|
|
219
|
+
this.importCreateSecrets(appName, operatorSecret, initialConfigPath);
|
|
153
220
|
|
|
154
221
|
console.log(`[Fly.io] Deploying image ${targetImage}...`);
|
|
155
|
-
const deployCmd = `${this.flyctl} deploy -c ${flyConfig} --image ${targetImage} --region ${targetRegion} --app ${appName} --now`;
|
|
156
|
-
execSync(deployCmd, { stdio: "inherit" });
|
|
222
|
+
const deployCmd = `${this.flyctl} deploy -c ${flyConfig} --image ${targetImage} --primary-region ${targetRegion} --app ${appName} --now`;
|
|
223
|
+
this.runtime.execSync(deployCmd, { stdio: "inherit" });
|
|
157
224
|
|
|
158
225
|
return `Successfully deployed ${appName} on Fly.io`;
|
|
159
226
|
}
|
|
160
227
|
|
|
228
|
+
private importCreateSecrets(appName: string, operatorSecret: string, initialConfigPath: string | undefined): void {
|
|
229
|
+
const lines = [
|
|
230
|
+
"ALLOW_MOCK=false",
|
|
231
|
+
`OPERATOR_SECRET=${operatorSecret}`
|
|
232
|
+
];
|
|
233
|
+
if (initialConfigPath) {
|
|
234
|
+
const configContent = fs.readFileSync(initialConfigPath, "utf8");
|
|
235
|
+
lines.push(`TOKENBUDDY_SELLER_CONFIG_B64=${Buffer.from(configContent, "utf8").toString("base64")}`);
|
|
236
|
+
}
|
|
237
|
+
const result = this.runtime.spawnSync(this.flyctl, ["secrets", "import", "--stage", "--app", appName], {
|
|
238
|
+
input: `${lines.join("\n")}\n`,
|
|
239
|
+
stdio: ["pipe", "inherit", "inherit"]
|
|
240
|
+
});
|
|
241
|
+
if (result.error) {
|
|
242
|
+
throw result.error;
|
|
243
|
+
}
|
|
244
|
+
if (result.status !== 0) {
|
|
245
|
+
throw new Error(`flyctl secrets import failed with exit code ${result.status}`);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
161
249
|
/**
|
|
162
250
|
* Destroy a seller app on Fly.io.
|
|
163
251
|
* @param nameOrApp Either a bare name (e.g. "86d81e") or a full app name (e.g. "tbs-86d81e").
|
|
@@ -170,12 +258,12 @@ export class FlyProvider {
|
|
|
170
258
|
return `[DRY-RUN] Will destroy fly app: ${appName}`;
|
|
171
259
|
}
|
|
172
260
|
|
|
173
|
-
if (!checkFlyctlInstalled(this.flyctl)) {
|
|
261
|
+
if (!this.runtime.checkFlyctlInstalled(this.flyctl)) {
|
|
174
262
|
throw new Error(`\`${this.flyctl}\` is not installed.`);
|
|
175
263
|
}
|
|
176
264
|
|
|
177
265
|
console.log(`[Fly.io] Destroying app ${appName}...`);
|
|
178
|
-
execSync(`${this.flyctl} apps destroy ${appName} --yes`, { stdio: "inherit" });
|
|
266
|
+
this.runtime.execSync(`${this.flyctl} apps destroy ${appName} --yes`, { stdio: "inherit" });
|
|
179
267
|
|
|
180
268
|
return `Successfully destroyed ${appName} on Fly.io`;
|
|
181
269
|
}
|
|
@@ -185,10 +273,10 @@ export class FlyProvider {
|
|
|
185
273
|
*/
|
|
186
274
|
public statusApp(name: string): string {
|
|
187
275
|
const appName = name.includes("-") ? name : `tb-seller-${name}`;
|
|
188
|
-
if (!checkFlyctlInstalled(this.flyctl)) {
|
|
276
|
+
if (!this.runtime.checkFlyctlInstalled(this.flyctl)) {
|
|
189
277
|
throw new Error(`\`${this.flyctl}\` is not installed.`);
|
|
190
278
|
}
|
|
191
|
-
return execSync(`${this.flyctl} status --app ${appName}`, { encoding: "utf8" });
|
|
279
|
+
return this.runtime.execSync(`${this.flyctl} status --app ${appName}`, { encoding: "utf8" }) as string;
|
|
192
280
|
}
|
|
193
281
|
|
|
194
282
|
/**
|
|
@@ -218,16 +306,16 @@ export class FlyProvider {
|
|
|
218
306
|
return lines.join("\n");
|
|
219
307
|
}
|
|
220
308
|
|
|
221
|
-
if (!checkFlyctlInstalled(this.flyctl)) {
|
|
309
|
+
if (!this.runtime.checkFlyctlInstalled(this.flyctl)) {
|
|
222
310
|
throw new Error(`\`${this.flyctl}\` is not installed.`);
|
|
223
311
|
}
|
|
224
312
|
|
|
225
|
-
const machinesJson = execSync(`${this.flyctl} machines list --app ${app} --json`, { encoding: "utf8" });
|
|
313
|
+
const machinesJson = this.runtime.execSync(`${this.flyctl} machines list --app ${app} --json`, { encoding: "utf8" }) as string;
|
|
226
314
|
const machineIds = parseFlyMachineIds(machinesJson, app);
|
|
227
315
|
|
|
228
316
|
console.log(`[Fly.io] Updating ${app} image on ${machineIds.length} machine(s)...`);
|
|
229
317
|
for (const machineId of machineIds) {
|
|
230
|
-
execSync(
|
|
318
|
+
this.runtime.execSync(
|
|
231
319
|
`${this.flyctl} machine update ${machineId} --app ${app} --image ${targetImage} --yes`,
|
|
232
320
|
{ stdio: "inherit" }
|
|
233
321
|
);
|
|
@@ -235,3 +323,14 @@ export class FlyProvider {
|
|
|
235
323
|
return `Successfully updated ${app} image`;
|
|
236
324
|
}
|
|
237
325
|
}
|
|
326
|
+
|
|
327
|
+
function requireReadableFile(filePath: string, label: string): void {
|
|
328
|
+
if (!fs.existsSync(filePath)) {
|
|
329
|
+
throw new Error(`${label} file does not exist: ${filePath}`);
|
|
330
|
+
}
|
|
331
|
+
const stat = fs.statSync(filePath);
|
|
332
|
+
if (!stat.isFile()) {
|
|
333
|
+
throw new Error(`${label} path is not a file: ${filePath}`);
|
|
334
|
+
}
|
|
335
|
+
fs.accessSync(filePath, fs.constants.R_OK);
|
|
336
|
+
}
|