cyberia 3.2.9 → 3.2.22

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.
Files changed (184) hide show
  1. package/.github/workflows/engine-cyberia.cd.yml +7 -0
  2. package/.github/workflows/engine-cyberia.ci.yml +14 -2
  3. package/.github/workflows/ghpkg.ci.yml +1 -0
  4. package/.github/workflows/npmpkg.ci.yml +10 -5
  5. package/.github/workflows/pwa-microservices-template-test.ci.yml +1 -1
  6. package/.github/workflows/release.cd.yml +1 -0
  7. package/.vscode/extensions.json +9 -9
  8. package/.vscode/settings.json +20 -4
  9. package/CHANGELOG.md +363 -1
  10. package/CLI-HELP.md +975 -1061
  11. package/README.md +190 -348
  12. package/bin/build.js +102 -125
  13. package/bin/build.template.js +33 -0
  14. package/bin/cyberia.js +238 -56
  15. package/bin/deploy.js +16 -3
  16. package/bin/index.js +238 -56
  17. package/bump.config.js +26 -0
  18. package/conf.js +131 -24
  19. package/deployment.yaml +76 -2
  20. package/hardhat/package-lock.json +113 -144
  21. package/hardhat/package.json +4 -3
  22. package/manifests/cronjobs/dd-cron/dd-cron-backup.yaml +2 -2
  23. package/manifests/cronjobs/dd-cron/dd-cron-dns.yaml +1 -1
  24. package/manifests/deployment/dd-cyberia-development/deployment.yaml +76 -2
  25. package/manifests/deployment/dd-default-development/deployment.yaml +2 -2
  26. package/manifests/kind-config-dev.yaml +8 -0
  27. package/manifests/lxd/lxd-admin-profile.yaml +12 -3
  28. package/manifests/mongodb/pv-pvc.yaml +44 -8
  29. package/manifests/mongodb/statefulset.yaml +55 -68
  30. package/manifests/mongodb-4.4/headless-service.yaml +10 -0
  31. package/manifests/mongodb-4.4/kustomization.yaml +3 -1
  32. package/manifests/mongodb-4.4/mongodb-nodeport.yaml +17 -0
  33. package/manifests/mongodb-4.4/pv-pvc.yaml +10 -14
  34. package/manifests/mongodb-4.4/statefulset.yaml +79 -0
  35. package/manifests/mongodb-4.4/storage-class.yaml +9 -0
  36. package/manifests/valkey/statefulset.yaml +1 -1
  37. package/manifests/valkey/valkey-nodeport.yaml +17 -0
  38. package/package.json +31 -19
  39. package/scripts/ipxe-setup.sh +52 -49
  40. package/scripts/k3s-node-setup.sh +81 -46
  41. package/scripts/link-local-underpost-cli.sh +6 -0
  42. package/scripts/lxd-vm-setup.sh +193 -8
  43. package/scripts/maas-nat-firewalld.sh +145 -0
  44. package/scripts/test-monitor.sh +250 -0
  45. package/src/api/atlas-sprite-sheet/atlas-sprite-sheet.router.js +38 -33
  46. package/src/api/atlas-sprite-sheet/atlas-sprite-sheet.service.js +16 -16
  47. package/src/api/core/core.router.js +19 -14
  48. package/src/api/core/core.service.js +5 -5
  49. package/src/api/crypto/crypto.router.js +18 -12
  50. package/src/api/crypto/crypto.service.js +3 -3
  51. package/src/api/cyberia-action/cyberia-action.model.js +1 -1
  52. package/src/api/cyberia-action/cyberia-action.router.js +22 -18
  53. package/src/api/cyberia-action/cyberia-action.service.js +5 -5
  54. package/src/api/cyberia-client-hints/cyberia-client-hints.controller.js +74 -0
  55. package/src/api/cyberia-client-hints/cyberia-client-hints.model.js +99 -0
  56. package/src/api/cyberia-client-hints/cyberia-client-hints.router.js +98 -0
  57. package/src/api/cyberia-client-hints/cyberia-client-hints.service.js +152 -0
  58. package/src/api/cyberia-dialogue/cyberia-dialogue.router.js +25 -20
  59. package/src/api/cyberia-dialogue/cyberia-dialogue.service.js +6 -6
  60. package/src/api/cyberia-entity/cyberia-entity.router.js +22 -18
  61. package/src/api/cyberia-entity/cyberia-entity.service.js +5 -5
  62. package/src/api/cyberia-instance/cyberia-fallback-world.js +79 -4
  63. package/src/api/cyberia-instance/cyberia-instance.router.js +57 -52
  64. package/src/api/cyberia-instance/cyberia-instance.service.js +10 -10
  65. package/src/api/cyberia-instance/cyberia-world-generator.js +3 -3
  66. package/src/api/cyberia-instance-conf/cyberia-instance-conf.model.js +14 -48
  67. package/src/api/cyberia-instance-conf/cyberia-instance-conf.router.js +22 -18
  68. package/src/api/cyberia-instance-conf/cyberia-instance-conf.service.js +5 -5
  69. package/src/api/cyberia-map/cyberia-map.router.js +35 -30
  70. package/src/api/cyberia-map/cyberia-map.service.js +7 -7
  71. package/src/api/cyberia-quest/cyberia-quest.model.js +1 -1
  72. package/src/api/cyberia-quest/cyberia-quest.router.js +22 -18
  73. package/src/api/cyberia-quest/cyberia-quest.service.js +5 -5
  74. package/src/api/cyberia-quest-progress/cyberia-quest-progress.router.js +22 -18
  75. package/src/api/cyberia-quest-progress/cyberia-quest-progress.service.js +5 -5
  76. package/src/api/cyberia-server-defaults/cyberia-server-defaults.js +458 -0
  77. package/src/api/default/default.router.js +22 -18
  78. package/src/api/default/default.service.js +5 -5
  79. package/src/api/document/document.router.js +28 -23
  80. package/src/api/document/document.service.js +100 -23
  81. package/src/api/file/file.router.js +19 -13
  82. package/src/api/file/file.service.js +9 -7
  83. package/src/api/instance/instance.router.js +29 -24
  84. package/src/api/instance/instance.service.js +6 -6
  85. package/src/api/ipfs/ipfs.router.js +21 -16
  86. package/src/api/ipfs/ipfs.service.js +8 -8
  87. package/src/api/object-layer/object-layer.router.js +512 -507
  88. package/src/api/object-layer/object-layer.service.js +17 -14
  89. package/src/api/object-layer-render-frames/object-layer-render-frames.router.js +22 -18
  90. package/src/api/object-layer-render-frames/object-layer-render-frames.service.js +5 -5
  91. package/src/api/test/test.router.js +17 -12
  92. package/src/api/types.js +24 -0
  93. package/src/api/user/guest.service.js +5 -4
  94. package/src/api/user/user.router.js +297 -288
  95. package/src/api/user/user.service.js +100 -35
  96. package/src/cli/baremetal.js +132 -101
  97. package/src/cli/cluster.js +700 -232
  98. package/src/cli/db.js +59 -60
  99. package/src/cli/deploy.js +291 -294
  100. package/src/cli/env.js +1 -4
  101. package/src/cli/fs.js +13 -3
  102. package/src/cli/image.js +58 -4
  103. package/src/cli/index.js +127 -15
  104. package/src/cli/ipfs.js +4 -6
  105. package/src/cli/kubectl.js +4 -1
  106. package/src/cli/lxd.js +1099 -223
  107. package/src/cli/monitor.js +396 -9
  108. package/src/cli/release.js +355 -146
  109. package/src/cli/repository.js +169 -30
  110. package/src/cli/run.js +347 -117
  111. package/src/cli/secrets.js +11 -2
  112. package/src/cli/test.js +9 -3
  113. package/src/client/Default.index.js +9 -3
  114. package/src/client/components/core/Auth.js +5 -0
  115. package/src/client/components/core/ClientEvents.js +76 -0
  116. package/src/client/components/core/EventBus.js +4 -0
  117. package/src/client/components/core/Modal.js +82 -41
  118. package/src/client/components/core/PanelForm.js +14 -10
  119. package/src/client/components/core/Worker.js +162 -363
  120. package/src/client/components/cyberia/MapEngineCyberia.js +1 -1
  121. package/src/client/components/cyberia/SharedDefaultsCyberia.js +330 -0
  122. package/src/client/public/cyberia-docs/ACTION-SYSTEM.md +55 -1
  123. package/src/client/public/cyberia-docs/ARCHITECTURE.md +223 -361
  124. package/src/client/public/cyberia-docs/CYBERIA-CLI.md +114 -327
  125. package/src/client/public/cyberia-docs/CYBERIA-CLIENT.md +200 -222
  126. package/src/client/public/cyberia-docs/CYBERIA-SERVER.md +212 -185
  127. package/src/client/public/cyberia-docs/CYBERIA.md +259 -0
  128. package/src/client/public/cyberia-docs/OFF-CHAIN-ECONOMY.md +2 -2
  129. package/src/client/public/cyberia-docs/QUEST-SYSTEM.md +23 -1
  130. package/src/client/public/cyberia-docs/ROADMAP.md +1 -1
  131. package/src/client/public/cyberia-docs/UNDERPOST-PLATFORM.md +106 -0
  132. package/src/client/public/cyberia-docs/WHITE-PAPER.md +1 -1
  133. package/src/client/services/cyberia-client-hints/cyberia-client-hints.service.js +99 -0
  134. package/src/client/ssr/views/CyberiaServerMetrics.js +982 -0
  135. package/src/client/sw/core.sw.js +174 -112
  136. package/src/db/DataBaseProvider.js +115 -15
  137. package/src/db/mariadb/MariaDB.js +2 -1
  138. package/src/db/mongo/MongoBootstrap.js +657 -0
  139. package/src/db/mongo/MongooseDB.js +130 -21
  140. package/src/grpc/cyberia/grpc-server.js +25 -57
  141. package/src/index.js +1 -1
  142. package/src/runtime/cyberia-client/Dockerfile +10 -7
  143. package/src/runtime/cyberia-client/Dockerfile.dev +67 -0
  144. package/src/runtime/cyberia-server/Dockerfile +11 -6
  145. package/src/runtime/cyberia-server/Dockerfile.dev +47 -0
  146. package/src/runtime/express/Express.js +2 -2
  147. package/src/runtime/wp/Dockerfile +3 -3
  148. package/src/runtime/wp/Wp.js +8 -5
  149. package/src/server/auth.js +2 -2
  150. package/src/server/catalog-underpost.js +61 -0
  151. package/src/server/catalog.js +77 -0
  152. package/src/server/client-build-docs.js +1 -1
  153. package/src/server/client-build.js +94 -129
  154. package/src/server/conf.js +496 -135
  155. package/src/server/ipfs-client.js +5 -3
  156. package/src/server/process.js +180 -19
  157. package/src/server/proxy.js +9 -2
  158. package/src/server/runtime-status.js +235 -0
  159. package/src/server/runtime.js +1 -1
  160. package/src/server/start.js +44 -11
  161. package/src/server/valkey.js +2 -0
  162. package/src/ws/IoInterface.js +16 -16
  163. package/src/ws/core/channels/core.ws.chat.js +11 -11
  164. package/src/ws/core/channels/core.ws.mailer.js +29 -29
  165. package/src/ws/core/channels/core.ws.stream.js +19 -19
  166. package/src/ws/core/core.ws.connection.js +8 -8
  167. package/src/ws/core/core.ws.server.js +6 -5
  168. package/src/ws/default/channels/default.ws.main.js +10 -10
  169. package/src/ws/default/default.ws.connection.js +4 -4
  170. package/src/ws/default/default.ws.server.js +4 -3
  171. package/test/deploy-monitor.test.js +251 -0
  172. package/bin/file.js +0 -202
  173. package/bin/vs.js +0 -74
  174. package/bin/zed.js +0 -84
  175. package/manifests/deployment/dd-test-development/deployment.yaml +0 -254
  176. package/manifests/deployment/dd-test-development/proxy.yaml +0 -102
  177. package/src/api/cyberia-instance-conf/cyberia-instance-conf.defaults.js +0 -574
  178. package/src/client/components/cyberia-portal/CommonCyberiaPortal.js +0 -467
  179. package/src/client/ssr/email/DefaultRecoverEmail.js +0 -21
  180. package/src/client/ssr/email/DefaultVerifyEmail.js +0 -17
  181. package/src/client/ssr/pages/CyberiaServerMetrics.js +0 -461
  182. /package/src/client/ssr/{offline → views}/Maintenance.js +0 -0
  183. /package/src/client/ssr/{offline → views}/NoNetworkConnection.js +0 -0
  184. /package/src/client/ssr/{pages → views}/Test.js +0 -0
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.
@@ -127,6 +185,12 @@ class UnderpostDeploy {
127
185
  * @param {Array<string>} cmd - Command to run in the deployment container.
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.
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.
130
194
  * @returns {string} - YAML deployment configuration for the specified deployment.
131
195
  * @memberof UnderpostDeploy
132
196
  */
@@ -142,6 +206,22 @@ class UnderpostDeploy {
142
206
  cmd,
143
207
  skipFullBuild,
144
208
  pullBundle,
209
+ imagePullPolicy,
210
+ // K8S lifecycle + probe wiring. Pass-through structures shaped like the
211
+ // upstream Kubernetes API, spliced verbatim into the container spec.
212
+ // lifecycle: { postStart: { exec: { command: [...] } }, preStop: { exec: { command: [...] } } }
213
+ // readinessProbe: { tcpSocket: { port: 8081 }, ... }
214
+ // livenessProbe: { tcpSocket: { port: 8081 }, ... }
215
+ // containerPort: integer; rendered as ports[0].containerPort. Optional.
216
+ lifecycle,
217
+ readinessProbe,
218
+ livenessProbe,
219
+ startupProbe,
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,
145
225
  }) {
146
226
  if (!cmd)
147
227
  cmd =
@@ -188,26 +268,76 @@ spec:
188
268
  containers:
189
269
  - name: ${deployId}-${env}-${suffix}
190
270
  image: ${containerImage}
191
- imagePullPolicy: ${containerImage.startsWith('localhost/') ? 'Never' : 'IfNotPresent'}
271
+ imagePullPolicy: ${imagePullPolicy ? imagePullPolicy : containerImage.startsWith('localhost/') ? 'Never' : 'IfNotPresent'}
192
272
  envFrom:
193
273
  - secretRef:
194
274
  name: underpost-config
195
275
  ${
196
- resources
197
- ? ` resources:
276
+ internalStatusPort
277
+ ? ` env:
278
+ - name: UNDERPOST_INTERNAL_PORT
279
+ value: "${internalStatusPort}"
280
+ `
281
+ : ''
282
+ }${
283
+ containerPort
284
+ ? ` ports:
285
+ - containerPort: ${containerPort}
286
+ `
287
+ : ''
288
+ }${
289
+ resources
290
+ ? ` resources:
198
291
  requests:
199
292
  memory: "${resources.requests.memory}"
200
293
  cpu: "${resources.requests.cpu}"
201
294
  limits:
202
295
  memory: "${resources.limits.memory}"
203
296
  cpu: "${resources.limits.cpu}"`
204
- : ''
205
- }
297
+ : ''
298
+ }
206
299
  command:
207
300
  - /bin/sh
208
301
  - -c
209
302
  - >
210
303
  ${cmd.join(' &&\n ')}
304
+ ${
305
+ readinessProbe
306
+ ? ` readinessProbe:
307
+ ${JSON.stringify(readinessProbe, null, 2)
308
+ .split('\n')
309
+ .map((l) => ' ' + l)
310
+ .join('\n')}
311
+ `
312
+ : ''
313
+ }${
314
+ livenessProbe
315
+ ? ` livenessProbe:
316
+ ${JSON.stringify(livenessProbe, null, 2)
317
+ .split('\n')
318
+ .map((l) => ' ' + l)
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')}
329
+ `
330
+ : ''
331
+ }${
332
+ lifecycle
333
+ ? ` lifecycle:
334
+ ${JSON.stringify(lifecycle, null, 2)
335
+ .split('\n')
336
+ .map((l) => ' ' + l)
337
+ .join('\n')}
338
+ `
339
+ : ''
340
+ }
211
341
 
212
342
  ${
213
343
  volumes.length > 0
@@ -237,17 +367,22 @@ spec:
237
367
  * @param {object} options - Options for the manifest build process.
238
368
  * @param {string} options.replicas - Number of replicas for each deployment.
239
369
  * @param {string} options.image - Docker image for the deployment.
240
- * @param {string} options.namespace - Kubernetes namespace for the deployment.
370
+ * @param {string} options.namespace - Kubernetes namespace for the deployment (defaults to "default").
241
371
  * @param {string} [options.versions] - Comma-separated list of versions to deploy.
242
372
  * @param {string} [options.cmd] - Custom initialization command for deploymentYamlPartsFactory (comma-separated commands).
243
- * @param {string} [options.timeoutResponse] - Timeout response setting for the deployment.
244
- * @param {string} [options.timeoutIdle] - Timeout idle setting for the deployment.
245
- * @param {string} [options.retryCount] - Retry count setting for the deployment.
246
- * @param {string} [options.retryPerTryTimeout] - Retry per-try timeout setting for the deployment.
247
- * @param {boolean} [options.disableDeploymentProxy] - Whether to disable deployment proxy.
248
- * @param {string} [options.traffic] - Traffic status for the deployment.
249
- * @param {boolean} [options.skipFullBuild] - Whether to skip the full client bundle build; forwarded to deploymentYamlPartsFactory to generate a pull-bundle startup command.
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.
250
382
  * @param {boolean} [options.pullBundle] - Whether to pull the pre-built client bundle from Cloudinary; forwarded to deploymentYamlPartsFactory. Use together with skipFullBuild.
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).
251
386
  * @returns {Promise<void>} - Promise that resolves when the manifest is built.
252
387
  * @memberof UnderpostDeploy
253
388
  */
@@ -272,6 +407,17 @@ spec:
272
407
 
273
408
  logger.info('port range', { deployId, fromPort, toPort });
274
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
+
275
421
  let deploymentYamlParts = '';
276
422
  for (const deploymentVersion of deploymentVersions) {
277
423
  deploymentYamlParts += `---
@@ -286,6 +432,11 @@ ${Underpost.deploy
286
432
  cmd: options.cmd ? options.cmd.split(',').map((c) => c.trim()) : undefined,
287
433
  skipFullBuild: options.skipFullBuild,
288
434
  pullBundle: options.pullBundle,
435
+ imagePullPolicy: options.imagePullPolicy,
436
+ internalStatusPort: options.disableRuntimeProbes ? undefined : internalPort,
437
+ readinessProbe: probes.readinessProbe,
438
+ livenessProbe: probes.livenessProbe,
439
+ startupProbe: probes.startupProbe,
289
440
  })
290
441
  .replace('{{ports}}', buildKindPorts(fromPort, toPort))}
291
442
  `;
@@ -328,7 +479,7 @@ ${Underpost.deploy
328
479
  : [];
329
480
 
330
481
  for (const host of Object.keys(confServer)) {
331
- if (env === 'production')
482
+ if (env === 'production' && options.cert === true)
332
483
  secretYaml += Underpost.deploy.buildCertManagerCertificate({ host, namespace: options.namespace });
333
484
 
334
485
  const pathPortAssignment = pathPortAssignmentData[host];
@@ -509,10 +660,15 @@ spec:
509
660
  const hostTest = options?.hostTest
510
661
  ? options.hostTest
511
662
  : Object.keys(loadConfServerJson(`./engine-private/conf/${deployId}/conf.server.json`))[0];
663
+ // Missing HTTPProxy is the canonical "no traffic colour set yet" state
664
+ // for blue/green rollouts. silentOnError swallows kubectl's NotFound
665
+ // exit so the function can return null cleanly.
512
666
  const info = shellExec(`sudo kubectl get HTTPProxy/${hostTest} -n ${options.namespace} -o yaml`, {
513
667
  silent: true,
514
668
  stdout: true,
669
+ silentOnError: true,
515
670
  });
671
+ if (!info) return null;
516
672
  return info.match('blue') ? 'blue' : info.match('green') ? 'green' : null;
517
673
  },
518
674
 
@@ -526,6 +682,7 @@ spec:
526
682
  * @memberof UnderpostDeploy
527
683
  */
528
684
  baseProxyYamlFactory({ host, env, options }) {
685
+ const includeTls = env !== 'development' || options.selfSigned === true;
529
686
  return `
530
687
  ---
531
688
  apiVersion: projectcontour.io/v1
@@ -536,11 +693,11 @@ metadata:
536
693
  spec:
537
694
  virtualhost:
538
695
  fqdn: ${host}${
539
- env === 'development'
540
- ? ''
541
- : `
696
+ includeTls
697
+ ? `
542
698
  tls:
543
699
  secretName: ${host}`
700
+ : ''
544
701
  }
545
702
  routes:`;
546
703
  },
@@ -556,36 +713,41 @@ spec:
556
713
  * @param {boolean} options.buildManifest - Whether to build the deployment manifest.
557
714
  * @param {boolean} options.infoUtil - Whether to display utility information.
558
715
  * @param {boolean} options.expose - Whether to expose the deployment.
559
- * @param {boolean} options.cert - Whether to create certificates for the deployment.
560
- * @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.
561
719
  * @param {string} options.versions - Comma-separated list of versions to deploy.
562
720
  * @param {string} options.image - Docker image for the deployment.
563
721
  * @param {string} options.traffic - Traffic status for the deployment.
564
722
  * @param {string} options.replicas - Number of replicas for the deployment.
565
723
  * @param {string} options.node - Node name for resource allocation.
566
- * @param {boolean} options.restoreHosts - Whether to restore the hosts file.
567
724
  * @param {boolean} options.disableUpdateDeployment - Whether to disable deployment updates.
568
725
  * @param {boolean} options.disableUpdateProxy - Whether to disable proxy updates.
569
726
  * @param {boolean} options.disableDeploymentProxy - Whether to disable deployment proxy.
570
727
  * @param {boolean} options.disableUpdateVolume - Whether to disable volume updates.
571
728
  * @param {boolean} options.status - Whether to display deployment status.
572
- * @param {boolean} options.etcHosts - Whether to display the /etc/hosts file.
573
729
  * @param {boolean} options.disableUpdateUnderpostConfig - Whether to disable Underpost config updates.
574
- * @param {string} [options.namespace] - Kubernetes namespace for the deployment.
575
- * @param {string} [options.timeoutResponse] - Timeout response setting for the deployment.
576
- * @param {string} [options.timeoutIdle] - Timeout idle setting for the deployment.
577
- * @param {string} [options.retryCount] - Retry count setting for the deployment.
578
- * @param {string} [options.retryPerTryTimeout] - Retry per-try timeout setting for the deployment.
579
- * @param {string} [options.kindType] - Type of Kubernetes resource to retrieve information for.
580
- * @param {number} [options.port] - Port number for exposing the deployment.
581
- * @param {string} [options.cmd] - Custom initialization command for deploymentYamlPartsFactory (comma-separated commands).
582
- * @param {number} [options.exposePort] - Local:remote port override when --expose is active (overrides auto-detected service port).
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.
583
742
  * @param {boolean} [options.k3s] - Whether to use k3s cluster context.
584
743
  * @param {boolean} [options.kubeadm] - Whether to use kubeadm cluster context.
585
744
  * @param {boolean} [options.kind] - Whether to use kind cluster context.
586
745
  * @param {boolean} [options.gitClean] - Whether to run git clean on volume mount paths before copying.
587
746
  * @param {boolean} [options.skipFullBuild] - Whether to skip the full client bundle build; passed through to buildManifest/deploymentYamlPartsFactory.
588
747
  * @param {boolean} [options.pullBundle] - Whether to pull the pre-built client bundle from Cloudinary; passed through to buildManifest/deploymentYamlPartsFactory. Use together with skipFullBuild.
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.
589
751
  * @returns {Promise<void>} - Promise that resolves when the deployment process is complete.
590
752
  * @memberof UnderpostDeploy
591
753
  */
@@ -606,13 +768,11 @@ spec:
606
768
  traffic: '',
607
769
  replicas: '',
608
770
  node: '',
609
- restoreHosts: false,
610
771
  disableUpdateDeployment: false,
611
772
  disableUpdateProxy: false,
612
773
  disableDeploymentProxy: false,
613
774
  disableUpdateVolume: false,
614
775
  status: false,
615
- etcHosts: false,
616
776
  disableUpdateUnderpostConfig: false,
617
777
  namespace: '',
618
778
  timeoutResponse: '',
@@ -622,11 +782,16 @@ spec:
622
782
  kindType: '',
623
783
  port: 0,
624
784
  exposePort: 0,
785
+ exposeLocalPort: 0,
786
+ localProxy: false,
787
+ tls: false,
788
+ selfSigned: false,
625
789
  cmd: '',
626
790
  k3s: false,
627
791
  kubeadm: false,
628
792
  kind: false,
629
793
  gitClean: false,
794
+ imagePullPolicy: '',
630
795
  },
631
796
  ) {
632
797
  const namespace = options.namespace ? options.namespace : 'default';
@@ -695,14 +860,6 @@ EOF`);
695
860
  return;
696
861
  }
697
862
  if (!options.disableUpdateUnderpostConfig) Underpost.deploy.configMap(env);
698
- let renderHosts = '';
699
- let etcHosts = [];
700
- if (options.restoreHosts === true) {
701
- const factoryResult = Underpost.deploy.etcHostFactory(etcHosts);
702
- renderHosts = factoryResult.renderHosts;
703
- logger.info(renderHosts);
704
- return;
705
- }
706
863
 
707
864
  for (const _deployId of deployList.split(',')) {
708
865
  const deployId = _deployId.trim();
@@ -710,20 +867,54 @@ EOF`);
710
867
  if (options.expose === true) {
711
868
  const kindType = options.kindType ? options.kindType : 'svc';
712
869
  const svc = Underpost.kubectl.get(deployId, kindType)[0];
713
- const port = options.exposePort
714
- ? parseInt(options.exposePort)
715
- : options.port
716
- ? parseInt(options.port)
717
- : kindType !== 'svc'
718
- ? 80
719
- : parseInt(svc[`PORT(S)`].split('/TCP')[0]);
720
- logger.info(deployId, {
721
- svc,
722
- port,
723
- });
724
- shellExec(`sudo kubectl port-forward -n ${namespace} ${kindType}/${svc.NAME} ${port}:${port}`, {
725
- async: true,
726
- });
870
+ if (!svc) {
871
+ logger.error(`No ${kindType} found matching '${deployId}', skipping expose`);
872
+ continue;
873
+ }
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
+ }
727
918
  continue;
728
919
  }
729
920
 
@@ -759,7 +950,6 @@ EOF`);
759
950
  if (Underpost.deploy.isValidTLSContext({ host, env, options }))
760
951
  shellExec(`sudo kubectl delete Certificate ${host} -n ${namespace} --ignore-not-found`);
761
952
  }
762
- if (!options.remove) etcHosts.push(host);
763
953
  }
764
954
 
765
955
  const manifestsPath =
@@ -776,56 +966,13 @@ EOF`);
776
966
  if (!options.disableUpdateProxy)
777
967
  shellExec(`sudo kubectl apply -f ./${manifestsPath}/proxy.yaml -n ${namespace}`);
778
968
 
779
- if (Underpost.deploy.isValidTLSContext({ host: Object.keys(confServer)[0], env, options }))
969
+ if (
970
+ Underpost.deploy.isValidTLSContext({ host: Object.keys(confServer)[0], env, options }) &&
971
+ !options.selfSigned
972
+ )
780
973
  shellExec(`sudo kubectl apply -f ./${manifestsPath}/secret.yaml -n ${namespace}`);
781
974
  }
782
975
  }
783
- if (options.etcHosts === true) {
784
- const factoryResult = Underpost.deploy.etcHostFactory(etcHosts);
785
- renderHosts = factoryResult.renderHosts;
786
- }
787
- if (renderHosts)
788
- logger.info(
789
- `
790
- ` + renderHosts,
791
- );
792
- },
793
- /**
794
- * Checks the status of a deployment.
795
- * @param {string} deployId - Deployment ID for which the status is being checked.
796
- * @param {string} env - Environment for which the status is being checked.
797
- * @param {string} traffic - Current traffic status for the deployment.
798
- * @param {Array<string>} ignoresNames - List of pod names to ignore.
799
- * @param {string} [namespace='default'] - Kubernetes namespace for the deployment.
800
- * @returns {object} - Object containing the status of the deployment.
801
- * @memberof UnderpostDeploy
802
- */
803
- async checkDeploymentReadyStatus(deployId, env, traffic, ignoresNames = [], namespace = 'default') {
804
- const cmd = `underpost config get container-status`;
805
- const pods = Underpost.kubectl.get(`${deployId}-${env}-${traffic}`, 'pods', namespace);
806
- const readyPods = [];
807
- const notReadyPods = [];
808
- for (const pod of pods) {
809
- const { NAME } = pod;
810
- if (ignoresNames && ignoresNames.find((t) => NAME.trim().toLowerCase().match(t.trim().toLowerCase()))) continue;
811
- const out = await new Promise((resolve) => {
812
- shellExec(`sudo kubectl exec -i ${NAME} -n ${namespace} -- sh -c "${cmd}"`, {
813
- silent: true,
814
- disableLog: true,
815
- callback: function (code, stdout, stderr) {
816
- return resolve(JSON.stringify({ code, stdout, stderr }));
817
- },
818
- });
819
- });
820
- pod.out = out;
821
- const ready = out.match(`${deployId}-${env}-running-deployment`);
822
- ready ? readyPods.push(pod) : notReadyPods.push(pod);
823
- }
824
- return {
825
- ready: pods.length > 0 && notReadyPods.length === 0,
826
- notReadyPods,
827
- readyPods,
828
- };
829
976
  },
830
977
  /**
831
978
  * Creates a Kubernetes Secret for a deployment (replaces configMap for secret data).
@@ -853,6 +1000,7 @@ EOF`);
853
1000
  * @param {string} options.timeoutIdle - Timeout idle setting for the deployment.
854
1001
  * @param {string} options.retryCount - Retry count setting for the deployment.
855
1002
  * @param {string} options.retryPerTryTimeout - Retry per-try timeout setting for the deployment.
1003
+ * @param {string} [options.imagePullPolicy] - Container imagePullPolicy override; forwarded to the manifest rebuild triggered here.
856
1004
  * @memberof UnderpostDeploy
857
1005
  */
858
1006
  switchTraffic(
@@ -866,12 +1014,14 @@ EOF`);
866
1014
  timeoutIdle: '',
867
1015
  retryCount: '',
868
1016
  retryPerTryTimeout: '',
1017
+ imagePullPolicy: '',
869
1018
  },
870
1019
  ) {
871
1020
  const timeoutFlags = Underpost.deploy.timeoutFlagsFactory(options);
1021
+ const imagePullPolicyFlag = options.imagePullPolicy ? ` --image-pull-policy ${options.imagePullPolicy}` : '';
872
1022
 
873
1023
  shellExec(
874
- `node bin deploy --info-router --build-manifest --traffic ${targetTraffic} --replicas ${replicas} --namespace ${namespace}${timeoutFlags} ${deployId} ${env}`,
1024
+ `node bin deploy --info-router --build-manifest --traffic ${targetTraffic} --replicas ${replicas} --namespace ${namespace}${timeoutFlags}${imagePullPolicyFlag} ${deployId} ${env}`,
875
1025
  );
876
1026
 
877
1027
  shellExec(`sudo kubectl apply -f ./engine-private/conf/${deployId}/build/${env}/proxy.yaml -n ${namespace}`);
@@ -922,15 +1072,12 @@ EOF`);
922
1072
  if (options.gitClean && volume.volumeMountPath) {
923
1073
  Underpost.repo.clean({ paths: [volume.volumeMountPath] });
924
1074
  }
925
- if (options.nodeName) {
926
- if (!fs.existsSync(rootVolumeHostPath)) fs.mkdirSync(rootVolumeHostPath, { recursive: true });
927
- fs.copySync(volume.volumeMountPath, rootVolumeHostPath);
928
- } else if (clusterContext === 'kind') {
929
- shellExec(`docker exec -i kind-worker bash -c "mkdir -p ${rootVolumeHostPath}"`);
930
- // shellExec(`docker cp ${volume.volumeMountPath} kind-worker:${rootVolumeHostPath}`);
931
- shellExec(`tar -C ${volume.volumeMountPath} -c . | docker cp - kind-worker:${rootVolumeHostPath}`);
1075
+ if (clusterContext === 'kind') {
1076
+ const kindNode = options.nodeName || 'kind-worker';
1077
+ shellExec(`docker exec -i ${kindNode} bash -c "mkdir -p ${rootVolumeHostPath}"`);
1078
+ shellExec(`tar -C ${volume.volumeMountPath} -c . | docker cp - ${kindNode}:${rootVolumeHostPath}`);
932
1079
  shellExec(
933
- `docker exec -i kind-worker bash -c "chown -R 1000:1000 ${rootVolumeHostPath}; chmod -R 755 ${rootVolumeHostPath}"`,
1080
+ `docker exec -i ${kindNode} bash -c "chown -R 1000:1000 ${rootVolumeHostPath}; chmod -R 755 ${rootVolumeHostPath}"`,
934
1081
  );
935
1082
  } else {
936
1083
  if (!fs.existsSync(rootVolumeHostPath)) fs.mkdirSync(rootVolumeHostPath, { recursive: true });
@@ -1070,42 +1217,6 @@ spec:
1070
1217
  storage: 5Gi`;
1071
1218
  },
1072
1219
 
1073
- /**
1074
- * Creates a hosts file for a deployment.
1075
- * @param {Array<string>} hosts - List of hosts to be added to the hosts file.
1076
- * @param {object} options - Options for the hosts file creation.
1077
- * @param {boolean} options.append - Whether to append to the existing hosts file.
1078
- * @returns {object} - Object containing the rendered hosts file.
1079
- * @memberof UnderpostDeploy
1080
- */
1081
- etcHostFactory(hosts = [], options = { append: false }) {
1082
- hosts = hosts.map((host) => {
1083
- try {
1084
- if (!host.startsWith('http')) host = `http://${host}`;
1085
- const hostname = new URL(host).hostname;
1086
- logger.info('Hostname extract valid', { host, hostname });
1087
- return hostname;
1088
- } catch (e) {
1089
- logger.warn('No hostname extract valid', host);
1090
- return host;
1091
- }
1092
- });
1093
- const renderHosts = `127.0.0.1 ${hosts.join(
1094
- ' ',
1095
- )} localhost localhost.localdomain localhost4 localhost4.localdomain4
1096
- ::1 localhost localhost.localdomain localhost6 localhost6.localdomain6`;
1097
-
1098
- if (options && options.append && fs.existsSync(`/etc/hosts`)) {
1099
- fs.writeFileSync(
1100
- `/etc/hosts`,
1101
- fs.readFileSync(`/etc/hosts`, 'utf8') +
1102
- `
1103
- ${renderHosts}`,
1104
- 'utf8',
1105
- );
1106
- } else fs.writeFileSync(`/etc/hosts`, renderHosts, 'utf8');
1107
- return { renderHosts };
1108
- },
1109
1220
  /**
1110
1221
  * Checks if a TLS context is valid.
1111
1222
  * @param {object} options - Options for the check.
@@ -1116,152 +1227,10 @@ ${renderHosts}`,
1116
1227
  * @memberof UnderpostDeploy
1117
1228
  */
1118
1229
  isValidTLSContext: ({ host, env, options }) =>
1119
- env === 'production' &&
1120
- options.cert === true &&
1121
- (!options.certHosts || options.certHosts.split(',').includes(host)),
1122
-
1123
- /**
1124
- * Monitors the ready status of a deployment.
1125
- * @param {string} deployId - Deployment ID for which the ready status is being monitored.
1126
- * @param {string} env - Environment for which the ready status is being monitored.
1127
- * @param {string} targetTraffic - Target traffic status for the deployment.
1128
- * @param {Array<string>} ignorePods - List of pod names to ignore.
1129
- * @param {string} [namespace='default'] - Kubernetes namespace for the deployment.
1130
- * @returns {object} - Object containing the ready status of the deployment.
1131
- * @memberof UnderpostDeploy
1132
- */
1133
- async monitorReadyRunner(deployId, env, targetTraffic, ignorePods = [], namespace = 'default') {
1134
- let checkStatusIteration = 0;
1135
- const checkStatusIterationMsDelay = 1000;
1136
- const maxIterations = 3000;
1137
- const deploymentId = `${deployId}-${env}-${targetTraffic}`;
1138
- const iteratorTag = `[${deploymentId}]`;
1139
- logger.info('Deployment init', { deployId, env, targetTraffic, checkStatusIterationMsDelay, namespace });
1140
- const minReadyOk = 3;
1141
- let readyOk = 0;
1142
- let result = {
1143
- ready: false,
1144
- notReadyPods: [],
1145
- readyPods: [],
1146
- };
1147
- let lastMsg = {};
1148
- while (readyOk < minReadyOk) {
1149
- if (checkStatusIteration >= maxIterations) {
1150
- logger.error(
1151
- `${iteratorTag} | Deployment check ready status timeout. Max iterations reached: ${maxIterations}`,
1152
- );
1153
- break;
1154
- }
1155
- result = await Underpost.deploy.checkDeploymentReadyStatus(deployId, env, targetTraffic, ignorePods, namespace);
1156
- if (result.ready === true) {
1157
- readyOk++;
1158
- logger.info(`${iteratorTag} | Deployment ready. Verification number: ${readyOk}`);
1159
- for (const pod of result.readyPods) {
1160
- const { NAME } = pod;
1161
- lastMsg[NAME] = 'Deployment ready';
1162
- console.log(
1163
- 'Target pod:',
1164
- NAME[NAME.match('green') ? 'bgGreen' : 'bgBlue'].bold.black,
1165
- '| Status:',
1166
- lastMsg[NAME].bold.magenta,
1167
- );
1168
- }
1169
- }
1170
-
1171
- {
1172
- let indexOf = -1;
1173
- for (const pod of result.notReadyPods) {
1174
- indexOf++;
1175
- const { NAME, out } = pod;
1176
-
1177
- if (out.match('not') && out.match('found') && checkStatusIteration <= 20 && out.match(deploymentId))
1178
- lastMsg[NAME] = 'Starting deployment';
1179
- // else if (out.match('not') && out.match('found') && checkStatusIteration <= 20 && out.match('underpost'))
1180
- // lastMsg[NAME] = 'Installing underpost cli';
1181
- else if (out.match('not') && out.match('found') && checkStatusIteration <= 20 && out.match('task'))
1182
- lastMsg[NAME] = 'Initializing setup task';
1183
- else if (out.match('Empty environment variables')) lastMsg[NAME] = 'Setup environment';
1184
- else if (out.match(`${deployId}-${env}-build-deployment`)) lastMsg[NAME] = 'Building apps/services';
1185
- else if (out.match(`${deployId}-${env}-initializing-deployment`))
1186
- lastMsg[NAME] = 'Initializing apps/services';
1187
- else if (!lastMsg[NAME]) lastMsg[NAME] = `Waiting for status`;
1188
-
1189
- console.log(
1190
- 'Target pod:',
1191
- NAME[NAME.match('green') ? 'bgGreen' : 'bgBlue'].bold.black,
1192
- '| Status:',
1193
- lastMsg[NAME].bold.magenta,
1194
- );
1195
- }
1196
- }
1197
- await timer(checkStatusIterationMsDelay);
1198
- checkStatusIteration++;
1199
- logger.info(
1200
- `${iteratorTag} | Deployment in progress... | Delay number monitor iterations: ${checkStatusIteration}`,
1201
- );
1202
- }
1203
- logger.info(
1204
- `${iteratorTag} | Deployment ready. | Total delay number monitor iterations: ${checkStatusIteration}`,
1205
- );
1206
- return result;
1207
- },
1208
-
1209
- /**
1210
- * Retrieves the currently loaded images in the Kubernetes cluster.
1211
- * @param {string} [node='kind-worker'] - Node name to check for loaded images.
1212
- * @param {object} options - Options for the image retrieval.
1213
- * @param {boolean} options.spec - Whether to retrieve images from the pod specifications.
1214
- * @param {string} options.namespace - Kubernetes namespace to filter pods.
1215
- * @returns {Array<object>} - Array of objects containing pod names and their corresponding images.
1216
- * @memberof UnderpostDeploy
1217
- */
1218
- getCurrentLoadedImages(node = 'kind-worker', options = { spec: false, namespace: '' }) {
1219
- if (options.spec) {
1220
- const raw = shellExec(
1221
- `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}'`,
1222
- {
1223
- stdout: true,
1224
- silent: true,
1225
- },
1226
- );
1227
- return raw
1228
- .split(`\n`)
1229
- .map((lines) => ({
1230
- pod: lines.split('\t')[0].replaceAll(':', '').trim(),
1231
- image: lines.split('\t')[1] ? lines.split('\t')[1].replaceAll(',', '').trim() : null,
1232
- }))
1233
- .filter((o) => o.image);
1234
- }
1235
- const raw = shellExec(node === 'kind-worker' ? `docker exec -i ${node} crictl images` : `crictl images`, {
1236
- stdout: true,
1237
- silent: true,
1238
- });
1239
-
1240
- const heads = raw
1241
- .split(`\n`)[0]
1242
- .split(' ')
1243
- .filter((_r) => _r.trim());
1244
-
1245
- const pods = raw
1246
- .split(`\n`)
1247
- .filter((r) => !r.match('IMAGE'))
1248
- .map((r) => r.split(' ').filter((_r) => _r.trim()));
1249
-
1250
- const result = [];
1251
-
1252
- for (const row of pods) {
1253
- if (row.length === 0) continue;
1254
- const pod = {};
1255
- let index = -1;
1256
- for (const head of heads) {
1257
- if (head in pod) continue;
1258
- index++;
1259
- pod[head] = row[index];
1260
- }
1261
- result.push(pod);
1262
- }
1263
- return result;
1264
- },
1230
+ (env === 'production' &&
1231
+ options.cert === true &&
1232
+ (!options.certHosts || options.certHosts.split(',').includes(host))) ||
1233
+ options.selfSigned === true,
1265
1234
 
1266
1235
  /**
1267
1236
  * Predefined resource templates for Kubernetes deployments.
@@ -1363,6 +1332,34 @@ ${renderHosts}`,
1363
1332
  return undefined;
1364
1333
  },
1365
1334
 
1335
+ /**
1336
+ * Extracts a non-standard `imagePullPolicy` key from an env-resolved
1337
+ * instance lifecycle block (the convention used in `conf.instances.json`,
1338
+ * where `imagePullPolicy` sits alongside `postStart`/`preStop` for
1339
+ * per-instance ergonomics) and returns a clean lifecycle hash that is
1340
+ * safe to splice into the K8S container spec.
1341
+ *
1342
+ * Returns `{ lifecycle, imagePullPolicy }`:
1343
+ * - `lifecycle` — the input minus `imagePullPolicy`, or `undefined` when
1344
+ * the resulting block is empty.
1345
+ * - `imagePullPolicy` — the extracted value, or `undefined` if absent.
1346
+ *
1347
+ * @param {object|undefined} lifecycle - Env-resolved lifecycle block
1348
+ * (already passed through pickEnv). May be `undefined`.
1349
+ * @returns {{ lifecycle: (object|undefined), imagePullPolicy: (string|undefined) }}
1350
+ * @memberof UnderpostDeploy
1351
+ */
1352
+ extractInstanceImagePullPolicy(lifecycle) {
1353
+ if (!lifecycle || typeof lifecycle !== 'object' || !('imagePullPolicy' in lifecycle)) {
1354
+ return { lifecycle, imagePullPolicy: undefined };
1355
+ }
1356
+ const { imagePullPolicy, ...rest } = lifecycle;
1357
+ return {
1358
+ lifecycle: Object.keys(rest).length > 0 ? rest : undefined,
1359
+ imagePullPolicy,
1360
+ };
1361
+ },
1362
+
1366
1363
  /**
1367
1364
  * Generates timeout flags string for deployment commands.
1368
1365
  * @param {object} options - Options containing timeout settings.