underpost 3.2.12 → 3.2.21
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/ghpkg.ci.yml +1 -0
- package/.github/workflows/npmpkg.ci.yml +9 -5
- package/CHANGELOG.md +114 -1
- package/CLI-HELP.md +973 -1130
- package/README.md +47 -41
- package/bin/build.js +88 -137
- package/bin/build.template.js +25 -179
- package/bin/deploy.js +4 -1
- package/bin/index.js +2 -2
- package/conf.js +11 -37
- package/manifests/cronjobs/dd-cron/dd-cron-backup.yaml +2 -2
- package/manifests/cronjobs/dd-cron/dd-cron-dns.yaml +1 -1
- package/manifests/deployment/dd-default-development/deployment.yaml +2 -2
- package/package.json +9 -14
- package/scripts/link-local-underpost-cli.sh +6 -0
- package/scripts/test-monitor.sh +86 -0
- package/src/cli/deploy.js +195 -274
- package/src/cli/env.js +1 -4
- package/src/cli/image.js +58 -4
- package/src/cli/index.js +39 -0
- package/src/cli/monitor.js +302 -6
- package/src/cli/release.js +26 -11
- package/src/cli/repository.js +98 -7
- package/src/cli/run.js +137 -69
- package/src/db/mongo/MongooseDB.js +2 -1
- package/src/index.js +1 -1
- package/src/runtime/wp/Dockerfile +3 -3
- package/src/server/catalog-underpost.js +61 -0
- package/src/server/catalog.js +77 -0
- package/src/server/conf.js +365 -56
- package/src/server/runtime-status.js +235 -0
- package/src/server/start.js +17 -8
- package/test/deploy-monitor.test.js +223 -0
- package/manifests/deployment/dd-test-development/deployment.yaml +0 -256
- package/manifests/deployment/dd-test-development/proxy.yaml +0 -102
package/src/cli/deploy.js
CHANGED
|
@@ -18,9 +18,9 @@ import {
|
|
|
18
18
|
} from '../server/conf.js';
|
|
19
19
|
import { loggerFactory } from '../server/logger.js';
|
|
20
20
|
import { shellExec } from '../server/process.js';
|
|
21
|
+
import { INTERNAL_READY_PATH, INTERNAL_HEALTH_PATH } from '../server/runtime-status.js';
|
|
21
22
|
import fs from 'fs-extra';
|
|
22
23
|
import dotenv from 'dotenv';
|
|
23
|
-
import { timer } from '../client/components/core/CommonJs.js';
|
|
24
24
|
import os from 'node:os';
|
|
25
25
|
import Underpost from '../index.js';
|
|
26
26
|
|
|
@@ -114,6 +114,64 @@ class UnderpostDeploy {
|
|
|
114
114
|
)
|
|
115
115
|
.join('')}`;
|
|
116
116
|
},
|
|
117
|
+
/**
|
|
118
|
+
* Builds Kubernetes probes that gate on the in-pod internal status endpoint.
|
|
119
|
+
*
|
|
120
|
+
* HTTP mode (default) aligns Kubernetes pod readiness with actual Underpost
|
|
121
|
+
* runtime readiness:
|
|
122
|
+
* - readinessProbe → GET /_internal/ready (200 only when running-deployment)
|
|
123
|
+
* - livenessProbe → GET /_internal/health (deadlock / hung-process detection)
|
|
124
|
+
* - startupProbe → GET /_internal/ready (long window for hot-built/slow boots)
|
|
125
|
+
*
|
|
126
|
+
* Migration: pass `useHttp: false` to emit the legacy TCP socket probes
|
|
127
|
+
* (port-bound only) for deployments not yet serving the internal endpoint.
|
|
128
|
+
*
|
|
129
|
+
* @param {object} opts
|
|
130
|
+
* @param {number} opts.port - In-pod internal status port (deployment base PORT).
|
|
131
|
+
* @param {boolean} [opts.useHttp=true] - Emit HTTP probes; false → legacy TCP.
|
|
132
|
+
* @param {boolean} [opts.liveness=true] - Include a livenessProbe.
|
|
133
|
+
* @param {boolean} [opts.startup=true] - Include a startupProbe.
|
|
134
|
+
* @returns {{readinessProbe: object, livenessProbe?: object, startupProbe?: object}}
|
|
135
|
+
* @memberof UnderpostDeploy
|
|
136
|
+
*/
|
|
137
|
+
runtimeProbesFactory({ port, useHttp = true, liveness = true, startup = true } = {}) {
|
|
138
|
+
if (!port) return {};
|
|
139
|
+
if (!useHttp) {
|
|
140
|
+
const tcp = { tcpSocket: { port }, initialDelaySeconds: 5, periodSeconds: 10, failureThreshold: 6 };
|
|
141
|
+
const probes = { readinessProbe: tcp };
|
|
142
|
+
if (liveness) probes.livenessProbe = { ...tcp, initialDelaySeconds: 30 };
|
|
143
|
+
return probes;
|
|
144
|
+
}
|
|
145
|
+
const probes = {
|
|
146
|
+
readinessProbe: {
|
|
147
|
+
httpGet: { path: INTERNAL_READY_PATH, port },
|
|
148
|
+
initialDelaySeconds: 5,
|
|
149
|
+
periodSeconds: 5,
|
|
150
|
+
timeoutSeconds: 3,
|
|
151
|
+
failureThreshold: 3,
|
|
152
|
+
},
|
|
153
|
+
};
|
|
154
|
+
if (liveness)
|
|
155
|
+
probes.livenessProbe = {
|
|
156
|
+
httpGet: { path: INTERNAL_HEALTH_PATH, port },
|
|
157
|
+
initialDelaySeconds: 30,
|
|
158
|
+
periodSeconds: 15,
|
|
159
|
+
timeoutSeconds: 3,
|
|
160
|
+
failureThreshold: 3,
|
|
161
|
+
};
|
|
162
|
+
if (startup)
|
|
163
|
+
// A startupProbe suspends readiness/liveness until it first succeeds, so
|
|
164
|
+
// its window bounds in-container hot builds and slow boots. 180 × 10s =
|
|
165
|
+
// 30 min before the pod is considered failed to start.
|
|
166
|
+
probes.startupProbe = {
|
|
167
|
+
httpGet: { path: INTERNAL_READY_PATH, port },
|
|
168
|
+
initialDelaySeconds: 10,
|
|
169
|
+
periodSeconds: 10,
|
|
170
|
+
timeoutSeconds: 3,
|
|
171
|
+
failureThreshold: 180,
|
|
172
|
+
};
|
|
173
|
+
return probes;
|
|
174
|
+
},
|
|
117
175
|
/**
|
|
118
176
|
* Creates a YAML deployment configuration for a deployment.
|
|
119
177
|
* @param {string} deployId - Deployment ID for which the deployment is being created.
|
|
@@ -128,6 +186,11 @@ class UnderpostDeploy {
|
|
|
128
186
|
* @param {boolean} skipFullBuild - Whether to skip the full client bundle build during deployment.
|
|
129
187
|
* @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
188
|
* @param {string} [imagePullPolicy] - Container imagePullPolicy override (`Always`, `IfNotPresent`, `Never`). When omitted, defaults to `Never` for `localhost/` images and `IfNotPresent` otherwise.
|
|
189
|
+
* @param {object} lifecycle - Kubernetes lifecycle hooks configuration for the deployment container.
|
|
190
|
+
* @param {object} readinessProbe - Kubernetes readiness probe configuration for the deployment container.
|
|
191
|
+
* @param {object} livenessProbe - Kubernetes liveness probe configuration for the deployment container.
|
|
192
|
+
* @param {object} startupProbe - Kubernetes startup probe configuration for the deployment container.
|
|
193
|
+
* @param {number} containerPort - Container port to expose for the deployment.
|
|
131
194
|
* @returns {string} - YAML deployment configuration for the specified deployment.
|
|
132
195
|
* @memberof UnderpostDeploy
|
|
133
196
|
*/
|
|
@@ -153,7 +216,12 @@ class UnderpostDeploy {
|
|
|
153
216
|
lifecycle,
|
|
154
217
|
readinessProbe,
|
|
155
218
|
livenessProbe,
|
|
219
|
+
startupProbe,
|
|
156
220
|
containerPort,
|
|
221
|
+
// Explicit, secret-free internal status port injected as an env var so the
|
|
222
|
+
// in-pod endpoint binds exactly what the probes and the monitor target,
|
|
223
|
+
// independent of the ambient `PORT` baked into the image/secret.
|
|
224
|
+
internalStatusPort,
|
|
157
225
|
}) {
|
|
158
226
|
if (!cmd)
|
|
159
227
|
cmd =
|
|
@@ -205,12 +273,19 @@ spec:
|
|
|
205
273
|
- secretRef:
|
|
206
274
|
name: underpost-config
|
|
207
275
|
${
|
|
208
|
-
|
|
209
|
-
? `
|
|
210
|
-
-
|
|
276
|
+
internalStatusPort
|
|
277
|
+
? ` env:
|
|
278
|
+
- name: UNDERPOST_INTERNAL_PORT
|
|
279
|
+
value: "${internalStatusPort}"
|
|
211
280
|
`
|
|
212
281
|
: ''
|
|
213
282
|
}${
|
|
283
|
+
containerPort
|
|
284
|
+
? ` ports:
|
|
285
|
+
- containerPort: ${containerPort}
|
|
286
|
+
`
|
|
287
|
+
: ''
|
|
288
|
+
}${
|
|
214
289
|
resources
|
|
215
290
|
? ` resources:
|
|
216
291
|
requests:
|
|
@@ -242,6 +317,15 @@ ${JSON.stringify(livenessProbe, null, 2)
|
|
|
242
317
|
.split('\n')
|
|
243
318
|
.map((l) => ' ' + l)
|
|
244
319
|
.join('\n')}
|
|
320
|
+
`
|
|
321
|
+
: ''
|
|
322
|
+
}${
|
|
323
|
+
startupProbe
|
|
324
|
+
? ` startupProbe:
|
|
325
|
+
${JSON.stringify(startupProbe, null, 2)
|
|
326
|
+
.split('\n')
|
|
327
|
+
.map((l) => ' ' + l)
|
|
328
|
+
.join('\n')}
|
|
245
329
|
`
|
|
246
330
|
: ''
|
|
247
331
|
}${
|
|
@@ -283,18 +367,22 @@ spec:
|
|
|
283
367
|
* @param {object} options - Options for the manifest build process.
|
|
284
368
|
* @param {string} options.replicas - Number of replicas for each deployment.
|
|
285
369
|
* @param {string} options.image - Docker image for the deployment.
|
|
286
|
-
* @param {string} options.namespace - Kubernetes namespace for the deployment.
|
|
370
|
+
* @param {string} options.namespace - Kubernetes namespace for the deployment (defaults to "default").
|
|
287
371
|
* @param {string} [options.versions] - Comma-separated list of versions to deploy.
|
|
288
372
|
* @param {string} [options.cmd] - Custom initialization command for deploymentYamlPartsFactory (comma-separated commands).
|
|
289
|
-
* @param {string} [options.timeoutResponse] -
|
|
290
|
-
* @param {string} [options.timeoutIdle] -
|
|
291
|
-
* @param {string} [options.retryCount] -
|
|
292
|
-
* @param {string} [options.retryPerTryTimeout] -
|
|
293
|
-
* @param {boolean} [options.disableDeploymentProxy] - Whether to disable deployment proxy.
|
|
294
|
-
* @param {string} [options.traffic] -
|
|
295
|
-
* @param {boolean} [options.
|
|
373
|
+
* @param {string} [options.timeoutResponse] - HTTPProxy per-route response timeout (e.g. "300000ms", "infinity").
|
|
374
|
+
* @param {string} [options.timeoutIdle] - HTTPProxy per-route idle timeout (e.g. "10s", "infinity").
|
|
375
|
+
* @param {string} [options.retryCount] - HTTPProxy per-route retry count (e.g. 3).
|
|
376
|
+
* @param {string} [options.retryPerTryTimeout] - HTTPProxy per-route per-try timeout (e.g. "150ms").
|
|
377
|
+
* @param {boolean} [options.disableDeploymentProxy] - Whether to disable deployment proxy route generation.
|
|
378
|
+
* @param {string} [options.traffic] - Comma-separated active traffic colour(s) used to select which versions receive traffic (e.g. "blue", "green").
|
|
379
|
+
* @param {boolean} [options.cert] - Whether to include cert-manager Certificate resources in secret.yaml (production only).
|
|
380
|
+
* @param {boolean} [options.selfSigned] - Whether to include TLS block in HTTPProxy using a pre-created self-signed secret. Enables HTTPS for development without cert-manager.
|
|
381
|
+
* @param {boolean} [options.skipFullBuild] - Whether to skip the full client bundle build; forwarded to deploymentYamlPartsFactory.
|
|
296
382
|
* @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.
|
|
383
|
+
* @param {string} [options.imagePullPolicy] - Container imagePullPolicy override (`Always`, `IfNotPresent`, `Never`); forwarded to deploymentYamlPartsFactory. Defaults to `Never` for `localhost/` images and `IfNotPresent` otherwise.
|
|
384
|
+
* @param {boolean} [options.disableRuntimeProbes] - Omit internal-status HTTP probes from generated manifests. When true no readiness/liveness/startup probes are emitted.
|
|
385
|
+
* @param {boolean} [options.tcpProbes] - Emit legacy TCP socket probes instead of HTTP internal-status probes (migration path).
|
|
298
386
|
* @returns {Promise<void>} - Promise that resolves when the manifest is built.
|
|
299
387
|
* @memberof UnderpostDeploy
|
|
300
388
|
*/
|
|
@@ -319,6 +407,17 @@ spec:
|
|
|
319
407
|
|
|
320
408
|
logger.info('port range', { deployId, fromPort, toPort });
|
|
321
409
|
|
|
410
|
+
// The internal status endpoint binds `fromPort - 1`: app instances bind
|
|
411
|
+
// the router range starting at `fromPort`, so this slot is always free
|
|
412
|
+
// inside the pod. It is injected into the pod env (UNDERPOST_INTERNAL_PORT)
|
|
413
|
+
// and used for both the probes and the monitor's port-forward target so
|
|
414
|
+
// all three agree regardless of the image's ambient PORT.
|
|
415
|
+
// Opt out with `--disable-runtime-probes` to keep legacy probe-less pods.
|
|
416
|
+
const internalPort = fromPort - 1;
|
|
417
|
+
const probes = options.disableRuntimeProbes
|
|
418
|
+
? {}
|
|
419
|
+
: Underpost.deploy.runtimeProbesFactory({ port: internalPort, useHttp: !options.tcpProbes });
|
|
420
|
+
|
|
322
421
|
let deploymentYamlParts = '';
|
|
323
422
|
for (const deploymentVersion of deploymentVersions) {
|
|
324
423
|
deploymentYamlParts += `---
|
|
@@ -334,6 +433,10 @@ ${Underpost.deploy
|
|
|
334
433
|
skipFullBuild: options.skipFullBuild,
|
|
335
434
|
pullBundle: options.pullBundle,
|
|
336
435
|
imagePullPolicy: options.imagePullPolicy,
|
|
436
|
+
internalStatusPort: options.disableRuntimeProbes ? undefined : internalPort,
|
|
437
|
+
readinessProbe: probes.readinessProbe,
|
|
438
|
+
livenessProbe: probes.livenessProbe,
|
|
439
|
+
startupProbe: probes.startupProbe,
|
|
337
440
|
})
|
|
338
441
|
.replace('{{ports}}', buildKindPorts(fromPort, toPort))}
|
|
339
442
|
`;
|
|
@@ -376,7 +479,7 @@ ${Underpost.deploy
|
|
|
376
479
|
: [];
|
|
377
480
|
|
|
378
481
|
for (const host of Object.keys(confServer)) {
|
|
379
|
-
if (env === 'production')
|
|
482
|
+
if (env === 'production' && options.cert === true)
|
|
380
483
|
secretYaml += Underpost.deploy.buildCertManagerCertificate({ host, namespace: options.namespace });
|
|
381
484
|
|
|
382
485
|
const pathPortAssignment = pathPortAssignmentData[host];
|
|
@@ -579,6 +682,7 @@ spec:
|
|
|
579
682
|
* @memberof UnderpostDeploy
|
|
580
683
|
*/
|
|
581
684
|
baseProxyYamlFactory({ host, env, options }) {
|
|
685
|
+
const includeTls = env !== 'development' || options.selfSigned === true;
|
|
582
686
|
return `
|
|
583
687
|
---
|
|
584
688
|
apiVersion: projectcontour.io/v1
|
|
@@ -589,11 +693,11 @@ metadata:
|
|
|
589
693
|
spec:
|
|
590
694
|
virtualhost:
|
|
591
695
|
fqdn: ${host}${
|
|
592
|
-
|
|
593
|
-
?
|
|
594
|
-
: `
|
|
696
|
+
includeTls
|
|
697
|
+
? `
|
|
595
698
|
tls:
|
|
596
699
|
secretName: ${host}`
|
|
700
|
+
: ''
|
|
597
701
|
}
|
|
598
702
|
routes:`;
|
|
599
703
|
},
|
|
@@ -609,8 +713,9 @@ spec:
|
|
|
609
713
|
* @param {boolean} options.buildManifest - Whether to build the deployment manifest.
|
|
610
714
|
* @param {boolean} options.infoUtil - Whether to display utility information.
|
|
611
715
|
* @param {boolean} options.expose - Whether to expose the deployment.
|
|
612
|
-
* @param {boolean} options.cert - Whether to create
|
|
613
|
-
* @param {string} options.certHosts - Comma-separated list of hosts for which to create certificates.
|
|
716
|
+
* @param {boolean} options.cert - Whether to create cert-manager Certificate resources for the deployment.
|
|
717
|
+
* @param {string} options.certHosts - Comma-separated list of hosts for which to create cert-manager certificates.
|
|
718
|
+
* @param {boolean} options.selfSigned - Use a pre-created self-signed TLS secret instead of cert-manager. The secret must already exist in the namespace with the same name as the host. Enables TLS in the Contour HTTPProxy virtualhost without requiring a production ClusterIssuer.
|
|
614
719
|
* @param {string} options.versions - Comma-separated list of versions to deploy.
|
|
615
720
|
* @param {string} options.image - Docker image for the deployment.
|
|
616
721
|
* @param {string} options.traffic - Traffic status for the deployment.
|
|
@@ -622,22 +727,27 @@ spec:
|
|
|
622
727
|
* @param {boolean} options.disableUpdateVolume - Whether to disable volume updates.
|
|
623
728
|
* @param {boolean} options.status - Whether to display deployment status.
|
|
624
729
|
* @param {boolean} options.disableUpdateUnderpostConfig - Whether to disable Underpost config updates.
|
|
625
|
-
* @param {string} [options.namespace] - Kubernetes namespace for the deployment.
|
|
626
|
-
* @param {string} [options.timeoutResponse] -
|
|
627
|
-
* @param {string} [options.timeoutIdle] -
|
|
628
|
-
* @param {string} [options.retryCount] -
|
|
629
|
-
* @param {string} [options.retryPerTryTimeout] -
|
|
630
|
-
* @param {string} [options.kindType] -
|
|
631
|
-
* @param {number} [options.port] - Port number for exposing the deployment.
|
|
632
|
-
* @param {string} [options.cmd] - Custom initialization command
|
|
633
|
-
* @param {number} [options.exposePort] -
|
|
730
|
+
* @param {string} [options.namespace] - Kubernetes namespace for the deployment (defaults to "default").
|
|
731
|
+
* @param {string} [options.timeoutResponse] - HTTPProxy per-route response timeout (e.g. "300000ms", "infinity").
|
|
732
|
+
* @param {string} [options.timeoutIdle] - HTTPProxy per-route idle timeout (e.g. "10s", "infinity").
|
|
733
|
+
* @param {string} [options.retryCount] - HTTPProxy per-route retry count (e.g. 3).
|
|
734
|
+
* @param {string} [options.retryPerTryTimeout] - HTTPProxy per-route per-try timeout (e.g. "150ms").
|
|
735
|
+
* @param {string} [options.kindType] - Kubernetes resource kind to target when using --expose (defaults to "svc").
|
|
736
|
+
* @param {number} [options.port] - Port number override for exposing the deployment.
|
|
737
|
+
* @param {string} [options.cmd] - Custom initialization command (comma-separated) for deploymentYamlPartsFactory.
|
|
738
|
+
* @param {number} [options.exposePort] - Remote port override when --expose is active (overrides auto-detected service port). Used as both local and remote port unless exposeLocalPort is also set.
|
|
739
|
+
* @param {number} [options.exposeLocalPort] - Local port override for --expose (e.g. 80); remote port is still auto-detected. Enables /etc/hosts access without a port in the browser URL.
|
|
740
|
+
* @param {boolean} [options.localProxy] - When true (with --expose), forward all service TCP ports locally and start the Node.js path-routing proxy for full path-based routing (e.g. /wp alongside /).
|
|
741
|
+
* @param {boolean} [options.tls] - When true (with --expose --local-proxy), start the proxy on port 443 with TLS using self-signed certificates resolved from the local SSL store.
|
|
634
742
|
* @param {boolean} [options.k3s] - Whether to use k3s cluster context.
|
|
635
743
|
* @param {boolean} [options.kubeadm] - Whether to use kubeadm cluster context.
|
|
636
744
|
* @param {boolean} [options.kind] - Whether to use kind cluster context.
|
|
637
745
|
* @param {boolean} [options.gitClean] - Whether to run git clean on volume mount paths before copying.
|
|
638
746
|
* @param {boolean} [options.skipFullBuild] - Whether to skip the full client bundle build; passed through to buildManifest/deploymentYamlPartsFactory.
|
|
639
747
|
* @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.
|
|
748
|
+
* @param {string} [options.imagePullPolicy] - Container imagePullPolicy override (`Always`, `IfNotPresent`, `Never`); passed through to buildManifest/deploymentYamlPartsFactory. Defaults to `Never` for `localhost/` images and `IfNotPresent` otherwise.
|
|
749
|
+
* @param {boolean} [options.disableRuntimeProbes] - Omit internal-status HTTP probes from generated manifests. When true no readiness/liveness/startup probes are emitted.
|
|
750
|
+
* @param {boolean} [options.tcpProbes] - Emit legacy TCP socket probes instead of HTTP internal-status probes.
|
|
641
751
|
* @returns {Promise<void>} - Promise that resolves when the deployment process is complete.
|
|
642
752
|
* @memberof UnderpostDeploy
|
|
643
753
|
*/
|
|
@@ -672,6 +782,10 @@ spec:
|
|
|
672
782
|
kindType: '',
|
|
673
783
|
port: 0,
|
|
674
784
|
exposePort: 0,
|
|
785
|
+
exposeLocalPort: 0,
|
|
786
|
+
localProxy: false,
|
|
787
|
+
tls: false,
|
|
788
|
+
selfSigned: false,
|
|
675
789
|
cmd: '',
|
|
676
790
|
k3s: false,
|
|
677
791
|
kubeadm: false,
|
|
@@ -757,20 +871,50 @@ EOF`);
|
|
|
757
871
|
logger.error(`No ${kindType} found matching '${deployId}', skipping expose`);
|
|
758
872
|
continue;
|
|
759
873
|
}
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
874
|
+
if (options.localProxy) {
|
|
875
|
+
const svcPorts = [
|
|
876
|
+
...new Set(
|
|
877
|
+
svc['PORT(S)']
|
|
878
|
+
.split(',')
|
|
879
|
+
.filter((p) => p.includes('/TCP'))
|
|
880
|
+
.map((p) => parseInt(p.split(':')[0])),
|
|
881
|
+
),
|
|
882
|
+
];
|
|
883
|
+
for (const svcPort of svcPorts) {
|
|
884
|
+
shellExec(`sudo kubectl port-forward -n ${namespace} ${kindType}/${svc.NAME} ${svcPort}:${svcPort}`, {
|
|
885
|
+
async: true,
|
|
886
|
+
});
|
|
887
|
+
}
|
|
888
|
+
const envFile = `./engine-private/conf/${deployId}/.env.${env}`;
|
|
889
|
+
let basePort = svcPorts[0] - 1;
|
|
890
|
+
if (fs.existsSync(envFile)) {
|
|
891
|
+
const portMatch = fs.readFileSync(envFile, 'utf8').match(/^PORT=(\d+)/m);
|
|
892
|
+
if (portMatch) basePort = parseInt(portMatch[1]);
|
|
893
|
+
}
|
|
894
|
+
logger.info(deployId, { svc, svcPorts, basePort });
|
|
895
|
+
const tlsFlag = options.tls ? ' tls' : '';
|
|
896
|
+
shellExec(
|
|
897
|
+
`NODE_ENV=${env} PORT=${basePort} DEV_PROXY_PORT_OFFSET=0 node src/proxy proxy ${deployId} ${env}${tlsFlag}`,
|
|
898
|
+
{ async: true },
|
|
899
|
+
);
|
|
900
|
+
} else {
|
|
901
|
+
const remotePort = options.exposePort
|
|
902
|
+
? parseInt(options.exposePort)
|
|
903
|
+
: options.port
|
|
904
|
+
? parseInt(options.port)
|
|
905
|
+
: kindType !== 'svc'
|
|
906
|
+
? 80
|
|
907
|
+
: parseInt(svc[`PORT(S)`].split('/TCP')[0]);
|
|
908
|
+
const localPort = options.exposeLocalPort ? parseInt(options.exposeLocalPort) : remotePort;
|
|
909
|
+
logger.info(deployId, {
|
|
910
|
+
svc,
|
|
911
|
+
localPort,
|
|
912
|
+
remotePort,
|
|
913
|
+
});
|
|
914
|
+
shellExec(`sudo kubectl port-forward -n ${namespace} ${kindType}/${svc.NAME} ${localPort}:${remotePort}`, {
|
|
915
|
+
async: true,
|
|
916
|
+
});
|
|
917
|
+
}
|
|
774
918
|
continue;
|
|
775
919
|
}
|
|
776
920
|
|
|
@@ -822,64 +966,14 @@ EOF`);
|
|
|
822
966
|
if (!options.disableUpdateProxy)
|
|
823
967
|
shellExec(`sudo kubectl apply -f ./${manifestsPath}/proxy.yaml -n ${namespace}`);
|
|
824
968
|
|
|
825
|
-
if (
|
|
969
|
+
if (
|
|
970
|
+
Underpost.deploy.isValidTLSContext({ host: Object.keys(confServer)[0], env, options }) &&
|
|
971
|
+
!options.selfSigned
|
|
972
|
+
)
|
|
826
973
|
shellExec(`sudo kubectl apply -f ./${manifestsPath}/secret.yaml -n ${namespace}`);
|
|
827
974
|
}
|
|
828
975
|
}
|
|
829
976
|
},
|
|
830
|
-
/**
|
|
831
|
-
* Checks the status of a deployment.
|
|
832
|
-
* @param {string} deployId - Deployment ID for which the status is being checked.
|
|
833
|
-
* @param {string} env - Environment for which the status is being checked.
|
|
834
|
-
* @param {string} traffic - Current traffic status for the deployment.
|
|
835
|
-
* @param {Array<string>} ignoresNames - List of pod names to ignore.
|
|
836
|
-
* @param {string} [namespace='default'] - Kubernetes namespace for the deployment.
|
|
837
|
-
* @returns {object} - Object containing the status of the deployment.
|
|
838
|
-
* @memberof UnderpostDeploy
|
|
839
|
-
*/
|
|
840
|
-
async checkDeploymentReadyStatus(deployId, env, traffic, ignoresNames = [], namespace = 'default') {
|
|
841
|
-
const pods = Underpost.kubectl.get(`${deployId}-${env}-${traffic}`, 'pods', namespace);
|
|
842
|
-
const readyPods = [];
|
|
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.
|
|
849
|
-
for (const pod of pods) {
|
|
850
|
-
const { NAME } = pod;
|
|
851
|
-
if (ignoresNames && ignoresNames.find((t) => NAME.trim().toLowerCase().match(t.trim().toLowerCase()))) continue;
|
|
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`, {
|
|
859
|
-
silent: true,
|
|
860
|
-
disableLog: true,
|
|
861
|
-
stdout: true,
|
|
862
|
-
silentOnError: true,
|
|
863
|
-
});
|
|
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);
|
|
876
|
-
}
|
|
877
|
-
return {
|
|
878
|
-
ready: pods.length > 0 && notReadyPods.length === 0,
|
|
879
|
-
notReadyPods,
|
|
880
|
-
readyPods,
|
|
881
|
-
};
|
|
882
|
-
},
|
|
883
977
|
/**
|
|
884
978
|
* Creates a Kubernetes Secret for a deployment (replaces configMap for secret data).
|
|
885
979
|
* Secrets are mounted as tmpfs (never written to node disk) and support RBAC restrictions.
|
|
@@ -1136,183 +1230,10 @@ spec:
|
|
|
1136
1230
|
* @memberof UnderpostDeploy
|
|
1137
1231
|
*/
|
|
1138
1232
|
isValidTLSContext: ({ host, env, options }) =>
|
|
1139
|
-
env === 'production' &&
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
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
|
-
*
|
|
1161
|
-
* @param {string} deployId - Deployment ID for which the ready status is being monitored.
|
|
1162
|
-
* @param {string} env - Environment for which the ready status is being monitored.
|
|
1163
|
-
* @param {string} targetTraffic - Target traffic status for the deployment.
|
|
1164
|
-
* @param {Array<string>} ignorePods - List of pod names to ignore.
|
|
1165
|
-
* @param {string} [namespace='default'] - Kubernetes namespace for the deployment.
|
|
1166
|
-
* @returns {object} - Object containing the ready status of the deployment.
|
|
1167
|
-
* @memberof UnderpostDeploy
|
|
1168
|
-
*/
|
|
1169
|
-
async monitorReadyRunner(deployId, env, targetTraffic, ignorePods = [], namespace = 'default') {
|
|
1170
|
-
const delayMs = 1000;
|
|
1171
|
-
const maxIterations = 3000;
|
|
1172
|
-
const deploymentId = `${deployId}-${env}-${targetTraffic}`;
|
|
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 },
|
|
1187
|
-
);
|
|
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;
|
|
1193
|
-
}
|
|
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);
|
|
1213
|
-
}
|
|
1214
|
-
|
|
1215
|
-
const allPodsK8sReady = allPods.length > 0 && result.notReadyPods.length === 0;
|
|
1216
|
-
|
|
1217
|
-
const allPodsStatusReady =
|
|
1218
|
-
allPods.length > 0 && allPods.every((pod) => podStatusCache.get(pod.NAME) === expectedContainerStatus);
|
|
1219
|
-
|
|
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}`);
|
|
1251
|
-
}
|
|
1252
|
-
}
|
|
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`,
|
|
1257
|
-
);
|
|
1258
|
-
},
|
|
1259
|
-
|
|
1260
|
-
/**
|
|
1261
|
-
* Retrieves the currently loaded images in the Kubernetes cluster.
|
|
1262
|
-
* @param {string} [node='kind-worker'] - Node name to check for loaded images.
|
|
1263
|
-
* @param {object} options - Options for the image retrieval.
|
|
1264
|
-
* @param {boolean} options.spec - Whether to retrieve images from the pod specifications.
|
|
1265
|
-
* @param {string} options.namespace - Kubernetes namespace to filter pods.
|
|
1266
|
-
* @returns {Array<object>} - Array of objects containing pod names and their corresponding images.
|
|
1267
|
-
* @memberof UnderpostDeploy
|
|
1268
|
-
*/
|
|
1269
|
-
getCurrentLoadedImages(node = 'kind-worker', options = { spec: false, namespace: '' }) {
|
|
1270
|
-
if (options.spec) {
|
|
1271
|
-
const raw = shellExec(
|
|
1272
|
-
`kubectl get pods ${options.namespace ? `--namespace ${options.namespace}` : `--all-namespaces`} -o=jsonpath='{range .items[*]}{"\\n"}{.metadata.namespace}{"/"}{.metadata.name}{":\\t"}{range .spec.containers[*]}{.image}{", "}{end}{end}'`,
|
|
1273
|
-
{
|
|
1274
|
-
stdout: true,
|
|
1275
|
-
silent: true,
|
|
1276
|
-
},
|
|
1277
|
-
);
|
|
1278
|
-
return raw
|
|
1279
|
-
.split(`\n`)
|
|
1280
|
-
.map((lines) => ({
|
|
1281
|
-
pod: lines.split('\t')[0].replaceAll(':', '').trim(),
|
|
1282
|
-
image: lines.split('\t')[1] ? lines.split('\t')[1].replaceAll(',', '').trim() : null,
|
|
1283
|
-
}))
|
|
1284
|
-
.filter((o) => o.image);
|
|
1285
|
-
}
|
|
1286
|
-
const raw = shellExec(node === 'kind-worker' ? `docker exec -i ${node} crictl images` : `crictl images`, {
|
|
1287
|
-
stdout: true,
|
|
1288
|
-
silent: true,
|
|
1289
|
-
});
|
|
1290
|
-
|
|
1291
|
-
const heads = raw
|
|
1292
|
-
.split(`\n`)[0]
|
|
1293
|
-
.split(' ')
|
|
1294
|
-
.filter((_r) => _r.trim());
|
|
1295
|
-
|
|
1296
|
-
const pods = raw
|
|
1297
|
-
.split(`\n`)
|
|
1298
|
-
.filter((r) => !r.match('IMAGE'))
|
|
1299
|
-
.map((r) => r.split(' ').filter((_r) => _r.trim()));
|
|
1300
|
-
|
|
1301
|
-
const result = [];
|
|
1302
|
-
|
|
1303
|
-
for (const row of pods) {
|
|
1304
|
-
if (row.length === 0) continue;
|
|
1305
|
-
const pod = {};
|
|
1306
|
-
let index = -1;
|
|
1307
|
-
for (const head of heads) {
|
|
1308
|
-
if (head in pod) continue;
|
|
1309
|
-
index++;
|
|
1310
|
-
pod[head] = row[index];
|
|
1311
|
-
}
|
|
1312
|
-
result.push(pod);
|
|
1313
|
-
}
|
|
1314
|
-
return result;
|
|
1315
|
-
},
|
|
1233
|
+
(env === 'production' &&
|
|
1234
|
+
options.cert === true &&
|
|
1235
|
+
(!options.certHosts || options.certHosts.split(',').includes(host))) ||
|
|
1236
|
+
options.selfSigned === true,
|
|
1316
1237
|
|
|
1317
1238
|
/**
|
|
1318
1239
|
* Predefined resource templates for Kubernetes deployments.
|
package/src/cli/env.js
CHANGED
|
@@ -95,10 +95,7 @@ class UnderpostRootEnv {
|
|
|
95
95
|
get(key, value, options = { plain: false, disableLog: false, copy: false }) {
|
|
96
96
|
const exeRootPath = `${getNpmRootPath()}/underpost`;
|
|
97
97
|
const envPath = `${exeRootPath}/.env`;
|
|
98
|
-
if (!fs.existsSync(envPath) || !fs.statSync(envPath).isFile())
|
|
99
|
-
logger.warn(`Empty environment variables`);
|
|
100
|
-
return undefined;
|
|
101
|
-
}
|
|
98
|
+
if (!fs.existsSync(envPath) || !fs.statSync(envPath).isFile()) return undefined;
|
|
102
99
|
const env = dotenv.parse(fs.readFileSync(envPath, 'utf8'));
|
|
103
100
|
if (!options.disableLog)
|
|
104
101
|
options?.plain === true ? console.log(env[key]) : logger.info(`${key}(${typeof env[key]})`, env[key]);
|