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
|
@@ -11,16 +11,29 @@
|
|
|
11
11
|
import fs from 'fs-extra';
|
|
12
12
|
import { loggerFactory } from '../../server/logger.js';
|
|
13
13
|
import { shellExec } from '../../server/process.js';
|
|
14
|
+
import { crictlCommandFactory } from '../../server/cri.js';
|
|
15
|
+
import { resolveReplicaCount } from '../../server/conf.js';
|
|
16
|
+
// Cyclic by construction (index -> cluster -> MongoBootstrap -> index), same as cluster.js.
|
|
17
|
+
// Safe because the binding is only dereferenced inside method bodies, never at module scope.
|
|
18
|
+
import Underpost from '../../index.js';
|
|
14
19
|
import {
|
|
20
|
+
MONGODB_DATA_ROOT,
|
|
15
21
|
MONGODB_DEFAULT_REPLICA_COUNT,
|
|
16
22
|
MONGODB_DEFAULT_REPLICA_SET,
|
|
17
23
|
MONGODB_SERVICE_NAME,
|
|
18
24
|
MONGODB_STATEFULSET_NAME,
|
|
25
|
+
MONGODB_STORAGE_CLASS_NAME,
|
|
26
|
+
MONGODB_STORAGE_CLASS_PROVISIONER,
|
|
19
27
|
resolveMongoReplicaHosts,
|
|
20
28
|
} from './MongooseDB.js';
|
|
21
29
|
|
|
22
30
|
const logger = loggerFactory(import.meta);
|
|
23
31
|
|
|
32
|
+
// Teardown sweeps ordinals rather than a live replica count: the deployed count is not knowable
|
|
33
|
+
// during a reset, and a member left behind from a larger previous deployment keeps its volume and
|
|
34
|
+
// stale replica-set config, which a later deploy then trips over.
|
|
35
|
+
const MONGODB_ORDINAL_SWEEP = 10;
|
|
36
|
+
|
|
24
37
|
/**
|
|
25
38
|
* @typedef {Object} MongoBootstrapOptions
|
|
26
39
|
* @property {string} [namespace='default'] - Kubernetes namespace.
|
|
@@ -135,6 +148,30 @@ class MongoBootstrap {
|
|
|
135
148
|
` return true;`,
|
|
136
149
|
`};`,
|
|
137
150
|
|
|
151
|
+
// Recovery for a node holding a config it is not a member of. A forced
|
|
152
|
+
// reconfig is the only way out: it is accepted on a non-primary node,
|
|
153
|
+
// which an ordinary reconfig is not. Auth is best-effort — when the set
|
|
154
|
+
// already has users the unauthenticated pass fails here and the caller's
|
|
155
|
+
// authenticated pass performs the recovery.
|
|
156
|
+
//
|
|
157
|
+
// The recovery config has ONE member. Forcing the full member list back
|
|
158
|
+
// in place leaves the node needing a majority of the others to elect it,
|
|
159
|
+
// and they are typically holding their own stale configs from the same
|
|
160
|
+
// dead cluster, so no election ever completes. A single-member set needs
|
|
161
|
+
// only its own vote and becomes writable at once; reconfigure() then
|
|
162
|
+
// widens it — the same two-step the pristine path takes after rs.initiate.
|
|
163
|
+
`const currentConfigVersion = () => { try { return rs.conf().version || 0; } catch(e) { return 0; } };`,
|
|
164
|
+
`const forceReconfig = () => {`,
|
|
165
|
+
` try { ensureAdminAuth(); } catch(e) {}`,
|
|
166
|
+
` const soloConfig = {`,
|
|
167
|
+
` _id: desiredConfig._id,`,
|
|
168
|
+
` version: currentConfigVersion() + 1,`,
|
|
169
|
+
` members: [{ _id: 0, host: "localhost:" + mePort }],`,
|
|
170
|
+
` };`,
|
|
171
|
+
` rs.reconfig(soloConfig, { force: true });`,
|
|
172
|
+
` print("SUCCESS_FORCE_RECONFIGURED");`,
|
|
173
|
+
`};`,
|
|
174
|
+
|
|
138
175
|
`const reconfigure = () => {`,
|
|
139
176
|
` if (!ensureAdminAuth()) return false;`,
|
|
140
177
|
` const cur = rs.conf();`,
|
|
@@ -149,28 +186,39 @@ class MongoBootstrap {
|
|
|
149
186
|
` return true;`,
|
|
150
187
|
`};`,
|
|
151
188
|
|
|
152
|
-
//
|
|
153
|
-
|
|
189
|
+
// Classify before acting. Three distinct states reach this script:
|
|
190
|
+
// pristine — no config yet (fresh volume)
|
|
191
|
+
// orphaned — a config exists but this node is not a member of it, which
|
|
192
|
+
// is what a volume retained from an earlier cluster leaves
|
|
193
|
+
// behind; mongod parks in REMOVED and never elects itself
|
|
194
|
+
// live — a usable config, or one hidden behind auth
|
|
195
|
+
// The distinction matters for ordering: an orphaned node must be force
|
|
196
|
+
// reconfigured BEFORE waiting for a primary, because it can never become
|
|
197
|
+
// writable on its own. Waiting first is an unconditional timeout.
|
|
198
|
+
`const matchesAny = (msg, list) => list.some(s => msg.includes(s));`,
|
|
199
|
+
`let state = "live";`,
|
|
154
200
|
`try {`,
|
|
155
201
|
` const s = rs.status();`,
|
|
156
|
-
` if (s
|
|
202
|
+
` if (!s || s.ok !== 1) state = "pristine";`,
|
|
157
203
|
`} catch(e) {`,
|
|
158
204
|
` const msg = String(e);`,
|
|
159
|
-
`
|
|
160
|
-
`
|
|
161
|
-
`
|
|
162
|
-
`
|
|
163
|
-
`
|
|
205
|
+
` const ORPHANED = ["not a member of it", "InvalidReplicaSetConfig", "maps to this node"];`,
|
|
206
|
+
` if (matchesAny(msg, ORPHANED)) state = "orphaned";`,
|
|
207
|
+
` else if (matchesAny(msg, ["NotYetInitialized", "no replset config"])) state = "pristine";`,
|
|
208
|
+
` else if (matchesAny(msg, ["requires authentication", "Unauthorized", "not authorized"])) state = "live";`,
|
|
209
|
+
` else throw e;`,
|
|
164
210
|
`}`,
|
|
211
|
+
`print("REPLSET_STATE_" + state.toUpperCase());`,
|
|
165
212
|
|
|
166
|
-
|
|
167
|
-
`if (!initialized) {`,
|
|
213
|
+
`if (state === "pristine") {`,
|
|
168
214
|
` try {`,
|
|
169
215
|
` rs.initiate({ _id: desiredConfig._id, members: [{ _id: 0, host: "localhost:" + mePort }] });`,
|
|
170
216
|
` } catch(e) {`,
|
|
171
217
|
` const msg = String(e);`,
|
|
172
218
|
` if (!msg.includes("already initialized") && !msg.includes("AlreadyInitialized")) throw e;`,
|
|
173
219
|
` }`,
|
|
220
|
+
`} else if (state === "orphaned") {`,
|
|
221
|
+
` forceReconfig();`,
|
|
174
222
|
`}`,
|
|
175
223
|
|
|
176
224
|
// Wait for primary, create user, then reconfig to full host list
|
|
@@ -189,7 +237,7 @@ class MongoBootstrap {
|
|
|
189
237
|
static findNodesMissingMongoMount(kindNodes) {
|
|
190
238
|
return kindNodes.filter((node) => {
|
|
191
239
|
const inspect = shellExec(
|
|
192
|
-
`sudo docker inspect ${node} --format '{{range .Mounts}}{{if eq .Destination "
|
|
240
|
+
`sudo docker inspect ${node} --format '{{range .Mounts}}{{if eq .Destination "${MONGODB_DATA_ROOT}"}}yes{{end}}{{end}}'`,
|
|
193
241
|
{ stdout: true, silent: true, silentOnError: true },
|
|
194
242
|
);
|
|
195
243
|
return !inspect.trim().includes('yes');
|
|
@@ -210,7 +258,7 @@ class MongoBootstrap {
|
|
|
210
258
|
logger.info('No Kind nodes detected for hostPath cleanup.');
|
|
211
259
|
return;
|
|
212
260
|
}
|
|
213
|
-
const basePath =
|
|
261
|
+
const basePath = MONGODB_DATA_ROOT;
|
|
214
262
|
for (const node of nodes) {
|
|
215
263
|
const prepareCmd = Array.from(
|
|
216
264
|
{ length: replicaCount },
|
|
@@ -232,12 +280,14 @@ class MongoBootstrap {
|
|
|
232
280
|
* an empty /data/db regardless of bind-mount staleness.
|
|
233
281
|
*
|
|
234
282
|
* @param {string[]} kindNodes - List of Kind node container names.
|
|
235
|
-
* @param {
|
|
283
|
+
* @param {number} [replicaCount=3] - Number of replica ordinal directories to clean. Must track
|
|
284
|
+
* the deployed replica count, or members above the third start on stale data.
|
|
285
|
+
* @param {string} [basePath='/data/mongodb'] - The base path containing the v<ordinal> subdirs.
|
|
236
286
|
*/
|
|
237
|
-
static remountKindMongoVolume(kindNodes, basePath =
|
|
287
|
+
static remountKindMongoVolume(kindNodes, replicaCount = MONGODB_DEFAULT_REPLICA_COUNT, basePath = MONGODB_DATA_ROOT) {
|
|
238
288
|
for (const node of kindNodes) {
|
|
239
289
|
logger.info(`Cleaning MongoDB data dirs inside Kind node '${node}'...`);
|
|
240
|
-
for (let i = 0; i <
|
|
290
|
+
for (let i = 0; i < replicaCount; i++) {
|
|
241
291
|
const dir = `${basePath}/v${i}`;
|
|
242
292
|
// Ensure directory exists, wipe all contents (including hidden files), set open permissions
|
|
243
293
|
// so the pod's initContainer chown can run without issues.
|
|
@@ -264,23 +314,174 @@ class MongoBootstrap {
|
|
|
264
314
|
|
|
265
315
|
/**
|
|
266
316
|
* Creates or updates Kubernetes secrets required by the MongoDB statefulset.
|
|
317
|
+
*
|
|
318
|
+
* Prefers the SOPS/Age encrypted store, exactly like the MariaDB/MySQL/PostgreSQL branches of
|
|
319
|
+
* cluster init: when `engine-private/secrets/<ns>/<name>.enc.yaml` exists it is decrypted
|
|
320
|
+
* straight into `kubectl apply`, and only otherwise is the secret seeded from its plaintext
|
|
321
|
+
* origin seed file.
|
|
322
|
+
*
|
|
323
|
+
* The seed path uses `--from-literal`, which places the credential in the command string and so
|
|
324
|
+
* in the process table and the command log; `disableLog` keeps it out of the log at least. The
|
|
325
|
+
* encrypted path has no such exposure — the value only ever crosses an anonymous pipe.
|
|
267
326
|
* @param {string} namespace - Target namespace.
|
|
268
327
|
* @param {string} enginePrivateRoot - Path to engine-private directory.
|
|
269
328
|
*/
|
|
270
329
|
static ensureMongoSecrets(namespace, enginePrivateRoot) {
|
|
271
|
-
|
|
272
|
-
|
|
330
|
+
if (!Underpost.secret.sops.applyIfPresent('mongodb-keyfile', namespace)) {
|
|
331
|
+
const keyfile = MongoBootstrap.readCredential(`${enginePrivateRoot}/mongodb-keyfile`);
|
|
332
|
+
shellExec(
|
|
333
|
+
`sudo kubectl create secret generic mongodb-keyfile` +
|
|
334
|
+
` --from-literal=mongodb-keyfile="${keyfile.replace(/'/g, "'\\''")}"` +
|
|
335
|
+
` --dry-run=client -o yaml | kubectl apply -f - -n ${namespace}`,
|
|
336
|
+
{ disableLog: true },
|
|
337
|
+
);
|
|
338
|
+
logger.info(`Seeded mongodb-keyfile from ${enginePrivateRoot}/mongodb-keyfile`);
|
|
339
|
+
}
|
|
273
340
|
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
`
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
341
|
+
if (!Underpost.secret.sops.applyIfPresent('mongodb-secret', namespace)) {
|
|
342
|
+
const { username, password } = MongoBootstrap.readMongoCredentials(enginePrivateRoot);
|
|
343
|
+
shellExec(
|
|
344
|
+
`sudo kubectl create secret generic mongodb-secret` +
|
|
345
|
+
` --from-literal=username="${username}" --from-literal=password="${password}"` +
|
|
346
|
+
` --dry-run=client -o yaml | kubectl apply -f - -n ${namespace}`,
|
|
347
|
+
{ disableLog: true },
|
|
348
|
+
);
|
|
349
|
+
logger.info(`Seeded mongodb-secret from ${enginePrivateRoot}/mongodb-{username,password}`);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* Renders one hostPath PersistentVolume per replica, each pre-bound to that member's claim.
|
|
355
|
+
*
|
|
356
|
+
* The volume set is a function of the replica count, so it is generated rather than read from a
|
|
357
|
+
* fixed manifest — a static file can only describe a fixed number, and any `--replicas` above
|
|
358
|
+
* it leaves the surplus members unbindable.
|
|
359
|
+
* @param {number} replicaCount - Number of members to provision volumes for.
|
|
360
|
+
* @param {string} namespace - Namespace the claims live in.
|
|
361
|
+
* @returns {string} Multi-document PersistentVolume YAML.
|
|
362
|
+
*/
|
|
363
|
+
static buildReplicaVolumeManifest(replicaCount, namespace) {
|
|
364
|
+
return Array.from({ length: replicaCount }, (_, i) =>
|
|
365
|
+
[
|
|
366
|
+
'apiVersion: v1',
|
|
367
|
+
'kind: PersistentVolume',
|
|
368
|
+
'metadata:',
|
|
369
|
+
` name: ${MONGODB_STATEFULSET_NAME}-pv-${i}`,
|
|
370
|
+
' labels:',
|
|
371
|
+
` app: ${MONGODB_STATEFULSET_NAME}`,
|
|
372
|
+
'spec:',
|
|
373
|
+
' capacity:',
|
|
374
|
+
' storage: 5Gi',
|
|
375
|
+
' accessModes:',
|
|
376
|
+
' - ReadWriteOnce',
|
|
377
|
+
' persistentVolumeReclaimPolicy: Retain',
|
|
378
|
+
` storageClassName: ${MONGODB_STORAGE_CLASS_NAME}`,
|
|
379
|
+
// claimRef pins each volume to exactly one member, so ordinals can never cross-bind and
|
|
380
|
+
// land two mongod processes on one data directory.
|
|
381
|
+
' claimRef:',
|
|
382
|
+
` namespace: ${namespace}`,
|
|
383
|
+
` name: ${MONGODB_STATEFULSET_NAME}-storage-${MONGODB_STATEFULSET_NAME}-${i}`,
|
|
384
|
+
' hostPath:',
|
|
385
|
+
` path: ${MONGODB_DATA_ROOT}/v${i}`,
|
|
386
|
+
' type: DirectoryOrCreate',
|
|
387
|
+
].join('\n'),
|
|
388
|
+
).join('\n---\n');
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/**
|
|
392
|
+
* Applies the generated replica volumes, and removes any volume left over from a larger previous
|
|
393
|
+
* replica count so a scale-down does not strand a PV bound to a claim that no longer exists.
|
|
394
|
+
* @param {string} namespace - Target namespace.
|
|
395
|
+
* @param {number} replicaCount - Number of members to provision volumes for.
|
|
396
|
+
*/
|
|
397
|
+
static applyReplicaVolumes(namespace, replicaCount) {
|
|
398
|
+
const manifest = MongoBootstrap.buildReplicaVolumeManifest(replicaCount, namespace);
|
|
399
|
+
shellExec(`kubectl apply -f - <<'UNDERPOST_MONGO_PV_EOF'\n${manifest}\nUNDERPOST_MONGO_PV_EOF`);
|
|
400
|
+
logger.info(`Applied ${replicaCount} MongoDB replica volume(s)`);
|
|
401
|
+
|
|
402
|
+
const stale = shellExec(`kubectl get pv -l app=${MONGODB_STATEFULSET_NAME} -o name 2>/dev/null || true`, {
|
|
403
|
+
stdout: true,
|
|
404
|
+
silent: true,
|
|
405
|
+
silentOnError: true,
|
|
406
|
+
})
|
|
407
|
+
.split('\n')
|
|
408
|
+
.map((name) => name.replace('persistentvolume/', '').trim())
|
|
409
|
+
.filter((name) => {
|
|
410
|
+
const ordinal = name.startsWith(`${MONGODB_STATEFULSET_NAME}-pv-`)
|
|
411
|
+
? Number(name.slice(`${MONGODB_STATEFULSET_NAME}-pv-`.length))
|
|
412
|
+
: NaN;
|
|
413
|
+
return Number.isInteger(ordinal) && ordinal >= replicaCount;
|
|
414
|
+
});
|
|
415
|
+
for (const name of stale) {
|
|
416
|
+
shellExec(`kubectl delete pv ${name} --ignore-not-found`);
|
|
417
|
+
logger.info(`Removed stale replica volume ${name} (beyond replica count ${replicaCount})`);
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
/**
|
|
422
|
+
* Reads the claim-to-volume-to-path mapping for every replica and logs it. Non-throwing, so it
|
|
423
|
+
* can enrich a failure path without masking the original error.
|
|
424
|
+
* @param {string} namespace - Target namespace.
|
|
425
|
+
* @param {number} replicaCount - Expected number of members.
|
|
426
|
+
* @returns {Array<{claim: string, volume: string, path: string}>} Bindings in ordinal order.
|
|
427
|
+
*/
|
|
428
|
+
static reportReplicaVolumeBindings(namespace, replicaCount) {
|
|
429
|
+
const bindings = [];
|
|
430
|
+
for (let i = 0; i < replicaCount; i++) {
|
|
431
|
+
const claim = `${MONGODB_STATEFULSET_NAME}-storage-${MONGODB_STATEFULSET_NAME}-${i}`;
|
|
432
|
+
const volume = shellExec(
|
|
433
|
+
`kubectl get pvc ${claim} -n ${namespace} -o jsonpath='{.spec.volumeName}' 2>/dev/null || true`,
|
|
434
|
+
{ stdout: true, silent: true, silentOnError: true },
|
|
435
|
+
).trim();
|
|
436
|
+
const path = volume
|
|
437
|
+
? shellExec(`kubectl get pv ${volume} -o jsonpath='{.spec.hostPath.path}' 2>/dev/null || true`, {
|
|
438
|
+
stdout: true,
|
|
439
|
+
silent: true,
|
|
440
|
+
silentOnError: true,
|
|
441
|
+
}).trim()
|
|
442
|
+
: '';
|
|
443
|
+
bindings.push({ claim, volume, path });
|
|
444
|
+
}
|
|
445
|
+
logger.info('MongoDB replica volume bindings', bindings);
|
|
446
|
+
return bindings;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
/**
|
|
450
|
+
* Verifies every replica PVC bound to its own distinct hostPath.
|
|
451
|
+
*
|
|
452
|
+
* Two members sharing one directory is unrecoverable at the mongod level — the second dies on
|
|
453
|
+
* `WiredTiger.lock: fcntl: Resource temporarily unavailable` after the readiness wait has
|
|
454
|
+
* already burned its timeout, and the real cause (a volume binding, not MongoDB) is invisible
|
|
455
|
+
* in the pod logs. Checking the binding directly turns that into an immediate, named failure.
|
|
456
|
+
* @param {string} namespace - Target namespace.
|
|
457
|
+
* @param {number} replicaCount - Expected number of members.
|
|
458
|
+
* @throws {Error} When a claim is unbound, or two claims share a backing path.
|
|
459
|
+
*/
|
|
460
|
+
static assertReplicaVolumeBindings(namespace, replicaCount) {
|
|
461
|
+
const bindings = MongoBootstrap.reportReplicaVolumeBindings(namespace, replicaCount);
|
|
462
|
+
|
|
463
|
+
const unbound = bindings.filter((binding) => !binding.volume);
|
|
464
|
+
if (unbound.length > 0)
|
|
465
|
+
throw new Error(
|
|
466
|
+
`MongoDB claims are not bound to a PersistentVolume: ${unbound.map((b) => b.claim).join(', ')}. ` +
|
|
467
|
+
`Expected one volume per replica (${replicaCount} total); check that the generated PVs applied ` +
|
|
468
|
+
`and that each claimRef matches.`,
|
|
469
|
+
);
|
|
470
|
+
|
|
471
|
+
// Only hostPath-backed volumes expose a path to compare; a dynamically provisioned volume
|
|
472
|
+
// reports none, which is itself worth surfacing since these PVs are meant to be static.
|
|
473
|
+
const byPath = bindings.reduce((acc, binding) => {
|
|
474
|
+
if (binding.path) (acc[binding.path] = acc[binding.path] || []).push(binding.claim);
|
|
475
|
+
return acc;
|
|
476
|
+
}, {});
|
|
477
|
+
const shared = Object.entries(byPath).filter(([, claims]) => claims.length > 1);
|
|
478
|
+
if (shared.length > 0)
|
|
479
|
+
throw new Error(
|
|
480
|
+
`MongoDB members would share a data directory, which mongod cannot survive: ` +
|
|
481
|
+
shared.map(([path, claims]) => `${path} <- ${claims.join(' + ')}`).join('; ') +
|
|
482
|
+
`. Delete the claims and PVs, then redeploy: ` +
|
|
483
|
+
`kubectl delete pvc -n ${namespace} -l app=mongodb; kubectl delete pv -l app=mongodb`,
|
|
484
|
+
);
|
|
284
485
|
}
|
|
285
486
|
|
|
286
487
|
/**
|
|
@@ -323,7 +524,15 @@ class MongoBootstrap {
|
|
|
323
524
|
} = options;
|
|
324
525
|
|
|
325
526
|
const enginePrivateRoot = `${process.cwd()}/engine-private`;
|
|
326
|
-
|
|
527
|
+
// No upward clamp: an explicit `--replicas 2` must deploy two members, not be silently
|
|
528
|
+
// raised to the default.
|
|
529
|
+
const effectiveReplicaCount = resolveReplicaCount(replicaCount, MONGODB_DEFAULT_REPLICA_COUNT);
|
|
530
|
+
if (effectiveReplicaCount % 2 === 0)
|
|
531
|
+
logger.warn(
|
|
532
|
+
`Deploying ${effectiveReplicaCount} MongoDB members. An even-sized replica set has no ` +
|
|
533
|
+
`majority when one member is down, so the set becomes read-only on a single failure. ` +
|
|
534
|
+
`Odd counts (3, 5) are recommended.`,
|
|
535
|
+
);
|
|
327
536
|
const mongoRootUsername = MongoBootstrap.readCredential(`${enginePrivateRoot}/mongodb-username`);
|
|
328
537
|
const mongoRootPassword = MongoBootstrap.readCredential(`${enginePrivateRoot}/mongodb-password`);
|
|
329
538
|
const mongoReplicaHosts = resolveMongoReplicaHosts({
|
|
@@ -364,16 +573,7 @@ class MongoBootstrap {
|
|
|
364
573
|
}
|
|
365
574
|
shellExec(`rm -f ${tarPath}`);
|
|
366
575
|
} else {
|
|
367
|
-
|
|
368
|
-
shellExec('test -S /var/run/crio/crio.sock && echo crio || echo containerd', {
|
|
369
|
-
stdout: true,
|
|
370
|
-
silent: true,
|
|
371
|
-
}).trim() === 'crio'
|
|
372
|
-
? 'unix:///var/run/crio/crio.sock'
|
|
373
|
-
: 'unix:///run/containerd/containerd.sock';
|
|
374
|
-
shellExec(
|
|
375
|
-
`sudo env PATH="$PATH:/usr/local/bin:/usr/bin" crictl --runtime-endpoint ${criSock} pull mongo:latest`,
|
|
376
|
-
);
|
|
576
|
+
shellExec(crictlCommandFactory('pull mongo:latest', { k3s: clusterType === 'k3s' }));
|
|
377
577
|
}
|
|
378
578
|
}
|
|
379
579
|
|
|
@@ -386,24 +586,61 @@ class MongoBootstrap {
|
|
|
386
586
|
|
|
387
587
|
// Clean data if reset or kind
|
|
388
588
|
if (reset || isKind) {
|
|
589
|
+
// Delete the StatefulSet's PVCs by name. A label selector cannot reach them: the
|
|
590
|
+
// `volumeClaimTemplates` entry carries no labels, so `-l app=mongodb` matches nothing and
|
|
591
|
+
// silently leaves the previous run's PVCs Bound. With `persistentVolumeReclaimPolicy:
|
|
592
|
+
// Retain` those stale claims keep their old PVs, so the freshly created `mongodb-pv-N`
|
|
593
|
+
// (whose `claimRef` names the same PVCs) never binds and pods mount the previous volumes —
|
|
594
|
+
// which is how two members end up on one hostPath and mongod dies on the WiredTiger lock.
|
|
595
|
+
for (let i = 0; i < MONGODB_ORDINAL_SWEEP; i++)
|
|
596
|
+
shellExec(
|
|
597
|
+
`kubectl delete pvc ${MONGODB_STATEFULSET_NAME}-storage-${MONGODB_STATEFULSET_NAME}-${i} -n ${namespace} --ignore-not-found`,
|
|
598
|
+
);
|
|
389
599
|
shellExec(`kubectl delete pvc -l app=mongodb -n ${namespace} --ignore-not-found`);
|
|
390
600
|
shellExec(`kubectl delete pvc mongodb-pvc -n ${namespace} --ignore-not-found`);
|
|
391
601
|
shellExec(`kubectl delete pv -l app=mongodb --ignore-not-found`);
|
|
392
602
|
shellExec(`kubectl delete pv mongodb-pv --ignore-not-found`);
|
|
393
603
|
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
604
|
+
// The data itself, for every cluster type. The PVs are hostPath with
|
|
605
|
+
// `persistentVolumeReclaimPolicy: Retain`, so deleting the objects frees
|
|
606
|
+
// nothing: the next run re-binds the same directories and mongod boots
|
|
607
|
+
// with the previous cluster's replica set config, ending up outside it.
|
|
608
|
+
// Only this node's copy is removed — on a multi-node cluster, wipe the
|
|
609
|
+
// other nodes' /data/mongodb before rebuilding there.
|
|
610
|
+
logger.info('Removing retained MongoDB hostPath data', {
|
|
611
|
+
path: MONGODB_DATA_ROOT,
|
|
612
|
+
replicas: effectiveReplicaCount,
|
|
613
|
+
});
|
|
614
|
+
shellExec(`sudo mkdir -p ${MONGODB_DATA_ROOT}`);
|
|
615
|
+
for (let i = 0; i < effectiveReplicaCount; i++) {
|
|
616
|
+
shellExec(`sudo rm -rf ${MONGODB_DATA_ROOT}/v${i}`);
|
|
617
|
+
shellExec(`sudo mkdir -p ${MONGODB_DATA_ROOT}/v${i}`);
|
|
402
618
|
}
|
|
619
|
+
// Fix any stale bind mounts caused by prior deletion of /data/mongodb on the host
|
|
620
|
+
if (isKind) MongoBootstrap.remountKindMongoVolume(kindNodes, effectiveReplicaCount);
|
|
403
621
|
}
|
|
404
622
|
|
|
405
623
|
// Apply manifests
|
|
624
|
+
// A StorageClass `provisioner` is immutable, so a plain apply fails against a class created
|
|
625
|
+
// with a different one. Clusters provisioned before the switch to `kubernetes.io/no-provisioner`
|
|
626
|
+
// carry the dynamic `rancher.io/local-path`; recreate in that case. Deleting the class is safe
|
|
627
|
+
// — bound PVs and PVCs reference it by name only and are untouched.
|
|
628
|
+
const storageClassProvisioner = shellExec(
|
|
629
|
+
`kubectl get storageclass ${MONGODB_STORAGE_CLASS_NAME} -o jsonpath='{.provisioner}' 2>/dev/null || true`,
|
|
630
|
+
{ stdout: true, silent: true, silentOnError: true },
|
|
631
|
+
).trim();
|
|
632
|
+
if (storageClassProvisioner && storageClassProvisioner !== MONGODB_STORAGE_CLASS_PROVISIONER) {
|
|
633
|
+
logger.warn(
|
|
634
|
+
`StorageClass ${MONGODB_STORAGE_CLASS_NAME} uses provisioner '${storageClassProvisioner}'; recreating as ` +
|
|
635
|
+
`'${MONGODB_STORAGE_CLASS_PROVISIONER}' so the static replica PVs bind deterministically.`,
|
|
636
|
+
);
|
|
637
|
+
shellExec(`kubectl delete storageclass ${MONGODB_STORAGE_CLASS_NAME} --ignore-not-found`);
|
|
638
|
+
}
|
|
406
639
|
shellExec(`kubectl apply -f ${underpostRoot}/manifests/mongodb/storage-class.yaml -n ${namespace}`);
|
|
640
|
+
// One PV per member, generated from the effective replica count. A static manifest can only
|
|
641
|
+
// ever describe a fixed number, so any `--replicas` above it leaves the extra members with no
|
|
642
|
+
// volume to bind and the StatefulSet stalls forever on Pending.
|
|
643
|
+
MongoBootstrap.applyReplicaVolumes(namespace, effectiveReplicaCount);
|
|
407
644
|
shellExec(`kubectl apply -k ${underpostRoot}/manifests/mongodb -n ${namespace}`);
|
|
408
645
|
shellExec(
|
|
409
646
|
`kubectl scale statefulset/${MONGODB_STATEFULSET_NAME} --replicas=${effectiveReplicaCount} -n ${namespace}`,
|
|
@@ -412,12 +649,17 @@ class MongoBootstrap {
|
|
|
412
649
|
// Wait for all pods
|
|
413
650
|
const failedCount = await MongoBootstrap.waitForPods(namespace, effectiveReplicaCount);
|
|
414
651
|
if (failedCount > 0) {
|
|
652
|
+
// Surface the volume topology before failing: a stalled rollout is far more often a binding
|
|
653
|
+
// problem than a MongoDB one, and the mapping names it immediately.
|
|
654
|
+
MongoBootstrap.reportReplicaVolumeBindings(namespace, effectiveReplicaCount);
|
|
415
655
|
throw new Error(
|
|
416
656
|
`MongoDB replica pods did not reach Running state in time. ` +
|
|
417
657
|
`Ensure podManagementPolicy is set to OrderedReady in statefulset.yaml.`,
|
|
418
658
|
);
|
|
419
659
|
}
|
|
420
660
|
|
|
661
|
+
MongoBootstrap.assertReplicaVolumeBindings(namespace, effectiveReplicaCount);
|
|
662
|
+
|
|
421
663
|
// Build the bootstrap script
|
|
422
664
|
const defaultHosts = Array.from(
|
|
423
665
|
{ length: effectiveReplicaCount },
|
|
@@ -528,13 +770,12 @@ class MongoBootstrap {
|
|
|
528
770
|
// Phase 4: Delete MongoDB PVCs and PVs (both current and legacy mongodb-4.4)
|
|
529
771
|
logger.info('Phase 4/6: Deleting MongoDB PersistentVolumeClaims and PersistentVolumes...');
|
|
530
772
|
// Delete PVCs from volumeClaimTemplates
|
|
531
|
-
for (let i = 0; i <
|
|
773
|
+
for (let i = 0; i < MONGODB_ORDINAL_SWEEP; i++) {
|
|
532
774
|
shellExec(`kubectl delete pvc mongodb-storage-mongodb-${i} -n ${namespace} --ignore-not-found`);
|
|
533
775
|
}
|
|
534
776
|
shellExec(`kubectl delete pvc mongodb-pvc -n ${namespace} --ignore-not-found`);
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
shellExec(`kubectl delete pv mongodb-pv-2 --ignore-not-found`);
|
|
777
|
+
for (let i = 0; i < MONGODB_ORDINAL_SWEEP; i++)
|
|
778
|
+
shellExec(`kubectl delete pv ${MONGODB_STATEFULSET_NAME}-pv-${i} --ignore-not-found`);
|
|
538
779
|
shellExec(`kubectl delete pv mongodb-pv --ignore-not-found`);
|
|
539
780
|
// Also catch any remaining PVs with the app=mongodb label
|
|
540
781
|
shellExec(`kubectl delete pv -l app=mongodb --ignore-not-found`);
|
|
@@ -550,10 +791,10 @@ class MongoBootstrap {
|
|
|
550
791
|
// IMPORTANT: Do NOT remove /data/mongodb itself — it is bind-mounted into Kind node
|
|
551
792
|
// containers by inode. Removing it makes the bind mount stale; only clear subdirs.
|
|
552
793
|
logger.info('Phase 5/6: Cleaning up MongoDB hostPath data...');
|
|
553
|
-
shellExec(`sudo mkdir -p
|
|
554
|
-
for (let i = 0; i <
|
|
555
|
-
shellExec(`sudo rm -rf /
|
|
556
|
-
shellExec(`sudo mkdir -p /
|
|
794
|
+
shellExec(`sudo mkdir -p ${MONGODB_DATA_ROOT}`);
|
|
795
|
+
for (let i = 0; i < MONGODB_ORDINAL_SWEEP; i++) {
|
|
796
|
+
shellExec(`sudo rm -rf ${MONGODB_DATA_ROOT}/v${i}`);
|
|
797
|
+
shellExec(`sudo mkdir -p ${MONGODB_DATA_ROOT}/v${i}`);
|
|
557
798
|
}
|
|
558
799
|
// For Kind: repair any stale bind mounts via nsenter (overmounts with current host inode)
|
|
559
800
|
if (isKind) {
|
|
@@ -15,6 +15,30 @@ const MONGODB_STATEFULSET_NAME = 'mongodb';
|
|
|
15
15
|
const MONGODB_DEFAULT_AUTH_SOURCE = 'admin';
|
|
16
16
|
const MONGODB_DEFAULT_REPLICA_SET = 'rs0';
|
|
17
17
|
const MONGODB_DEFAULT_REPLICA_COUNT = 3;
|
|
18
|
+
// Node-local base path backing the hostPath PVs (`<root>/v<replica index>`),
|
|
19
|
+
// generated one-per-replica by MongoBootstrap.applyReplicaVolumes().
|
|
20
|
+
const MONGODB_DATA_ROOT = '/data/mongodb';
|
|
21
|
+
// Replica volumes are hostPath PVs generated one per member, each pinned by `claimRef`. The class
|
|
22
|
+
// must stay static: a dynamic provisioner competes with the static binder and makes
|
|
23
|
+
// member-to-directory assignment non-deterministic.
|
|
24
|
+
const MONGODB_STORAGE_CLASS_NAME = 'mongodb-storage-class';
|
|
25
|
+
const MONGODB_STORAGE_CLASS_PROVISIONER = 'kubernetes.io/no-provisioner';
|
|
26
|
+
/**
|
|
27
|
+
* Mongoose connection options for MongoDB, with sensible defaults for production and development environments.
|
|
28
|
+
* @type {import('mongoose').ConnectOptions}
|
|
29
|
+
*/
|
|
30
|
+
const MONGODB_CONNECTION_OPTIONS = {
|
|
31
|
+
autoIndex: process.env.NODE_ENV !== 'production',
|
|
32
|
+
heartbeatFrequencyMS: 10000,
|
|
33
|
+
maxConnecting: 2,
|
|
34
|
+
maxPoolSize: 10,
|
|
35
|
+
minPoolSize: 0,
|
|
36
|
+
retryReads: true,
|
|
37
|
+
retryWrites: true,
|
|
38
|
+
serverSelectionTimeoutMS: 30000,
|
|
39
|
+
socketTimeoutMS: 120000,
|
|
40
|
+
waitQueueTimeoutMS: 10000,
|
|
41
|
+
};
|
|
18
42
|
|
|
19
43
|
/**
|
|
20
44
|
* Resolves MongoDB replica hosts from explicit input or StatefulSet defaults.
|
|
@@ -49,6 +73,26 @@ const resolveMongoReplicaHosts = ({ hostList = '', replicaCount = MONGODB_DEFAUL
|
|
|
49
73
|
* 3. No built-in defaults — both `host` and `name` are required from the caller or environment.
|
|
50
74
|
*/
|
|
51
75
|
class MongooseDBService {
|
|
76
|
+
/**
|
|
77
|
+
* Establishes a Mongoose connection to the specified MongoDB instance.
|
|
78
|
+
*
|
|
79
|
+
* @async
|
|
80
|
+
* @param {object|string} configOrHost - Either a db config object or a legacy host string.
|
|
81
|
+
* @param {string} [configOrHost.host] - Legacy single host or comma-separated host list.
|
|
82
|
+
* @param {string} [configOrHost.name] - The database name.
|
|
83
|
+
* @param {string} [configOrHost.replicaSet] - The MongoDB replica set name.
|
|
84
|
+
* @param {string} [configOrHost.authSource] - The authentication database.
|
|
85
|
+
* @param {string} [configOrHost.user] - The MongoDB username.
|
|
86
|
+
* @param {string} [configOrHost.password] - The MongoDB password.
|
|
87
|
+
* @param {string} [name] - Legacy database name when a host string is passed.
|
|
88
|
+
* @returns {Promise<mongoose.Connection>} A promise that resolves to the established Mongoose connection object.
|
|
89
|
+
* @throws {Error} If neither the argument nor the corresponding environment variable supplies a value.
|
|
90
|
+
*/
|
|
91
|
+
async connect(configOrHost, name) {
|
|
92
|
+
const uri = this.buildUri(configOrHost, name);
|
|
93
|
+
// if (process.env.NODE_ENV === 'development') logger.info(`Connecting to MongoDB with URI`, uri);
|
|
94
|
+
return await mongoose.createConnection(uri, MONGODB_CONNECTION_OPTIONS).asPromise();
|
|
95
|
+
}
|
|
52
96
|
/**
|
|
53
97
|
* Normalizes Mongo host inputs into plain host:port entries.
|
|
54
98
|
* @param {Array<string>|string} hosts - Host input as list or comma-separated string.
|
|
@@ -125,38 +169,6 @@ class MongooseDBService {
|
|
|
125
169
|
return `mongodb://${credentials}${config.hosts.join(',')}/${config.dbName}${query.size ? `?${query.toString()}` : ''}`;
|
|
126
170
|
}
|
|
127
171
|
|
|
128
|
-
/**
|
|
129
|
-
* Establishes a Mongoose connection to the specified MongoDB instance.
|
|
130
|
-
*
|
|
131
|
-
* @async
|
|
132
|
-
* @param {object|string} configOrHost - Either a db config object or a legacy host string.
|
|
133
|
-
* @param {string} [configOrHost.host] - Legacy single host or comma-separated host list.
|
|
134
|
-
* @param {string} [configOrHost.name] - The database name.
|
|
135
|
-
* @param {string} [configOrHost.replicaSet] - The MongoDB replica set name.
|
|
136
|
-
* @param {string} [configOrHost.authSource] - The authentication database.
|
|
137
|
-
* @param {string} [configOrHost.user] - The MongoDB username.
|
|
138
|
-
* @param {string} [configOrHost.password] - The MongoDB password.
|
|
139
|
-
* @param {string} [name] - Legacy database name when a host string is passed.
|
|
140
|
-
* @returns {Promise<mongoose.Connection>} A promise that resolves to the established Mongoose connection object.
|
|
141
|
-
* @throws {Error} If neither the argument nor the corresponding environment variable supplies a value.
|
|
142
|
-
*/
|
|
143
|
-
async connect(configOrHost, name) {
|
|
144
|
-
const uri = this.buildUri(configOrHost, name);
|
|
145
|
-
if (process.env.NODE_ENV === 'development') logger.info(`Connecting to MongoDB with URI`, uri);
|
|
146
|
-
return await mongoose
|
|
147
|
-
.createConnection(uri, {
|
|
148
|
-
autoIndex: process.env.NODE_ENV !== 'production',
|
|
149
|
-
heartbeatFrequencyMS: 10000,
|
|
150
|
-
maxPoolSize: 20,
|
|
151
|
-
minPoolSize: 2,
|
|
152
|
-
retryReads: true,
|
|
153
|
-
retryWrites: true,
|
|
154
|
-
serverSelectionTimeoutMS: 5000,
|
|
155
|
-
socketTimeoutMS: 45000,
|
|
156
|
-
})
|
|
157
|
-
.asPromise();
|
|
158
|
-
}
|
|
159
|
-
|
|
160
172
|
/**
|
|
161
173
|
* Dynamically loads Mongoose models for a list of APIs and binds them to the given connection.
|
|
162
174
|
*
|
|
@@ -174,6 +186,10 @@ class MongooseDBService {
|
|
|
174
186
|
const { ProviderSchema } = await import(`../../api/${api}/${api}.model.js`);
|
|
175
187
|
const keyModel = getCapVariableName(api); // Assuming this returns a capitalized model name
|
|
176
188
|
models[keyModel] = conn.model(keyModel, ProviderSchema);
|
|
189
|
+
// Mongoose emits 'error' on the model when an autoIndex build fails; with no
|
|
190
|
+
// listener attached EventEmitter rethrows and takes the whole process down.
|
|
191
|
+
// A stale or conflicting index must degrade to a log, not kill startup.
|
|
192
|
+
models[keyModel].on('error', (error) => logger.error(`${keyModel} index build failed: ${error.message}`));
|
|
177
193
|
}
|
|
178
194
|
|
|
179
195
|
return models;
|
|
@@ -191,9 +207,12 @@ const MongooseDB = new MongooseDBService();
|
|
|
191
207
|
export {
|
|
192
208
|
MongooseDB,
|
|
193
209
|
MongooseDBService as MongooseDBClass,
|
|
210
|
+
MONGODB_DATA_ROOT,
|
|
194
211
|
MONGODB_DEFAULT_REPLICA_COUNT,
|
|
195
212
|
MONGODB_DEFAULT_REPLICA_SET,
|
|
196
213
|
MONGODB_SERVICE_NAME,
|
|
197
214
|
MONGODB_STATEFULSET_NAME,
|
|
215
|
+
MONGODB_STORAGE_CLASS_NAME,
|
|
216
|
+
MONGODB_STORAGE_CLASS_PROVISIONER,
|
|
198
217
|
resolveMongoReplicaHosts,
|
|
199
218
|
};
|
package/src/index.js
CHANGED
|
@@ -26,6 +26,8 @@ import UnderpostStatic from './cli/static.js';
|
|
|
26
26
|
import UnderpostTest from './cli/test.js';
|
|
27
27
|
import UnderpostRelease from './cli/release.js';
|
|
28
28
|
import UnderpostSystemProvisionig from './cli/system.js';
|
|
29
|
+
import UnderpostVultr from './cli/vultr.js';
|
|
30
|
+
import UnderpostWireguard from './cli/wireguard.js';
|
|
29
31
|
|
|
30
32
|
import UnderpostDns from './server/dns.js';
|
|
31
33
|
import UnderpostBackup from './server/backup.js';
|
|
@@ -45,7 +47,7 @@ class Underpost {
|
|
|
45
47
|
* @type {String}
|
|
46
48
|
* @memberof Underpost
|
|
47
49
|
*/
|
|
48
|
-
static version = 'v3.
|
|
50
|
+
static version = 'v3.3.0';
|
|
49
51
|
|
|
50
52
|
/**
|
|
51
53
|
* Required Node.js major version
|
|
@@ -252,6 +254,26 @@ class Underpost {
|
|
|
252
254
|
return UnderpostSystemProvisionig.API;
|
|
253
255
|
}
|
|
254
256
|
|
|
257
|
+
/**
|
|
258
|
+
* Edge hub WireGuard/HAProxy cli API
|
|
259
|
+
* @static
|
|
260
|
+
* @type {UnderpostWireguard.API}
|
|
261
|
+
* @memberof Underpost
|
|
262
|
+
*/
|
|
263
|
+
static get wireguard() {
|
|
264
|
+
return UnderpostWireguard.API;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Vultr bandwidth guard cli API
|
|
269
|
+
* @static
|
|
270
|
+
* @type {UnderpostVultr.API}
|
|
271
|
+
* @memberof Underpost
|
|
272
|
+
*/
|
|
273
|
+
static get vultr() {
|
|
274
|
+
return UnderpostVultr.API;
|
|
275
|
+
}
|
|
276
|
+
|
|
255
277
|
/**
|
|
256
278
|
* Dns server API
|
|
257
279
|
* @static
|
|
@@ -352,6 +374,8 @@ export {
|
|
|
352
374
|
UnderpostStartUp,
|
|
353
375
|
UnderpostRelease,
|
|
354
376
|
UnderpostTLS,
|
|
377
|
+
UnderpostVultr,
|
|
378
|
+
UnderpostWireguard,
|
|
355
379
|
};
|
|
356
380
|
|
|
357
381
|
export default Underpost;
|