underpost 3.1.3 → 3.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.
Files changed (92) hide show
  1. package/.env.example +0 -2
  2. package/.github/workflows/ghpkg.ci.yml +4 -4
  3. package/.github/workflows/npmpkg.ci.yml +28 -11
  4. package/.github/workflows/publish.ci.yml +6 -0
  5. package/.github/workflows/pwa-microservices-template-page.cd.yml +4 -5
  6. package/.github/workflows/pwa-microservices-template-test.ci.yml +3 -3
  7. package/.github/workflows/release.cd.yml +13 -8
  8. package/CHANGELOG.md +396 -1
  9. package/CLI-HELP.md +53 -6
  10. package/Dockerfile +4 -2
  11. package/README.md +3 -2
  12. package/bin/build.js +18 -12
  13. package/bin/deploy.js +177 -124
  14. package/bin/file.js +3 -0
  15. package/conf.js +3 -2
  16. package/manifests/cronjobs/dd-cron/dd-cron-backup.yaml +5 -2
  17. package/manifests/cronjobs/dd-cron/dd-cron-dns.yaml +5 -2
  18. package/manifests/deployment/dd-default-development/deployment.yaml +2 -2
  19. package/manifests/deployment/dd-test-development/deployment.yaml +88 -74
  20. package/manifests/deployment/dd-test-development/proxy.yaml +13 -4
  21. package/manifests/deployment/playwright/deployment.yaml +1 -1
  22. package/nodemon.json +1 -1
  23. package/package.json +22 -15
  24. package/scripts/rhel-grpc-setup.sh +56 -0
  25. package/src/api/file/file.ref.json +18 -0
  26. package/src/api/user/user.service.js +8 -7
  27. package/src/cli/cluster.js +7 -7
  28. package/src/cli/db.js +726 -825
  29. package/src/cli/deploy.js +151 -93
  30. package/src/cli/env.js +19 -0
  31. package/src/cli/fs.js +5 -2
  32. package/src/cli/index.js +45 -2
  33. package/src/cli/kubectl.js +211 -0
  34. package/src/cli/release.js +284 -0
  35. package/src/cli/repository.js +434 -75
  36. package/src/cli/run.js +189 -34
  37. package/src/cli/secrets.js +73 -0
  38. package/src/cli/test.js +3 -3
  39. package/src/client/Default.index.js +3 -4
  40. package/src/client/components/core/AppStore.js +69 -0
  41. package/src/client/components/core/CalendarCore.js +2 -2
  42. package/src/client/components/core/DropDown.js +137 -17
  43. package/src/client/components/core/Keyboard.js +2 -2
  44. package/src/client/components/core/LogIn.js +2 -2
  45. package/src/client/components/core/LogOut.js +2 -2
  46. package/src/client/components/core/Modal.js +0 -1
  47. package/src/client/components/core/Panel.js +0 -1
  48. package/src/client/components/core/PanelForm.js +19 -19
  49. package/src/client/components/core/SocketIo.js +82 -29
  50. package/src/client/components/core/SocketIoHandler.js +75 -0
  51. package/src/client/components/core/Stream.js +143 -95
  52. package/src/client/components/core/Webhook.js +40 -7
  53. package/src/client/components/default/AppStoreDefault.js +5 -0
  54. package/src/client/components/default/LogInDefault.js +3 -3
  55. package/src/client/components/default/LogOutDefault.js +2 -2
  56. package/src/client/components/default/MenuDefault.js +5 -5
  57. package/src/client/components/default/SocketIoDefault.js +3 -51
  58. package/src/client/services/core/core.service.js +20 -8
  59. package/src/client/services/user/user.management.js +2 -2
  60. package/src/index.js +24 -1
  61. package/src/runtime/express/Dockerfile +4 -0
  62. package/src/runtime/express/Express.js +18 -1
  63. package/src/runtime/lampp/Dockerfile +13 -2
  64. package/src/runtime/lampp/Lampp.js +27 -4
  65. package/src/runtime/wp/Dockerfile +68 -0
  66. package/src/runtime/wp/Wp.js +639 -0
  67. package/src/server/auth.js +24 -1
  68. package/src/server/backup.js +57 -23
  69. package/src/server/client-build-docs.js +9 -2
  70. package/src/server/client-build.js +31 -31
  71. package/src/server/client-formatted.js +109 -57
  72. package/src/server/cron.js +23 -18
  73. package/src/server/ipfs-client.js +24 -1
  74. package/src/server/peer.js +8 -0
  75. package/src/server/runtime.js +25 -1
  76. package/src/server/start.js +3 -2
  77. package/src/ws/IoInterface.js +1 -10
  78. package/src/ws/IoServer.js +14 -33
  79. package/src/ws/core/channels/core.ws.chat.js +65 -20
  80. package/src/ws/core/channels/core.ws.mailer.js +113 -32
  81. package/src/ws/core/channels/core.ws.stream.js +90 -31
  82. package/src/ws/core/core.ws.connection.js +12 -33
  83. package/src/ws/core/core.ws.emit.js +10 -26
  84. package/src/ws/core/core.ws.server.js +25 -58
  85. package/src/ws/default/channels/default.ws.main.js +53 -12
  86. package/src/ws/default/default.ws.connection.js +26 -13
  87. package/src/ws/default/default.ws.server.js +30 -12
  88. package/src/client/components/default/ElementsDefault.js +0 -38
  89. package/src/ws/core/management/core.ws.chat.js +0 -8
  90. package/src/ws/core/management/core.ws.mailer.js +0 -16
  91. package/src/ws/core/management/core.ws.stream.js +0 -8
  92. package/src/ws/default/management/default.ws.main.js +0 -8
package/src/cli/deploy.js CHANGED
@@ -132,22 +132,16 @@ class UnderpostDeploy {
132
132
  cmd = [
133
133
  `npm install -g npm@11.2.0`,
134
134
  `npm install -g underpost`,
135
- `underpost secret underpost --create-from-file /etc/config/.env.${env}`,
135
+ `underpost secret underpost --create-from-env`,
136
136
  `underpost start --build --run ${deployId} ${env}`,
137
137
  ];
138
138
  const packageJson = JSON.parse(fs.readFileSync('./package.json', 'utf8'));
139
- if (!volumes)
140
- volumes = [
141
- {
142
- volumeMountPath: '/etc/config',
143
- volumeName: 'config-volume',
144
- configMap: 'underpost-config',
145
- },
146
- ];
139
+ if (!volumes) volumes = [];
147
140
  const confVolume = fs.existsSync(`./engine-private/conf/${deployId}/conf.volume.json`)
148
141
  ? JSON.parse(fs.readFileSync(`./engine-private/conf/${deployId}/conf.volume.json`, 'utf8'))
149
142
  : [];
150
143
  volumes = volumes.concat(confVolume);
144
+ const containerImage = image ? image : `localhost/rockylinux9-underpost:v${packageJson.version}`;
151
145
  return `apiVersion: apps/v1
152
146
  kind: Deployment
153
147
  metadata:
@@ -155,6 +149,7 @@ metadata:
155
149
  namespace: ${namespace ? namespace : 'default'}
156
150
  labels:
157
151
  app: ${deployId}-${env}-${suffix}
152
+ deploy-id: ${deployId}-${env}
158
153
  spec:
159
154
  replicas: ${replicas}
160
155
  selector:
@@ -164,10 +159,14 @@ spec:
164
159
  metadata:
165
160
  labels:
166
161
  app: ${deployId}-${env}-${suffix}
162
+ deploy-id: ${deployId}-${env}
167
163
  spec:
168
164
  containers:
169
165
  - name: ${deployId}-${env}-${suffix}
170
- image: ${image ? image : `localhost/rockylinux9-underpost:v${packageJson.version}`}
166
+ image: ${containerImage}
167
+ envFrom:
168
+ - secretRef:
169
+ name: underpost-config
171
170
  ${
172
171
  resources
173
172
  ? ` resources:
@@ -183,13 +182,17 @@ ${
183
182
  - /bin/sh
184
183
  - -c
185
184
  - >
186
- ${cmd.join(` && `)}
185
+ ${cmd.join(' &&\n ')}
187
186
 
188
- ${Underpost.deploy
189
- .volumeFactory(volumes.map((v) => ((v.version = `${deployId}-${env}-${suffix}`), v)))
190
- .render.split(`\n`)
191
- .map((l) => ' ' + l)
192
- .join(`\n`)}
187
+ ${
188
+ volumes.length > 0
189
+ ? Underpost.deploy
190
+ .volumeFactory(volumes.map((v) => ((v.version = `${deployId}-${env}-${suffix}`), v)))
191
+ .render.split(`\n`)
192
+ .map((l) => ' ' + l)
193
+ .join(`\n`)
194
+ : ''
195
+ }
193
196
  ---
194
197
  apiVersion: v1
195
198
  kind: Service
@@ -260,6 +263,14 @@ ${Underpost.deploy
260
263
  }
261
264
  fs.writeFileSync(`./engine-private/conf/${deployId}/build/${env}/deployment.yaml`, deploymentYamlParts, 'utf8');
262
265
 
266
+ Underpost.deploy.buildGrpcServiceManifest({
267
+ deployId,
268
+ env,
269
+ confServer,
270
+ namespace: options.namespace,
271
+ traffic: options.traffic && typeof options.traffic === 'string' ? options.traffic.split(',') : ['blue'],
272
+ });
273
+
263
274
  const confVolume = fs.existsSync(`./engine-private/conf/${deployId}/conf.volume.json`)
264
275
  ? JSON.parse(fs.readFileSync(`./engine-private/conf/${deployId}/conf.volume.json`, 'utf8'))
265
276
  : [];
@@ -369,6 +380,67 @@ ${Underpost.deploy
369
380
  }
370
381
  }
371
382
  },
383
+ /**
384
+ * Builds and writes a gRPC ClusterIP service YAML for a deployment.
385
+ * Scans conf.server.json for gRPC ports and emits grpc-service.yaml under
386
+ * `engine-private/conf/<deployId>/build/<env>/`. The selector always uses the
387
+ * explicit `app: <deployId>-<env>-<traffic>` label to target only the active
388
+ * colour (blue or green).
389
+ * @param {string} deployId - Deployment ID.
390
+ * @param {string} env - Environment ('development' or 'production').
391
+ * @param {object} confServer - Parsed conf.server.json content.
392
+ * @param {string} [namespace='default'] - Kubernetes namespace.
393
+ * @param {string[]} [traffic=['blue']] - Active traffic colour(s) ('blue', 'green', or both).
394
+ * @param {string|null} [host=null] - Specific host to scan for gRPC ports. If null, all hosts are scanned.
395
+ * @returns {string|null} - Path to the written YAML file, or null if no gRPC ports found.
396
+ * @memberof UnderpostDeploy
397
+ */
398
+ buildGrpcServiceManifest({ deployId, env, confServer, namespace = 'default', traffic = ['blue'], host = null }) {
399
+ const grpcPorts = new Set();
400
+ const hostsToScan = host ? [host] : Object.keys(confServer);
401
+ for (const h of hostsToScan) {
402
+ if (!confServer[h]) continue;
403
+ for (const path of Object.keys(confServer[h])) {
404
+ const grpc = confServer[h][path].grpc;
405
+ if (grpc && grpc.port) grpcPorts.add(parseInt(grpc.port));
406
+ }
407
+ }
408
+ if (grpcPorts.size === 0) return null;
409
+ const grpcPortsList = [...grpcPorts]
410
+ .map(
411
+ (port) => ` - name: grpc-${port}
412
+ protocol: TCP
413
+ port: ${port}
414
+ targetPort: ${port}`,
415
+ )
416
+ .join('\n');
417
+ let grpcServiceYaml = '';
418
+ for (const color of traffic) {
419
+ const grpcServiceName = `${deployId}-grpc-service-${env}-${color}`;
420
+ const selectorYaml = `app: ${deployId}-${env}-${color}`;
421
+ grpcServiceYaml += `---
422
+ apiVersion: v1
423
+ kind: Service
424
+ metadata:
425
+ name: ${grpcServiceName}
426
+ namespace: ${namespace}
427
+ labels:
428
+ app: ${grpcServiceName}
429
+ spec:
430
+ type: ClusterIP
431
+ selector:
432
+ ${selectorYaml}
433
+ ports:
434
+ ${grpcPortsList}
435
+ `;
436
+ logger.info(
437
+ `gRPC ClusterIP service YAML written: ${grpcServiceName} (selector: ${selectorYaml}, ports: ${[...grpcPorts].join(', ')})`,
438
+ );
439
+ }
440
+ const yamlPath = `./engine-private/conf/${deployId}/build/${env}/grpc-service.yaml`;
441
+ fs.writeFileSync(yamlPath, grpcServiceYaml, 'utf8');
442
+ return yamlPath;
443
+ },
372
444
  /**
373
445
  * Builds a Certificate resource for a host using cert-manager.
374
446
  * @param {string} host - Hostname for which the certificate is being built.
@@ -478,6 +550,10 @@ spec:
478
550
  * @param {string} [options.kindType] - Type of Kubernetes resource to retrieve information for.
479
551
  * @param {number} [options.port] - Port number for exposing the deployment.
480
552
  * @param {string} [options.cmd] - Custom initialization command for deploymentYamlPartsFactory (comma-separated commands).
553
+ * @param {boolean} [options.k3s] - Whether to use k3s cluster context.
554
+ * @param {boolean} [options.kubeadm] - Whether to use kubeadm cluster context.
555
+ * @param {boolean} [options.kind] - Whether to use kind cluster context.
556
+ * @param {boolean} [options.gitClean] - Whether to run git clean on volume mount paths before copying.
481
557
  * @returns {Promise<void>} - Promise that resolves when the deployment process is complete.
482
558
  * @memberof UnderpostDeploy
483
559
  */
@@ -515,6 +591,10 @@ spec:
515
591
  port: 0,
516
592
  exposePort: 0,
517
593
  cmd: '',
594
+ k3s: false,
595
+ kubeadm: false,
596
+ kind: false,
597
+ gitClean: false,
518
598
  },
519
599
  ) {
520
600
  const namespace = options.namespace ? options.namespace : 'default';
@@ -553,7 +633,7 @@ EOF`);
553
633
  env,
554
634
  traffic: Underpost.deploy.getCurrentTraffic(deployId, { namespace }),
555
635
  router: await Underpost.deploy.routerFactory(deployId, env),
556
- pods: await Underpost.deploy.get(deployId),
636
+ pods: await Underpost.kubectl.get(deployId),
557
637
  instances,
558
638
  });
559
639
  }
@@ -595,7 +675,7 @@ EOF`);
595
675
  if (!deployId) continue;
596
676
  if (options.expose === true) {
597
677
  const kindType = options.kindType ? options.kindType : 'svc';
598
- const svc = Underpost.deploy.get(deployId, kindType)[0];
678
+ const svc = Underpost.kubectl.get(deployId, kindType)[0];
599
679
  const port = options.exposePort
600
680
  ? parseInt(options.exposePort)
601
681
  : options.port
@@ -634,6 +714,8 @@ EOF`);
634
714
  version,
635
715
  namespace,
636
716
  nodeName: options.node ? options.node : env === 'development' ? 'kind-worker' : os.hostname(),
717
+ clusterContext: options.k3s ? 'k3s' : options.kubeadm ? 'kubeadm' : 'kind',
718
+ gitClean: options.gitClean || false,
637
719
  });
638
720
  }
639
721
 
@@ -652,8 +734,11 @@ EOF`);
652
734
  : `manifests/deployment/${deployId}-${env}`;
653
735
 
654
736
  if (!options.remove) {
655
- if (!options.disableUpdateDeployment)
737
+ if (!options.disableUpdateDeployment) {
656
738
  shellExec(`sudo kubectl apply -f ./${manifestsPath}/deployment.yaml -n ${namespace}`);
739
+ const grpcServicePath = `./${manifestsPath}/grpc-service.yaml`;
740
+ if (fs.existsSync(grpcServicePath)) shellExec(`sudo kubectl apply -f ${grpcServicePath} -n ${namespace}`);
741
+ }
657
742
  if (!options.disableUpdateProxy)
658
743
  shellExec(`sudo kubectl apply -f ./${manifestsPath}/proxy.yaml -n ${namespace}`);
659
744
 
@@ -671,65 +756,6 @@ EOF`);
671
756
  ` + renderHosts,
672
757
  );
673
758
  },
674
- /**
675
- * Retrieves information about a deployment.
676
- * @param {string} deployId - Deployment ID for which information is being retrieved.
677
- * @param {string} kindType - Type of Kubernetes resource to retrieve information for (e.g. 'pods').
678
- * @param {string} namespace - Kubernetes namespace to retrieve information from.
679
- * @returns {Array<object>} - Array of objects containing information about the deployment.
680
- * @memberof UnderpostDeploy
681
- */
682
- get(deployId, kindType = 'pods', namespace = '') {
683
- const raw = shellExec(
684
- `sudo kubectl get ${kindType}${namespace ? ` -n ${namespace}` : ` --all-namespaces`} -o wide`,
685
- {
686
- stdout: true,
687
- disableLog: true,
688
- silent: true,
689
- },
690
- );
691
-
692
- const heads = raw
693
- .split(`\n`)[0]
694
- .split(' ')
695
- .filter((_r) => _r.trim());
696
-
697
- const pods = raw
698
- .split(`\n`)
699
- .filter((r) => (deployId ? r.match(deployId) : r.trim() && !r.match('NAME')))
700
- .map((r) => r.split(' ').filter((_r) => _r.trim()));
701
-
702
- const result = [];
703
-
704
- for (const row of pods) {
705
- const pod = {};
706
- let index = -1;
707
- for (const head of heads) {
708
- index++;
709
- pod[head] = row[index];
710
- }
711
- result.push(pod);
712
- }
713
-
714
- return result;
715
- },
716
-
717
- /**
718
- * Checks if a container file exists in a pod.
719
- * @param {object} options - Options for the check.
720
- * @param {string} options.podName - Name of the pod to check.
721
- * @param {string} options.path - Path to the container file to check.
722
- * @returns {boolean} - True if the container file exists, false otherwise.
723
- * @memberof UnderpostDeploy
724
- */
725
- existsContainerFile({ podName, path }) {
726
- const result = shellExec(`kubectl exec ${podName} -- test -f ${path} && echo "true" || echo "false"`, {
727
- stdout: true,
728
- disableLog: true,
729
- silent: true,
730
- }).trim();
731
- return result === 'true';
732
- },
733
759
  /**
734
760
  * Checks the status of a deployment.
735
761
  * @param {string} deployId - Deployment ID for which the status is being checked.
@@ -742,7 +768,7 @@ EOF`);
742
768
  */
743
769
  async checkDeploymentReadyStatus(deployId, env, traffic, ignoresNames = [], namespace = 'default') {
744
770
  const cmd = `underpost config get container-status`;
745
- const pods = Underpost.deploy.get(`${deployId}-${env}-${traffic}`, 'pods', namespace);
771
+ const pods = Underpost.kubectl.get(`${deployId}-${env}-${traffic}`, 'pods', namespace);
746
772
  const readyPods = [];
747
773
  const notReadyPods = [];
748
774
  for (const pod of pods) {
@@ -768,15 +794,16 @@ EOF`);
768
794
  };
769
795
  },
770
796
  /**
771
- * Creates a configmap for a deployment.
772
- * @param {string} env - Environment for which the configmap is being created.
773
- * @param {string} [namespace='default'] - Kubernetes namespace for the configmap.
797
+ * Creates a Kubernetes Secret for a deployment (replaces configMap for secret data).
798
+ * Secrets are mounted as tmpfs (never written to node disk) and support RBAC restrictions.
799
+ * @param {string} env - Environment for which the secret is being created.
800
+ * @param {string} [namespace='default'] - Kubernetes namespace for the secret.
774
801
  * @memberof UnderpostDeploy
775
802
  */
776
803
  configMap(env, namespace = 'default') {
777
- shellExec(`kubectl delete configmap underpost-config -n ${namespace} --ignore-not-found`);
804
+ shellExec(`kubectl delete secret underpost-config -n ${namespace} --ignore-not-found`);
778
805
  shellExec(
779
- `kubectl create configmap underpost-config --from-file=/home/dd/engine/engine-private/conf/dd-cron/.env.${env} --dry-run=client -o yaml | kubectl apply -f - -n ${namespace}`,
806
+ `kubectl create secret generic underpost-config --from-env-file=/home/dd/engine/engine-private/conf/dd-cron/.env.${env} --dry-run=client -o yaml | kubectl apply -f - -n ${namespace}`,
780
807
  );
781
808
  },
782
809
  /**
@@ -814,6 +841,9 @@ EOF`);
814
841
 
815
842
  shellExec(`sudo kubectl apply -f ./engine-private/conf/${deployId}/build/${env}/proxy.yaml -n ${namespace}`);
816
843
 
844
+ const grpcServicePath = `./engine-private/conf/${deployId}/build/${env}/grpc-service.yaml`;
845
+ if (fs.existsSync(grpcServicePath)) shellExec(`kubectl apply -f ${grpcServicePath} -n ${namespace}`);
846
+
817
847
  Underpost.env.set(`${deployId}-${env}-traffic`, targetTraffic);
818
848
  },
819
849
 
@@ -829,6 +859,8 @@ EOF`);
829
859
  * @param {string} options.version - Version of the deployment.
830
860
  * @param {string} options.namespace - Kubernetes namespace for the deployment.
831
861
  * @param {string} options.nodeName - Node name for the deployment.
862
+ * @param {string} [options.clusterContext='kind'] - Cluster context type ('kind', 'kubeadm', or 'k3s').
863
+ * @param {boolean} [options.gitClean=false] - Whether to run git clean on volumeMountPath before copying.
832
864
  * @memberof UnderpostDeploy
833
865
  */
834
866
  deployVolume(
@@ -839,6 +871,8 @@ EOF`);
839
871
  version: '',
840
872
  namespace: '',
841
873
  nodeName: '',
874
+ clusterContext: 'kind',
875
+ gitClean: false,
842
876
  },
843
877
  ) {
844
878
  if (!volume.claimName) {
@@ -846,19 +880,26 @@ EOF`);
846
880
  return;
847
881
  }
848
882
  const { deployId, env, version, namespace } = options;
883
+ const clusterContext = options.clusterContext || 'kind';
849
884
  const pvcId = `${volume.claimName}-${deployId}-${env}-${version}`;
850
885
  const pvId = `${volume.claimName.replace('pvc-', 'pv-')}-${deployId}-${env}-${version}`;
851
886
  const rootVolumeHostPath = `/home/dd/engine/volume/${pvId}`;
887
+ if (options.gitClean && volume.volumeMountPath) {
888
+ Underpost.repo.clean({ paths: [volume.volumeMountPath] });
889
+ }
852
890
  if (options.nodeName) {
853
891
  if (!fs.existsSync(rootVolumeHostPath)) fs.mkdirSync(rootVolumeHostPath, { recursive: true });
854
892
  fs.copySync(volume.volumeMountPath, rootVolumeHostPath);
855
- } else {
893
+ } else if (clusterContext === 'kind') {
856
894
  shellExec(`docker exec -i kind-worker bash -c "mkdir -p ${rootVolumeHostPath}"`);
857
895
  // shellExec(`docker cp ${volume.volumeMountPath} kind-worker:${rootVolumeHostPath}`);
858
896
  shellExec(`tar -C ${volume.volumeMountPath} -c . | docker cp - kind-worker:${rootVolumeHostPath}`);
859
897
  shellExec(
860
898
  `docker exec -i kind-worker bash -c "chown -R 1000:1000 ${rootVolumeHostPath}; chmod -R 755 ${rootVolumeHostPath}"`,
861
899
  );
900
+ } else {
901
+ if (!fs.existsSync(rootVolumeHostPath)) fs.mkdirSync(rootVolumeHostPath, { recursive: true });
902
+ fs.copySync(volume.volumeMountPath, rootVolumeHostPath);
862
903
  }
863
904
  shellExec(`kubectl delete pvc ${pvcId} -n ${namespace} --ignore-not-found`);
864
905
  shellExec(`kubectl delete pv ${pvId} --ignore-not-found`);
@@ -881,6 +922,8 @@ EOF
881
922
  * @param {string} volume.volumeType - Type of the volume (e.g. 'Directory').
882
923
  * @param {string|null} volume.claimName - Name of the persistent volume claim (if applicable).
883
924
  * @param {string|null} volume.configMap - Name of the config map (if applicable).
925
+ * @param {string|null} volume.secret - Name of the Kubernetes Secret (if applicable). Mounts as readOnly.
926
+ * @param {boolean} [volume.emptyDir=false] - If true, uses an emptyDir volume (writable tmpfs).
884
927
  * @returns {object} - Object containing the rendered volume mounts and volumes.
885
928
  * @memberof UnderpostDeploy
886
929
  */
@@ -902,7 +945,17 @@ EOF
902
945
  let _volumes = `
903
946
  volumes:`;
904
947
  volumes.map((volumeData) => {
905
- let { volumeName, volumeMountPath, volumeHostPath, volumeType, claimName, configMap, version } = volumeData;
948
+ let {
949
+ volumeName,
950
+ volumeMountPath,
951
+ volumeHostPath,
952
+ volumeType,
953
+ claimName,
954
+ configMap,
955
+ secret,
956
+ emptyDir,
957
+ version,
958
+ } = volumeData;
906
959
  if (version) {
907
960
  volumeName = `${volumeName}-${version}`;
908
961
  claimName = claimName ? `${claimName}-${version}` : null;
@@ -910,18 +963,23 @@ EOF
910
963
  _volumeMounts += `
911
964
  - name: ${volumeName}
912
965
  mountPath: ${volumeMountPath}
913
- `;
966
+ ${secret ? ` readOnly: true\n` : ''}`;
914
967
 
915
968
  _volumes += `
916
969
  - name: ${volumeName}
917
970
  ${
918
- configMap
919
- ? ` configMap:
971
+ emptyDir
972
+ ? ` emptyDir: {}`
973
+ : secret
974
+ ? ` secret:
975
+ secretName: ${secret}`
976
+ : configMap
977
+ ? ` configMap:
920
978
  name: ${configMap}`
921
- : claimName
922
- ? ` persistentVolumeClaim:
979
+ : claimName
980
+ ? ` persistentVolumeClaim:
923
981
  claimName: ${claimName}`
924
- : ` hostPath:
982
+ : ` hostPath:
925
983
  path: ${volumeHostPath}
926
984
  type: ${volumeType}
927
985
  `
@@ -1041,7 +1099,7 @@ ${renderHosts}`,
1041
1099
  async monitorReadyRunner(deployId, env, targetTraffic, ignorePods = [], namespace = 'default', outLogType = '') {
1042
1100
  let checkStatusIteration = 0;
1043
1101
  const checkStatusIterationMsDelay = 1000;
1044
- const maxIterations = 500;
1102
+ const maxIterations = 3000;
1045
1103
  const deploymentId = `${deployId}-${env}-${targetTraffic}`;
1046
1104
  const iteratorTag = `[${deploymentId}]`;
1047
1105
  logger.info('Deployment init', { deployId, env, targetTraffic, checkStatusIterationMsDelay, namespace });
package/src/cli/env.js CHANGED
@@ -12,6 +12,19 @@ import { pbcopy } from '../server/process.js';
12
12
 
13
13
  const logger = loggerFactory(import.meta);
14
14
 
15
+ /**
16
+ * Guards an env file path against stale directory artifacts.
17
+ * Removes the path if it exists as a directory (e.g. `.env/` created by a previous EISDIR bug).
18
+ * @param {string} envPath - The path to the environment file.
19
+ * @memberof UnderpostEnv
20
+ */
21
+ const guardEnvPath = (envPath) => {
22
+ if (fs.existsSync(envPath) && !fs.statSync(envPath).isFile()) {
23
+ logger.warn(`Removing stale directory at env path: ${envPath}`);
24
+ fs.removeSync(envPath);
25
+ }
26
+ };
27
+
15
28
  /**
16
29
  * @class UnderpostRootEnv
17
30
  * @description Manages the environment variables of the underpost root.
@@ -31,6 +44,7 @@ class UnderpostRootEnv {
31
44
  */
32
45
  set(key, value, options = { deployId: '', build: false }) {
33
46
  const _set = (envPath, key, value) => {
47
+ guardEnvPath(envPath);
34
48
  let env = {};
35
49
  if (fs.existsSync(envPath)) env = dotenv.parse(fs.readFileSync(envPath, 'utf8'));
36
50
  env[key] = value;
@@ -48,6 +62,7 @@ class UnderpostRootEnv {
48
62
  return;
49
63
  }
50
64
  const exeRootPath = `${getNpmRootPath()}/underpost`;
65
+ fs.ensureDirSync(exeRootPath);
51
66
  const envPath = `${exeRootPath}/.env`;
52
67
  _set(envPath, key, value);
53
68
  },
@@ -60,6 +75,7 @@ class UnderpostRootEnv {
60
75
  delete(key) {
61
76
  const exeRootPath = `${getNpmRootPath()}/underpost`;
62
77
  const envPath = `${exeRootPath}/.env`;
78
+ guardEnvPath(envPath);
63
79
  let env = {};
64
80
  if (fs.existsSync(envPath)) env = dotenv.parse(fs.readFileSync(envPath, 'utf8'));
65
81
  delete env[key];
@@ -101,6 +117,7 @@ class UnderpostRootEnv {
101
117
  list(key, value, options = {}) {
102
118
  const exeRootPath = `${getNpmRootPath()}/underpost`;
103
119
  const envPath = `${exeRootPath}/.env`;
120
+ guardEnvPath(envPath);
104
121
  if (!fs.existsSync(envPath)) {
105
122
  logger.warn(`Empty environment variables`);
106
123
  return {};
@@ -140,3 +157,5 @@ class UnderpostRootEnv {
140
157
  }
141
158
 
142
159
  export default UnderpostRootEnv;
160
+
161
+ export { guardEnvPath };
package/src/cli/fs.js CHANGED
@@ -101,13 +101,16 @@ class UnderpostFileStorage {
101
101
  }
102
102
  }
103
103
  if (options.pull === true) {
104
+ let pullSkipCount = 0;
104
105
  for (const _path of Object.keys(storage)) {
105
106
  if (!fs.existsSync(_path) || options.force === true) {
106
107
  if (options.force === true && fs.existsSync(_path)) fs.removeSync(_path);
107
108
  await Underpost.fs.pull(_path, options);
108
- } else logger.warn(`Pull path already exists`, _path);
109
+ } else pullSkipCount++;
109
110
  }
110
- shellExec(`cd ${path} && git init && git add . && git commit -m "Base pull state"`);
111
+ if (pullSkipCount > 0) logger.warn(`Pull skipped ${pullSkipCount} files that already exist`);
112
+ Underpost.repo.initLocalRepo({ path });
113
+ shellExec(`cd ${path} && git add . && git commit -m "Base pull state"`);
111
114
  } else {
112
115
  const files =
113
116
  options.git === true ? Underpost.repo.getChangedFiles(path) : await fs.readdir(path, { recursive: true });
package/src/cli/index.js CHANGED
@@ -87,7 +87,7 @@ program
87
87
  .argument(`[commit-type]`, `The type of commit to perform. Options: ${Object.keys(commitData).join(', ')}.`)
88
88
  .argument(`[module-tag]`, 'Optional: Sets a specific module tag for the commit.')
89
89
  .argument(`[message]`, 'Optional: Provides an additional custom message for the commit.')
90
- .option(`--log <latest-n>`, 'Shows commit history from the specified number of latest n path commits.')
90
+ .option(`--log [latest-n]`, 'Shows commit history from the specified number of latest n path commits.')
91
91
  .option('--last-msg <latest-n>', 'Displays the last n commit message.')
92
92
  .option('--empty', 'Allows committing with empty files.')
93
93
  .option('--copy', 'Copies the generated commit message to the clipboard.')
@@ -108,7 +108,14 @@ program
108
108
  '--changelog-no-hash',
109
109
  'Excludes commit hashes from the generated changelog entries (used with --changelog-build).',
110
110
  )
111
+ .option('--unpush', 'With --log, automatically sets range to unpushed commits ahead of remote.')
111
112
  .option('-b', 'Shows the current Git branch name.')
113
+ .option('-p [branch]', 'Shows the reflog for the specified branch.')
114
+ .option('--bc <commit-hash>', 'Shows branches that contain the specified commit.')
115
+ .option(
116
+ '--is-remote-repo <url-repo>',
117
+ 'Checks whether a remote Git repository URL is reachable. Prints true or false.',
118
+ )
112
119
  .description('Manages commits to a GitHub repository, supporting various commit types and options.')
113
120
  .action(Underpost.repo.commit);
114
121
 
@@ -308,6 +315,9 @@ program
308
315
  'Retrieves current network traffic data from resource deployments and the host machine network configuration.',
309
316
  )
310
317
  .option('--kubeadm', 'Enables the kubeadm context for deployment operations.')
318
+ .option('--k3s', 'Enables the k3s context for deployment operations.')
319
+ .option('--kind', 'Enables the kind context for deployment operations.')
320
+ .option('--git-clean', 'Runs git clean on volume mount paths before copying.')
311
321
  .option('--etc-hosts', 'Enables the etc-hosts context for deployment operations.')
312
322
  .option('--restore-hosts', 'Restores default `/etc/hosts` entries.')
313
323
  .option('--disable-update-underpost-config', 'Disables updates to Underpost configuration during deployment.')
@@ -327,10 +337,12 @@ program
327
337
  .argument('<platform>', `The secret management platform. Options: ${Object.keys(Underpost.secret).join(', ')}.`)
328
338
  .option('--init', 'Initializes the secrets platform environment.')
329
339
  .option('--create-from-file <path-env-file>', 'Creates secrets from a specified environment file.')
340
+ .option('--create-from-env', 'Creates secrets from container environment variables (envFrom: secretRef).')
330
341
  .option('--list', 'Lists all available secrets for the platform.')
331
342
  .description(`Manages secrets for various platforms.`)
332
343
  .action((...args) => {
333
344
  if (args[1].createFromFile) return Underpost.secret[args[0]].createFromEnvFile(args[1].createFromFile);
345
+ if (args[1].createFromEnv) return Underpost.secret[args[0]].createFromContainerEnv();
334
346
  if (args[1].list) return Underpost.secret[args[0]].list();
335
347
  if (args[1].init) return Underpost.secret[args[0]].init();
336
348
  });
@@ -413,6 +425,7 @@ program
413
425
  .option('--kubeadm', 'Enables the kubeadm context for database operations.')
414
426
  .option('--kind', 'Enables the kind context for database operations.')
415
427
  .option('--k3s', 'Enables the k3s context for database operations.')
428
+ .option('--repo-backup', 'Backs up repositories (git commit+push) inside deployment pods via kubectl exec.')
416
429
  .description(
417
430
  'Manages database operations with support for MariaDB and MongoDB, including import/export, multi-pod targeting, and Git integration.',
418
431
  )
@@ -458,7 +471,6 @@ program
458
471
  '--create-job-now',
459
472
  'After applying manifests, immediately create a Job from each CronJob (requires --apply).',
460
473
  )
461
- .option('--ssh', 'Execute backup commands via SSH on the remote node instead of locally.')
462
474
  .description('Manages cron jobs: execute jobs directly or generate and apply K8s CronJob manifests.')
463
475
  .action(Underpost.cron.callback);
464
476
 
@@ -597,6 +609,7 @@ program
597
609
  .option('--kubeadm', 'Sets the kubeadm cluster context for the runner execution.')
598
610
  .option('--k3s', 'Sets the k3s cluster context for the runner execution.')
599
611
  .option('--kind', 'Sets the kind cluster context for the runner execution.')
612
+ .option('--git-clean', 'Runs git clean on volume mount paths before copying.')
600
613
  .option('--log-type <log-type>', 'Sets the log type for the runner execution.')
601
614
  .option('--deploy-id <deploy-id>', 'Sets deploy id context for the runner execution.')
602
615
  .option('--user <user>', 'Sets user context for the runner execution.')
@@ -640,6 +653,7 @@ program
640
653
  'Format: semicolon-separated entries of "ip=hostname1,hostname2" ' +
641
654
  '(e.g., "127.0.0.1=foo.local,bar.local;10.1.2.3=foo.remote,bar.remote").',
642
655
  )
656
+ .option('--copy', 'Copies the runner output to the clipboard (supported by: generate-pass, template-deploy-local).')
643
657
  .description('Runs specified scripts using various runners.')
644
658
  .action(Underpost.run.callback);
645
659
 
@@ -764,4 +778,33 @@ program
764
778
  )
765
779
  .action(Underpost.baremetal.callback);
766
780
 
781
+ program
782
+ .command('release')
783
+ .argument('[version]', 'The new version string to set (e.g., "3.1.4"). Defaults to current version.')
784
+ .option('--build', 'Builds a new version: tests template, bumps versions, rebuilds manifests and configs.')
785
+ .option('--deploy', 'Deploys the release: syncs secrets, commits, and pushes to remote repositories.')
786
+ .option(
787
+ '--ci-push <deploy-id>',
788
+ 'Local equivalent of engine-*.ci.yml: builds dd-{deploy-id} and pushes to the engine-{deploy-id} repository. ' +
789
+ 'Accepts the suffix (e.g., "cyberia"), "dd-cyberia", or "engine-cyberia".',
790
+ )
791
+ .option(
792
+ '--message <message>',
793
+ 'Commit message for --ci-push or --pwa-build (defaults to last commit of the engine repository).',
794
+ )
795
+ .option(
796
+ '--pwa-build',
797
+ 'Runs the pwa-microservices-template update flow: always re-clones, syncs engine sources, installs, builds, and pushes.',
798
+ )
799
+ .description('Release orchestrator for building new versions and deploying releases of the Underpost CLI.')
800
+ .action(async (version, options) => {
801
+ if (options.build) return Underpost.release.build(version, options);
802
+ if (options.deploy) return Underpost.release.deploy(version, options);
803
+ if (options.ciPush) return Underpost.release.ci(options.ciPush, options.message, options);
804
+ if (options.pwaBuild) return Underpost.release.pwa(options.message, options);
805
+ console.log(
806
+ 'Please specify --build, --deploy, --ci-push, or --pwa-build. Use "underpost release --help" for details.',
807
+ );
808
+ });
809
+
767
810
  export { program };