@underpostnet/cyberia 3.2.90 → 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 (129) hide show
  1. package/.env.example +16 -0
  2. package/.github/workflows/coverall.cyberia.ci.yml +87 -0
  3. package/.github/workflows/cyberia-client.cd.yml +1 -18
  4. package/.github/workflows/cyberia-server.cd.yml +1 -18
  5. package/.github/workflows/docker-image.cyberia-client.ci.yml +1 -1
  6. package/.github/workflows/docker-image.cyberia-client.dev.ci.yml +1 -1
  7. package/.github/workflows/docker-image.cyberia-server.ci.yml +1 -1
  8. package/.github/workflows/docker-image.cyberia-server.dev.ci.yml +1 -1
  9. package/.github/workflows/docker-image.engine-cyberia.ci.yml +1 -1
  10. package/.github/workflows/docker-image.engine-cyberia.dev.ci.yml +1 -1
  11. package/.github/workflows/engine-cyberia.cd.yml +3 -78
  12. package/.github/workflows/ghpkg.ci.yml +7 -1
  13. package/.github/workflows/pwa-microservices-template-page.cd.yml +1 -16
  14. package/.github/workflows/pwa-microservices-template-test.ci.yml +1 -1
  15. package/.github/workflows/release.cd.yml +1 -9
  16. package/CHANGELOG.md +110 -1
  17. package/CLI-HELP.md +139 -9
  18. package/Dockerfile +1 -1
  19. package/Dockerfile.dev +1 -1
  20. package/Dockerfile.test +1 -1
  21. package/bin/build.js +7 -5
  22. package/bin/cyberia.js +73 -53
  23. package/bin/deploy.js +1 -1
  24. package/bin/index.js +73 -53
  25. package/compose.env +16 -0
  26. package/conf.js +2 -0
  27. package/deploy/dd-cyberia/deploy.sh +42 -0
  28. package/deploy/dd-cyberia/init.sh +63 -0
  29. package/deploy/dd-cyberia/sync-deploy.sh +194 -0
  30. package/deploy/lib/logging.sh +96 -0
  31. package/deploy/pwa-microservices-template/deploy.sh +72 -0
  32. package/deploy/release/deploy.sh +62 -0
  33. package/deployment.yaml +1 -1
  34. package/docker-compose.yml +31 -37
  35. package/hardhat/package-lock.json +9 -9
  36. package/hardhat/package.json +3 -3
  37. package/manifests/cronjobs/dd-cron/dd-cron-backup.yaml +5 -1
  38. package/manifests/cronjobs/dd-cron/dd-cron-dns.yaml +1 -1
  39. package/manifests/cronjobs/dd-cron/dd-cron-vultr.yaml +52 -0
  40. package/manifests/deployment/dd-cyberia-development/deployment.yaml +1 -1
  41. package/manifests/deployment/dd-default-development/deployment.yaml +2 -2
  42. package/package.json +2 -2
  43. package/scripts/audit-selinux.sh +64 -0
  44. package/scripts/coverall-test-cyberia.sh +24 -0
  45. package/scripts/coverall-test.sh +24 -0
  46. package/scripts/gpu-diag.sh +0 -0
  47. package/scripts/ip-info.sh +0 -0
  48. package/scripts/k3s-node-setup.sh +18 -15
  49. package/scripts/kubeadm-node-setup.sh +12 -23
  50. package/scripts/link-local-underpost-cli.sh +0 -0
  51. package/scripts/lxd-vm-setup.sh +0 -0
  52. package/scripts/maas-nat-firewalld.sh +0 -0
  53. package/scripts/nat-iptables.sh +2 -0
  54. package/scripts/rhel-grpc-setup.sh +0 -0
  55. package/scripts/rocky-kickstart.sh +25 -9
  56. package/scripts/test-monitor.sh +1 -1
  57. package/src/api/cyberia-instance/cyberia-fallback-default-items.js +63 -0
  58. package/src/api/cyberia-instance/cyberia-fallback-world.js +8 -1
  59. package/src/api/cyberia-instance/cyberia-instance.controller.js +3 -0
  60. package/src/api/cyberia-instance/cyberia-instance.router.js +13 -0
  61. package/src/api/cyberia-instance/cyberia-instance.service.js +43 -0
  62. package/src/api/cyberia-instance-conf/cyberia-instance-conf.model.js +1 -0
  63. package/src/api/cyberia-server-defaults/cyberia-server-defaults.js +16 -3
  64. package/src/api/object-layer/object-layer.model.js +204 -3
  65. package/src/api/object-layer/object-layer.service.js +1 -14
  66. package/src/cli/baremetal.js +1 -2
  67. package/src/cli/cloud-init.js +1 -1
  68. package/src/cli/cluster.js +73 -68
  69. package/src/cli/db.js +9 -2
  70. package/src/cli/deploy.js +21 -5
  71. package/src/cli/docker-compose.js +1 -1
  72. package/src/cli/env.js +1 -1
  73. package/src/cli/image.js +0 -1
  74. package/src/cli/index.js +121 -9
  75. package/src/cli/lxd.js +1 -1
  76. package/src/cli/monitor.js +1 -1
  77. package/src/cli/release.js +57 -22
  78. package/src/cli/repository.js +11 -9
  79. package/src/cli/run.js +36 -9
  80. package/src/cli/ssh.js +198 -77
  81. package/src/cli/system.js +26 -13
  82. package/src/cli/test.js +1 -1
  83. package/src/cli/vultr.js +583 -0
  84. package/src/cli/wireguard.js +2125 -0
  85. package/src/client/components/cyberia/FallbackWorldEngineCyberia.js +368 -0
  86. package/src/client/components/cyberia-portal/AppShellCyberiaPortal.js +33 -0
  87. package/src/client/components/cyberia-portal/RouterCyberiaPortal.js +4 -0
  88. package/src/client/components/cyberia-portal/TranslateCyberiaPortal.js +4 -0
  89. package/src/client/public/cyberia-docs/ARCHITECTURE.md +17 -3
  90. package/src/client/public/cyberia-docs/CYBERIA-SERVER.md +1 -1
  91. package/src/client/public/cyberia-docs/ROADMAP.md +1 -1
  92. package/src/client/public/cyberia-docs/WHITE-PAPER.md +1 -1
  93. package/src/client/services/cyberia-instance/cyberia-instance.service.js +40 -0
  94. package/src/client/services/object-layer/object-layer.management.js +4 -4
  95. package/src/client-builder/client-build.js +20 -14
  96. package/src/db/mongo/MongooseDB.js +4 -0
  97. package/src/index.js +25 -1
  98. package/src/projects/cyberia/catalog-cyberia.js +5 -0
  99. package/src/projects/cyberia/generate-saga.js +14 -23
  100. package/src/projects/cyberia/instance-data.js +21 -2
  101. package/src/projects/cyberia/object-layer.js +11 -21
  102. package/src/projects/underpost/catalog-underpost.js +4 -1
  103. package/src/runtime/cyberia-client/Dockerfile +1 -1
  104. package/src/runtime/cyberia-client/Dockerfile.dev +1 -1
  105. package/src/runtime/cyberia-server/Dockerfile +1 -1
  106. package/src/runtime/cyberia-server/Dockerfile.dev +1 -1
  107. package/src/runtime/engine-cyberia/Dockerfile +1 -1
  108. package/src/runtime/engine-cyberia/Dockerfile.dev +1 -1
  109. package/src/runtime/engine-cyberia/Dockerfile.test +1 -1
  110. package/src/runtime/engine-cyberia/compose.env +16 -0
  111. package/src/runtime/engine-cyberia/docker-compose.yml +31 -37
  112. package/src/server/backup.js +1 -1
  113. package/src/server/conf.js +18 -108
  114. package/src/server/cron.js +249 -51
  115. package/src/server/dns.js +100 -6
  116. package/src/server/environment.js +98 -0
  117. package/src/server/forward-proxy.js +549 -0
  118. package/src/server/middlewares.js +56 -1
  119. package/src/server/process.js +0 -1
  120. package/src/server/selinux.js +185 -0
  121. package/src/server/systemd.js +205 -0
  122. package/src/server/underpost-compression.js +186 -0
  123. package/src/server/underpost-gateway.js +20 -10
  124. package/src/server/underpost-ingress.js +18 -2
  125. package/test/cyberia-instance-conf-defaults.test.js +1 -0
  126. package/test/selinux.test.js +71 -0
  127. package/test/underpost-gateway.test.js +41 -0
  128. package/test/underpost-ingress.test.js +52 -0
  129. package/test/wireguard-edge.test.js +1177 -0
package/src/cli/deploy.js CHANGED
@@ -10,7 +10,6 @@ import {
10
10
  buildProxyRouter,
11
11
  clusterTypeFactory,
12
12
  Config,
13
- cronDeployIdResolve,
14
13
  deployHostsFactory,
15
14
  deployRangePortFactory,
16
15
  gatewayApiEnabledFactory,
@@ -24,8 +23,11 @@ import {
24
23
  schedulableNodeFactory,
25
24
  trafficFromRoutingInfoFactory,
26
25
  } from '../server/conf.js';
26
+ import { cronDeployIdResolve } from '../server/cron.js';
27
27
  import { loggerFactory } from '../server/logger.js';
28
+ import { HOST_VOLUME_ROOT } from '../server/environment.js';
28
29
  import { shellExec } from '../server/process.js';
30
+ import { runSELinuxCommands, selinuxRestoreconCommandFactory } from '../server/selinux.js';
29
31
  import { INTERNAL_READY_PATH, INTERNAL_HEALTH_PATH } from '../server/runtime-status.js';
30
32
  import { staticContextRoutesFactory, statusPageRoutesFactory } from '../client-builder/client-build.js';
31
33
  import {
@@ -152,6 +154,13 @@ const gatewayDurationFactory = (value) => {
152
154
 
153
155
  const logger = loggerFactory(import.meta);
154
156
 
157
+ // hostPath volume trees are written under the operator's home directory, which
158
+ // carries a label no unprivileged container can read. Cluster bring-up registers
159
+ // the persistent mapping for HOST_VOLUME_ROOT; this applies it to what a deploy
160
+ // just wrote. A no-op where SELinux or its userspace is absent.
161
+ const restoreContainerContext = (path) =>
162
+ runSELinuxCommands([selinuxRestoreconCommandFactory(path)], { execute: shellExec });
163
+
155
164
  /**
156
165
  * @class UnderpostDeploy
157
166
  * @description Manages the deployment of applications and services.
@@ -724,7 +733,7 @@ ${Underpost.deploy
724
733
  if (!volume.claimName) continue;
725
734
  const pvcId = `${volume.claimName}-${deployId}-${env}-${deploymentVersion}`;
726
735
  const pvId = pvcId.replace(/^pvc-/, 'pv-');
727
- const hostPath = `/home/dd/engine/volume/${pvId}`;
736
+ const hostPath = `${HOST_VOLUME_ROOT}/${pvId}`;
728
737
  volumeYaml += `---\n${Underpost.deploy.persistentVolumeFactory({
729
738
  pvcId,
730
739
  namespace: options.namespace,
@@ -1697,13 +1706,13 @@ ${rules}`;
1697
1706
 
1698
1707
  /**
1699
1708
  * Node directory backing the static utility's volume, following the same
1700
- * `/home/dd/engine/volume/<pv>` convention as every other hostPath volume.
1709
+ * `HOST_VOLUME_ROOT/<pv>` convention as every other hostPath volume.
1701
1710
  * @param {object} [options] - Deploy/run options.
1702
1711
  * @returns {string} Absolute host path.
1703
1712
  * @memberof UnderpostDeploy
1704
1713
  */
1705
1714
  underpostGatewayRootFactory(options = {}) {
1706
- return options.underpostGatewayRoot || `/home/dd/engine/volume/${UNDERPOST_GATEWAY.volumeName}`;
1715
+ return options.underpostGatewayRoot || `${HOST_VOLUME_ROOT}/${UNDERPOST_GATEWAY.volumeName}`;
1707
1716
  },
1708
1717
 
1709
1718
  /**
@@ -1881,6 +1890,12 @@ ${rules}`;
1881
1890
  ? 'project'
1882
1891
  : null,
1883
1892
  });
1893
+ // The documents were just written under the operator's home tree, whose
1894
+ // policy label (user_home_t) the unprivileged gateway container cannot
1895
+ // read — it would answer 403 for every one of them. The persistent
1896
+ // mapping is registered at cluster bring-up; restore it here so files
1897
+ // this pass created carry it too.
1898
+ restoreContainerContext(hostRoot);
1884
1899
  logger.info('Static edge documents placed', {
1885
1900
  deployId,
1886
1901
  podName: podName || '(no running workload; placed from this checkout)',
@@ -2632,7 +2647,7 @@ EOF`);
2632
2647
  const clusterContext = options.clusterContext || 'kind';
2633
2648
  const pvcId = `${volume.claimName}-${deployId}-${env}-${version}`;
2634
2649
  const pvId = `${volume.claimName.replace('pvc-', 'pv-')}-${deployId}-${env}-${version}`;
2635
- const rootVolumeHostPath = `/home/dd/engine/volume/${pvId}`;
2650
+ const rootVolumeHostPath = `${HOST_VOLUME_ROOT}/${pvId}`;
2636
2651
  if (options.gitClean && volume.volumeMountPath) {
2637
2652
  Underpost.repo.clean({ paths: [volume.volumeMountPath] });
2638
2653
  }
@@ -2656,6 +2671,7 @@ EOF`);
2656
2671
  // Target node is the control plane / current host: write directly.
2657
2672
  if (!fs.existsSync(rootVolumeHostPath)) fs.mkdirSync(rootVolumeHostPath, { recursive: true });
2658
2673
  fs.copySync(volume.volumeMountPath, rootVolumeHostPath);
2674
+ restoreContainerContext(rootVolumeHostPath);
2659
2675
  } else {
2660
2676
  // Target node is remote: fs.copySync would only write the control-plane
2661
2677
  // filesystem, leaving the real node's hostPath empty. Ship the folder to
@@ -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.90
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
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,7 +10,6 @@ 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';
15
14
  import { crictlCommandFactory } from '../server/cri.js';
16
15
 
package/src/cli/index.js CHANGED
@@ -2,7 +2,8 @@ import dotenv from 'dotenv';
2
2
  import fs from 'fs-extra';
3
3
 
4
4
  import { Command } from 'commander';
5
- import { getNpmRootPath, getUnderpostRootPath, loadConf } from '../server/conf.js';
5
+ import { loadConf } from '../server/conf.js';
6
+ import { getNpmRootPath, getUnderpostRootPath } from '../server/environment.js';
6
7
  import { commitData } from '../client/components/core/CommonJs.js';
7
8
 
8
9
  import Underpost from '../index.js';
@@ -51,6 +52,7 @@ program
51
52
  .option('--merge-zip <build-prefix>', 'Merge split ZIP parts back into a single ZIP file for the given build prefix')
52
53
  .option('--lite-build', 'Skip full build (default is full build)')
53
54
  .option('--icons-build', 'Build icons')
55
+ .option('--ssr', 'Rebuild only SSR views defined in conf.ssr.json, leaving client assets untouched')
54
56
  .description('Builds client assets, single replicas, and/or syncs environment ports.')
55
57
  .action(Underpost.repo.client);
56
58
 
@@ -143,7 +145,7 @@ program
143
145
  '--switch-repo <url>',
144
146
  'Switches the git remote (origin) to <url> and force-pulls the target branch, overwriting the current working tree (discards local commits and tracked changes). Accepts a full URL or "owner/repo".',
145
147
  )
146
- .option('--target-branch <branch>', 'Target branch for --switch-repo (default: master).')
148
+ .option('--target-branch <branch>', 'Target branch for --switch-repo (default: remote default branch).')
147
149
  .description('Manages commits to a GitHub repository, supporting various commit types and options.')
148
150
  .action(Underpost.repo.commit);
149
151
 
@@ -254,6 +256,10 @@ program
254
256
  .option('--ban-egress-clear', 'Clears all banned egress IP addresses.')
255
257
  .option('--ban-both-add', 'Adds IP addresses to both banned ingress and egress lists.')
256
258
  .option('--ban-both-remove', 'Removes IP addresses from both banned ingress and egress lists.')
259
+ .option('--block-all-egress', 'Blocks all outbound traffic from this host (keeps established/related connections).')
260
+ .option('--unblock-all-egress', 'Unblocks all outbound traffic and restores default ACCEPT policy.')
261
+ .option('--block-all-ingress', 'Blocks all new inbound traffic to this host (keeps established/related connections).')
262
+ .option('--unblock-all-ingress', 'Unblocks all inbound traffic and restores default ACCEPT policy.')
257
263
  .option('--mac', 'Prints the MAC address of the main network interface.')
258
264
  .description('Displays the current public machine IP addresses.')
259
265
  .action(Underpost.dns.ipDispatcher);
@@ -592,19 +598,23 @@ program
592
598
 
593
599
  program
594
600
  .command('cron')
595
- .argument('[deploy-list]', 'A comma-separated list of deployment IDs (e.g., "default-a,default-b").')
601
+ .argument(
602
+ '[deploy-list]',
603
+ 'A comma-separated list of deployment IDs (e.g., "default-a,default-b"). In manifest modes its first entry is the manifest owner deploy-id.',
604
+ )
596
605
  .argument(
597
606
  '[job-list]',
598
- `A comma-separated list of job IDs. Options: ${Underpost.cron.getJobsIDs()}. Defaults to all available jobs.`,
607
+ `A comma-separated list of job IDs. Options: ${Underpost.cron.getJobsIDs()}. Defaults to all available jobs, and restricts which jobs are generated in manifest modes.`,
599
608
  )
600
609
  .option('--generate-k8s-cronjobs', 'Generates Kubernetes CronJob YAML manifests from cron configuration.')
601
- .option('--apply', 'Applies generated K8s CronJob manifests to the cluster via kubectl.')
610
+ .option('--apply', 'Generates and applies K8s CronJob manifests to the cluster via kubectl (never runs jobs).')
602
611
  .option(
603
- '--setup-start [deploy-id]',
604
- 'Updates deploy-id package.json start script and generates+applies its K8s CronJob manifests.',
612
+ '--setup-start',
613
+ 'Updates deploy-list package.json start script and generates+applies its K8s CronJob manifests.',
605
614
  )
606
615
  .option('--namespace <namespace>', 'Kubernetes namespace for the CronJob resources (default: "default").')
607
616
  .option('--image <image>', 'Custom container image for the CronJob pods.')
617
+ .option('--node-name <node-name>', 'Pins the CronJob pods to this node via a kubernetes.io/hostname nodeSelector.')
608
618
  .option('--git', 'Pass --git flag to cron job execution.')
609
619
  .option('--cmd <cmd>', 'Optional pre-script commands to run before cron execution.')
610
620
  .option('--dev', 'Use local ./ base path instead of global underpost installation.')
@@ -614,7 +624,7 @@ program
614
624
  .option('--dry-run', 'Preview cron jobs without executing them.')
615
625
  .option(
616
626
  '--create-job-now',
617
- 'After applying manifests, immediately create a Job from each CronJob (requires --apply).',
627
+ 'Creates a Job from each CronJob on the cluster now (implies manifest mode; combine with --apply to publish first).',
618
628
  )
619
629
  .description('Manages cron jobs: execute jobs directly or generate and apply K8s CronJob manifests.')
620
630
  .action(Underpost.cron.callback);
@@ -696,6 +706,108 @@ program
696
706
  .description('Manages SSH credentials and sessions for remote access to cluster nodes or services.')
697
707
  .action(Underpost.ssh.callback);
698
708
 
709
+ // `underpost wireguard` and `underpost haproxy` are two entrypoints onto one
710
+ // subsystem: the L3 transport and the gateway in front of it are configured from
711
+ // the same deploy state, so they share a single option set and a single action
712
+ // rather than drifting into two half-overlapping command surfaces.
713
+ const edgeCommandFactory = (name, description) =>
714
+ program
715
+ .command(name)
716
+ .option(
717
+ '--deploy-id <deploy-id>',
718
+ 'Deploy IDs whose conf.server.json/conf.instances.json define the routes. ' +
719
+ 'Accepts one id or a comma-separated list; defaults to "dd", every deploy in dd.router, ' +
720
+ 'because the edge holds one pair of map files for the whole cluster.',
721
+ )
722
+ .option('--interface <name>', 'WireGuard interface name (default: "wg0").')
723
+ .option('--wireguard-install', 'Installs the wireguard-tools, haproxy and iptables host packages.')
724
+ .option('--wireguard-setup', 'Generates keys, builds the interface config, and applies local network rules.')
725
+ .option('--server', 'Configures this node as the hub endpoint accepting inbound tunnel traffic.')
726
+ .option('--client', 'Configures this node as a spoke endpoint maintaining an outbound tunnel.')
727
+ .option('--port <port>', 'WireGuard UDP listening port (default: 51820).')
728
+ .option(
729
+ '--cidr <cidr>',
730
+ 'Hub interface address with prefix when used with --server (e.g. "10.0.0.1/24"); ' +
731
+ 'the overlay subnet a spoke routes back through the hub when used with --client (default: "10.0.0.0/24").',
732
+ )
733
+ .option('--peer-ip <ip>', 'Tunnel address of the target spoke. Required with --client and --peer-add.')
734
+ .option('--endpoint <host:port>', 'Hub host and port a spoke dials. Required with --client.')
735
+ .option('--public-key <key>', 'Hub public key with --client; spoke public key with --peer-add.')
736
+ .option('--peer-add <peer-id>', 'Registers a spoke and applies it to the running hub without a restart.')
737
+ .option('--peer-remove <peer-id>', 'Removes a spoke from the registry and from the running hub.')
738
+ .option('--allowed-ips <cidrs>', 'Comma-separated CIDRs routed to the spoke (e.g. "10.0.0.2/32,192.168.10.0/24").')
739
+ .option('--hosts <hosts>', 'Comma-separated hostnames bound to the spoke, overriding instance resolution.')
740
+ .option('--instances <instances>', 'Comma-separated conf.instances.json ids bound to the spoke.')
741
+ .option('--default', 'Marks the spoke as the fallback for hostnames that match no other binding.')
742
+ .option('--haproxy-setup', 'Installs HAProxy, publishes the current routes, and enables the daemon.')
743
+ .option('--haproxy-sync', 'Recompiles the SNI/Host maps from deploy config and hot-reloads HAProxy.')
744
+ .option(
745
+ '--status',
746
+ 'Prints the whole edge context without changing anything: role, interface, tunnel address, ' +
747
+ 'public key, daemon states, peers with their bindings and link health, and the resolved routing.',
748
+ )
749
+ .option(
750
+ '--build-conf',
751
+ 'Writes only engine-private/deploy/conf.wireguard.json and touches no host state. ' +
752
+ 'Combine with --wireguard-setup / --peer-add / --peer-remove to author the topology off-box; ' +
753
+ 'alone it normalizes and validates the existing registry.',
754
+ )
755
+ .option(
756
+ '--forward-proxy-server',
757
+ 'Ensures the hub HTTP/CONNECT forward proxy runs as the underpost-forward-proxy systemd service, ' +
758
+ 'bound to the tunnel address only (default port 1080), and returns. Authenticates every request with ' +
759
+ 'FORWARD_PROXY_API_KEY, so spokes can reach the internet through the VPS public address. Idempotent: ' +
760
+ 're-running converges on the one service and restarts it only when the unit actually changed.',
761
+ )
762
+ .option(
763
+ '--forward-proxy-server-host <host>',
764
+ 'Address the forward proxy binds, overriding the hub tunnel address from the registry.',
765
+ )
766
+ .option('--forward-proxy-server-port <port>', 'Port the forward proxy binds (default: 1080).')
767
+ .option(
768
+ '--ssh-forward-port <port>',
769
+ 'Publishes the default spoke SSH port on this public TCP port of the hub, so CI with no fixed ' +
770
+ 'address can reach the cluster node (e.g. 2222). "0" closes it. Stored in the registry.',
771
+ )
772
+ .option('--wireguard-start', 'Enables and starts wg-quick@<interface> and the QUIC forward.')
773
+ .option('--wireguard-stop', 'Tears down the interface and removes its transient packet rules.')
774
+ .option('--wireguard-reset', 'Removes generated configs and packet rules, keeping the key pair and registry.')
775
+ .option('--wireguard-reinstall', 'Full purge, package reinstall and re-key; every spoke must re-register.')
776
+ .option('--dry-run', 'Prints the files and commands the run would apply, without touching the host.')
777
+ .description(description)
778
+ .action(Underpost.wireguard.callback);
779
+
780
+ edgeCommandFactory(
781
+ 'wireguard',
782
+ 'Manages the WireGuard L3 hub-and-spoke transport and the HAProxy edge gateway in front of it.',
783
+ );
784
+ edgeCommandFactory(
785
+ 'haproxy',
786
+ 'Manages the HAProxy edge gateway over the WireGuard transport (same subsystem as `underpost wireguard`).',
787
+ );
788
+
789
+ program
790
+ .command('vultr')
791
+ .argument('[deploy-list]', 'A comma-separated list of deployment IDs, logged for attribution.')
792
+ .option('--instance-id <instance-id>', 'Vultr instance id to meter (default: VULTR_INSTANCE_ID).')
793
+ .option('--api-key <api-key>', 'Vultr API key (default: VULTR_API_KEY). Prefer the environment over this flag.')
794
+ .option(
795
+ '--threshold <ratio>',
796
+ 'Fraction of the plan quota that triggers the egress block; "0.80" and "80" are both accepted (default: 0.80).',
797
+ )
798
+ .option('--metric <metric>', '"total" (incoming + outgoing, default) or "outgoing" for egress alone.')
799
+ .option('--month <yyyy-mm>', 'Billing month to sum (default: the current UTC month).')
800
+ .option('--all-dates', 'Sum every daily bucket the API returns instead of scoping to one month.')
801
+ .option('--host <ip>', 'Edge VPS to block (default: VULTR_VPS_IP, then DEFAULT_SSH_HOST).')
802
+ .option('--user <user>', 'SSH user on the edge VPS (default: VULTR_SSH_USER, then DEFAULT_SSH_USER, then "root").')
803
+ .option('--key-path <path>', 'SSH private key (default: VULTR_SSH_KEY_PATH, then DEFAULT_SSH_KEY_PATH).')
804
+ .option('--port <port>', 'SSH port on the edge VPS (default: VULTR_SSH_PORT, then DEFAULT_SSH_PORT, then 22).')
805
+ .option('--force', 'Re-apply the egress block even if it was already applied for this cycle.')
806
+ .option('--auto-unblock', 'Restore egress automatically once consumption falls back under the threshold.')
807
+ .option('--dry-run', 'Reports the consumption and the action it would take, without touching the edge VPS.')
808
+ .description('Meters the edge VPS bandwidth against its Vultr plan quota and blocks egress before overage accrues.')
809
+ .action(Underpost.vultr.callback);
810
+
699
811
  program
700
812
  .command('run')
701
813
  .argument('<runner-id>', `The runner ID to run. Options: ${Underpost.run.RUNNERS}.`)
@@ -769,7 +881,7 @@ program
769
881
  .option('--cmd-cron-jobs <cmd-cron-jobs>', 'Pre-script commands to run before cron job execution.')
770
882
  .option(
771
883
  '--deploy-id-cron-jobs <deploy-id-cron-jobs>',
772
- 'Specifies deployment IDs to synchronize cron jobs with during execution.',
884
+ 'Cron deploy-id to set up during sync; defaults to dd.cron, "none" skips cron setup entirely.',
773
885
  )
774
886
  .option('--timezone <timezone>', 'Sets the timezone for the runner execution.')
775
887
  .option('--kubeadm', 'Sets the kubeadm cluster context for the runner execution.')
package/src/cli/lxd.js CHANGED
@@ -5,7 +5,7 @@
5
5
  *
6
6
  */
7
7
 
8
- import { getNpmRootPath } from '../server/conf.js';
8
+ import { getNpmRootPath } from '../server/environment.js';
9
9
  import { pbcopy, shellExec } from '../server/process.js';
10
10
  import walk from 'ignore-walk';
11
11
  import fs from 'fs-extra';
@@ -8,10 +8,10 @@ import {
8
8
  loadReplicas,
9
9
  pathPortAssignmentFactory,
10
10
  loadConfServerJson,
11
- loadCronDeployEnv,
12
11
  etcHostFactory,
13
12
  deployRangePortFactory,
14
13
  } from '../server/conf.js';
14
+ import { loadCronDeployEnv } from '../server/cron.js';
15
15
  import { loggerFactory } from '../server/logger.js';
16
16
  import { timer } from '../client/components/core/CommonJs.js';
17
17
  import {
@@ -56,6 +56,10 @@ const buildVersionBumpTargets = () => [
56
56
  file: 'src/index.js',
57
57
  patterns: /(static version = ['"]v)\d+\.\d+\.\d+(?=['"])/g,
58
58
  },
59
+ {
60
+ file: 'src/cli/run.js',
61
+ patterns: /(const version = ['"]v)\d+\.\d+\.\d+(?=['"])/g,
62
+ },
59
63
 
60
64
  // ── README + CLI-HELP fallbacks. CLI-HELP is regenerated by `deploy cli-docs`, but bump
61
65
  // it too so the file is consistent if the regeneration step is skipped. ──
@@ -107,6 +111,14 @@ const buildVersionBumpTargets = () => [
107
111
  recursive: true,
108
112
  },
109
113
 
114
+ // ── Deployment runner scripts carry image defaults passed to deploy/sync commands. ──
115
+ {
116
+ dir: 'deploy',
117
+ match: /\.sh$/,
118
+ patterns: /(underpost\/[a-z0-9-]+:v)\d+\.\d+\.\d+/g,
119
+ recursive: true,
120
+ },
121
+
110
122
  // ── GitHub Actions workflows. Single sweep across docker-image.*.ci.yml, engine-*.cd.yml,
111
123
  // and the root docker-image.ci.yml — replaces previous three separate loops. ──
112
124
  {
@@ -261,6 +273,19 @@ const bumpAuxiliaryFiles = (oldVersion, newVersion, { dryRun = false } = {}) =>
261
273
  return report;
262
274
  };
263
275
 
276
+ /**
277
+ * Returns whether a manifest's canonical top-level version would change. Used by dry-runs
278
+ * because bumpp has no non-mutating API option.
279
+ */
280
+ const wouldBumpManifestVersion = (filePath, newVersion) => {
281
+ try {
282
+ const manifest = JSON.parse(fs.readFileSync(filePath, 'utf8'));
283
+ return typeof manifest.version === 'string' && manifest.version !== newVersion;
284
+ } catch {
285
+ return false;
286
+ }
287
+ };
288
+
264
289
  /**
265
290
  * Prints a per-file change summary table.
266
291
  */
@@ -379,8 +404,8 @@ class UnderpostRelease {
379
404
  * 1. Loads production env and pulls latest code; kills dev servers on ports 4001-4003.
380
405
  * 2. Builds and smoke-tests the pwa-microservices-template (aborts on error).
381
406
  * 3. Canonical version files: delegates to `bumpp` (driven by `bump.config.js`). Bumps
382
- * package.json, package-lock.json (root + packages['']), and every
383
- * engine-private/conf/**\/package.json.
407
+ * package.json, package-lock.json (root + packages['']), Hardhat's package metadata,
408
+ * and every engine-private/conf/**\/package.json.
384
409
  * 4. Auxiliary files: anchored-regex walker over VERSION_BUMP_TARGETS — covers
385
410
  * src/index.js, README, cyberia-docs, nexodev docs, manifests (deployment + cronjobs),
386
411
  * .github/workflows (image tags, type=raw, UNDERPOST_VERSION build-args), and
@@ -420,26 +445,36 @@ class UnderpostRelease {
420
445
  if (!templateOk) return;
421
446
  }
422
447
 
423
- // ── Canonical version files: delegate to bumpp (package.json, package-lock.json,
424
- // engine-private/conf/**/package.json). bumpp owns lockfile semantics (root version
425
- // + packages[''].version) so we don't reimplement them. ──
426
- const { versionBump } = await import('bumpp');
427
- const bumppResult = await versionBump({
428
- release: newVersion,
429
- files: [
430
- 'package.json',
431
- 'package-lock.json',
432
- ...(fs.existsSync('./engine-private/conf') ? ['engine-private/conf/**/package.json'] : []),
433
- ],
434
- commit: false,
435
- tag: false,
436
- push: false,
437
- confirm: false,
438
- recursive: false,
439
- ignoreScripts: true,
440
- dry: dryRun,
441
- });
442
- logger.info(`bumpp updated ${bumppResult?.files?.length ?? 0} canonical file(s).`);
448
+ // ── Canonical version files: bumpp owns lockfile semantics (root version +
449
+ // packages[''].version). Its API has no non-mutating dry-run, so previews inspect
450
+ // manifest versions directly rather than invoking it. ──
451
+ const canonicalVersionFiles = [
452
+ 'package.json',
453
+ 'package-lock.json',
454
+ 'hardhat/package.json',
455
+ 'hardhat/package-lock.json',
456
+ ...walkDir('engine-private/conf', /^package\.json$/, true),
457
+ ];
458
+ if (dryRun) {
459
+ const pendingCanonicalFiles = canonicalVersionFiles.filter((file) =>
460
+ wouldBumpManifestVersion(file, newVersion),
461
+ );
462
+ logger.info(`bumpp would update ${pendingCanonicalFiles.length} canonical file(s).`);
463
+ for (const file of pendingCanonicalFiles) console.log(` ${file}`);
464
+ } else {
465
+ const { versionBump } = await import('bumpp');
466
+ const bumppResult = await versionBump({
467
+ release: newVersion,
468
+ files: canonicalVersionFiles,
469
+ commit: false,
470
+ tag: false,
471
+ push: false,
472
+ confirm: false,
473
+ recursive: false,
474
+ ignoreScripts: true,
475
+ });
476
+ logger.info(`bumpp updated ${bumppResult?.updatedFiles?.length ?? 0} canonical file(s).`);
477
+ }
443
478
 
444
479
  // ── Auxiliary files: anchored-regex walker over VERSION_BUMP_TARGETS. ──
445
480
  const report = bumpAuxiliaryFiles(version, newVersion, { dryRun });
@@ -11,7 +11,6 @@ import { actionInitLog, loggerFactory } from '../server/logger.js';
11
11
  import path from 'path';
12
12
  import fs from 'fs-extra';
13
13
  import {
14
- getNpmRootPath,
15
14
  Config,
16
15
  loadConf,
17
16
  readConfJson,
@@ -20,9 +19,9 @@ import {
20
19
  loadConfServerJson,
21
20
  getDataDeploy,
22
21
  buildReplicaId,
23
- writeEnv,
24
22
  readConfInstances,
25
23
  } from '../server/conf.js';
24
+ import { getNpmRootPath, writeEnv } from '../server/environment.js';
26
25
  import { buildClient, unzipClientBuild, mergeClientBuildZip } from '../client-builder/client-build.js';
27
26
  import { DefaultConf } from '../../conf.js';
28
27
  import Underpost from '../index.js';
@@ -109,7 +108,7 @@ class UnderpostRepository {
109
108
  * @param {boolean} [options.changelogNoHash=false] - If true, omits commit hashes from the changelog entries.
110
109
  * @param {boolean} [options.remoteUrl=false] - If true, prints the current git remote URL (origin) in plain text and returns.
111
110
  * @param {string} [options.switchRepo=''] - If set, switches the remote `origin` to this URL and force-pulls the target branch, overwriting the current working tree.
112
- * @param {string} [options.targetBranch=''] - Target branch for `switchRepo` (defaults to `master`).
111
+ * @param {string} [options.targetBranch=''] - Target branch for `switchRepo` (defaults to the remote's default branch).
113
112
  * @memberof UnderpostRepository
114
113
  */
115
114
  commit(
@@ -181,7 +180,7 @@ class UnderpostRepository {
181
180
  Underpost.repo.switchRemote({
182
181
  path: repoPath,
183
182
  url: options.switchRepo,
184
- branch: options.targetBranch || 'master',
183
+ branch: options.targetBranch,
185
184
  });
186
185
  return;
187
186
  }
@@ -906,6 +905,7 @@ class UnderpostRepository {
906
905
  split: options.split || '',
907
906
  fullBuild: options.liteBuild ? false : true,
908
907
  iconsBuild: options.iconsBuild || false,
908
+ ssrOnly: options.ssr || false,
909
909
  });
910
910
  for (const replicaDeployId of deployIdSingleReplicas) {
911
911
  if (!fs.existsSync(`./engine-private/replica/${replicaDeployId}`)) {
@@ -917,6 +917,7 @@ class UnderpostRepository {
917
917
  split: options.split || '',
918
918
  liteBuild: options.liteBuild || false,
919
919
  iconsBuild: options.iconsBuild || false,
920
+ ssr: options.ssr || false,
920
921
  });
921
922
  }
922
923
 
@@ -1428,14 +1429,15 @@ Prevent build private config repo.`,
1428
1429
  * @param {object} opts
1429
1430
  * @param {string} opts.url - New remote URL (full URL or "owner/repo" short form).
1430
1431
  * @param {string} [opts.path='.'] - Path to the git repository.
1431
- * @param {string} [opts.branch='master'] - Target branch to overwrite the current tree with.
1432
+ * @param {string} [opts.branch] - Target branch to overwrite the current tree with. Defaults to the remote's default branch.
1432
1433
  * @param {string} [opts.remote='origin'] - Remote name to set and fetch from.
1433
1434
  * @returns {void}
1434
1435
  * @memberof UnderpostRepository
1435
1436
  */
1436
- switchRemote({ url, path: repoPath = '.', branch = 'master', remote = 'origin' }) {
1437
+ switchRemote({ url, path: repoPath = '.', branch = '', remote = 'origin' }) {
1437
1438
  if (!url) throw new Error('switchRemote requires a target remote url');
1438
1439
  if (!fs.existsSync(`${repoPath}/.git`)) throw new Error(`switchRemote: not a git repository: ${repoPath}`);
1440
+ const targetBranch = branch || Underpost.repo.getDefaultBranch(url);
1439
1441
  // Token-free URL for the stored remote; auth-injected URL only for the fetch.
1440
1442
  let normalized = url;
1441
1443
  if (!url.startsWith('http://') && !url.startsWith('https://') && !url.startsWith('git@')) {
@@ -1445,12 +1447,12 @@ Prevent build private config repo.`,
1445
1447
  const current = Underpost.repo.getRemoteUrl({ path: repoPath, remote });
1446
1448
  if (!current) shellExec(`cd "${repoPath}" && git remote add ${remote} "${normalized}"`);
1447
1449
  else shellExec(`cd "${repoPath}" && git remote set-url ${remote} "${normalized}"`);
1448
- logger.info('switchRemote', { path: repoPath, remote, branch, url: normalized });
1449
- shellExec(`cd "${repoPath}" && GIT_TERMINAL_PROMPT=0 git fetch --force "${authUrl}" ${branch}`);
1450
+ logger.info('switchRemote', { path: repoPath, remote, branch: targetBranch, url: normalized });
1451
+ shellExec(`cd "${repoPath}" && GIT_TERMINAL_PROMPT=0 git fetch --force "${authUrl}" ${targetBranch}`);
1450
1452
  // reset --hard first clears the worktree so the checkout cannot be blocked
1451
1453
  // by conflicting local changes; -B points the target branch at the fetched tip.
1452
1454
  shellExec(`cd "${repoPath}" && git reset --hard FETCH_HEAD`);
1453
- shellExec(`cd "${repoPath}" && git checkout -B ${branch} FETCH_HEAD`);
1455
+ shellExec(`cd "${repoPath}" && git checkout -B ${targetBranch} FETCH_HEAD`);
1454
1456
  },
1455
1457
 
1456
1458
  /**
package/src/cli/run.js CHANGED
@@ -4,13 +4,12 @@
4
4
  * @namespace UnderpostRun
5
5
  */
6
6
 
7
- import { daemonProcess, getTerminalPid, pbcopy, shellCd, shellExec } from '../server/process.js';
7
+ import { daemonProcess, pbcopy, shellCd, shellExec } from '../server/process.js';
8
8
  import {
9
9
  awaitDeployMonitor,
10
10
  buildKindPorts,
11
11
  clusterContextFactory,
12
12
  clusterTypeFactory,
13
- cronDeployIdResolve,
14
13
  dispatchBuildInstanceEnv,
15
14
  deployHostsFactory,
16
15
  etcHostFactory,
@@ -21,7 +20,6 @@ import {
21
20
  exposeTcpPortsFactory,
22
21
  gatewayApiEnabledFactory,
23
22
  generateSecurePassword,
24
- getNpmRootPath,
25
23
  instanceHttpRouteRulesFactory,
26
24
  instanceInterceptStatusesFactory,
27
25
  instancePortFactory,
@@ -36,7 +34,6 @@ import {
36
34
  resolveEnvScoped,
37
35
  selectConfInstances,
38
36
  waitForPort,
39
- writeEnv,
40
37
  clusterInstancesFactory,
41
38
  deployTrafficEntriesFactory,
42
39
  curlStatusChainFactory,
@@ -49,6 +46,8 @@ import {
49
46
  stopPlanFactory,
50
47
  trafficFromRoutingInfoFactory,
51
48
  } from '../server/conf.js';
49
+ import { cronDeployIdResolve } from '../server/cron.js';
50
+ import { getNpmRootPath, writeEnv } from '../server/environment.js';
52
51
  import { actionInitLog, loggerFactory } from '../server/logger.js';
53
52
 
54
53
  import fs from 'fs-extra';
@@ -127,7 +126,7 @@ const logger = loggerFactory(import.meta);
127
126
  * @property {string} confServerPath - The configuration server path.
128
127
  * @property {string} underpostRoot - The root path of the Underpost installation.
129
128
  * @property {string} cmdCronJobs - Pre-script commands to run before cron job execution.
130
- * @property {string} deployIdCronJobs - The deployment ID for cron jobs.
129
+ * @property {string} deployIdCronJobs - Cron deploy-id passed to `cron --setup-start`; unset resolves dd.cron, `none` skips cron setup.
131
130
  * @property {string} timezone - The timezone to set.
132
131
  * @property {boolean} kubeadm - Whether to run in kubeadm mode.
133
132
  * @property {boolean} kind - Whether to run in kind mode.
@@ -1172,8 +1171,16 @@ echo -e "[code]\nname=Visual Studio Code\nbaseurl=https://packages.microsoft.com
1172
1171
  if (!validVersion) throw new Error('Version mismatch');
1173
1172
  }
1174
1173
  if (options.timezone !== 'none') shellExec(`${baseCommand} run${baseClusterCommand} tz`);
1175
- if (options.deployIdCronJobs !== 'none')
1176
- shellExec(`node bin cron${baseClusterCommand}${clusterFlag} --setup-start --git --apply`);
1174
+ if (options.deployIdCronJobs !== 'none') {
1175
+ // --deploy-id-cron-jobs overrides the cron deploy-id; unset falls back to
1176
+ // dd.cron, the same source `cron --setup-start` resolves against on its own.
1177
+ const cronDeployId = options.deployIdCronJobs || cronDeployIdResolve() || '';
1178
+ if (cronDeployId && !/^[a-zA-Z0-9._,-]+$/.test(cronDeployId))
1179
+ throw new Error(`Invalid cron deploy-id: ${cronDeployId}`);
1180
+ shellExec(
1181
+ `node bin cron${cronDeployId ? ` ${cronDeployId}` : ''}${baseClusterCommand}${clusterFlag} --setup-start --git --apply`,
1182
+ );
1183
+ }
1177
1184
  }
1178
1185
 
1179
1186
  const currentTraffic = isDeployRunnerContext(path, options)
@@ -1220,7 +1227,7 @@ echo -e "[code]\nname=Visual Studio Code\nbaseurl=https://packages.microsoft.com
1220
1227
  // runner. Build the host-side SSR documents before generating routes unless
1221
1228
  // the caller explicitly selected a pre-built bundle workflow.
1222
1229
  if (isDeployRunnerContext(path, options) && !options.skipFullBuild)
1223
- shellExec(`${baseCommand} client ${deployId} ${env}`);
1230
+ shellExec(`${baseCommand} client ${deployId} ${env} --ssr`);
1224
1231
 
1225
1232
  shellExec(
1226
1233
  `${baseCommand} deploy${clusterFlag} --build-manifest --sync --info-router --replicas ${replicas} --node ${node}${
@@ -3090,7 +3097,7 @@ EOF`);
3090
3097
  const hostListenResult = etcHostFactory(hosts);
3091
3098
  logger.info(hostListenResult.renderHosts);
3092
3099
  }
3093
- const version = 'v3.2.90';
3100
+ const version = 'v3.3.0';
3094
3101
  const instanceOptionsFactory = (deployId, instanceId) => ({
3095
3102
  ...options,
3096
3103
  ...clusterContextFactory(clusterType),
@@ -4906,6 +4913,26 @@ EOF`;
4906
4913
  }
4907
4914
  },
4908
4915
 
4916
+ /**
4917
+ * @method kubeadm-wireguard
4918
+ * @description
4919
+ * Configures Calico to keep using the Kubernetes NodeInternalIP after WireGuard
4920
+ * is added to the node. This prevents the WireGuard interface (for example
4921
+ * 10.0.0.2) from being selected as the Calico/BGP node address.
4922
+ *
4923
+ * @param {string} path - Unused.
4924
+ * @param {UnderpostRunDefaultOptions} options - The default underpost runner options.
4925
+ * @memberof UnderpostRun
4926
+ */
4927
+ 'kubeadm-wireguard': (path, options = DEFAULT_OPTION) => {
4928
+ shellExec(`kubectl patch installation.operator.tigera.io default --type='json' \
4929
+ -p='[
4930
+ {"op":"replace","path":"/spec/calicoNetwork/nodeAddressAutodetectionV4","value":{"kubernetes":"NodeInternalIP"}}
4931
+ ]'`);
4932
+
4933
+ shellExec(`kubectl rollout restart daemonset/calico-node -n calico-system`);
4934
+ },
4935
+
4909
4936
  /**
4910
4937
  * @method build-cluster-deployment-manifests
4911
4938
  * @description Builds deployment manifests for both production and development environments using `node bin deploy --build-manifest`, syncing them, and setting replicas to 1 for the `dd` deployment.