underpost 3.2.9 → 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 +122 -1
- package/CLI-HELP.md +22 -7
- package/README.md +37 -8
- package/bin/build.js +26 -9
- package/bin/deploy.js +20 -21
- package/bin/file.js +31 -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 +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/mongodb/pv-pvc.yaml +44 -8
- package/manifests/mongodb/statefulset.yaml +55 -68
- package/package.json +27 -12
- package/scripts/k3s-node-setup.sh +28 -9
- 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 +196 -55
- package/src/cli/db.js +59 -60
- package/src/cli/deploy.js +273 -159
- package/src/cli/fs.js +3 -1
- package/src/cli/index.js +16 -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 +58 -7
- package/src/cli/run.js +152 -25
- package/src/cli/test.js +9 -3
- package/src/client/Default.index.js +9 -3
- package/src/client/components/core/Auth.js +4 -0
- 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 +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/process.js +180 -19
- package/src/server/runtime.js +1 -1
- package/src/server/start.js +12 -4
- 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/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.
|
|
@@ -127,6 +122,7 @@ class UnderpostDeploy {
|
|
|
127
122
|
* @param {Array<string>} cmd - Command to run in the deployment container.
|
|
128
123
|
* @param {boolean} skipFullBuild - Whether to skip the full client bundle build during deployment.
|
|
129
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.
|
|
130
126
|
* @returns {string} - YAML deployment configuration for the specified deployment.
|
|
131
127
|
* @memberof UnderpostDeploy
|
|
132
128
|
*/
|
|
@@ -142,22 +138,33 @@ class UnderpostDeploy {
|
|
|
142
138
|
cmd,
|
|
143
139
|
skipFullBuild,
|
|
144
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,
|
|
145
152
|
}) {
|
|
146
153
|
if (!cmd)
|
|
147
154
|
cmd =
|
|
148
155
|
pullBundle || skipFullBuild
|
|
149
156
|
? [
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
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
|
+
]
|
|
155
162
|
: [
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
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
|
+
];
|
|
161
168
|
const packageJson = JSON.parse(fs.readFileSync('./package.json', 'utf8'));
|
|
162
169
|
if (!volumes) volumes = [];
|
|
163
170
|
const confVolume = fs.existsSync(`./engine-private/conf/${deployId}/conf.volume.json`)
|
|
@@ -188,36 +195,64 @@ spec:
|
|
|
188
195
|
containers:
|
|
189
196
|
- name: ${deployId}-${env}-${suffix}
|
|
190
197
|
image: ${containerImage}
|
|
191
|
-
imagePullPolicy: ${containerImage.startsWith('localhost/') ? 'Never' : 'IfNotPresent'}
|
|
198
|
+
imagePullPolicy: ${imagePullPolicy ? imagePullPolicy : containerImage.startsWith('localhost/') ? 'Never' : 'IfNotPresent'}
|
|
192
199
|
envFrom:
|
|
193
200
|
- secretRef:
|
|
194
201
|
name: underpost-config
|
|
195
|
-
${
|
|
196
|
-
|
|
197
|
-
|
|
202
|
+
${containerPort
|
|
203
|
+
? ` ports:
|
|
204
|
+
- containerPort: ${containerPort}
|
|
205
|
+
`
|
|
206
|
+
: ''
|
|
207
|
+
}${resources
|
|
208
|
+
? ` resources:
|
|
198
209
|
requests:
|
|
199
210
|
memory: "${resources.requests.memory}"
|
|
200
211
|
cpu: "${resources.requests.cpu}"
|
|
201
212
|
limits:
|
|
202
213
|
memory: "${resources.limits.memory}"
|
|
203
214
|
cpu: "${resources.limits.cpu}"`
|
|
204
|
-
|
|
205
|
-
}
|
|
215
|
+
: ''
|
|
216
|
+
}
|
|
206
217
|
command:
|
|
207
218
|
- /bin/sh
|
|
208
219
|
- -c
|
|
209
220
|
- >
|
|
210
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
|
+
}
|
|
211
247
|
|
|
212
|
-
${
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
}
|
|
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
|
+
}
|
|
221
256
|
---
|
|
222
257
|
apiVersion: v1
|
|
223
258
|
kind: Service
|
|
@@ -248,6 +283,7 @@ spec:
|
|
|
248
283
|
* @param {string} [options.traffic] - Traffic status for the deployment.
|
|
249
284
|
* @param {boolean} [options.skipFullBuild] - Whether to skip the full client bundle build; forwarded to deploymentYamlPartsFactory to generate a pull-bundle startup command.
|
|
250
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.
|
|
251
287
|
* @returns {Promise<void>} - Promise that resolves when the manifest is built.
|
|
252
288
|
* @memberof UnderpostDeploy
|
|
253
289
|
*/
|
|
@@ -276,18 +312,19 @@ spec:
|
|
|
276
312
|
for (const deploymentVersion of deploymentVersions) {
|
|
277
313
|
deploymentYamlParts += `---
|
|
278
314
|
${Underpost.deploy
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
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))}
|
|
291
328
|
`;
|
|
292
329
|
}
|
|
293
330
|
fs.writeFileSync(`./engine-private/conf/${deployId}/build/${env}/deployment.yaml`, deploymentYamlParts, 'utf8');
|
|
@@ -339,20 +376,20 @@ ${Underpost.deploy
|
|
|
339
376
|
let proxyRoutes = '';
|
|
340
377
|
const globalTimeoutPolicy =
|
|
341
378
|
(options.timeoutResponse && options.timeoutResponse !== '') ||
|
|
342
|
-
|
|
379
|
+
(options.timeoutIdle && options.timeoutIdle !== '')
|
|
343
380
|
? {
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
381
|
+
response: options.timeoutResponse,
|
|
382
|
+
idle: options.timeoutIdle,
|
|
383
|
+
}
|
|
347
384
|
: undefined;
|
|
348
385
|
const globalRetryPolicy =
|
|
349
386
|
options.retryCount ||
|
|
350
|
-
|
|
351
|
-
|
|
387
|
+
options.retryCount === 0 ||
|
|
388
|
+
(options.retryPerTryTimeout && options.retryPerTryTimeout !== '')
|
|
352
389
|
? {
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
390
|
+
count: options.retryCount,
|
|
391
|
+
perTryTimeout: options.retryPerTryTimeout,
|
|
392
|
+
}
|
|
356
393
|
: undefined;
|
|
357
394
|
if (!options.disableDeploymentProxy)
|
|
358
395
|
for (const conditionObj of pathPortAssignment) {
|
|
@@ -509,10 +546,15 @@ spec:
|
|
|
509
546
|
const hostTest = options?.hostTest
|
|
510
547
|
? options.hostTest
|
|
511
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.
|
|
512
552
|
const info = shellExec(`sudo kubectl get HTTPProxy/${hostTest} -n ${options.namespace} -o yaml`, {
|
|
513
553
|
silent: true,
|
|
514
554
|
stdout: true,
|
|
555
|
+
silentOnError: true,
|
|
515
556
|
});
|
|
557
|
+
if (!info) return null;
|
|
516
558
|
return info.match('blue') ? 'blue' : info.match('green') ? 'green' : null;
|
|
517
559
|
},
|
|
518
560
|
|
|
@@ -535,13 +577,12 @@ metadata:
|
|
|
535
577
|
namespace: ${options.namespace}
|
|
536
578
|
spec:
|
|
537
579
|
virtualhost:
|
|
538
|
-
fqdn: ${host}${
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
: `
|
|
580
|
+
fqdn: ${host}${env === 'development'
|
|
581
|
+
? ''
|
|
582
|
+
: `
|
|
542
583
|
tls:
|
|
543
584
|
secretName: ${host}`
|
|
544
|
-
|
|
585
|
+
}
|
|
545
586
|
routes:`;
|
|
546
587
|
},
|
|
547
588
|
|
|
@@ -586,6 +627,7 @@ spec:
|
|
|
586
627
|
* @param {boolean} [options.gitClean] - Whether to run git clean on volume mount paths before copying.
|
|
587
628
|
* @param {boolean} [options.skipFullBuild] - Whether to skip the full client bundle build; passed through to buildManifest/deploymentYamlPartsFactory.
|
|
588
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.
|
|
589
631
|
* @returns {Promise<void>} - Promise that resolves when the deployment process is complete.
|
|
590
632
|
* @memberof UnderpostDeploy
|
|
591
633
|
*/
|
|
@@ -627,6 +669,7 @@ spec:
|
|
|
627
669
|
kubeadm: false,
|
|
628
670
|
kind: false,
|
|
629
671
|
gitClean: false,
|
|
672
|
+
imagePullPolicy: '',
|
|
630
673
|
},
|
|
631
674
|
) {
|
|
632
675
|
const namespace = options.namespace ? options.namespace : 'default';
|
|
@@ -801,25 +844,41 @@ EOF`);
|
|
|
801
844
|
* @memberof UnderpostDeploy
|
|
802
845
|
*/
|
|
803
846
|
async checkDeploymentReadyStatus(deployId, env, traffic, ignoresNames = [], namespace = 'default') {
|
|
804
|
-
const cmd = `underpost config get container-status`;
|
|
805
847
|
const pods = Underpost.kubectl.get(`${deployId}-${env}-${traffic}`, 'pods', namespace);
|
|
806
848
|
const readyPods = [];
|
|
807
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.
|
|
808
855
|
for (const pod of pods) {
|
|
809
856
|
const { NAME } = pod;
|
|
810
857
|
if (ignoresNames && ignoresNames.find((t) => NAME.trim().toLowerCase().match(t.trim().toLowerCase()))) continue;
|
|
811
|
-
|
|
812
|
-
|
|
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`, {
|
|
813
865
|
silent: true,
|
|
814
866
|
disableLog: true,
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
},
|
|
867
|
+
stdout: true,
|
|
868
|
+
silentOnError: true,
|
|
818
869
|
});
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
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);
|
|
823
882
|
}
|
|
824
883
|
return {
|
|
825
884
|
ready: pods.length > 0 && notReadyPods.length === 0,
|
|
@@ -853,6 +912,7 @@ EOF`);
|
|
|
853
912
|
* @param {string} options.timeoutIdle - Timeout idle setting for the deployment.
|
|
854
913
|
* @param {string} options.retryCount - Retry count setting for the deployment.
|
|
855
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.
|
|
856
916
|
* @memberof UnderpostDeploy
|
|
857
917
|
*/
|
|
858
918
|
switchTraffic(
|
|
@@ -866,12 +926,14 @@ EOF`);
|
|
|
866
926
|
timeoutIdle: '',
|
|
867
927
|
retryCount: '',
|
|
868
928
|
retryPerTryTimeout: '',
|
|
929
|
+
imagePullPolicy: '',
|
|
869
930
|
},
|
|
870
931
|
) {
|
|
871
932
|
const timeoutFlags = Underpost.deploy.timeoutFlagsFactory(options);
|
|
933
|
+
const imagePullPolicyFlag = options.imagePullPolicy ? ` --image-pull-policy ${options.imagePullPolicy}` : '';
|
|
872
934
|
|
|
873
935
|
shellExec(
|
|
874
|
-
`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}`,
|
|
875
937
|
);
|
|
876
938
|
|
|
877
939
|
shellExec(`sudo kubectl apply -f ./engine-private/conf/${deployId}/build/${env}/proxy.yaml -n ${namespace}`);
|
|
@@ -940,10 +1002,10 @@ EOF`);
|
|
|
940
1002
|
shellExec(`kubectl delete pv ${pvId} --ignore-not-found`);
|
|
941
1003
|
shellExec(`kubectl apply -f - -n ${namespace} <<EOF
|
|
942
1004
|
${Underpost.deploy.persistentVolumeFactory({
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
})}
|
|
1005
|
+
hostPath: rootVolumeHostPath,
|
|
1006
|
+
pvcId,
|
|
1007
|
+
namespace,
|
|
1008
|
+
})}
|
|
947
1009
|
EOF
|
|
948
1010
|
`);
|
|
949
1011
|
},
|
|
@@ -1002,23 +1064,22 @@ ${secret ? ` readOnly: true\n` : ''}`;
|
|
|
1002
1064
|
|
|
1003
1065
|
_volumes += `
|
|
1004
1066
|
- name: ${volumeName}
|
|
1005
|
-
${
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
:
|
|
1009
|
-
? ` secret:
|
|
1067
|
+
${emptyDir
|
|
1068
|
+
? ` emptyDir: {}`
|
|
1069
|
+
: secret
|
|
1070
|
+
? ` secret:
|
|
1010
1071
|
secretName: ${secret}`
|
|
1011
|
-
|
|
1012
|
-
|
|
1072
|
+
: configMap
|
|
1073
|
+
? ` configMap:
|
|
1013
1074
|
name: ${configMap}`
|
|
1014
|
-
|
|
1015
|
-
|
|
1075
|
+
: claimName
|
|
1076
|
+
? ` persistentVolumeClaim:
|
|
1016
1077
|
claimName: ${claimName}`
|
|
1017
|
-
|
|
1078
|
+
: ` hostPath:
|
|
1018
1079
|
path: ${volumeHostPath}
|
|
1019
1080
|
type: ${volumeType}
|
|
1020
1081
|
`
|
|
1021
|
-
|
|
1082
|
+
}
|
|
1022
1083
|
|
|
1023
1084
|
`;
|
|
1024
1085
|
});
|
|
@@ -1099,7 +1160,7 @@ spec:
|
|
|
1099
1160
|
fs.writeFileSync(
|
|
1100
1161
|
`/etc/hosts`,
|
|
1101
1162
|
fs.readFileSync(`/etc/hosts`, 'utf8') +
|
|
1102
|
-
|
|
1163
|
+
`
|
|
1103
1164
|
${renderHosts}`,
|
|
1104
1165
|
'utf8',
|
|
1105
1166
|
);
|
|
@@ -1119,9 +1180,25 @@ ${renderHosts}`,
|
|
|
1119
1180
|
env === 'production' &&
|
|
1120
1181
|
options.cert === true &&
|
|
1121
1182
|
(!options.certHosts || options.certHosts.split(',').includes(host)),
|
|
1122
|
-
|
|
1123
1183
|
/**
|
|
1124
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
|
+
*
|
|
1125
1202
|
* @param {string} deployId - Deployment ID for which the ready status is being monitored.
|
|
1126
1203
|
* @param {string} env - Environment for which the ready status is being monitored.
|
|
1127
1204
|
* @param {string} targetTraffic - Target traffic status for the deployment.
|
|
@@ -1131,79 +1208,88 @@ ${renderHosts}`,
|
|
|
1131
1208
|
* @memberof UnderpostDeploy
|
|
1132
1209
|
*/
|
|
1133
1210
|
async monitorReadyRunner(deployId, env, targetTraffic, ignorePods = [], namespace = 'default') {
|
|
1134
|
-
|
|
1135
|
-
const checkStatusIterationMsDelay = 1000;
|
|
1211
|
+
const delayMs = 1000;
|
|
1136
1212
|
const maxIterations = 3000;
|
|
1137
1213
|
const deploymentId = `${deployId}-${env}-${targetTraffic}`;
|
|
1138
|
-
const
|
|
1139
|
-
|
|
1140
|
-
const
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
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 },
|
|
1152
1228
|
);
|
|
1153
|
-
|
|
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;
|
|
1154
1234
|
}
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
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);
|
|
1169
1252
|
}
|
|
1170
1253
|
|
|
1171
|
-
|
|
1172
|
-
let indexOf = -1;
|
|
1173
|
-
for (const pod of result.notReadyPods) {
|
|
1174
|
-
indexOf++;
|
|
1175
|
-
const { NAME, out } = pod;
|
|
1254
|
+
const allPodsK8sReady = allPods.length > 0 && result.notReadyPods.length === 0;
|
|
1176
1255
|
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
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`;
|
|
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)`;
|
|
1188
1263
|
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
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}`);
|
|
1196
1286
|
}
|
|
1197
|
-
await timer(checkStatusIterationMsDelay);
|
|
1198
|
-
checkStatusIteration++;
|
|
1199
|
-
logger.info(
|
|
1200
|
-
`${iteratorTag} | Deployment in progress... | Delay number monitor iterations: ${checkStatusIteration}`,
|
|
1201
|
-
);
|
|
1202
1287
|
}
|
|
1203
|
-
|
|
1204
|
-
|
|
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`,
|
|
1205
1292
|
);
|
|
1206
|
-
return result;
|
|
1207
1293
|
},
|
|
1208
1294
|
|
|
1209
1295
|
/**
|
|
@@ -1363,6 +1449,34 @@ ${renderHosts}`,
|
|
|
1363
1449
|
return undefined;
|
|
1364
1450
|
},
|
|
1365
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
|
+
|
|
1366
1480
|
/**
|
|
1367
1481
|
* Generates timeout flags string for deployment commands.
|
|
1368
1482
|
* @param {object} options - Options containing timeout settings.
|
package/src/cli/fs.js
CHANGED
|
@@ -154,7 +154,9 @@ class UnderpostFileStorage {
|
|
|
154
154
|
// For bundle pulls into ./build the git step is unwanted and would error on a non-repo path.
|
|
155
155
|
if (options.git === true) {
|
|
156
156
|
Underpost.repo.initLocalRepo({ path });
|
|
157
|
-
shellExec(`cd ${path} && git add . && git commit -m "Base pull state"
|
|
157
|
+
shellExec(`cd ${path} && git add . && git commit -m "Base pull state"`, {
|
|
158
|
+
silentOnError: true
|
|
159
|
+
});
|
|
158
160
|
}
|
|
159
161
|
} else {
|
|
160
162
|
const files =
|