@qpjoy/tunnel-cli 0.2.1 → 0.2.3
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 +34 -0
- package/README.setup.md +13 -0
- package/dist/index.js +270 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -132,6 +132,40 @@ Docker/Compose build containers, the CLI also injects container-facing variables
|
|
|
132
132
|
such as `MARKET_CONTAINER_HTTP_PROXY` and `QP_TUNNEL_CONTAINER_HTTP_PROXY`
|
|
133
133
|
pointing at `host.docker.internal:<mixed-port>`.
|
|
134
134
|
|
|
135
|
+
### K8s/containerd Image Preload
|
|
136
|
+
|
|
137
|
+
Kubernetes on kubeadm/containerd does not use Docker's image store. If Docker
|
|
138
|
+
Compose can pull images through `tun-on` but pods still hit `ImagePullBackOff`,
|
|
139
|
+
preload the runtime images into containerd's `k8s.io` namespace on the K8s host:
|
|
140
|
+
|
|
141
|
+
```bash
|
|
142
|
+
sudo qp-tunnel-cli tun-on
|
|
143
|
+
sudo qp-tunnel-cli k8s preload-images
|
|
144
|
+
sudo qp-tunnel-cli tun-off
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
The default preload set matches the MX Launcher Internal runtime images:
|
|
148
|
+
`postgres:16-alpine`, `coredns/coredns:1.11.3`, and `caddy:2.8.4-alpine`.
|
|
149
|
+
Add more images as needed:
|
|
150
|
+
|
|
151
|
+
```bash
|
|
152
|
+
sudo qp-tunnel-cli k8s preload-images \
|
|
153
|
+
--image postgres:16-alpine \
|
|
154
|
+
--image qpjoy/mx-launcher-server:shadow
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
If the cluster already has pods stuck in `ImagePullBackOff`, read the current
|
|
158
|
+
pod specs and preload their referenced images:
|
|
159
|
+
|
|
160
|
+
```bash
|
|
161
|
+
sudo qp-tunnel-cli k8s preload-images --from-cluster
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
The command pulls missing images with Docker, saves them, and imports them with
|
|
165
|
+
`ctr -n k8s.io images import`, so kubelet can start pods without reaching the
|
|
166
|
+
remote registry. After preloading a previously failed pod image, restart the pod
|
|
167
|
+
or rerun the deployment rollout.
|
|
168
|
+
|
|
135
169
|
Install the bundled script as a normal Linux command:
|
|
136
170
|
|
|
137
171
|
```bash
|
package/README.setup.md
CHANGED
|
@@ -19,4 +19,17 @@ qp-tunnel-cli curl google.com
|
|
|
19
19
|
|
|
20
20
|
# 删除mac的HDO进程
|
|
21
21
|
qp-tunnel-cli hdo down --interface hdo-client
|
|
22
|
+
|
|
23
|
+
# tunnel-cli
|
|
24
|
+
qp-tunnel-cli install --url 'http://download:qpjoy@23.225.161.60:3434/peer_intelligent01.mihomo.yaml'
|
|
25
|
+
|
|
26
|
+
# K8s/containerd 镜像预热:Docker 能拉,但 kubelet/containerd 不能拉时用
|
|
27
|
+
sudo qp-tunnel-cli tun-on
|
|
28
|
+
sudo qp-tunnel-cli k8s preload-images
|
|
29
|
+
sudo qp-tunnel-cli tun-off
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
qp-tunnel-cli k8s preload-images --from-cluster
|
|
22
35
|
```
|
package/dist/index.js
CHANGED
|
@@ -4,6 +4,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
4
4
|
const node_child_process_1 = require("node:child_process");
|
|
5
5
|
const node_fs_1 = require("node:fs");
|
|
6
6
|
const promises_1 = require("node:fs/promises");
|
|
7
|
+
const node_os_1 = require("node:os");
|
|
7
8
|
const node_path_1 = require("node:path");
|
|
8
9
|
const hdo_1 = require("./hdo");
|
|
9
10
|
const args = process.argv.slice(2);
|
|
@@ -12,6 +13,8 @@ const bundledClientScript = (0, node_path_1.resolve)(packageRoot, 'resources/mih
|
|
|
12
13
|
const repoClientScript = (0, node_path_1.resolve)(packageRoot, '../../../scripts/mihomo-client.sh');
|
|
13
14
|
const defaultInstallTarget = '/usr/local/bin/mihomo-client';
|
|
14
15
|
const defaultMihomoConfigFile = '/etc/mihomo-client/config.yaml';
|
|
16
|
+
const defaultK8sImageNamespace = 'k8s.io';
|
|
17
|
+
const defaultK8sRuntimeImages = ['postgres:16-alpine', 'coredns/coredns:1.11.3', 'caddy:2.8.4-alpine'];
|
|
15
18
|
const defaultNoProxyEntries = [
|
|
16
19
|
'localhost',
|
|
17
20
|
'127.0.0.1',
|
|
@@ -84,6 +87,7 @@ Usage:
|
|
|
84
87
|
qp-tunnel-cli install-script [--target /usr/local/bin/mihomo-client]
|
|
85
88
|
qp-tunnel-cli script-path
|
|
86
89
|
qp-tunnel-cli client-help
|
|
90
|
+
qp-tunnel-cli k8s preload-images [--image postgres:16-alpine]
|
|
87
91
|
qp-tunnel-cli hdo enroll --server-url https://domestic.example.com --username user
|
|
88
92
|
qp-tunnel-cli <mihomo-client command> [options]
|
|
89
93
|
qp-tunnel-cli -- <command> [args...]
|
|
@@ -97,6 +101,7 @@ Common commands:
|
|
|
97
101
|
qp-tunnel-cli egress-on
|
|
98
102
|
qp-tunnel-cli tun-on
|
|
99
103
|
qp-tunnel-cli tun-off
|
|
104
|
+
qp-tunnel-cli k8s preload-images
|
|
100
105
|
qp-tunnel-cli update-subscription
|
|
101
106
|
qp-tunnel-cli hdo status
|
|
102
107
|
qp-tunnel-cli uninstall --purge
|
|
@@ -111,6 +116,12 @@ receive HTTP_PROXY=http://127.0.0.1:<mixed-port>; Docker/Compose build contexts
|
|
|
111
116
|
also receive container-facing variables such as MARKET_CONTAINER_HTTP_PROXY and
|
|
112
117
|
QP_TUNNEL_CONTAINER_HTTP_PROXY=http://host.docker.internal:<mixed-port>.
|
|
113
118
|
|
|
119
|
+
K8s/containerd hosts keep a separate image store from Docker. After tun-on or
|
|
120
|
+
egress-on makes Docker pulls work, preload runtime images into containerd:
|
|
121
|
+
sudo qp-tunnel-cli tun-on
|
|
122
|
+
sudo qp-tunnel-cli k8s preload-images
|
|
123
|
+
sudo qp-tunnel-cli tun-off
|
|
124
|
+
|
|
114
125
|
Install the script as a normal server command:
|
|
115
126
|
sudo qp-tunnel-cli install-script
|
|
116
127
|
sudo mihomo-client status
|
|
@@ -268,6 +279,261 @@ function runExternalCommand(commandArgs) {
|
|
|
268
279
|
});
|
|
269
280
|
exitFromSpawn(result);
|
|
270
281
|
}
|
|
282
|
+
function commandAvailable(command) {
|
|
283
|
+
const result = (0, node_child_process_1.spawnSync)('sh', ['-c', `command -v ${command} >/dev/null 2>&1`], {
|
|
284
|
+
stdio: 'ignore',
|
|
285
|
+
});
|
|
286
|
+
return result.status === 0;
|
|
287
|
+
}
|
|
288
|
+
function shellQuote(value) {
|
|
289
|
+
if (/^[A-Za-z0-9_./:@%+=,-]+$/.test(value)) {
|
|
290
|
+
return value;
|
|
291
|
+
}
|
|
292
|
+
return `'${value.replace(/'/g, "'\\''")}'`;
|
|
293
|
+
}
|
|
294
|
+
function runStep(command, commandArgs, dryRun = false) {
|
|
295
|
+
process.stdout.write(`+ ${[command, ...commandArgs].map(shellQuote).join(' ')}\n`);
|
|
296
|
+
if (dryRun) {
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
const result = (0, node_child_process_1.spawnSync)(command, commandArgs, {
|
|
300
|
+
stdio: 'inherit',
|
|
301
|
+
env: process.env,
|
|
302
|
+
});
|
|
303
|
+
if (result.error) {
|
|
304
|
+
process.stderr.write(`${result.error.message}\n`);
|
|
305
|
+
process.exit(1);
|
|
306
|
+
}
|
|
307
|
+
if (result.signal) {
|
|
308
|
+
process.stderr.write(`Command terminated by signal ${result.signal}\n`);
|
|
309
|
+
process.exit(1);
|
|
310
|
+
}
|
|
311
|
+
if ((result.status ?? 0) !== 0) {
|
|
312
|
+
process.exit(result.status ?? 1);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
function commandSucceeds(command, commandArgs) {
|
|
316
|
+
const result = (0, node_child_process_1.spawnSync)(command, commandArgs, {
|
|
317
|
+
stdio: 'ignore',
|
|
318
|
+
env: process.env,
|
|
319
|
+
});
|
|
320
|
+
return !result.error && !result.signal && result.status === 0;
|
|
321
|
+
}
|
|
322
|
+
function splitImageList(value) {
|
|
323
|
+
return value
|
|
324
|
+
.split(/[\s,]+/)
|
|
325
|
+
.map((item) => item.trim())
|
|
326
|
+
.filter(Boolean);
|
|
327
|
+
}
|
|
328
|
+
function dedupe(values) {
|
|
329
|
+
const seen = new Set();
|
|
330
|
+
const result = [];
|
|
331
|
+
for (const value of values) {
|
|
332
|
+
if (seen.has(value))
|
|
333
|
+
continue;
|
|
334
|
+
seen.add(value);
|
|
335
|
+
result.push(value);
|
|
336
|
+
}
|
|
337
|
+
return result;
|
|
338
|
+
}
|
|
339
|
+
function k8sHelp() {
|
|
340
|
+
process.stdout.write(`Usage:
|
|
341
|
+
qp-tunnel-cli k8s help
|
|
342
|
+
qp-tunnel-cli k8s preload-images [options]
|
|
343
|
+
|
|
344
|
+
Options:
|
|
345
|
+
--image <image> Add one image. Can be repeated.
|
|
346
|
+
--images <images> Add comma- or space-separated images.
|
|
347
|
+
--from-cluster Include images referenced by current Kubernetes pods.
|
|
348
|
+
--namespace <name> containerd namespace. Default: ${defaultK8sImageNamespace}
|
|
349
|
+
--no-pull Import only images already present in Docker.
|
|
350
|
+
--dry-run Print commands without running them.
|
|
351
|
+
|
|
352
|
+
Default images used when no image options are provided and --from-cluster is not set:
|
|
353
|
+
${defaultK8sRuntimeImages.join(' ')}
|
|
354
|
+
|
|
355
|
+
The preload command pulls with Docker, saves each image, then imports it into
|
|
356
|
+
containerd's k8s.io namespace so kubelet can start pods without reaching the
|
|
357
|
+
remote registry itself. Run it after tun-on or egress-on on the K8s host.
|
|
358
|
+
`);
|
|
359
|
+
}
|
|
360
|
+
function parseK8sPreloadArgs(commandArgs) {
|
|
361
|
+
let namespace = defaultK8sImageNamespace;
|
|
362
|
+
let pull = true;
|
|
363
|
+
let dryRun = false;
|
|
364
|
+
let fromCluster = false;
|
|
365
|
+
const images = [];
|
|
366
|
+
for (let index = 0; index < commandArgs.length; index += 1) {
|
|
367
|
+
const arg = commandArgs[index];
|
|
368
|
+
if (arg === '--image') {
|
|
369
|
+
const value = commandArgs[index + 1];
|
|
370
|
+
if (!value) {
|
|
371
|
+
process.stderr.write('Missing value for --image.\n');
|
|
372
|
+
process.exit(1);
|
|
373
|
+
}
|
|
374
|
+
images.push(value);
|
|
375
|
+
index += 1;
|
|
376
|
+
}
|
|
377
|
+
else if (arg === '--images') {
|
|
378
|
+
const value = commandArgs[index + 1];
|
|
379
|
+
if (!value) {
|
|
380
|
+
process.stderr.write('Missing value for --images.\n');
|
|
381
|
+
process.exit(1);
|
|
382
|
+
}
|
|
383
|
+
images.push(...splitImageList(value));
|
|
384
|
+
index += 1;
|
|
385
|
+
}
|
|
386
|
+
else if (arg === '--namespace') {
|
|
387
|
+
const value = commandArgs[index + 1];
|
|
388
|
+
if (!value) {
|
|
389
|
+
process.stderr.write('Missing value for --namespace.\n');
|
|
390
|
+
process.exit(1);
|
|
391
|
+
}
|
|
392
|
+
namespace = value;
|
|
393
|
+
index += 1;
|
|
394
|
+
}
|
|
395
|
+
else if (arg === '--no-pull') {
|
|
396
|
+
pull = false;
|
|
397
|
+
}
|
|
398
|
+
else if (arg === '--dry-run') {
|
|
399
|
+
dryRun = true;
|
|
400
|
+
}
|
|
401
|
+
else if (arg === '--from-cluster') {
|
|
402
|
+
fromCluster = true;
|
|
403
|
+
}
|
|
404
|
+
else if (arg === '--help' || arg === '-h') {
|
|
405
|
+
k8sHelp();
|
|
406
|
+
process.exit(0);
|
|
407
|
+
}
|
|
408
|
+
else {
|
|
409
|
+
process.stderr.write(`Unknown k8s preload-images option: ${arg}\n`);
|
|
410
|
+
process.exit(1);
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
return {
|
|
414
|
+
dryRun,
|
|
415
|
+
fromCluster,
|
|
416
|
+
images: dedupe(images),
|
|
417
|
+
namespace,
|
|
418
|
+
pull,
|
|
419
|
+
};
|
|
420
|
+
}
|
|
421
|
+
function ensureK8sHostTools(dryRun, fromCluster) {
|
|
422
|
+
if (dryRun) {
|
|
423
|
+
return;
|
|
424
|
+
}
|
|
425
|
+
const requiredCommands = fromCluster ? ['docker', 'ctr', 'kubectl'] : ['docker', 'ctr'];
|
|
426
|
+
const missing = requiredCommands.filter((command) => !commandAvailable(command));
|
|
427
|
+
if (missing.length > 0) {
|
|
428
|
+
process.stderr.write(`Missing required command(s): ${missing.join(', ')}\nInstall Docker and containerd, then retry on the K8s host.\n`);
|
|
429
|
+
process.exit(1);
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
function collectImagesFromContainerList(value, images) {
|
|
433
|
+
if (!Array.isArray(value)) {
|
|
434
|
+
return;
|
|
435
|
+
}
|
|
436
|
+
for (const container of value) {
|
|
437
|
+
if (typeof container === 'object' &&
|
|
438
|
+
container !== null &&
|
|
439
|
+
'image' in container &&
|
|
440
|
+
typeof container.image === 'string') {
|
|
441
|
+
images.push(container.image);
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
function collectClusterPodImages(dryRun) {
|
|
446
|
+
process.stdout.write('+ kubectl get pods -A -o json\n');
|
|
447
|
+
if (dryRun) {
|
|
448
|
+
return [];
|
|
449
|
+
}
|
|
450
|
+
const result = (0, node_child_process_1.spawnSync)('kubectl', ['get', 'pods', '-A', '-o', 'json'], {
|
|
451
|
+
encoding: 'utf8',
|
|
452
|
+
env: process.env,
|
|
453
|
+
});
|
|
454
|
+
if (result.error) {
|
|
455
|
+
process.stderr.write(`${result.error.message}\n`);
|
|
456
|
+
process.exit(1);
|
|
457
|
+
}
|
|
458
|
+
if (result.signal) {
|
|
459
|
+
process.stderr.write(`Command terminated by signal ${result.signal}\n`);
|
|
460
|
+
process.exit(1);
|
|
461
|
+
}
|
|
462
|
+
if ((result.status ?? 0) !== 0) {
|
|
463
|
+
process.stderr.write(result.stderr || 'kubectl get pods failed.\n');
|
|
464
|
+
process.exit(result.status ?? 1);
|
|
465
|
+
}
|
|
466
|
+
const parsed = JSON.parse(result.stdout);
|
|
467
|
+
const images = [];
|
|
468
|
+
for (const item of parsed.items ?? []) {
|
|
469
|
+
const spec = item.spec ?? {};
|
|
470
|
+
collectImagesFromContainerList(spec.initContainers, images);
|
|
471
|
+
collectImagesFromContainerList(spec.containers, images);
|
|
472
|
+
collectImagesFromContainerList(spec.ephemeralContainers, images);
|
|
473
|
+
}
|
|
474
|
+
return dedupe(images);
|
|
475
|
+
}
|
|
476
|
+
function importDockerImageIntoContainerd(image, namespace, dryRun) {
|
|
477
|
+
const tempDir = dryRun ? '/tmp/qp-tunnel-cli-k8s-dry-run' : (0, node_fs_1.mkdtempSync)((0, node_path_1.join)((0, node_os_1.tmpdir)(), 'qp-tunnel-cli-k8s-'));
|
|
478
|
+
const archive = (0, node_path_1.join)(tempDir, 'image.tar');
|
|
479
|
+
try {
|
|
480
|
+
runStep('docker', ['save', image, '-o', archive], dryRun);
|
|
481
|
+
runStep('ctr', ['-n', namespace, 'images', 'import', archive], dryRun);
|
|
482
|
+
}
|
|
483
|
+
finally {
|
|
484
|
+
if (!dryRun) {
|
|
485
|
+
(0, node_fs_1.rmSync)(tempDir, { recursive: true, force: true });
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
function preloadK8sImages(commandArgs) {
|
|
490
|
+
if (process.platform !== 'linux') {
|
|
491
|
+
process.stderr.write('K8s/containerd image preload targets Linux servers. Run this on the K8s host.\n');
|
|
492
|
+
process.exit(1);
|
|
493
|
+
}
|
|
494
|
+
if (!isRoot()) {
|
|
495
|
+
sudoSelf(['k8s', 'preload-images', ...commandArgs]);
|
|
496
|
+
}
|
|
497
|
+
const options = parseK8sPreloadArgs(commandArgs);
|
|
498
|
+
ensureK8sHostTools(options.dryRun, options.fromCluster);
|
|
499
|
+
const clusterImages = options.fromCluster ? collectClusterPodImages(options.dryRun) : [];
|
|
500
|
+
const images = dedupe([
|
|
501
|
+
...(options.images.length > 0 || options.fromCluster ? options.images : defaultK8sRuntimeImages),
|
|
502
|
+
...clusterImages,
|
|
503
|
+
]);
|
|
504
|
+
if (images.length === 0) {
|
|
505
|
+
process.stderr.write('No K8s images found to preload.\n');
|
|
506
|
+
process.exit(1);
|
|
507
|
+
}
|
|
508
|
+
process.stdout.write(`Preloading ${images.length} image(s) into containerd namespace ${options.namespace}\n`);
|
|
509
|
+
for (const image of images) {
|
|
510
|
+
process.stdout.write(`\nImage: ${image}\n`);
|
|
511
|
+
const inDocker = !options.dryRun && commandSucceeds('docker', ['image', 'inspect', image]);
|
|
512
|
+
if (!inDocker && options.pull) {
|
|
513
|
+
runStep('docker', ['pull', image], options.dryRun);
|
|
514
|
+
}
|
|
515
|
+
else if (!inDocker && !options.pull) {
|
|
516
|
+
process.stderr.write(`Docker image is missing and --no-pull was set: ${image}\n`);
|
|
517
|
+
process.exit(1);
|
|
518
|
+
}
|
|
519
|
+
importDockerImageIntoContainerd(image, options.namespace, options.dryRun);
|
|
520
|
+
}
|
|
521
|
+
process.stdout.write('\nK8s/containerd image preload complete.\n');
|
|
522
|
+
}
|
|
523
|
+
function runK8sCommand(commandArgs) {
|
|
524
|
+
const subcommand = commandArgs[0] ?? 'help';
|
|
525
|
+
if (subcommand === 'help' || subcommand === '--help' || subcommand === '-h') {
|
|
526
|
+
k8sHelp();
|
|
527
|
+
return;
|
|
528
|
+
}
|
|
529
|
+
if (subcommand === 'preload' || subcommand === 'preload-images' || subcommand === 'containerd-preload') {
|
|
530
|
+
preloadK8sImages(commandArgs.slice(1));
|
|
531
|
+
return;
|
|
532
|
+
}
|
|
533
|
+
process.stderr.write(`Unknown k8s command: ${subcommand}\n`);
|
|
534
|
+
k8sHelp();
|
|
535
|
+
process.exit(1);
|
|
536
|
+
}
|
|
271
537
|
function parseInstallScriptArgs(scriptArgs) {
|
|
272
538
|
let target = defaultInstallTarget;
|
|
273
539
|
for (let index = 0; index < scriptArgs.length; index += 1) {
|
|
@@ -353,6 +619,10 @@ async function main() {
|
|
|
353
619
|
installClientScript(args.slice(1));
|
|
354
620
|
return;
|
|
355
621
|
}
|
|
622
|
+
if (command === 'k8s') {
|
|
623
|
+
runK8sCommand(args.slice(1));
|
|
624
|
+
return;
|
|
625
|
+
}
|
|
356
626
|
if (command === 'hdo' || command === 'hdo-enroll' || command === 'hdo-refresh') {
|
|
357
627
|
const hdoArgs = command === 'hdo' ? args.slice(1) : [command.replace(/^hdo-/, ''), ...args.slice(1)];
|
|
358
628
|
await (0, hdo_1.runHdoCli)(hdoArgs, { isRoot, sudoSelf });
|