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.
- package/.env.example +0 -2
- package/.github/workflows/ghpkg.ci.yml +4 -4
- package/.github/workflows/npmpkg.ci.yml +28 -11
- package/.github/workflows/publish.ci.yml +6 -0
- package/.github/workflows/pwa-microservices-template-page.cd.yml +4 -5
- package/.github/workflows/pwa-microservices-template-test.ci.yml +3 -3
- package/.github/workflows/release.cd.yml +13 -8
- package/CHANGELOG.md +396 -1
- package/CLI-HELP.md +53 -6
- package/Dockerfile +4 -2
- 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 +5 -2
- package/manifests/cronjobs/dd-cron/dd-cron-dns.yaml +5 -2
- package/manifests/deployment/dd-default-development/deployment.yaml +2 -2
- package/manifests/deployment/dd-test-development/deployment.yaml +88 -74
- 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 +726 -825
- package/src/cli/deploy.js +151 -93
- package/src/cli/env.js +19 -0
- package/src/cli/fs.js +5 -2
- package/src/cli/index.js +45 -2
- package/src/cli/kubectl.js +211 -0
- package/src/cli/release.js +284 -0
- package/src/cli/repository.js +434 -75
- package/src/cli/run.js +189 -34
- package/src/cli/secrets.js +73 -0
- 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 +137 -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/Dockerfile +4 -0
- package/src/runtime/express/Express.js +18 -1
- package/src/runtime/lampp/Dockerfile +13 -2
- package/src/runtime/lampp/Lampp.js +27 -4
- package/src/runtime/wp/Dockerfile +68 -0
- package/src/runtime/wp/Wp.js +639 -0
- package/src/server/auth.js +24 -1
- package/src/server/backup.js +57 -23
- 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/cron.js +23 -18
- 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 +3 -2
- 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
|
@@ -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-
|
|
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: ${
|
|
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
|
-
${
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|
772
|
-
*
|
|
773
|
-
* @param {string}
|
|
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
|
|
804
|
+
shellExec(`kubectl delete secret underpost-config -n ${namespace} --ignore-not-found`);
|
|
778
805
|
shellExec(
|
|
779
|
-
`kubectl create
|
|
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 {
|
|
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
|
-
|
|
919
|
-
? `
|
|
971
|
+
emptyDir
|
|
972
|
+
? ` emptyDir: {}`
|
|
973
|
+
: secret
|
|
974
|
+
? ` secret:
|
|
975
|
+
secretName: ${secret}`
|
|
976
|
+
: configMap
|
|
977
|
+
? ` configMap:
|
|
920
978
|
name: ${configMap}`
|
|
921
|
-
|
|
922
|
-
|
|
979
|
+
: claimName
|
|
980
|
+
? ` persistentVolumeClaim:
|
|
923
981
|
claimName: ${claimName}`
|
|
924
|
-
|
|
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 =
|
|
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
|
|
109
|
+
} else pullSkipCount++;
|
|
109
110
|
}
|
|
110
|
-
|
|
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
|
|
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 };
|