underpost 3.2.8 → 3.2.10
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/settings.json +10 -5
- package/CHANGELOG.md +223 -2
- package/CLI-HELP.md +36 -7
- package/README.md +38 -9
- package/bin/build.js +27 -11
- package/bin/deploy.js +20 -21
- package/bin/file.js +32 -13
- package/bin/index.js +2 -1
- package/bin/vs.js +1 -1
- package/bump.config.js +26 -0
- package/conf.js +20 -4
- package/manifests/cronjobs/dd-cron/dd-cron-backup.yaml +2 -2
- package/manifests/cronjobs/dd-cron/dd-cron-dns.yaml +2 -2
- 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/mongodb/pv-pvc.yaml +44 -8
- package/manifests/mongodb/statefulset.yaml +55 -68
- package/package.json +40 -25
- package/scripts/k3s-node-setup.sh +30 -11
- package/scripts/nat-iptables.sh +103 -18
- 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 +20 -11
- package/src/cli/cluster.js +243 -55
- package/src/cli/db.js +106 -62
- package/src/cli/deploy.js +297 -154
- package/src/cli/fs.js +19 -3
- package/src/cli/index.js +37 -9
- package/src/cli/ipfs.js +4 -6
- package/src/cli/kubectl.js +4 -1
- package/src/cli/lxd.js +217 -135
- package/src/cli/release.js +289 -131
- package/src/cli/repository.js +91 -34
- package/src/cli/run.js +297 -56
- package/src/cli/test.js +9 -3
- package/src/client/Default.index.js +9 -3
- package/src/client/components/core/Auth.js +19 -5
- package/src/client/components/core/Docs.js +6 -34
- package/src/client/components/core/FileExplorer.js +6 -6
- package/src/client/components/core/Modal.js +65 -2
- package/src/client/components/core/PanelForm.js +56 -52
- package/src/client/components/core/Recover.js +4 -4
- package/src/client/components/core/Worker.js +170 -350
- package/src/client/services/default/default.management.js +20 -25
- package/src/client/services/user/guest.service.js +10 -3
- package/src/client/sw/core.sw.js +174 -112
- package/src/db/DataBaseProvider.js +120 -20
- package/src/db/mongo/MongoBootstrap.js +587 -0
- package/src/db/mongo/MongooseDB.js +126 -22
- 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 +20 -65
- package/src/server/data-query.js +32 -20
- package/src/server/dns.js +22 -0
- package/src/server/process.js +180 -19
- package/src/server/runtime.js +1 -1
- package/src/server/start.js +26 -7
- package/src/server/valkey.js +9 -2
- 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/typedoc.json +10 -1
- 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
|
@@ -78,8 +78,7 @@ class UnderpostDeploy {
|
|
|
78
78
|
return `
|
|
79
79
|
- conditions:
|
|
80
80
|
- prefix: ${path}
|
|
81
|
-
${
|
|
82
|
-
pathRewritePolicy
|
|
81
|
+
${pathRewritePolicy
|
|
83
82
|
? `pathRewritePolicy:
|
|
84
83
|
replacePrefix:
|
|
85
84
|
${pathRewritePolicy.map(
|
|
@@ -89,30 +88,26 @@ class UnderpostDeploy {
|
|
|
89
88
|
).join(`
|
|
90
89
|
`)}`
|
|
91
90
|
: ''
|
|
92
|
-
|
|
93
|
-
timeoutPolicy
|
|
94
|
-
|
|
95
|
-
timeoutPolicy.idle ? ` idle: ${timeoutPolicy.idle}\n` : ''
|
|
96
|
-
}`
|
|
91
|
+
}${timeoutPolicy
|
|
92
|
+
? `\n timeoutPolicy:\n${timeoutPolicy.response ? ` response: ${timeoutPolicy.response}\n` : ''}${timeoutPolicy.idle ? ` idle: ${timeoutPolicy.idle}\n` : ''
|
|
93
|
+
}`
|
|
97
94
|
: ''
|
|
98
|
-
|
|
99
|
-
retryPolicy
|
|
100
|
-
|
|
101
|
-
retryPolicy.perTryTimeout ? ` perTryTimeout: ${retryPolicy.perTryTimeout}\n` : ''
|
|
102
|
-
}`
|
|
95
|
+
}${retryPolicy
|
|
96
|
+
? `\n retryPolicy:\n${retryPolicy.count !== undefined ? ` count: ${retryPolicy.count}\n` : ''}${retryPolicy.perTryTimeout ? ` perTryTimeout: ${retryPolicy.perTryTimeout}\n` : ''
|
|
97
|
+
}`
|
|
103
98
|
: ''
|
|
104
|
-
|
|
99
|
+
}
|
|
105
100
|
enableWebsockets: true
|
|
106
101
|
services:
|
|
107
102
|
${deploymentVersions
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
103
|
+
.map(
|
|
104
|
+
(version, i) =>
|
|
105
|
+
` - name: ${serviceId ? serviceId : `${deployId}-${env}-${version}-service`}
|
|
111
106
|
port: ${port}
|
|
112
107
|
weight: ${i === 0 ? 100 : 0}
|
|
113
108
|
`,
|
|
114
|
-
|
|
115
|
-
|
|
109
|
+
)
|
|
110
|
+
.join('')}`;
|
|
116
111
|
},
|
|
117
112
|
/**
|
|
118
113
|
* Creates a YAML deployment configuration for a deployment.
|
|
@@ -125,17 +120,51 @@ class UnderpostDeploy {
|
|
|
125
120
|
* @param {string} namespace - Kubernetes namespace for the deployment.
|
|
126
121
|
* @param {Array<object>} volumes - Volume configurations for the deployment.
|
|
127
122
|
* @param {Array<string>} cmd - Command to run in the deployment container.
|
|
123
|
+
* @param {boolean} skipFullBuild - Whether to skip the full client bundle build during deployment.
|
|
124
|
+
* @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.
|
|
125
|
+
* @param {string} [imagePullPolicy] - Container imagePullPolicy override (`Always`, `IfNotPresent`, `Never`). When omitted, defaults to `Never` for `localhost/` images and `IfNotPresent` otherwise.
|
|
128
126
|
* @returns {string} - YAML deployment configuration for the specified deployment.
|
|
129
127
|
* @memberof UnderpostDeploy
|
|
130
128
|
*/
|
|
131
|
-
deploymentYamlPartsFactory({
|
|
129
|
+
deploymentYamlPartsFactory({
|
|
130
|
+
deployId,
|
|
131
|
+
env,
|
|
132
|
+
suffix,
|
|
133
|
+
resources,
|
|
134
|
+
replicas,
|
|
135
|
+
image,
|
|
136
|
+
namespace,
|
|
137
|
+
volumes,
|
|
138
|
+
cmd,
|
|
139
|
+
skipFullBuild,
|
|
140
|
+
pullBundle,
|
|
141
|
+
imagePullPolicy,
|
|
142
|
+
// K8S lifecycle + probe wiring. Pass-through structures shaped like the
|
|
143
|
+
// upstream Kubernetes API, spliced verbatim into the container spec.
|
|
144
|
+
// lifecycle: { postStart: { exec: { command: [...] } }, preStop: { exec: { command: [...] } } }
|
|
145
|
+
// readinessProbe: { tcpSocket: { port: 8081 }, ... }
|
|
146
|
+
// livenessProbe: { tcpSocket: { port: 8081 }, ... }
|
|
147
|
+
// containerPort: integer; rendered as ports[0].containerPort. Optional.
|
|
148
|
+
lifecycle,
|
|
149
|
+
readinessProbe,
|
|
150
|
+
livenessProbe,
|
|
151
|
+
containerPort,
|
|
152
|
+
}) {
|
|
132
153
|
if (!cmd)
|
|
133
|
-
cmd =
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
154
|
+
cmd =
|
|
155
|
+
pullBundle || skipFullBuild
|
|
156
|
+
? [
|
|
157
|
+
// When pullBundle (or skipFullBuild) is set the container pulls the pre-built client
|
|
158
|
+
// bundle from Cloudinary (push-bundle must have been run on the dev machine beforehand).
|
|
159
|
+
`underpost secret underpost --create-from-env`,
|
|
160
|
+
`underpost start --build --run --pull-bundle --skip-full-build ${deployId} ${env}`,
|
|
161
|
+
]
|
|
162
|
+
: [
|
|
163
|
+
// `npm install -g npm@11.2.0`,
|
|
164
|
+
// `npm install -g underpost`,
|
|
165
|
+
`underpost secret underpost --create-from-env`,
|
|
166
|
+
`underpost start --build --run ${deployId} ${env}`,
|
|
167
|
+
];
|
|
139
168
|
const packageJson = JSON.parse(fs.readFileSync('./package.json', 'utf8'));
|
|
140
169
|
if (!volumes) volumes = [];
|
|
141
170
|
const confVolume = fs.existsSync(`./engine-private/conf/${deployId}/conf.volume.json`)
|
|
@@ -166,36 +195,64 @@ spec:
|
|
|
166
195
|
containers:
|
|
167
196
|
- name: ${deployId}-${env}-${suffix}
|
|
168
197
|
image: ${containerImage}
|
|
169
|
-
imagePullPolicy: ${containerImage.startsWith('localhost/') ? 'Never' : 'IfNotPresent'}
|
|
198
|
+
imagePullPolicy: ${imagePullPolicy ? imagePullPolicy : containerImage.startsWith('localhost/') ? 'Never' : 'IfNotPresent'}
|
|
170
199
|
envFrom:
|
|
171
200
|
- secretRef:
|
|
172
201
|
name: underpost-config
|
|
173
|
-
${
|
|
174
|
-
|
|
175
|
-
|
|
202
|
+
${containerPort
|
|
203
|
+
? ` ports:
|
|
204
|
+
- containerPort: ${containerPort}
|
|
205
|
+
`
|
|
206
|
+
: ''
|
|
207
|
+
}${resources
|
|
208
|
+
? ` resources:
|
|
176
209
|
requests:
|
|
177
210
|
memory: "${resources.requests.memory}"
|
|
178
211
|
cpu: "${resources.requests.cpu}"
|
|
179
212
|
limits:
|
|
180
213
|
memory: "${resources.limits.memory}"
|
|
181
214
|
cpu: "${resources.limits.cpu}"`
|
|
182
|
-
|
|
183
|
-
}
|
|
215
|
+
: ''
|
|
216
|
+
}
|
|
184
217
|
command:
|
|
185
218
|
- /bin/sh
|
|
186
219
|
- -c
|
|
187
220
|
- >
|
|
188
221
|
${cmd.join(' &&\n ')}
|
|
222
|
+
${readinessProbe
|
|
223
|
+
? ` readinessProbe:
|
|
224
|
+
${JSON.stringify(readinessProbe, null, 2)
|
|
225
|
+
.split('\n')
|
|
226
|
+
.map((l) => ' ' + l)
|
|
227
|
+
.join('\n')}
|
|
228
|
+
`
|
|
229
|
+
: ''
|
|
230
|
+
}${livenessProbe
|
|
231
|
+
? ` livenessProbe:
|
|
232
|
+
${JSON.stringify(livenessProbe, null, 2)
|
|
233
|
+
.split('\n')
|
|
234
|
+
.map((l) => ' ' + l)
|
|
235
|
+
.join('\n')}
|
|
236
|
+
`
|
|
237
|
+
: ''
|
|
238
|
+
}${lifecycle
|
|
239
|
+
? ` lifecycle:
|
|
240
|
+
${JSON.stringify(lifecycle, null, 2)
|
|
241
|
+
.split('\n')
|
|
242
|
+
.map((l) => ' ' + l)
|
|
243
|
+
.join('\n')}
|
|
244
|
+
`
|
|
245
|
+
: ''
|
|
246
|
+
}
|
|
189
247
|
|
|
190
|
-
${
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
}
|
|
248
|
+
${volumes.length > 0
|
|
249
|
+
? Underpost.deploy
|
|
250
|
+
.volumeFactory(volumes.map((v) => ((v.version = `${deployId}-${env}-${suffix}`), v)))
|
|
251
|
+
.render.split(`\n`)
|
|
252
|
+
.map((l) => ' ' + l)
|
|
253
|
+
.join(`\n`)
|
|
254
|
+
: ''
|
|
255
|
+
}
|
|
199
256
|
---
|
|
200
257
|
apiVersion: v1
|
|
201
258
|
kind: Service
|
|
@@ -224,6 +281,9 @@ spec:
|
|
|
224
281
|
* @param {string} [options.retryPerTryTimeout] - Retry per-try timeout setting for the deployment.
|
|
225
282
|
* @param {boolean} [options.disableDeploymentProxy] - Whether to disable deployment proxy.
|
|
226
283
|
* @param {string} [options.traffic] - Traffic status for the deployment.
|
|
284
|
+
* @param {boolean} [options.skipFullBuild] - Whether to skip the full client bundle build; forwarded to deploymentYamlPartsFactory to generate a pull-bundle startup command.
|
|
285
|
+
* @param {boolean} [options.pullBundle] - Whether to pull the pre-built client bundle from Cloudinary; forwarded to deploymentYamlPartsFactory. Use together with skipFullBuild.
|
|
286
|
+
* @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.
|
|
227
287
|
* @returns {Promise<void>} - Promise that resolves when the manifest is built.
|
|
228
288
|
* @memberof UnderpostDeploy
|
|
229
289
|
*/
|
|
@@ -252,16 +312,19 @@ spec:
|
|
|
252
312
|
for (const deploymentVersion of deploymentVersions) {
|
|
253
313
|
deploymentYamlParts += `---
|
|
254
314
|
${Underpost.deploy
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
315
|
+
.deploymentYamlPartsFactory({
|
|
316
|
+
deployId,
|
|
317
|
+
env,
|
|
318
|
+
suffix: deploymentVersion,
|
|
319
|
+
replicas,
|
|
320
|
+
image,
|
|
321
|
+
namespace: options.namespace,
|
|
322
|
+
cmd: options.cmd ? options.cmd.split(',').map((c) => c.trim()) : undefined,
|
|
323
|
+
skipFullBuild: options.skipFullBuild,
|
|
324
|
+
pullBundle: options.pullBundle,
|
|
325
|
+
imagePullPolicy: options.imagePullPolicy,
|
|
326
|
+
})
|
|
327
|
+
.replace('{{ports}}', buildKindPorts(fromPort, toPort))}
|
|
265
328
|
`;
|
|
266
329
|
}
|
|
267
330
|
fs.writeFileSync(`./engine-private/conf/${deployId}/build/${env}/deployment.yaml`, deploymentYamlParts, 'utf8');
|
|
@@ -313,20 +376,20 @@ ${Underpost.deploy
|
|
|
313
376
|
let proxyRoutes = '';
|
|
314
377
|
const globalTimeoutPolicy =
|
|
315
378
|
(options.timeoutResponse && options.timeoutResponse !== '') ||
|
|
316
|
-
|
|
379
|
+
(options.timeoutIdle && options.timeoutIdle !== '')
|
|
317
380
|
? {
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
381
|
+
response: options.timeoutResponse,
|
|
382
|
+
idle: options.timeoutIdle,
|
|
383
|
+
}
|
|
321
384
|
: undefined;
|
|
322
385
|
const globalRetryPolicy =
|
|
323
386
|
options.retryCount ||
|
|
324
|
-
|
|
325
|
-
|
|
387
|
+
options.retryCount === 0 ||
|
|
388
|
+
(options.retryPerTryTimeout && options.retryPerTryTimeout !== '')
|
|
326
389
|
? {
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
390
|
+
count: options.retryCount,
|
|
391
|
+
perTryTimeout: options.retryPerTryTimeout,
|
|
392
|
+
}
|
|
330
393
|
: undefined;
|
|
331
394
|
if (!options.disableDeploymentProxy)
|
|
332
395
|
for (const conditionObj of pathPortAssignment) {
|
|
@@ -483,10 +546,15 @@ spec:
|
|
|
483
546
|
const hostTest = options?.hostTest
|
|
484
547
|
? options.hostTest
|
|
485
548
|
: Object.keys(loadConfServerJson(`./engine-private/conf/${deployId}/conf.server.json`))[0];
|
|
549
|
+
// Missing HTTPProxy is the canonical "no traffic colour set yet" state
|
|
550
|
+
// for blue/green rollouts. silentOnError swallows kubectl's NotFound
|
|
551
|
+
// exit so the function can return null cleanly.
|
|
486
552
|
const info = shellExec(`sudo kubectl get HTTPProxy/${hostTest} -n ${options.namespace} -o yaml`, {
|
|
487
553
|
silent: true,
|
|
488
554
|
stdout: true,
|
|
555
|
+
silentOnError: true,
|
|
489
556
|
});
|
|
557
|
+
if (!info) return null;
|
|
490
558
|
return info.match('blue') ? 'blue' : info.match('green') ? 'green' : null;
|
|
491
559
|
},
|
|
492
560
|
|
|
@@ -509,13 +577,12 @@ metadata:
|
|
|
509
577
|
namespace: ${options.namespace}
|
|
510
578
|
spec:
|
|
511
579
|
virtualhost:
|
|
512
|
-
fqdn: ${host}${
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
: `
|
|
580
|
+
fqdn: ${host}${env === 'development'
|
|
581
|
+
? ''
|
|
582
|
+
: `
|
|
516
583
|
tls:
|
|
517
584
|
secretName: ${host}`
|
|
518
|
-
|
|
585
|
+
}
|
|
519
586
|
routes:`;
|
|
520
587
|
},
|
|
521
588
|
|
|
@@ -553,10 +620,14 @@ spec:
|
|
|
553
620
|
* @param {string} [options.kindType] - Type of Kubernetes resource to retrieve information for.
|
|
554
621
|
* @param {number} [options.port] - Port number for exposing the deployment.
|
|
555
622
|
* @param {string} [options.cmd] - Custom initialization command for deploymentYamlPartsFactory (comma-separated commands).
|
|
623
|
+
* @param {number} [options.exposePort] - Local:remote port override when --expose is active (overrides auto-detected service port).
|
|
556
624
|
* @param {boolean} [options.k3s] - Whether to use k3s cluster context.
|
|
557
625
|
* @param {boolean} [options.kubeadm] - Whether to use kubeadm cluster context.
|
|
558
626
|
* @param {boolean} [options.kind] - Whether to use kind cluster context.
|
|
559
627
|
* @param {boolean} [options.gitClean] - Whether to run git clean on volume mount paths before copying.
|
|
628
|
+
* @param {boolean} [options.skipFullBuild] - Whether to skip the full client bundle build; passed through to buildManifest/deploymentYamlPartsFactory.
|
|
629
|
+
* @param {boolean} [options.pullBundle] - Whether to pull the pre-built client bundle from Cloudinary; passed through to buildManifest/deploymentYamlPartsFactory. Use together with skipFullBuild.
|
|
630
|
+
* @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.
|
|
560
631
|
* @returns {Promise<void>} - Promise that resolves when the deployment process is complete.
|
|
561
632
|
* @memberof UnderpostDeploy
|
|
562
633
|
*/
|
|
@@ -598,6 +669,7 @@ spec:
|
|
|
598
669
|
kubeadm: false,
|
|
599
670
|
kind: false,
|
|
600
671
|
gitClean: false,
|
|
672
|
+
imagePullPolicy: '',
|
|
601
673
|
},
|
|
602
674
|
) {
|
|
603
675
|
const namespace = options.namespace ? options.namespace : 'default';
|
|
@@ -772,25 +844,41 @@ EOF`);
|
|
|
772
844
|
* @memberof UnderpostDeploy
|
|
773
845
|
*/
|
|
774
846
|
async checkDeploymentReadyStatus(deployId, env, traffic, ignoresNames = [], namespace = 'default') {
|
|
775
|
-
const cmd = `underpost config get container-status`;
|
|
776
847
|
const pods = Underpost.kubectl.get(`${deployId}-${env}-${traffic}`, 'pods', namespace);
|
|
777
848
|
const readyPods = [];
|
|
778
849
|
const notReadyPods = [];
|
|
850
|
+
|
|
851
|
+
// Readiness signal: the pod's Kubernetes `Ready` condition driven by the
|
|
852
|
+
// container's readinessProbe (TCP socket, HTTP get, or exec). Set by kubelet
|
|
853
|
+
// when the probe passes. A failed or crashing runtime never becomes Ready —
|
|
854
|
+
// kubelet surfaces CrashLoopBackOff and this gate stays closed.
|
|
779
855
|
for (const pod of pods) {
|
|
780
856
|
const { NAME } = pod;
|
|
781
857
|
if (ignoresNames && ignoresNames.find((t) => NAME.trim().toLowerCase().match(t.trim().toLowerCase()))) continue;
|
|
782
|
-
|
|
783
|
-
|
|
858
|
+
|
|
859
|
+
let podJson = null;
|
|
860
|
+
try {
|
|
861
|
+
// Pod may not exist yet (between deployment apply and pod
|
|
862
|
+
// scheduling). silentOnError lets the monitor loop continue
|
|
863
|
+
// instead of aborting on the transient NotFound exit.
|
|
864
|
+
const raw = shellExec(`sudo kubectl get pod ${NAME} -n ${namespace} -o json`, {
|
|
784
865
|
silent: true,
|
|
785
866
|
disableLog: true,
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
},
|
|
867
|
+
stdout: true,
|
|
868
|
+
silentOnError: true,
|
|
789
869
|
});
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
870
|
+
podJson = raw ? JSON.parse(raw) : null;
|
|
871
|
+
} catch (_) {
|
|
872
|
+
podJson = null;
|
|
873
|
+
}
|
|
874
|
+
const conditions = podJson?.status?.conditions || [];
|
|
875
|
+
const readyCondition = conditions.find((c) => c.type === 'Ready');
|
|
876
|
+
const k8sReady = readyCondition?.status === 'True';
|
|
877
|
+
|
|
878
|
+
pod.out = JSON.stringify({ k8sReady, condition: readyCondition ?? null });
|
|
879
|
+
|
|
880
|
+
if (k8sReady) readyPods.push(pod);
|
|
881
|
+
else notReadyPods.push(pod);
|
|
794
882
|
}
|
|
795
883
|
return {
|
|
796
884
|
ready: pods.length > 0 && notReadyPods.length === 0,
|
|
@@ -824,6 +912,7 @@ EOF`);
|
|
|
824
912
|
* @param {string} options.timeoutIdle - Timeout idle setting for the deployment.
|
|
825
913
|
* @param {string} options.retryCount - Retry count setting for the deployment.
|
|
826
914
|
* @param {string} options.retryPerTryTimeout - Retry per-try timeout setting for the deployment.
|
|
915
|
+
* @param {string} [options.imagePullPolicy] - Container imagePullPolicy override; forwarded to the manifest rebuild triggered here.
|
|
827
916
|
* @memberof UnderpostDeploy
|
|
828
917
|
*/
|
|
829
918
|
switchTraffic(
|
|
@@ -837,12 +926,14 @@ EOF`);
|
|
|
837
926
|
timeoutIdle: '',
|
|
838
927
|
retryCount: '',
|
|
839
928
|
retryPerTryTimeout: '',
|
|
929
|
+
imagePullPolicy: '',
|
|
840
930
|
},
|
|
841
931
|
) {
|
|
842
932
|
const timeoutFlags = Underpost.deploy.timeoutFlagsFactory(options);
|
|
933
|
+
const imagePullPolicyFlag = options.imagePullPolicy ? ` --image-pull-policy ${options.imagePullPolicy}` : '';
|
|
843
934
|
|
|
844
935
|
shellExec(
|
|
845
|
-
`node bin deploy --info-router --build-manifest --traffic ${targetTraffic} --replicas ${replicas} --namespace ${namespace}${timeoutFlags} ${deployId} ${env}`,
|
|
936
|
+
`node bin deploy --info-router --build-manifest --traffic ${targetTraffic} --replicas ${replicas} --namespace ${namespace}${timeoutFlags}${imagePullPolicyFlag} ${deployId} ${env}`,
|
|
846
937
|
);
|
|
847
938
|
|
|
848
939
|
shellExec(`sudo kubectl apply -f ./engine-private/conf/${deployId}/build/${env}/proxy.yaml -n ${namespace}`);
|
|
@@ -911,10 +1002,10 @@ EOF`);
|
|
|
911
1002
|
shellExec(`kubectl delete pv ${pvId} --ignore-not-found`);
|
|
912
1003
|
shellExec(`kubectl apply -f - -n ${namespace} <<EOF
|
|
913
1004
|
${Underpost.deploy.persistentVolumeFactory({
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
})}
|
|
1005
|
+
hostPath: rootVolumeHostPath,
|
|
1006
|
+
pvcId,
|
|
1007
|
+
namespace,
|
|
1008
|
+
})}
|
|
918
1009
|
EOF
|
|
919
1010
|
`);
|
|
920
1011
|
},
|
|
@@ -973,23 +1064,22 @@ ${secret ? ` readOnly: true\n` : ''}`;
|
|
|
973
1064
|
|
|
974
1065
|
_volumes += `
|
|
975
1066
|
- name: ${volumeName}
|
|
976
|
-
${
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
:
|
|
980
|
-
? ` secret:
|
|
1067
|
+
${emptyDir
|
|
1068
|
+
? ` emptyDir: {}`
|
|
1069
|
+
: secret
|
|
1070
|
+
? ` secret:
|
|
981
1071
|
secretName: ${secret}`
|
|
982
|
-
|
|
983
|
-
|
|
1072
|
+
: configMap
|
|
1073
|
+
? ` configMap:
|
|
984
1074
|
name: ${configMap}`
|
|
985
|
-
|
|
986
|
-
|
|
1075
|
+
: claimName
|
|
1076
|
+
? ` persistentVolumeClaim:
|
|
987
1077
|
claimName: ${claimName}`
|
|
988
|
-
|
|
1078
|
+
: ` hostPath:
|
|
989
1079
|
path: ${volumeHostPath}
|
|
990
1080
|
type: ${volumeType}
|
|
991
1081
|
`
|
|
992
|
-
|
|
1082
|
+
}
|
|
993
1083
|
|
|
994
1084
|
`;
|
|
995
1085
|
});
|
|
@@ -1070,7 +1160,7 @@ spec:
|
|
|
1070
1160
|
fs.writeFileSync(
|
|
1071
1161
|
`/etc/hosts`,
|
|
1072
1162
|
fs.readFileSync(`/etc/hosts`, 'utf8') +
|
|
1073
|
-
|
|
1163
|
+
`
|
|
1074
1164
|
${renderHosts}`,
|
|
1075
1165
|
'utf8',
|
|
1076
1166
|
);
|
|
@@ -1090,9 +1180,25 @@ ${renderHosts}`,
|
|
|
1090
1180
|
env === 'production' &&
|
|
1091
1181
|
options.cert === true &&
|
|
1092
1182
|
(!options.certHosts || options.certHosts.split(',').includes(host)),
|
|
1093
|
-
|
|
1094
1183
|
/**
|
|
1095
1184
|
* Monitors the ready status of a deployment.
|
|
1185
|
+
*
|
|
1186
|
+
* Ready signal:
|
|
1187
|
+
* The orchestrator gate is the Kubernetes pod Ready condition. When the
|
|
1188
|
+
* container's `readinessProbe` succeeds, kubelet flips
|
|
1189
|
+
* `status.conditions[Ready]` to True and `checkDeploymentReadyStatus`
|
|
1190
|
+
* returns the pod in `readyPods`. This is the only required signal — see
|
|
1191
|
+
* `src/client/public/nexodev/docs/references/Deploy custom instance to K8S.md`.
|
|
1192
|
+
*
|
|
1193
|
+
* Container-status:
|
|
1194
|
+
* `underpost config get container-status` is still read from each pod for
|
|
1195
|
+
* the display column and for early-abort on `error`, but it is no longer
|
|
1196
|
+
* required to equal `<deploy>-running-deployment` to finish the monitor.
|
|
1197
|
+
* Older implementations gated on it; that produced false timeouts for
|
|
1198
|
+
* runtimes (e.g. cyberia-server's Go binary, cyberia-client's server.py)
|
|
1199
|
+
* whose startup sequence didn't reliably overwrite the
|
|
1200
|
+
* `initializing-deployment` stamp set by the postStart lifecycle hook.
|
|
1201
|
+
*
|
|
1096
1202
|
* @param {string} deployId - Deployment ID for which the ready status is being monitored.
|
|
1097
1203
|
* @param {string} env - Environment for which the ready status is being monitored.
|
|
1098
1204
|
* @param {string} targetTraffic - Target traffic status for the deployment.
|
|
@@ -1102,79 +1208,88 @@ ${renderHosts}`,
|
|
|
1102
1208
|
* @memberof UnderpostDeploy
|
|
1103
1209
|
*/
|
|
1104
1210
|
async monitorReadyRunner(deployId, env, targetTraffic, ignorePods = [], namespace = 'default') {
|
|
1105
|
-
|
|
1106
|
-
const checkStatusIterationMsDelay = 1000;
|
|
1211
|
+
const delayMs = 1000;
|
|
1107
1212
|
const maxIterations = 3000;
|
|
1108
1213
|
const deploymentId = `${deployId}-${env}-${targetTraffic}`;
|
|
1109
|
-
const
|
|
1110
|
-
|
|
1111
|
-
const
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1214
|
+
const expectedContainerStatus = `${deployId}-${env}-running-deployment`;
|
|
1215
|
+
const tag = `[${deploymentId}]`;
|
|
1216
|
+
const containerStatusDefault = 'waiting for status';
|
|
1217
|
+
|
|
1218
|
+
logger.info('Deployment init', { deployId, env, targetTraffic, namespace });
|
|
1219
|
+
|
|
1220
|
+
// Per-pod cache of last-known container-status (persists across retries)
|
|
1221
|
+
const podStatusCache = new Map();
|
|
1222
|
+
|
|
1223
|
+
const readContainerStatus = (podName) => {
|
|
1224
|
+
try {
|
|
1225
|
+
const raw = shellExec(
|
|
1226
|
+
`sudo kubectl exec ${podName} -n ${namespace} -- sh -c 'underpost config get container-status --plain'`,
|
|
1227
|
+
{ silent: true, disableLog: true, stdout: true, silentOnError: true },
|
|
1123
1228
|
);
|
|
1124
|
-
|
|
1229
|
+
const val = raw ? raw.toString().trim() : '';
|
|
1230
|
+
return val && val !== 'undefined' ? val : containerStatusDefault;
|
|
1231
|
+
} catch (_) {
|
|
1232
|
+
// exec failed (e.g. pod not yet running) — preserve last known value
|
|
1233
|
+
return podStatusCache.get(podName) || containerStatusDefault;
|
|
1125
1234
|
}
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1235
|
+
};
|
|
1236
|
+
|
|
1237
|
+
|
|
1238
|
+
for (let i = 0; i < maxIterations; i++) {
|
|
1239
|
+
const result = await Underpost.deploy.checkDeploymentReadyStatus(
|
|
1240
|
+
deployId, env, targetTraffic, ignorePods, namespace,
|
|
1241
|
+
);
|
|
1242
|
+
|
|
1243
|
+
const allPods = [...result.readyPods, ...result.notReadyPods];
|
|
1244
|
+
|
|
1245
|
+
// Update cache with latest status for each pod (informational + error gate)
|
|
1246
|
+
for (const pod of allPods) {
|
|
1247
|
+
if (!pod?.NAME) continue;
|
|
1248
|
+
const status = readContainerStatus(pod.NAME);
|
|
1249
|
+
if (status === 'error')
|
|
1250
|
+
throw new Error(`Pod ${pod.NAME} has error status`);
|
|
1251
|
+
podStatusCache.set(pod.NAME, status);
|
|
1140
1252
|
}
|
|
1141
1253
|
|
|
1142
|
-
|
|
1143
|
-
let indexOf = -1;
|
|
1144
|
-
for (const pod of result.notReadyPods) {
|
|
1145
|
-
indexOf++;
|
|
1146
|
-
const { NAME, out } = pod;
|
|
1254
|
+
const allPodsK8sReady = allPods.length > 0 && result.notReadyPods.length === 0;
|
|
1147
1255
|
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
else if (out.match(`${deployId}-${env}-build-deployment`)) lastMsg[NAME] = 'Building apps/services';
|
|
1156
|
-
else if (out.match(`${deployId}-${env}-initializing-deployment`))
|
|
1157
|
-
lastMsg[NAME] = 'Initializing apps/services';
|
|
1158
|
-
else if (!lastMsg[NAME]) lastMsg[NAME] = `Waiting for status`;
|
|
1256
|
+
// Print snapshot for every pod — annotate when container-status hasn't caught
|
|
1257
|
+
// up to the K8S Ready condition (informational only; no longer gates exit).
|
|
1258
|
+
for (const pod of allPods) {
|
|
1259
|
+
const status = podStatusCache.get(pod.NAME) || containerStatusDefault;
|
|
1260
|
+
const podStatus = pod.STATUS || 'Unknown';
|
|
1261
|
+
const statusMatchesExpected = status === expectedContainerStatus;
|
|
1262
|
+
const statusDisplay = statusMatchesExpected ? status : `${status} (advisory)`;
|
|
1159
1263
|
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1264
|
+
console.log(
|
|
1265
|
+
'Target pod:',
|
|
1266
|
+
pod.NAME[pod.NAME.includes('green') ? 'bgGreen' : 'bgBlue'].bold.black,
|
|
1267
|
+
'| Pod status:',
|
|
1268
|
+
podStatus.bold.yellow,
|
|
1269
|
+
'| Runtime status:',
|
|
1270
|
+
statusDisplay.bold.cyan,
|
|
1271
|
+
);
|
|
1272
|
+
}
|
|
1273
|
+
|
|
1274
|
+
// Finish as soon as every pod is K8S-Ready. The readinessProbe (TCP
|
|
1275
|
+
// socket on the listening port) is the source of truth — a runtime
|
|
1276
|
+
// that can't bind never reaches Ready and the monitor will time out.
|
|
1277
|
+
if (allPodsK8sReady) {
|
|
1278
|
+
logger.info(`${tag} | All pods Ready (K8S readinessProbe satisfied)`);
|
|
1279
|
+
return result;
|
|
1280
|
+
}
|
|
1281
|
+
|
|
1282
|
+
await timer(delayMs);
|
|
1283
|
+
|
|
1284
|
+
if ((i + 1) % 10 === 0) {
|
|
1285
|
+
logger.info(`${tag} | In progress... iteration ${i + 1}`);
|
|
1167
1286
|
}
|
|
1168
|
-
await timer(checkStatusIterationMsDelay);
|
|
1169
|
-
checkStatusIteration++;
|
|
1170
|
-
logger.info(
|
|
1171
|
-
`${iteratorTag} | Deployment in progress... | Delay number monitor iterations: ${checkStatusIteration}`,
|
|
1172
|
-
);
|
|
1173
1287
|
}
|
|
1174
|
-
|
|
1175
|
-
|
|
1288
|
+
|
|
1289
|
+
logger.error(`${tag} | Deployment timeout after ${maxIterations} iterations`);
|
|
1290
|
+
throw new Error(
|
|
1291
|
+
`monitorReadyRunner timeout: ${deploymentId} did not become Ready within ${maxIterations}*${delayMs}ms`,
|
|
1176
1292
|
);
|
|
1177
|
-
return result;
|
|
1178
1293
|
},
|
|
1179
1294
|
|
|
1180
1295
|
/**
|
|
@@ -1334,6 +1449,34 @@ ${renderHosts}`,
|
|
|
1334
1449
|
return undefined;
|
|
1335
1450
|
},
|
|
1336
1451
|
|
|
1452
|
+
/**
|
|
1453
|
+
* Extracts a non-standard `imagePullPolicy` key from an env-resolved
|
|
1454
|
+
* instance lifecycle block (the convention used in `conf.instances.json`,
|
|
1455
|
+
* where `imagePullPolicy` sits alongside `postStart`/`preStop` for
|
|
1456
|
+
* per-instance ergonomics) and returns a clean lifecycle hash that is
|
|
1457
|
+
* safe to splice into the K8S container spec.
|
|
1458
|
+
*
|
|
1459
|
+
* Returns `{ lifecycle, imagePullPolicy }`:
|
|
1460
|
+
* - `lifecycle` — the input minus `imagePullPolicy`, or `undefined` when
|
|
1461
|
+
* the resulting block is empty.
|
|
1462
|
+
* - `imagePullPolicy` — the extracted value, or `undefined` if absent.
|
|
1463
|
+
*
|
|
1464
|
+
* @param {object|undefined} lifecycle - Env-resolved lifecycle block
|
|
1465
|
+
* (already passed through pickEnv). May be `undefined`.
|
|
1466
|
+
* @returns {{ lifecycle: (object|undefined), imagePullPolicy: (string|undefined) }}
|
|
1467
|
+
* @memberof UnderpostDeploy
|
|
1468
|
+
*/
|
|
1469
|
+
extractInstanceImagePullPolicy(lifecycle) {
|
|
1470
|
+
if (!lifecycle || typeof lifecycle !== 'object' || !('imagePullPolicy' in lifecycle)) {
|
|
1471
|
+
return { lifecycle, imagePullPolicy: undefined };
|
|
1472
|
+
}
|
|
1473
|
+
const { imagePullPolicy, ...rest } = lifecycle;
|
|
1474
|
+
return {
|
|
1475
|
+
lifecycle: Object.keys(rest).length > 0 ? rest : undefined,
|
|
1476
|
+
imagePullPolicy,
|
|
1477
|
+
};
|
|
1478
|
+
},
|
|
1479
|
+
|
|
1337
1480
|
/**
|
|
1338
1481
|
* Generates timeout flags string for deployment commands.
|
|
1339
1482
|
* @param {object} options - Options containing timeout settings.
|