underpost 3.2.80 → 3.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.
Files changed (84) hide show
  1. package/.github/workflows/ghpkg.ci.yml +7 -1
  2. package/.github/workflows/pwa-microservices-template-page.cd.yml +1 -16
  3. package/.github/workflows/pwa-microservices-template-test.ci.yml +1 -1
  4. package/.github/workflows/release.cd.yml +1 -9
  5. package/CHANGELOG.md +291 -1
  6. package/CLI-HELP.md +174 -23
  7. package/README.md +5 -2
  8. package/bin/build.js +7 -5
  9. package/bin/deploy.js +19 -17
  10. package/deploy/lib/logging.sh +96 -0
  11. package/deploy/pwa-microservices-template/deploy.sh +72 -0
  12. package/deploy/release/deploy.sh +62 -0
  13. package/docker-compose.yml +1 -1
  14. package/manifests/cronjobs/dd-cron/dd-cron-backup.yaml +5 -1
  15. package/manifests/cronjobs/dd-cron/dd-cron-dns.yaml +1 -1
  16. package/manifests/cronjobs/dd-cron/dd-cron-vultr.yaml +52 -0
  17. package/manifests/deployment/dd-default-development/deployment.yaml +2 -2
  18. package/manifests/deployment/playwright/deployment.yaml +1 -1
  19. package/manifests/mongodb/kustomization.yaml +4 -1
  20. package/manifests/mongodb/statefulset.yaml +4 -0
  21. package/manifests/mongodb/storage-class.yaml +9 -2
  22. package/package.json +19 -19
  23. package/scripts/audit-selinux.sh +64 -0
  24. package/scripts/coverall-test.sh +24 -0
  25. package/scripts/gpu-diag.sh +0 -0
  26. package/scripts/ip-info.sh +0 -0
  27. package/scripts/k3s-node-setup.sh +18 -15
  28. package/scripts/kubeadm-node-setup.sh +12 -23
  29. package/scripts/link-local-underpost-cli.sh +0 -0
  30. package/scripts/lxd-vm-setup.sh +0 -0
  31. package/scripts/maas-nat-firewalld.sh +0 -0
  32. package/scripts/nat-iptables.sh +12 -4
  33. package/scripts/rhel-grpc-setup.sh +0 -0
  34. package/scripts/rocky-kickstart.sh +25 -9
  35. package/scripts/test-monitor.sh +4 -3
  36. package/src/cli/baremetal.js +1 -2
  37. package/src/cli/cloud-init.js +1 -1
  38. package/src/cli/cluster.js +786 -96
  39. package/src/cli/db.js +11 -4
  40. package/src/cli/deploy.js +1698 -177
  41. package/src/cli/docker-compose.js +19 -178
  42. package/src/cli/env.js +1 -1
  43. package/src/cli/image.js +15 -7
  44. package/src/cli/index.js +245 -44
  45. package/src/cli/ipfs.js +82 -11
  46. package/src/cli/lxd.js +1 -1
  47. package/src/cli/monitor.js +2 -2
  48. package/src/cli/release.js +57 -22
  49. package/src/cli/repository.js +12 -10
  50. package/src/cli/run.js +2195 -427
  51. package/src/cli/secrets.js +969 -0
  52. package/src/cli/ssh.js +206 -105
  53. package/src/cli/system.js +26 -13
  54. package/src/cli/test.js +1 -1
  55. package/src/cli/vultr.js +583 -0
  56. package/src/cli/wireguard.js +2125 -0
  57. package/src/client-builder/client-build.js +102 -13
  58. package/src/client-builder/ssr.js +27 -73
  59. package/src/db/mongo/MongoBootstrap.js +295 -54
  60. package/src/db/mongo/MongooseDB.js +51 -32
  61. package/src/index.js +25 -1
  62. package/src/projects/underpost/catalog-underpost.js +4 -1
  63. package/src/server/backup.js +1 -1
  64. package/src/server/conf.js +1216 -168
  65. package/src/server/cri.js +70 -0
  66. package/src/server/cron.js +249 -51
  67. package/src/server/dns.js +100 -6
  68. package/src/server/environment.js +98 -0
  69. package/src/server/forward-proxy.js +549 -0
  70. package/src/server/middlewares.js +56 -1
  71. package/src/server/process.js +0 -1
  72. package/src/server/selinux.js +185 -0
  73. package/src/server/systemd.js +205 -0
  74. package/src/server/underpost-compression.js +186 -0
  75. package/src/server/underpost-gateway.js +1083 -0
  76. package/src/server/underpost-ingress.js +380 -0
  77. package/test/cluster-instances.test.js +435 -0
  78. package/test/deploy-node-placement.test.js +45 -0
  79. package/test/instance-traffic-plan.test.js +710 -0
  80. package/test/selinux.test.js +71 -0
  81. package/test/sops-secret-store.test.js +612 -0
  82. package/test/underpost-gateway.test.js +510 -0
  83. package/test/underpost-ingress.test.js +305 -0
  84. package/test/wireguard-edge.test.js +1177 -0
@@ -12,7 +12,6 @@ import fs from 'fs-extra';
12
12
  import nodePath from 'path';
13
13
  import { getRootDirectory, shellExec } from '../server/process.js';
14
14
  import { loggerFactory } from '../server/logger.js';
15
- import { loadInstanceTopology } from '../server/conf.js';
16
15
  import Nginx from '../runtime/nginx/Nginx.js';
17
16
 
18
17
  const logger = loggerFactory(import.meta);
@@ -82,9 +81,10 @@ class UnderpostDockerCompose {
82
81
  * Resolves the canonical directory for a custom docker-compose workflow,
83
82
  * keyed by `--deploy-id` + `--docker-compose-id`:
84
83
  * `engine-private/conf/<deploy-id>/docker-compose/<docker-compose-id>`.
85
- * This directory ships its own `docker-compose.yml`, `compose.env`, and
86
- * `nginx.conf` (used as-is, never generated). Returns null when
87
- * `--docker-compose-id` is not set (the default, self-generating workflow).
84
+ * This directory ships its own `docker-compose.yml`, `compose.env`, and any
85
+ * project-specific bind-mounted files (all used as-is, never generated).
86
+ * Returns null when `--docker-compose-id` is not set (the default,
87
+ * self-generating workflow).
88
88
  * @param {object} options - CLI options.
89
89
  * @returns {string|null} Repo-root-relative canonical dir, or null.
90
90
  * @memberof UnderpostDockerCompose
@@ -100,8 +100,8 @@ class UnderpostDockerCompose {
100
100
  * so behavior is independent of the caller's working directory.
101
101
  *
102
102
  * Custom workflow (`--docker-compose-id`): the compose file, env-file, and
103
- * bind-mounted config (nginx.conf, mongodb/) all live in the canonical dir, so
104
- * compose runs with `--project-directory` pinned there and no app-override.
103
+ * project-owned bind-mounted config all live in the canonical dir, so compose
104
+ * runs with `--project-directory` pinned there and no app-override.
105
105
  * @param {object} options - CLI options.
106
106
  * @returns {string} The base command string (without a subcommand).
107
107
  * @memberof UnderpostDockerCompose
@@ -192,7 +192,7 @@ services:
192
192
  MONGO_IMAGE=mongo:latest
193
193
  VALKEY_IMAGE=valkey/valkey:latest
194
194
  APP_IMAGE=underpost/underpost-engine
195
- APP_TAG=v3.2.80
195
+ APP_TAG=v3.3.0
196
196
  PROXY_IMAGE=nginx:stable-alpine
197
197
  PROMETHEUS_IMAGE=prom/prometheus:latest
198
198
  GRAFANA_IMAGE=grafana/grafana:latest
@@ -362,150 +362,6 @@ datasources:
362
362
  `;
363
363
  }
364
364
 
365
- /**
366
- * Expands the deploy's `multiInstance` topology (from conf.instances.json, via
367
- * {@link loadInstanceTopology}) into the per-variant descriptors the compose
368
- * generators consume. Application-agnostic: it only reads variant code/slug/
369
- * path and the default code. Returns null when the deploy is single-instance.
370
- * @param {object} options - CLI options.
371
- * @returns {?{defaultCode: string, variants: Array<object>}}
372
- * @memberof UnderpostDockerCompose
373
- */
374
- static instanceTopology(options = {}) {
375
- const deployId = options.deployId || DEFAULT_DEPLOY_ID;
376
- let topology = null;
377
- try {
378
- topology = loadInstanceTopology(deployId);
379
- } catch {
380
- return null;
381
- }
382
- if (!topology || !Array.isArray(topology.variants) || topology.variants.length === 0) return null;
383
- const defaultCode = topology.default || topology.variants[0].code;
384
- const variants = topology.variants.map((v) => {
385
- const path = v.path || '/';
386
- const isDefault = !(v.slug || '');
387
- return {
388
- code: v.code,
389
- slug: v.slug || '',
390
- path,
391
- isDefault,
392
- // Service/container suffix: empty for the default variant so it keeps
393
- // the historic `cyberia-server` / `cyberia-client` names (and their
394
- // published host ports); `-forest`, `-test`, … for the rest.
395
- suffix: v.slug ? `-${v.slug}` : '',
396
- // Backend prefix strip, mirroring the K8s pathRewritePolicy: the Go
397
- // server serves `/ws` at its root, so a `/FOREST` prefix is stripped.
398
- strip: path !== '/',
399
- };
400
- });
401
- return { defaultCode, variants };
402
- }
403
-
404
- /**
405
- * Renders the nginx reverse-proxy config for the multi-instance Cyberia stack
406
- * as a SINGLE localhost gateway (dev compose is single-host; production uses
407
- * the K8s HTTPProxy, not this file). All three tiers share one origin, routed
408
- * by URL sub-path so `http://localhost/` works with no /etc/hosts:
409
- * /api/*, /assets/* -> engine-cyberia (shared content authority)
410
- * /<slug>/ws -> cyberia-server-<slug> (prefix stripped; the Go
411
- * runtime is instance-agnostic, selected by
412
- * INSTANCE_CODE), and /ws -> the default server
413
- * /<slug>, / -> cyberia-client-<slug> (prefix kept; the WASM
414
- * reads the instance from the URL)
415
- * Upstreams resolve lazily through Docker's embedded DNS so a variant that is
416
- * briefly down never fails nginx startup. Returns null for single-instance.
417
- * @param {object} options - CLI options.
418
- * @returns {?string} nginx.conf content, or null.
419
- * @memberof UnderpostDockerCompose
420
- */
421
- static instancesNginxContent(options = {}) {
422
- const topology = UnderpostDockerCompose.instanceTopology(options);
423
- if (!topology) return null;
424
- const { variants } = topology;
425
-
426
- const headers = ` proxy_set_header Host $host;
427
- proxy_set_header X-Real-IP $remote_addr;
428
- proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
429
- proxy_set_header X-Forwarded-Proto $scheme;
430
- proxy_set_header Upgrade $http_upgrade;
431
- proxy_set_header Connection $connection_upgrade;
432
- proxy_read_timeout 3600s;`;
433
-
434
- const proxyBlock = (variable, upstream, extra = '') =>
435
- ` set ${variable} ${upstream};
436
- ${extra} proxy_pass http://${variable};
437
- ${headers}`;
438
-
439
- // Websocket tier: /<path>/ws -> server (prefix stripped); default -> = /ws.
440
- // Longest-prefix ensures /<path>/ws beats the /<path> client location.
441
- const wsLocations = variants
442
- .map((v) => {
443
- const upstream = `cyberia-server${v.suffix}:8081`;
444
- const varName = `$up_server_${v.slug || 'default'}`;
445
- if (v.isDefault) return ` location = /ws {\n${proxyBlock(varName, upstream)}\n }`;
446
- return ` location ${v.path}/ws {\n${proxyBlock(
447
- varName,
448
- upstream,
449
- ` rewrite ^${v.path}/(.*)$ /$1 break;\n`,
450
- )}\n }`;
451
- })
452
- .join('\n\n');
453
-
454
- // Presentation tier: /<path> -> client (prefix kept); default -> catch-all /.
455
- const clientLocations = variants
456
- .map((v) => {
457
- const upstream = `cyberia-client${v.suffix}:8081`;
458
- const varName = `$up_client_${v.slug || 'default'}`;
459
- if (v.isDefault) return ` location / {\n${proxyBlock(varName, upstream)}\n }`;
460
- return ` location = ${v.path} { return 301 ${v.path}/; }
461
- location ${v.path} {\n${proxyBlock(varName, upstream)}\n }`;
462
- })
463
- .join('\n\n');
464
-
465
- const deployId = options.deployId || DEFAULT_DEPLOY_ID;
466
- return `# Generated by 'underpost docker-compose --generate --deploy-id ${deployId} --docker-compose-id ${options.dockerComposeId || 'cyberia'}' — do not hand-edit.
467
- # Source of truth: engine-private/conf/${deployId}/conf.instances.json (multiInstance).
468
- # Single localhost gateway (dev compose). Access http://localhost/ (default),
469
- # http://localhost/<PATH> per variant. Client origins point back here
470
- # (compose.env CYBERIA_WS_ORIGIN=ws://localhost, CYBERIA_ENGINE_API_ORIGIN=http://localhost).
471
-
472
- map $http_upgrade $connection_upgrade {
473
- default upgrade;
474
- '' close;
475
- }
476
-
477
- # Docker embedded DNS: resolve upstream names per request so a variant that is
478
- # briefly down never fails nginx startup or config reload.
479
- resolver 127.0.0.11 ipv6=off valid=10s;
480
-
481
- proxy_http_version 1.1;
482
-
483
- server {
484
- listen 80 default_server;
485
- server_name localhost 127.0.0.1 _;
486
-
487
- location = /healthz {
488
- access_log off;
489
- return 200 "ok\\n";
490
- add_header Content-Type text/plain;
491
- }
492
-
493
- # Shared content authority: REST API + engine-served assets (fonts, ui-icons).
494
- location /api/ {
495
- ${proxyBlock('$up_engine', 'engine-cyberia:4005')}
496
- }
497
-
498
- location /assets/ {
499
- ${proxyBlock('$up_engine', 'engine-cyberia:4005')}
500
- }
501
-
502
- ${wsLocations}
503
-
504
- ${clientLocations}
505
- }
506
- `;
507
- }
508
-
509
365
  /**
510
366
  * Renders all dynamic supporting files: the nginx reverse-proxy config (from
511
367
  * PROXY_HOSTS), the monitoring configs (Prometheus + Grafana datasource), and
@@ -516,25 +372,17 @@ ${clientLocations}
516
372
  * @memberof UnderpostDockerCompose
517
373
  */
518
374
  static generate(options = {}) {
519
- // Custom workflow: docker-compose.yml and compose.env are hand-authored in
520
- // the canonical dir and used as-is do NOT generate them. Only (re)write
521
- // the two generated artifacts: the MongoDB entrypoint (replica-set
522
- // bootstrap) and, for a multi-instance deploy, nginx.conf (per-variant
523
- // sub-path routing derived from the conf.instances.json multiInstance block,
524
- // kept in sync with the service tiers hand-authored in docker-compose.yml).
375
+ // A named custom workflow is fully owned by its canonical directory. The
376
+ // generic CLI validates the standard entry files but never imports project
377
+ // modules or generates application-specific nginx, env, or helper content.
525
378
  const composeIdBase = UnderpostDockerCompose.composeIdBase(options);
526
379
  if (composeIdBase) {
527
- const mongoEntrypointPath = UnderpostDockerCompose.resolve(`${composeIdBase}/mongodb/entrypoint.sh`);
528
- fs.mkdirpSync(nodePath.dirname(mongoEntrypointPath));
529
- fs.writeFileSync(mongoEntrypointPath, UnderpostDockerCompose.mongoEntrypointContent(), { mode: 0o755 });
530
- logger.info('mongodb entrypoint written (custom workflow)', { path: mongoEntrypointPath });
531
-
532
- const nginx = UnderpostDockerCompose.instancesNginxContent(options);
533
- if (nginx) {
534
- const nginxPath = UnderpostDockerCompose.resolve(`${composeIdBase}/nginx.conf`);
535
- fs.writeFileSync(nginxPath, nginx, 'utf8');
536
- logger.info('multi-instance nginx.conf written', { path: nginxPath });
537
- }
380
+ const required = ['docker-compose.yml', 'compose.env'].map((name) =>
381
+ UnderpostDockerCompose.resolve(`${composeIdBase}/${name}`),
382
+ );
383
+ const missing = required.filter((path) => !fs.existsSync(path));
384
+ if (missing.length) throw new Error(`Custom Docker Compose workflow is missing: ${missing.join(', ')}`);
385
+ logger.info('custom Docker Compose workflow uses canonical files as-is', { path: composeIdBase });
538
386
  return;
539
387
  }
540
388
 
@@ -644,17 +492,10 @@ ${clientLocations}
644
492
  silentOnError: true,
645
493
  });
646
494
 
647
- // Custom workflow: docker-compose.yml and compose.env are hand-authored
648
- // source never prune them. Drop only the generated mongo entrypoint;
649
- // nginx.conf is left in place so the mount stays valid and --generate/--up
650
- // rewrites it.
495
+ // A named custom workflow owns every file beneath its canonical directory.
496
+ // Reset tears down Docker resources but never prunes project-owned files.
651
497
  const composeIdBase = UnderpostDockerCompose.composeIdBase(options);
652
498
  if (composeIdBase) {
653
- const mongoEntrypointPath = UnderpostDockerCompose.resolve(`${composeIdBase}/mongodb/entrypoint.sh`);
654
- if (fs.existsSync(mongoEntrypointPath)) {
655
- fs.removeSync(mongoEntrypointPath);
656
- logger.info('removed generated artifact', { path: mongoEntrypointPath });
657
- }
658
499
  logger.info('Docker Compose reset complete. Run `--up` to recreate the stack.');
659
500
  return;
660
501
  }
@@ -699,7 +540,7 @@ ${clientLocations}
699
540
  * @param {boolean} [options.shell] - Open an interactive shell in `target` (default: app).
700
541
  * @param {string} [options.exec] - General-purpose passthrough docker compose subcommand.
701
542
  * @param {string} [options.deployId] - Deployment to run as the app (default: dd-default). `dd-default` self-bootstraps a fresh engine; any other id runs the standard `underpost start` command.
702
- * @param {string} [options.dockerComposeId] - Custom-workflow selector. When set, use the canonical stack at `engine-private/conf/<deploy-id>/docker-compose/<docker-compose-id>/` (docker-compose.yml + compose.env + nginx.conf, used as-is), skipping nginx/env/app-override generation. Used by the Cyberia MMO ecosystem (`--deploy-id dd-cyberia --docker-compose-id cyberia`).
543
+ * @param {string} [options.dockerComposeId] - Custom-workflow selector. When set, use the canonical stack at `engine-private/conf/<deploy-id>/docker-compose/<docker-compose-id>/` (docker-compose.yml + compose.env, used as-is), skipping nginx/env/app-override generation.
703
544
  * @param {string} [options.env] - Deployment environment for non-default deploy ids (default: development).
704
545
  * @param {string} [options.composeFile] - Override compose file path.
705
546
  * @param {string} [options.envFile] - Override env-file path.
package/src/cli/env.js CHANGED
@@ -4,7 +4,7 @@
4
4
  * @namespace UnderpostEnv
5
5
  */
6
6
 
7
- import { getNpmRootPath, writeEnv } from '../server/conf.js';
7
+ import { getNpmRootPath, writeEnv } from '../server/environment.js';
8
8
  import fs from 'fs-extra';
9
9
  import { loggerFactory } from '../server/logger.js';
10
10
  import dotenv from 'dotenv';
package/src/cli/image.js CHANGED
@@ -10,8 +10,8 @@ import nodePath from 'path';
10
10
  import crypto from 'crypto';
11
11
  import { loggerFactory } from '../server/logger.js';
12
12
  import Underpost from '../index.js';
13
- import { getNpmRootPath } from '../server/conf.js';
14
13
  import { shellExec } from '../server/process.js';
14
+ import { crictlCommandFactory } from '../server/cri.js';
15
15
 
16
16
  const logger = loggerFactory(import.meta);
17
17
 
@@ -225,6 +225,7 @@ class UnderpostImage {
225
225
  * @param {object} options - Options for the image retrieval.
226
226
  * @param {boolean} options.spec - Whether to retrieve images from the pod specifications.
227
227
  * @param {string} options.namespace - Kubernetes namespace to filter pods.
228
+ * @param {boolean} [options.k3s] - Resolve the CRI endpoint as K3s' embedded containerd.
228
229
  * @returns {Array<object>} - Array of objects containing pod names and their corresponding images.
229
230
  * @memberof UnderpostImage
230
231
  */
@@ -245,10 +246,17 @@ class UnderpostImage {
245
246
  }))
246
247
  .filter((o) => o.image);
247
248
  }
248
- const raw = shellExec(node === 'kind-worker' ? `docker exec -i ${node} crictl images` : `crictl images`, {
249
- stdout: true,
250
- silent: true,
251
- });
249
+ // Outside kind, target the live CRI endpoint explicitly: /etc/crictl.yaml
250
+ // may still point at a CRI-O socket the host no longer runs.
251
+ const raw = shellExec(
252
+ node === 'kind-worker'
253
+ ? `docker exec -i ${node} crictl images`
254
+ : crictlCommandFactory('images', options || {}),
255
+ {
256
+ stdout: true,
257
+ silent: true,
258
+ },
259
+ );
252
260
 
253
261
  const heads = raw
254
262
  .split(`\n`)[0]
@@ -312,7 +320,7 @@ class UnderpostImage {
312
320
  shellExec(`docker exec -i kind-control-plane crictl rmi ${imageName}`);
313
321
  shellExec(`docker exec -i kind-worker crictl rmi ${imageName}`);
314
322
  } else if (kubeadm === true) {
315
- shellExec(`crictl rmi ${imageName}`);
323
+ shellExec(crictlCommandFactory(`rmi ${imageName}`));
316
324
  } else if (k3s === true) {
317
325
  shellExec(`sudo k3s ctr images rm ${imageName}`);
318
326
  }
@@ -381,7 +389,7 @@ class UnderpostImage {
381
389
  shellExec(`docker pull ${image}`);
382
390
  shellExec(`sudo kind load docker-image ${image}`);
383
391
  } else {
384
- shellExec(`sudo crictl pull ${image}`);
392
+ shellExec(crictlCommandFactory(`pull ${image}`, { k3s: targetK3s }));
385
393
  }
386
394
  },
387
395
  };