underpost 3.2.22 → 3.2.30

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 (57) hide show
  1. package/.github/workflows/ghpkg.ci.yml +1 -1
  2. package/.github/workflows/gitlab.ci.yml +1 -1
  3. package/.github/workflows/npmpkg.ci.yml +1 -1
  4. package/.github/workflows/publish.ci.yml +2 -2
  5. package/.github/workflows/pwa-microservices-template-page.cd.yml +1 -1
  6. package/.github/workflows/pwa-microservices-template-test.ci.yml +1 -1
  7. package/CHANGELOG.md +181 -1
  8. package/CLI-HELP.md +61 -2
  9. package/README.md +3 -2
  10. package/baremetal/commission-workflows.json +1 -0
  11. package/bin/build.js +1 -0
  12. package/docker-compose.yml +224 -0
  13. package/manifests/cronjobs/dd-cron/dd-cron-backup.yaml +1 -1
  14. package/manifests/cronjobs/dd-cron/dd-cron-dns.yaml +1 -1
  15. package/manifests/deployment/dd-default-development/deployment.yaml +2 -2
  16. package/package.json +27 -15
  17. package/scripts/kubeadm-node-setup.sh +317 -0
  18. package/scripts/rocky-kickstart.sh +877 -185
  19. package/scripts/rpmfusion-ffmpeg-setup.sh +26 -50
  20. package/scripts/test-monitor.sh +3 -5
  21. package/src/cli/baremetal.js +717 -16
  22. package/src/cli/cluster.js +3 -1
  23. package/src/cli/deploy.js +118 -9
  24. package/src/cli/docker-compose.js +591 -0
  25. package/src/cli/env.js +11 -2
  26. package/src/cli/fs.js +35 -11
  27. package/src/cli/index.js +89 -0
  28. package/src/cli/kickstart.js +142 -20
  29. package/src/cli/repository.js +153 -11
  30. package/src/cli/run.js +285 -8
  31. package/src/cli/secrets.js +7 -2
  32. package/src/cli/ssh.js +234 -0
  33. package/src/cli/static.js +2 -2
  34. package/src/{server → client-builder}/client-build-docs.js +15 -5
  35. package/src/{server → client-builder}/client-build-live.js +3 -3
  36. package/src/{server → client-builder}/client-build.js +25 -22
  37. package/src/{server → client-builder}/client-dev-server.js +3 -3
  38. package/src/{server → client-builder}/client-icons.js +2 -2
  39. package/src/{server → client-builder}/ssr.js +5 -5
  40. package/src/client.build.js +1 -3
  41. package/src/client.dev.js +1 -1
  42. package/src/db/mongo/MongoBootstrap.js +12 -12
  43. package/src/index.js +12 -1
  44. package/src/mailer/EmailRender.js +1 -1
  45. package/src/{server → projects/underpost}/catalog-underpost.js +1 -2
  46. package/src/runtime/express/Express.js +2 -2
  47. package/src/runtime/nginx/Nginx.js +250 -0
  48. package/src/server/catalog.js +7 -14
  49. package/src/server/conf.js +3 -3
  50. package/src/server/runtime-status.js +18 -1
  51. package/src/server/start.js +12 -2
  52. package/src/server.js +6 -2
  53. package/test/deploy-monitor.test.js +26 -10
  54. package/typedoc.json +3 -1
  55. package/src/server/ipfs-client.js +0 -599
  56. /package/src/client/ssr/{Render.js → RootDocument.js} +0 -0
  57. /package/src/{server → client-builder}/client-formatted.js +0 -0
@@ -691,7 +691,9 @@ EOF
691
691
  // shellExec(
692
692
  // `sudo sed -i '/SystemdCgroup = true/a selinux_disabled = true' /etc/containerd/config.toml`,
693
693
  // );
694
- shellExec(`sudo service docker restart`); // Restart docker after containerd config changes
694
+ // Restart docker after containerd config changes. Rocky 9 uses systemctl,
695
+ // not the legacy service command.
696
+ shellExec(`sudo systemctl restart docker || sudo service docker restart || true`);
695
697
  shellExec(`sudo systemctl enable --now containerd.service`);
696
698
  shellExec(`sudo systemctl restart containerd`); // Restart containerd to apply changes
697
699
 
package/src/cli/deploy.js CHANGED
@@ -383,6 +383,10 @@ spec:
383
383
  * @param {string} [options.imagePullPolicy] - Container imagePullPolicy override (`Always`, `IfNotPresent`, `Never`); forwarded to deploymentYamlPartsFactory. Defaults to `Never` for `localhost/` images and `IfNotPresent` otherwise.
384
384
  * @param {boolean} [options.disableRuntimeProbes] - Omit internal-status HTTP probes from generated manifests. When true no readiness/liveness/startup probes are emitted.
385
385
  * @param {boolean} [options.tcpProbes] - Emit legacy TCP socket probes instead of HTTP internal-status probes (migration path).
386
+ * @param {string} [options.node] - Explicit target node for hostPath PV nodeAffinity pinning; resolved through {@link UnderpostDeploy.resolveDeployNode} together with the cluster flags.
387
+ * @param {boolean} [options.kind] - Kind cluster context; affects the cluster-type node default when no explicit node is set.
388
+ * @param {boolean} [options.kubeadm] - Kubeadm cluster context; affects the cluster-type node default when no explicit node is set.
389
+ * @param {boolean} [options.k3s] - K3s cluster context; affects the cluster-type node default when no explicit node is set.
386
390
  * @returns {Promise<void>} - Promise that resolves when the manifest is built.
387
391
  * @memberof UnderpostDeploy
388
392
  */
@@ -455,6 +459,15 @@ ${Underpost.deploy
455
459
  ? JSON.parse(fs.readFileSync(`./engine-private/conf/${deployId}/conf.volume.json`, 'utf8'))
456
460
  : [];
457
461
  if (confVolume.length > 0) {
462
+ // Mirror deployVolume's data-node resolution so the generated manifest
463
+ // pins the PV to the same node that physically receives the volume data.
464
+ const pvDataNode = Underpost.deploy.resolveDeployNode({
465
+ node: options.node,
466
+ kind: options.kind,
467
+ kubeadm: options.kubeadm,
468
+ k3s: options.k3s,
469
+ env,
470
+ });
458
471
  let volumeYaml = '';
459
472
  for (const deploymentVersion of deploymentVersions) {
460
473
  for (const volume of confVolume) {
@@ -466,6 +479,7 @@ ${Underpost.deploy
466
479
  pvcId,
467
480
  namespace: options.namespace,
468
481
  hostPath,
482
+ nodeName: pvDataNode,
469
483
  })}\n`;
470
484
  }
471
485
  }
@@ -720,7 +734,8 @@ spec:
720
734
  * @param {string} options.image - Docker image for the deployment.
721
735
  * @param {string} options.traffic - Traffic status for the deployment.
722
736
  * @param {string} options.replicas - Number of replicas for the deployment.
723
- * @param {string} options.node - Node name for resource allocation.
737
+ * @param {string} options.node - Explicit target node (highest precedence in the node chain). When empty, {@link UnderpostDeploy.resolveDeployNode} falls back to the cluster-type default (`kind-worker` for kind, host for kubeadm/k3s). Used for both volume placement and hostPath PV nodeAffinity.
738
+ * @param {string} [options.sshKeyPath] - Private key path for node SSH operations, forwarded to deployVolume when shipping a hostPath volume to a remote target node over SSH. Defaults to engine-private/deploy/id_rsa.
724
739
  * @param {boolean} options.disableUpdateDeployment - Whether to disable deployment updates.
725
740
  * @param {boolean} options.disableUpdateProxy - Whether to disable proxy updates.
726
741
  * @param {boolean} options.disableDeploymentProxy - Whether to disable deployment proxy.
@@ -938,9 +953,16 @@ EOF`);
938
953
  env,
939
954
  version,
940
955
  namespace,
941
- nodeName: options.node ? options.node : env === 'development' ? 'kind-worker' : os.hostname(),
956
+ nodeName: Underpost.deploy.resolveDeployNode({
957
+ node: options.node,
958
+ kind: options.kind,
959
+ kubeadm: options.kubeadm,
960
+ k3s: options.k3s,
961
+ env,
962
+ }),
942
963
  clusterContext: options.k3s ? 'k3s' : options.kubeadm ? 'kubeadm' : 'kind',
943
964
  gitClean: options.gitClean || false,
965
+ sshKeyPath: options.sshKeyPath || '',
944
966
  });
945
967
  }
946
968
 
@@ -969,8 +991,12 @@ EOF`);
969
991
  if (
970
992
  Underpost.deploy.isValidTLSContext({ host: Object.keys(confServer)[0], env, options }) &&
971
993
  !options.selfSigned
972
- )
973
- shellExec(`sudo kubectl apply -f ./${manifestsPath}/secret.yaml -n ${namespace}`);
994
+ ) {
995
+ const secretPath = `./${manifestsPath}/secret.yaml`;
996
+ if (fs.existsSync(secretPath) && fs.readFileSync(secretPath, 'utf8').trim()) {
997
+ shellExec(`sudo kubectl apply -f ${secretPath} -n ${namespace}`);
998
+ } else logger.info('Skipping secret.yaml apply (no objects yet; applied by the --cert step)');
999
+ }
974
1000
  }
975
1001
  }
976
1002
  },
@@ -1032,6 +1058,37 @@ EOF`);
1032
1058
  Underpost.env.set(`${deployId}-${env}-traffic`, targetTraffic);
1033
1059
  },
1034
1060
 
1061
+ /**
1062
+ * Resolves the effective target node for a deployment, applying a single
1063
+ * precedence chain shared by every deploy workflow — the default `deploy`
1064
+ * callback, `run sync`, and custom `run instance` — so node customization
1065
+ * behaves identically everywhere:
1066
+ *
1067
+ * 1. **Explicit node** — `node` (the resolved `--node` value). Upstream
1068
+ * runners derive it from the comma-path field or `--node-name`
1069
+ * (`run sync`: `path.split(',')[4]` > `--node-name` > default) and from
1070
+ * `--node-name` directly (`run instance`).
1071
+ * 2. **Cluster-type default** — when no explicit node is given: `kind-worker`
1072
+ * for a kind cluster (the node that hosts kind hostPath volumes),
1073
+ * otherwise the control-plane / current host (`os.hostname()`) for
1074
+ * kubeadm / k3s. With no explicit cluster flag, `development` is treated
1075
+ * as kind and `production` as the host, preserving legacy behaviour.
1076
+ *
1077
+ * @param {object} params
1078
+ * @param {string} [params.node=''] - Explicit node (`--node`); highest precedence.
1079
+ * @param {boolean} [params.kind=false] - Kind cluster context.
1080
+ * @param {boolean} [params.kubeadm=false] - Kubeadm cluster context.
1081
+ * @param {boolean} [params.k3s=false] - K3s cluster context.
1082
+ * @param {string} [params.env=''] - Deployment environment; tie-breaker when no cluster flag is set.
1083
+ * @returns {string} The effective node name.
1084
+ * @memberof UnderpostDeploy
1085
+ */
1086
+ resolveDeployNode({ node = '', kind = false, kubeadm = false, k3s = false, env = '' } = {}) {
1087
+ if (node) return node;
1088
+ const isKind = kind || (!kubeadm && !k3s && env !== 'production');
1089
+ return isKind ? 'kind-worker' : os.hostname();
1090
+ },
1091
+
1035
1092
  /**
1036
1093
  * Deploys a volume for a deployment.
1037
1094
  * @param {object} volume - Volume configuration.
@@ -1043,9 +1100,10 @@ EOF`);
1043
1100
  * @param {string} options.env - Environment for the deployment.
1044
1101
  * @param {string} options.version - Version of the deployment.
1045
1102
  * @param {string} options.namespace - Kubernetes namespace for the deployment.
1046
- * @param {string} options.nodeName - Node name for the deployment.
1103
+ * @param {string} options.nodeName - Effective target node (already resolved via {@link UnderpostDeploy.resolveDeployNode}). The volume data is written/shipped here and the PV is pinned to it; an empty value falls back to the cluster-type default inside this method.
1047
1104
  * @param {string} [options.clusterContext='kind'] - Cluster context type ('kind', 'kubeadm', or 'k3s').
1048
1105
  * @param {boolean} [options.gitClean=false] - Whether to run git clean on volumeMountPath before copying.
1106
+ * @param {string} [options.sshKeyPath=''] - Private key path used when the target node is remote and the volume is shipped over SSH. Empty falls back to copyDirToNode's default (engine-private/deploy/id_rsa).
1049
1107
  * @memberof UnderpostDeploy
1050
1108
  */
1051
1109
  deployVolume(
@@ -1058,6 +1116,7 @@ EOF`);
1058
1116
  nodeName: '',
1059
1117
  clusterContext: 'kind',
1060
1118
  gitClean: false,
1119
+ sshKeyPath: '',
1061
1120
  },
1062
1121
  ) {
1063
1122
  if (!volume.claimName) {
@@ -1072,16 +1131,48 @@ EOF`);
1072
1131
  if (options.gitClean && volume.volumeMountPath) {
1073
1132
  Underpost.repo.clean({ paths: [volume.volumeMountPath] });
1074
1133
  }
1134
+ // The node that physically receives the volume data. hostPath volumes are
1135
+ // node-local, so the data must land on the node where the pod will run, and
1136
+ // the PV is pinned there (nodeAffinity) so the scheduler co-locates the pod
1137
+ // with its volume — never mounting an empty DirectoryOrCreate on another node.
1138
+ let dataNode;
1075
1139
  if (clusterContext === 'kind') {
1076
1140
  const kindNode = options.nodeName || 'kind-worker';
1141
+ dataNode = kindNode;
1077
1142
  shellExec(`docker exec -i ${kindNode} bash -c "mkdir -p ${rootVolumeHostPath}"`);
1078
1143
  shellExec(`tar -C ${volume.volumeMountPath} -c . | docker cp - ${kindNode}:${rootVolumeHostPath}`);
1079
1144
  shellExec(
1080
1145
  `docker exec -i ${kindNode} bash -c "chown -R 1000:1000 ${rootVolumeHostPath}; chmod -R 755 ${rootVolumeHostPath}"`,
1081
1146
  );
1082
1147
  } else {
1083
- if (!fs.existsSync(rootVolumeHostPath)) fs.mkdirSync(rootVolumeHostPath, { recursive: true });
1084
- fs.copySync(volume.volumeMountPath, rootVolumeHostPath);
1148
+ const localHost = os.hostname();
1149
+ dataNode = options.nodeName || localHost;
1150
+ if (dataNode === localHost) {
1151
+ // Target node is the control plane / current host: write directly.
1152
+ if (!fs.existsSync(rootVolumeHostPath)) fs.mkdirSync(rootVolumeHostPath, { recursive: true });
1153
+ fs.copySync(volume.volumeMountPath, rootVolumeHostPath);
1154
+ } else {
1155
+ // Target node is remote: fs.copySync would only write the control-plane
1156
+ // filesystem, leaving the real node's hostPath empty. Ship the folder to
1157
+ // the node over SSH so the data exists where the pod is pinned.
1158
+ const nodeHost =
1159
+ shellExec(
1160
+ `kubectl get node ${dataNode} -o jsonpath='{.status.addresses[?(@.type=="InternalIP")].address}'`,
1161
+ { stdout: true, silent: true, silentOnError: true },
1162
+ ).trim() || dataNode;
1163
+ logger.info('Shipping volume to remote node over SSH', {
1164
+ node: dataNode,
1165
+ host: nodeHost,
1166
+ src: volume.volumeMountPath,
1167
+ dest: rootVolumeHostPath,
1168
+ });
1169
+ Underpost.ssh.copyDirToNode({
1170
+ host: nodeHost,
1171
+ localDir: volume.volumeMountPath,
1172
+ remoteDir: rootVolumeHostPath,
1173
+ ...(options.sshKeyPath ? { keyPath: options.sshKeyPath } : {}),
1174
+ });
1175
+ }
1085
1176
  }
1086
1177
  shellExec(`kubectl delete pvc ${pvcId} -n ${namespace} --ignore-not-found`);
1087
1178
  shellExec(`kubectl delete pv ${pvId} --ignore-not-found`);
@@ -1090,6 +1181,7 @@ ${Underpost.deploy.persistentVolumeFactory({
1090
1181
  hostPath: rootVolumeHostPath,
1091
1182
  pvcId,
1092
1183
  namespace,
1184
+ nodeName: dataNode,
1093
1185
  })}
1094
1186
  EOF
1095
1187
  `);
@@ -1178,11 +1270,28 @@ ${secret ? ` readOnly: true\n` : ''}`;
1178
1270
  * @param {string} options.hostPath - Host path for the persistent volume.
1179
1271
  * @param {string} options.pvcId - Persistent volume claim ID.
1180
1272
  * @param {string} [options.namespace='default'] - Kubernetes namespace for the PVC claimRef.
1273
+ * @param {string} [options.nodeName=''] - Node name to which the persistent volume is pinned (optional).
1181
1274
  * @returns {string} - YAML configuration for the persistent volume and claim.
1182
1275
  * @memberof UnderpostDeploy
1183
1276
  */
1184
- persistentVolumeFactory({ hostPath, pvcId, namespace = 'default' }) {
1277
+ persistentVolumeFactory({ hostPath, pvcId, namespace = 'default', nodeName = '' }) {
1185
1278
  const pvId = pvcId.replace(/^pvc-/, 'pv-');
1279
+ // hostPath volumes are node-local: deployVolume writes the content to the
1280
+ // filesystem of a single node. Without nodeAffinity the scheduler can place
1281
+ // the pod on a different node and mount an empty DirectoryOrCreate hostPath
1282
+ // (missing the materialized assets). Pin the PV to the node that holds the
1283
+ // data so the pod is always co-located with its volume.
1284
+ const nodeAffinity = nodeName
1285
+ ? `
1286
+ nodeAffinity:
1287
+ required:
1288
+ nodeSelectorTerms:
1289
+ - matchExpressions:
1290
+ - key: kubernetes.io/hostname
1291
+ operator: In
1292
+ values:
1293
+ - ${nodeName}`
1294
+ : '';
1186
1295
  return `apiVersion: v1
1187
1296
  kind: PersistentVolume
1188
1297
  metadata:
@@ -1193,7 +1302,7 @@ spec:
1193
1302
  accessModes:
1194
1303
  - ReadWriteOnce
1195
1304
  persistentVolumeReclaimPolicy: Retain
1196
- storageClassName: manual
1305
+ storageClassName: manual${nodeAffinity}
1197
1306
  claimRef:
1198
1307
  apiVersion: v1
1199
1308
  kind: PersistentVolumeClaim