@tokenbuddy/tb-admin 1.0.36 → 1.0.38
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/cli.js +98 -25
- package/dist/src/config.d.ts +8 -2
- package/dist/src/config.js +17 -5
- package/dist/src/display-format.js +6 -14
- package/dist/src/init-command.d.ts +50 -0
- package/dist/src/init-command.js +347 -0
- package/dist/src/providers/fly-io.d.ts +3 -0
- package/dist/src/providers/fly-io.js +137 -0
- package/dist/src/providers/provider-definition.d.ts +38 -0
- package/dist/src/providers/provider-definition.js +2 -0
- package/dist/src/seller.d.ts +2 -0
- package/dist/src/seller.js +30 -13
- package/dist/src/server-cmd.d.ts +1 -0
- package/dist/src/server-cmd.js +9 -2
- package/dist/src/ui-actions.d.ts +3 -0
- package/dist/src/ui-actions.js +199 -27
- package/dist/src/ui-command.js +3 -2
- package/dist/src/ui-state.d.ts +1 -3
- package/dist/src/ui-state.js +4 -8
- package/dist/src/ui-static.js +43 -15
- package/dist/src/workdir.d.ts +21 -0
- package/dist/src/workdir.js +50 -0
- package/package.json +9 -3
- package/templates/providers/fly.io/admin.toml.example +18 -0
- package/templates/providers/fly.io/deploy-secrets/bootstrap/README.md +18 -0
- package/templates/providers/fly.io/deploy-secrets/bootstrap/admin-web.example.env +3 -0
- package/templates/providers/fly.io/deploy-secrets/bootstrap/cloudflare-r2.example.env +6 -0
- package/templates/providers/fly.io/deploy-secrets/bootstrap/registry-signing-key.example.json +6 -0
- package/templates/providers/fly.io/deploy-secrets/bootstrap/registry.example.json +14 -0
- package/templates/providers/fly.io/deploy-secrets/bootstrap/tb-registry.example.yaml +14 -0
- package/templates/providers/fly.io/deploy-secrets/seller-configs/README.md +13 -0
- package/templates/providers/fly.io/deploy-secrets/seller-configs/seller.example.yaml +35 -0
- package/templates/providers/fly.io/env/deploy.env.example +12 -0
- package/templates/providers/fly.io/fly/fly.tb-registry.toml +31 -0
- package/templates/providers/fly.io/fly/fly.tb-seller.toml +25 -0
- package/templates/providers/fly.io/provider.toml.example +10 -0
- package/dist/src/bootstrap-registry.d.ts.map +0 -1
- package/dist/src/bootstrap-registry.js.map +0 -1
- package/dist/src/cli.d.ts.map +0 -1
- package/dist/src/cli.js.map +0 -1
- package/dist/src/client.d.ts.map +0 -1
- package/dist/src/client.js.map +0 -1
- package/dist/src/config.d.ts.map +0 -1
- package/dist/src/config.js.map +0 -1
- package/dist/src/display-format.d.ts.map +0 -1
- package/dist/src/display-format.js.map +0 -1
- package/dist/src/index.d.ts.map +0 -1
- package/dist/src/index.js.map +0 -1
- package/dist/src/provider.d.ts.map +0 -1
- package/dist/src/provider.js.map +0 -1
- package/dist/src/seller.d.ts.map +0 -1
- package/dist/src/seller.js.map +0 -1
- package/dist/src/server-cmd.d.ts.map +0 -1
- package/dist/src/server-cmd.js.map +0 -1
- package/dist/src/ui-actions.d.ts.map +0 -1
- package/dist/src/ui-actions.js.map +0 -1
- package/dist/src/ui-command.d.ts.map +0 -1
- package/dist/src/ui-command.js.map +0 -1
- package/dist/src/ui-server.d.ts.map +0 -1
- package/dist/src/ui-server.js.map +0 -1
- package/dist/src/ui-state.d.ts.map +0 -1
- package/dist/src/ui-state.js.map +0 -1
- package/dist/src/ui-static.d.ts.map +0 -1
- package/dist/src/ui-static.js.map +0 -1
- package/dist/src/upstream-balance-probe.d.ts.map +0 -1
- package/dist/src/upstream-balance-probe.js.map +0 -1
- package/dist/src/vendor-client.d.ts.map +0 -1
- package/dist/src/vendor-client.js.map +0 -1
- package/dist/src/vendor-commands.d.ts.map +0 -1
- package/dist/src/vendor-commands.js.map +0 -1
- package/src/bootstrap-registry.ts +0 -90
- package/src/cli.ts +0 -1614
- package/src/client.ts +0 -179
- package/src/config.ts +0 -194
- package/src/display-format.ts +0 -411
- package/src/index.ts +0 -11
- package/src/provider.ts +0 -150
- package/src/seller.ts +0 -538
- package/src/server-cmd.ts +0 -362
- package/src/ui-actions.ts +0 -1040
- package/src/ui-command.ts +0 -44
- package/src/ui-server.ts +0 -353
- package/src/ui-state.ts +0 -1318
- package/src/ui-static.ts +0 -673
- package/src/upstream-balance-probe.ts +0 -13
- package/src/vendor-client.ts +0 -23
- package/src/vendor-commands.ts +0 -65
- package/tests/admin.test.ts +0 -2162
- package/tests/seller.test.ts +0 -388
- package/tests/ui-state-fleet.test.ts +0 -526
- package/tests/ui-static-row.test.ts +0 -467
- package/tests/vendor-cli.test.ts +0 -241
- package/tsconfig.json +0 -8
package/src/server-cmd.ts
DELETED
|
@@ -1,362 +0,0 @@
|
|
|
1
|
-
import { execSync, spawnSync, type SpawnSyncReturns } from "child_process";
|
|
2
|
-
import * as fs from "fs";
|
|
3
|
-
import { SellerProviderConfig } from "./config.js";
|
|
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
|
-
|
|
23
|
-
/**
|
|
24
|
-
* 检查 flyctl 是否在 PATH 中(或在指定路径)。
|
|
25
|
-
*
|
|
26
|
-
* @param flyctlPath 可选 flyctl 完整路径
|
|
27
|
-
* @returns 是否可用
|
|
28
|
-
*/
|
|
29
|
-
export function checkFlyctlInstalled(flyctlPath?: string): boolean {
|
|
30
|
-
try {
|
|
31
|
-
execSync(`which ${flyctlPath || "flyctl"}`, { stdio: "ignore" });
|
|
32
|
-
return true;
|
|
33
|
-
} catch {
|
|
34
|
-
return false;
|
|
35
|
-
}
|
|
36
|
-
}
|
|
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
|
-
|
|
73
|
-
export function parseFlyMachineIds(json: string, app: string): string[] {
|
|
74
|
-
let parsed: unknown;
|
|
75
|
-
try {
|
|
76
|
-
parsed = JSON.parse(json || "[]");
|
|
77
|
-
} catch {
|
|
78
|
-
throw new Error(`fly machines list returned invalid JSON for ${app}`);
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
if (!Array.isArray(parsed)) {
|
|
82
|
-
throw new Error(`fly machines list returned an unexpected shape for ${app}`);
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
const ids = parsed
|
|
86
|
-
.map((item) => {
|
|
87
|
-
if (item && typeof item === "object" && "id" in item) {
|
|
88
|
-
const id = (item as { id?: unknown }).id;
|
|
89
|
-
return typeof id === "string" && id.length > 0 ? id : undefined;
|
|
90
|
-
}
|
|
91
|
-
return undefined;
|
|
92
|
-
})
|
|
93
|
-
.filter((id): id is string => Boolean(id));
|
|
94
|
-
|
|
95
|
-
if (ids.length === 0) {
|
|
96
|
-
throw new Error(`fly app ${app} has no machines to update`);
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
return ids;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
/**
|
|
103
|
-
* `FlyProvider.createSeller` 的输入。
|
|
104
|
-
* 字段未提供时回退到 `SellerProviderConfig` 的默认值。
|
|
105
|
-
*/
|
|
106
|
-
export interface SellerCreateOptions {
|
|
107
|
-
name: string;
|
|
108
|
-
app?: string;
|
|
109
|
-
region?: string;
|
|
110
|
-
image?: string;
|
|
111
|
-
operatorSecret: string;
|
|
112
|
-
flyConfig?: string;
|
|
113
|
-
volumeName?: string;
|
|
114
|
-
volumeSizeGb?: number;
|
|
115
|
-
volumeId?: string;
|
|
116
|
-
volumeSnapshotRetentionDays?: number;
|
|
117
|
-
initialConfigPath?: string;
|
|
118
|
-
dryRun?: boolean;
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
/**
|
|
122
|
-
* Fly.io seller provider:把 admin CLI 的 `tb-admin server create` 命令转成 `flyctl` 调用。
|
|
123
|
-
* 支持 `dryRun` 模式:只打印会执行的命令而不实际修改 Fly 资源。
|
|
124
|
-
*/
|
|
125
|
-
export class FlyProvider {
|
|
126
|
-
private providerConfig?: SellerProviderConfig;
|
|
127
|
-
private readonly runtime: Required<FlyProviderRuntime>;
|
|
128
|
-
|
|
129
|
-
constructor(providerConfig?: SellerProviderConfig, runtime?: FlyProviderRuntime) {
|
|
130
|
-
this.providerConfig = providerConfig;
|
|
131
|
-
this.runtime = {
|
|
132
|
-
checkFlyctlInstalled,
|
|
133
|
-
execSync,
|
|
134
|
-
spawnSync,
|
|
135
|
-
imageInspector: inspectDockerImage,
|
|
136
|
-
...runtime
|
|
137
|
-
};
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
private get flyctl(): string {
|
|
141
|
-
return this.providerConfig?.flyctl_path || "flyctl";
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
private flyExecOptions(options: Parameters<typeof execSync>[1] = {}): Parameters<typeof execSync>[1] {
|
|
145
|
-
return {
|
|
146
|
-
...options,
|
|
147
|
-
env: this.flyEnv(options.env)
|
|
148
|
-
};
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
private flySpawnOptions(options: Parameters<typeof spawnSync>[2] = {}): Parameters<typeof spawnSync>[2] {
|
|
152
|
-
return {
|
|
153
|
-
...options,
|
|
154
|
-
env: this.flyEnv(options.env)
|
|
155
|
-
};
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
private flyEnv(env: NodeJS.ProcessEnv | undefined): NodeJS.ProcessEnv {
|
|
159
|
-
const configuredToken = this.providerConfig?.token;
|
|
160
|
-
const merged = {
|
|
161
|
-
...process.env,
|
|
162
|
-
...(env || {})
|
|
163
|
-
};
|
|
164
|
-
if (configuredToken && !merged.FLY_API_TOKEN) {
|
|
165
|
-
merged.FLY_API_TOKEN = configuredToken;
|
|
166
|
-
}
|
|
167
|
-
return merged;
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
/**
|
|
171
|
-
* List apps on Fly.io
|
|
172
|
-
*/
|
|
173
|
-
public listApps(): string {
|
|
174
|
-
if (!this.runtime.checkFlyctlInstalled(this.flyctl)) {
|
|
175
|
-
throw new Error(`\`${this.flyctl}\` is not installed on your system PATH.`);
|
|
176
|
-
}
|
|
177
|
-
return this.runtime.execSync(`${this.flyctl} apps list`, this.flyExecOptions({ encoding: "utf8" })) as string;
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
/**
|
|
181
|
-
* Create and deploy a new seller app on Fly.io
|
|
182
|
-
*/
|
|
183
|
-
public createSeller(options: SellerCreateOptions): string {
|
|
184
|
-
const { name, dryRun } = options;
|
|
185
|
-
// --app overrides the auto-prefixed name; allows bare "tbs-86d81e" or "tb-seller-foo"
|
|
186
|
-
const appName = options.app || `tb-seller-${name}`;
|
|
187
|
-
|
|
188
|
-
const targetImage = options.image;
|
|
189
|
-
const targetRegion = options.region
|
|
190
|
-
|| this.providerConfig?.default_region
|
|
191
|
-
|| "sin";
|
|
192
|
-
const operatorSecret = options.operatorSecret
|
|
193
|
-
|| this.providerConfig?.operator_secret
|
|
194
|
-
|| "";
|
|
195
|
-
const flyConfig = options.flyConfig;
|
|
196
|
-
const volumeName = options.volumeName
|
|
197
|
-
|| this.providerConfig?.volume_name
|
|
198
|
-
|| "tb_seller_data";
|
|
199
|
-
const volumeSizeGb = options.volumeSizeGb
|
|
200
|
-
|| this.providerConfig?.volume_size_gb
|
|
201
|
-
|| 1;
|
|
202
|
-
const volumeSnapshotRetentionDays = options.volumeSnapshotRetentionDays;
|
|
203
|
-
const volumeId = options.volumeId;
|
|
204
|
-
const initialConfigPath = options.initialConfigPath;
|
|
205
|
-
|
|
206
|
-
if (!targetImage) {
|
|
207
|
-
throw new Error("seller create requires --image registry.fly.io/tb-seller:<v>");
|
|
208
|
-
}
|
|
209
|
-
if (!flyConfig) {
|
|
210
|
-
throw new Error("seller create requires --fly-config deploy/fly.io/fly.tb-seller.toml");
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
if (dryRun) {
|
|
214
|
-
const lines = [
|
|
215
|
-
`[DRY-RUN] Will create fly app: ${appName}`,
|
|
216
|
-
` Region: ${targetRegion}`,
|
|
217
|
-
` Image: ${targetImage}`,
|
|
218
|
-
` Secret: OPERATOR_SECRET=***${operatorSecret.slice(-4) || "????"}`,
|
|
219
|
-
` Volume: ${volumeName} (${volumeSizeGb}GB)`,
|
|
220
|
-
];
|
|
221
|
-
if (flyConfig) lines.push(` Fly config: ${flyConfig}`);
|
|
222
|
-
if (initialConfigPath) lines.push(` Initial config secret: TOKENBUDDY_SELLER_CONFIG_B64 from ${initialConfigPath}`);
|
|
223
|
-
if (volumeId) lines.push(` Volume ID: ${volumeId}`);
|
|
224
|
-
if (volumeSnapshotRetentionDays !== undefined) lines.push(` Volume snapshot retention: ${volumeSnapshotRetentionDays} days`);
|
|
225
|
-
return lines.join("\n");
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
if (!this.runtime.checkFlyctlInstalled(this.flyctl)) {
|
|
229
|
-
throw new Error(`\`${this.flyctl}\` is not installed on PATH.`);
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
if (!operatorSecret) {
|
|
233
|
-
throw new Error("operator_secret is required. Provide --operator-secret or configure seller_providers.fly.operator_secret");
|
|
234
|
-
}
|
|
235
|
-
requireReadableFile(flyConfig, "Fly config");
|
|
236
|
-
if (initialConfigPath) {
|
|
237
|
-
requireReadableFile(initialConfigPath, "Initial seller config");
|
|
238
|
-
}
|
|
239
|
-
requirePublishedDockerImage(targetImage, this.runtime.imageInspector);
|
|
240
|
-
|
|
241
|
-
console.log(`[Fly.io] Creating app ${appName}...`);
|
|
242
|
-
this.runtime.execSync(`${this.flyctl} apps create ${appName} --machines`, this.flyExecOptions({ stdio: "inherit" }));
|
|
243
|
-
|
|
244
|
-
console.log(`[Fly.io] Setting secrets...`);
|
|
245
|
-
this.importCreateSecrets(appName, operatorSecret, initialConfigPath);
|
|
246
|
-
|
|
247
|
-
console.log(`[Fly.io] Deploying image ${targetImage}...`);
|
|
248
|
-
const deployCmd = `${this.flyctl} deploy -c ${flyConfig} --image ${targetImage} --primary-region ${targetRegion} --app ${appName} --now`;
|
|
249
|
-
this.runtime.execSync(deployCmd, this.flyExecOptions({ stdio: "inherit" }));
|
|
250
|
-
|
|
251
|
-
return `Successfully deployed ${appName} on Fly.io`;
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
private importCreateSecrets(appName: string, operatorSecret: string, initialConfigPath: string | undefined): void {
|
|
255
|
-
const lines = [
|
|
256
|
-
"ALLOW_MOCK=false",
|
|
257
|
-
`OPERATOR_SECRET=${operatorSecret}`
|
|
258
|
-
];
|
|
259
|
-
if (initialConfigPath) {
|
|
260
|
-
const configContent = fs.readFileSync(initialConfigPath, "utf8");
|
|
261
|
-
lines.push(`TOKENBUDDY_SELLER_CONFIG_B64=${Buffer.from(configContent, "utf8").toString("base64")}`);
|
|
262
|
-
}
|
|
263
|
-
const result = this.runtime.spawnSync(this.flyctl, ["secrets", "import", "--stage", "--app", appName], this.flySpawnOptions({
|
|
264
|
-
input: `${lines.join("\n")}\n`,
|
|
265
|
-
stdio: ["pipe", "inherit", "inherit"]
|
|
266
|
-
}));
|
|
267
|
-
if (result.error) {
|
|
268
|
-
throw result.error;
|
|
269
|
-
}
|
|
270
|
-
if (result.status !== 0) {
|
|
271
|
-
throw new Error(`flyctl secrets import failed with exit code ${result.status}`);
|
|
272
|
-
}
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
/**
|
|
276
|
-
* Destroy a seller app on Fly.io.
|
|
277
|
-
* @param nameOrApp Either a bare name (e.g. "86d81e") or a full app name (e.g. "tbs-86d81e").
|
|
278
|
-
* If the value contains a hyphen, it is treated as the full app name.
|
|
279
|
-
* @param dryRun
|
|
280
|
-
*/
|
|
281
|
-
public removeSeller(nameOrApp: string, dryRun?: boolean): string {
|
|
282
|
-
const appName = nameOrApp.includes("-") ? nameOrApp : `tb-seller-${nameOrApp}`;
|
|
283
|
-
if (dryRun) {
|
|
284
|
-
return `[DRY-RUN] Will destroy fly app: ${appName}`;
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
if (!this.runtime.checkFlyctlInstalled(this.flyctl)) {
|
|
288
|
-
throw new Error(`\`${this.flyctl}\` is not installed.`);
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
console.log(`[Fly.io] Destroying app ${appName}...`);
|
|
292
|
-
this.runtime.execSync(`${this.flyctl} apps destroy ${appName} --yes`, this.flyExecOptions({ stdio: "inherit" }));
|
|
293
|
-
|
|
294
|
-
return `Successfully destroyed ${appName} on Fly.io`;
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
/**
|
|
298
|
-
* Get status of a specific seller app
|
|
299
|
-
*/
|
|
300
|
-
public statusApp(name: string): string {
|
|
301
|
-
const appName = name.includes("-") ? name : `tb-seller-${name}`;
|
|
302
|
-
if (!this.runtime.checkFlyctlInstalled(this.flyctl)) {
|
|
303
|
-
throw new Error(`\`${this.flyctl}\` is not installed.`);
|
|
304
|
-
}
|
|
305
|
-
return this.runtime.execSync(`${this.flyctl} status --app ${appName}`, this.flyExecOptions({ encoding: "utf8" })) as string;
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
/**
|
|
309
|
-
* Update an existing seller app's Machines to a new image.
|
|
310
|
-
*
|
|
311
|
-
* This intentionally does not call `fly deploy --config`: existing seller
|
|
312
|
-
* deploys must not reconcile volumes, mounts, or service configuration.
|
|
313
|
-
*/
|
|
314
|
-
public deploySeller(options: {
|
|
315
|
-
app: string;
|
|
316
|
-
config?: string;
|
|
317
|
-
image?: string;
|
|
318
|
-
dryRun?: boolean;
|
|
319
|
-
}): string {
|
|
320
|
-
const { app, image, dryRun } = options;
|
|
321
|
-
const targetImage = image;
|
|
322
|
-
|
|
323
|
-
if (!targetImage) {
|
|
324
|
-
throw new Error("seller deploy requires --image registry.fly.io/tb-seller:<v>");
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
if (dryRun) {
|
|
328
|
-
const lines = [`[DRY-RUN] Will update existing app machines: ${app}`];
|
|
329
|
-
lines.push(` Image: ${targetImage}`);
|
|
330
|
-
lines.push(" Config: unchanged");
|
|
331
|
-
lines.push(" Volumes: unchanged");
|
|
332
|
-
return lines.join("\n");
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
if (!this.runtime.checkFlyctlInstalled(this.flyctl)) {
|
|
336
|
-
throw new Error(`\`${this.flyctl}\` is not installed.`);
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
const machinesJson = this.runtime.execSync(`${this.flyctl} machines list --app ${app} --json`, this.flyExecOptions({ encoding: "utf8" })) as string;
|
|
340
|
-
const machineIds = parseFlyMachineIds(machinesJson, app);
|
|
341
|
-
|
|
342
|
-
console.log(`[Fly.io] Updating ${app} image on ${machineIds.length} machine(s)...`);
|
|
343
|
-
for (const machineId of machineIds) {
|
|
344
|
-
this.runtime.execSync(
|
|
345
|
-
`${this.flyctl} machine update ${machineId} --app ${app} --image ${targetImage} --yes`,
|
|
346
|
-
this.flyExecOptions({ stdio: "inherit" })
|
|
347
|
-
);
|
|
348
|
-
}
|
|
349
|
-
return `Successfully updated ${app} image`;
|
|
350
|
-
}
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
function requireReadableFile(filePath: string, label: string): void {
|
|
354
|
-
if (!fs.existsSync(filePath)) {
|
|
355
|
-
throw new Error(`${label} file does not exist: ${filePath}`);
|
|
356
|
-
}
|
|
357
|
-
const stat = fs.statSync(filePath);
|
|
358
|
-
if (!stat.isFile()) {
|
|
359
|
-
throw new Error(`${label} path is not a file: ${filePath}`);
|
|
360
|
-
}
|
|
361
|
-
fs.accessSync(filePath, fs.constants.R_OK);
|
|
362
|
-
}
|