@qpjoy/tunnel-cli 0.1.12 → 0.2.2

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 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,12 @@ 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
22
30
  ```
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 });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@qpjoy/tunnel-cli",
3
- "version": "0.1.12",
3
+ "version": "0.2.2",
4
4
  "description": "Global QPJoy Tunnel CLI for mihomo-client and cross-platform HDO mesh enrollment.",
5
5
  "private": false,
6
6
  "type": "commonjs",
@@ -22,7 +22,7 @@
22
22
  "access": "public"
23
23
  },
24
24
  "dependencies": {
25
- "@qpjoy/electron-core-wireguard": "^0.1.30"
25
+ "@qpjoy/electron-core-wireguard": "^0.2.1"
26
26
  },
27
27
  "devDependencies": {
28
28
  "@types/node": "^22.10.7"