underpost 3.2.80 → 3.3.0
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 +7 -1
- package/.github/workflows/pwa-microservices-template-page.cd.yml +1 -16
- package/.github/workflows/pwa-microservices-template-test.ci.yml +1 -1
- package/.github/workflows/release.cd.yml +1 -9
- package/CHANGELOG.md +291 -1
- package/CLI-HELP.md +174 -23
- package/README.md +5 -2
- package/bin/build.js +7 -5
- package/bin/deploy.js +19 -17
- package/deploy/lib/logging.sh +96 -0
- package/deploy/pwa-microservices-template/deploy.sh +72 -0
- package/deploy/release/deploy.sh +62 -0
- package/docker-compose.yml +1 -1
- package/manifests/cronjobs/dd-cron/dd-cron-backup.yaml +5 -1
- package/manifests/cronjobs/dd-cron/dd-cron-dns.yaml +1 -1
- package/manifests/cronjobs/dd-cron/dd-cron-vultr.yaml +52 -0
- package/manifests/deployment/dd-default-development/deployment.yaml +2 -2
- package/manifests/deployment/playwright/deployment.yaml +1 -1
- package/manifests/mongodb/kustomization.yaml +4 -1
- package/manifests/mongodb/statefulset.yaml +4 -0
- package/manifests/mongodb/storage-class.yaml +9 -2
- package/package.json +19 -19
- package/scripts/audit-selinux.sh +64 -0
- package/scripts/coverall-test.sh +24 -0
- package/scripts/gpu-diag.sh +0 -0
- package/scripts/ip-info.sh +0 -0
- package/scripts/k3s-node-setup.sh +18 -15
- package/scripts/kubeadm-node-setup.sh +12 -23
- package/scripts/link-local-underpost-cli.sh +0 -0
- package/scripts/lxd-vm-setup.sh +0 -0
- package/scripts/maas-nat-firewalld.sh +0 -0
- package/scripts/nat-iptables.sh +12 -4
- package/scripts/rhel-grpc-setup.sh +0 -0
- package/scripts/rocky-kickstart.sh +25 -9
- package/scripts/test-monitor.sh +4 -3
- package/src/cli/baremetal.js +1 -2
- package/src/cli/cloud-init.js +1 -1
- package/src/cli/cluster.js +786 -96
- package/src/cli/db.js +11 -4
- package/src/cli/deploy.js +1698 -177
- package/src/cli/docker-compose.js +19 -178
- package/src/cli/env.js +1 -1
- package/src/cli/image.js +15 -7
- package/src/cli/index.js +245 -44
- package/src/cli/ipfs.js +82 -11
- package/src/cli/lxd.js +1 -1
- package/src/cli/monitor.js +2 -2
- package/src/cli/release.js +57 -22
- package/src/cli/repository.js +12 -10
- package/src/cli/run.js +2195 -427
- package/src/cli/secrets.js +969 -0
- package/src/cli/ssh.js +206 -105
- package/src/cli/system.js +26 -13
- package/src/cli/test.js +1 -1
- package/src/cli/vultr.js +583 -0
- package/src/cli/wireguard.js +2125 -0
- package/src/client-builder/client-build.js +102 -13
- package/src/client-builder/ssr.js +27 -73
- package/src/db/mongo/MongoBootstrap.js +295 -54
- package/src/db/mongo/MongooseDB.js +51 -32
- package/src/index.js +25 -1
- package/src/projects/underpost/catalog-underpost.js +4 -1
- package/src/server/backup.js +1 -1
- package/src/server/conf.js +1216 -168
- package/src/server/cri.js +70 -0
- package/src/server/cron.js +249 -51
- package/src/server/dns.js +100 -6
- package/src/server/environment.js +98 -0
- package/src/server/forward-proxy.js +549 -0
- package/src/server/middlewares.js +56 -1
- package/src/server/process.js +0 -1
- package/src/server/selinux.js +185 -0
- package/src/server/systemd.js +205 -0
- package/src/server/underpost-compression.js +186 -0
- package/src/server/underpost-gateway.js +1083 -0
- package/src/server/underpost-ingress.js +380 -0
- package/test/cluster-instances.test.js +435 -0
- package/test/deploy-node-placement.test.js +45 -0
- package/test/instance-traffic-plan.test.js +710 -0
- package/test/selinux.test.js +71 -0
- package/test/sops-secret-store.test.js +612 -0
- package/test/underpost-gateway.test.js +510 -0
- package/test/underpost-ingress.test.js +305 -0
- package/test/wireguard-edge.test.js +1177 -0
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CRI (Container Runtime Interface) endpoint resolution shared by every layer
|
|
3
|
+
* that shells out to `crictl`: the cluster CLI, image management, and the
|
|
4
|
+
* database bootstraps. Lives under `src/server` so the db layer can import it
|
|
5
|
+
* without depending on the CLI god-object.
|
|
6
|
+
* @module src/server/cri.js
|
|
7
|
+
* @namespace CriEndpoint
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { shellExec } from './process.js';
|
|
11
|
+
|
|
12
|
+
const CRIO_SOCKET_PATH = '/var/run/crio/crio.sock';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* @constant CRI_SOCKETS
|
|
16
|
+
* @description The CRI endpoints this platform ever targets.
|
|
17
|
+
* @memberof CriEndpoint
|
|
18
|
+
*/
|
|
19
|
+
const CRI_SOCKETS = {
|
|
20
|
+
crio: `unix://${CRIO_SOCKET_PATH}`,
|
|
21
|
+
containerd: 'unix:///run/containerd/containerd.sock',
|
|
22
|
+
k3s: 'unix:///run/k3s/containerd/containerd.sock',
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* @method resolveCriSocket
|
|
27
|
+
* @description Resolves the CRI endpoint a `crictl` or `kubeadm` call should
|
|
28
|
+
* target. K3s runs its own embedded containerd; a kubeadm host uses CRI-O only
|
|
29
|
+
* while that socket actually exists, otherwise the host-level containerd.
|
|
30
|
+
*
|
|
31
|
+
* Detection is by socket presence, never by configuration, so a host whose
|
|
32
|
+
* CRI-O install was removed or stopped still resolves to a runtime that answers.
|
|
33
|
+
* @param {object} [options] - Resolution inputs.
|
|
34
|
+
* @param {boolean} [options.k3s=false] - Whether the cluster is K3s-based.
|
|
35
|
+
* @param {string} [options.criSocket] - Explicit endpoint override (highest precedence).
|
|
36
|
+
* @returns {string} CRI endpoint URI.
|
|
37
|
+
* @memberof CriEndpoint
|
|
38
|
+
*/
|
|
39
|
+
const resolveCriSocket = (options = {}) => {
|
|
40
|
+
if (options?.criSocket) return options.criSocket;
|
|
41
|
+
if (options?.k3s) return CRI_SOCKETS.k3s;
|
|
42
|
+
const runtime = shellExec(`test -S ${CRIO_SOCKET_PATH} && echo crio || echo containerd`, {
|
|
43
|
+
stdout: true,
|
|
44
|
+
silent: true,
|
|
45
|
+
}).trim();
|
|
46
|
+
return runtime === 'crio' ? CRI_SOCKETS.crio : CRI_SOCKETS.containerd;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* @method crictlCommandFactory
|
|
51
|
+
* @description Builds a `crictl` invocation pinned to the live CRI endpoint.
|
|
52
|
+
*
|
|
53
|
+
* Both `--runtime-endpoint` and `--image-endpoint` are passed. crictl resolves
|
|
54
|
+
* the two independently, and `run install-crio` writes both into
|
|
55
|
+
* `/etc/crictl.yaml` pointing at CRI-O; overriding only the runtime leaves image
|
|
56
|
+
* operations (`pull`, `rmi`, `images`) validating a `crio.sock` that no longer
|
|
57
|
+
* exists, which fails with "validate CRI v1 image API for endpoint".
|
|
58
|
+
*
|
|
59
|
+
* crictl is not on sudo's secure_path, hence the explicit PATH.
|
|
60
|
+
* @param {string} args - crictl subcommand and arguments (e.g. `pull mongo:latest`).
|
|
61
|
+
* @param {object} [options] - Forwarded to {@link CriEndpoint.resolveCriSocket}.
|
|
62
|
+
* @returns {string} Full shell command.
|
|
63
|
+
* @memberof CriEndpoint
|
|
64
|
+
*/
|
|
65
|
+
const crictlCommandFactory = (args, options = {}) => {
|
|
66
|
+
const socket = resolveCriSocket(options);
|
|
67
|
+
return `sudo env PATH="$PATH:/usr/local/bin:/usr/bin" crictl --runtime-endpoint ${socket} --image-endpoint ${socket} ${args}`;
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
export { CRI_SOCKETS, resolveCriSocket, crictlCommandFactory };
|
package/src/server/cron.js
CHANGED
|
@@ -7,8 +7,9 @@
|
|
|
7
7
|
import { loggerFactory } from './logger.js';
|
|
8
8
|
import { shellExec } from './process.js';
|
|
9
9
|
import fs from 'fs-extra';
|
|
10
|
+
import dotenv from 'dotenv';
|
|
10
11
|
import Underpost from '../index.js';
|
|
11
|
-
import { getUnderpostRootPath
|
|
12
|
+
import { getUnderpostRootPath } from './environment.js';
|
|
12
13
|
|
|
13
14
|
const logger = loggerFactory(import.meta);
|
|
14
15
|
|
|
@@ -17,6 +18,45 @@ const enginePath = '/home/dd/engine';
|
|
|
17
18
|
const cronVolumeName = 'underpost-cron-container-volume';
|
|
18
19
|
const shareEnvVolumeName = 'underpost-share-env';
|
|
19
20
|
const underpostContainerEnvDir = '/usr/lib/node_modules/underpost';
|
|
21
|
+
const DEFAULT_CRON_ID = 'dd-cron';
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Resolves the deploy ID stored in `engine-private/deploy/dd.cron`.
|
|
25
|
+
* @returns {string|null}
|
|
26
|
+
* @memberof UnderpostCron
|
|
27
|
+
*/
|
|
28
|
+
const cronDeployIdResolve = () => {
|
|
29
|
+
const path = './engine-private/deploy/dd.cron';
|
|
30
|
+
if (!fs.existsSync(path)) return null;
|
|
31
|
+
return fs.readFileSync(path, 'utf8').trim() || null;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Loads cron and router deployment environment files into `process.env`.
|
|
36
|
+
* @returns {void}
|
|
37
|
+
* @memberof UnderpostCron
|
|
38
|
+
*/
|
|
39
|
+
const loadCronDeployEnv = () => {
|
|
40
|
+
const envName = process.env.NODE_ENV || 'production';
|
|
41
|
+
const cronDeployId = cronDeployIdResolve();
|
|
42
|
+
|
|
43
|
+
if (cronDeployId) {
|
|
44
|
+
const path = `./engine-private/conf/${cronDeployId}/.env.${envName}`;
|
|
45
|
+
if (fs.existsSync(path)) process.env = { ...process.env, ...dotenv.parse(fs.readFileSync(path, 'utf8')) };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const routerPath = './engine-private/deploy/dd.router';
|
|
49
|
+
if (!fs.existsSync(routerPath)) return;
|
|
50
|
+
for (const deployId of fs.readFileSync(routerPath, 'utf8').trim().split(',')) {
|
|
51
|
+
const id = deployId.trim();
|
|
52
|
+
const path = `./engine-private/conf/${id}/.env.${envName}`;
|
|
53
|
+
if (!id || !fs.existsSync(path)) continue;
|
|
54
|
+
const env = dotenv.parse(fs.readFileSync(path, 'utf8'));
|
|
55
|
+
for (const [key, value] of Object.entries(env)) {
|
|
56
|
+
if (!(key in process.env)) process.env[key] = value;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
};
|
|
20
60
|
|
|
21
61
|
/**
|
|
22
62
|
* Generates a Kubernetes CronJob YAML manifest string.
|
|
@@ -36,6 +76,8 @@ const underpostContainerEnvDir = '/usr/lib/node_modules/underpost';
|
|
|
36
76
|
* @param {boolean} [params.k3s=false] - Pass --k3s flag to the cron command inside the container
|
|
37
77
|
* @param {boolean} [params.kind=false] - Pass --kind flag to the cron command inside the container
|
|
38
78
|
* @param {boolean} [params.kubeadm=false] - Pass --kubeadm flag to the cron command inside the container
|
|
79
|
+
* @param {string} [params.nodeName] - Pin the Job pod to this node via a `kubernetes.io/hostname` nodeSelector.
|
|
80
|
+
* Placement is a manifest concern only, so it is never forwarded to the cron command inside the container.
|
|
39
81
|
* @returns {string} Kubernetes CronJob YAML manifest
|
|
40
82
|
* @memberof UnderpostCron
|
|
41
83
|
*/
|
|
@@ -54,6 +96,7 @@ const cronJobYamlFactory = ({
|
|
|
54
96
|
k3s = false,
|
|
55
97
|
kind = false,
|
|
56
98
|
kubeadm = false,
|
|
99
|
+
nodeName = '',
|
|
57
100
|
}) => {
|
|
58
101
|
const containerImage = image || `underpost/underpost-engine:${Underpost.version}`;
|
|
59
102
|
|
|
@@ -64,9 +107,11 @@ const cronJobYamlFactory = ({
|
|
|
64
107
|
.replace(/^-|-$/g, '')
|
|
65
108
|
.substring(0, 52);
|
|
66
109
|
|
|
110
|
+
const cronDeployId = cronDeployIdResolve();
|
|
111
|
+
|
|
67
112
|
const cronBin = 'node bin'; // dev ? 'node bin' : 'underpost';
|
|
68
113
|
const flags = `${git ? '--git ' : ''}${dev ? '--dev ' : ''}${dryRun ? '--dry-run ' : ''}${k3s ? '--k3s ' : ''}${kind ? '--kind ' : ''}${kubeadm ? '--kubeadm ' : ''}`;
|
|
69
|
-
const commands = [`cd ${enginePath}`]; // `node bin run secret`
|
|
114
|
+
const commands = [`cd ${enginePath}`, `node bin env ${cronDeployId} ${dev ? `development` : `production`}`]; // `node bin run secret`
|
|
70
115
|
if (cmd) commands.push(cmd);
|
|
71
116
|
commands.push(`${cronBin} cron ${deployList} ${jobList} ${flags}`);
|
|
72
117
|
const fullCommand = commands.join(' &&\n ');
|
|
@@ -87,6 +132,10 @@ spec:
|
|
|
87
132
|
failedJobsHistoryLimit: 1
|
|
88
133
|
suspend: ${suspend}
|
|
89
134
|
jobTemplate:
|
|
135
|
+
metadata:
|
|
136
|
+
labels:
|
|
137
|
+
app: ${sanitizedName}
|
|
138
|
+
managed-by: underpost
|
|
90
139
|
spec:
|
|
91
140
|
template:
|
|
92
141
|
metadata:
|
|
@@ -94,7 +143,13 @@ spec:
|
|
|
94
143
|
app: ${sanitizedName}
|
|
95
144
|
managed-by: underpost
|
|
96
145
|
spec:
|
|
97
|
-
|
|
146
|
+
${
|
|
147
|
+
nodeName
|
|
148
|
+
? ` nodeSelector:
|
|
149
|
+
kubernetes.io/hostname: ${nodeName}
|
|
150
|
+
`
|
|
151
|
+
: ''
|
|
152
|
+
} containers:
|
|
98
153
|
- name: ${sanitizedName}
|
|
99
154
|
image: ${containerImage}
|
|
100
155
|
command:
|
|
@@ -138,20 +193,90 @@ const syncEngineToKindWorker = () => {
|
|
|
138
193
|
|
|
139
194
|
/**
|
|
140
195
|
* Resolves the deploy-id to use for cron job generation.
|
|
141
|
-
*
|
|
196
|
+
* Uses the explicit value or the deploy ID stored in `dd.cron`.
|
|
142
197
|
*
|
|
143
198
|
* @param {string} [deployId] - Explicit deploy-id override
|
|
144
199
|
* @memberof UnderpostCron
|
|
145
200
|
* @returns {string|null} Resolved deploy-id or null if not found
|
|
146
201
|
*/
|
|
147
|
-
const resolveDeployId = (deployId) =>
|
|
148
|
-
if (deployId) return deployId;
|
|
202
|
+
const resolveDeployId = (deployId) => deployId || cronDeployIdResolve();
|
|
149
203
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
204
|
+
/**
|
|
205
|
+
* Parses a comma-separated CLI list into a trimmed, non-empty array.
|
|
206
|
+
*
|
|
207
|
+
* @param {string|string[]} [value] - Raw CLI value
|
|
208
|
+
* @memberof UnderpostCron
|
|
209
|
+
* @returns {string[]} Parsed entries
|
|
210
|
+
*/
|
|
211
|
+
const parseList = (value) => {
|
|
212
|
+
if (Array.isArray(value)) return value.map((entry) => `${entry}`.trim()).filter(Boolean);
|
|
213
|
+
if (typeof value !== 'string') return [];
|
|
214
|
+
return value
|
|
215
|
+
.split(',')
|
|
216
|
+
.map((entry) => entry.trim())
|
|
217
|
+
.filter(Boolean);
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Normalizes and validates a `--node-name` value before it reaches a manifest or a shell.
|
|
222
|
+
*
|
|
223
|
+
* @param {string} [nodeName] - Raw CLI value
|
|
224
|
+
* @memberof UnderpostCron
|
|
225
|
+
* @returns {string} Trimmed node name, or '' when unset
|
|
226
|
+
* @throws {Error} When the value is not a valid Kubernetes node name
|
|
227
|
+
*/
|
|
228
|
+
const resolveNodeName = (nodeName) => {
|
|
229
|
+
const node = `${nodeName || ''}`.trim();
|
|
230
|
+
if (node && !/^[a-zA-Z0-9._-]+$/.test(node)) throw new Error(`Invalid Kubernetes node name: ${node}`);
|
|
231
|
+
return node;
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Checks whether a node is registered on the cluster.
|
|
236
|
+
*
|
|
237
|
+
* @param {string} nodeName - Node name
|
|
238
|
+
* @memberof UnderpostCron
|
|
239
|
+
* @returns {boolean} True when the node exists
|
|
240
|
+
*/
|
|
241
|
+
const nodeExists = (nodeName) => {
|
|
242
|
+
const stdout = shellExec(`kubectl get node ${nodeName} -o name`, {
|
|
243
|
+
silent: true,
|
|
244
|
+
stdout: true,
|
|
245
|
+
silentOnError: true,
|
|
246
|
+
disableLog: true,
|
|
247
|
+
});
|
|
248
|
+
return `${stdout || ''}`.trim().length > 0;
|
|
249
|
+
};
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Checks whether a CronJob is already published on the cluster.
|
|
253
|
+
*
|
|
254
|
+
* @param {string} cronJobName - Sanitized CronJob name
|
|
255
|
+
* @param {string} namespace - Kubernetes namespace
|
|
256
|
+
* @memberof UnderpostCron
|
|
257
|
+
* @returns {boolean} True when the CronJob exists
|
|
258
|
+
*/
|
|
259
|
+
const cronJobExists = (cronJobName, namespace) => {
|
|
260
|
+
const stdout = shellExec(`kubectl get cronjob ${cronJobName} -n ${namespace} --ignore-not-found -o name`, {
|
|
261
|
+
silent: true,
|
|
262
|
+
stdout: true,
|
|
263
|
+
silentOnError: true,
|
|
264
|
+
disableLog: true,
|
|
265
|
+
});
|
|
266
|
+
return `${stdout || ''}`.trim().length > 0;
|
|
267
|
+
};
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Resolves the manifest owner deploy-id from the `deploy-list` positional argument.
|
|
271
|
+
* The `default` sentinel means "not provided", deferring to the dd.cron file.
|
|
272
|
+
*
|
|
273
|
+
* @param {string} [deployList] - Comma-separated deploy IDs from the CLI
|
|
274
|
+
* @memberof UnderpostCron
|
|
275
|
+
* @returns {string|undefined} Owner deploy-id, or undefined when unspecified
|
|
276
|
+
*/
|
|
277
|
+
const deployIdFromList = (deployList) => {
|
|
278
|
+
const [deployId] = parseList(deployList);
|
|
279
|
+
return !deployId || deployId === 'default' ? undefined : deployId;
|
|
155
280
|
};
|
|
156
281
|
|
|
157
282
|
/**
|
|
@@ -165,6 +290,7 @@ class UnderpostCron {
|
|
|
165
290
|
return {
|
|
166
291
|
dns: Underpost.dns,
|
|
167
292
|
backup: Underpost.backup,
|
|
293
|
+
vultr: Underpost.vultr,
|
|
168
294
|
};
|
|
169
295
|
}
|
|
170
296
|
|
|
@@ -172,8 +298,12 @@ class UnderpostCron {
|
|
|
172
298
|
/**
|
|
173
299
|
* CLI entry point for the `underpost cron` command.
|
|
174
300
|
*
|
|
175
|
-
*
|
|
176
|
-
*
|
|
301
|
+
* Manifest modes (`--setup-start`, `--generate-k8s-cronjobs`, `--apply`, `--create-job-now`)
|
|
302
|
+
* never run job callbacks in this process: they write and publish manifests, and hand the
|
|
303
|
+
* work to the cluster. All of them are scoped to `job-list` when given.
|
|
304
|
+
*
|
|
305
|
+
* @param {string} deployList - Comma-separated deploy IDs; in manifest modes its first entry is the manifest owner deploy-id
|
|
306
|
+
* @param {string} jobList - Comma-separated job IDs; in manifest modes it restricts which conf.cron.json jobs are generated
|
|
177
307
|
* @param {Object} options - CLI flags
|
|
178
308
|
* @param {boolean} [options.generateK8sCronjobs] - Generate K8s CronJob YAML manifests
|
|
179
309
|
* @param {boolean} [options.apply] - Apply manifests to the cluster
|
|
@@ -182,32 +312,44 @@ class UnderpostCron {
|
|
|
182
312
|
* @param {string} [options.cmd] - Optional pre-script commands to run before cron execution
|
|
183
313
|
* @param {string} [options.namespace] - Kubernetes namespace
|
|
184
314
|
* @param {string} [options.image] - Custom container image
|
|
185
|
-
* @param {
|
|
315
|
+
* @param {boolean} [options.setupStart] - Update the deploy-id package.json start script and generate+apply its cron jobs
|
|
186
316
|
* @param {boolean} [options.k3s] - Use k3s cluster context (apply directly on host)
|
|
187
317
|
* @param {boolean} [options.kind] - Use kind cluster context (apply via kind-worker container)
|
|
188
318
|
* @param {boolean} [options.kubeadm] - Use kubeadm cluster context (apply directly on host)
|
|
189
319
|
* @param {boolean} [options.dryRun] - Preview cron jobs without executing them
|
|
190
320
|
* @param {boolean} [options.createJobNow] - After applying, immediately create a Job from each CronJob (requires --apply)
|
|
321
|
+
* @param {string} [options.nodeName] - Pin generated CronJob pods to this node (manifest modes only)
|
|
191
322
|
* @memberof UnderpostCron
|
|
192
323
|
*/
|
|
193
|
-
callback: async function (
|
|
194
|
-
deployList = 'default',
|
|
195
|
-
jobList = Object.keys(Underpost.cron.JOB).join(','),
|
|
196
|
-
options = {},
|
|
197
|
-
) {
|
|
324
|
+
callback: async function (deployList, jobList, options = {}) {
|
|
198
325
|
loadCronDeployEnv();
|
|
199
|
-
if (options.setupStart) return await Underpost.cron.setupDeployStart(options.setupStart, options);
|
|
200
326
|
|
|
201
|
-
|
|
327
|
+
const jobFilter = parseList(jobList);
|
|
328
|
+
|
|
329
|
+
if (options.setupStart) return await Underpost.cron.setupDeployStart(deployList, { ...options, jobFilter });
|
|
330
|
+
|
|
331
|
+
if (options.generateK8sCronjobs || options.apply || options.createJobNow)
|
|
332
|
+
return await Underpost.cron.generateK8sCronJobs({
|
|
333
|
+
...options,
|
|
334
|
+
deployId: deployIdFromList(deployList),
|
|
335
|
+
jobFilter,
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
const resolvedDeployList = deployList || 'default';
|
|
339
|
+
const resolvedJobList = jobFilter.length > 0 ? jobFilter : Object.keys(Underpost.cron.JOB);
|
|
340
|
+
|
|
341
|
+
if (options.nodeName)
|
|
342
|
+
logger.warn(`--node-name is a manifest placement flag and has no effect on direct execution`, {
|
|
343
|
+
nodeName: options.nodeName,
|
|
344
|
+
});
|
|
202
345
|
|
|
203
|
-
for (const
|
|
204
|
-
const jobId = _jobId.trim();
|
|
346
|
+
for (const jobId of resolvedJobList) {
|
|
205
347
|
if (Underpost.cron.JOB[jobId]) {
|
|
206
348
|
if (options.dryRun) {
|
|
207
|
-
logger.info(`[dry-run] Would execute cron job`, { jobId, deployList, options });
|
|
349
|
+
logger.info(`[dry-run] Would execute cron job`, { jobId, deployList: resolvedDeployList, options });
|
|
208
350
|
} else {
|
|
209
|
-
logger.info(`Executing cron job`, { jobId, deployList, options });
|
|
210
|
-
await Underpost.cron.JOB[jobId].callback(
|
|
351
|
+
logger.info(`Executing cron job`, { jobId, deployList: resolvedDeployList, options });
|
|
352
|
+
await Underpost.cron.JOB[jobId].callback(resolvedDeployList, options);
|
|
211
353
|
}
|
|
212
354
|
} else {
|
|
213
355
|
logger.warn(`Unknown cron job: ${jobId}`);
|
|
@@ -218,14 +360,16 @@ class UnderpostCron {
|
|
|
218
360
|
/**
|
|
219
361
|
* Update the package.json start script for the given deploy-id and generate+apply its K8s CronJob manifests.
|
|
220
362
|
*
|
|
221
|
-
* @param {string}
|
|
363
|
+
* @param {string} [deployList] - Comma-separated deploy IDs; its first entry is the deploy-id whose package.json is updated. Falls back to the dd.cron file
|
|
222
364
|
* @param {Object} [options] - Additional options forwarded to generateK8sCronJobs
|
|
365
|
+
* @param {string[]} [options.jobFilter] - Restrict the setup to these job IDs
|
|
223
366
|
* @param {boolean} [options.createJobNow] - After applying, immediately create a Job from each CronJob
|
|
224
367
|
* @param {boolean} [options.dryRun] - Pass --dry-run=client to kubectl commands
|
|
225
368
|
* @param {boolean} [options.apply] - Whether to apply generated manifests to the cluster
|
|
226
369
|
* @param {boolean} [options.git] - Pass --git flag to cron CLI commands
|
|
227
370
|
* @param {boolean} [options.dev] - Use local ./ base path instead of global underpost installation
|
|
228
371
|
* @param {string} [options.cmd] - Optional pre-script commands to run before cron execution
|
|
372
|
+
* @param {string} [options.nodeName] - Pin every generated CronJob's pod to this node
|
|
229
373
|
* @param {string} [options.namespace] - Kubernetes namespace for the CronJobs
|
|
230
374
|
* @param {string} [options.image] - Custom container image override for the CronJobs
|
|
231
375
|
* @param {boolean} [options.k3s] - k3s cluster context (apply directly on host)
|
|
@@ -233,8 +377,18 @@ class UnderpostCron {
|
|
|
233
377
|
* @param {boolean} [options.kubeadm] - kubeadm cluster context (apply directly on host)
|
|
234
378
|
* @memberof UnderpostCron
|
|
235
379
|
*/
|
|
236
|
-
setupDeployStart: async function (
|
|
237
|
-
|
|
380
|
+
setupDeployStart: async function (deployList, options = {}) {
|
|
381
|
+
// Validated up front: an invalid node name must not leave a rewritten package.json behind.
|
|
382
|
+
const nodeName = resolveNodeName(options.nodeName);
|
|
383
|
+
const requestedDeployId = deployIdFromList(deployList);
|
|
384
|
+
const deployId = resolveDeployId(requestedDeployId);
|
|
385
|
+
if (!deployId) {
|
|
386
|
+
logger.warn(
|
|
387
|
+
'Could not resolve deploy-id. Provide it as the deploy-list argument or create engine-private/deploy/dd.cron',
|
|
388
|
+
);
|
|
389
|
+
return;
|
|
390
|
+
}
|
|
391
|
+
if (!requestedDeployId) logger.info(`Resolved cron deploy-id from dd.cron`, { deployId });
|
|
238
392
|
const confDir = `./engine-private/conf/${deployId}`;
|
|
239
393
|
const packageJsonPath = `${confDir}/package.json`;
|
|
240
394
|
const confCronPath = `${confDir}/conf.cron.json`;
|
|
@@ -251,17 +405,23 @@ class UnderpostCron {
|
|
|
251
405
|
return;
|
|
252
406
|
}
|
|
253
407
|
|
|
254
|
-
const
|
|
255
|
-
|
|
256
|
-
|
|
408
|
+
const jobFilter = parseList(options.jobFilter);
|
|
409
|
+
const enabledJobs = Object.keys(confCron.jobs).filter(
|
|
410
|
+
(job) => confCron.jobs[job].enabled !== false && (jobFilter.length === 0 || jobFilter.includes(job)),
|
|
411
|
+
);
|
|
412
|
+
if (enabledJobs.length === 0) {
|
|
413
|
+
logger.warn(
|
|
414
|
+
`No enabled cron jobs for deploy-id: ${deployId}`,
|
|
415
|
+
jobFilter.length > 0 ? { jobFilter } : undefined,
|
|
416
|
+
);
|
|
257
417
|
return;
|
|
258
418
|
}
|
|
259
419
|
|
|
260
|
-
//
|
|
420
|
+
// Start script only references manifests generateK8sCronJobs actually writes
|
|
261
421
|
if (fs.existsSync(packageJsonPath)) {
|
|
262
422
|
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
|
|
263
423
|
let startCommand = 'echo "Starting cron jobs..."';
|
|
264
|
-
for (const job of
|
|
424
|
+
for (const job of enabledJobs)
|
|
265
425
|
startCommand += ` && kubectl apply -f ./manifests/cronjobs/${deployId}/${deployId}-${job}.yaml`;
|
|
266
426
|
if (!packageJson.scripts) packageJson.scripts = {};
|
|
267
427
|
packageJson.scripts.start = startCommand;
|
|
@@ -274,6 +434,8 @@ class UnderpostCron {
|
|
|
274
434
|
|
|
275
435
|
await Underpost.cron.generateK8sCronJobs({
|
|
276
436
|
deployId,
|
|
437
|
+
jobFilter,
|
|
438
|
+
nodeName,
|
|
277
439
|
namespace: options.namespace,
|
|
278
440
|
image: options.image,
|
|
279
441
|
apply: options.apply,
|
|
@@ -281,7 +443,7 @@ class UnderpostCron {
|
|
|
281
443
|
git: !!options.git,
|
|
282
444
|
dev: !!options.dev,
|
|
283
445
|
kubeadm: !!options.kubeadm,
|
|
284
|
-
cmd: options.cmd
|
|
446
|
+
cmd: options.cmd,
|
|
285
447
|
k3s: !!options.k3s,
|
|
286
448
|
kind: !!options.kind,
|
|
287
449
|
dryRun: !!options.dryRun,
|
|
@@ -295,6 +457,7 @@ class UnderpostCron {
|
|
|
295
457
|
*
|
|
296
458
|
* @param {Object} options
|
|
297
459
|
* @param {string} [options.deployId] - Explicit deploy-id (overrides dd.cron file lookup)
|
|
460
|
+
* @param {string|string[]} [options.jobFilter] - Restrict generation/apply to these job IDs (empty means all)
|
|
298
461
|
* @param {boolean} [options.git=false] - Pass --git flag to cron CLI commands
|
|
299
462
|
* @param {boolean} [options.dev=false] - Use local ./ base path instead of global underpost
|
|
300
463
|
* @param {string} [options.cmd] - Optional pre-script commands
|
|
@@ -306,15 +469,17 @@ class UnderpostCron {
|
|
|
306
469
|
* @param {boolean} [options.kubeadm=false] - kubeadm cluster context (apply directly on host)
|
|
307
470
|
* @param {boolean} [options.createJobNow=false] - After applying, create a Job from each CronJob immediately
|
|
308
471
|
* @param {boolean} [options.dryRun=false] - Pass --dry-run=client to kubectl commands
|
|
472
|
+
* @param {string} [options.nodeName] - Pin every generated CronJob's pod to this node
|
|
309
473
|
* @memberof UnderpostCron
|
|
310
474
|
*/
|
|
311
475
|
generateK8sCronJobs: async function (options = {}) {
|
|
312
476
|
const namespace = options.namespace || 'default';
|
|
477
|
+
const nodeName = resolveNodeName(options.nodeName);
|
|
313
478
|
const jobDeployId = resolveDeployId(options.deployId);
|
|
314
479
|
|
|
315
480
|
if (!jobDeployId) {
|
|
316
481
|
logger.warn(
|
|
317
|
-
'Could not resolve deploy-id. Provide
|
|
482
|
+
'Could not resolve deploy-id. Provide it as the deploy-list argument or create engine-private/deploy/dd.cron',
|
|
318
483
|
);
|
|
319
484
|
return;
|
|
320
485
|
}
|
|
@@ -333,12 +498,26 @@ class UnderpostCron {
|
|
|
333
498
|
return;
|
|
334
499
|
}
|
|
335
500
|
|
|
501
|
+
const jobFilter = parseList(options.jobFilter);
|
|
502
|
+
const targetJobs = Object.keys(confCronConfig.jobs).filter(
|
|
503
|
+
(job) => jobFilter.length === 0 || jobFilter.includes(job),
|
|
504
|
+
);
|
|
505
|
+
|
|
506
|
+
if (targetJobs.length === 0) {
|
|
507
|
+
logger.warn(`No cron jobs matched the requested job list`, {
|
|
508
|
+
deployId: jobDeployId,
|
|
509
|
+
jobFilter,
|
|
510
|
+
available: Object.keys(confCronConfig.jobs),
|
|
511
|
+
});
|
|
512
|
+
return;
|
|
513
|
+
}
|
|
514
|
+
|
|
336
515
|
const outputDir = `./manifests/cronjobs/${jobDeployId}`;
|
|
337
516
|
fs.mkdirSync(outputDir, { recursive: true });
|
|
338
517
|
|
|
339
518
|
const generatedFiles = [];
|
|
340
519
|
|
|
341
|
-
for (const job of
|
|
520
|
+
for (const job of targetJobs) {
|
|
342
521
|
const jobConfig = confCronConfig.jobs[job];
|
|
343
522
|
|
|
344
523
|
if (jobConfig.enabled === false) {
|
|
@@ -365,18 +544,29 @@ class UnderpostCron {
|
|
|
365
544
|
k3s: !!options.k3s,
|
|
366
545
|
kind: !!options.kind,
|
|
367
546
|
kubeadm: !!options.kubeadm,
|
|
547
|
+
nodeName,
|
|
368
548
|
});
|
|
369
549
|
|
|
370
550
|
const yamlFilePath = `${outputDir}/${cronJobName}.yaml`;
|
|
371
551
|
fs.writeFileSync(yamlFilePath, yamlContent, 'utf8');
|
|
372
552
|
generatedFiles.push(yamlFilePath);
|
|
373
553
|
|
|
374
|
-
logger.info(`Generated CronJob manifest: ${yamlFilePath}`, {
|
|
554
|
+
logger.info(`Generated CronJob manifest: ${yamlFilePath}`, {
|
|
555
|
+
job,
|
|
556
|
+
expression,
|
|
557
|
+
namespace,
|
|
558
|
+
...(nodeName ? { nodeName } : {}),
|
|
559
|
+
});
|
|
375
560
|
}
|
|
376
561
|
|
|
377
562
|
if (options.apply) {
|
|
563
|
+
// A nodeSelector naming a node that is not registered leaves every Job Pending at its
|
|
564
|
+
// next fire, silently. Warn rather than throw: the node may join before the schedule.
|
|
565
|
+
if (nodeName && !nodeExists(nodeName))
|
|
566
|
+
logger.warn(`Target node not found on the cluster; pods will stay Pending until it joins`, { nodeName });
|
|
567
|
+
|
|
378
568
|
// Delete existing CronJobs before applying new ones
|
|
379
|
-
for (const job of
|
|
569
|
+
for (const job of targetJobs) {
|
|
380
570
|
const cronJobName = `${jobDeployId}-${job}`;
|
|
381
571
|
shellExec(`kubectl delete cronjob ${cronJobName} --namespace=${namespace} --ignore-not-found`);
|
|
382
572
|
}
|
|
@@ -406,9 +596,10 @@ class UnderpostCron {
|
|
|
406
596
|
} else {
|
|
407
597
|
logger.info(`Manifests generated in ${outputDir}. Use --apply to deploy to the cluster.`);
|
|
408
598
|
}
|
|
409
|
-
// Create an immediate Job from each CronJob if requested
|
|
599
|
+
// Create an immediate Job from each CronJob if requested. Runs after --apply so the
|
|
600
|
+
// Job is always cloned from the manifest this invocation just published.
|
|
410
601
|
if (options.createJobNow) {
|
|
411
|
-
for (const job of
|
|
602
|
+
for (const job of targetJobs) {
|
|
412
603
|
const jobConfig = confCronConfig.jobs[job];
|
|
413
604
|
if (jobConfig.enabled === false) continue;
|
|
414
605
|
|
|
@@ -419,6 +610,15 @@ class UnderpostCron {
|
|
|
419
610
|
.replace(/^-|-$/g, '')
|
|
420
611
|
.substring(0, 52);
|
|
421
612
|
|
|
613
|
+
if (!cronJobExists(cronJobName, namespace)) {
|
|
614
|
+
logger.warn(`CronJob not found on the cluster, skipping immediate Job`, {
|
|
615
|
+
cronJobName,
|
|
616
|
+
namespace,
|
|
617
|
+
hint: 'add --apply to publish the manifest first',
|
|
618
|
+
});
|
|
619
|
+
continue;
|
|
620
|
+
}
|
|
621
|
+
|
|
422
622
|
const immediateJobName = `${cronJobName}-now-${Date.now()}`.substring(0, 63);
|
|
423
623
|
logger.info(`Creating immediate Job from CronJob: ${cronJobName}`, { jobName: immediateJobName });
|
|
424
624
|
shellExec(`kubectl create job ${immediateJobName} --from=cronjob/${cronJobName} -n ${namespace}`);
|
|
@@ -436,17 +636,15 @@ class UnderpostCron {
|
|
|
436
636
|
* @memberof UnderpostCron
|
|
437
637
|
*/
|
|
438
638
|
getRelatedDeployIdList(jobId) {
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
logger.warn(`Deploy file not found: ${deployFilePath}, using default`);
|
|
444
|
-
return fs.existsSync('./engine-private/deploy/dd.cron')
|
|
445
|
-
? fs.readFileSync('./engine-private/deploy/dd.cron', 'utf8').trim()
|
|
446
|
-
: 'dd-cron';
|
|
639
|
+
if (jobId === 'backup') {
|
|
640
|
+
const routerFilePath = './engine-private/deploy/dd.router';
|
|
641
|
+
if (fs.existsSync(routerFilePath)) return fs.readFileSync(routerFilePath, 'utf8').trim();
|
|
642
|
+
logger.warn(`Deploy file not found: ${routerFilePath}, falling back to the cron deploy-id`);
|
|
447
643
|
}
|
|
448
644
|
|
|
449
|
-
|
|
645
|
+
const cronDeployId = cronDeployIdResolve();
|
|
646
|
+
if (!cronDeployId) logger.warn(`Cron deploy-id not resolved, using default`, { jobId, default: DEFAULT_CRON_ID });
|
|
647
|
+
return cronDeployId || DEFAULT_CRON_ID;
|
|
450
648
|
},
|
|
451
649
|
|
|
452
650
|
/**
|
|
@@ -473,4 +671,4 @@ class UnderpostCron {
|
|
|
473
671
|
|
|
474
672
|
export default UnderpostCron;
|
|
475
673
|
|
|
476
|
-
export { cronJobYamlFactory, resolveDeployId };
|
|
674
|
+
export { cronDeployIdResolve, cronJobYamlFactory, loadCronDeployEnv, resolveDeployId };
|