cyberia 3.2.12 → 3.2.70
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/.env.example +127 -68
- package/.github/workflows/cyberia-client.cd.yml +40 -0
- package/.github/workflows/cyberia-server.cd.yml +40 -0
- package/.github/workflows/docker-image.cyberia-client.ci.yml +49 -0
- package/.github/workflows/docker-image.cyberia-client.dev.ci.yml +48 -0
- package/.github/workflows/docker-image.cyberia-server.ci.yml +69 -0
- package/.github/workflows/docker-image.cyberia-server.dev.ci.yml +69 -0
- package/.github/workflows/docker-image.engine-cyberia.ci.yml +52 -0
- package/.github/workflows/docker-image.engine-cyberia.dev.ci.yml +52 -0
- package/.github/workflows/engine-cyberia.cd.yml +34 -24
- package/.github/workflows/engine-cyberia.ci.yml +27 -5
- package/.github/workflows/ghpkg.ci.yml +89 -1
- package/.github/workflows/gitlab.ci.yml +1 -1
- package/.github/workflows/hardhat.ci.yml +1 -1
- package/.github/workflows/npmpkg.ci.yml +19 -12
- package/.github/workflows/publish.ci.yml +2 -2
- package/.github/workflows/publish.cyberia.ci.yml +5 -16
- package/.github/workflows/pwa-microservices-template-page.cd.yml +1 -8
- package/.github/workflows/pwa-microservices-template-test.ci.yml +1 -1
- package/CHANGELOG.md +451 -1
- package/CLI-HELP.md +1040 -1130
- package/Dockerfile +141 -43
- package/Dockerfile.dev +143 -0
- package/Dockerfile.test +165 -0
- package/README.md +1 -1
- package/baremetal/commission-workflows.json +1 -0
- package/bin/build.js +128 -136
- package/bin/build.template.js +25 -179
- package/bin/cyberia.js +1089 -188
- package/bin/deploy.js +6 -3
- package/bin/index.js +1089 -188
- package/bump.config.js +1 -0
- package/compose.env +131 -0
- package/conf.js +19 -1
- package/deployment.yaml +74 -2
- package/docker-compose.yml +316 -0
- package/hardhat/hardhat.config.js +2 -2
- package/hardhat/package-lock.json +620 -2041
- package/hardhat/package.json +7 -5
- package/hardhat/scripts/deployObjectLayerToken.js +18 -18
- package/hardhat/test/ObjectLayerToken.js +378 -274
- package/ipfs/configure-ipfs.sh +13 -0
- package/manifests/cronjobs/dd-cron/dd-cron-backup.yaml +2 -2
- package/manifests/cronjobs/dd-cron/dd-cron-dns.yaml +1 -1
- package/manifests/deployment/dd-cyberia-development/deployment.yaml +74 -2
- package/manifests/deployment/dd-cyberia-development/grpc-service.yaml +17 -0
- package/manifests/deployment/dd-cyberia-development/pv-pvc.yaml +32 -0
- package/manifests/deployment/dd-default-development/deployment.yaml +2 -2
- package/mongodb/entrypoint.sh +76 -0
- package/nginx.conf +87 -0
- package/package.json +34 -22
- package/pv-pvc.yaml +32 -0
- package/scripts/disk-clean.sh +85 -60
- package/scripts/kubeadm-node-setup.sh +317 -0
- package/scripts/link-local-underpost-cli.sh +6 -0
- package/scripts/rocky-kickstart.sh +877 -185
- package/scripts/rpmfusion-ffmpeg-setup.sh +26 -50
- package/scripts/test-monitor.sh +248 -0
- package/src/api/atlas-sprite-sheet/atlas-sprite-sheet.router.js +43 -7
- package/src/api/atlas-sprite-sheet/atlas-sprite-sheet.service.js +17 -25
- package/src/api/cyberia-action/cyberia-action.controller.js +19 -0
- package/src/api/cyberia-action/cyberia-action.model.js +21 -29
- package/src/api/cyberia-action/cyberia-action.router.js +42 -7
- package/src/api/cyberia-action/cyberia-action.service.js +20 -4
- package/src/api/cyberia-client-hints/cyberia-client-hints.model.js +58 -57
- package/src/api/cyberia-dialogue/cyberia-dialogue.router.js +37 -6
- package/src/api/cyberia-dialogue/cyberia-dialogue.service.js +8 -2
- package/src/api/cyberia-entity/cyberia-entity.router.js +39 -7
- package/src/api/cyberia-entity-type-default/cyberia-entity-type-default.controller.js +74 -0
- package/src/api/cyberia-entity-type-default/cyberia-entity-type-default.model.js +67 -0
- package/src/api/cyberia-entity-type-default/cyberia-entity-type-default.router.js +63 -0
- package/src/api/cyberia-entity-type-default/cyberia-entity-type-default.service.js +46 -0
- package/src/api/cyberia-instance/cyberia-fallback-world.js +62 -10
- package/src/api/cyberia-instance/cyberia-instance.model.js +32 -2
- package/src/api/cyberia-instance/cyberia-instance.router.js +6 -6
- package/src/api/cyberia-instance/cyberia-world-generator.js +116 -3
- package/src/api/cyberia-instance-conf/cyberia-instance-conf.model.js +3 -0
- package/src/api/cyberia-instance-conf/cyberia-instance-conf.router.js +39 -7
- package/src/api/cyberia-map/cyberia-map.router.js +21 -6
- package/src/api/cyberia-quest/cyberia-quest.controller.js +38 -0
- package/src/api/cyberia-quest/cyberia-quest.router.js +47 -7
- package/src/api/cyberia-quest/cyberia-quest.service.js +59 -4
- package/src/api/cyberia-saga/cyberia-saga.controller.js +74 -0
- package/src/api/cyberia-saga/cyberia-saga.model.js +59 -0
- package/src/api/cyberia-saga/cyberia-saga.router.js +63 -0
- package/src/api/cyberia-saga/cyberia-saga.service.js +42 -0
- package/src/api/cyberia-server-defaults/cyberia-server-defaults.js +551 -122
- package/src/api/cyberia-skill/cyberia-skill.controller.js +74 -0
- package/src/api/cyberia-skill/cyberia-skill.model.js +50 -0
- package/src/api/cyberia-skill/cyberia-skill.router.js +63 -0
- package/src/api/cyberia-skill/cyberia-skill.service.js +42 -0
- package/src/api/ipfs/ipfs.service.js +28 -15
- package/src/api/object-layer/object-layer.router.js +25 -15
- package/src/api/object-layer/object-layer.service.js +15 -20
- package/src/api/object-layer-render-frames/object-layer-render-frames.router.js +39 -7
- package/src/cli/baremetal.js +717 -16
- package/src/cli/cluster.js +80 -5
- package/src/cli/deploy.js +326 -292
- package/src/cli/docker-compose.js +648 -0
- package/src/cli/env.js +12 -6
- package/src/cli/fs.js +75 -30
- package/src/cli/image.js +187 -55
- package/src/cli/index.js +157 -6
- package/src/cli/kickstart.js +142 -20
- package/src/cli/monitor.js +387 -6
- package/src/cli/release.js +72 -15
- package/src/cli/repository.js +339 -40
- package/src/cli/run.js +678 -186
- package/src/cli/secrets.js +82 -48
- package/src/cli/ssh.js +234 -0
- package/src/cli/static.js +2 -2
- package/src/client/components/core/PanelForm.js +44 -44
- package/src/client/components/cyberia/ActionEngineCyberia.js +1867 -0
- package/src/client/components/cyberia/EntityEngineCyberia.js +585 -0
- package/src/client/components/cyberia/InstanceEngineCyberia.js +219 -13
- package/src/client/components/cyberia/MapEngineCyberia.js +154 -71
- package/src/client/components/cyberia/ObjectLayerEngineModal.js +40 -63
- package/src/client/components/cyberia/ObjectLayerEngineViewer.js +34 -20
- package/src/client/components/cyberia/SharedDefaultsCyberia.js +195 -4
- package/src/client/components/cyberia-portal/AppShellCyberiaPortal.js +66 -0
- package/src/client/components/cyberia-portal/RouterCyberiaPortal.js +8 -0
- package/src/client/components/cyberia-portal/TranslateCyberiaPortal.js +8 -0
- package/src/client/public/cyberia-docs/ACTION-SYSTEM.md +92 -20
- package/src/client/public/cyberia-docs/ARCHITECTURE.md +272 -50
- package/src/client/public/cyberia-docs/CYBERIA-CLI.md +3 -3
- package/src/client/public/cyberia-docs/CYBERIA-CLIENT.md +39 -22
- package/src/client/public/cyberia-docs/CYBERIA-LORE.md +88 -0
- package/src/client/public/cyberia-docs/CYBERIA-SAGA.md +394 -0
- package/src/client/public/cyberia-docs/CYBERIA-SERVER.md +28 -12
- package/src/client/public/cyberia-docs/CYBERIA.md +1 -1
- package/src/client/public/cyberia-docs/QUEST-SYSTEM.md +27 -5
- package/src/client/public/cyberia-docs/ROADMAP.md +1 -1
- package/src/client/public/cyberia-docs/WHITE-PAPER.md +1 -1
- package/src/client/services/cyberia-entity-type-default/cyberia-entity-type-default.service.js +99 -0
- package/src/client/services/cyberia-instance/cyberia-instance.management.js +2 -2
- package/src/client/services/cyberia-map/cyberia-map.management.js +2 -2
- package/src/client/services/cyberia-saga/cyberia-saga.service.js +99 -0
- package/src/client/services/cyberia-skill/cyberia-skill.service.js +99 -0
- package/src/client/services/object-layer/object-layer.management.js +6 -6
- package/src/{server → client-builder}/client-build-docs.js +15 -5
- package/src/{server → client-builder}/client-build-live.js +3 -3
- package/src/{server → client-builder}/client-build.js +25 -22
- package/src/{server → client-builder}/client-dev-server.js +3 -3
- package/src/{server → client-builder}/client-icons.js +2 -2
- package/src/{server → client-builder}/ssr.js +5 -5
- package/src/client.build.js +1 -3
- package/src/client.dev.js +1 -1
- package/src/db/mongo/MongoBootstrap.js +12 -12
- package/src/db/mongo/MongooseDB.js +2 -1
- package/src/grpc/cyberia/grpc-server.js +255 -70
- package/src/index.js +12 -1
- package/src/mailer/EmailRender.js +1 -1
- package/src/{server → projects/cyberia}/atlas-sprite-sheet-generator.js +2 -2
- package/src/{server → projects/cyberia}/besu-genesis-generator.js +3 -3
- package/src/projects/cyberia/catalog-cyberia.js +81 -0
- package/src/projects/cyberia/gemini-client.js +175 -0
- package/src/projects/cyberia/generate-saga.js +2107 -0
- package/src/{server → projects/cyberia}/ipfs-client.js +6 -4
- package/src/{server → projects/cyberia}/object-layer.js +12 -108
- package/src/{server → projects/cyberia}/semantic-layer-generator-floor.js +1 -1
- package/src/{server → projects/cyberia}/semantic-layer-generator-resource.js +1 -1
- package/src/{server → projects/cyberia}/semantic-layer-generator-skin.js +1 -1
- package/src/{server → projects/cyberia}/semantic-layer-generator.js +2 -2
- package/src/{server → projects/cyberia}/shape-generator.js +2 -2
- package/src/projects/underpost/catalog-underpost.js +60 -0
- package/src/runtime/cyberia-client/Dockerfile +31 -83
- package/src/runtime/cyberia-client/Dockerfile.dev +23 -30
- package/src/runtime/cyberia-server/Dockerfile +23 -42
- package/src/runtime/cyberia-server/Dockerfile.dev +20 -35
- package/src/runtime/engine-cyberia/Dockerfile +143 -0
- package/src/runtime/engine-cyberia/Dockerfile.dev +143 -0
- package/src/runtime/engine-cyberia/Dockerfile.test +165 -0
- package/src/runtime/engine-cyberia/compose.env +131 -0
- package/src/runtime/engine-cyberia/docker-compose.yml +316 -0
- package/src/runtime/engine-cyberia/ipfs/configure-ipfs.sh +13 -0
- package/src/runtime/engine-cyberia/mongodb/entrypoint.sh +76 -0
- package/src/runtime/engine-cyberia/nginx.conf +87 -0
- package/src/runtime/express/Express.js +2 -2
- package/src/runtime/nginx/Nginx.js +250 -0
- package/src/runtime/wp/Dockerfile +3 -3
- package/src/server/catalog.js +72 -0
- package/src/server/conf.js +415 -60
- package/src/server/runtime-status.js +252 -0
- package/src/server/start.js +42 -11
- package/src/server.js +6 -2
- package/test/cyberia-instance-conf-defaults.test.js +140 -0
- package/test/deploy-monitor.test.js +267 -0
- package/test/shape-generator.test.js +7 -1
- package/typedoc.dd-cyberia.json +3 -1
- package/typedoc.json +3 -1
- package/manifests/deployment/dd-test-development/deployment.yaml +0 -256
- package/manifests/deployment/dd-test-development/proxy.yaml +0 -102
- /package/src/client/ssr/{Render.js → RootDocument.js} +0 -0
- /package/src/{server → client-builder}/client-formatted.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.
|
|
@@ -128,6 +186,11 @@ class UnderpostDeploy {
|
|
|
128
186
|
* @param {boolean} skipFullBuild - Whether to skip the full client bundle build during deployment.
|
|
129
187
|
* @param {boolean} pullBundle - Whether to pull the pre-built client bundle from Cloudinary before starting. Use together with skipFullBuild to skip the local build entirely.
|
|
130
188
|
* @param {string} [imagePullPolicy] - Container imagePullPolicy override (`Always`, `IfNotPresent`, `Never`). When omitted, defaults to `Never` for `localhost/` images and `IfNotPresent` otherwise.
|
|
189
|
+
* @param {object} lifecycle - Kubernetes lifecycle hooks configuration for the deployment container.
|
|
190
|
+
* @param {object} readinessProbe - Kubernetes readiness probe configuration for the deployment container.
|
|
191
|
+
* @param {object} livenessProbe - Kubernetes liveness probe configuration for the deployment container.
|
|
192
|
+
* @param {object} startupProbe - Kubernetes startup probe configuration for the deployment container.
|
|
193
|
+
* @param {number} containerPort - Container port to expose for the deployment.
|
|
131
194
|
* @returns {string} - YAML deployment configuration for the specified deployment.
|
|
132
195
|
* @memberof UnderpostDeploy
|
|
133
196
|
*/
|
|
@@ -153,7 +216,12 @@ class UnderpostDeploy {
|
|
|
153
216
|
lifecycle,
|
|
154
217
|
readinessProbe,
|
|
155
218
|
livenessProbe,
|
|
219
|
+
startupProbe,
|
|
156
220
|
containerPort,
|
|
221
|
+
// Explicit, secret-free internal status port injected as an env var so the
|
|
222
|
+
// in-pod endpoint binds exactly what the probes and the monitor target,
|
|
223
|
+
// independent of the ambient `PORT` baked into the image/secret.
|
|
224
|
+
internalStatusPort,
|
|
157
225
|
}) {
|
|
158
226
|
if (!cmd)
|
|
159
227
|
cmd =
|
|
@@ -205,12 +273,19 @@ spec:
|
|
|
205
273
|
- secretRef:
|
|
206
274
|
name: underpost-config
|
|
207
275
|
${
|
|
208
|
-
|
|
209
|
-
? `
|
|
210
|
-
-
|
|
276
|
+
internalStatusPort
|
|
277
|
+
? ` env:
|
|
278
|
+
- name: UNDERPOST_INTERNAL_PORT
|
|
279
|
+
value: "${internalStatusPort}"
|
|
211
280
|
`
|
|
212
281
|
: ''
|
|
213
282
|
}${
|
|
283
|
+
containerPort
|
|
284
|
+
? ` ports:
|
|
285
|
+
- containerPort: ${containerPort}
|
|
286
|
+
`
|
|
287
|
+
: ''
|
|
288
|
+
}${
|
|
214
289
|
resources
|
|
215
290
|
? ` resources:
|
|
216
291
|
requests:
|
|
@@ -242,6 +317,15 @@ ${JSON.stringify(livenessProbe, null, 2)
|
|
|
242
317
|
.split('\n')
|
|
243
318
|
.map((l) => ' ' + l)
|
|
244
319
|
.join('\n')}
|
|
320
|
+
`
|
|
321
|
+
: ''
|
|
322
|
+
}${
|
|
323
|
+
startupProbe
|
|
324
|
+
? ` startupProbe:
|
|
325
|
+
${JSON.stringify(startupProbe, null, 2)
|
|
326
|
+
.split('\n')
|
|
327
|
+
.map((l) => ' ' + l)
|
|
328
|
+
.join('\n')}
|
|
245
329
|
`
|
|
246
330
|
: ''
|
|
247
331
|
}${
|
|
@@ -283,18 +367,26 @@ spec:
|
|
|
283
367
|
* @param {object} options - Options for the manifest build process.
|
|
284
368
|
* @param {string} options.replicas - Number of replicas for each deployment.
|
|
285
369
|
* @param {string} options.image - Docker image for the deployment.
|
|
286
|
-
* @param {string} options.namespace - Kubernetes namespace for the deployment.
|
|
370
|
+
* @param {string} options.namespace - Kubernetes namespace for the deployment (defaults to "default").
|
|
287
371
|
* @param {string} [options.versions] - Comma-separated list of versions to deploy.
|
|
288
372
|
* @param {string} [options.cmd] - Custom initialization command for deploymentYamlPartsFactory (comma-separated commands).
|
|
289
|
-
* @param {string} [options.timeoutResponse] -
|
|
290
|
-
* @param {string} [options.timeoutIdle] -
|
|
291
|
-
* @param {string} [options.retryCount] -
|
|
292
|
-
* @param {string} [options.retryPerTryTimeout] -
|
|
293
|
-
* @param {boolean} [options.disableDeploymentProxy] - Whether to disable deployment proxy.
|
|
294
|
-
* @param {string} [options.traffic] -
|
|
295
|
-
* @param {boolean} [options.
|
|
373
|
+
* @param {string} [options.timeoutResponse] - HTTPProxy per-route response timeout (e.g. "300000ms", "infinity").
|
|
374
|
+
* @param {string} [options.timeoutIdle] - HTTPProxy per-route idle timeout (e.g. "10s", "infinity").
|
|
375
|
+
* @param {string} [options.retryCount] - HTTPProxy per-route retry count (e.g. 3).
|
|
376
|
+
* @param {string} [options.retryPerTryTimeout] - HTTPProxy per-route per-try timeout (e.g. "150ms").
|
|
377
|
+
* @param {boolean} [options.disableDeploymentProxy] - Whether to disable deployment proxy route generation.
|
|
378
|
+
* @param {string} [options.traffic] - Comma-separated active traffic colour(s) used to select which versions receive traffic (e.g. "blue", "green").
|
|
379
|
+
* @param {boolean} [options.cert] - Whether to include cert-manager Certificate resources in secret.yaml (production only).
|
|
380
|
+
* @param {boolean} [options.selfSigned] - Whether to include TLS block in HTTPProxy using a pre-created self-signed secret. Enables HTTPS for development without cert-manager.
|
|
381
|
+
* @param {boolean} [options.skipFullBuild] - Whether to skip the full client bundle build; forwarded to deploymentYamlPartsFactory.
|
|
296
382
|
* @param {boolean} [options.pullBundle] - Whether to pull the pre-built client bundle from Cloudinary; forwarded to deploymentYamlPartsFactory. Use together with skipFullBuild.
|
|
297
|
-
* @param {string} [options.imagePullPolicy] - Container imagePullPolicy override (`Always`, `IfNotPresent`, `Never`); forwarded to deploymentYamlPartsFactory.
|
|
383
|
+
* @param {string} [options.imagePullPolicy] - Container imagePullPolicy override (`Always`, `IfNotPresent`, `Never`); forwarded to deploymentYamlPartsFactory. Defaults to `Never` for `localhost/` images and `IfNotPresent` otherwise.
|
|
384
|
+
* @param {boolean} [options.disableRuntimeProbes] - Omit internal-status HTTP probes from generated manifests. When true no readiness/liveness/startup probes are emitted.
|
|
385
|
+
* @param {boolean} [options.tcpProbes] - Emit legacy TCP socket probes instead of HTTP internal-status probes (migration path).
|
|
386
|
+
* @param {string} [options.node] - Explicit target node for hostPath PV nodeAffinity pinning; resolved through {@link UnderpostDeploy.resolveDeployNode} together with the cluster flags.
|
|
387
|
+
* @param {boolean} [options.kind] - Kind cluster context; affects the cluster-type node default when no explicit node is set.
|
|
388
|
+
* @param {boolean} [options.kubeadm] - Kubeadm cluster context; affects the cluster-type node default when no explicit node is set.
|
|
389
|
+
* @param {boolean} [options.k3s] - K3s cluster context; affects the cluster-type node default when no explicit node is set.
|
|
298
390
|
* @returns {Promise<void>} - Promise that resolves when the manifest is built.
|
|
299
391
|
* @memberof UnderpostDeploy
|
|
300
392
|
*/
|
|
@@ -319,6 +411,17 @@ spec:
|
|
|
319
411
|
|
|
320
412
|
logger.info('port range', { deployId, fromPort, toPort });
|
|
321
413
|
|
|
414
|
+
// The internal status endpoint binds `fromPort - 1`: app instances bind
|
|
415
|
+
// the router range starting at `fromPort`, so this slot is always free
|
|
416
|
+
// inside the pod. It is injected into the pod env (UNDERPOST_INTERNAL_PORT)
|
|
417
|
+
// and used for both the probes and the monitor's port-forward target so
|
|
418
|
+
// all three agree regardless of the image's ambient PORT.
|
|
419
|
+
// Opt out with `--disable-runtime-probes` to keep legacy probe-less pods.
|
|
420
|
+
const internalPort = fromPort - 1;
|
|
421
|
+
const probes = options.disableRuntimeProbes
|
|
422
|
+
? {}
|
|
423
|
+
: Underpost.deploy.runtimeProbesFactory({ port: internalPort, useHttp: !options.tcpProbes });
|
|
424
|
+
|
|
322
425
|
let deploymentYamlParts = '';
|
|
323
426
|
for (const deploymentVersion of deploymentVersions) {
|
|
324
427
|
deploymentYamlParts += `---
|
|
@@ -334,6 +437,10 @@ ${Underpost.deploy
|
|
|
334
437
|
skipFullBuild: options.skipFullBuild,
|
|
335
438
|
pullBundle: options.pullBundle,
|
|
336
439
|
imagePullPolicy: options.imagePullPolicy,
|
|
440
|
+
internalStatusPort: options.disableRuntimeProbes ? undefined : internalPort,
|
|
441
|
+
readinessProbe: probes.readinessProbe,
|
|
442
|
+
livenessProbe: probes.livenessProbe,
|
|
443
|
+
startupProbe: probes.startupProbe,
|
|
337
444
|
})
|
|
338
445
|
.replace('{{ports}}', buildKindPorts(fromPort, toPort))}
|
|
339
446
|
`;
|
|
@@ -352,6 +459,15 @@ ${Underpost.deploy
|
|
|
352
459
|
? JSON.parse(fs.readFileSync(`./engine-private/conf/${deployId}/conf.volume.json`, 'utf8'))
|
|
353
460
|
: [];
|
|
354
461
|
if (confVolume.length > 0) {
|
|
462
|
+
// Mirror deployVolume's data-node resolution so the generated manifest
|
|
463
|
+
// pins the PV to the same node that physically receives the volume data.
|
|
464
|
+
const pvDataNode = Underpost.deploy.resolveDeployNode({
|
|
465
|
+
node: options.node,
|
|
466
|
+
kind: options.kind,
|
|
467
|
+
kubeadm: options.kubeadm,
|
|
468
|
+
k3s: options.k3s,
|
|
469
|
+
env,
|
|
470
|
+
});
|
|
355
471
|
let volumeYaml = '';
|
|
356
472
|
for (const deploymentVersion of deploymentVersions) {
|
|
357
473
|
for (const volume of confVolume) {
|
|
@@ -363,6 +479,7 @@ ${Underpost.deploy
|
|
|
363
479
|
pvcId,
|
|
364
480
|
namespace: options.namespace,
|
|
365
481
|
hostPath,
|
|
482
|
+
nodeName: pvDataNode,
|
|
366
483
|
})}\n`;
|
|
367
484
|
}
|
|
368
485
|
}
|
|
@@ -376,7 +493,7 @@ ${Underpost.deploy
|
|
|
376
493
|
: [];
|
|
377
494
|
|
|
378
495
|
for (const host of Object.keys(confServer)) {
|
|
379
|
-
if (env === 'production')
|
|
496
|
+
if (env === 'production' && options.cert === true)
|
|
380
497
|
secretYaml += Underpost.deploy.buildCertManagerCertificate({ host, namespace: options.namespace });
|
|
381
498
|
|
|
382
499
|
const pathPortAssignment = pathPortAssignmentData[host];
|
|
@@ -445,7 +562,7 @@ ${Underpost.deploy
|
|
|
445
562
|
const yamlPath = `./engine-private/conf/${deployId}/build/${env}/secret.yaml`;
|
|
446
563
|
fs.writeFileSync(yamlPath, secretYaml, 'utf8');
|
|
447
564
|
} else {
|
|
448
|
-
const deploymentsFiles = ['Dockerfile', 'proxy.yaml', 'deployment.yaml', 'pv-pvc.yaml'];
|
|
565
|
+
const deploymentsFiles = ['Dockerfile', 'proxy.yaml', 'deployment.yaml', 'pv-pvc.yaml', 'grpc-service.yaml'];
|
|
449
566
|
for (const file of deploymentsFiles) {
|
|
450
567
|
if (fs.existsSync(`./engine-private/conf/${deployId}/build/${env}/${file}`)) {
|
|
451
568
|
fs.copyFileSync(
|
|
@@ -579,6 +696,7 @@ spec:
|
|
|
579
696
|
* @memberof UnderpostDeploy
|
|
580
697
|
*/
|
|
581
698
|
baseProxyYamlFactory({ host, env, options }) {
|
|
699
|
+
const includeTls = env !== 'development' || options.selfSigned === true;
|
|
582
700
|
return `
|
|
583
701
|
---
|
|
584
702
|
apiVersion: projectcontour.io/v1
|
|
@@ -589,11 +707,11 @@ metadata:
|
|
|
589
707
|
spec:
|
|
590
708
|
virtualhost:
|
|
591
709
|
fqdn: ${host}${
|
|
592
|
-
|
|
593
|
-
?
|
|
594
|
-
: `
|
|
710
|
+
includeTls
|
|
711
|
+
? `
|
|
595
712
|
tls:
|
|
596
713
|
secretName: ${host}`
|
|
714
|
+
: ''
|
|
597
715
|
}
|
|
598
716
|
routes:`;
|
|
599
717
|
},
|
|
@@ -609,35 +727,42 @@ spec:
|
|
|
609
727
|
* @param {boolean} options.buildManifest - Whether to build the deployment manifest.
|
|
610
728
|
* @param {boolean} options.infoUtil - Whether to display utility information.
|
|
611
729
|
* @param {boolean} options.expose - Whether to expose the deployment.
|
|
612
|
-
* @param {boolean} options.cert - Whether to create
|
|
613
|
-
* @param {string} options.certHosts - Comma-separated list of hosts for which to create certificates.
|
|
730
|
+
* @param {boolean} options.cert - Whether to create cert-manager Certificate resources for the deployment.
|
|
731
|
+
* @param {string} options.certHosts - Comma-separated list of hosts for which to create cert-manager certificates.
|
|
732
|
+
* @param {boolean} options.selfSigned - Use a pre-created self-signed TLS secret instead of cert-manager. The secret must already exist in the namespace with the same name as the host. Enables TLS in the Contour HTTPProxy virtualhost without requiring a production ClusterIssuer.
|
|
614
733
|
* @param {string} options.versions - Comma-separated list of versions to deploy.
|
|
615
734
|
* @param {string} options.image - Docker image for the deployment.
|
|
616
735
|
* @param {string} options.traffic - Traffic status for the deployment.
|
|
617
736
|
* @param {string} options.replicas - Number of replicas for the deployment.
|
|
618
|
-
* @param {string} options.node -
|
|
737
|
+
* @param {string} options.node - Explicit target node (highest precedence in the node chain). When empty, {@link UnderpostDeploy.resolveDeployNode} falls back to the cluster-type default (`kind-worker` for kind, host for kubeadm/k3s). Used for both volume placement and hostPath PV nodeAffinity.
|
|
738
|
+
* @param {string} [options.sshKeyPath] - Private key path for node SSH operations, forwarded to deployVolume when shipping a hostPath volume to a remote target node over SSH. Defaults to engine-private/deploy/id_rsa.
|
|
619
739
|
* @param {boolean} options.disableUpdateDeployment - Whether to disable deployment updates.
|
|
620
740
|
* @param {boolean} options.disableUpdateProxy - Whether to disable proxy updates.
|
|
621
741
|
* @param {boolean} options.disableDeploymentProxy - Whether to disable deployment proxy.
|
|
622
742
|
* @param {boolean} options.disableUpdateVolume - Whether to disable volume updates.
|
|
623
743
|
* @param {boolean} options.status - Whether to display deployment status.
|
|
624
744
|
* @param {boolean} options.disableUpdateUnderpostConfig - Whether to disable Underpost config updates.
|
|
625
|
-
* @param {string} [options.namespace] - Kubernetes namespace for the deployment.
|
|
626
|
-
* @param {string} [options.timeoutResponse] -
|
|
627
|
-
* @param {string} [options.timeoutIdle] -
|
|
628
|
-
* @param {string} [options.retryCount] -
|
|
629
|
-
* @param {string} [options.retryPerTryTimeout] -
|
|
630
|
-
* @param {string} [options.kindType] -
|
|
631
|
-
* @param {number} [options.port] - Port number for exposing the deployment.
|
|
632
|
-
* @param {string} [options.cmd] - Custom initialization command
|
|
633
|
-
* @param {number} [options.exposePort] -
|
|
745
|
+
* @param {string} [options.namespace] - Kubernetes namespace for the deployment (defaults to "default").
|
|
746
|
+
* @param {string} [options.timeoutResponse] - HTTPProxy per-route response timeout (e.g. "300000ms", "infinity").
|
|
747
|
+
* @param {string} [options.timeoutIdle] - HTTPProxy per-route idle timeout (e.g. "10s", "infinity").
|
|
748
|
+
* @param {string} [options.retryCount] - HTTPProxy per-route retry count (e.g. 3).
|
|
749
|
+
* @param {string} [options.retryPerTryTimeout] - HTTPProxy per-route per-try timeout (e.g. "150ms").
|
|
750
|
+
* @param {string} [options.kindType] - Kubernetes resource kind to target when using --expose (defaults to "svc").
|
|
751
|
+
* @param {number} [options.port] - Port number override for exposing the deployment.
|
|
752
|
+
* @param {string} [options.cmd] - Custom initialization command (comma-separated) for deploymentYamlPartsFactory.
|
|
753
|
+
* @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.
|
|
754
|
+
* @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.
|
|
755
|
+
* @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 /).
|
|
756
|
+
* @param {boolean} [options.tls] - When true (with --expose --local-proxy), start the proxy on port 443 with TLS using self-signed certificates resolved from the local SSL store.
|
|
634
757
|
* @param {boolean} [options.k3s] - Whether to use k3s cluster context.
|
|
635
758
|
* @param {boolean} [options.kubeadm] - Whether to use kubeadm cluster context.
|
|
636
759
|
* @param {boolean} [options.kind] - Whether to use kind cluster context.
|
|
637
760
|
* @param {boolean} [options.gitClean] - Whether to run git clean on volume mount paths before copying.
|
|
638
761
|
* @param {boolean} [options.skipFullBuild] - Whether to skip the full client bundle build; passed through to buildManifest/deploymentYamlPartsFactory.
|
|
639
762
|
* @param {boolean} [options.pullBundle] - Whether to pull the pre-built client bundle from Cloudinary; passed through to buildManifest/deploymentYamlPartsFactory. Use together with skipFullBuild.
|
|
640
|
-
* @param {string} [options.imagePullPolicy] - Container imagePullPolicy override (`Always`, `IfNotPresent`, `Never`); passed through to buildManifest/deploymentYamlPartsFactory.
|
|
763
|
+
* @param {string} [options.imagePullPolicy] - Container imagePullPolicy override (`Always`, `IfNotPresent`, `Never`); passed through to buildManifest/deploymentYamlPartsFactory. Defaults to `Never` for `localhost/` images and `IfNotPresent` otherwise.
|
|
764
|
+
* @param {boolean} [options.disableRuntimeProbes] - Omit internal-status HTTP probes from generated manifests. When true no readiness/liveness/startup probes are emitted.
|
|
765
|
+
* @param {boolean} [options.tcpProbes] - Emit legacy TCP socket probes instead of HTTP internal-status probes.
|
|
641
766
|
* @returns {Promise<void>} - Promise that resolves when the deployment process is complete.
|
|
642
767
|
* @memberof UnderpostDeploy
|
|
643
768
|
*/
|
|
@@ -672,6 +797,10 @@ spec:
|
|
|
672
797
|
kindType: '',
|
|
673
798
|
port: 0,
|
|
674
799
|
exposePort: 0,
|
|
800
|
+
exposeLocalPort: 0,
|
|
801
|
+
localProxy: false,
|
|
802
|
+
tls: false,
|
|
803
|
+
selfSigned: false,
|
|
675
804
|
cmd: '',
|
|
676
805
|
k3s: false,
|
|
677
806
|
kubeadm: false,
|
|
@@ -757,20 +886,50 @@ EOF`);
|
|
|
757
886
|
logger.error(`No ${kindType} found matching '${deployId}', skipping expose`);
|
|
758
887
|
continue;
|
|
759
888
|
}
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
889
|
+
if (options.localProxy) {
|
|
890
|
+
const svcPorts = [
|
|
891
|
+
...new Set(
|
|
892
|
+
svc['PORT(S)']
|
|
893
|
+
.split(',')
|
|
894
|
+
.filter((p) => p.includes('/TCP'))
|
|
895
|
+
.map((p) => parseInt(p.split(':')[0])),
|
|
896
|
+
),
|
|
897
|
+
];
|
|
898
|
+
for (const svcPort of svcPorts) {
|
|
899
|
+
shellExec(`sudo kubectl port-forward -n ${namespace} ${kindType}/${svc.NAME} ${svcPort}:${svcPort}`, {
|
|
900
|
+
async: true,
|
|
901
|
+
});
|
|
902
|
+
}
|
|
903
|
+
const envFile = `./engine-private/conf/${deployId}/.env.${env}`;
|
|
904
|
+
let basePort = svcPorts[0] - 1;
|
|
905
|
+
if (fs.existsSync(envFile)) {
|
|
906
|
+
const portMatch = fs.readFileSync(envFile, 'utf8').match(/^PORT=(\d+)/m);
|
|
907
|
+
if (portMatch) basePort = parseInt(portMatch[1]);
|
|
908
|
+
}
|
|
909
|
+
logger.info(deployId, { svc, svcPorts, basePort });
|
|
910
|
+
const tlsFlag = options.tls ? ' tls' : '';
|
|
911
|
+
shellExec(
|
|
912
|
+
`NODE_ENV=${env} PORT=${basePort} DEV_PROXY_PORT_OFFSET=0 node src/proxy proxy ${deployId} ${env}${tlsFlag}`,
|
|
913
|
+
{ async: true },
|
|
914
|
+
);
|
|
915
|
+
} else {
|
|
916
|
+
const remotePort = options.exposePort
|
|
917
|
+
? parseInt(options.exposePort)
|
|
918
|
+
: options.port
|
|
919
|
+
? parseInt(options.port)
|
|
920
|
+
: kindType !== 'svc'
|
|
921
|
+
? 80
|
|
922
|
+
: parseInt(svc[`PORT(S)`].split('/TCP')[0]);
|
|
923
|
+
const localPort = options.exposeLocalPort ? parseInt(options.exposeLocalPort) : remotePort;
|
|
924
|
+
logger.info(deployId, {
|
|
925
|
+
svc,
|
|
926
|
+
localPort,
|
|
927
|
+
remotePort,
|
|
928
|
+
});
|
|
929
|
+
shellExec(`sudo kubectl port-forward -n ${namespace} ${kindType}/${svc.NAME} ${localPort}:${remotePort}`, {
|
|
930
|
+
async: true,
|
|
931
|
+
});
|
|
932
|
+
}
|
|
774
933
|
continue;
|
|
775
934
|
}
|
|
776
935
|
|
|
@@ -794,9 +953,16 @@ EOF`);
|
|
|
794
953
|
env,
|
|
795
954
|
version,
|
|
796
955
|
namespace,
|
|
797
|
-
nodeName:
|
|
956
|
+
nodeName: Underpost.deploy.resolveDeployNode({
|
|
957
|
+
node: options.node,
|
|
958
|
+
kind: options.kind,
|
|
959
|
+
kubeadm: options.kubeadm,
|
|
960
|
+
k3s: options.k3s,
|
|
961
|
+
env,
|
|
962
|
+
}),
|
|
798
963
|
clusterContext: options.k3s ? 'k3s' : options.kubeadm ? 'kubeadm' : 'kind',
|
|
799
964
|
gitClean: options.gitClean || false,
|
|
965
|
+
sshKeyPath: options.sshKeyPath || '',
|
|
800
966
|
});
|
|
801
967
|
}
|
|
802
968
|
|
|
@@ -822,63 +988,17 @@ EOF`);
|
|
|
822
988
|
if (!options.disableUpdateProxy)
|
|
823
989
|
shellExec(`sudo kubectl apply -f ./${manifestsPath}/proxy.yaml -n ${namespace}`);
|
|
824
990
|
|
|
825
|
-
if (
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
* @param {string} traffic - Current traffic status for the deployment.
|
|
835
|
-
* @param {Array<string>} ignoresNames - List of pod names to ignore.
|
|
836
|
-
* @param {string} [namespace='default'] - Kubernetes namespace for the deployment.
|
|
837
|
-
* @returns {object} - Object containing the status of the deployment.
|
|
838
|
-
* @memberof UnderpostDeploy
|
|
839
|
-
*/
|
|
840
|
-
async checkDeploymentReadyStatus(deployId, env, traffic, ignoresNames = [], namespace = 'default') {
|
|
841
|
-
const pods = Underpost.kubectl.get(`${deployId}-${env}-${traffic}`, 'pods', namespace);
|
|
842
|
-
const readyPods = [];
|
|
843
|
-
const notReadyPods = [];
|
|
844
|
-
|
|
845
|
-
// Readiness signal: the pod's Kubernetes `Ready` condition driven by the
|
|
846
|
-
// container's readinessProbe (TCP socket, HTTP get, or exec). Set by kubelet
|
|
847
|
-
// when the probe passes. A failed or crashing runtime never becomes Ready —
|
|
848
|
-
// kubelet surfaces CrashLoopBackOff and this gate stays closed.
|
|
849
|
-
for (const pod of pods) {
|
|
850
|
-
const { NAME } = pod;
|
|
851
|
-
if (ignoresNames && ignoresNames.find((t) => NAME.trim().toLowerCase().match(t.trim().toLowerCase()))) continue;
|
|
852
|
-
|
|
853
|
-
let podJson = null;
|
|
854
|
-
try {
|
|
855
|
-
// Pod may not exist yet (between deployment apply and pod
|
|
856
|
-
// scheduling). silentOnError lets the monitor loop continue
|
|
857
|
-
// instead of aborting on the transient NotFound exit.
|
|
858
|
-
const raw = shellExec(`sudo kubectl get pod ${NAME} -n ${namespace} -o json`, {
|
|
859
|
-
silent: true,
|
|
860
|
-
disableLog: true,
|
|
861
|
-
stdout: true,
|
|
862
|
-
silentOnError: true,
|
|
863
|
-
});
|
|
864
|
-
podJson = raw ? JSON.parse(raw) : null;
|
|
865
|
-
} catch (_) {
|
|
866
|
-
podJson = null;
|
|
991
|
+
if (
|
|
992
|
+
Underpost.deploy.isValidTLSContext({ host: Object.keys(confServer)[0], env, options }) &&
|
|
993
|
+
!options.selfSigned
|
|
994
|
+
) {
|
|
995
|
+
const secretPath = `./${manifestsPath}/secret.yaml`;
|
|
996
|
+
if (fs.existsSync(secretPath) && fs.readFileSync(secretPath, 'utf8').trim()) {
|
|
997
|
+
shellExec(`sudo kubectl apply -f ${secretPath} -n ${namespace}`);
|
|
998
|
+
} else logger.info('Skipping secret.yaml apply (no objects yet; applied by the --cert step)');
|
|
999
|
+
}
|
|
867
1000
|
}
|
|
868
|
-
const conditions = podJson?.status?.conditions || [];
|
|
869
|
-
const readyCondition = conditions.find((c) => c.type === 'Ready');
|
|
870
|
-
const k8sReady = readyCondition?.status === 'True';
|
|
871
|
-
|
|
872
|
-
pod.out = JSON.stringify({ k8sReady, condition: readyCondition ?? null });
|
|
873
|
-
|
|
874
|
-
if (k8sReady) readyPods.push(pod);
|
|
875
|
-
else notReadyPods.push(pod);
|
|
876
1001
|
}
|
|
877
|
-
return {
|
|
878
|
-
ready: pods.length > 0 && notReadyPods.length === 0,
|
|
879
|
-
notReadyPods,
|
|
880
|
-
readyPods,
|
|
881
|
-
};
|
|
882
1002
|
},
|
|
883
1003
|
/**
|
|
884
1004
|
* Creates a Kubernetes Secret for a deployment (replaces configMap for secret data).
|
|
@@ -889,10 +1009,17 @@ EOF`);
|
|
|
889
1009
|
*/
|
|
890
1010
|
configMap(env, namespace = 'default') {
|
|
891
1011
|
const cronDeployId = cronDeployIdResolve() || 'dd-cron';
|
|
1012
|
+
const envFilePath = `/home/dd/engine/engine-private/conf/${cronDeployId}/.env.${env}`;
|
|
1013
|
+
// `--from-env-file` turns every KEY=VALUE into a secret key that the Deployment injects via
|
|
1014
|
+
// `envFrom`. Strip shell/runtime-critical keys (notably PATH) first — an injected PATH
|
|
1015
|
+
// overrides the image's own and breaks coreutils/sudo resolution inside the pod.
|
|
1016
|
+
const sanitizedEnvPath = `${envFilePath}.secret`;
|
|
1017
|
+
fs.writeFileSync(sanitizedEnvPath, Underpost.secret.sanitizeSecretEnvFile(fs.readFileSync(envFilePath, 'utf8')));
|
|
892
1018
|
shellExec(`kubectl delete secret underpost-config -n ${namespace} --ignore-not-found`);
|
|
893
1019
|
shellExec(
|
|
894
|
-
`kubectl create secret generic underpost-config --from-env-file
|
|
1020
|
+
`kubectl create secret generic underpost-config --from-env-file=${sanitizedEnvPath} --dry-run=client -o yaml | kubectl apply -f - -n ${namespace}`,
|
|
895
1021
|
);
|
|
1022
|
+
fs.removeSync(sanitizedEnvPath);
|
|
896
1023
|
},
|
|
897
1024
|
/**
|
|
898
1025
|
* Switches the traffic for a deployment.
|
|
@@ -938,6 +1065,37 @@ EOF`);
|
|
|
938
1065
|
Underpost.env.set(`${deployId}-${env}-traffic`, targetTraffic);
|
|
939
1066
|
},
|
|
940
1067
|
|
|
1068
|
+
/**
|
|
1069
|
+
* Resolves the effective target node for a deployment, applying a single
|
|
1070
|
+
* precedence chain shared by every deploy workflow — the default `deploy`
|
|
1071
|
+
* callback, `run sync`, and custom `run instance` — so node customization
|
|
1072
|
+
* behaves identically everywhere:
|
|
1073
|
+
*
|
|
1074
|
+
* 1. **Explicit node** — `node` (the resolved `--node` value). Upstream
|
|
1075
|
+
* runners derive it from the comma-path field or `--node-name`
|
|
1076
|
+
* (`run sync`: `path.split(',')[4]` > `--node-name` > default) and from
|
|
1077
|
+
* `--node-name` directly (`run instance`).
|
|
1078
|
+
* 2. **Cluster-type default** — when no explicit node is given: `kind-worker`
|
|
1079
|
+
* for a kind cluster (the node that hosts kind hostPath volumes),
|
|
1080
|
+
* otherwise the control-plane / current host (`os.hostname()`) for
|
|
1081
|
+
* kubeadm / k3s. With no explicit cluster flag, `development` is treated
|
|
1082
|
+
* as kind and `production` as the host, preserving legacy behaviour.
|
|
1083
|
+
*
|
|
1084
|
+
* @param {object} params
|
|
1085
|
+
* @param {string} [params.node=''] - Explicit node (`--node`); highest precedence.
|
|
1086
|
+
* @param {boolean} [params.kind=false] - Kind cluster context.
|
|
1087
|
+
* @param {boolean} [params.kubeadm=false] - Kubeadm cluster context.
|
|
1088
|
+
* @param {boolean} [params.k3s=false] - K3s cluster context.
|
|
1089
|
+
* @param {string} [params.env=''] - Deployment environment; tie-breaker when no cluster flag is set.
|
|
1090
|
+
* @returns {string} The effective node name.
|
|
1091
|
+
* @memberof UnderpostDeploy
|
|
1092
|
+
*/
|
|
1093
|
+
resolveDeployNode({ node = '', kind = false, kubeadm = false, k3s = false, env = '' } = {}) {
|
|
1094
|
+
if (node) return node;
|
|
1095
|
+
const isKind = kind || (!kubeadm && !k3s && env !== 'production');
|
|
1096
|
+
return isKind ? 'kind-worker' : os.hostname();
|
|
1097
|
+
},
|
|
1098
|
+
|
|
941
1099
|
/**
|
|
942
1100
|
* Deploys a volume for a deployment.
|
|
943
1101
|
* @param {object} volume - Volume configuration.
|
|
@@ -949,9 +1107,10 @@ EOF`);
|
|
|
949
1107
|
* @param {string} options.env - Environment for the deployment.
|
|
950
1108
|
* @param {string} options.version - Version of the deployment.
|
|
951
1109
|
* @param {string} options.namespace - Kubernetes namespace for the deployment.
|
|
952
|
-
* @param {string} options.nodeName -
|
|
1110
|
+
* @param {string} options.nodeName - Effective target node (already resolved via {@link UnderpostDeploy.resolveDeployNode}). The volume data is written/shipped here and the PV is pinned to it; an empty value falls back to the cluster-type default inside this method.
|
|
953
1111
|
* @param {string} [options.clusterContext='kind'] - Cluster context type ('kind', 'kubeadm', or 'k3s').
|
|
954
1112
|
* @param {boolean} [options.gitClean=false] - Whether to run git clean on volumeMountPath before copying.
|
|
1113
|
+
* @param {string} [options.sshKeyPath=''] - Private key path used when the target node is remote and the volume is shipped over SSH. Empty falls back to copyDirToNode's default (engine-private/deploy/id_rsa).
|
|
955
1114
|
* @memberof UnderpostDeploy
|
|
956
1115
|
*/
|
|
957
1116
|
deployVolume(
|
|
@@ -964,6 +1123,7 @@ EOF`);
|
|
|
964
1123
|
nodeName: '',
|
|
965
1124
|
clusterContext: 'kind',
|
|
966
1125
|
gitClean: false,
|
|
1126
|
+
sshKeyPath: '',
|
|
967
1127
|
},
|
|
968
1128
|
) {
|
|
969
1129
|
if (!volume.claimName) {
|
|
@@ -978,19 +1138,48 @@ EOF`);
|
|
|
978
1138
|
if (options.gitClean && volume.volumeMountPath) {
|
|
979
1139
|
Underpost.repo.clean({ paths: [volume.volumeMountPath] });
|
|
980
1140
|
}
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
1141
|
+
// The node that physically receives the volume data. hostPath volumes are
|
|
1142
|
+
// node-local, so the data must land on the node where the pod will run, and
|
|
1143
|
+
// the PV is pinned there (nodeAffinity) so the scheduler co-locates the pod
|
|
1144
|
+
// with its volume — never mounting an empty DirectoryOrCreate on another node.
|
|
1145
|
+
let dataNode;
|
|
1146
|
+
if (clusterContext === 'kind') {
|
|
1147
|
+
const kindNode = options.nodeName || 'kind-worker';
|
|
1148
|
+
dataNode = kindNode;
|
|
1149
|
+
shellExec(`docker exec -i ${kindNode} bash -c "mkdir -p ${rootVolumeHostPath}"`);
|
|
1150
|
+
shellExec(`tar -C ${volume.volumeMountPath} -c . | docker cp - ${kindNode}:${rootVolumeHostPath}`);
|
|
988
1151
|
shellExec(
|
|
989
|
-
`docker exec -i
|
|
1152
|
+
`docker exec -i ${kindNode} bash -c "chown -R 1000:1000 ${rootVolumeHostPath}; chmod -R 755 ${rootVolumeHostPath}"`,
|
|
990
1153
|
);
|
|
991
1154
|
} else {
|
|
992
|
-
|
|
993
|
-
|
|
1155
|
+
const localHost = os.hostname();
|
|
1156
|
+
dataNode = options.nodeName || localHost;
|
|
1157
|
+
if (dataNode === localHost) {
|
|
1158
|
+
// Target node is the control plane / current host: write directly.
|
|
1159
|
+
if (!fs.existsSync(rootVolumeHostPath)) fs.mkdirSync(rootVolumeHostPath, { recursive: true });
|
|
1160
|
+
fs.copySync(volume.volumeMountPath, rootVolumeHostPath);
|
|
1161
|
+
} else {
|
|
1162
|
+
// Target node is remote: fs.copySync would only write the control-plane
|
|
1163
|
+
// filesystem, leaving the real node's hostPath empty. Ship the folder to
|
|
1164
|
+
// the node over SSH so the data exists where the pod is pinned.
|
|
1165
|
+
const nodeHost =
|
|
1166
|
+
shellExec(
|
|
1167
|
+
`kubectl get node ${dataNode} -o jsonpath='{.status.addresses[?(@.type=="InternalIP")].address}'`,
|
|
1168
|
+
{ stdout: true, silent: true, silentOnError: true },
|
|
1169
|
+
).trim() || dataNode;
|
|
1170
|
+
logger.info('Shipping volume to remote node over SSH', {
|
|
1171
|
+
node: dataNode,
|
|
1172
|
+
host: nodeHost,
|
|
1173
|
+
src: volume.volumeMountPath,
|
|
1174
|
+
dest: rootVolumeHostPath,
|
|
1175
|
+
});
|
|
1176
|
+
Underpost.ssh.copyDirToNode({
|
|
1177
|
+
host: nodeHost,
|
|
1178
|
+
localDir: volume.volumeMountPath,
|
|
1179
|
+
remoteDir: rootVolumeHostPath,
|
|
1180
|
+
...(options.sshKeyPath ? { keyPath: options.sshKeyPath } : {}),
|
|
1181
|
+
});
|
|
1182
|
+
}
|
|
994
1183
|
}
|
|
995
1184
|
shellExec(`kubectl delete pvc ${pvcId} -n ${namespace} --ignore-not-found`);
|
|
996
1185
|
shellExec(`kubectl delete pv ${pvId} --ignore-not-found`);
|
|
@@ -999,6 +1188,7 @@ ${Underpost.deploy.persistentVolumeFactory({
|
|
|
999
1188
|
hostPath: rootVolumeHostPath,
|
|
1000
1189
|
pvcId,
|
|
1001
1190
|
namespace,
|
|
1191
|
+
nodeName: dataNode,
|
|
1002
1192
|
})}
|
|
1003
1193
|
EOF
|
|
1004
1194
|
`);
|
|
@@ -1087,11 +1277,28 @@ ${secret ? ` readOnly: true\n` : ''}`;
|
|
|
1087
1277
|
* @param {string} options.hostPath - Host path for the persistent volume.
|
|
1088
1278
|
* @param {string} options.pvcId - Persistent volume claim ID.
|
|
1089
1279
|
* @param {string} [options.namespace='default'] - Kubernetes namespace for the PVC claimRef.
|
|
1280
|
+
* @param {string} [options.nodeName=''] - Node name to which the persistent volume is pinned (optional).
|
|
1090
1281
|
* @returns {string} - YAML configuration for the persistent volume and claim.
|
|
1091
1282
|
* @memberof UnderpostDeploy
|
|
1092
1283
|
*/
|
|
1093
|
-
persistentVolumeFactory({ hostPath, pvcId, namespace = 'default' }) {
|
|
1284
|
+
persistentVolumeFactory({ hostPath, pvcId, namespace = 'default', nodeName = '' }) {
|
|
1094
1285
|
const pvId = pvcId.replace(/^pvc-/, 'pv-');
|
|
1286
|
+
// hostPath volumes are node-local: deployVolume writes the content to the
|
|
1287
|
+
// filesystem of a single node. Without nodeAffinity the scheduler can place
|
|
1288
|
+
// the pod on a different node and mount an empty DirectoryOrCreate hostPath
|
|
1289
|
+
// (missing the materialized assets). Pin the PV to the node that holds the
|
|
1290
|
+
// data so the pod is always co-located with its volume.
|
|
1291
|
+
const nodeAffinity = nodeName
|
|
1292
|
+
? `
|
|
1293
|
+
nodeAffinity:
|
|
1294
|
+
required:
|
|
1295
|
+
nodeSelectorTerms:
|
|
1296
|
+
- matchExpressions:
|
|
1297
|
+
- key: kubernetes.io/hostname
|
|
1298
|
+
operator: In
|
|
1299
|
+
values:
|
|
1300
|
+
- ${nodeName}`
|
|
1301
|
+
: '';
|
|
1095
1302
|
return `apiVersion: v1
|
|
1096
1303
|
kind: PersistentVolume
|
|
1097
1304
|
metadata:
|
|
@@ -1102,7 +1309,7 @@ spec:
|
|
|
1102
1309
|
accessModes:
|
|
1103
1310
|
- ReadWriteOnce
|
|
1104
1311
|
persistentVolumeReclaimPolicy: Retain
|
|
1105
|
-
storageClassName: manual
|
|
1312
|
+
storageClassName: manual${nodeAffinity}
|
|
1106
1313
|
claimRef:
|
|
1107
1314
|
apiVersion: v1
|
|
1108
1315
|
kind: PersistentVolumeClaim
|
|
@@ -1136,183 +1343,10 @@ spec:
|
|
|
1136
1343
|
* @memberof UnderpostDeploy
|
|
1137
1344
|
*/
|
|
1138
1345
|
isValidTLSContext: ({ host, env, options }) =>
|
|
1139
|
-
env === 'production' &&
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
* Monitors the ready status of a deployment.
|
|
1144
|
-
*
|
|
1145
|
-
* Ready signal:
|
|
1146
|
-
* The orchestrator gate is the Kubernetes pod Ready condition. When the
|
|
1147
|
-
* container's `readinessProbe` succeeds, kubelet flips
|
|
1148
|
-
* `status.conditions[Ready]` to True and `checkDeploymentReadyStatus`
|
|
1149
|
-
* returns the pod in `readyPods`. This is the only required signal — see
|
|
1150
|
-
* `src/client/public/nexodev/docs/references/Deploy custom instance to K8S.md`.
|
|
1151
|
-
*
|
|
1152
|
-
* Container-status:
|
|
1153
|
-
* `underpost config get container-status` is read from each pod for both
|
|
1154
|
-
* the display column and as a second ready gate alongside the K8S Ready
|
|
1155
|
-
* condition. Both must be satisfied before the monitor exits:
|
|
1156
|
-
* 1. K8S readinessProbe (TCP socket) — ensures the port is bound.
|
|
1157
|
-
* 2. container-status == `<deploy>-<env>-running-deployment` — ensures
|
|
1158
|
-
* the application has completed its own startup sequence.
|
|
1159
|
-
* Early-abort on `error` container-status remains in effect.
|
|
1160
|
-
*
|
|
1161
|
-
* @param {string} deployId - Deployment ID for which the ready status is being monitored.
|
|
1162
|
-
* @param {string} env - Environment for which the ready status is being monitored.
|
|
1163
|
-
* @param {string} targetTraffic - Target traffic status for the deployment.
|
|
1164
|
-
* @param {Array<string>} ignorePods - List of pod names to ignore.
|
|
1165
|
-
* @param {string} [namespace='default'] - Kubernetes namespace for the deployment.
|
|
1166
|
-
* @returns {object} - Object containing the ready status of the deployment.
|
|
1167
|
-
* @memberof UnderpostDeploy
|
|
1168
|
-
*/
|
|
1169
|
-
async monitorReadyRunner(deployId, env, targetTraffic, ignorePods = [], namespace = 'default') {
|
|
1170
|
-
const delayMs = 1000;
|
|
1171
|
-
const maxIterations = 3000;
|
|
1172
|
-
const deploymentId = `${deployId}-${env}-${targetTraffic}`;
|
|
1173
|
-
const expectedContainerStatus = `${deployId}-${env}-running-deployment`;
|
|
1174
|
-
const tag = `[${deploymentId}]`;
|
|
1175
|
-
const containerStatusDefault = 'waiting for status';
|
|
1176
|
-
|
|
1177
|
-
logger.info('Deployment init', { deployId, env, targetTraffic, namespace });
|
|
1178
|
-
|
|
1179
|
-
// Per-pod cache of last-known container-status (persists across retries)
|
|
1180
|
-
const podStatusCache = new Map();
|
|
1181
|
-
|
|
1182
|
-
const readContainerStatus = (podName) => {
|
|
1183
|
-
try {
|
|
1184
|
-
const raw = shellExec(
|
|
1185
|
-
`sudo kubectl exec ${podName} -n ${namespace} -- sh -c 'underpost config get container-status --plain'`,
|
|
1186
|
-
{ silent: true, disableLog: true, stdout: true, silentOnError: true },
|
|
1187
|
-
);
|
|
1188
|
-
const val = raw ? raw.toString().trim() : '';
|
|
1189
|
-
return val && val !== 'undefined' ? val : containerStatusDefault;
|
|
1190
|
-
} catch (_) {
|
|
1191
|
-
// exec failed (e.g. pod not yet running) — preserve last known value
|
|
1192
|
-
return podStatusCache.get(podName) || containerStatusDefault;
|
|
1193
|
-
}
|
|
1194
|
-
};
|
|
1195
|
-
|
|
1196
|
-
for (let i = 0; i < maxIterations; i++) {
|
|
1197
|
-
const result = await Underpost.deploy.checkDeploymentReadyStatus(
|
|
1198
|
-
deployId,
|
|
1199
|
-
env,
|
|
1200
|
-
targetTraffic,
|
|
1201
|
-
ignorePods,
|
|
1202
|
-
namespace,
|
|
1203
|
-
);
|
|
1204
|
-
|
|
1205
|
-
const allPods = [...result.readyPods, ...result.notReadyPods];
|
|
1206
|
-
|
|
1207
|
-
// Update cache with latest status for each pod (informational + error gate)
|
|
1208
|
-
for (const pod of allPods) {
|
|
1209
|
-
if (!pod?.NAME) continue;
|
|
1210
|
-
const status = readContainerStatus(pod.NAME);
|
|
1211
|
-
if (status === 'error') throw new Error(`Pod ${pod.NAME} has error status`);
|
|
1212
|
-
podStatusCache.set(pod.NAME, status);
|
|
1213
|
-
}
|
|
1214
|
-
|
|
1215
|
-
const allPodsK8sReady = allPods.length > 0 && result.notReadyPods.length === 0;
|
|
1216
|
-
|
|
1217
|
-
const allPodsStatusReady =
|
|
1218
|
-
allPods.length > 0 && allPods.every((pod) => podStatusCache.get(pod.NAME) === expectedContainerStatus);
|
|
1219
|
-
|
|
1220
|
-
// Print snapshot for every pod — annotate when container-status hasn't caught
|
|
1221
|
-
// up to the K8S Ready condition yet.
|
|
1222
|
-
for (const pod of allPods) {
|
|
1223
|
-
const status = podStatusCache.get(pod.NAME) || containerStatusDefault;
|
|
1224
|
-
const podStatus = pod.STATUS || 'Unknown';
|
|
1225
|
-
const statusMatchesExpected = status === expectedContainerStatus;
|
|
1226
|
-
const statusDisplay = statusMatchesExpected ? status : `${status} (pending)`;
|
|
1227
|
-
|
|
1228
|
-
console.log(
|
|
1229
|
-
'Target pod:',
|
|
1230
|
-
pod.NAME[pod.NAME.includes('green') ? 'bgGreen' : 'bgBlue'].bold.black,
|
|
1231
|
-
'| Pod status:',
|
|
1232
|
-
podStatus.bold.yellow,
|
|
1233
|
-
'| Runtime status:',
|
|
1234
|
-
statusDisplay.bold.cyan,
|
|
1235
|
-
);
|
|
1236
|
-
}
|
|
1237
|
-
|
|
1238
|
-
// Both K8S readinessProbe AND container-status must be satisfied before
|
|
1239
|
-
// declaring the deployment ready. The TCP probe ensures the port is bound;
|
|
1240
|
-
// container-status == running-deployment ensures the application has
|
|
1241
|
-
// completed its own startup sequence so traffic is not switched prematurely.
|
|
1242
|
-
if (allPodsK8sReady && allPodsStatusReady) {
|
|
1243
|
-
logger.info(`${tag} | All pods Ready (K8S readinessProbe satisfied)`);
|
|
1244
|
-
return result;
|
|
1245
|
-
}
|
|
1246
|
-
|
|
1247
|
-
await timer(delayMs);
|
|
1248
|
-
|
|
1249
|
-
if ((i + 1) % 10 === 0) {
|
|
1250
|
-
logger.info(`${tag} | In progress... iteration ${i + 1}`);
|
|
1251
|
-
}
|
|
1252
|
-
}
|
|
1253
|
-
|
|
1254
|
-
logger.error(`${tag} | Deployment timeout after ${maxIterations} iterations`);
|
|
1255
|
-
throw new Error(
|
|
1256
|
-
`monitorReadyRunner timeout: ${deploymentId} did not become Ready within ${maxIterations}*${delayMs}ms`,
|
|
1257
|
-
);
|
|
1258
|
-
},
|
|
1259
|
-
|
|
1260
|
-
/**
|
|
1261
|
-
* Retrieves the currently loaded images in the Kubernetes cluster.
|
|
1262
|
-
* @param {string} [node='kind-worker'] - Node name to check for loaded images.
|
|
1263
|
-
* @param {object} options - Options for the image retrieval.
|
|
1264
|
-
* @param {boolean} options.spec - Whether to retrieve images from the pod specifications.
|
|
1265
|
-
* @param {string} options.namespace - Kubernetes namespace to filter pods.
|
|
1266
|
-
* @returns {Array<object>} - Array of objects containing pod names and their corresponding images.
|
|
1267
|
-
* @memberof UnderpostDeploy
|
|
1268
|
-
*/
|
|
1269
|
-
getCurrentLoadedImages(node = 'kind-worker', options = { spec: false, namespace: '' }) {
|
|
1270
|
-
if (options.spec) {
|
|
1271
|
-
const raw = shellExec(
|
|
1272
|
-
`kubectl get pods ${options.namespace ? `--namespace ${options.namespace}` : `--all-namespaces`} -o=jsonpath='{range .items[*]}{"\\n"}{.metadata.namespace}{"/"}{.metadata.name}{":\\t"}{range .spec.containers[*]}{.image}{", "}{end}{end}'`,
|
|
1273
|
-
{
|
|
1274
|
-
stdout: true,
|
|
1275
|
-
silent: true,
|
|
1276
|
-
},
|
|
1277
|
-
);
|
|
1278
|
-
return raw
|
|
1279
|
-
.split(`\n`)
|
|
1280
|
-
.map((lines) => ({
|
|
1281
|
-
pod: lines.split('\t')[0].replaceAll(':', '').trim(),
|
|
1282
|
-
image: lines.split('\t')[1] ? lines.split('\t')[1].replaceAll(',', '').trim() : null,
|
|
1283
|
-
}))
|
|
1284
|
-
.filter((o) => o.image);
|
|
1285
|
-
}
|
|
1286
|
-
const raw = shellExec(node === 'kind-worker' ? `docker exec -i ${node} crictl images` : `crictl images`, {
|
|
1287
|
-
stdout: true,
|
|
1288
|
-
silent: true,
|
|
1289
|
-
});
|
|
1290
|
-
|
|
1291
|
-
const heads = raw
|
|
1292
|
-
.split(`\n`)[0]
|
|
1293
|
-
.split(' ')
|
|
1294
|
-
.filter((_r) => _r.trim());
|
|
1295
|
-
|
|
1296
|
-
const pods = raw
|
|
1297
|
-
.split(`\n`)
|
|
1298
|
-
.filter((r) => !r.match('IMAGE'))
|
|
1299
|
-
.map((r) => r.split(' ').filter((_r) => _r.trim()));
|
|
1300
|
-
|
|
1301
|
-
const result = [];
|
|
1302
|
-
|
|
1303
|
-
for (const row of pods) {
|
|
1304
|
-
if (row.length === 0) continue;
|
|
1305
|
-
const pod = {};
|
|
1306
|
-
let index = -1;
|
|
1307
|
-
for (const head of heads) {
|
|
1308
|
-
if (head in pod) continue;
|
|
1309
|
-
index++;
|
|
1310
|
-
pod[head] = row[index];
|
|
1311
|
-
}
|
|
1312
|
-
result.push(pod);
|
|
1313
|
-
}
|
|
1314
|
-
return result;
|
|
1315
|
-
},
|
|
1346
|
+
(env === 'production' &&
|
|
1347
|
+
options.cert === true &&
|
|
1348
|
+
(!options.certHosts || options.certHosts.split(',').includes(host))) ||
|
|
1349
|
+
options.selfSigned === true,
|
|
1316
1350
|
|
|
1317
1351
|
/**
|
|
1318
1352
|
* Predefined resource templates for Kubernetes deployments.
|