@underpostnet/cyberia 3.3.76 → 3.3.77
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/coverall.cyberia.ci.yml +5 -0
- package/.github/workflows/docker-image.cyberia-client.ci.yml +1 -1
- package/.github/workflows/docker-image.cyberia-client.dev.ci.yml +1 -1
- package/.github/workflows/docker-image.cyberia-server.ci.yml +1 -1
- package/.github/workflows/docker-image.cyberia-server.dev.ci.yml +1 -1
- package/.github/workflows/docker-image.engine-cyberia.ci.yml +1 -1
- package/.github/workflows/docker-image.engine-cyberia.dev.ci.yml +1 -1
- package/.github/workflows/engine-cyberia.ci.yml +1 -1
- package/CHANGELOG.md +35 -122
- package/CLI-HELP.md +2 -2
- package/Dockerfile +1 -1
- package/Dockerfile.dev +1 -1
- package/Dockerfile.test +1 -1
- package/README.md +1 -1
- package/bin/build.js +65 -10
- package/bin/cyberia.js +4 -4
- package/bin/index.js +4 -4
- package/conf.js +0 -1
- package/deploy/lib/config.sh +1 -1
- package/deploy/lib/host.sh +1 -1
- package/deployment.yaml +1 -1
- package/docker-compose.yml +7 -7
- package/docs/coverage/cyberia/api/cyberia-instance/cyberia-fallback-capture.js.html +1591 -0
- package/docs/coverage/cyberia/api/cyberia-instance/index.html +116 -0
- package/docs/coverage/cyberia/api/cyberia-server-defaults/cyberia-server-defaults.js.html +4138 -0
- package/docs/coverage/cyberia/api/cyberia-server-defaults/index.html +116 -0
- package/docs/coverage/cyberia/api/object-layer/index.html +116 -0
- package/docs/coverage/cyberia/api/object-layer/object-layer.model.js.html +1393 -0
- package/docs/coverage/cyberia/base.css +362 -0
- package/docs/coverage/cyberia/block-navigation.js +82 -0
- package/docs/coverage/cyberia/favicon.png +0 -0
- package/docs/coverage/cyberia/index.html +161 -0
- package/docs/coverage/cyberia/prettify.css +101 -0
- package/docs/coverage/cyberia/prettify.js +937 -0
- package/docs/coverage/cyberia/projects/cyberia/atlas-sprite-sheet-generator.js.html +1315 -0
- package/docs/coverage/cyberia/projects/cyberia/index.html +176 -0
- package/docs/coverage/cyberia/projects/cyberia/instance-backup.js.html +715 -0
- package/docs/coverage/cyberia/projects/cyberia/instance-data.js.html +2182 -0
- package/docs/coverage/cyberia/projects/cyberia/shape-generator.js.html +3265 -0
- package/docs/coverage/cyberia/projects/cyberia/stat-balance.js.html +328 -0
- package/docs/coverage/cyberia/sort-arrow-sprite.png +0 -0
- package/docs/coverage/cyberia/sorter.js +205 -0
- package/hardhat/package-lock.json +2 -2
- package/hardhat/package.json +1 -1
- package/manifests/cronjobs/dd-cron/dd-cron-backup.yaml +1 -1
- package/manifests/cronjobs/dd-cron/dd-cron-vultr.yaml +1 -1
- package/manifests/deployment/dd-cyberia-development/deployment.yaml +1 -1
- package/manifests/deployment/dd-default-development/deployment.yaml +2 -2
- package/package.json +2 -2
- package/scripts/test-monitor.sh +1 -1
- package/src/cli/cluster.js +73 -18
- package/src/cli/deploy.js +211 -30
- package/src/cli/docker-compose.js +1 -1
- package/src/cli/release.js +3 -1
- package/src/cli/repository.js +7 -9
- package/src/cli/run.js +10 -3
- package/src/client/components/core/Badge.js +0 -1
- package/src/client/components/core/CssCore.js +8 -1
- package/src/client/components/core/Modal.js +29 -30
- package/src/client/components/core/ToolTip.js +15 -0
- package/src/client/components/cryptokoyn/AppShellCryptokoyn.js +0 -2
- package/src/client/components/cyberia-portal/RouterCyberiaPortal.js +0 -4
- package/src/client/components/default/AppShellDefault.js +1 -4
- package/src/client/components/itemledger/AppShellItemledger.js +0 -2
- package/src/client/components/underpost/AppShellUnderpost.js +1 -1
- package/src/client/public/cyberia-docs/CYBERIA.md +1 -1
- package/src/client/public/cyberia-docs/ROADMAP.md +1 -1
- package/src/client/public/cyberia-docs/WHITE-PAPER.md +1 -1
- package/src/client/ssr/views/Cyberia404.js +3 -3
- package/src/index.js +1 -1
- package/src/runtime/engine-cyberia/Dockerfile +1 -1
- package/src/runtime/engine-cyberia/Dockerfile.dev +1 -1
- package/src/runtime/engine-cyberia/Dockerfile.test +1 -1
- package/src/runtime/engine-cyberia/docker-compose.yml +7 -7
- package/src/server/network/underpost-gateway.js +113 -14
- package/src/server/runtime/conf.js +36 -7
- package/src/server/runtime/start.js +11 -2
- package/test/integration/infra/4-ingress/underpost-ingress.test.js +1 -1
- package/test/unit/catalog.test.js +13 -2
- package/test/unit/conf-resolution.test.js +41 -7
- package/test/unit/cyberia/instance-data.test.js +314 -0
- package/test/unit/deploy-legacy-gateway-sweep.test.js +63 -0
- package/test/unit/gateway-pod-fetch.test.js +95 -0
- package/test/unit/letsencrypt-issuer.test.js +101 -0
- package/test/unit/release-bump.test.js +3 -1
- package/test/unit/start-options.test.js +12 -0
|
@@ -26,8 +26,6 @@ class AppShellCryptokoyn {
|
|
|
26
26
|
const RouterInstance = RouterCryptokoyn.instance();
|
|
27
27
|
|
|
28
28
|
const { barConfig } = await Themes[Css.currentTheme]();
|
|
29
|
-
|
|
30
|
-
const badgeNotificationMenuStyle = { top: '-33px', left: '24px' };
|
|
31
29
|
const barMode = undefined; // 'top-bottom-bar';
|
|
32
30
|
await Modal.instance({
|
|
33
31
|
id: 'modal-menu',
|
|
@@ -56,10 +56,6 @@ class RouterCyberiaPortal {
|
|
|
56
56
|
title: 'cyberia-entity-engine',
|
|
57
57
|
render: () => s(`.main-btn-cyberia-entity-engine`).click(),
|
|
58
58
|
},
|
|
59
|
-
'/fallback-world-engine': {
|
|
60
|
-
title: 'fallback-world-engine',
|
|
61
|
-
render: () => s(`.main-btn-fallback-world-engine`).click(),
|
|
62
|
-
},
|
|
63
59
|
'/instance-selection': {
|
|
64
60
|
title: 'instance-selection',
|
|
65
61
|
render: () => s(`.main-btn-instance-selection`).click(),
|
|
@@ -31,8 +31,6 @@ class AppShellDefault {
|
|
|
31
31
|
const RouterInstance = RouterDefault.instance();
|
|
32
32
|
|
|
33
33
|
const { barConfig } = await Themes[Css.currentTheme]();
|
|
34
|
-
|
|
35
|
-
const badgeNotificationMenuStyle = { top: '-33px', left: '24px' };
|
|
36
34
|
const barMode = undefined; // 'top-bottom-bar';
|
|
37
35
|
await Modal.instance({
|
|
38
36
|
id: 'modal-menu',
|
|
@@ -197,8 +195,7 @@ class AppShellDefault {
|
|
|
197
195
|
${await Badge.instance({
|
|
198
196
|
id: 'main-btn-chat',
|
|
199
197
|
type: 'circle-red',
|
|
200
|
-
|
|
201
|
-
classList: 'hide',
|
|
198
|
+
classList: 'hide badge-notification-menu',
|
|
202
199
|
})}`,
|
|
203
200
|
attrs: `data-id="chat"`,
|
|
204
201
|
tabHref: `${getProxyPath()}chat`,
|
|
@@ -27,8 +27,6 @@ class AppShellItemledger {
|
|
|
27
27
|
const RouterInstance = RouterItemledger.instance();
|
|
28
28
|
|
|
29
29
|
const { barConfig } = await Themes[Css.currentTheme]();
|
|
30
|
-
|
|
31
|
-
const badgeNotificationMenuStyle = { top: '-33px', left: '24px' };
|
|
32
30
|
const barMode = undefined; // 'top-bottom-bar';
|
|
33
31
|
await Modal.instance({
|
|
34
32
|
id: 'modal-menu',
|
|
@@ -20,7 +20,7 @@ _Stackable Rendering Layers as a Unified Tokenized Reality_
|
|
|
20
20
|
<!-- CI/CD and health -->
|
|
21
21
|
<div align="center">
|
|
22
22
|
|
|
23
|
-
[](https://github.com/underpostnet/engine-cyberia/actions/workflows/docker-image.engine-cyberia.ci.yml) [](https://github.com/underpostnet/engine-cyberia/actions/workflows/coverall.cyberia.ci.yml) [](https://coveralls.io/github/underpostnet/engine-cyberia?branch=main) [](https://github.com/underpostnet/engine-cyberia/actions/workflows/docker-image.engine-cyberia.ci.yml) [](https://github.com/underpostnet/engine-cyberia/actions/workflows/coverall.cyberia.ci.yml) [](https://coveralls.io/github/underpostnet/engine-cyberia?branch=main) [](https://socket.dev/npm/package/cyberia/overview/3.3.77)
|
|
24
24
|
|
|
25
25
|
</div>
|
|
26
26
|
|
|
@@ -155,7 +155,7 @@ const main = () => {
|
|
|
155
155
|
text-align: center;
|
|
156
156
|
padding: 24px;
|
|
157
157
|
}
|
|
158
|
-
.badge {
|
|
158
|
+
.status-badge {
|
|
159
159
|
display: inline-flex;
|
|
160
160
|
align-items: center;
|
|
161
161
|
gap: 8px;
|
|
@@ -168,7 +168,7 @@ const main = () => {
|
|
|
168
168
|
margin-bottom: 26px;
|
|
169
169
|
box-shadow: 0 0 12px rgba(34, 211, 238, 0.35);
|
|
170
170
|
}
|
|
171
|
-
.badge .dot {
|
|
171
|
+
.status-badge .dot {
|
|
172
172
|
width: 8px;
|
|
173
173
|
height: 8px;
|
|
174
174
|
background: var(--green);
|
|
@@ -296,7 +296,7 @@ const main = () => {
|
|
|
296
296
|
</style>
|
|
297
297
|
<div class="grid"></div>
|
|
298
298
|
<main class="wrap">
|
|
299
|
-
<span class="badge"><span class="dot"></span> INSTANCE ${instanceLabel()}</span>
|
|
299
|
+
<span class="status-badge"><span class="dot"></span> INSTANCE ${instanceLabel()}</span>
|
|
300
300
|
<div class="code" data-text="404">404</div>
|
|
301
301
|
<div class="headline">SIGNAL LOST</div>
|
|
302
302
|
<p class="sub">
|
package/src/index.js
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
# Stage 1 — builder: clone the private deploy, build it, then scrub secrets.
|
|
7
7
|
# ---------------------------------------------------------------------------
|
|
8
8
|
FROM rockylinux/rockylinux:9 AS builder
|
|
9
|
-
ARG UNDERPOST_VERSION=3.3.
|
|
9
|
+
ARG UNDERPOST_VERSION=3.3.77
|
|
10
10
|
# Pin Node to an exact patch: dnf's nodejs:24 module lags (24.14.1) while
|
|
11
11
|
# underpost's dependencies require >=24.15.0, so install the official binary.
|
|
12
12
|
ARG NODE_VERSION=24.15.0
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
# Stage 1 — builder: clone the private deploy, build it, then scrub secrets.
|
|
7
7
|
# ---------------------------------------------------------------------------
|
|
8
8
|
FROM rockylinux/rockylinux:9 AS builder
|
|
9
|
-
ARG UNDERPOST_VERSION=3.3.
|
|
9
|
+
ARG UNDERPOST_VERSION=3.3.77
|
|
10
10
|
# Pin Node to an exact patch: dnf's nodejs:24 module lags (24.14.1) while
|
|
11
11
|
# underpost's dependencies require >=24.15.0, so install the official binary.
|
|
12
12
|
ARG NODE_VERSION=24.15.0
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
# Stage 1 — builder: clone the private deploy, build it, then scrub secrets.
|
|
7
7
|
# ---------------------------------------------------------------------------
|
|
8
8
|
FROM rockylinux/rockylinux:9 AS builder
|
|
9
|
-
ARG UNDERPOST_VERSION=3.3.
|
|
9
|
+
ARG UNDERPOST_VERSION=3.3.77
|
|
10
10
|
# Pin Node to an exact patch: dnf's nodejs:24 module lags (24.14.1) while
|
|
11
11
|
# underpost's dependencies require >=24.15.0, so install the official binary.
|
|
12
12
|
ARG NODE_VERSION=24.15.0
|
|
@@ -135,7 +135,7 @@ services:
|
|
|
135
135
|
# server starts but rejects connections until seeded). Serves REST/WS on 4005
|
|
136
136
|
# and the engine gRPC data service on 50051.
|
|
137
137
|
engine-cyberia-runtime:
|
|
138
|
-
image: ${ENGINE_CYBERIA_IMAGE:-underpost/engine-cyberia-dev}:${ENGINE_CYBERIA_TAG:-v3.3.
|
|
138
|
+
image: ${ENGINE_CYBERIA_IMAGE:-underpost/engine-cyberia-dev}:${ENGINE_CYBERIA_TAG:-v3.3.77}
|
|
139
139
|
container_name: dd-cyberia-engine
|
|
140
140
|
command:
|
|
141
141
|
- /bin/sh
|
|
@@ -190,7 +190,7 @@ services:
|
|
|
190
190
|
# gateway on host port 8081, so one port can route every instance path.
|
|
191
191
|
|
|
192
192
|
cyberia-server-runtime:
|
|
193
|
-
image: ${CYBERIA_SERVER_IMAGE:-underpost/cyberia-server-dev}:${CYBERIA_SERVER_TAG:-v3.3.
|
|
193
|
+
image: ${CYBERIA_SERVER_IMAGE:-underpost/cyberia-server-dev}:${CYBERIA_SERVER_TAG:-v3.3.77}
|
|
194
194
|
container_name: dd-cyberia-server
|
|
195
195
|
environment:
|
|
196
196
|
INSTANCE_CODE: amethyst-strata-expansion
|
|
@@ -228,7 +228,7 @@ services:
|
|
|
228
228
|
restart: unless-stopped
|
|
229
229
|
|
|
230
230
|
cyberia-server-forest:
|
|
231
|
-
image: ${CYBERIA_SERVER_IMAGE:-underpost/cyberia-server-dev}:${CYBERIA_SERVER_TAG:-v3.3.
|
|
231
|
+
image: ${CYBERIA_SERVER_IMAGE:-underpost/cyberia-server-dev}:${CYBERIA_SERVER_TAG:-v3.3.77}
|
|
232
232
|
container_name: dd-cyberia-server-forest
|
|
233
233
|
environment:
|
|
234
234
|
INSTANCE_CODE: FOREST
|
|
@@ -266,7 +266,7 @@ services:
|
|
|
266
266
|
restart: unless-stopped
|
|
267
267
|
|
|
268
268
|
cyberia-server-test:
|
|
269
|
-
image: ${CYBERIA_SERVER_IMAGE:-underpost/cyberia-server-dev}:${CYBERIA_SERVER_TAG:-v3.3.
|
|
269
|
+
image: ${CYBERIA_SERVER_IMAGE:-underpost/cyberia-server-dev}:${CYBERIA_SERVER_TAG:-v3.3.77}
|
|
270
270
|
container_name: dd-cyberia-server-test
|
|
271
271
|
environment:
|
|
272
272
|
INSTANCE_CODE: TEST
|
|
@@ -315,7 +315,7 @@ services:
|
|
|
315
315
|
# cyberia-server-test -> cyberia-client-test
|
|
316
316
|
|
|
317
317
|
cyberia-client-runtime:
|
|
318
|
-
image: ${CYBERIA_CLIENT_IMAGE:-underpost/cyberia-client-dev}:${CYBERIA_CLIENT_TAG:-v3.3.
|
|
318
|
+
image: ${CYBERIA_CLIENT_IMAGE:-underpost/cyberia-client-dev}:${CYBERIA_CLIENT_TAG:-v3.3.77}
|
|
319
319
|
container_name: dd-cyberia-client
|
|
320
320
|
environment:
|
|
321
321
|
CYBERIA_PORT: '8081'
|
|
@@ -340,7 +340,7 @@ services:
|
|
|
340
340
|
restart: unless-stopped
|
|
341
341
|
|
|
342
342
|
cyberia-client-forest:
|
|
343
|
-
image: ${CYBERIA_CLIENT_IMAGE:-underpost/cyberia-client-dev}:${CYBERIA_CLIENT_TAG:-v3.3.
|
|
343
|
+
image: ${CYBERIA_CLIENT_IMAGE:-underpost/cyberia-client-dev}:${CYBERIA_CLIENT_TAG:-v3.3.77}
|
|
344
344
|
container_name: dd-cyberia-client-forest
|
|
345
345
|
environment:
|
|
346
346
|
CYBERIA_PORT: '8081'
|
|
@@ -365,7 +365,7 @@ services:
|
|
|
365
365
|
restart: unless-stopped
|
|
366
366
|
|
|
367
367
|
cyberia-client-test:
|
|
368
|
-
image: ${CYBERIA_CLIENT_IMAGE:-underpost/cyberia-client-dev}:${CYBERIA_CLIENT_TAG:-v3.3.
|
|
368
|
+
image: ${CYBERIA_CLIENT_IMAGE:-underpost/cyberia-client-dev}:${CYBERIA_CLIENT_TAG:-v3.3.77}
|
|
369
369
|
container_name: dd-cyberia-client-test
|
|
370
370
|
environment:
|
|
371
371
|
CYBERIA_PORT: '8081'
|
|
@@ -957,6 +957,104 @@ const placeInstanceStaticAssets = ({ instances, options, label }) => {
|
|
|
957
957
|
return assertStaticAssets({ records, hostRoot, label });
|
|
958
958
|
};
|
|
959
959
|
|
|
960
|
+
/**
|
|
961
|
+
* @method parseRawHttpResponse
|
|
962
|
+
* @description Splits a raw HTTP/1.x response into its status code and body. The body
|
|
963
|
+
* is everything after the first blank line, byte for byte, so it can be hashed against
|
|
964
|
+
* the document on disk.
|
|
965
|
+
* @param {string} raw - Response as read off the socket.
|
|
966
|
+
* @returns {{status: string, body: string}} Status code (empty when no status line) and body.
|
|
967
|
+
* @memberof UnderpostGateway
|
|
968
|
+
*/
|
|
969
|
+
const parseRawHttpResponse = (raw = '') => {
|
|
970
|
+
const status = /^HTTP\/[0-9.]+\s+([0-9]{3})/.exec(raw)?.[1] || '';
|
|
971
|
+
if (!status) return { status: '', body: '' };
|
|
972
|
+
const separator = raw.search(/\r?\n\r?\n/);
|
|
973
|
+
if (separator < 0) return { status, body: '' };
|
|
974
|
+
return { status, body: raw.slice(separator).replace(/^\r?\n\r?\n/, '') };
|
|
975
|
+
};
|
|
976
|
+
|
|
977
|
+
/**
|
|
978
|
+
* @method gatewayPodFetch
|
|
979
|
+
* @description One request against the gateway from inside its own pod, returning the
|
|
980
|
+
* status and the body even when the status is an error.
|
|
981
|
+
*
|
|
982
|
+
* Written on the socket with `nc` rather than fetched with `wget`: the image ships
|
|
983
|
+
* busybox, whose `wget` discards the body of any non-2xx response and exits — so a
|
|
984
|
+
* fallback document, which is by definition delivered with an upstream-failure status,
|
|
985
|
+
* could never be read through it. The probe that did so failed every attempt on a
|
|
986
|
+
* correct edge and looked like a hang. HTTP/1.0 keeps the reply unchunked, so the body
|
|
987
|
+
* is the document verbatim. Where `nc` is absent the `wget` pair still yields the status.
|
|
988
|
+
* @param {object} params
|
|
989
|
+
* @param {string} params.namespace - Namespace the gateway runs in.
|
|
990
|
+
* @param {string} params.host - Host header to present.
|
|
991
|
+
* @param {string} [params.path] - Request path.
|
|
992
|
+
* @returns {{status: string, body: string}} What the gateway answered.
|
|
993
|
+
* @memberof UnderpostGateway
|
|
994
|
+
*/
|
|
995
|
+
const gatewayPodFetch = ({ namespace, host, path = '/' }) => {
|
|
996
|
+
const exec = (script) =>
|
|
997
|
+
`${
|
|
998
|
+
shellExec(`kubectl exec -n ${namespace} deploy/${UNDERPOST_GATEWAY.name} -- sh -c "${script}"`, {
|
|
999
|
+
stdout: true,
|
|
1000
|
+
silent: true,
|
|
1001
|
+
silentOnError: true,
|
|
1002
|
+
disableLog: true,
|
|
1003
|
+
}) ?? ''
|
|
1004
|
+
}`;
|
|
1005
|
+
const request = path || '/';
|
|
1006
|
+
const raw = exec(
|
|
1007
|
+
`printf 'GET ${request} HTTP/1.0\\r\\nHost: ${host}\\r\\n\\r\\n' | nc -w 10 127.0.0.1 ${UNDERPOST_GATEWAY.port} 2>/dev/null || true`,
|
|
1008
|
+
);
|
|
1009
|
+
const parsed = parseRawHttpResponse(raw);
|
|
1010
|
+
if (parsed.status) return parsed;
|
|
1011
|
+
const url = `http://127.0.0.1${request}`;
|
|
1012
|
+
const headers = exec(`wget -S -O /dev/null -T 10 --header 'Host: ${host}' ${url} 2>&1 || true`);
|
|
1013
|
+
const status = [...headers.matchAll(/HTTP\/[0-9.]+\s+([0-9]{3})/g)].pop()?.[1] || '';
|
|
1014
|
+
const body = exec(`wget -q -O - -T 10 --header 'Host: ${host}' ${url} 2>/dev/null || true`);
|
|
1015
|
+
return { status, body };
|
|
1016
|
+
};
|
|
1017
|
+
|
|
1018
|
+
/**
|
|
1019
|
+
* @method gatewayFetch
|
|
1020
|
+
* @description One request against the gateway, returning the status and the body
|
|
1021
|
+
* even when the status is an error — the shape every fallback probe asserts on.
|
|
1022
|
+
*
|
|
1023
|
+
* Sent from this host with `curl` to the gateway Service first: the sync runs on the
|
|
1024
|
+
* node that holds the gateway's volume, so the ClusterIP is reachable, and curl keeps
|
|
1025
|
+
* the body of an error response. Only when that yields no status line does it fall back
|
|
1026
|
+
* to {@link UnderpostGateway.gatewayPodFetch}, which needs nothing on the host at all.
|
|
1027
|
+
* @param {object} params
|
|
1028
|
+
* @param {string} params.namespace - Namespace the gateway runs in.
|
|
1029
|
+
* @param {string} params.host - Host header to present.
|
|
1030
|
+
* @param {string} [params.path] - Request path.
|
|
1031
|
+
* @returns {{status: string, body: string, via: string}} What the gateway answered, and which way.
|
|
1032
|
+
* @memberof UnderpostGateway
|
|
1033
|
+
*/
|
|
1034
|
+
const gatewayFetch = ({ namespace, host, path = '/' }) => {
|
|
1035
|
+
const clusterIp = `${
|
|
1036
|
+
shellExec(`kubectl get svc ${UNDERPOST_GATEWAY.serviceName} -n ${namespace} -o jsonpath='{.spec.clusterIP}'`, {
|
|
1037
|
+
stdout: true,
|
|
1038
|
+
silent: true,
|
|
1039
|
+
silentOnError: true,
|
|
1040
|
+
disableLog: true,
|
|
1041
|
+
}) ?? ''
|
|
1042
|
+
}`.trim();
|
|
1043
|
+
if (/^[0-9a-f.:]+$/i.test(clusterIp)) {
|
|
1044
|
+
const raw = `${
|
|
1045
|
+
shellExec(
|
|
1046
|
+
`curl -sS -i --noproxy '*' --max-time 10 -H 'Host: ${host}' http://${clusterIp}:${UNDERPOST_GATEWAY.port}${
|
|
1047
|
+
path || '/'
|
|
1048
|
+
} 2>/dev/null || true`,
|
|
1049
|
+
{ stdout: true, silent: true, silentOnError: true, disableLog: true },
|
|
1050
|
+
) ?? ''
|
|
1051
|
+
}`;
|
|
1052
|
+
const parsed = parseRawHttpResponse(raw);
|
|
1053
|
+
if (parsed.status) return { ...parsed, via: 'host' };
|
|
1054
|
+
}
|
|
1055
|
+
return { ...gatewayPodFetch({ namespace, host, path }), via: 'pod' };
|
|
1056
|
+
};
|
|
1057
|
+
|
|
960
1058
|
/**
|
|
961
1059
|
* @method gatewayFallbackProbeRunner
|
|
962
1060
|
* @description Proves the edge answers each configured fallback with the exact
|
|
@@ -1018,26 +1116,24 @@ const gatewayFallbackProbeRunner = async ({ checks, options, label, gatewayStatu
|
|
|
1018
1116
|
silent: true,
|
|
1019
1117
|
silentOnError: true,
|
|
1020
1118
|
}).trim();
|
|
1021
|
-
} else {
|
|
1022
|
-
const request = `http://127.0.0.1${check.path || '/'}`;
|
|
1023
|
-
body = shellExec(
|
|
1024
|
-
`kubectl exec -n ${namespace} deploy/${UNDERPOST_GATEWAY.name} -- sh -c ` +
|
|
1025
|
-
`"wget -q -O - -T 10 --header 'Host: ${check.host}' ${request} 2>/dev/null || true"`,
|
|
1026
|
-
{ stdout: true, silent: true, silentOnError: true },
|
|
1027
|
-
);
|
|
1028
|
-
const headers = shellExec(
|
|
1029
|
-
`kubectl exec -n ${namespace} deploy/${UNDERPOST_GATEWAY.name} -- sh -c ` +
|
|
1030
|
-
`"wget -S -O /dev/null -T 10 --header 'Host: ${check.host}' ${request} 2>&1 || true"`,
|
|
1031
|
-
{ stdout: true, silent: true, silentOnError: true },
|
|
1032
|
-
);
|
|
1033
|
-
status = [...headers.matchAll(/HTTP\/[0-9.]+\s+([0-9]{3})/g)].pop()?.[1] || '';
|
|
1034
|
-
}
|
|
1119
|
+
} else ({ status, body } = gatewayFetch({ namespace, host: check.host, path: check.path }));
|
|
1035
1120
|
actualHash = crypto
|
|
1036
1121
|
.createHash('sha256')
|
|
1037
1122
|
.update(body || '')
|
|
1038
1123
|
.digest('hex');
|
|
1039
1124
|
passed = /^50[234]$/.test(status) && !!expectedHash && actualHash === expectedHash;
|
|
1040
1125
|
if (passed) break;
|
|
1126
|
+
// The first miss is reported as it happens: a probe that polls for minutes with
|
|
1127
|
+
// nothing on the console reads as a hang, and what it saw is the whole diagnosis.
|
|
1128
|
+
if (attempts === 1)
|
|
1129
|
+
logger.warn(`[${label}] Fallback probe not yet satisfied; polling`, {
|
|
1130
|
+
host: check.host,
|
|
1131
|
+
path: check.path || '/',
|
|
1132
|
+
status: status || '(none)',
|
|
1133
|
+
bodyBytes: Buffer.byteLength(body || ''),
|
|
1134
|
+
bodyMatchesConfiguredAsset: actualHash === expectedHash,
|
|
1135
|
+
expectedAsset: expectedHash ? check.assetPath : '(missing on disk)',
|
|
1136
|
+
});
|
|
1041
1137
|
if (attempts < 30) await timer(2000);
|
|
1042
1138
|
}
|
|
1043
1139
|
const result = {
|
|
@@ -1059,6 +1155,8 @@ export {
|
|
|
1059
1155
|
UNDERPOST_GATEWAY,
|
|
1060
1156
|
assertStaticAssets,
|
|
1061
1157
|
gatewayFallbackProbeRunner,
|
|
1158
|
+
gatewayFetch,
|
|
1159
|
+
gatewayPodFetch,
|
|
1062
1160
|
gatewayStaticAssetExists,
|
|
1063
1161
|
hostInstanceRegistryPathFactory,
|
|
1064
1162
|
hostServerConfFactory,
|
|
@@ -1071,6 +1169,7 @@ export {
|
|
|
1071
1169
|
kubernetesUpstreamFactory,
|
|
1072
1170
|
underpostGatewayManifestsFactory,
|
|
1073
1171
|
nginxConfFactory,
|
|
1172
|
+
parseRawHttpResponse,
|
|
1074
1173
|
seedDefaultStatusPage,
|
|
1075
1174
|
staticLocationFactory,
|
|
1076
1175
|
staticPathSegmentFactory,
|
|
@@ -3001,14 +3001,41 @@ const syncPrivateConf = (deployId, extraPaths = []) => {
|
|
|
3001
3001
|
};
|
|
3002
3002
|
|
|
3003
3003
|
/**
|
|
3004
|
-
*
|
|
3005
|
-
*
|
|
3006
|
-
*
|
|
3007
|
-
*
|
|
3008
|
-
*
|
|
3004
|
+
* Mirrors a deploy's public template sources from the engine working tree to the source
|
|
3005
|
+
* repo they came from. The engine ignores every one of these paths, so an edit made there
|
|
3006
|
+
* never reaches a diff; the source repo is where it is reviewed and committed. Each path
|
|
3007
|
+
* the engine holds is replaced wholesale there (removed, then copied) so the source repo's
|
|
3008
|
+
* `git status` reads as exactly the engine tree's state, deletions included. The engine
|
|
3009
|
+
* copies stay in place: this is a copy, not a move. A path the engine tree does not hold
|
|
3010
|
+
* is left untouched in the source repo rather than deleted.
|
|
3011
|
+
*
|
|
3012
|
+
* @method syncDeployIdSourcesBack
|
|
3013
|
+
* @param {Array<[string, string]>} [sourceMoves=[]] - Public `[src, dest]` pairs.
|
|
3014
|
+
* @returns {string[]} The source paths that were mirrored.
|
|
3015
|
+
* @memberof ServerConfBuilder
|
|
3016
|
+
*/
|
|
3017
|
+
const syncDeployIdSourcesBack = (sourceMoves = []) => {
|
|
3018
|
+
const mirrored = [];
|
|
3019
|
+
for (const [src, dest] of sourceMoves) {
|
|
3020
|
+
if (!fs.existsSync(dest)) continue;
|
|
3021
|
+
fs.removeSync(src);
|
|
3022
|
+
fs.copySync(dest, src);
|
|
3023
|
+
mirrored.push(src);
|
|
3024
|
+
}
|
|
3025
|
+
return mirrored;
|
|
3026
|
+
};
|
|
3027
|
+
|
|
3028
|
+
/**
|
|
3029
|
+
* Brings a deploy's public template sources into line ahead of the build copy step. The
|
|
3030
|
+
* engine working tree is the working copy and the source repo its git view, so a path the
|
|
3031
|
+
* engine already holds is mirrored out to the repo ({@link syncDeployIdSourcesBack}) and
|
|
3032
|
+
* only a path the engine lacks is copied in. Nothing is moved: the repo's working tree
|
|
3033
|
+
* keeps every file, so a build never leaves it reading as wholesale deletions, and an
|
|
3034
|
+
* engine-side edit is never overwritten by the repo's copy. Safe to rerun: each step is
|
|
3035
|
+
* guarded by `existsSync`.
|
|
3009
3036
|
*
|
|
3010
3037
|
* @method syncDeployIdSources
|
|
3011
|
-
* @param {Array<[string, string]>} [sourceMoves=[]] - Public `[src, dest]`
|
|
3038
|
+
* @param {Array<[string, string]>} [sourceMoves=[]] - Public `[src, dest]` pairs.
|
|
3012
3039
|
* @returns {boolean} `true` when any sources were declared, else `false`.
|
|
3013
3040
|
* @memberof ServerConfBuilder
|
|
3014
3041
|
*/
|
|
@@ -3016,7 +3043,8 @@ const syncDeployIdSources = (sourceMoves = []) => {
|
|
|
3016
3043
|
if (!sourceMoves.length) return false;
|
|
3017
3044
|
for (const dir of ['src/api', 'src/client/components', 'src/client/public', 'src/client/services'])
|
|
3018
3045
|
fs.mkdirSync(dir, { recursive: true });
|
|
3019
|
-
|
|
3046
|
+
syncDeployIdSourcesBack(sourceMoves);
|
|
3047
|
+
for (const [src, dest] of sourceMoves) if (!fs.existsSync(dest) && fs.existsSync(src)) fs.copySync(src, dest);
|
|
3020
3048
|
return true;
|
|
3021
3049
|
};
|
|
3022
3050
|
|
|
@@ -3448,6 +3476,7 @@ export {
|
|
|
3448
3476
|
waitForPort,
|
|
3449
3477
|
syncPrivateConf,
|
|
3450
3478
|
syncDeployIdSources,
|
|
3479
|
+
syncDeployIdSourcesBack,
|
|
3451
3480
|
gitOriginRepositoryName,
|
|
3452
3481
|
ensureTemplateCheckout,
|
|
3453
3482
|
pruneTemplateWorkTree,
|
|
@@ -333,10 +333,19 @@ class UnderpostStartUp {
|
|
|
333
333
|
const result = await awaitDeployMonitor(true);
|
|
334
334
|
if (result === true) {
|
|
335
335
|
// Withdraw every domain's local traces once the deployment is serving. Three calls
|
|
336
|
-
// rather than one cross-domain sweep: each domain owns what it put on disk.
|
|
336
|
+
// rather than one cross-domain sweep: each domain owns what it put on disk. Only the
|
|
337
|
+
// host domain is forced: its `--force` removes the ephemeral `engine-private` clone,
|
|
338
|
+
// which must not outlive the boot inside a container. For the other two, `--force`
|
|
339
|
+
// reaches into the cluster (the app Secret, secret purges) and is never wanted here.
|
|
337
340
|
if (env === 'production' && Underpost.state.isInsideContainer())
|
|
338
341
|
for (const domain of [Underpost.secret, Underpost.host, Underpost.app])
|
|
339
|
-
domain.clean({
|
|
342
|
+
domain.clean({
|
|
343
|
+
env,
|
|
344
|
+
namespace: 'default',
|
|
345
|
+
args: {},
|
|
346
|
+
dryRun: false,
|
|
347
|
+
force: domain === Underpost.host,
|
|
348
|
+
});
|
|
340
349
|
setTimeout(() => {
|
|
341
350
|
setRuntimeStatus(deployId, env, RUNTIME_STATUS.RUNNING);
|
|
342
351
|
setStartContainerStatus(deployId, env);
|
|
@@ -263,7 +263,7 @@ describe('shared ingress front', () => {
|
|
|
263
263
|
);
|
|
264
264
|
const testCandidate = install.indexOf("execIngress('nginx -t -c /tmp/nginx.candidate.conf'");
|
|
265
265
|
const reload = install.indexOf('nginx -s reload -c /tmp/nginx.conf', testCandidate);
|
|
266
|
-
const persist = install.indexOf("
|
|
266
|
+
const persist = install.indexOf("`kubectl apply -f - -n ${namespace} <<'EOF'", reload);
|
|
267
267
|
expect(testCandidate).to.be.greaterThan(-1);
|
|
268
268
|
expect(reload).to.be.greaterThan(testCandidate);
|
|
269
269
|
expect(persist).to.be.greaterThan(reload);
|
|
@@ -14,6 +14,12 @@ import { TEST_TIERS } from '../../src/server/build/testing.js';
|
|
|
14
14
|
const catalogs = await loadProductCatalogs();
|
|
15
15
|
const describeProducts = describe.skipIf(catalogs.length === 0);
|
|
16
16
|
|
|
17
|
+
// A product template keeps its own catalog but has every other product sliced away, so only
|
|
18
|
+
// the unsliced engine tree carries every catalog and every tier directory. `buildTemplate`
|
|
19
|
+
// drops `build:template` from the manifest and every product manifest inherits that, which
|
|
20
|
+
// makes the script the one durable marker of the unsliced tree.
|
|
21
|
+
const unsliced = !!JSON.parse(fs.readFileSync('./package.json', 'utf8')).scripts?.['build:template'];
|
|
22
|
+
|
|
17
23
|
describeProducts('product catalogs', () => {
|
|
18
24
|
it('every catalog carries the uniform shape', () => {
|
|
19
25
|
for (const catalog of catalogs)
|
|
@@ -54,14 +60,19 @@ describeProducts('product catalogs and the test tiers', () => {
|
|
|
54
60
|
catalogs.some(({ stripPaths }) => stripPaths.includes(`./${directory}`)),
|
|
55
61
|
);
|
|
56
62
|
|
|
57
|
-
it('has products that own at least one tier', () => {
|
|
63
|
+
it.skipIf(!unsliced)('has products that own at least one tier', () => {
|
|
58
64
|
expect(strippedTiers).to.not.be.empty;
|
|
59
65
|
});
|
|
60
66
|
|
|
61
|
-
it('gives every tier a directory that exists in an unsliced tree', () => {
|
|
67
|
+
it.skipIf(!unsliced)('gives every tier a directory that exists in an unsliced tree', () => {
|
|
62
68
|
for (const { name, directory } of TEST_TIERS) expect(fs.existsSync(directory), name).to.equal(true);
|
|
63
69
|
});
|
|
64
70
|
|
|
71
|
+
it('carries the directory of every tier a present product owns', () => {
|
|
72
|
+
// Holds in a product template too: what it kept of itself must be whole.
|
|
73
|
+
for (const { name, directory } of strippedTiers) expect(fs.existsSync(directory), name).to.equal(true);
|
|
74
|
+
});
|
|
75
|
+
|
|
65
76
|
it('ships every tier it strips from the base template', () => {
|
|
66
77
|
for (const { name, directory } of strippedTiers) {
|
|
67
78
|
const owner = catalogs.find(({ stripPaths }) => stripPaths.includes(`./${directory}`));
|
|
@@ -35,6 +35,7 @@ import {
|
|
|
35
35
|
resolveHostKeyContext,
|
|
36
36
|
resolveReplicaCount,
|
|
37
37
|
syncDeployIdSources,
|
|
38
|
+
syncDeployIdSourcesBack,
|
|
38
39
|
syncPrivateConf,
|
|
39
40
|
updatePrivateEngineTestRepo,
|
|
40
41
|
updatePrivateTemplateRepo,
|
|
@@ -757,17 +758,50 @@ describe('private conf sync', () => {
|
|
|
757
758
|
expect(syncDeployIdSources([])).to.equal(false);
|
|
758
759
|
});
|
|
759
760
|
|
|
760
|
-
it('
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
761
|
+
it('copies in only the sources the engine tree lacks, and never moves', () => {
|
|
762
|
+
// Regression: the repo copy used to be moved over the engine copy, so a build discarded
|
|
763
|
+
// every edit made in the engine tree and left the repo reading as wholesale deletions.
|
|
764
|
+
const calls = [];
|
|
765
|
+
const present = new Set(['../engine-x/src/new.js', '../engine-x/src/edited.js', 'src/api/edited.js']);
|
|
766
|
+
vi.spyOn(fs, 'mkdirSync').mockImplementation(() => {});
|
|
767
|
+
vi.spyOn(fs, 'existsSync').mockImplementation((filePath) => present.has(`${filePath}`));
|
|
768
|
+
vi.spyOn(fs, 'removeSync').mockImplementation((target) => calls.push(`remove ${target}`));
|
|
769
|
+
vi.spyOn(fs, 'copySync').mockImplementation((src, dest) => calls.push(`copy ${src} -> ${dest}`));
|
|
770
|
+
vi.spyOn(fs, 'moveSync').mockImplementation(() => calls.push('move'));
|
|
764
771
|
expect(
|
|
765
772
|
syncDeployIdSources([
|
|
766
|
-
['src/
|
|
767
|
-
['src/
|
|
773
|
+
['../engine-x/src/new.js', 'src/api/new.js'],
|
|
774
|
+
['../engine-x/src/edited.js', 'src/api/edited.js'],
|
|
775
|
+
['../engine-x/src/absent.js', 'src/api/absent.js'],
|
|
768
776
|
]),
|
|
769
777
|
).to.equal(true);
|
|
770
|
-
expect(
|
|
778
|
+
expect(calls).to.deep.equal([
|
|
779
|
+
// The engine copy wins: it is mirrored out before anything is copied in.
|
|
780
|
+
'remove ../engine-x/src/edited.js',
|
|
781
|
+
'copy src/api/edited.js -> ../engine-x/src/edited.js',
|
|
782
|
+
'copy ../engine-x/src/new.js -> src/api/new.js',
|
|
783
|
+
]);
|
|
784
|
+
});
|
|
785
|
+
|
|
786
|
+
it('mirrors every source the tree holds back to its repo, replacing it whole', () => {
|
|
787
|
+
// The engine ignores the moved-in paths, so the source repo is the only place a change to
|
|
788
|
+
// them is diffed; replacing rather than overlaying makes deletions reach that diff too.
|
|
789
|
+
const calls = [];
|
|
790
|
+
vi.spyOn(fs, 'existsSync').mockImplementation((filePath) => `${filePath}`.includes('present'));
|
|
791
|
+
vi.spyOn(fs, 'removeSync').mockImplementation((target) => calls.push(`remove ${target}`));
|
|
792
|
+
vi.spyOn(fs, 'copySync').mockImplementation((src, dest) => calls.push(`copy ${src} -> ${dest}`));
|
|
793
|
+
vi.spyOn(fs, 'moveSync').mockImplementation(() => calls.push('move'));
|
|
794
|
+
expect(
|
|
795
|
+
syncDeployIdSourcesBack([
|
|
796
|
+
['../engine-x/src/present.js', 'src/api/present.js'],
|
|
797
|
+
['../engine-x/src/absent.js', 'src/api/absent.js'],
|
|
798
|
+
]),
|
|
799
|
+
).to.deep.equal(['../engine-x/src/present.js']);
|
|
800
|
+
expect(calls).to.deep.equal([
|
|
801
|
+
'remove ../engine-x/src/present.js',
|
|
802
|
+
'copy src/api/present.js -> ../engine-x/src/present.js',
|
|
803
|
+
]);
|
|
804
|
+
expect(syncDeployIdSourcesBack()).to.deep.equal([]);
|
|
771
805
|
});
|
|
772
806
|
});
|
|
773
807
|
|