underpost 3.2.28 → 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/.github/workflows/ghpkg.ci.yml +87 -0
- package/.github/workflows/npmpkg.ci.yml +9 -6
- package/.github/workflows/pwa-microservices-template-page.cd.yml +0 -7
- package/CHANGELOG.md +161 -1
- package/CLI-HELP.md +17 -6
- package/README.md +2 -2
- package/bin/build.js +30 -0
- package/bin/deploy.js +2 -2
- package/bump.config.js +1 -0
- package/docker-compose.yml +26 -26
- package/manifests/cronjobs/dd-cron/dd-cron-backup.yaml +1 -1
- package/manifests/cronjobs/dd-cron/dd-cron-dns.yaml +1 -1
- package/manifests/deployment/dd-default-development/deployment.yaml +2 -2
- package/package.json +10 -10
- package/scripts/disk-clean.sh +85 -60
- package/scripts/test-monitor.sh +3 -5
- package/src/cli/cluster.js +77 -4
- package/src/cli/deploy.js +121 -9
- package/src/cli/docker-compose.js +58 -1
- package/src/cli/env.js +11 -2
- package/src/cli/fs.js +45 -24
- package/src/cli/image.js +129 -51
- package/src/cli/index.js +35 -6
- package/src/cli/release.js +46 -4
- package/src/cli/repository.js +166 -22
- package/src/cli/run.js +273 -118
- package/src/cli/secrets.js +82 -48
- package/src/cli/ssh.js +44 -0
- package/src/client.build.js +0 -2
- package/src/db/mongo/MongoBootstrap.js +12 -12
- package/src/index.js +1 -1
- package/src/server/catalog.js +2 -0
- package/src/server/conf.js +0 -3
- package/src/server/runtime-status.js +18 -1
- package/src/server/start.js +12 -2
- package/src/server.js +5 -1
- package/test/deploy-monitor.test.js +26 -10
package/src/cli/secrets.js
CHANGED
|
@@ -13,6 +13,58 @@ import { loggerFactory } from '../server/logger.js';
|
|
|
13
13
|
|
|
14
14
|
const logger = loggerFactory(import.meta);
|
|
15
15
|
|
|
16
|
+
// Shell/runtime-critical and Kubernetes-injected env keys that must never be persisted as
|
|
17
|
+
// application secrets nor injected into a pod via `envFrom`. An injected PATH (or HOME, etc.)
|
|
18
|
+
// overrides the container image's own and breaks coreutils/sudo resolution inside the pod
|
|
19
|
+
// ("rm: command not found"). Single source of truth for both container-env capture and the
|
|
20
|
+
// `underpost-config` secret built from an env file.
|
|
21
|
+
const RESERVED_ENV_KEYS = new Set([
|
|
22
|
+
'HOME',
|
|
23
|
+
'HOSTNAME',
|
|
24
|
+
'PATH',
|
|
25
|
+
'TERM',
|
|
26
|
+
'SHLVL',
|
|
27
|
+
'PWD',
|
|
28
|
+
'_',
|
|
29
|
+
'LANG',
|
|
30
|
+
'LANGUAGE',
|
|
31
|
+
'LC_ALL',
|
|
32
|
+
'container',
|
|
33
|
+
'SHELL',
|
|
34
|
+
'USER',
|
|
35
|
+
'LOGNAME',
|
|
36
|
+
'MAIL',
|
|
37
|
+
'OLDPWD',
|
|
38
|
+
'LESSOPEN',
|
|
39
|
+
'LESSCLOSE',
|
|
40
|
+
'LS_COLORS',
|
|
41
|
+
'DISPLAY',
|
|
42
|
+
'COLORTERM',
|
|
43
|
+
'EDITOR',
|
|
44
|
+
'VISUAL',
|
|
45
|
+
'TERM_PROGRAM',
|
|
46
|
+
'TERM_PROGRAM_VERSION',
|
|
47
|
+
'SSH_AUTH_SOCK',
|
|
48
|
+
'SSH_CLIENT',
|
|
49
|
+
'SSH_CONNECTION',
|
|
50
|
+
'SSH_TTY',
|
|
51
|
+
'XDG_SESSION_ID',
|
|
52
|
+
'XDG_RUNTIME_DIR',
|
|
53
|
+
'XDG_DATA_DIRS',
|
|
54
|
+
'XDG_CONFIG_DIRS',
|
|
55
|
+
'DBUS_SESSION_BUS_ADDRESS',
|
|
56
|
+
'GPG_AGENT_INFO',
|
|
57
|
+
'WINDOWID',
|
|
58
|
+
'DESKTOP_SESSION',
|
|
59
|
+
'SESSION_MANAGER',
|
|
60
|
+
'XAUTHORITY',
|
|
61
|
+
'WAYLAND_DISPLAY',
|
|
62
|
+
'which_declare',
|
|
63
|
+
]);
|
|
64
|
+
const RESERVED_ENV_KEY_PREFIXES = ['KUBERNETES_', 'npm_', 'NODE_'];
|
|
65
|
+
const isReservedEnvKey = (key) =>
|
|
66
|
+
RESERVED_ENV_KEYS.has(key) || RESERVED_ENV_KEY_PREFIXES.some((prefix) => key.startsWith(prefix));
|
|
67
|
+
|
|
16
68
|
/**
|
|
17
69
|
* @class UnderpostSecret
|
|
18
70
|
* @description Manages the secrets of the application.
|
|
@@ -49,67 +101,49 @@ class UnderpostSecret {
|
|
|
49
101
|
*/
|
|
50
102
|
createFromContainerEnv() {
|
|
51
103
|
Underpost.env.clean();
|
|
52
|
-
const systemKeys = new Set([
|
|
53
|
-
'HOME',
|
|
54
|
-
'HOSTNAME',
|
|
55
|
-
'PATH',
|
|
56
|
-
'TERM',
|
|
57
|
-
'SHLVL',
|
|
58
|
-
'PWD',
|
|
59
|
-
'_',
|
|
60
|
-
'LANG',
|
|
61
|
-
'LANGUAGE',
|
|
62
|
-
'LC_ALL',
|
|
63
|
-
'container',
|
|
64
|
-
'SHELL',
|
|
65
|
-
'USER',
|
|
66
|
-
'LOGNAME',
|
|
67
|
-
'MAIL',
|
|
68
|
-
'OLDPWD',
|
|
69
|
-
'LESSOPEN',
|
|
70
|
-
'LESSCLOSE',
|
|
71
|
-
'LS_COLORS',
|
|
72
|
-
'DISPLAY',
|
|
73
|
-
'COLORTERM',
|
|
74
|
-
'EDITOR',
|
|
75
|
-
'VISUAL',
|
|
76
|
-
'TERM_PROGRAM',
|
|
77
|
-
'TERM_PROGRAM_VERSION',
|
|
78
|
-
'SSH_AUTH_SOCK',
|
|
79
|
-
'SSH_CLIENT',
|
|
80
|
-
'SSH_CONNECTION',
|
|
81
|
-
'SSH_TTY',
|
|
82
|
-
'XDG_SESSION_ID',
|
|
83
|
-
'XDG_RUNTIME_DIR',
|
|
84
|
-
'XDG_DATA_DIRS',
|
|
85
|
-
'XDG_CONFIG_DIRS',
|
|
86
|
-
'DBUS_SESSION_BUS_ADDRESS',
|
|
87
|
-
'GPG_AGENT_INFO',
|
|
88
|
-
'WINDOWID',
|
|
89
|
-
'DESKTOP_SESSION',
|
|
90
|
-
'SESSION_MANAGER',
|
|
91
|
-
'XAUTHORITY',
|
|
92
|
-
'WAYLAND_DISPLAY',
|
|
93
|
-
'which_declare',
|
|
94
|
-
]);
|
|
95
|
-
const systemKeyPrefixes = ['KUBERNETES_', 'npm_', 'NODE_'];
|
|
96
104
|
for (const [key, value] of Object.entries(process.env)) {
|
|
97
|
-
if (
|
|
98
|
-
if (systemKeyPrefixes.some((prefix) => key.startsWith(prefix))) continue;
|
|
105
|
+
if (isReservedEnvKey(key)) continue;
|
|
99
106
|
Underpost.env.set(key, value);
|
|
100
107
|
}
|
|
101
108
|
},
|
|
102
109
|
},
|
|
103
110
|
|
|
111
|
+
/**
|
|
112
|
+
* @method sanitizeSecretEnvFile
|
|
113
|
+
* @description Strips shell/runtime-critical and Kubernetes-injected keys (PATH, HOME, …) from
|
|
114
|
+
* raw `.env` file content so the resulting `underpost-config` secret can be safely injected via
|
|
115
|
+
* `envFrom` without clobbering the container image's own PATH. Blank lines and comments are
|
|
116
|
+
* preserved. Uses the same {@link RESERVED_ENV_KEYS} blocklist as container-env capture.
|
|
117
|
+
* @param {string} envFileContent - Raw contents of a `.env.<env>` file.
|
|
118
|
+
* @returns {string} Filtered env-file content.
|
|
119
|
+
* @memberof UnderpostSecret
|
|
120
|
+
*/
|
|
121
|
+
sanitizeSecretEnvFile(envFileContent) {
|
|
122
|
+
return envFileContent
|
|
123
|
+
.split('\n')
|
|
124
|
+
.filter((line) => {
|
|
125
|
+
const trimmed = line.trimStart();
|
|
126
|
+
if (!trimmed || trimmed.startsWith('#')) return true;
|
|
127
|
+
const key = line.slice(0, line.indexOf('=')).trim();
|
|
128
|
+
return !key || !isReservedEnvKey(key);
|
|
129
|
+
})
|
|
130
|
+
.join('\n');
|
|
131
|
+
},
|
|
132
|
+
|
|
104
133
|
/**
|
|
105
134
|
* Removes all filesystem traces of secrets after deployment startup.
|
|
106
135
|
* Centralizes the defense-in-depth cleanup performed
|
|
136
|
+
* @param {object} options - Options for cleaning the environment.
|
|
137
|
+
* @param {Array<string>} [options.keepKeys=[]] - List of keys to keep in the environment file. If provided, only these keys will be retained.
|
|
107
138
|
* @memberof UnderpostSecret
|
|
108
139
|
*/
|
|
109
|
-
globalSecretClean() {
|
|
140
|
+
globalSecretClean(options = { keepKeys: [] }) {
|
|
141
|
+
const { keepKeys } = options;
|
|
110
142
|
loadConf('clean');
|
|
111
143
|
Underpost.repo.cleanupPrivateEngineRepo();
|
|
112
|
-
Underpost.env.clean(
|
|
144
|
+
Underpost.env.clean({
|
|
145
|
+
keepKeys: keepKeys.length > 0 ? keepKeys : ['container-status', 'start-container-status'],
|
|
146
|
+
});
|
|
113
147
|
},
|
|
114
148
|
};
|
|
115
149
|
}
|
package/src/cli/ssh.js
CHANGED
|
@@ -496,6 +496,50 @@ EOF`);
|
|
|
496
496
|
if (options.status) shellExec('service sshd status');
|
|
497
497
|
},
|
|
498
498
|
|
|
499
|
+
/**
|
|
500
|
+
* Synchronously copies a local directory to a remote host over key-only SSH,
|
|
501
|
+
* streaming it as a tar archive (no intermediate file) and fixing ownership.
|
|
502
|
+
* Mirrors the kind-node `tar | docker cp` provisioning pattern but targets a
|
|
503
|
+
* real node via SSH, so node-local hostPath volumes can be materialized on the
|
|
504
|
+
* node where the pod will actually run.
|
|
505
|
+
*
|
|
506
|
+
* Idempotent and re-runnable: `mkdir -p` + `tar -x` overwrite in place. Throws
|
|
507
|
+
* on any SSH/tar failure so an empty-volume deploy is never produced silently.
|
|
508
|
+
*
|
|
509
|
+
* @function copyDirToNode
|
|
510
|
+
* @memberof UnderpostSSH
|
|
511
|
+
* @param {object} params
|
|
512
|
+
* @param {string} params.host - Target host/IP (key-only SSH reachable).
|
|
513
|
+
* @param {string} params.localDir - Local source directory.
|
|
514
|
+
* @param {string} params.remoteDir - Destination directory on the node.
|
|
515
|
+
* @param {number} [params.port=22] - SSH port.
|
|
516
|
+
* @param {string} [params.user='root'] - SSH user (key-only).
|
|
517
|
+
* @param {string} [params.keyPath='./engine-private/deploy/id_rsa'] - Private key path.
|
|
518
|
+
* @param {string} [params.owner='1000:1000'] - chown target on the node (empty to skip).
|
|
519
|
+
* @param {string} [params.mode='755'] - chmod mode on the node (empty to skip).
|
|
520
|
+
* @returns {void}
|
|
521
|
+
*/
|
|
522
|
+
copyDirToNode: ({
|
|
523
|
+
host,
|
|
524
|
+
localDir,
|
|
525
|
+
remoteDir,
|
|
526
|
+
port = 22,
|
|
527
|
+
user = 'root',
|
|
528
|
+
keyPath = './engine-private/deploy/id_rsa',
|
|
529
|
+
owner = '1000:1000',
|
|
530
|
+
mode = '755',
|
|
531
|
+
}) => {
|
|
532
|
+
if (!host) throw new Error('copyDirToNode requires a host');
|
|
533
|
+
if (!localDir || !fs.existsSync(localDir)) throw new Error(`copyDirToNode: local dir not found: ${localDir}`);
|
|
534
|
+
if (!remoteDir) throw new Error('copyDirToNode requires a remoteDir');
|
|
535
|
+
shellExec(`chmod 600 ${keyPath}`, { silent: true, silentOnError: true, disableLog: true });
|
|
536
|
+
const sshOpts = `-i ${keyPath} -o BatchMode=yes -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -p ${port}`;
|
|
537
|
+
shellExec(`ssh ${sshOpts} ${user}@${host} 'mkdir -p ${remoteDir}'`);
|
|
538
|
+
shellExec(`tar -C ${localDir} -c . | ssh ${sshOpts} ${user}@${host} 'tar -C ${remoteDir} -x'`);
|
|
539
|
+
const fixups = `${owner ? `chown -R ${owner} ${remoteDir}; ` : ''}${mode ? `chmod -R ${mode} ${remoteDir}` : ''}`.trim();
|
|
540
|
+
if (fixups) shellExec(`ssh ${sshOpts} ${user}@${host} '${fixups}'`);
|
|
541
|
+
},
|
|
542
|
+
|
|
499
543
|
/**
|
|
500
544
|
* Generic SSH remote command runner that SSH execution logic.
|
|
501
545
|
* Executes arbitrary shell commands on a remote server via SSH with proper credential handling.
|
package/src/client.build.js
CHANGED
|
@@ -290,19 +290,16 @@ class MongoBootstrap {
|
|
|
290
290
|
* @returns {Promise<number>} Number of pods that failed to become ready (0 = all good).
|
|
291
291
|
*/
|
|
292
292
|
static async waitForPods(namespace, replicaCount) {
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
const failed = results.filter((r) => !r.ready).map((r) => `${MONGODB_STATEFULSET_NAME}-${r.index}`);
|
|
302
|
-
if (failed.length > 0) {
|
|
303
|
-
logger.error('MongoDB pods did not become ready', { failed });
|
|
293
|
+
let failedCount = 0;
|
|
294
|
+
for (let i = 0; i < replicaCount; i++) {
|
|
295
|
+
const podName = `${MONGODB_STATEFULSET_NAME}-${i}`;
|
|
296
|
+
const result = shellExec(`kubectl wait --for=condition=Ready pod/${podName} -n ${namespace} --timeout=60s`);
|
|
297
|
+
if (result.code !== 0) {
|
|
298
|
+
logger.error(`Pod ${podName} did not become ready`);
|
|
299
|
+
failedCount++;
|
|
300
|
+
}
|
|
304
301
|
}
|
|
305
|
-
return
|
|
302
|
+
return failedCount;
|
|
306
303
|
}
|
|
307
304
|
|
|
308
305
|
/**
|
|
@@ -620,6 +617,9 @@ class MongoBootstrap {
|
|
|
620
617
|
process.env.DB_PASSWORD ||
|
|
621
618
|
readTrimmedFile('./engine-private/mongodb-password');
|
|
622
619
|
|
|
620
|
+
// Ensure the pod is ready before querying
|
|
621
|
+
shellExec(`kubectl wait --for=condition=Ready pod/${podName} -n ${namespace} --timeout=60s`);
|
|
622
|
+
|
|
623
623
|
const evalExpr = 'rs.status().members.filter(m=>m.stateStr=="PRIMARY").map(m=>m.name)';
|
|
624
624
|
|
|
625
625
|
const cli = disableAuth ? 'mongo' : 'mongosh';
|
package/src/index.js
CHANGED
package/src/server/catalog.js
CHANGED
package/src/server/conf.js
CHANGED
|
@@ -476,9 +476,6 @@ const loadConf = (deployId = DEFAULT_DEPLOY_ID, subConf) => {
|
|
|
476
476
|
fs.removeSync(`${path}/.env.production`);
|
|
477
477
|
fs.removeSync(`${path}/.env.development`);
|
|
478
478
|
fs.removeSync(`${path}/.env.test`);
|
|
479
|
-
if (fs.existsSync(`${path}/typedoc.json`)) shellExec(`git checkout ${path}/typedoc.json`);
|
|
480
|
-
shellExec(`git checkout ${path}/package.json`);
|
|
481
|
-
shellExec(`git checkout ${path}/package-lock.json`);
|
|
482
479
|
return;
|
|
483
480
|
}
|
|
484
481
|
const folder = getConfFolder(deployId);
|
|
@@ -40,6 +40,7 @@ const RUNTIME_STATUS = {
|
|
|
40
40
|
};
|
|
41
41
|
|
|
42
42
|
const CONTAINER_STATUS_KEY = 'container-status';
|
|
43
|
+
const START_CONTAINER_STATUS_KEY = 'start-container-status';
|
|
43
44
|
const INTERNAL_STATUS_PATH = '/_internal/status';
|
|
44
45
|
const INTERNAL_READY_PATH = '/_internal/ready';
|
|
45
46
|
const INTERNAL_HEALTH_PATH = '/_internal/health';
|
|
@@ -118,6 +119,20 @@ const normalizeContainerStatus = (raw) => {
|
|
|
118
119
|
const getRuntimeStatus = () =>
|
|
119
120
|
normalizeContainerStatus(Underpost.env.get(CONTAINER_STATUS_KEY, undefined, { disableLog: true }));
|
|
120
121
|
|
|
122
|
+
/**
|
|
123
|
+
* Reads the start-container-status env key — an insulated marker set by the
|
|
124
|
+
* start pipeline after it completes the running phase. Unlike container-status
|
|
125
|
+
* (which external scripts / backup failures may clobber), this key is written
|
|
126
|
+
* once and survives globalSecretClean. Used exclusively by the readinessProbe
|
|
127
|
+
* endpoint so K8s pod readiness is never derailed by lifecycle noise.
|
|
128
|
+
* @memberof RuntimeStatus
|
|
129
|
+
* @returns {string|undefined}
|
|
130
|
+
*/
|
|
131
|
+
const getStartContainerStatus = () => {
|
|
132
|
+
const raw = Underpost.env.get(START_CONTAINER_STATUS_KEY, undefined, { disableLog: true });
|
|
133
|
+
return raw && typeof raw === 'string' && raw.trim() ? raw.trim() : undefined;
|
|
134
|
+
};
|
|
135
|
+
|
|
121
136
|
/**
|
|
122
137
|
* Minimal, secret-free payload served by the internal status endpoint and used
|
|
123
138
|
* by the monitor for failure classification and observability.
|
|
@@ -188,7 +203,7 @@ const startInternalStatusServer = (port = resolveInternalStatusPort()) => {
|
|
|
188
203
|
case INTERNAL_HEALTH_PATH:
|
|
189
204
|
return sendJson(200, { status: 'ok' });
|
|
190
205
|
case INTERNAL_READY_PATH:
|
|
191
|
-
return
|
|
206
|
+
return getStartContainerStatus()
|
|
192
207
|
? sendJson(200, { status: RUNTIME_STATUS.RUNNING })
|
|
193
208
|
: sendJson(503, { status: getRuntimeStatus() ?? null });
|
|
194
209
|
case INTERNAL_STATUS_PATH:
|
|
@@ -220,6 +235,7 @@ const stopInternalStatusServer = () =>
|
|
|
220
235
|
export {
|
|
221
236
|
RUNTIME_STATUS,
|
|
222
237
|
CONTAINER_STATUS_KEY,
|
|
238
|
+
START_CONTAINER_STATUS_KEY,
|
|
223
239
|
INTERNAL_STATUS_PATH,
|
|
224
240
|
INTERNAL_READY_PATH,
|
|
225
241
|
INTERNAL_HEALTH_PATH,
|
|
@@ -228,6 +244,7 @@ export {
|
|
|
228
244
|
containerStatusValue,
|
|
229
245
|
normalizeContainerStatus,
|
|
230
246
|
getRuntimeStatus,
|
|
247
|
+
getStartContainerStatus,
|
|
231
248
|
runtimeStatusPayload,
|
|
232
249
|
setRuntimeStatus,
|
|
233
250
|
startInternalStatusServer,
|
package/src/server/start.js
CHANGED
|
@@ -8,7 +8,14 @@ import fs from 'fs-extra';
|
|
|
8
8
|
import { awaitDeployMonitor } from './conf.js';
|
|
9
9
|
import { actionInitLog, loggerFactory } from './logger.js';
|
|
10
10
|
import { shellCd, shellExec } from './process.js';
|
|
11
|
-
import {
|
|
11
|
+
import {
|
|
12
|
+
RUNTIME_STATUS,
|
|
13
|
+
START_CONTAINER_STATUS_KEY,
|
|
14
|
+
setRuntimeStatus,
|
|
15
|
+
startInternalStatusServer,
|
|
16
|
+
deployStatusPort,
|
|
17
|
+
containerStatusValue,
|
|
18
|
+
} from './runtime-status.js';
|
|
12
19
|
import Underpost from '../index.js';
|
|
13
20
|
const logger = loggerFactory(import.meta);
|
|
14
21
|
|
|
@@ -241,7 +248,10 @@ class UnderpostStartUp {
|
|
|
241
248
|
const result = await awaitDeployMonitor(true);
|
|
242
249
|
if (result === true) {
|
|
243
250
|
if (env === 'production' && Underpost.env.isInsideContainer()) Underpost.secret.globalSecretClean();
|
|
244
|
-
|
|
251
|
+
setTimeout(() => {
|
|
252
|
+
setRuntimeStatus(deployId, env, RUNTIME_STATUS.RUNNING);
|
|
253
|
+
Underpost.env.set(START_CONTAINER_STATUS_KEY, containerStatusValue(deployId, env, RUNTIME_STATUS.RUNNING));
|
|
254
|
+
});
|
|
245
255
|
} else {
|
|
246
256
|
setRuntimeStatus(deployId, env, RUNTIME_STATUS.ERROR);
|
|
247
257
|
}
|
package/src/server.js
CHANGED
|
@@ -3,18 +3,22 @@
|
|
|
3
3
|
// https://nodejs.org/api
|
|
4
4
|
// https://expressjs.com/en/4x/api.html
|
|
5
5
|
|
|
6
|
+
import dotenv from 'dotenv';
|
|
6
7
|
import { loggerFactory } from './server/logger.js';
|
|
7
8
|
import { buildClient } from './client-builder/client-build.js';
|
|
8
9
|
import { buildRuntime } from './server/runtime.js';
|
|
9
10
|
import { ProcessController } from './server/process.js';
|
|
10
11
|
import { Config } from './server/conf.js';
|
|
12
|
+
|
|
13
|
+
dotenv.config();
|
|
14
|
+
|
|
11
15
|
await Config.build();
|
|
12
16
|
|
|
13
17
|
const logger = loggerFactory(import.meta);
|
|
14
18
|
|
|
15
19
|
await logger.setUpInfo();
|
|
16
20
|
|
|
17
|
-
await buildClient();
|
|
21
|
+
if (process.env.NODE_ENV === 'development') await buildClient();
|
|
18
22
|
|
|
19
23
|
await buildRuntime();
|
|
20
24
|
|
|
@@ -69,12 +69,10 @@ describe('Deploy monitor — two-phase state machine (e2e, real HTTP transport)'
|
|
|
69
69
|
process.env.npm_config_prefix = tmpPrefix;
|
|
70
70
|
|
|
71
71
|
Underpost.env.set('container-status', 'init');
|
|
72
|
+
Underpost.env.set('start-container-status', '');
|
|
72
73
|
const npmRoot = shellExec('npm root -g', { stdout: true, silent: true, disableLog: true }).trim();
|
|
73
74
|
envFile = path.join(npmRoot, 'underpost', '.env');
|
|
74
75
|
|
|
75
|
-
// Real in-pod internal status server: serves container-status from the same
|
|
76
|
-
// env file the runtime writes. Bound in this test process; the monitor's
|
|
77
|
-
// port-forward tunnel (localPort == INTERNAL_PORT) resolves straight to it.
|
|
78
76
|
process.env.UNDERPOST_INTERNAL_PORT = String(INTERNAL_PORT);
|
|
79
77
|
startInternalStatusServer(INTERNAL_PORT);
|
|
80
78
|
|
|
@@ -148,10 +146,11 @@ try {
|
|
|
148
146
|
beforeEach(() => {
|
|
149
147
|
// Deploy in flight: app not yet reporting running.
|
|
150
148
|
Underpost.env.set('container-status', INIT_STATUS);
|
|
149
|
+
// start-container-status is unset (empty) — the readinessProbe
|
|
150
|
+
// sees 503 until the start pipeline completes and stamps this key.
|
|
151
|
+
Underpost.env.set('start-container-status', '');
|
|
151
152
|
});
|
|
152
153
|
|
|
153
|
-
// Spawns the real monitorReadyRunner with the fake cluster on PATH; resolves
|
|
154
|
-
// with its exit code. `overrides` inject deterministic timing / target port.
|
|
155
154
|
const spawnMonitor = (overrides = {}) =>
|
|
156
155
|
new Promise((resolve) => {
|
|
157
156
|
const envVars = {
|
|
@@ -160,9 +159,6 @@ try {
|
|
|
160
159
|
POD_NAME,
|
|
161
160
|
npm_config_prefix: tmpPrefix,
|
|
162
161
|
UNDERPOST_INTERNAL_PORT: String(INTERNAL_PORT),
|
|
163
|
-
// Pin the tunnel's local port so the no-op fake port-forward + the real
|
|
164
|
-
// internal server (bound to INTERNAL_PORT in this process) resolve to the
|
|
165
|
-
// same address the monitor's HTTP GET targets.
|
|
166
162
|
UNDERPOST_PF_LOCAL_PORT: String(INTERNAL_PORT),
|
|
167
163
|
UNDERPOST_MONITOR_DELAY_MS: '100',
|
|
168
164
|
UNDERPOST_MONITOR_MAX_ITERATIONS: '60',
|
|
@@ -183,12 +179,15 @@ try {
|
|
|
183
179
|
|
|
184
180
|
it('success (default exec transport): both phases satisfied → monitor exits 0', async () => {
|
|
185
181
|
Underpost.env.set('container-status', RUNNING_STATUS);
|
|
182
|
+
// start-container-status not needed for monitor (reads container-status);
|
|
183
|
+
// this test exercises the default exec transport path.
|
|
186
184
|
const code = await spawnMonitor({ FAKE_POD_READY: 'True' });
|
|
187
185
|
expect(code).to.equal(0);
|
|
188
186
|
});
|
|
189
187
|
|
|
190
188
|
it('success (opt-in http transport): both phases satisfied → monitor exits 0', async () => {
|
|
191
189
|
Underpost.env.set('container-status', RUNNING_STATUS);
|
|
190
|
+
Underpost.env.set('start-container-status', RUNNING_STATUS);
|
|
192
191
|
const code = await spawnMonitor({ FAKE_POD_READY: 'True', MON_TRANSPORT: 'http' });
|
|
193
192
|
expect(code).to.equal(0);
|
|
194
193
|
});
|
|
@@ -202,13 +201,13 @@ try {
|
|
|
202
201
|
it('readiness mismatch: runtime running but pod not Ready → never succeeds (exits 1)', async () => {
|
|
203
202
|
// Phase 2 satisfied, Phase 1 not: success requires BOTH, so it must time out.
|
|
204
203
|
Underpost.env.set('container-status', RUNNING_STATUS);
|
|
204
|
+
// start-container-status is NOT set — the readinessProbe would fail,
|
|
205
|
+
// but the monitor only checks container-status via exec/http transport.
|
|
205
206
|
const code = await spawnMonitor({ FAKE_POD_READY: 'False', UNDERPOST_MONITOR_MAX_ITERATIONS: '4' });
|
|
206
207
|
expect(code).to.equal(1);
|
|
207
208
|
});
|
|
208
209
|
|
|
209
210
|
it('transport failure (http): endpoint unreachable is never success (exits 1)', async () => {
|
|
210
|
-
// Opt into http and point the monitor at a port with no internal server; the
|
|
211
|
-
// HTTP read always fails, so runtime readiness is never confirmed → timeout.
|
|
212
211
|
Underpost.env.set('container-status', RUNNING_STATUS);
|
|
213
212
|
const code = await spawnMonitor({
|
|
214
213
|
MON_TRANSPORT: 'http',
|
|
@@ -221,6 +220,8 @@ try {
|
|
|
221
220
|
|
|
222
221
|
it('timeout: runtime stuck initializing → monitor exits 1', async () => {
|
|
223
222
|
Underpost.env.set('container-status', INIT_STATUS);
|
|
223
|
+
// start-container-status remains unset — readinessProbe returns 503
|
|
224
|
+
// (correct, since the runtime is still initializing).
|
|
224
225
|
const code = await spawnMonitor({ FAKE_POD_READY: 'True', UNDERPOST_MONITOR_MAX_ITERATIONS: '4' });
|
|
225
226
|
expect(code).to.equal(1);
|
|
226
227
|
});
|
|
@@ -235,6 +236,21 @@ try {
|
|
|
235
236
|
expect(await monitorExit).to.equal(1);
|
|
236
237
|
});
|
|
237
238
|
|
|
239
|
+
it('readinessProbe: start-container-status set → returns 200 (probe passes)', async () => {
|
|
240
|
+
Underpost.env.set('start-container-status', RUNNING_STATUS);
|
|
241
|
+
const res = await fetch(`http://127.0.0.1:${INTERNAL_PORT}/_internal/ready`);
|
|
242
|
+
expect(res.status).to.equal(200);
|
|
243
|
+
const body = await res.json();
|
|
244
|
+
expect(body.status).to.equal('running-deployment');
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
it('readinessProbe: start-container-status unset → returns 503 (probe fails)', async () => {
|
|
248
|
+
// Unset from beforeEach re-stamp; clear it.
|
|
249
|
+
Underpost.env.set('start-container-status', '');
|
|
250
|
+
const res = await fetch(`http://127.0.0.1:${INTERNAL_PORT}/_internal/ready`);
|
|
251
|
+
expect(res.status).to.equal(503);
|
|
252
|
+
});
|
|
253
|
+
|
|
238
254
|
// Custom instances (cyberia-*) gate on K8s Ready and read status via exec;
|
|
239
255
|
// their runtime never stamps `running-deployment` (stays `initializing`).
|
|
240
256
|
it('instance (kubernetes gate + exec): K8s Ready with initializing status → exits 0', async () => {
|