underpost 3.2.90 → 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 +110 -1
- package/CLI-HELP.md +139 -9
- package/README.md +5 -2
- package/bin/build.js +7 -5
- package/bin/deploy.js +1 -1
- 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/package.json +5 -5
- 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 +2 -0
- package/scripts/rhel-grpc-setup.sh +0 -0
- package/scripts/rocky-kickstart.sh +25 -9
- package/scripts/test-monitor.sh +1 -1
- package/src/cli/baremetal.js +1 -2
- package/src/cli/cloud-init.js +1 -1
- package/src/cli/cluster.js +73 -68
- package/src/cli/db.js +9 -2
- package/src/cli/deploy.js +21 -5
- package/src/cli/docker-compose.js +1 -1
- package/src/cli/env.js +1 -1
- package/src/cli/image.js +0 -1
- package/src/cli/index.js +121 -9
- package/src/cli/lxd.js +1 -1
- package/src/cli/monitor.js +1 -1
- package/src/cli/release.js +57 -22
- package/src/cli/repository.js +11 -9
- package/src/cli/run.js +36 -9
- package/src/cli/ssh.js +198 -77
- 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 +20 -14
- package/src/db/mongo/MongooseDB.js +4 -0
- 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 +18 -108
- 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 +20 -10
- package/src/server/underpost-ingress.js +18 -2
- package/test/selinux.test.js +71 -0
- package/test/underpost-gateway.test.js +41 -0
- package/test/underpost-ingress.test.js +52 -0
- package/test/wireguard-edge.test.js +1177 -0
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 };
|
package/src/server/dns.js
CHANGED
|
@@ -5,15 +5,13 @@
|
|
|
5
5
|
* @namespace UnderpostDns
|
|
6
6
|
*/
|
|
7
7
|
import axios from 'axios';
|
|
8
|
-
import fs from 'fs';
|
|
9
8
|
import validator from 'validator';
|
|
10
9
|
import { loggerFactory } from './logger.js';
|
|
11
10
|
import dns from 'node:dns';
|
|
12
11
|
import os from 'node:os';
|
|
13
12
|
import { shellExec, pbcopy } from './process.js';
|
|
14
13
|
import Underpost from '../index.js';
|
|
15
|
-
import {
|
|
16
|
-
import { resolveDeployId } from './cron.js';
|
|
14
|
+
import { readConfJson } from './conf.js';
|
|
17
15
|
|
|
18
16
|
const logger = loggerFactory(import.meta);
|
|
19
17
|
|
|
@@ -271,6 +269,83 @@ class Dns {
|
|
|
271
269
|
logger.info('Cleared all egress bans.');
|
|
272
270
|
}
|
|
273
271
|
|
|
272
|
+
/**
|
|
273
|
+
* Blocks all outbound traffic from this host, except for established/related connections.
|
|
274
|
+
* This is useful for security purposes, especially in a dynamic DNS context where you want to prevent
|
|
275
|
+
* any new outbound connections while still allowing existing ones (like SSH) to continue.
|
|
276
|
+
* @static
|
|
277
|
+
* @memberof UnderpostDns
|
|
278
|
+
*/
|
|
279
|
+
static blockAllEgress() {
|
|
280
|
+
// Clear any existing egress rules.
|
|
281
|
+
shellExec(`sudo nft flush chain inet filter output`, { silent: true });
|
|
282
|
+
shellExec(`sudo nft flush chain inet filter forward`, { silent: true });
|
|
283
|
+
|
|
284
|
+
// Allow return traffic for established/related connections.
|
|
285
|
+
// This keeps existing inbound connections such as SSH alive.
|
|
286
|
+
shellExec(`sudo nft add rule inet filter output ct state established,related counter accept`, { silent: true });
|
|
287
|
+
|
|
288
|
+
// Block all new outbound connections from this host and forwarded traffic.
|
|
289
|
+
shellExec(`sudo nft chain inet filter output '{ policy drop; }'`, { silent: true });
|
|
290
|
+
shellExec(`sudo nft chain inet filter forward '{ policy drop; }'`, { silent: true });
|
|
291
|
+
|
|
292
|
+
logger.info('All outbound traffic blocked.');
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Unblocks all outbound traffic from this host and forwarded interfaces.
|
|
297
|
+
* Restores default output and forward chain policies to ACCEPT and clears egress rules.
|
|
298
|
+
* @static
|
|
299
|
+
* @memberof UnderpostDns
|
|
300
|
+
*/
|
|
301
|
+
static unblockAllEgress() {
|
|
302
|
+
// Restore default chain policies to accept all traffic.
|
|
303
|
+
shellExec(`sudo nft chain inet filter output '{ policy accept; }'`, { silent: true });
|
|
304
|
+
shellExec(`sudo nft chain inet filter forward '{ policy accept; }'`, { silent: true });
|
|
305
|
+
|
|
306
|
+
// Clear any existing egress blocking rules.
|
|
307
|
+
shellExec(`sudo nft flush chain inet filter output`, { silent: true });
|
|
308
|
+
shellExec(`sudo nft flush chain inet filter forward`, { silent: true });
|
|
309
|
+
|
|
310
|
+
logger.info('All outbound traffic unblocked and restored to default ACCEPT policy.');
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Blocks all new inbound traffic to this host, except for established/related connections.
|
|
315
|
+
* This prevents any new incoming connections while keeping existing sessions (like SSH) alive.
|
|
316
|
+
* @static
|
|
317
|
+
* @memberof UnderpostDns
|
|
318
|
+
*/
|
|
319
|
+
static blockAllIngress() {
|
|
320
|
+
// Clear any existing ingress rules.
|
|
321
|
+
shellExec(`sudo nft flush chain inet filter input`, { silent: true });
|
|
322
|
+
|
|
323
|
+
// Allow return traffic for established/related connections.
|
|
324
|
+
// This keeps active inbound/outbound sessions alive.
|
|
325
|
+
shellExec(`sudo nft add rule inet filter input ct state established,related counter accept`, { silent: true });
|
|
326
|
+
|
|
327
|
+
// Block all new inbound connections to this host.
|
|
328
|
+
shellExec(`sudo nft chain inet filter input '{ policy drop; }'`, { silent: true });
|
|
329
|
+
|
|
330
|
+
logger.info('All new inbound traffic blocked.');
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* Unblocks all inbound traffic to this host.
|
|
335
|
+
* Restores default input chain policy to ACCEPT and clears ingress rules.
|
|
336
|
+
* @static
|
|
337
|
+
* @memberof UnderpostDns
|
|
338
|
+
*/
|
|
339
|
+
static unblockAllIngress() {
|
|
340
|
+
// Restore default chain policy to accept all incoming traffic.
|
|
341
|
+
shellExec(`sudo nft chain inet filter input '{ policy accept; }'`, { silent: true });
|
|
342
|
+
|
|
343
|
+
// Clear any existing ingress blocking rules.
|
|
344
|
+
shellExec(`sudo nft flush chain inet filter input`, { silent: true });
|
|
345
|
+
|
|
346
|
+
logger.info('All inbound traffic unblocked and restored to default ACCEPT policy.');
|
|
347
|
+
}
|
|
348
|
+
|
|
274
349
|
/**
|
|
275
350
|
* Performs the dynamic DNS update logic.
|
|
276
351
|
* It checks if the public IP has changed and, if so, updates the configured DNS records.
|
|
@@ -279,10 +354,8 @@ class Dns {
|
|
|
279
354
|
* @memberof UnderpostDns
|
|
280
355
|
* @param {string} deployList Comma-separated string of deployment IDs to process.
|
|
281
356
|
* @returns {Promise<void>}
|
|
282
|
-
|
|
357
|
+
*/
|
|
283
358
|
static async callback(deployList) {
|
|
284
|
-
// loadCronDeployEnv();
|
|
285
|
-
|
|
286
359
|
const isOnline = await Dns.isInternetConnection();
|
|
287
360
|
|
|
288
361
|
if (!isOnline) return;
|
|
@@ -440,6 +513,10 @@ class Dns {
|
|
|
440
513
|
* @property {boolean} [options.banEgressClear=false] - Clear all banned egress IPs.
|
|
441
514
|
* @property {boolean} [options.banBothAdd=false] - Ban IPs from both ingress and egress.
|
|
442
515
|
* @property {boolean} [options.banBothRemove=false] - Unban IPs from both ingress and egress.
|
|
516
|
+
* @property {boolean} [options.blockAllEgress=false] - Block all outbound traffic from this host.
|
|
517
|
+
* @property {boolean} [options.unblockAllEgress=false] - Unblock all outbound traffic.
|
|
518
|
+
* @property {boolean} [options.blockAllIngress=false] - Block all new inbound traffic to this host.
|
|
519
|
+
* @property {boolean} [options.unblockAllIngress=false] - Unblock all inbound traffic.
|
|
443
520
|
* @property {boolean} [options.dhcp=false] - Get local DHCP IP instead of public IP.
|
|
444
521
|
* @property {boolean} [options.copy=false] - Copy the public IP to clipboard.
|
|
445
522
|
* @return {Promise<string|void>} The public IP if no ban/unban action is taken.
|
|
@@ -457,6 +534,10 @@ class Dns {
|
|
|
457
534
|
banEgressClear: false,
|
|
458
535
|
banBothAdd: false,
|
|
459
536
|
banBothRemove: false,
|
|
537
|
+
blockAllEgress: false,
|
|
538
|
+
unblockAllEgress: false,
|
|
539
|
+
blockAllIngress: false,
|
|
540
|
+
unblockAllIngress: false,
|
|
460
541
|
copy: false,
|
|
461
542
|
dhcp: false,
|
|
462
543
|
},
|
|
@@ -507,6 +588,19 @@ class Dns {
|
|
|
507
588
|
});
|
|
508
589
|
}
|
|
509
590
|
|
|
591
|
+
if (options.blockAllEgress) {
|
|
592
|
+
return Dns.blockAllEgress();
|
|
593
|
+
}
|
|
594
|
+
if (options.unblockAllEgress) {
|
|
595
|
+
return Dns.unblockAllEgress();
|
|
596
|
+
}
|
|
597
|
+
if (options.blockAllIngress) {
|
|
598
|
+
return Dns.blockAllIngress();
|
|
599
|
+
}
|
|
600
|
+
if (options.unblockAllIngress) {
|
|
601
|
+
return Dns.unblockAllIngress();
|
|
602
|
+
}
|
|
603
|
+
|
|
510
604
|
if (options.mac) {
|
|
511
605
|
const mac = Dns.getMainInterfaceMac();
|
|
512
606
|
console.log(mac);
|