underpost 3.2.9 → 3.2.11
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/.github/workflows/npmpkg.ci.yml +1 -0
- package/.github/workflows/pwa-microservices-template-test.ci.yml +1 -1
- package/.github/workflows/release.cd.yml +1 -0
- package/.vscode/extensions.json +9 -9
- package/.vscode/settings.json +20 -4
- package/CHANGELOG.md +195 -1
- package/CLI-HELP.md +92 -23
- package/README.md +38 -9
- package/bin/build.js +27 -7
- package/bin/build.template.js +187 -0
- package/bin/deploy.js +12 -2
- package/bin/index.js +2 -1
- package/bump.config.js +26 -0
- package/conf.js +20 -7
- 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 +4 -2
- package/manifests/kind-config-dev.yaml +8 -0
- package/manifests/lxd/lxd-admin-profile.yaml +12 -3
- package/manifests/mongodb/pv-pvc.yaml +44 -8
- package/manifests/mongodb/statefulset.yaml +55 -68
- package/manifests/mongodb-4.4/headless-service.yaml +10 -0
- package/manifests/mongodb-4.4/kustomization.yaml +3 -1
- package/manifests/mongodb-4.4/mongodb-nodeport.yaml +17 -0
- package/manifests/mongodb-4.4/pv-pvc.yaml +10 -14
- package/manifests/mongodb-4.4/statefulset.yaml +79 -0
- package/manifests/mongodb-4.4/storage-class.yaml +9 -0
- package/manifests/valkey/statefulset.yaml +1 -1
- package/manifests/valkey/valkey-nodeport.yaml +17 -0
- package/package.json +27 -12
- package/scripts/ipxe-setup.sh +52 -49
- package/scripts/k3s-node-setup.sh +81 -46
- package/scripts/lxd-vm-setup.sh +193 -8
- package/scripts/maas-nat-firewalld.sh +145 -0
- package/src/api/core/core.router.js +19 -14
- package/src/api/core/core.service.js +5 -5
- package/src/api/default/default.router.js +22 -18
- package/src/api/default/default.service.js +5 -5
- package/src/api/document/document.router.js +28 -23
- package/src/api/document/document.service.js +100 -23
- package/src/api/file/file.router.js +19 -13
- package/src/api/file/file.service.js +9 -7
- package/src/api/test/test.router.js +17 -12
- package/src/api/types.js +24 -0
- package/src/api/user/guest.service.js +5 -4
- package/src/api/user/user.router.js +297 -288
- package/src/api/user/user.service.js +100 -35
- package/src/cli/baremetal.js +132 -101
- package/src/cli/cluster.js +700 -232
- package/src/cli/db.js +59 -60
- package/src/cli/deploy.js +216 -137
- package/src/cli/fs.js +13 -3
- package/src/cli/index.js +80 -15
- package/src/cli/ipfs.js +4 -6
- package/src/cli/kubectl.js +4 -1
- package/src/cli/lxd.js +1099 -223
- package/src/cli/monitor.js +9 -3
- package/src/cli/release.js +334 -140
- package/src/cli/repository.js +68 -23
- package/src/cli/run.js +191 -47
- package/src/cli/secrets.js +11 -2
- package/src/cli/test.js +9 -3
- package/src/client/Default.index.js +9 -3
- package/src/client/components/core/Auth.js +5 -0
- package/src/client/components/core/ClientEvents.js +76 -0
- package/src/client/components/core/EventBus.js +4 -0
- package/src/client/components/core/Modal.js +82 -41
- package/src/client/components/core/PanelForm.js +56 -52
- package/src/client/components/core/Worker.js +162 -363
- package/src/client/sw/core.sw.js +174 -112
- package/src/db/DataBaseProvider.js +115 -15
- package/src/db/mariadb/MariaDB.js +2 -1
- package/src/db/mongo/MongoBootstrap.js +657 -0
- package/src/db/mongo/MongooseDB.js +129 -21
- package/src/index.js +1 -1
- package/src/runtime/express/Express.js +2 -2
- package/src/runtime/wp/Wp.js +8 -5
- package/src/server/auth.js +2 -2
- package/src/server/client-build-docs.js +1 -1
- package/src/server/client-build.js +94 -129
- package/src/server/conf.js +81 -79
- package/src/server/process.js +180 -19
- package/src/server/proxy.js +9 -2
- package/src/server/runtime.js +1 -1
- package/src/server/start.js +16 -4
- package/src/server/valkey.js +2 -0
- package/src/ws/IoInterface.js +16 -16
- package/src/ws/core/channels/core.ws.chat.js +11 -11
- package/src/ws/core/channels/core.ws.mailer.js +29 -29
- package/src/ws/core/channels/core.ws.stream.js +19 -19
- package/src/ws/core/core.ws.connection.js +8 -8
- package/src/ws/core/core.ws.server.js +6 -5
- package/src/ws/default/channels/default.ws.main.js +10 -10
- package/src/ws/default/default.ws.connection.js +4 -4
- package/src/ws/default/default.ws.server.js +4 -3
- package/bin/file.js +0 -202
- package/bin/vs.js +0 -74
- package/bin/zed.js +0 -84
- package/src/client/ssr/email/DefaultRecoverEmail.js +0 -21
- package/src/client/ssr/email/DefaultVerifyEmail.js +0 -17
- /package/src/client/ssr/{offline → views}/Maintenance.js +0 -0
- /package/src/client/ssr/{offline → views}/NoNetworkConnection.js +0 -0
- /package/src/client/ssr/{pages → views}/Test.js +0 -0
package/src/cli/deploy.js
CHANGED
|
@@ -127,6 +127,7 @@ class UnderpostDeploy {
|
|
|
127
127
|
* @param {Array<string>} cmd - Command to run in the deployment container.
|
|
128
128
|
* @param {boolean} skipFullBuild - Whether to skip the full client bundle build during deployment.
|
|
129
129
|
* @param {boolean} pullBundle - Whether to pull the pre-built client bundle from Cloudinary before starting. Use together with skipFullBuild to skip the local build entirely.
|
|
130
|
+
* @param {string} [imagePullPolicy] - Container imagePullPolicy override (`Always`, `IfNotPresent`, `Never`). When omitted, defaults to `Never` for `localhost/` images and `IfNotPresent` otherwise.
|
|
130
131
|
* @returns {string} - YAML deployment configuration for the specified deployment.
|
|
131
132
|
* @memberof UnderpostDeploy
|
|
132
133
|
*/
|
|
@@ -142,6 +143,17 @@ class UnderpostDeploy {
|
|
|
142
143
|
cmd,
|
|
143
144
|
skipFullBuild,
|
|
144
145
|
pullBundle,
|
|
146
|
+
imagePullPolicy,
|
|
147
|
+
// K8S lifecycle + probe wiring. Pass-through structures shaped like the
|
|
148
|
+
// upstream Kubernetes API, spliced verbatim into the container spec.
|
|
149
|
+
// lifecycle: { postStart: { exec: { command: [...] } }, preStop: { exec: { command: [...] } } }
|
|
150
|
+
// readinessProbe: { tcpSocket: { port: 8081 }, ... }
|
|
151
|
+
// livenessProbe: { tcpSocket: { port: 8081 }, ... }
|
|
152
|
+
// containerPort: integer; rendered as ports[0].containerPort. Optional.
|
|
153
|
+
lifecycle,
|
|
154
|
+
readinessProbe,
|
|
155
|
+
livenessProbe,
|
|
156
|
+
containerPort,
|
|
145
157
|
}) {
|
|
146
158
|
if (!cmd)
|
|
147
159
|
cmd =
|
|
@@ -188,26 +200,60 @@ spec:
|
|
|
188
200
|
containers:
|
|
189
201
|
- name: ${deployId}-${env}-${suffix}
|
|
190
202
|
image: ${containerImage}
|
|
191
|
-
imagePullPolicy: ${containerImage.startsWith('localhost/') ? 'Never' : 'IfNotPresent'}
|
|
203
|
+
imagePullPolicy: ${imagePullPolicy ? imagePullPolicy : containerImage.startsWith('localhost/') ? 'Never' : 'IfNotPresent'}
|
|
192
204
|
envFrom:
|
|
193
205
|
- secretRef:
|
|
194
206
|
name: underpost-config
|
|
195
207
|
${
|
|
196
|
-
|
|
197
|
-
? `
|
|
208
|
+
containerPort
|
|
209
|
+
? ` ports:
|
|
210
|
+
- containerPort: ${containerPort}
|
|
211
|
+
`
|
|
212
|
+
: ''
|
|
213
|
+
}${
|
|
214
|
+
resources
|
|
215
|
+
? ` resources:
|
|
198
216
|
requests:
|
|
199
217
|
memory: "${resources.requests.memory}"
|
|
200
218
|
cpu: "${resources.requests.cpu}"
|
|
201
219
|
limits:
|
|
202
220
|
memory: "${resources.limits.memory}"
|
|
203
221
|
cpu: "${resources.limits.cpu}"`
|
|
204
|
-
|
|
205
|
-
}
|
|
222
|
+
: ''
|
|
223
|
+
}
|
|
206
224
|
command:
|
|
207
225
|
- /bin/sh
|
|
208
226
|
- -c
|
|
209
227
|
- >
|
|
210
228
|
${cmd.join(' &&\n ')}
|
|
229
|
+
${
|
|
230
|
+
readinessProbe
|
|
231
|
+
? ` readinessProbe:
|
|
232
|
+
${JSON.stringify(readinessProbe, null, 2)
|
|
233
|
+
.split('\n')
|
|
234
|
+
.map((l) => ' ' + l)
|
|
235
|
+
.join('\n')}
|
|
236
|
+
`
|
|
237
|
+
: ''
|
|
238
|
+
}${
|
|
239
|
+
livenessProbe
|
|
240
|
+
? ` livenessProbe:
|
|
241
|
+
${JSON.stringify(livenessProbe, null, 2)
|
|
242
|
+
.split('\n')
|
|
243
|
+
.map((l) => ' ' + l)
|
|
244
|
+
.join('\n')}
|
|
245
|
+
`
|
|
246
|
+
: ''
|
|
247
|
+
}${
|
|
248
|
+
lifecycle
|
|
249
|
+
? ` lifecycle:
|
|
250
|
+
${JSON.stringify(lifecycle, null, 2)
|
|
251
|
+
.split('\n')
|
|
252
|
+
.map((l) => ' ' + l)
|
|
253
|
+
.join('\n')}
|
|
254
|
+
`
|
|
255
|
+
: ''
|
|
256
|
+
}
|
|
211
257
|
|
|
212
258
|
${
|
|
213
259
|
volumes.length > 0
|
|
@@ -248,6 +294,7 @@ spec:
|
|
|
248
294
|
* @param {string} [options.traffic] - Traffic status for the deployment.
|
|
249
295
|
* @param {boolean} [options.skipFullBuild] - Whether to skip the full client bundle build; forwarded to deploymentYamlPartsFactory to generate a pull-bundle startup command.
|
|
250
296
|
* @param {boolean} [options.pullBundle] - Whether to pull the pre-built client bundle from Cloudinary; forwarded to deploymentYamlPartsFactory. Use together with skipFullBuild.
|
|
297
|
+
* @param {string} [options.imagePullPolicy] - Container imagePullPolicy override (`Always`, `IfNotPresent`, `Never`); forwarded to deploymentYamlPartsFactory. When omitted, the builder defaults to `Never` for `localhost/` images and `IfNotPresent` otherwise.
|
|
251
298
|
* @returns {Promise<void>} - Promise that resolves when the manifest is built.
|
|
252
299
|
* @memberof UnderpostDeploy
|
|
253
300
|
*/
|
|
@@ -286,6 +333,7 @@ ${Underpost.deploy
|
|
|
286
333
|
cmd: options.cmd ? options.cmd.split(',').map((c) => c.trim()) : undefined,
|
|
287
334
|
skipFullBuild: options.skipFullBuild,
|
|
288
335
|
pullBundle: options.pullBundle,
|
|
336
|
+
imagePullPolicy: options.imagePullPolicy,
|
|
289
337
|
})
|
|
290
338
|
.replace('{{ports}}', buildKindPorts(fromPort, toPort))}
|
|
291
339
|
`;
|
|
@@ -509,10 +557,15 @@ spec:
|
|
|
509
557
|
const hostTest = options?.hostTest
|
|
510
558
|
? options.hostTest
|
|
511
559
|
: Object.keys(loadConfServerJson(`./engine-private/conf/${deployId}/conf.server.json`))[0];
|
|
560
|
+
// Missing HTTPProxy is the canonical "no traffic colour set yet" state
|
|
561
|
+
// for blue/green rollouts. silentOnError swallows kubectl's NotFound
|
|
562
|
+
// exit so the function can return null cleanly.
|
|
512
563
|
const info = shellExec(`sudo kubectl get HTTPProxy/${hostTest} -n ${options.namespace} -o yaml`, {
|
|
513
564
|
silent: true,
|
|
514
565
|
stdout: true,
|
|
566
|
+
silentOnError: true,
|
|
515
567
|
});
|
|
568
|
+
if (!info) return null;
|
|
516
569
|
return info.match('blue') ? 'blue' : info.match('green') ? 'green' : null;
|
|
517
570
|
},
|
|
518
571
|
|
|
@@ -563,13 +616,11 @@ spec:
|
|
|
563
616
|
* @param {string} options.traffic - Traffic status for the deployment.
|
|
564
617
|
* @param {string} options.replicas - Number of replicas for the deployment.
|
|
565
618
|
* @param {string} options.node - Node name for resource allocation.
|
|
566
|
-
* @param {boolean} options.restoreHosts - Whether to restore the hosts file.
|
|
567
619
|
* @param {boolean} options.disableUpdateDeployment - Whether to disable deployment updates.
|
|
568
620
|
* @param {boolean} options.disableUpdateProxy - Whether to disable proxy updates.
|
|
569
621
|
* @param {boolean} options.disableDeploymentProxy - Whether to disable deployment proxy.
|
|
570
622
|
* @param {boolean} options.disableUpdateVolume - Whether to disable volume updates.
|
|
571
623
|
* @param {boolean} options.status - Whether to display deployment status.
|
|
572
|
-
* @param {boolean} options.etcHosts - Whether to display the /etc/hosts file.
|
|
573
624
|
* @param {boolean} options.disableUpdateUnderpostConfig - Whether to disable Underpost config updates.
|
|
574
625
|
* @param {string} [options.namespace] - Kubernetes namespace for the deployment.
|
|
575
626
|
* @param {string} [options.timeoutResponse] - Timeout response setting for the deployment.
|
|
@@ -586,6 +637,7 @@ spec:
|
|
|
586
637
|
* @param {boolean} [options.gitClean] - Whether to run git clean on volume mount paths before copying.
|
|
587
638
|
* @param {boolean} [options.skipFullBuild] - Whether to skip the full client bundle build; passed through to buildManifest/deploymentYamlPartsFactory.
|
|
588
639
|
* @param {boolean} [options.pullBundle] - Whether to pull the pre-built client bundle from Cloudinary; passed through to buildManifest/deploymentYamlPartsFactory. Use together with skipFullBuild.
|
|
640
|
+
* @param {string} [options.imagePullPolicy] - Container imagePullPolicy override (`Always`, `IfNotPresent`, `Never`); passed through to buildManifest/deploymentYamlPartsFactory. When omitted, the builder defaults to `Never` for `localhost/` images and `IfNotPresent` otherwise.
|
|
589
641
|
* @returns {Promise<void>} - Promise that resolves when the deployment process is complete.
|
|
590
642
|
* @memberof UnderpostDeploy
|
|
591
643
|
*/
|
|
@@ -606,13 +658,11 @@ spec:
|
|
|
606
658
|
traffic: '',
|
|
607
659
|
replicas: '',
|
|
608
660
|
node: '',
|
|
609
|
-
restoreHosts: false,
|
|
610
661
|
disableUpdateDeployment: false,
|
|
611
662
|
disableUpdateProxy: false,
|
|
612
663
|
disableDeploymentProxy: false,
|
|
613
664
|
disableUpdateVolume: false,
|
|
614
665
|
status: false,
|
|
615
|
-
etcHosts: false,
|
|
616
666
|
disableUpdateUnderpostConfig: false,
|
|
617
667
|
namespace: '',
|
|
618
668
|
timeoutResponse: '',
|
|
@@ -627,6 +677,7 @@ spec:
|
|
|
627
677
|
kubeadm: false,
|
|
628
678
|
kind: false,
|
|
629
679
|
gitClean: false,
|
|
680
|
+
imagePullPolicy: '',
|
|
630
681
|
},
|
|
631
682
|
) {
|
|
632
683
|
const namespace = options.namespace ? options.namespace : 'default';
|
|
@@ -695,14 +746,6 @@ EOF`);
|
|
|
695
746
|
return;
|
|
696
747
|
}
|
|
697
748
|
if (!options.disableUpdateUnderpostConfig) Underpost.deploy.configMap(env);
|
|
698
|
-
let renderHosts = '';
|
|
699
|
-
let etcHosts = [];
|
|
700
|
-
if (options.restoreHosts === true) {
|
|
701
|
-
const factoryResult = Underpost.deploy.etcHostFactory(etcHosts);
|
|
702
|
-
renderHosts = factoryResult.renderHosts;
|
|
703
|
-
logger.info(renderHosts);
|
|
704
|
-
return;
|
|
705
|
-
}
|
|
706
749
|
|
|
707
750
|
for (const _deployId of deployList.split(',')) {
|
|
708
751
|
const deployId = _deployId.trim();
|
|
@@ -710,6 +753,10 @@ EOF`);
|
|
|
710
753
|
if (options.expose === true) {
|
|
711
754
|
const kindType = options.kindType ? options.kindType : 'svc';
|
|
712
755
|
const svc = Underpost.kubectl.get(deployId, kindType)[0];
|
|
756
|
+
if (!svc) {
|
|
757
|
+
logger.error(`No ${kindType} found matching '${deployId}', skipping expose`);
|
|
758
|
+
continue;
|
|
759
|
+
}
|
|
713
760
|
const port = options.exposePort
|
|
714
761
|
? parseInt(options.exposePort)
|
|
715
762
|
: options.port
|
|
@@ -759,7 +806,6 @@ EOF`);
|
|
|
759
806
|
if (Underpost.deploy.isValidTLSContext({ host, env, options }))
|
|
760
807
|
shellExec(`sudo kubectl delete Certificate ${host} -n ${namespace} --ignore-not-found`);
|
|
761
808
|
}
|
|
762
|
-
if (!options.remove) etcHosts.push(host);
|
|
763
809
|
}
|
|
764
810
|
|
|
765
811
|
const manifestsPath =
|
|
@@ -780,15 +826,6 @@ EOF`);
|
|
|
780
826
|
shellExec(`sudo kubectl apply -f ./${manifestsPath}/secret.yaml -n ${namespace}`);
|
|
781
827
|
}
|
|
782
828
|
}
|
|
783
|
-
if (options.etcHosts === true) {
|
|
784
|
-
const factoryResult = Underpost.deploy.etcHostFactory(etcHosts);
|
|
785
|
-
renderHosts = factoryResult.renderHosts;
|
|
786
|
-
}
|
|
787
|
-
if (renderHosts)
|
|
788
|
-
logger.info(
|
|
789
|
-
`
|
|
790
|
-
` + renderHosts,
|
|
791
|
-
);
|
|
792
829
|
},
|
|
793
830
|
/**
|
|
794
831
|
* Checks the status of a deployment.
|
|
@@ -801,25 +838,41 @@ EOF`);
|
|
|
801
838
|
* @memberof UnderpostDeploy
|
|
802
839
|
*/
|
|
803
840
|
async checkDeploymentReadyStatus(deployId, env, traffic, ignoresNames = [], namespace = 'default') {
|
|
804
|
-
const cmd = `underpost config get container-status`;
|
|
805
841
|
const pods = Underpost.kubectl.get(`${deployId}-${env}-${traffic}`, 'pods', namespace);
|
|
806
842
|
const readyPods = [];
|
|
807
843
|
const notReadyPods = [];
|
|
844
|
+
|
|
845
|
+
// Readiness signal: the pod's Kubernetes `Ready` condition driven by the
|
|
846
|
+
// container's readinessProbe (TCP socket, HTTP get, or exec). Set by kubelet
|
|
847
|
+
// when the probe passes. A failed or crashing runtime never becomes Ready —
|
|
848
|
+
// kubelet surfaces CrashLoopBackOff and this gate stays closed.
|
|
808
849
|
for (const pod of pods) {
|
|
809
850
|
const { NAME } = pod;
|
|
810
851
|
if (ignoresNames && ignoresNames.find((t) => NAME.trim().toLowerCase().match(t.trim().toLowerCase()))) continue;
|
|
811
|
-
|
|
812
|
-
|
|
852
|
+
|
|
853
|
+
let podJson = null;
|
|
854
|
+
try {
|
|
855
|
+
// Pod may not exist yet (between deployment apply and pod
|
|
856
|
+
// scheduling). silentOnError lets the monitor loop continue
|
|
857
|
+
// instead of aborting on the transient NotFound exit.
|
|
858
|
+
const raw = shellExec(`sudo kubectl get pod ${NAME} -n ${namespace} -o json`, {
|
|
813
859
|
silent: true,
|
|
814
860
|
disableLog: true,
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
},
|
|
861
|
+
stdout: true,
|
|
862
|
+
silentOnError: true,
|
|
818
863
|
});
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
864
|
+
podJson = raw ? JSON.parse(raw) : null;
|
|
865
|
+
} catch (_) {
|
|
866
|
+
podJson = null;
|
|
867
|
+
}
|
|
868
|
+
const conditions = podJson?.status?.conditions || [];
|
|
869
|
+
const readyCondition = conditions.find((c) => c.type === 'Ready');
|
|
870
|
+
const k8sReady = readyCondition?.status === 'True';
|
|
871
|
+
|
|
872
|
+
pod.out = JSON.stringify({ k8sReady, condition: readyCondition ?? null });
|
|
873
|
+
|
|
874
|
+
if (k8sReady) readyPods.push(pod);
|
|
875
|
+
else notReadyPods.push(pod);
|
|
823
876
|
}
|
|
824
877
|
return {
|
|
825
878
|
ready: pods.length > 0 && notReadyPods.length === 0,
|
|
@@ -853,6 +906,7 @@ EOF`);
|
|
|
853
906
|
* @param {string} options.timeoutIdle - Timeout idle setting for the deployment.
|
|
854
907
|
* @param {string} options.retryCount - Retry count setting for the deployment.
|
|
855
908
|
* @param {string} options.retryPerTryTimeout - Retry per-try timeout setting for the deployment.
|
|
909
|
+
* @param {string} [options.imagePullPolicy] - Container imagePullPolicy override; forwarded to the manifest rebuild triggered here.
|
|
856
910
|
* @memberof UnderpostDeploy
|
|
857
911
|
*/
|
|
858
912
|
switchTraffic(
|
|
@@ -866,12 +920,14 @@ EOF`);
|
|
|
866
920
|
timeoutIdle: '',
|
|
867
921
|
retryCount: '',
|
|
868
922
|
retryPerTryTimeout: '',
|
|
923
|
+
imagePullPolicy: '',
|
|
869
924
|
},
|
|
870
925
|
) {
|
|
871
926
|
const timeoutFlags = Underpost.deploy.timeoutFlagsFactory(options);
|
|
927
|
+
const imagePullPolicyFlag = options.imagePullPolicy ? ` --image-pull-policy ${options.imagePullPolicy}` : '';
|
|
872
928
|
|
|
873
929
|
shellExec(
|
|
874
|
-
`node bin deploy --info-router --build-manifest --traffic ${targetTraffic} --replicas ${replicas} --namespace ${namespace}${timeoutFlags} ${deployId} ${env}`,
|
|
930
|
+
`node bin deploy --info-router --build-manifest --traffic ${targetTraffic} --replicas ${replicas} --namespace ${namespace}${timeoutFlags}${imagePullPolicyFlag} ${deployId} ${env}`,
|
|
875
931
|
);
|
|
876
932
|
|
|
877
933
|
shellExec(`sudo kubectl apply -f ./engine-private/conf/${deployId}/build/${env}/proxy.yaml -n ${namespace}`);
|
|
@@ -1070,42 +1126,6 @@ spec:
|
|
|
1070
1126
|
storage: 5Gi`;
|
|
1071
1127
|
},
|
|
1072
1128
|
|
|
1073
|
-
/**
|
|
1074
|
-
* Creates a hosts file for a deployment.
|
|
1075
|
-
* @param {Array<string>} hosts - List of hosts to be added to the hosts file.
|
|
1076
|
-
* @param {object} options - Options for the hosts file creation.
|
|
1077
|
-
* @param {boolean} options.append - Whether to append to the existing hosts file.
|
|
1078
|
-
* @returns {object} - Object containing the rendered hosts file.
|
|
1079
|
-
* @memberof UnderpostDeploy
|
|
1080
|
-
*/
|
|
1081
|
-
etcHostFactory(hosts = [], options = { append: false }) {
|
|
1082
|
-
hosts = hosts.map((host) => {
|
|
1083
|
-
try {
|
|
1084
|
-
if (!host.startsWith('http')) host = `http://${host}`;
|
|
1085
|
-
const hostname = new URL(host).hostname;
|
|
1086
|
-
logger.info('Hostname extract valid', { host, hostname });
|
|
1087
|
-
return hostname;
|
|
1088
|
-
} catch (e) {
|
|
1089
|
-
logger.warn('No hostname extract valid', host);
|
|
1090
|
-
return host;
|
|
1091
|
-
}
|
|
1092
|
-
});
|
|
1093
|
-
const renderHosts = `127.0.0.1 ${hosts.join(
|
|
1094
|
-
' ',
|
|
1095
|
-
)} localhost localhost.localdomain localhost4 localhost4.localdomain4
|
|
1096
|
-
::1 localhost localhost.localdomain localhost6 localhost6.localdomain6`;
|
|
1097
|
-
|
|
1098
|
-
if (options && options.append && fs.existsSync(`/etc/hosts`)) {
|
|
1099
|
-
fs.writeFileSync(
|
|
1100
|
-
`/etc/hosts`,
|
|
1101
|
-
fs.readFileSync(`/etc/hosts`, 'utf8') +
|
|
1102
|
-
`
|
|
1103
|
-
${renderHosts}`,
|
|
1104
|
-
'utf8',
|
|
1105
|
-
);
|
|
1106
|
-
} else fs.writeFileSync(`/etc/hosts`, renderHosts, 'utf8');
|
|
1107
|
-
return { renderHosts };
|
|
1108
|
-
},
|
|
1109
1129
|
/**
|
|
1110
1130
|
* Checks if a TLS context is valid.
|
|
1111
1131
|
* @param {object} options - Options for the check.
|
|
@@ -1119,9 +1139,25 @@ ${renderHosts}`,
|
|
|
1119
1139
|
env === 'production' &&
|
|
1120
1140
|
options.cert === true &&
|
|
1121
1141
|
(!options.certHosts || options.certHosts.split(',').includes(host)),
|
|
1122
|
-
|
|
1123
1142
|
/**
|
|
1124
1143
|
* Monitors the ready status of a deployment.
|
|
1144
|
+
*
|
|
1145
|
+
* Ready signal:
|
|
1146
|
+
* The orchestrator gate is the Kubernetes pod Ready condition. When the
|
|
1147
|
+
* container's `readinessProbe` succeeds, kubelet flips
|
|
1148
|
+
* `status.conditions[Ready]` to True and `checkDeploymentReadyStatus`
|
|
1149
|
+
* returns the pod in `readyPods`. This is the only required signal — see
|
|
1150
|
+
* `src/client/public/nexodev/docs/references/Deploy custom instance to K8S.md`.
|
|
1151
|
+
*
|
|
1152
|
+
* Container-status:
|
|
1153
|
+
* `underpost config get container-status` is read from each pod for both
|
|
1154
|
+
* the display column and as a second ready gate alongside the K8S Ready
|
|
1155
|
+
* condition. Both must be satisfied before the monitor exits:
|
|
1156
|
+
* 1. K8S readinessProbe (TCP socket) — ensures the port is bound.
|
|
1157
|
+
* 2. container-status == `<deploy>-<env>-running-deployment` — ensures
|
|
1158
|
+
* the application has completed its own startup sequence.
|
|
1159
|
+
* Early-abort on `error` container-status remains in effect.
|
|
1160
|
+
*
|
|
1125
1161
|
* @param {string} deployId - Deployment ID for which the ready status is being monitored.
|
|
1126
1162
|
* @param {string} env - Environment for which the ready status is being monitored.
|
|
1127
1163
|
* @param {string} targetTraffic - Target traffic status for the deployment.
|
|
@@ -1131,79 +1167,94 @@ ${renderHosts}`,
|
|
|
1131
1167
|
* @memberof UnderpostDeploy
|
|
1132
1168
|
*/
|
|
1133
1169
|
async monitorReadyRunner(deployId, env, targetTraffic, ignorePods = [], namespace = 'default') {
|
|
1134
|
-
|
|
1135
|
-
const checkStatusIterationMsDelay = 1000;
|
|
1170
|
+
const delayMs = 1000;
|
|
1136
1171
|
const maxIterations = 3000;
|
|
1137
1172
|
const deploymentId = `${deployId}-${env}-${targetTraffic}`;
|
|
1138
|
-
const
|
|
1139
|
-
|
|
1140
|
-
const
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1173
|
+
const expectedContainerStatus = `${deployId}-${env}-running-deployment`;
|
|
1174
|
+
const tag = `[${deploymentId}]`;
|
|
1175
|
+
const containerStatusDefault = 'waiting for status';
|
|
1176
|
+
|
|
1177
|
+
logger.info('Deployment init', { deployId, env, targetTraffic, namespace });
|
|
1178
|
+
|
|
1179
|
+
// Per-pod cache of last-known container-status (persists across retries)
|
|
1180
|
+
const podStatusCache = new Map();
|
|
1181
|
+
|
|
1182
|
+
const readContainerStatus = (podName) => {
|
|
1183
|
+
try {
|
|
1184
|
+
const raw = shellExec(
|
|
1185
|
+
`sudo kubectl exec ${podName} -n ${namespace} -- sh -c 'underpost config get container-status --plain'`,
|
|
1186
|
+
{ silent: true, disableLog: true, stdout: true, silentOnError: true },
|
|
1152
1187
|
);
|
|
1153
|
-
|
|
1188
|
+
const val = raw ? raw.toString().trim() : '';
|
|
1189
|
+
return val && val !== 'undefined' ? val : containerStatusDefault;
|
|
1190
|
+
} catch (_) {
|
|
1191
|
+
// exec failed (e.g. pod not yet running) — preserve last known value
|
|
1192
|
+
return podStatusCache.get(podName) || containerStatusDefault;
|
|
1154
1193
|
}
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1194
|
+
};
|
|
1195
|
+
|
|
1196
|
+
for (let i = 0; i < maxIterations; i++) {
|
|
1197
|
+
const result = await Underpost.deploy.checkDeploymentReadyStatus(
|
|
1198
|
+
deployId,
|
|
1199
|
+
env,
|
|
1200
|
+
targetTraffic,
|
|
1201
|
+
ignorePods,
|
|
1202
|
+
namespace,
|
|
1203
|
+
);
|
|
1204
|
+
|
|
1205
|
+
const allPods = [...result.readyPods, ...result.notReadyPods];
|
|
1206
|
+
|
|
1207
|
+
// Update cache with latest status for each pod (informational + error gate)
|
|
1208
|
+
for (const pod of allPods) {
|
|
1209
|
+
if (!pod?.NAME) continue;
|
|
1210
|
+
const status = readContainerStatus(pod.NAME);
|
|
1211
|
+
if (status === 'error') throw new Error(`Pod ${pod.NAME} has error status`);
|
|
1212
|
+
podStatusCache.set(pod.NAME, status);
|
|
1169
1213
|
}
|
|
1170
1214
|
|
|
1171
|
-
|
|
1172
|
-
let indexOf = -1;
|
|
1173
|
-
for (const pod of result.notReadyPods) {
|
|
1174
|
-
indexOf++;
|
|
1175
|
-
const { NAME, out } = pod;
|
|
1215
|
+
const allPodsK8sReady = allPods.length > 0 && result.notReadyPods.length === 0;
|
|
1176
1216
|
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
// else if (out.match('not') && out.match('found') && checkStatusIteration <= 20 && out.match('underpost'))
|
|
1180
|
-
// lastMsg[NAME] = 'Installing underpost cli';
|
|
1181
|
-
else if (out.match('not') && out.match('found') && checkStatusIteration <= 20 && out.match('task'))
|
|
1182
|
-
lastMsg[NAME] = 'Initializing setup task';
|
|
1183
|
-
else if (out.match('Empty environment variables')) lastMsg[NAME] = 'Setup environment';
|
|
1184
|
-
else if (out.match(`${deployId}-${env}-build-deployment`)) lastMsg[NAME] = 'Building apps/services';
|
|
1185
|
-
else if (out.match(`${deployId}-${env}-initializing-deployment`))
|
|
1186
|
-
lastMsg[NAME] = 'Initializing apps/services';
|
|
1187
|
-
else if (!lastMsg[NAME]) lastMsg[NAME] = `Waiting for status`;
|
|
1217
|
+
const allPodsStatusReady =
|
|
1218
|
+
allPods.length > 0 && allPods.every((pod) => podStatusCache.get(pod.NAME) === expectedContainerStatus);
|
|
1188
1219
|
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
}
|
|
1220
|
+
// Print snapshot for every pod — annotate when container-status hasn't caught
|
|
1221
|
+
// up to the K8S Ready condition yet.
|
|
1222
|
+
for (const pod of allPods) {
|
|
1223
|
+
const status = podStatusCache.get(pod.NAME) || containerStatusDefault;
|
|
1224
|
+
const podStatus = pod.STATUS || 'Unknown';
|
|
1225
|
+
const statusMatchesExpected = status === expectedContainerStatus;
|
|
1226
|
+
const statusDisplay = statusMatchesExpected ? status : `${status} (pending)`;
|
|
1227
|
+
|
|
1228
|
+
console.log(
|
|
1229
|
+
'Target pod:',
|
|
1230
|
+
pod.NAME[pod.NAME.includes('green') ? 'bgGreen' : 'bgBlue'].bold.black,
|
|
1231
|
+
'| Pod status:',
|
|
1232
|
+
podStatus.bold.yellow,
|
|
1233
|
+
'| Runtime status:',
|
|
1234
|
+
statusDisplay.bold.cyan,
|
|
1235
|
+
);
|
|
1236
|
+
}
|
|
1237
|
+
|
|
1238
|
+
// Both K8S readinessProbe AND container-status must be satisfied before
|
|
1239
|
+
// declaring the deployment ready. The TCP probe ensures the port is bound;
|
|
1240
|
+
// container-status == running-deployment ensures the application has
|
|
1241
|
+
// completed its own startup sequence so traffic is not switched prematurely.
|
|
1242
|
+
if (allPodsK8sReady && allPodsStatusReady) {
|
|
1243
|
+
logger.info(`${tag} | All pods Ready (K8S readinessProbe satisfied)`);
|
|
1244
|
+
return result;
|
|
1245
|
+
}
|
|
1246
|
+
|
|
1247
|
+
await timer(delayMs);
|
|
1248
|
+
|
|
1249
|
+
if ((i + 1) % 10 === 0) {
|
|
1250
|
+
logger.info(`${tag} | In progress... iteration ${i + 1}`);
|
|
1196
1251
|
}
|
|
1197
|
-
await timer(checkStatusIterationMsDelay);
|
|
1198
|
-
checkStatusIteration++;
|
|
1199
|
-
logger.info(
|
|
1200
|
-
`${iteratorTag} | Deployment in progress... | Delay number monitor iterations: ${checkStatusIteration}`,
|
|
1201
|
-
);
|
|
1202
1252
|
}
|
|
1203
|
-
|
|
1204
|
-
|
|
1253
|
+
|
|
1254
|
+
logger.error(`${tag} | Deployment timeout after ${maxIterations} iterations`);
|
|
1255
|
+
throw new Error(
|
|
1256
|
+
`monitorReadyRunner timeout: ${deploymentId} did not become Ready within ${maxIterations}*${delayMs}ms`,
|
|
1205
1257
|
);
|
|
1206
|
-
return result;
|
|
1207
1258
|
},
|
|
1208
1259
|
|
|
1209
1260
|
/**
|
|
@@ -1363,6 +1414,34 @@ ${renderHosts}`,
|
|
|
1363
1414
|
return undefined;
|
|
1364
1415
|
},
|
|
1365
1416
|
|
|
1417
|
+
/**
|
|
1418
|
+
* Extracts a non-standard `imagePullPolicy` key from an env-resolved
|
|
1419
|
+
* instance lifecycle block (the convention used in `conf.instances.json`,
|
|
1420
|
+
* where `imagePullPolicy` sits alongside `postStart`/`preStop` for
|
|
1421
|
+
* per-instance ergonomics) and returns a clean lifecycle hash that is
|
|
1422
|
+
* safe to splice into the K8S container spec.
|
|
1423
|
+
*
|
|
1424
|
+
* Returns `{ lifecycle, imagePullPolicy }`:
|
|
1425
|
+
* - `lifecycle` — the input minus `imagePullPolicy`, or `undefined` when
|
|
1426
|
+
* the resulting block is empty.
|
|
1427
|
+
* - `imagePullPolicy` — the extracted value, or `undefined` if absent.
|
|
1428
|
+
*
|
|
1429
|
+
* @param {object|undefined} lifecycle - Env-resolved lifecycle block
|
|
1430
|
+
* (already passed through pickEnv). May be `undefined`.
|
|
1431
|
+
* @returns {{ lifecycle: (object|undefined), imagePullPolicy: (string|undefined) }}
|
|
1432
|
+
* @memberof UnderpostDeploy
|
|
1433
|
+
*/
|
|
1434
|
+
extractInstanceImagePullPolicy(lifecycle) {
|
|
1435
|
+
if (!lifecycle || typeof lifecycle !== 'object' || !('imagePullPolicy' in lifecycle)) {
|
|
1436
|
+
return { lifecycle, imagePullPolicy: undefined };
|
|
1437
|
+
}
|
|
1438
|
+
const { imagePullPolicy, ...rest } = lifecycle;
|
|
1439
|
+
return {
|
|
1440
|
+
lifecycle: Object.keys(rest).length > 0 ? rest : undefined,
|
|
1441
|
+
imagePullPolicy,
|
|
1442
|
+
};
|
|
1443
|
+
},
|
|
1444
|
+
|
|
1366
1445
|
/**
|
|
1367
1446
|
* Generates timeout flags string for deployment commands.
|
|
1368
1447
|
* @param {object} options - Options containing timeout settings.
|
package/src/cli/fs.js
CHANGED
|
@@ -127,7 +127,11 @@ class UnderpostFileStorage {
|
|
|
127
127
|
if (options.git === true) {
|
|
128
128
|
const gitPath = hasPathFilter ? basePath : '.';
|
|
129
129
|
shellExec(`cd ${gitPath} && git add .`);
|
|
130
|
-
shellExec(`underpost cmt ${gitPath} feat
|
|
130
|
+
shellExec(`underpost cmt ${gitPath} feat`, {
|
|
131
|
+
silentOnError: true,
|
|
132
|
+
silent: true,
|
|
133
|
+
disableLog: true,
|
|
134
|
+
});
|
|
131
135
|
}
|
|
132
136
|
|
|
133
137
|
return;
|
|
@@ -154,7 +158,9 @@ class UnderpostFileStorage {
|
|
|
154
158
|
// For bundle pulls into ./build the git step is unwanted and would error on a non-repo path.
|
|
155
159
|
if (options.git === true) {
|
|
156
160
|
Underpost.repo.initLocalRepo({ path });
|
|
157
|
-
shellExec(`cd ${path} && git add . && git commit -m "Base pull state"
|
|
161
|
+
shellExec(`cd ${path} && git add . && git commit -m "Base pull state"`, {
|
|
162
|
+
silentOnError: true,
|
|
163
|
+
});
|
|
158
164
|
}
|
|
159
165
|
} else {
|
|
160
166
|
const files =
|
|
@@ -173,7 +179,11 @@ class UnderpostFileStorage {
|
|
|
173
179
|
Underpost.fs.writeStorageConf(storage, storageConf);
|
|
174
180
|
if (options.git === true) {
|
|
175
181
|
shellExec(`cd ${path} && git add .`);
|
|
176
|
-
shellExec(`underpost cmt ${path} feat
|
|
182
|
+
shellExec(`underpost cmt ${path} feat`, {
|
|
183
|
+
silentOnError: true,
|
|
184
|
+
silent: true,
|
|
185
|
+
disableLog: true,
|
|
186
|
+
});
|
|
177
187
|
}
|
|
178
188
|
},
|
|
179
189
|
/**
|