@underpostnet/underpost 2.99.0 → 2.99.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/cli/image.js CHANGED
@@ -8,7 +8,7 @@ import fs from 'fs-extra';
8
8
  import dotenv from 'dotenv';
9
9
  import { loggerFactory } from '../server/logger.js';
10
10
  import Underpost from '../index.js';
11
- import { getUnderpostRootPath } from '../server/conf.js';
11
+ import { getNpmRootPath, getUnderpostRootPath } from '../server/conf.js';
12
12
  import { shellExec } from '../server/process.js';
13
13
 
14
14
  dotenv.config();
@@ -32,7 +32,7 @@ class UnderpostImage {
32
32
  * @param {boolean} [options.kind=false] - If true, load image into Kind cluster.
33
33
  * @param {boolean} [options.kubeadm=false] - If true, load image into Kubeadm cluster.
34
34
  * @param {boolean} [options.k3s=false] - If true, load image into K3s cluster.
35
- * @param {string} [options.path=false] - Path to the Dockerfile context.
35
+ * @param {string} [options.path=''] - Path to the Dockerfile context.
36
36
  * @param {boolean} [options.dev=false] - If true, use development mode.
37
37
  * @param {string} [options.version=''] - Version tag for the image.
38
38
  * @param {string} [options.imageName=''] - Custom name for the image.
@@ -43,7 +43,7 @@ class UnderpostImage {
43
43
  kind: false,
44
44
  kubeadm: false,
45
45
  k3s: false,
46
- path: false,
46
+ path: '',
47
47
  dev: false,
48
48
  version: '',
49
49
  imageName: '',
@@ -137,7 +137,7 @@ class UnderpostImage {
137
137
  ),
138
138
  );
139
139
  for (const key of Object.keys(envObj)) {
140
- secretsInput += ` && export ${key}="${envObj[key]}" `; // Example: $(cat gitlab-token.txt)
140
+ secretsInput += ` && export ${key}="${envObj[key]}" `;
141
141
  secretDockerInput += ` --secret id=${key},env=${key} \ `;
142
142
  }
143
143
  }
package/src/cli/index.js CHANGED
@@ -456,6 +456,9 @@ program
456
456
  .option('--retry-count <count>', 'Sets HTTPProxy per-route retry count (e.g., 3).')
457
457
  .option('--retry-per-try-timeout <duration>', 'Sets HTTPProxy retry per-try timeout (e.g., "150ms").')
458
458
  .option('--disable-private-conf-update', 'Disables updates to private configuration during execution.')
459
+ .option('--versions <deployment-versions>', 'Specifies the deployment versions to monitor. eg. "blue,green", "green"')
460
+ .option('--ready-deployment', 'Run in ready deployment monitor mode.')
461
+ .option('--promote', 'Promotes the deployment after monitoring.')
459
462
  .description('Manages health server monitoring for specified deployments.')
460
463
  .action(Underpost.monitor.callback);
461
464
 
@@ -554,6 +557,20 @@ program
554
557
  .option('--retry-count <count>', 'Sets HTTPProxy per-route retry count (e.g., 3).')
555
558
  .option('--retry-per-try-timeout <duration>', 'Sets HTTPProxy retry per-try timeout (e.g., "150ms").')
556
559
  .option('--disable-private-conf-update', 'Disables updates to private configuration during execution.')
560
+ .option('--logs', 'Streams logs during the runner execution.')
561
+ .option('--monitor-status <status>', 'Sets the status to monitor for pod/resource (default: "Running").')
562
+ .option(
563
+ '--monitor-status-kind-type <kind-type>',
564
+ 'Sets the Kubernetes resource kind type to monitor (default: "pods").',
565
+ )
566
+ .option(
567
+ '--monitor-status-delta-ms <milliseconds>',
568
+ 'Sets the polling interval in milliseconds for status monitoring (default: 1000).',
569
+ )
570
+ .option(
571
+ '--monitor-status-max-attempts <attempts>',
572
+ 'Sets the maximum number of status check attempts (default: 600).',
573
+ )
557
574
  .description('Runs specified scripts using various runners.')
558
575
  .action(Underpost.run.callback);
559
576
 
@@ -593,7 +610,13 @@ program
593
610
  .action(Underpost.lxd.callback);
594
611
 
595
612
  program
596
- .command('baremetal [workflow-id] [ip-address] [hostname] [ip-file-server] [ip-config] [netmask] [dns-server]')
613
+ .command('baremetal [workflow-id]')
614
+ .option('--ip-address <ip-address>', 'The IP address of the control server or the local machine.')
615
+ .option('--hostname <hostname>', 'The hostname of the target baremetal machine.')
616
+ .option('--ip-file-server <ip-file-server>', 'The IP address of the file server (NFS/TFTP).')
617
+ .option('--ip-config <ip-config>', 'IP configuration string for the baremetal machine.')
618
+ .option('--netmask <netmask>', 'Netmask of network.')
619
+ .option('--dns-server <dns-server>', 'DNS server IP address.')
597
620
  .option('--control-server-install', 'Installs the baremetal control server.')
598
621
  .option('--control-server-uninstall', 'Uninstalls the baremetal control server.')
599
622
  .option('--control-server-restart', 'Restarts the baremetal control server.')
@@ -42,6 +42,9 @@ class UnderpostMonitor {
42
42
  * @param {string} [options.timeoutIdle=''] - Timeout for idle connections.
43
43
  * @param {string} [options.retryCount=''] - Number of retry attempts for health checks.
44
44
  * @param {string} [options.retryPerTryTimeout=''] - Timeout per retry attempt.
45
+ * @param {boolean} [options.promote=false] - Promote the deployment after monitoring.
46
+ * @param {boolean} [options.readyDeployment=false] - Monitor until the deployment is ready.
47
+ * @param {string} [options.versions=''] - Specific version of the deployment to monitor.
45
48
  * @param {object} [commanderOptions] - Options passed from the command line interface.
46
49
  * @param {object} [auxRouter] - Optional router configuration for the deployment.
47
50
  * @memberof UnderpostMonitor
@@ -61,6 +64,9 @@ class UnderpostMonitor {
61
64
  timeoutIdle: '',
62
65
  retryCount: '',
63
66
  retryPerTryTimeout: '',
67
+ promote: false,
68
+ readyDeployment: false,
69
+ versions: '',
64
70
  },
65
71
  commanderOptions,
66
72
  auxRouter,
@@ -79,6 +85,17 @@ class UnderpostMonitor {
79
85
  return;
80
86
  }
81
87
 
88
+ if (options.readyDeployment) {
89
+ for (const version of options.versions.split(',')) {
90
+ (async () => {
91
+ await Underpost.deploy.monitorReadyRunner(deployId, env, version, [], options.namespace, 'underpost');
92
+ if (options.promote)
93
+ Underpost.deploy.switchTraffic(deployId, env, version, options.replicas, options.namespace, options);
94
+ })();
95
+ }
96
+ return;
97
+ }
98
+
82
99
  const router = auxRouter ?? (await Underpost.deploy.routerFactory(deployId, env));
83
100
 
84
101
  const confServer = loadReplicas(
package/src/cli/run.js CHANGED
@@ -80,6 +80,11 @@ const logger = loggerFactory(import.meta);
80
80
  * @property {string} user - The user to run as.
81
81
  * @property {string} pid - The process ID.
82
82
  * @property {boolean} disablePrivateConfUpdate - Whether to disable private configuration updates.
83
+ * @property {string} monitorStatus - The monitor status option.
84
+ * @property {string} monitorStatusKindType - The monitor status kind type option.
85
+ * @property {string} monitorStatusDeltaMs - The monitor status delta in milliseconds.
86
+ * @property {string} monitorStatusMaxAttempts - The maximum number of attempts for monitor status.
87
+ * @property {boolean} logs - Whether to enable logs.
83
88
  * @memberof UnderpostRun
84
89
  */
85
90
  const DEFAULT_OPTION = {
@@ -134,6 +139,11 @@ const DEFAULT_OPTION = {
134
139
  user: '',
135
140
  pid: '',
136
141
  disablePrivateConfUpdate: false,
142
+ monitorStatus: '',
143
+ monitorStatusKindType: '',
144
+ monitorStatusDeltaMs: '',
145
+ monitorStatusMaxAttempts: '',
146
+ logs: false,
137
147
  };
138
148
 
139
149
  /**
@@ -255,17 +265,25 @@ class UnderpostRun {
255
265
  },
256
266
 
257
267
  /**
258
- * @method ssh-cluster-info
259
- * @description Executes the `ssh-cluster-info.sh` script to display cluster connection information.
268
+ * @method ssh-deploy-info
269
+ * @description Retrieves deployment status and pod information from a remote server via SSH.
260
270
  * @param {string} path - The input value, identifier, or path for the operation.
261
271
  * @param {Object} options - The default underpost runner options for customizing workflow
262
272
  * @memberof UnderpostRun
263
273
  */
264
- 'ssh-cluster-info': async (path, options = DEFAULT_OPTION) => {
265
- const { underpostRoot } = options;
266
- if (options.deployId && options.user) await Underpost.ssh.setDefautlSshCredentials(options);
267
- shellExec(`chmod +x ${underpostRoot}/scripts/ssh-cluster-info.sh`);
268
- shellExec(`${underpostRoot}/scripts/ssh-cluster-info.sh`);
274
+ 'ssh-deploy-info': async (path = '', options = DEFAULT_OPTION) => {
275
+ const env = options.dev ? 'development' : 'production';
276
+ await Underpost.ssh.sshRemoteRunner(
277
+ `node bin deploy ${path ? path : 'dd'} ${env} --status && kubectl get pods -A`,
278
+ {
279
+ deployId: options.deployId,
280
+ user: options.user,
281
+ dev: options.dev,
282
+ remote: true,
283
+ useSudo: true,
284
+ cd: '/home/dd/engine',
285
+ },
286
+ );
269
287
  },
270
288
 
271
289
  /**
@@ -494,19 +512,19 @@ class UnderpostRun {
494
512
  if (targetTraffic) versions = versions ? versions : targetTraffic;
495
513
 
496
514
  const timeoutFlags = Underpost.deploy.timeoutFlagsFactory(options);
515
+ const cmdString = options.cmd
516
+ ? ' --cmd ' + (options.cmd.find((c) => c.match('"')) ? '"' + options.cmd + '"' : "'" + options.cmd + "'")
517
+ : '';
497
518
 
498
519
  shellExec(
499
520
  `${baseCommand} deploy --kubeadm --build-manifest --sync --info-router --replicas ${replicas} --node ${node}${
500
521
  image ? ` --image ${image}` : ''
501
522
  }${versions ? ` --versions ${versions}` : ''}${
502
523
  options.namespace ? ` --namespace ${options.namespace}` : ''
503
- }${timeoutFlags} dd ${env}`,
524
+ }${timeoutFlags}${cmdString} dd ${env}`,
504
525
  );
505
526
 
506
527
  if (isDeployRunnerContext(path, options)) {
507
- const cmdString = options.cmd
508
- ? ` --cmd ${options.cmd.find((c) => c.match('"')) ? `"${options.cmd}"` : `'${options.cmd}'`}`
509
- : '';
510
528
  shellExec(
511
529
  `${baseCommand} deploy --kubeadm${cmdString} --replicas ${replicas} --disable-update-proxy ${deployId} ${env} --versions ${versions}${
512
530
  options.namespace ? ` --namespace ${options.namespace}` : ''
@@ -552,29 +570,121 @@ class UnderpostRun {
552
570
  * @memberof UnderpostRun
553
571
  */
554
572
  'ssh-deploy-stop': async (path, options = DEFAULT_OPTION) => {
555
- const env = options.dev ? 'development' : 'production';
556
573
  const baseCommand = options.dev ? 'node bin' : 'underpost';
557
574
  const baseClusterCommand = options.dev ? ' --dev' : '';
558
- await Underpost.ssh.setDefautlSshCredentials(options);
559
- shellExec(`#!/usr/bin/env bash
560
- set -euo pipefail
561
575
 
562
- REMOTE_USER=$(node bin config get --plain DEFAULT_SSH_USER)
563
- REMOTE_HOST=$(node bin config get --plain DEFAULT_SSH_HOST)
564
- REMOTE_PORT=$(node bin config get --plain DEFAULT_SSH_PORT)
565
- SSH_KEY=$(node bin config get --plain DEFAULT_SSH_KEY_PATH)
566
-
567
- chmod 600 "$SSH_KEY"
568
-
569
- ssh -i "$SSH_KEY" -o BatchMode=yes "$REMOTE_USER@$REMOTE_HOST" -p $REMOTE_PORT sh <<EOF
570
- cd /home/dd/engine
571
- sudo -n -- /bin/bash -lc "${[
576
+ const remoteCommand = [
572
577
  `${baseCommand} run${baseClusterCommand} stop${path ? ` ${path}` : ''}`,
573
578
  ` --deploy-id ${options.deployId}${options.instanceId ? ` --instance-id ${options.instanceId}` : ''}`,
574
579
  ` --namespace ${options.namespace}${options.hosts ? ` --hosts ${options.hosts}` : ''}`,
575
- ].join('')}"
576
- EOF
577
- `);
580
+ ].join('');
581
+
582
+ await Underpost.ssh.sshRemoteRunner(remoteCommand, {
583
+ deployId: options.deployId,
584
+ user: options.user,
585
+ dev: options.dev,
586
+ remote: true,
587
+ useSudo: true,
588
+ cd: '/home/dd/engine',
589
+ });
590
+ },
591
+
592
+ /**
593
+ * @method ssh-deploy-db-rollback
594
+ * @description Performs a database rollback on remote deployment via SSH.
595
+ * @param {string} path - Comma-separated deployId and optional number of commits to reset (format: "deployId,nCommits")
596
+ * @param {Object} options - The default underpost runner options for customizing workflow
597
+ * @param {string} options.deployId - The deployment identifier
598
+ * @param {string} options.user - The SSH user for credential lookup
599
+ * @param {boolean} options.dev - Development mode flag
600
+ * @memberof UnderpostRun
601
+ */
602
+ 'ssh-deploy-db-rollback': async (path = '', options = DEFAULT_OPTION) => {
603
+ const baseCommand = options.dev ? 'node bin' : 'underpost';
604
+ let [deployId, nCommitsReset] = path.split(',');
605
+ if (!nCommitsReset) nCommitsReset = 1;
606
+
607
+ const remoteCommand = `${baseCommand} db ${deployId} --git --kubeadm --primary-pod --force-clone --macro-rollback-export ${nCommitsReset}${options.namespace ? ` --ns ${options.namespace}` : ''}`;
608
+
609
+ await Underpost.ssh.sshRemoteRunner(remoteCommand, {
610
+ deployId: options.deployId,
611
+ user: options.user,
612
+ dev: options.dev,
613
+ remote: true,
614
+ useSudo: true,
615
+ cd: '/home/dd/engine',
616
+ });
617
+ },
618
+
619
+ /**
620
+ * @method ssh-deploy-db
621
+ * @description Imports/restores a database on remote deployment via SSH.
622
+ * @param {string} path - The deployment ID for database import
623
+ * @param {Object} options - The default underpost runner options for customizing workflow
624
+ * @param {string} options.deployId - The deployment identifier
625
+ * @param {string} options.user - The SSH user for credential lookup
626
+ * @param {boolean} options.dev - Development mode flag
627
+ * @memberof UnderpostRun
628
+ */
629
+ 'ssh-deploy-db': async (path, options = DEFAULT_OPTION) => {
630
+ const baseCommand = options.dev ? 'node bin' : 'underpost';
631
+
632
+ const remoteCommand = `${baseCommand} db ${path} --import --drop --preserveUUID --git --kubeadm --primary-pod --force-clone${options.namespace ? ` --ns ${options.namespace}` : ''}`;
633
+
634
+ await Underpost.ssh.sshRemoteRunner(remoteCommand, {
635
+ deployId: options.deployId,
636
+ user: options.user,
637
+ dev: options.dev,
638
+ remote: true,
639
+ useSudo: true,
640
+ cd: '/home/dd/engine',
641
+ });
642
+ },
643
+
644
+ /**
645
+ * @method ssh-deploy-db-status
646
+ * @description Retrieves database status/stats for a deployment (or all deployments from dd.router) via SSH.
647
+ * @param {string} path - Comma-separated deployId(s) or 'dd' to use the dd.router list.
648
+ * @param {Object} options - Runner options (uses options.deployId for SSH host lookup).
649
+ * @param {string} options.deployId - Deployment identifier used for SSH config lookup.
650
+ * @param {string} options.user - SSH user for credential lookup.
651
+ * @param {boolean} options.dev - Development mode flag.
652
+ * @param {string} [options.namespace] - Kubernetes namespace to pass to the db check.
653
+ * @memberof UnderpostRun
654
+ */
655
+ 'ssh-deploy-db-status': async (path = '', options = DEFAULT_OPTION) => {
656
+ const baseCommand = options.dev ? 'node bin' : 'underpost';
657
+
658
+ let deployList = [];
659
+ if (!path || path === 'dd') {
660
+ if (!fs.existsSync('./engine-private/deploy/dd.router')) {
661
+ logger.warn('dd.router not found; nothing to run');
662
+ return;
663
+ }
664
+ deployList = fs
665
+ .readFileSync('./engine-private/deploy/dd.router', 'utf8')
666
+ .split(',')
667
+ .map((d) => d.trim())
668
+ .filter(Boolean);
669
+ } else {
670
+ deployList = path
671
+ .split(',')
672
+ .map((d) => d.trim())
673
+ .filter(Boolean);
674
+ }
675
+
676
+ for (const deployId of deployList) {
677
+ const remoteCommand = `${baseCommand} db ${deployId} --stats --kubeadm --primary-pod${options.namespace ? ` --ns ${options.namespace}` : ''}`;
678
+
679
+ await Underpost.ssh.sshRemoteRunner(remoteCommand, {
680
+ deployId: options.deployId,
681
+ user: options.user,
682
+ dev: options.dev,
683
+ remote: true,
684
+ useSudo: true,
685
+ cd: '/home/dd/engine',
686
+ });
687
+ }
578
688
  },
579
689
 
580
690
  /**
@@ -897,86 +1007,6 @@ EOF
897
1007
  shellExec(`${underpostRoot}/scripts/ip-info.sh ${path}`);
898
1008
  },
899
1009
 
900
- /**
901
- * @method monitor
902
- * @description Monitors a specific pod (identified by `path`) for the existence of a file (`/await`), and performs conditional actions (like file copying and opening Firefox) when the file is removed.
903
- * @param {string} path - The input value, identifier, or path for the operation (used as the name of the pod to monitor).
904
- * @param {Object} options - The default underpost runner options for customizing workflow
905
- * @memberof UnderpostRun
906
- */
907
- monitor: (path, options = DEFAULT_OPTION) => {
908
- const pid = getTerminalPid();
909
- logger.info('monitor pid', pid);
910
- const checkPath = '/await';
911
- const _monitor = async () => {
912
- const result = Underpost.deploy.existsContainerFile({ podName: path, path: checkPath });
913
- logger.info('monitor', result);
914
- if (result === true) {
915
- switch (path) {
916
- case 'tf-vae-test':
917
- {
918
- const nameSpace = options.namespace;
919
- const podName = path;
920
- const basePath = '/home/dd';
921
- const scriptPath = '/site/en/tutorials/generative/cvae.py';
922
- // shellExec(
923
- // `sudo kubectl cp ${nameSpace}/${podName}:${basePath}/docs${scriptPath} ${basePath}/lab/src/${scriptPath
924
- // .split('/')
925
- // .pop()}`,
926
- // );
927
- // const file = fs.readFileSync(`${basePath}/lab/src/${scriptPath.split('/').pop()}`, 'utf8');
928
- // fs.writeFileSync(
929
- // `${basePath}/lab/src/${scriptPath.split('/').pop()}`,
930
- // file.replace(
931
- // `import time`,
932
- // `import time
933
- // print('=== SCRIPT UPDATE TEST ===')`,
934
- // ),
935
- // 'utf8',
936
- // );
937
- shellExec(
938
- `sudo kubectl cp ${basePath}/lab/src/${scriptPath
939
- .split('/')
940
- .pop()} ${nameSpace}/${podName}:${basePath}/docs${scriptPath}`,
941
- );
942
- // shellExec(`sudo kubectl exec -i ${podName} -- sh -c "ipython ${basePath}/docs${scriptPath}"`);
943
- shellExec(`sudo kubectl exec -i ${podName} -- sh -c "rm -rf ${checkPath}"`);
944
-
945
- {
946
- const checkPath = `/latent_space_plot.png`;
947
- const outsPaths = [];
948
- logger.info('monitor', checkPath);
949
- while (!Underpost.deploy.existsContainerFile({ podName, path: `/home/dd/docs${checkPath}` }))
950
- await timer(1000);
951
-
952
- {
953
- const toPath = `${basePath}/lab${checkPath}`;
954
- outsPaths.push(toPath);
955
- shellExec(`sudo kubectl cp ${nameSpace}/${podName}:${basePath}/docs${checkPath} ${toPath}`);
956
- }
957
-
958
- for (let i of range(1, 10)) {
959
- i = `/image_at_epoch_${setPad(i, '0', 4)}.png`;
960
- const toPath = `${basePath}/lab/${i}`;
961
- outsPaths.push(toPath);
962
- shellExec(`sudo kubectl cp ${nameSpace}/${podName}:${basePath}/docs${i} ${toPath}`);
963
- }
964
- openTerminal(`firefox ${outsPaths.join(' ')}`, { single: true });
965
- }
966
- shellExec(`sudo kill -9 ${pid}`);
967
- }
968
- break;
969
-
970
- default:
971
- break;
972
- }
973
- return;
974
- }
975
- await timer(1000);
976
- _monitor();
977
- };
978
- _monitor();
979
- },
980
1010
  /**
981
1011
  * @method db-client
982
1012
  * @description Deploys and exposes the Adminer database client application (using `adminer:4.7.6-standalone` image) on the cluster.
@@ -1034,15 +1064,20 @@ EOF
1034
1064
  `git config user.email '${email}' && ` +
1035
1065
  `git config credential.interactive always &&` +
1036
1066
  `git config pull.rebase false`,
1067
+ {
1068
+ disableLog: true,
1069
+ silent: true,
1070
+ },
1037
1071
  );
1038
1072
 
1039
- console.log(
1040
- shellExec(`git config list`, { silent: true, stdout: true })
1041
- .replaceAll('user.email', 'user.email'.yellow)
1042
- .replaceAll(username, username.green)
1043
- .replaceAll('user.name', 'user.name'.yellow)
1044
- .replaceAll(email, email.green),
1045
- );
1073
+ if (options.logs)
1074
+ console.log(
1075
+ shellExec(`git config list`, { silent: true, stdout: true })
1076
+ .replaceAll('user.email', 'user.email'.yellow)
1077
+ .replaceAll(username, username.green)
1078
+ .replaceAll('user.name', 'user.name'.yellow)
1079
+ .replaceAll(email, email.green),
1080
+ );
1046
1081
  },
1047
1082
 
1048
1083
  /**
@@ -1134,7 +1169,9 @@ EOF
1134
1169
  }
1135
1170
  await timer(5000);
1136
1171
  for (const deployId of deployList) {
1137
- shellExec(`${baseCommand} db ${deployId} --import --git --drop --preserveUUID --primary-pod`);
1172
+ shellExec(
1173
+ `${baseCommand} db ${deployId} --import --git --drop --preserveUUID --primary-pod${options.namespace ? ` --ns ${options.namespace}` : ''}`,
1174
+ );
1138
1175
  }
1139
1176
  await timer(5000);
1140
1177
  shellExec(`${baseCommand} cluster${baseClusterCommand} --${clusterType} --pull-image --valkey`);
@@ -1148,7 +1185,7 @@ EOF
1148
1185
  shellExec(
1149
1186
  `${baseCommand} deploy ${deployId} ${env} --${clusterType}${env === 'production' ? ' --cert' : ''}${
1150
1187
  env === 'development' ? ' --etc-hosts' : ''
1151
- }`,
1188
+ }${options.namespace ? ` --namespace ${options.namespace}` : ''}`,
1152
1189
  );
1153
1190
  }
1154
1191
  },
@@ -1165,7 +1202,7 @@ EOF
1165
1202
  if (!validVersion) throw new Error('Version mismatch');
1166
1203
  const currentTraffic = Underpost.deploy.getCurrentTraffic(deployId, { namespace: options.namespace });
1167
1204
  const targetTraffic = currentTraffic === 'blue' ? 'green' : 'blue';
1168
- const env = 'production';
1205
+ const env = options.dev ? 'development' : 'production';
1169
1206
  const ignorePods = Underpost.deploy
1170
1207
  .get(`${deployId}-${env}-${targetTraffic}`, 'pods', options.namespace)
1171
1208
  .map((p) => p.NAME);
@@ -1268,7 +1305,11 @@ EOF
1268
1305
  });
1269
1306
  }
1270
1307
  await awaitDeployMonitor(true);
1271
- shellExec(`npm run dev-proxy ${deployId} ${subConf} ${host} ${_path}${options.tls ? ' tls' : ''}`);
1308
+ shellExec(
1309
+ `./node_modules/.bin/env-cmd -f .env.development node src/proxy proxy ${deployId} ${subConf} ${host} ${_path}${
1310
+ options.tls ? ' tls' : ''
1311
+ }`,
1312
+ );
1272
1313
  },
1273
1314
 
1274
1315
  /**
@@ -1538,15 +1579,74 @@ EOF
1538
1579
  * @memberof UnderpostRun
1539
1580
  */
1540
1581
  'tf-vae-test': async (path, options = DEFAULT_OPTION) => {
1541
- const { underpostRoot } = options;
1542
1582
  const podName = 'tf-vae-test';
1543
1583
  await Underpost.run.CALL('deploy-job', '', {
1584
+ logs: options.logs,
1544
1585
  podName,
1545
1586
  // volumeMountPath: '/custom_images',
1546
1587
  // volumeHostPath: '/home/dd/engine/src/client/public/cyberia/assets/skin',
1547
1588
  on: {
1548
1589
  init: async () => {
1549
- openTerminal(`node bin run --dev monitor ${podName}`);
1590
+ // const pid = getTerminalPid();
1591
+ // shellExec(`sudo kill -9 ${pid}`);
1592
+ (async () => {
1593
+ const nameSpace = options.namespace;
1594
+ const basePath = '/home/dd';
1595
+ const scriptPath = '/site/en/tutorials/generative/cvae.py';
1596
+
1597
+ const { close } = await (async () => {
1598
+ const checkAwaitPath = '/await';
1599
+ while (!Underpost.deploy.existsContainerFile({ podName, path: checkAwaitPath })) {
1600
+ logger.info('monitor', checkAwaitPath);
1601
+ await timer(1000);
1602
+ }
1603
+
1604
+ return {
1605
+ close: () => shellExec(`sudo kubectl exec -i ${podName} -- sh -c "rm -rf ${checkAwaitPath}"`),
1606
+ };
1607
+ })();
1608
+
1609
+ const localScriptPath = `${basePath}/lab/src/${scriptPath.split('/').pop()}`;
1610
+ if (!fs.existsSync(localScriptPath)) {
1611
+ throw new Error(`Local override script not found: ${localScriptPath}`);
1612
+ }
1613
+
1614
+ shellExec(`sudo kubectl cp ${localScriptPath} ${nameSpace}/${podName}:${basePath}/docs${scriptPath}`);
1615
+
1616
+ close();
1617
+
1618
+ {
1619
+ const checkPath = `/latent_space_plot.png`;
1620
+ const outsPaths = [];
1621
+ const labDir = `${basePath}/lab`;
1622
+
1623
+ logger.info('monitor', checkPath);
1624
+ {
1625
+ const checkAwaitPath = `/home/dd/docs${checkPath}`;
1626
+ while (!Underpost.deploy.existsContainerFile({ podName, path: checkAwaitPath })) {
1627
+ logger.info('waiting for', checkAwaitPath);
1628
+ await timer(1000);
1629
+ }
1630
+ }
1631
+
1632
+ {
1633
+ const toPath = `${labDir}${checkPath}`;
1634
+ outsPaths.push(toPath);
1635
+ shellExec(`sudo kubectl cp ${nameSpace}/${podName}:${basePath}/docs${checkPath} ${toPath}`);
1636
+ }
1637
+
1638
+ for (let i of range(1, 10)) {
1639
+ const fileName = `image_at_epoch_${setPad(i, '0', 4)}.png`;
1640
+ const fromPath = `/${fileName}`;
1641
+ const toPath = `${labDir}/${fileName}`;
1642
+ outsPaths.push(toPath);
1643
+ shellExec(`sudo kubectl cp ${nameSpace}/${podName}:${basePath}/docs${fromPath} ${toPath}`);
1644
+ }
1645
+
1646
+ openTerminal(`firefox ${outsPaths.join(' ')}`, { single: true });
1647
+ process.exit(0);
1648
+ }
1649
+ })();
1550
1650
  },
1551
1651
  },
1552
1652
  args: [
@@ -1771,10 +1871,16 @@ EOF`;
1771
1871
  shellExec(`kubectl delete pod ${podName} -n ${namespace} --ignore-not-found`);
1772
1872
  console.log(cmd);
1773
1873
  shellExec(cmd, { disableLog: true });
1774
- const successInstance = await Underpost.test.statusMonitor(podName);
1874
+ const successInstance = await Underpost.test.statusMonitor(
1875
+ podName,
1876
+ options.monitorStatus || 'Running',
1877
+ options.monitorStatusKindType || 'pods',
1878
+ options.monitorStatusDeltaMs || 1000,
1879
+ options.monitorStatusMaxAttempts || 600,
1880
+ );
1775
1881
  if (successInstance) {
1776
1882
  options.on?.init ? await options.on.init() : null;
1777
- shellExec(`kubectl logs -f ${podName} -n ${namespace}`);
1883
+ if (options.logs) shellExec(`kubectl logs -f ${podName} -n ${namespace}`, { async: true });
1778
1884
  }
1779
1885
  },
1780
1886
  };
package/src/cli/ssh.js CHANGED
@@ -496,6 +496,55 @@ EOF`);
496
496
  if (options.status) shellExec('service sshd status');
497
497
  },
498
498
 
499
+ /**
500
+ * Generic SSH remote command runner that centralizes SSH execution logic.
501
+ * Executes arbitrary shell commands on a remote server via SSH with proper credential handling.
502
+ * @async
503
+ * @function sshRemoteRunner
504
+ * @param {string} remoteCommand - The command to execute on the remote server
505
+ * @param {Object} options - Configuration options for SSH execution
506
+ * @param {string} [options.deployId] - Deployment ID for credential lookup
507
+ * @param {string} [options.user] - SSH user for credential lookup
508
+ * @param {boolean} [options.dev=false] - Development mode flag
509
+ * @param {string} [options.cd='/home/dd/engine'] - Working directory on remote server
510
+ * @param {boolean} [options.useSudo=true] - Whether to use sudo for command execution
511
+ * @param {boolean} [options.remote=true] - Whether to execute as remote command (if false, runs locally)
512
+ * @returns {Promise<string>} Output from the shell execution
513
+ * @memberof UnderpostSSH
514
+ */
515
+ sshRemoteRunner: async (remoteCommand, options = {}) => {
516
+ const { deployId = '', user = '', dev = false, cd = '/home/dd/engine', useSudo = true, remote = true } = options;
517
+
518
+ // If not executing remotely, just run locally
519
+ if (!remote) {
520
+ return shellExec(remoteCommand);
521
+ }
522
+
523
+ // Set up SSH credentials from config
524
+ if (deployId && user) {
525
+ await Underpost.ssh.setDefautlSshCredentials({ deployId, user });
526
+ }
527
+
528
+ // Build the complete SSH command
529
+ const sshScript = `#!/usr/bin/env bash
530
+ set -euo pipefail
531
+
532
+ REMOTE_USER=$(node bin config get --plain DEFAULT_SSH_USER)
533
+ REMOTE_HOST=$(node bin config get --plain DEFAULT_SSH_HOST)
534
+ REMOTE_PORT=$(node bin config get --plain DEFAULT_SSH_PORT)
535
+ SSH_KEY=$(node bin config get --plain DEFAULT_SSH_KEY_PATH)
536
+
537
+ chmod 600 "$SSH_KEY"
538
+
539
+ ssh -i "$SSH_KEY" -o BatchMode=yes "$REMOTE_USER@$REMOTE_HOST" -p $REMOTE_PORT sh <<EOF
540
+ ${cd ? `cd ${cd}` : ''}
541
+ ${useSudo ? `sudo -n -- /bin/bash -lc "${remoteCommand}"` : remoteCommand}
542
+ EOF
543
+ `;
544
+
545
+ return shellExec(sshScript, { stdout: true });
546
+ },
547
+
499
548
  /**
500
549
  * Loads saved SSH credentials from config and sets them in the UnderpostRootEnv API.
501
550
  * @async
package/src/cli/test.js CHANGED
@@ -59,7 +59,19 @@ class UnderpostTest {
59
59
  * @param {number} options.maxAttempts - The maximum number of attempts.
60
60
  * @memberof UnderpostTest
61
61
  */
62
- async callback(deployList = '', options = { itc: false, sh: false, logs: false }) {
62
+ async callback(
63
+ deployList = '',
64
+ options = {
65
+ itc: false,
66
+ sh: false,
67
+ logs: false,
68
+ podName: '',
69
+ podStatus: '',
70
+ kindType: '',
71
+ deltaMs: 1000,
72
+ maxAttempts: 60 * 5,
73
+ },
74
+ ) {
63
75
  if (
64
76
  options.podName &&
65
77
  typeof options.podName === 'string' &&