underpost 3.1.3 → 3.2.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.
- package/.env.example +0 -2
- package/.github/workflows/ghpkg.ci.yml +4 -4
- package/.github/workflows/npmpkg.ci.yml +28 -11
- package/.github/workflows/pwa-microservices-template-page.cd.yml +3 -4
- package/.github/workflows/pwa-microservices-template-test.ci.yml +3 -3
- package/.github/workflows/release.cd.yml +4 -4
- package/CHANGELOG.md +324 -1
- package/CLI-HELP.md +49 -3
- package/README.md +3 -2
- package/bin/build.js +18 -12
- package/bin/deploy.js +177 -124
- package/bin/file.js +3 -0
- package/conf.js +3 -2
- package/manifests/cronjobs/dd-cron/dd-cron-backup.yaml +1 -1
- package/manifests/cronjobs/dd-cron/dd-cron-dns.yaml +1 -1
- package/manifests/deployment/dd-default-development/deployment.yaml +2 -2
- package/manifests/deployment/dd-test-development/deployment.yaml +72 -50
- package/manifests/deployment/dd-test-development/proxy.yaml +13 -4
- package/manifests/deployment/playwright/deployment.yaml +1 -1
- package/nodemon.json +1 -1
- package/package.json +22 -15
- package/scripts/rhel-grpc-setup.sh +56 -0
- package/src/api/file/file.ref.json +18 -0
- package/src/api/user/user.service.js +8 -7
- package/src/cli/cluster.js +7 -7
- package/src/cli/db.js +76 -242
- package/src/cli/deploy.js +104 -65
- package/src/cli/env.js +1 -0
- package/src/cli/fs.js +2 -1
- package/src/cli/index.js +42 -1
- package/src/cli/kubectl.js +211 -0
- package/src/cli/release.js +284 -0
- package/src/cli/repository.js +291 -75
- package/src/cli/run.js +188 -33
- package/src/cli/test.js +3 -3
- package/src/client/Default.index.js +3 -4
- package/src/client/components/core/AppStore.js +69 -0
- package/src/client/components/core/CalendarCore.js +2 -2
- package/src/client/components/core/DropDown.js +129 -17
- package/src/client/components/core/Keyboard.js +2 -2
- package/src/client/components/core/LogIn.js +2 -2
- package/src/client/components/core/LogOut.js +2 -2
- package/src/client/components/core/Modal.js +0 -1
- package/src/client/components/core/Panel.js +0 -1
- package/src/client/components/core/PanelForm.js +19 -19
- package/src/client/components/core/SocketIo.js +82 -29
- package/src/client/components/core/SocketIoHandler.js +75 -0
- package/src/client/components/core/Stream.js +143 -95
- package/src/client/components/core/Webhook.js +40 -7
- package/src/client/components/default/AppStoreDefault.js +5 -0
- package/src/client/components/default/LogInDefault.js +3 -3
- package/src/client/components/default/LogOutDefault.js +2 -2
- package/src/client/components/default/MenuDefault.js +5 -5
- package/src/client/components/default/SocketIoDefault.js +3 -51
- package/src/client/services/core/core.service.js +20 -8
- package/src/client/services/user/user.management.js +2 -2
- package/src/index.js +24 -1
- package/src/runtime/express/Express.js +18 -1
- package/src/runtime/lampp/Dockerfile +9 -2
- package/src/runtime/lampp/Lampp.js +4 -3
- package/src/runtime/wp/Dockerfile +64 -0
- package/src/runtime/wp/Wp.js +497 -0
- package/src/server/auth.js +24 -1
- package/src/server/backup.js +19 -1
- package/src/server/client-build-docs.js +9 -2
- package/src/server/client-build.js +31 -31
- package/src/server/client-formatted.js +109 -57
- package/src/server/ipfs-client.js +24 -1
- package/src/server/peer.js +8 -0
- package/src/server/runtime.js +25 -1
- package/src/server/start.js +6 -0
- package/src/ws/IoInterface.js +1 -10
- package/src/ws/IoServer.js +14 -33
- package/src/ws/core/channels/core.ws.chat.js +65 -20
- package/src/ws/core/channels/core.ws.mailer.js +113 -32
- package/src/ws/core/channels/core.ws.stream.js +90 -31
- package/src/ws/core/core.ws.connection.js +12 -33
- package/src/ws/core/core.ws.emit.js +10 -26
- package/src/ws/core/core.ws.server.js +25 -58
- package/src/ws/default/channels/default.ws.main.js +53 -12
- package/src/ws/default/default.ws.connection.js +26 -13
- package/src/ws/default/default.ws.server.js +30 -12
- package/src/client/components/default/ElementsDefault.js +0 -38
- package/src/ws/core/management/core.ws.chat.js +0 -8
- package/src/ws/core/management/core.ws.mailer.js +0 -16
- package/src/ws/core/management/core.ws.stream.js +0 -8
- package/src/ws/default/management/default.ws.main.js +0 -8
package/src/cli/deploy.js
CHANGED
|
@@ -155,6 +155,7 @@ metadata:
|
|
|
155
155
|
namespace: ${namespace ? namespace : 'default'}
|
|
156
156
|
labels:
|
|
157
157
|
app: ${deployId}-${env}-${suffix}
|
|
158
|
+
deploy-id: ${deployId}-${env}
|
|
158
159
|
spec:
|
|
159
160
|
replicas: ${replicas}
|
|
160
161
|
selector:
|
|
@@ -164,6 +165,7 @@ spec:
|
|
|
164
165
|
metadata:
|
|
165
166
|
labels:
|
|
166
167
|
app: ${deployId}-${env}-${suffix}
|
|
168
|
+
deploy-id: ${deployId}-${env}
|
|
167
169
|
spec:
|
|
168
170
|
containers:
|
|
169
171
|
- name: ${deployId}-${env}-${suffix}
|
|
@@ -260,6 +262,14 @@ ${Underpost.deploy
|
|
|
260
262
|
}
|
|
261
263
|
fs.writeFileSync(`./engine-private/conf/${deployId}/build/${env}/deployment.yaml`, deploymentYamlParts, 'utf8');
|
|
262
264
|
|
|
265
|
+
Underpost.deploy.buildGrpcServiceManifest({
|
|
266
|
+
deployId,
|
|
267
|
+
env,
|
|
268
|
+
confServer,
|
|
269
|
+
namespace: options.namespace,
|
|
270
|
+
traffic: options.traffic && typeof options.traffic === 'string' ? options.traffic.split(',') : ['blue'],
|
|
271
|
+
});
|
|
272
|
+
|
|
263
273
|
const confVolume = fs.existsSync(`./engine-private/conf/${deployId}/conf.volume.json`)
|
|
264
274
|
? JSON.parse(fs.readFileSync(`./engine-private/conf/${deployId}/conf.volume.json`, 'utf8'))
|
|
265
275
|
: [];
|
|
@@ -369,6 +379,67 @@ ${Underpost.deploy
|
|
|
369
379
|
}
|
|
370
380
|
}
|
|
371
381
|
},
|
|
382
|
+
/**
|
|
383
|
+
* Builds and writes a gRPC ClusterIP service YAML for a deployment.
|
|
384
|
+
* Scans conf.server.json for gRPC ports and emits grpc-service.yaml under
|
|
385
|
+
* `engine-private/conf/<deployId>/build/<env>/`. The selector always uses the
|
|
386
|
+
* explicit `app: <deployId>-<env>-<traffic>` label to target only the active
|
|
387
|
+
* colour (blue or green).
|
|
388
|
+
* @param {string} deployId - Deployment ID.
|
|
389
|
+
* @param {string} env - Environment ('development' or 'production').
|
|
390
|
+
* @param {object} confServer - Parsed conf.server.json content.
|
|
391
|
+
* @param {string} [namespace='default'] - Kubernetes namespace.
|
|
392
|
+
* @param {string[]} [traffic=['blue']] - Active traffic colour(s) ('blue', 'green', or both).
|
|
393
|
+
* @param {string|null} [host=null] - Specific host to scan for gRPC ports. If null, all hosts are scanned.
|
|
394
|
+
* @returns {string|null} - Path to the written YAML file, or null if no gRPC ports found.
|
|
395
|
+
* @memberof UnderpostDeploy
|
|
396
|
+
*/
|
|
397
|
+
buildGrpcServiceManifest({ deployId, env, confServer, namespace = 'default', traffic = ['blue'], host = null }) {
|
|
398
|
+
const grpcPorts = new Set();
|
|
399
|
+
const hostsToScan = host ? [host] : Object.keys(confServer);
|
|
400
|
+
for (const h of hostsToScan) {
|
|
401
|
+
if (!confServer[h]) continue;
|
|
402
|
+
for (const path of Object.keys(confServer[h])) {
|
|
403
|
+
const grpc = confServer[h][path].grpc;
|
|
404
|
+
if (grpc && grpc.port) grpcPorts.add(parseInt(grpc.port));
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
if (grpcPorts.size === 0) return null;
|
|
408
|
+
const grpcPortsList = [...grpcPorts]
|
|
409
|
+
.map(
|
|
410
|
+
(port) => ` - name: grpc-${port}
|
|
411
|
+
protocol: TCP
|
|
412
|
+
port: ${port}
|
|
413
|
+
targetPort: ${port}`,
|
|
414
|
+
)
|
|
415
|
+
.join('\n');
|
|
416
|
+
let grpcServiceYaml = '';
|
|
417
|
+
for (const color of traffic) {
|
|
418
|
+
const grpcServiceName = `${deployId}-grpc-service-${env}-${color}`;
|
|
419
|
+
const selectorYaml = `app: ${deployId}-${env}-${color}`;
|
|
420
|
+
grpcServiceYaml += `---
|
|
421
|
+
apiVersion: v1
|
|
422
|
+
kind: Service
|
|
423
|
+
metadata:
|
|
424
|
+
name: ${grpcServiceName}
|
|
425
|
+
namespace: ${namespace}
|
|
426
|
+
labels:
|
|
427
|
+
app: ${grpcServiceName}
|
|
428
|
+
spec:
|
|
429
|
+
type: ClusterIP
|
|
430
|
+
selector:
|
|
431
|
+
${selectorYaml}
|
|
432
|
+
ports:
|
|
433
|
+
${grpcPortsList}
|
|
434
|
+
`;
|
|
435
|
+
logger.info(
|
|
436
|
+
`gRPC ClusterIP service YAML written: ${grpcServiceName} (selector: ${selectorYaml}, ports: ${[...grpcPorts].join(', ')})`,
|
|
437
|
+
);
|
|
438
|
+
}
|
|
439
|
+
const yamlPath = `./engine-private/conf/${deployId}/build/${env}/grpc-service.yaml`;
|
|
440
|
+
fs.writeFileSync(yamlPath, grpcServiceYaml, 'utf8');
|
|
441
|
+
return yamlPath;
|
|
442
|
+
},
|
|
372
443
|
/**
|
|
373
444
|
* Builds a Certificate resource for a host using cert-manager.
|
|
374
445
|
* @param {string} host - Hostname for which the certificate is being built.
|
|
@@ -478,6 +549,10 @@ spec:
|
|
|
478
549
|
* @param {string} [options.kindType] - Type of Kubernetes resource to retrieve information for.
|
|
479
550
|
* @param {number} [options.port] - Port number for exposing the deployment.
|
|
480
551
|
* @param {string} [options.cmd] - Custom initialization command for deploymentYamlPartsFactory (comma-separated commands).
|
|
552
|
+
* @param {boolean} [options.k3s] - Whether to use k3s cluster context.
|
|
553
|
+
* @param {boolean} [options.kubeadm] - Whether to use kubeadm cluster context.
|
|
554
|
+
* @param {boolean} [options.kind] - Whether to use kind cluster context.
|
|
555
|
+
* @param {boolean} [options.gitClean] - Whether to run git clean on volume mount paths before copying.
|
|
481
556
|
* @returns {Promise<void>} - Promise that resolves when the deployment process is complete.
|
|
482
557
|
* @memberof UnderpostDeploy
|
|
483
558
|
*/
|
|
@@ -515,6 +590,10 @@ spec:
|
|
|
515
590
|
port: 0,
|
|
516
591
|
exposePort: 0,
|
|
517
592
|
cmd: '',
|
|
593
|
+
k3s: false,
|
|
594
|
+
kubeadm: false,
|
|
595
|
+
kind: false,
|
|
596
|
+
gitClean: false,
|
|
518
597
|
},
|
|
519
598
|
) {
|
|
520
599
|
const namespace = options.namespace ? options.namespace : 'default';
|
|
@@ -553,7 +632,7 @@ EOF`);
|
|
|
553
632
|
env,
|
|
554
633
|
traffic: Underpost.deploy.getCurrentTraffic(deployId, { namespace }),
|
|
555
634
|
router: await Underpost.deploy.routerFactory(deployId, env),
|
|
556
|
-
pods: await Underpost.
|
|
635
|
+
pods: await Underpost.kubectl.get(deployId),
|
|
557
636
|
instances,
|
|
558
637
|
});
|
|
559
638
|
}
|
|
@@ -595,7 +674,7 @@ EOF`);
|
|
|
595
674
|
if (!deployId) continue;
|
|
596
675
|
if (options.expose === true) {
|
|
597
676
|
const kindType = options.kindType ? options.kindType : 'svc';
|
|
598
|
-
const svc = Underpost.
|
|
677
|
+
const svc = Underpost.kubectl.get(deployId, kindType)[0];
|
|
599
678
|
const port = options.exposePort
|
|
600
679
|
? parseInt(options.exposePort)
|
|
601
680
|
: options.port
|
|
@@ -634,6 +713,8 @@ EOF`);
|
|
|
634
713
|
version,
|
|
635
714
|
namespace,
|
|
636
715
|
nodeName: options.node ? options.node : env === 'development' ? 'kind-worker' : os.hostname(),
|
|
716
|
+
clusterContext: options.k3s ? 'k3s' : options.kubeadm ? 'kubeadm' : 'kind',
|
|
717
|
+
gitClean: options.gitClean || false,
|
|
637
718
|
});
|
|
638
719
|
}
|
|
639
720
|
|
|
@@ -652,8 +733,11 @@ EOF`);
|
|
|
652
733
|
: `manifests/deployment/${deployId}-${env}`;
|
|
653
734
|
|
|
654
735
|
if (!options.remove) {
|
|
655
|
-
if (!options.disableUpdateDeployment)
|
|
736
|
+
if (!options.disableUpdateDeployment) {
|
|
656
737
|
shellExec(`sudo kubectl apply -f ./${manifestsPath}/deployment.yaml -n ${namespace}`);
|
|
738
|
+
const grpcServicePath = `./${manifestsPath}/grpc-service.yaml`;
|
|
739
|
+
if (fs.existsSync(grpcServicePath)) shellExec(`sudo kubectl apply -f ${grpcServicePath} -n ${namespace}`);
|
|
740
|
+
}
|
|
657
741
|
if (!options.disableUpdateProxy)
|
|
658
742
|
shellExec(`sudo kubectl apply -f ./${manifestsPath}/proxy.yaml -n ${namespace}`);
|
|
659
743
|
|
|
@@ -671,65 +755,6 @@ EOF`);
|
|
|
671
755
|
` + renderHosts,
|
|
672
756
|
);
|
|
673
757
|
},
|
|
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
758
|
/**
|
|
734
759
|
* Checks the status of a deployment.
|
|
735
760
|
* @param {string} deployId - Deployment ID for which the status is being checked.
|
|
@@ -742,7 +767,7 @@ EOF`);
|
|
|
742
767
|
*/
|
|
743
768
|
async checkDeploymentReadyStatus(deployId, env, traffic, ignoresNames = [], namespace = 'default') {
|
|
744
769
|
const cmd = `underpost config get container-status`;
|
|
745
|
-
const pods = Underpost.
|
|
770
|
+
const pods = Underpost.kubectl.get(`${deployId}-${env}-${traffic}`, 'pods', namespace);
|
|
746
771
|
const readyPods = [];
|
|
747
772
|
const notReadyPods = [];
|
|
748
773
|
for (const pod of pods) {
|
|
@@ -814,6 +839,9 @@ EOF`);
|
|
|
814
839
|
|
|
815
840
|
shellExec(`sudo kubectl apply -f ./engine-private/conf/${deployId}/build/${env}/proxy.yaml -n ${namespace}`);
|
|
816
841
|
|
|
842
|
+
const grpcServicePath = `./engine-private/conf/${deployId}/build/${env}/grpc-service.yaml`;
|
|
843
|
+
if (fs.existsSync(grpcServicePath)) shellExec(`kubectl apply -f ${grpcServicePath} -n ${namespace}`);
|
|
844
|
+
|
|
817
845
|
Underpost.env.set(`${deployId}-${env}-traffic`, targetTraffic);
|
|
818
846
|
},
|
|
819
847
|
|
|
@@ -829,6 +857,8 @@ EOF`);
|
|
|
829
857
|
* @param {string} options.version - Version of the deployment.
|
|
830
858
|
* @param {string} options.namespace - Kubernetes namespace for the deployment.
|
|
831
859
|
* @param {string} options.nodeName - Node name for the deployment.
|
|
860
|
+
* @param {string} [options.clusterContext='kind'] - Cluster context type ('kind', 'kubeadm', or 'k3s').
|
|
861
|
+
* @param {boolean} [options.gitClean=false] - Whether to run git clean on volumeMountPath before copying.
|
|
832
862
|
* @memberof UnderpostDeploy
|
|
833
863
|
*/
|
|
834
864
|
deployVolume(
|
|
@@ -839,6 +869,8 @@ EOF`);
|
|
|
839
869
|
version: '',
|
|
840
870
|
namespace: '',
|
|
841
871
|
nodeName: '',
|
|
872
|
+
clusterContext: 'kind',
|
|
873
|
+
gitClean: false,
|
|
842
874
|
},
|
|
843
875
|
) {
|
|
844
876
|
if (!volume.claimName) {
|
|
@@ -846,19 +878,26 @@ EOF`);
|
|
|
846
878
|
return;
|
|
847
879
|
}
|
|
848
880
|
const { deployId, env, version, namespace } = options;
|
|
881
|
+
const clusterContext = options.clusterContext || 'kind';
|
|
849
882
|
const pvcId = `${volume.claimName}-${deployId}-${env}-${version}`;
|
|
850
883
|
const pvId = `${volume.claimName.replace('pvc-', 'pv-')}-${deployId}-${env}-${version}`;
|
|
851
884
|
const rootVolumeHostPath = `/home/dd/engine/volume/${pvId}`;
|
|
885
|
+
if (options.gitClean && volume.volumeMountPath) {
|
|
886
|
+
Underpost.repo.clean({ paths: [volume.volumeMountPath] });
|
|
887
|
+
}
|
|
852
888
|
if (options.nodeName) {
|
|
853
889
|
if (!fs.existsSync(rootVolumeHostPath)) fs.mkdirSync(rootVolumeHostPath, { recursive: true });
|
|
854
890
|
fs.copySync(volume.volumeMountPath, rootVolumeHostPath);
|
|
855
|
-
} else {
|
|
891
|
+
} else if (clusterContext === 'kind') {
|
|
856
892
|
shellExec(`docker exec -i kind-worker bash -c "mkdir -p ${rootVolumeHostPath}"`);
|
|
857
893
|
// shellExec(`docker cp ${volume.volumeMountPath} kind-worker:${rootVolumeHostPath}`);
|
|
858
894
|
shellExec(`tar -C ${volume.volumeMountPath} -c . | docker cp - kind-worker:${rootVolumeHostPath}`);
|
|
859
895
|
shellExec(
|
|
860
896
|
`docker exec -i kind-worker bash -c "chown -R 1000:1000 ${rootVolumeHostPath}; chmod -R 755 ${rootVolumeHostPath}"`,
|
|
861
897
|
);
|
|
898
|
+
} else {
|
|
899
|
+
if (!fs.existsSync(rootVolumeHostPath)) fs.mkdirSync(rootVolumeHostPath, { recursive: true });
|
|
900
|
+
fs.copySync(volume.volumeMountPath, rootVolumeHostPath);
|
|
862
901
|
}
|
|
863
902
|
shellExec(`kubectl delete pvc ${pvcId} -n ${namespace} --ignore-not-found`);
|
|
864
903
|
shellExec(`kubectl delete pv ${pvId} --ignore-not-found`);
|
|
@@ -1041,7 +1080,7 @@ ${renderHosts}`,
|
|
|
1041
1080
|
async monitorReadyRunner(deployId, env, targetTraffic, ignorePods = [], namespace = 'default', outLogType = '') {
|
|
1042
1081
|
let checkStatusIteration = 0;
|
|
1043
1082
|
const checkStatusIterationMsDelay = 1000;
|
|
1044
|
-
const maxIterations =
|
|
1083
|
+
const maxIterations = 3000;
|
|
1045
1084
|
const deploymentId = `${deployId}-${env}-${targetTraffic}`;
|
|
1046
1085
|
const iteratorTag = `[${deploymentId}]`;
|
|
1047
1086
|
logger.info('Deployment init', { deployId, env, targetTraffic, checkStatusIterationMsDelay, namespace });
|
package/src/cli/env.js
CHANGED
package/src/cli/fs.js
CHANGED
|
@@ -107,7 +107,8 @@ class UnderpostFileStorage {
|
|
|
107
107
|
await Underpost.fs.pull(_path, options);
|
|
108
108
|
} else logger.warn(`Pull path already exists`, _path);
|
|
109
109
|
}
|
|
110
|
-
|
|
110
|
+
Underpost.repo.initLocalRepo({ path });
|
|
111
|
+
shellExec(`cd ${path} && git add . && git commit -m "Base pull state"`);
|
|
111
112
|
} else {
|
|
112
113
|
const files =
|
|
113
114
|
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
|
|
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.')
|
|
@@ -597,6 +607,7 @@ program
|
|
|
597
607
|
.option('--kubeadm', 'Sets the kubeadm cluster context for the runner execution.')
|
|
598
608
|
.option('--k3s', 'Sets the k3s cluster context for the runner execution.')
|
|
599
609
|
.option('--kind', 'Sets the kind cluster context for the runner execution.')
|
|
610
|
+
.option('--git-clean', 'Runs git clean on volume mount paths before copying.')
|
|
600
611
|
.option('--log-type <log-type>', 'Sets the log type for the runner execution.')
|
|
601
612
|
.option('--deploy-id <deploy-id>', 'Sets deploy id context for the runner execution.')
|
|
602
613
|
.option('--user <user>', 'Sets user context for the runner execution.')
|
|
@@ -640,6 +651,7 @@ program
|
|
|
640
651
|
'Format: semicolon-separated entries of "ip=hostname1,hostname2" ' +
|
|
641
652
|
'(e.g., "127.0.0.1=foo.local,bar.local;10.1.2.3=foo.remote,bar.remote").',
|
|
642
653
|
)
|
|
654
|
+
.option('--copy', 'Copies the runner output to the clipboard (supported by: generate-pass, template-deploy-local).')
|
|
643
655
|
.description('Runs specified scripts using various runners.')
|
|
644
656
|
.action(Underpost.run.callback);
|
|
645
657
|
|
|
@@ -764,4 +776,33 @@ program
|
|
|
764
776
|
)
|
|
765
777
|
.action(Underpost.baremetal.callback);
|
|
766
778
|
|
|
779
|
+
program
|
|
780
|
+
.command('release')
|
|
781
|
+
.argument('[version]', 'The new version string to set (e.g., "3.1.4"). Defaults to current version.')
|
|
782
|
+
.option('--build', 'Builds a new version: tests template, bumps versions, rebuilds manifests and configs.')
|
|
783
|
+
.option('--deploy', 'Deploys the release: syncs secrets, commits, and pushes to remote repositories.')
|
|
784
|
+
.option(
|
|
785
|
+
'--ci-push <deploy-id>',
|
|
786
|
+
'Local equivalent of engine-*.ci.yml: builds dd-{deploy-id} and pushes to the engine-{deploy-id} repository. ' +
|
|
787
|
+
'Accepts the suffix (e.g., "cyberia"), "dd-cyberia", or "engine-cyberia".',
|
|
788
|
+
)
|
|
789
|
+
.option(
|
|
790
|
+
'--message <message>',
|
|
791
|
+
'Commit message for --ci-push or --pwa-build (defaults to last commit of the engine repository).',
|
|
792
|
+
)
|
|
793
|
+
.option(
|
|
794
|
+
'--pwa-build',
|
|
795
|
+
'Runs the pwa-microservices-template update flow: always re-clones, syncs engine sources, installs, builds, and pushes.',
|
|
796
|
+
)
|
|
797
|
+
.description('Release orchestrator for building new versions and deploying releases of the Underpost CLI.')
|
|
798
|
+
.action(async (version, options) => {
|
|
799
|
+
if (options.build) return Underpost.release.build(version, options);
|
|
800
|
+
if (options.deploy) return Underpost.release.deploy(version, options);
|
|
801
|
+
if (options.ciPush) return Underpost.release.ci(options.ciPush, options.message, options);
|
|
802
|
+
if (options.pwaBuild) return Underpost.release.pwa(options.message, options);
|
|
803
|
+
console.log(
|
|
804
|
+
'Please specify --build, --deploy, --ci-push, or --pwa-build. Use "underpost release --help" for details.',
|
|
805
|
+
);
|
|
806
|
+
});
|
|
807
|
+
|
|
767
808
|
export { program };
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Kubectl module providing low-level Kubernetes resource management primitives.
|
|
3
|
+
* Centralises pod querying, file transfer, and in-container execution operations
|
|
4
|
+
* that were previously scattered across db, deploy, and cluster modules.
|
|
5
|
+
* @module src/cli/kubectl.js
|
|
6
|
+
* @namespace UnderpostKubectl
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { loggerFactory } from '../server/logger.js';
|
|
10
|
+
import { shellExec } from '../server/process.js';
|
|
11
|
+
import Underpost from '../index.js';
|
|
12
|
+
|
|
13
|
+
const logger = loggerFactory(import.meta);
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Redacts credentials from shell command strings before logging.
|
|
17
|
+
* Masks passwords in `-p<password>`, `--password=<password>`, and `-P <password>` patterns.
|
|
18
|
+
* @param {string} cmd - The raw command string.
|
|
19
|
+
* @returns {string} The command with credentials replaced by `***`.
|
|
20
|
+
* @memberof UnderpostKubectl
|
|
21
|
+
*/
|
|
22
|
+
const sanitizeCommand = (cmd) => {
|
|
23
|
+
if (typeof cmd !== 'string') return cmd;
|
|
24
|
+
return cmd
|
|
25
|
+
.replace(/-p['"]?[^\s'"]+/g, '-p***')
|
|
26
|
+
.replace(/--password=['"]?[^\s'"]+/g, '--password=***')
|
|
27
|
+
.replace(/-P\s+['"]?[^\s'"]+/g, '-P ***');
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* @class UnderpostKubectl
|
|
32
|
+
* @description Kubernetes cluster resource management primitives.
|
|
33
|
+
* Provides a unified interface for kubectl operations: resource listing, in-pod
|
|
34
|
+
* command execution, file transfer, and pod discovery/filtering.
|
|
35
|
+
* All methods are stateless and safe to call from any other CLI module.
|
|
36
|
+
* @memberof UnderpostKubectl
|
|
37
|
+
*/
|
|
38
|
+
class UnderpostKubectl {
|
|
39
|
+
static API = {
|
|
40
|
+
/**
|
|
41
|
+
* Lists Kubernetes resources matching `deployId`, parsed into plain objects.
|
|
42
|
+
* Equivalent to `kubectl get <kindType> -o wide`, filtered by name substring.
|
|
43
|
+
* @param {string} deployId - Substring to match against resource names. Empty string returns all.
|
|
44
|
+
* @param {string} [kindType='pods'] - Resource kind: pods, deployments, svc, nodes, …
|
|
45
|
+
* @param {string} [namespace=''] - Namespace to query; empty string → --all-namespaces.
|
|
46
|
+
* @returns {Array<object>} Parsed rows keyed by column header (NAME, STATUS, NODE, …).
|
|
47
|
+
* @memberof UnderpostKubectl
|
|
48
|
+
*/
|
|
49
|
+
get(deployId, kindType = 'pods', namespace = '') {
|
|
50
|
+
const raw = shellExec(
|
|
51
|
+
`sudo kubectl get ${kindType}${namespace ? ` -n ${namespace}` : ` --all-namespaces`} -o wide`,
|
|
52
|
+
{ stdout: true, disableLog: true, silent: true },
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
const heads = raw
|
|
56
|
+
.split(`\n`)[0]
|
|
57
|
+
.split(' ')
|
|
58
|
+
.filter((_r) => _r.trim());
|
|
59
|
+
|
|
60
|
+
const pods = raw
|
|
61
|
+
.split(`\n`)
|
|
62
|
+
.filter((r) => (deployId ? r.match(deployId) : r.trim() && !r.match('NAME')))
|
|
63
|
+
.map((r) => r.split(' ').filter((_r) => _r.trim()));
|
|
64
|
+
|
|
65
|
+
const result = [];
|
|
66
|
+
for (const row of pods) {
|
|
67
|
+
const pod = {};
|
|
68
|
+
let index = -1;
|
|
69
|
+
for (const head of heads) {
|
|
70
|
+
index++;
|
|
71
|
+
pod[head] = row[index];
|
|
72
|
+
}
|
|
73
|
+
result.push(pod);
|
|
74
|
+
}
|
|
75
|
+
return result;
|
|
76
|
+
},
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Executes a kubectl command with credential-safe logging and error propagation.
|
|
80
|
+
* @param {string} command - Full kubectl command string.
|
|
81
|
+
* @param {object} [options={}] - Execution options.
|
|
82
|
+
* @param {string} [options.context=''] - Human-readable label for log messages.
|
|
83
|
+
* @returns {string} stdout output from the command.
|
|
84
|
+
* @throws {Error} Re-throws any execution error after logging.
|
|
85
|
+
* @memberof UnderpostKubectl
|
|
86
|
+
*/
|
|
87
|
+
run(command, options = {}) {
|
|
88
|
+
const { context = '' } = options;
|
|
89
|
+
try {
|
|
90
|
+
logger.info(`Executing kubectl command`, { command: sanitizeCommand(command), context });
|
|
91
|
+
return shellExec(command, { stdout: true, disableLog: true });
|
|
92
|
+
} catch (error) {
|
|
93
|
+
logger.error(`kubectl command failed`, { command: sanitizeCommand(command), error: error.message, context });
|
|
94
|
+
throw error;
|
|
95
|
+
}
|
|
96
|
+
},
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Runs a shell command inside a pod container via `kubectl exec`.
|
|
100
|
+
* @param {object} params
|
|
101
|
+
* @param {string} params.podName - Target pod name.
|
|
102
|
+
* @param {string} params.namespace - Pod namespace.
|
|
103
|
+
* @param {string} params.command - Shell command to run inside the container.
|
|
104
|
+
* @returns {string} stdout output from the in-pod command.
|
|
105
|
+
* @throws {Error} Re-throws any execution error after logging.
|
|
106
|
+
* @memberof UnderpostKubectl
|
|
107
|
+
*/
|
|
108
|
+
exec({ podName, namespace, command }) {
|
|
109
|
+
try {
|
|
110
|
+
const kubectlCmd = `sudo kubectl exec -n ${namespace} -i ${podName} -- sh -c "${command}"`;
|
|
111
|
+
return Underpost.kubectl.run(kubectlCmd, { context: `exec in pod ${podName}` });
|
|
112
|
+
} catch (error) {
|
|
113
|
+
logger.error('Failed to execute command in pod', {
|
|
114
|
+
podName,
|
|
115
|
+
command: sanitizeCommand(command),
|
|
116
|
+
error: error.message,
|
|
117
|
+
});
|
|
118
|
+
throw error;
|
|
119
|
+
}
|
|
120
|
+
},
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Copies a local file into a pod via `kubectl cp`.
|
|
124
|
+
* @param {object} params
|
|
125
|
+
* @param {string} params.sourcePath - Local source path.
|
|
126
|
+
* @param {string} params.podName - Target pod name.
|
|
127
|
+
* @param {string} params.namespace - Pod namespace.
|
|
128
|
+
* @param {string} params.destPath - Destination path inside the container.
|
|
129
|
+
* @returns {boolean} `true` on success, `false` on error.
|
|
130
|
+
* @memberof UnderpostKubectl
|
|
131
|
+
*/
|
|
132
|
+
cpTo({ sourcePath, podName, namespace, destPath }) {
|
|
133
|
+
try {
|
|
134
|
+
const command = `sudo kubectl cp ${sourcePath} ${namespace}/${podName}:${destPath}`;
|
|
135
|
+
Underpost.kubectl.run(command, { context: `copy to pod ${podName}` });
|
|
136
|
+
return true;
|
|
137
|
+
} catch (error) {
|
|
138
|
+
logger.error('Failed to copy file to pod', { sourcePath, podName, destPath, error: error.message });
|
|
139
|
+
return false;
|
|
140
|
+
}
|
|
141
|
+
},
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Copies a file from a pod to the local filesystem via `kubectl cp`.
|
|
145
|
+
* @param {object} params
|
|
146
|
+
* @param {string} params.podName - Source pod name.
|
|
147
|
+
* @param {string} params.namespace - Pod namespace.
|
|
148
|
+
* @param {string} params.sourcePath - Source path inside the container.
|
|
149
|
+
* @param {string} params.destPath - Local destination path.
|
|
150
|
+
* @returns {boolean} `true` on success, `false` on error.
|
|
151
|
+
* @memberof UnderpostKubectl
|
|
152
|
+
*/
|
|
153
|
+
cpFrom({ podName, namespace, sourcePath, destPath }) {
|
|
154
|
+
try {
|
|
155
|
+
const command = `sudo kubectl cp ${namespace}/${podName}:${sourcePath} ${destPath}`;
|
|
156
|
+
Underpost.kubectl.run(command, { context: `copy from pod ${podName}` });
|
|
157
|
+
return true;
|
|
158
|
+
} catch (error) {
|
|
159
|
+
logger.error('Failed to copy file from pod', { podName, sourcePath, destPath, error: error.message });
|
|
160
|
+
return false;
|
|
161
|
+
}
|
|
162
|
+
},
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Checks whether a file exists inside a pod container.
|
|
166
|
+
* @param {object} params
|
|
167
|
+
* @param {string} params.podName - Pod name.
|
|
168
|
+
* @param {string} params.path - Absolute path inside the container to test.
|
|
169
|
+
* @returns {boolean} `true` if the file exists.
|
|
170
|
+
* @memberof UnderpostKubectl
|
|
171
|
+
*/
|
|
172
|
+
existsFile({ podName, path }) {
|
|
173
|
+
const result = shellExec(`kubectl exec ${podName} -- test -f ${path} && echo "true" || echo "false"`, {
|
|
174
|
+
stdout: true,
|
|
175
|
+
disableLog: true,
|
|
176
|
+
silent: true,
|
|
177
|
+
}).trim();
|
|
178
|
+
return result === 'true';
|
|
179
|
+
},
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Returns a filtered list of pods from the cluster.
|
|
183
|
+
* Supports wildcard glob patterns on pod names and optional deployId substring filtering.
|
|
184
|
+
* @param {object} [criteria={}] - Filter criteria.
|
|
185
|
+
* @param {string} [criteria.deployId] - Substring to match against pod names (forwards to `get`).
|
|
186
|
+
* @param {string} [criteria.podNames] - Comma-separated glob patterns (supports `*`).
|
|
187
|
+
* @param {string} [criteria.namespace='default'] - Kubernetes namespace to query.
|
|
188
|
+
* @returns {Array<object>} Filtered pod rows from `get`.
|
|
189
|
+
* @memberof UnderpostKubectl
|
|
190
|
+
*/
|
|
191
|
+
getFilteredPods(criteria = {}) {
|
|
192
|
+
const { podNames, namespace = 'default', deployId } = criteria;
|
|
193
|
+
try {
|
|
194
|
+
let pods = Underpost.kubectl.get(deployId || '', 'pods', namespace);
|
|
195
|
+
if (podNames) {
|
|
196
|
+
const patterns = podNames.split(',').map((p) => p.trim());
|
|
197
|
+
pods = pods.filter((pod) =>
|
|
198
|
+
patterns.some((pattern) => new RegExp('^' + pattern.replace(/\*/g, '.*') + '$').test(pod.NAME)),
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
logger.info(`Found ${pods.length} pod(s) matching criteria`, { criteria, podNames: pods.map((p) => p.NAME) });
|
|
202
|
+
return pods;
|
|
203
|
+
} catch (error) {
|
|
204
|
+
logger.error('Error filtering pods', { error: error.message, criteria });
|
|
205
|
+
return [];
|
|
206
|
+
}
|
|
207
|
+
},
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
export default UnderpostKubectl;
|