underpost 3.2.70 → 3.2.90
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/publish.ci.yml +3 -3
- package/.github/workflows/release.cd.yml +1 -1
- package/CHANGELOG.md +1358 -1038
- package/CLI-HELP.md +39 -16
- package/README.md +3 -3
- package/bin/build.js +10 -4
- package/bin/deploy.js +18 -16
- package/docker-compose.yml +1 -1
- package/manifests/cronjobs/dd-cron/dd-cron-backup.yaml +1 -1
- package/manifests/cronjobs/dd-cron/dd-cron-dns.yaml +1 -1
- package/manifests/deployment/dd-default-development/deployment.yaml +2 -2
- package/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 +20 -20
- package/scripts/nat-iptables.sh +10 -4
- package/scripts/test-monitor.sh +4 -3
- package/src/api/core/core.controller.js +4 -65
- package/src/api/core/core.router.js +8 -14
- package/src/api/default/default.controller.js +2 -70
- package/src/api/default/default.router.js +7 -17
- package/src/api/document/document.controller.js +5 -77
- package/src/api/document/document.router.js +9 -13
- package/src/api/file/file.controller.js +9 -53
- package/src/api/file/file.router.js +14 -6
- package/src/api/test/test.controller.js +8 -53
- package/src/api/test/test.router.js +1 -4
- package/src/cli/cluster.js +771 -66
- package/src/cli/db.js +6 -4
- package/src/cli/deploy.js +1715 -168
- package/src/cli/docker-compose.js +19 -24
- package/src/cli/fs.js +0 -1
- package/src/cli/image.js +40 -13
- package/src/cli/index.js +129 -35
- package/src/cli/ipfs.js +82 -11
- package/src/cli/monitor.js +1 -1
- package/src/cli/release.js +4 -0
- package/src/cli/repository.js +14 -3
- package/src/cli/run.js +2253 -439
- package/src/cli/secrets.js +969 -0
- package/src/cli/ssh.js +38 -39
- package/src/client/components/core/Modal.js +38 -4
- package/src/client-builder/client-build.js +94 -11
- package/src/client-builder/ssr.js +27 -73
- package/src/db/mongo/MongoBootstrap.js +295 -54
- package/src/db/mongo/MongooseDB.js +47 -32
- package/src/index.js +1 -1
- package/src/server/conf.js +1307 -6
- package/src/server/cri.js +70 -0
- package/src/server/downloader.js +3 -3
- package/src/server/middlewares.js +152 -0
- package/src/server/underpost-gateway.js +1073 -0
- package/src/server/underpost-ingress.js +364 -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/sops-secret-store.test.js +612 -0
- package/test/underpost-gateway.test.js +469 -0
- package/test/underpost-ingress.test.js +253 -0
|
@@ -81,9 +81,10 @@ class UnderpostDockerCompose {
|
|
|
81
81
|
* Resolves the canonical directory for a custom docker-compose workflow,
|
|
82
82
|
* keyed by `--deploy-id` + `--docker-compose-id`:
|
|
83
83
|
* `engine-private/conf/<deploy-id>/docker-compose/<docker-compose-id>`.
|
|
84
|
-
* This directory ships its own `docker-compose.yml`, `compose.env`, and
|
|
85
|
-
*
|
|
86
|
-
* `--docker-compose-id` is not set (the default,
|
|
84
|
+
* This directory ships its own `docker-compose.yml`, `compose.env`, and any
|
|
85
|
+
* project-specific bind-mounted files (all used as-is, never generated).
|
|
86
|
+
* Returns null when `--docker-compose-id` is not set (the default,
|
|
87
|
+
* self-generating workflow).
|
|
87
88
|
* @param {object} options - CLI options.
|
|
88
89
|
* @returns {string|null} Repo-root-relative canonical dir, or null.
|
|
89
90
|
* @memberof UnderpostDockerCompose
|
|
@@ -99,8 +100,8 @@ class UnderpostDockerCompose {
|
|
|
99
100
|
* so behavior is independent of the caller's working directory.
|
|
100
101
|
*
|
|
101
102
|
* Custom workflow (`--docker-compose-id`): the compose file, env-file, and
|
|
102
|
-
* bind-mounted config
|
|
103
|
-
*
|
|
103
|
+
* project-owned bind-mounted config all live in the canonical dir, so compose
|
|
104
|
+
* runs with `--project-directory` pinned there and no app-override.
|
|
104
105
|
* @param {object} options - CLI options.
|
|
105
106
|
* @returns {string} The base command string (without a subcommand).
|
|
106
107
|
* @memberof UnderpostDockerCompose
|
|
@@ -191,7 +192,7 @@ services:
|
|
|
191
192
|
MONGO_IMAGE=mongo:latest
|
|
192
193
|
VALKEY_IMAGE=valkey/valkey:latest
|
|
193
194
|
APP_IMAGE=underpost/underpost-engine
|
|
194
|
-
APP_TAG=v3.2.
|
|
195
|
+
APP_TAG=v3.2.90
|
|
195
196
|
PROXY_IMAGE=nginx:stable-alpine
|
|
196
197
|
PROMETHEUS_IMAGE=prom/prometheus:latest
|
|
197
198
|
GRAFANA_IMAGE=grafana/grafana:latest
|
|
@@ -371,17 +372,17 @@ datasources:
|
|
|
371
372
|
* @memberof UnderpostDockerCompose
|
|
372
373
|
*/
|
|
373
374
|
static generate(options = {}) {
|
|
374
|
-
//
|
|
375
|
-
//
|
|
376
|
-
//
|
|
377
|
-
// the single generated infra artifact, into <canonical>/mongodb so the
|
|
378
|
-
// stack stays self-contained under `--project-directory`.
|
|
375
|
+
// A named custom workflow is fully owned by its canonical directory. The
|
|
376
|
+
// generic CLI validates the standard entry files but never imports project
|
|
377
|
+
// modules or generates application-specific nginx, env, or helper content.
|
|
379
378
|
const composeIdBase = UnderpostDockerCompose.composeIdBase(options);
|
|
380
379
|
if (composeIdBase) {
|
|
381
|
-
const
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
380
|
+
const required = ['docker-compose.yml', 'compose.env'].map((name) =>
|
|
381
|
+
UnderpostDockerCompose.resolve(`${composeIdBase}/${name}`),
|
|
382
|
+
);
|
|
383
|
+
const missing = required.filter((path) => !fs.existsSync(path));
|
|
384
|
+
if (missing.length) throw new Error(`Custom Docker Compose workflow is missing: ${missing.join(', ')}`);
|
|
385
|
+
logger.info('custom Docker Compose workflow uses canonical files as-is', { path: composeIdBase });
|
|
385
386
|
return;
|
|
386
387
|
}
|
|
387
388
|
|
|
@@ -491,16 +492,10 @@ datasources:
|
|
|
491
492
|
silentOnError: true,
|
|
492
493
|
});
|
|
493
494
|
|
|
494
|
-
//
|
|
495
|
-
//
|
|
496
|
-
// artifact (the mongo entrypoint), regenerated on the next --up/--generate.
|
|
495
|
+
// A named custom workflow owns every file beneath its canonical directory.
|
|
496
|
+
// Reset tears down Docker resources but never prunes project-owned files.
|
|
497
497
|
const composeIdBase = UnderpostDockerCompose.composeIdBase(options);
|
|
498
498
|
if (composeIdBase) {
|
|
499
|
-
const mongoEntrypointPath = UnderpostDockerCompose.resolve(`${composeIdBase}/mongodb/entrypoint.sh`);
|
|
500
|
-
if (fs.existsSync(mongoEntrypointPath)) {
|
|
501
|
-
fs.removeSync(mongoEntrypointPath);
|
|
502
|
-
logger.info('removed generated artifact', { path: mongoEntrypointPath });
|
|
503
|
-
}
|
|
504
499
|
logger.info('Docker Compose reset complete. Run `--up` to recreate the stack.');
|
|
505
500
|
return;
|
|
506
501
|
}
|
|
@@ -545,7 +540,7 @@ datasources:
|
|
|
545
540
|
* @param {boolean} [options.shell] - Open an interactive shell in `target` (default: app).
|
|
546
541
|
* @param {string} [options.exec] - General-purpose passthrough docker compose subcommand.
|
|
547
542
|
* @param {string} [options.deployId] - Deployment to run as the app (default: dd-default). `dd-default` self-bootstraps a fresh engine; any other id runs the standard `underpost start` command.
|
|
548
|
-
* @param {string} [options.dockerComposeId] - Custom-workflow selector. When set, use the canonical stack at `engine-private/conf/<deploy-id>/docker-compose/<docker-compose-id>/` (docker-compose.yml + compose.env
|
|
543
|
+
* @param {string} [options.dockerComposeId] - Custom-workflow selector. When set, use the canonical stack at `engine-private/conf/<deploy-id>/docker-compose/<docker-compose-id>/` (docker-compose.yml + compose.env, used as-is), skipping nginx/env/app-override generation.
|
|
549
544
|
* @param {string} [options.env] - Deployment environment for non-default deploy ids (default: development).
|
|
550
545
|
* @param {string} [options.composeFile] - Override compose file path.
|
|
551
546
|
* @param {string} [options.envFile] - Override env-file path.
|
package/src/cli/fs.js
CHANGED
package/src/cli/image.js
CHANGED
|
@@ -12,6 +12,7 @@ import { loggerFactory } from '../server/logger.js';
|
|
|
12
12
|
import Underpost from '../index.js';
|
|
13
13
|
import { getNpmRootPath } from '../server/conf.js';
|
|
14
14
|
import { shellExec } from '../server/process.js';
|
|
15
|
+
import { crictlCommandFactory } from '../server/cri.js';
|
|
15
16
|
|
|
16
17
|
const logger = loggerFactory(import.meta);
|
|
17
18
|
|
|
@@ -117,20 +118,38 @@ class UnderpostImage {
|
|
|
117
118
|
};
|
|
118
119
|
// addBuildSecret('github_token', process.env.GITHUB_TOKEN);
|
|
119
120
|
addBuildSecret('github_username', process.env.GITHUB_USERNAME);
|
|
120
|
-
//
|
|
121
|
-
//
|
|
122
|
-
//
|
|
123
|
-
//
|
|
121
|
+
// Build secrets are created dynamically: each `RUN --mount=type=secret,id=<x>`
|
|
122
|
+
// id maps to a host env var; only the ones actually set are passed, so a
|
|
123
|
+
// Dockerfile that omits (or comments out) a mount just never sees it. Extend
|
|
124
|
+
// this map — plus a merge of any caller-supplied `options.buildSecrets` — to
|
|
125
|
+
// add more without touching the call sites. Secrets never persist in image
|
|
126
|
+
// history (unlike build-args); they live in 0600 temp files removed post-build.
|
|
127
|
+
const BUILD_SECRET_ENV = {
|
|
128
|
+
// // Cloudinary creds power build-time asset pulls (`node bin fs --pull`).
|
|
129
|
+
// cloudinary_cloud_name: 'CLOUDINARY_CLOUD_NAME',
|
|
130
|
+
// cloudinary_api_key: 'CLOUDINARY_API_KEY',
|
|
131
|
+
// cloudinary_api_secret: 'CLOUDINARY_API_SECRET',
|
|
132
|
+
};
|
|
133
|
+
for (const [id, envVar] of Object.entries(BUILD_SECRET_ENV)) addBuildSecret(id, process.env[envVar]);
|
|
134
|
+
for (const [id, value] of Object.entries(options.buildSecrets || {})) addBuildSecret(id, value);
|
|
124
135
|
const secretArgs = secretFlags.length ? ` ${secretFlags.join(' ')}` : '';
|
|
125
|
-
if (secretFlags.length)
|
|
126
|
-
|
|
136
|
+
if (secretFlags.length) logger.info('Passing host credentials as build secrets', { ids: secretFlags.length });
|
|
137
|
+
|
|
138
|
+
// Non-secret build args (e.g. INSTANCE_CODES for engine-cyberia): injected
|
|
139
|
+
// dynamically from `options.buildArgs`, overriding the Dockerfile's ARG
|
|
140
|
+
// defaults. Unlike secrets these DO persist in image metadata, so only
|
|
141
|
+
// non-sensitive values belong here.
|
|
142
|
+
const buildArgFlags = Object.entries(options.buildArgs || {})
|
|
143
|
+
.filter(([, v]) => v !== undefined && v !== null && v !== '')
|
|
144
|
+
.map(([k, v]) => `--build-arg ${k}=${JSON.stringify(String(v))}`);
|
|
145
|
+
const buildArgStr = buildArgFlags.length ? ` ${buildArgFlags.join(' ')}` : '';
|
|
127
146
|
|
|
128
147
|
if (path)
|
|
129
148
|
try {
|
|
130
149
|
shellExec(
|
|
131
150
|
`cd ${path} && sudo podman build -f ./${
|
|
132
151
|
dockerfileName && typeof dockerfileName === 'string' ? dockerfileName : 'Dockerfile'
|
|
133
|
-
} -t ${imageName} --pull=never --cap-add=CAP_AUDIT_WRITE${cache}${secretArgs} --network host`,
|
|
152
|
+
} -t ${imageName} --pull=never --cap-add=CAP_AUDIT_WRITE${cache}${secretArgs}${buildArgStr} --network host`,
|
|
134
153
|
);
|
|
135
154
|
} finally {
|
|
136
155
|
for (const file of secretTmpFiles) {
|
|
@@ -207,6 +226,7 @@ class UnderpostImage {
|
|
|
207
226
|
* @param {object} options - Options for the image retrieval.
|
|
208
227
|
* @param {boolean} options.spec - Whether to retrieve images from the pod specifications.
|
|
209
228
|
* @param {string} options.namespace - Kubernetes namespace to filter pods.
|
|
229
|
+
* @param {boolean} [options.k3s] - Resolve the CRI endpoint as K3s' embedded containerd.
|
|
210
230
|
* @returns {Array<object>} - Array of objects containing pod names and their corresponding images.
|
|
211
231
|
* @memberof UnderpostImage
|
|
212
232
|
*/
|
|
@@ -227,10 +247,17 @@ class UnderpostImage {
|
|
|
227
247
|
}))
|
|
228
248
|
.filter((o) => o.image);
|
|
229
249
|
}
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
250
|
+
// Outside kind, target the live CRI endpoint explicitly: /etc/crictl.yaml
|
|
251
|
+
// may still point at a CRI-O socket the host no longer runs.
|
|
252
|
+
const raw = shellExec(
|
|
253
|
+
node === 'kind-worker'
|
|
254
|
+
? `docker exec -i ${node} crictl images`
|
|
255
|
+
: crictlCommandFactory('images', options || {}),
|
|
256
|
+
{
|
|
257
|
+
stdout: true,
|
|
258
|
+
silent: true,
|
|
259
|
+
},
|
|
260
|
+
);
|
|
234
261
|
|
|
235
262
|
const heads = raw
|
|
236
263
|
.split(`\n`)[0]
|
|
@@ -294,7 +321,7 @@ class UnderpostImage {
|
|
|
294
321
|
shellExec(`docker exec -i kind-control-plane crictl rmi ${imageName}`);
|
|
295
322
|
shellExec(`docker exec -i kind-worker crictl rmi ${imageName}`);
|
|
296
323
|
} else if (kubeadm === true) {
|
|
297
|
-
shellExec(`
|
|
324
|
+
shellExec(crictlCommandFactory(`rmi ${imageName}`));
|
|
298
325
|
} else if (k3s === true) {
|
|
299
326
|
shellExec(`sudo k3s ctr images rm ${imageName}`);
|
|
300
327
|
}
|
|
@@ -363,7 +390,7 @@ class UnderpostImage {
|
|
|
363
390
|
shellExec(`docker pull ${image}`);
|
|
364
391
|
shellExec(`sudo kind load docker-image ${image}`);
|
|
365
392
|
} else {
|
|
366
|
-
shellExec(`
|
|
393
|
+
shellExec(crictlCommandFactory(`pull ${image}`, { k3s: targetK3s }));
|
|
367
394
|
}
|
|
368
395
|
},
|
|
369
396
|
};
|
package/src/cli/index.js
CHANGED
|
@@ -108,6 +108,10 @@ program
|
|
|
108
108
|
.option('--edit', 'Edit last commit.')
|
|
109
109
|
.option('--deploy-id <deploy-id>', 'Sets the deployment configuration ID for the commit context.')
|
|
110
110
|
.option('--cached', 'Commit staged changes only or context.')
|
|
111
|
+
.option(
|
|
112
|
+
'--init-repo [origin]',
|
|
113
|
+
'Initialize a git repository at the specified path. Optionally set the git remote origin URL.',
|
|
114
|
+
)
|
|
111
115
|
.option('--hashes <hashes>', 'Comma-separated list of specific file hashes of commits.')
|
|
112
116
|
.option('--extension <extension>', 'specific file extensions of commits.')
|
|
113
117
|
.option('--changelog', 'Print the plain changelog of the last N commits (see --from-n-commit, default 1).')
|
|
@@ -260,7 +264,8 @@ program
|
|
|
260
264
|
.option('--reset', `Deletes all clusters and prunes all related data and caches.`)
|
|
261
265
|
.option(
|
|
262
266
|
'--reset-mongodb',
|
|
263
|
-
`Performs a hard cleanup of only MongoDB-related resources (StatefulSet, PVCs/PVs, Secrets, ConfigMaps, caches) without restarting the whole node
|
|
267
|
+
`Performs a hard cleanup of only MongoDB-related resources (StatefulSet, PVCs/PVs, Secrets, ConfigMaps, caches) without restarting the whole node. ` +
|
|
268
|
+
`Combined with --mongodb it instead wipes the retained hostPath volumes as part of that deploy, so the replica set starts from empty data.`,
|
|
264
269
|
)
|
|
265
270
|
.option('--mariadb', 'Initializes the cluster with a MariaDB statefulset.')
|
|
266
271
|
.option('--mysql', 'Initializes the cluster with a MySQL statefulset.')
|
|
@@ -271,6 +276,16 @@ program
|
|
|
271
276
|
.option('--valkey', 'Initializes the cluster with a Valkey service.')
|
|
272
277
|
.option('--ipfs', 'Initializes the cluster with an ipfs-cluster statefulset.')
|
|
273
278
|
.option('--contour', 'Initializes the cluster with Project Contour base HTTPProxy and Envoy.')
|
|
279
|
+
.option(
|
|
280
|
+
'--gateway-api',
|
|
281
|
+
'Initializes the cluster with the Gateway API control plane (CRDs, Envoy Gateway, GatewayClass) used by generated HTTPRoute + QUIC/HTTP3 manifests. ' +
|
|
282
|
+
'With --dev the data plane binds the listener ports on the host network for direct browser access.',
|
|
283
|
+
)
|
|
284
|
+
.option('--gateway-class <name>', 'GatewayClass name to provision (default "eg").')
|
|
285
|
+
.option(
|
|
286
|
+
'--ingress-node <node-name>',
|
|
287
|
+
'Dedicated node for underpost-ingress when both routing stacks coexist. Workload placement flags do not move it.',
|
|
288
|
+
)
|
|
274
289
|
.option(
|
|
275
290
|
'--node-port',
|
|
276
291
|
'Exposes enabled ready services (e.g. MongoDB 4.4, Valkey) to the host/public network via their NodePort Service manifest.',
|
|
@@ -328,7 +343,6 @@ program
|
|
|
328
343
|
.option('--remove', 'Deletes specified deployments and their associated services.')
|
|
329
344
|
.option('--sync', 'Synchronizes deployment environment variables, ports, and replica counts.')
|
|
330
345
|
.option('--info-router', 'Displays the current router structure and configuration.')
|
|
331
|
-
.option('--expose', 'Exposes services matching the provided deployment ID list.')
|
|
332
346
|
.option('--cert', 'Resets TLS/SSL certificate secrets for deployments.')
|
|
333
347
|
.option('--cert-hosts <hosts>', 'Resets TLS/SSL certificate secrets for specified hosts.')
|
|
334
348
|
.option(
|
|
@@ -338,6 +352,10 @@ program
|
|
|
338
352
|
'Enables TLS in the Contour HTTPProxy virtualhost without requiring a production ClusterIssuer.',
|
|
339
353
|
)
|
|
340
354
|
.option('--node <node>', 'Sets optional node for deployment operations.')
|
|
355
|
+
.option(
|
|
356
|
+
'--ingress-node <node-name>',
|
|
357
|
+
'Explicitly relocates the shared host-network ingress; ordinary --node placement never moves it.',
|
|
358
|
+
)
|
|
341
359
|
.option(
|
|
342
360
|
'--ssh-key-path <path>',
|
|
343
361
|
'Private key path for node SSH operations. Currently used when shipping a hostPath volume to a remote target node over SSH. Defaults to engine-private/deploy/id_rsa.',
|
|
@@ -346,6 +364,10 @@ program
|
|
|
346
364
|
'--build-manifest',
|
|
347
365
|
'Builds Kubernetes YAML manifests, including deployments, services, proxies, and secrets.',
|
|
348
366
|
)
|
|
367
|
+
.option(
|
|
368
|
+
'--sync-static',
|
|
369
|
+
'Places the SSR status pages and intercepted contexts in the gateway static utility tree, so the edge serves them instead of the application pods. Prefers the running workload and falls back to this checkout, so it can seed the tree before the deployment exists and refresh it once the deployment is Ready.',
|
|
370
|
+
)
|
|
349
371
|
.option('--replicas <replicas>', 'Sets a custom number of replicas for deployments.')
|
|
350
372
|
.option('--image <image>', 'Sets a custom image for deployments.')
|
|
351
373
|
.option('--versions <deployment-versions>', 'A comma-separated list of custom deployment versions.')
|
|
@@ -358,35 +380,31 @@ program
|
|
|
358
380
|
.option('--retry-count <count>', 'Sets HTTPProxy per-route retry count (e.g., 3).')
|
|
359
381
|
.option('--retry-per-try-timeout <duration>', 'Sets HTTPProxy retry per-try timeout (e.g., "150ms").')
|
|
360
382
|
.option('--disable-update-deployment', 'Disables updates to deployments.')
|
|
361
|
-
.option(
|
|
383
|
+
.option(
|
|
384
|
+
'--disable-runtime-probes',
|
|
385
|
+
'Deprecated compatibility flag; readiness probes remain mandatory. Use --tcp-probes for legacy workloads.',
|
|
386
|
+
)
|
|
362
387
|
.option('--tcp-probes', 'Generates legacy TCP socket probes instead of HTTP internal-status probes (migration).')
|
|
363
388
|
.option('--disable-update-proxy', 'Disables updates to proxies.')
|
|
364
389
|
.option('--disable-deployment-proxy', 'Disables proxies of deployments.')
|
|
365
|
-
.option('--disable-update-volume', 'Disables updates to volume mounts during deployment.')
|
|
366
390
|
.option(
|
|
367
|
-
'--
|
|
368
|
-
'
|
|
391
|
+
'--gateway-api',
|
|
392
|
+
'Routes through the Gateway API stack (Gateway + HTTPRoute) instead of the Contour HTTPProxy. ' +
|
|
393
|
+
'Both manifest sets are always generated; this selects which one is applied.',
|
|
394
|
+
)
|
|
395
|
+
.option('--gateway-class <name>', 'GatewayClass name for generated Gateway manifests (default "eg").')
|
|
396
|
+
.option(
|
|
397
|
+
'--disable-http3',
|
|
398
|
+
'Omits the QUIC/HTTP3 listener config and the Alt-Svc advertisement from Gateway API manifests.',
|
|
369
399
|
)
|
|
400
|
+
.option('--quic-port <port>', 'UDP port advertised for QUIC/HTTP3 in generated Gateway API manifests (default 443).')
|
|
401
|
+
.option('--disable-update-volume', 'Disables updates to volume mounts during deployment.')
|
|
370
402
|
.option('--kubeadm', 'Enables the kubeadm context for deployment operations.')
|
|
371
403
|
.option('--k3s', 'Enables the k3s context for deployment operations.')
|
|
372
404
|
.option('--kind', 'Enables the kind context for deployment operations.')
|
|
373
405
|
.option('--git-clean', 'Runs git clean on volume mount paths before copying.')
|
|
374
406
|
.option('--disable-update-underpost-config', 'Disables updates to Underpost configuration during deployment.')
|
|
375
407
|
.option('--namespace <namespace>', 'Kubernetes namespace for deployment operations (defaults to "default").')
|
|
376
|
-
.option('--kind-type <kind-type>', 'Specifies the Kind cluster type for deployment operations.')
|
|
377
|
-
.option('--port <port>', 'Sets up port forwarding from local to remote ports.')
|
|
378
|
-
.option(
|
|
379
|
-
'--expose-port <port>',
|
|
380
|
-
'Sets the local:remote port to expose when --expose is active (overrides auto-detected service port).',
|
|
381
|
-
)
|
|
382
|
-
.option(
|
|
383
|
-
'--expose-local-port <port>',
|
|
384
|
-
'Sets a different local port for --expose (e.g. 80) while keeping the remote service port. Useful for /etc/hosts local access without specifying a port in the browser.',
|
|
385
|
-
)
|
|
386
|
-
.option(
|
|
387
|
-
'--local-proxy',
|
|
388
|
-
'Forward all service TCP ports locally and start the Node.js path-routing proxy. Enables full path-based routing (e.g. /wp alongside /) without needing --expose-local-port. Requires --expose.',
|
|
389
|
-
)
|
|
390
408
|
.option('--cmd <cmd>', 'Custom initialization command for deployment (comma-separated commands).')
|
|
391
409
|
.option(
|
|
392
410
|
'--skip-full-build',
|
|
@@ -400,30 +418,71 @@ program
|
|
|
400
418
|
'--image-pull-policy <policy>',
|
|
401
419
|
'Override container imagePullPolicy in the generated deployment manifest (Always, IfNotPresent, Never). Defaults to Never for localhost/ images and IfNotPresent otherwise.',
|
|
402
420
|
)
|
|
403
|
-
.option(
|
|
404
|
-
'--tls',
|
|
405
|
-
'Enables TLS for the local proxy started by --expose --local-proxy. ' +
|
|
406
|
-
'The proxy will serve HTTPS on port 443 using self-signed certificates resolved from the local SSL store. ' +
|
|
407
|
-
'Use together with --expose and --local-proxy.',
|
|
408
|
-
)
|
|
409
421
|
.description('Manages application deployments, defaulting to deploying development pods.')
|
|
410
422
|
.action(Underpost.deploy.callback);
|
|
411
423
|
|
|
412
424
|
program
|
|
413
425
|
.command('secret')
|
|
414
|
-
.argument(
|
|
426
|
+
.argument(
|
|
427
|
+
'[platform]',
|
|
428
|
+
`The secret management platform. Options: ${Object.keys(Underpost.secret).join(', ')}. Defaults to "sops".`,
|
|
429
|
+
'sops',
|
|
430
|
+
)
|
|
415
431
|
.option('--init', 'Initializes the secrets platform environment.')
|
|
416
432
|
.option('--create-from-file <path-env-file>', 'Creates secrets from a specified environment file.')
|
|
417
433
|
.option('--create-from-env', 'Creates secrets from container environment variables (envFrom: secretRef).')
|
|
418
434
|
.option('--global-clean', 'Removes all filesystem traces of secrets (engine-private, .env, conf cache).')
|
|
419
435
|
.option('--list', 'Lists all available secrets for the platform.')
|
|
436
|
+
.option(
|
|
437
|
+
'--encrypt <plaintext-path>',
|
|
438
|
+
'Encrypts a plaintext Secret manifest into the Git-tracked SOPS store and shreds the source (sops platform).',
|
|
439
|
+
)
|
|
440
|
+
.option(
|
|
441
|
+
'--apply',
|
|
442
|
+
'Decrypts stored SOPS manifests and streams them into kubectl apply, without writing plaintext to disk (sops platform).',
|
|
443
|
+
)
|
|
444
|
+
.option('--namespace <namespace>', 'Kubernetes namespace for secret operations (defaults to "default").')
|
|
445
|
+
.option(
|
|
446
|
+
'--install-tools',
|
|
447
|
+
'Installs the sops and age host binaries only, without running a full cluster host initialization.',
|
|
448
|
+
)
|
|
449
|
+
.option(
|
|
450
|
+
'--rotate',
|
|
451
|
+
'Re-keys every stored SOPS manifest onto --recipient. Secret values are unchanged, so no workload restart is needed.',
|
|
452
|
+
)
|
|
453
|
+
.option('--recipient <age-public-key>', 'Incoming Age public recipient for --rotate.')
|
|
454
|
+
.option(
|
|
455
|
+
'--prune-recipients',
|
|
456
|
+
'With --rotate, makes --recipient the only recipient, revoking every previous key (use after a key compromise). ' +
|
|
457
|
+
'Requires --force, and revokes CI/CD keys too unless they are named in --keep-recipients.',
|
|
458
|
+
)
|
|
459
|
+
.option(
|
|
460
|
+
'--keep-recipients <age-public-keys>',
|
|
461
|
+
'Comma-separated recipients to retain while --prune-recipients revokes the rest (e.g. the CI/CD key).',
|
|
462
|
+
)
|
|
463
|
+
.option(
|
|
464
|
+
'--purge <secret-name>',
|
|
465
|
+
'Emergency removal: deletes the live Kubernetes Secret and takes its encrypted manifest out of the store.',
|
|
466
|
+
)
|
|
467
|
+
.option(
|
|
468
|
+
'--force',
|
|
469
|
+
'Confirms the irreversible variant: deletes the manifest instead of archiving it (--purge), ' +
|
|
470
|
+
'revokes recipients (--rotate --prune-recipients), or replaces an existing manifest (--encrypt).',
|
|
471
|
+
)
|
|
472
|
+
.option('--dry-run', 'Reports what --apply, --rotate, or --purge would do without changing anything.')
|
|
420
473
|
.description(`Manages secrets for various platforms.`)
|
|
421
|
-
.action((
|
|
422
|
-
|
|
423
|
-
if (
|
|
424
|
-
if (
|
|
425
|
-
if (
|
|
426
|
-
if (
|
|
474
|
+
.action((platform, options) => {
|
|
475
|
+
// Host tooling install is platform-independent, so it resolves before any platform lookup.
|
|
476
|
+
if (options.installTools) return Underpost.secret.sops.installTooling();
|
|
477
|
+
if (options.globalClean) return Underpost.secret.globalSecretClean();
|
|
478
|
+
if (options.rotate) return Underpost.secret[platform].rotate(options.recipient, options);
|
|
479
|
+
if (options.purge) return Underpost.secret[platform].purge(options.purge, options);
|
|
480
|
+
if (options.encrypt) return Underpost.secret[platform].encrypt(options.encrypt, options.namespace, options);
|
|
481
|
+
if (options.apply) return Underpost.secret[platform].apply(options.namespace, options);
|
|
482
|
+
if (options.createFromFile) return Underpost.secret[platform].createFromEnvFile(options.createFromFile);
|
|
483
|
+
if (options.createFromEnv) return Underpost.secret[platform].createFromContainerEnv();
|
|
484
|
+
if (options.list) return Underpost.secret[platform].list();
|
|
485
|
+
if (options.init) return Underpost.secret[platform].init();
|
|
427
486
|
});
|
|
428
487
|
|
|
429
488
|
program
|
|
@@ -648,11 +707,24 @@ program
|
|
|
648
707
|
.option('--replicas <replicas>', 'Sets a custom number of replicas for deployment.')
|
|
649
708
|
.option('--pod-name <pod-name>', 'Optional: Specifies the pod name for execution.')
|
|
650
709
|
.option('--node-name <node-name>', 'Optional: Specifies the node name for execution.')
|
|
710
|
+
.option(
|
|
711
|
+
'--ingress-node <node-name>',
|
|
712
|
+
'Dedicated node for the host-network underpost-ingress listener. Workload --node-name never relocates it.',
|
|
713
|
+
)
|
|
651
714
|
.option(
|
|
652
715
|
'--ssh-key-path <path>',
|
|
653
716
|
'Optional: Private key path for node SSH operations, forwarded to volume shipping over SSH. Defaults to engine-private/deploy/id_rsa.',
|
|
654
717
|
)
|
|
655
718
|
.option('--port <port>', 'Optional: Specifies the port for execution.')
|
|
719
|
+
.option(
|
|
720
|
+
'--expose-container-ports <ports>',
|
|
721
|
+
'Comma-separated Service/container ports; multiple matched resources consume values by resource index.',
|
|
722
|
+
)
|
|
723
|
+
.option(
|
|
724
|
+
'--expose-host-ports <ports>',
|
|
725
|
+
'Comma-separated host ports paired with container ports by resource/port index.',
|
|
726
|
+
)
|
|
727
|
+
.option('--local-proxy', 'Starts the development path proxy after the expose runner creates its port-forwards.')
|
|
656
728
|
.option('--etc-hosts', 'Enables etc-hosts context for the runner execution.')
|
|
657
729
|
.option('--volume-host-path <volume-host-path>', 'Optional: Specifies the volume host path for test execution.')
|
|
658
730
|
.option('--volume-mount-path <volume-mount-path>', 'Optional: Specifies the volume mount path for test execution.')
|
|
@@ -688,7 +760,10 @@ program
|
|
|
688
760
|
'--resource-template-id <resource-template-id >',
|
|
689
761
|
'Specifies a resource template ID for the runner execution.',
|
|
690
762
|
)
|
|
691
|
-
.option(
|
|
763
|
+
.option(
|
|
764
|
+
'--expose',
|
|
765
|
+
'Enables exposure-only behavior in compatible runners; the expose runner itself does not require this flag.',
|
|
766
|
+
)
|
|
692
767
|
.option('--conf-server-path <conf-server-path>', 'Sets a custom configuration server path.')
|
|
693
768
|
.option('--underpost-root <underpost-root>', 'Sets a custom Underpost root path.')
|
|
694
769
|
.option('--cmd-cron-jobs <cmd-cron-jobs>', 'Pre-script commands to run before cron job execution.')
|
|
@@ -700,7 +775,10 @@ program
|
|
|
700
775
|
.option('--kubeadm', 'Sets the kubeadm cluster context for the runner execution.')
|
|
701
776
|
.option('--k3s', 'Sets the k3s cluster context for the runner execution.')
|
|
702
777
|
.option('--kind', 'Sets the kind cluster context for the runner execution.')
|
|
703
|
-
.option(
|
|
778
|
+
.option(
|
|
779
|
+
'--traffic <traffic>',
|
|
780
|
+
'Blue/green traffic colour to bake into generated manifests (default: blue). `stop` accepts a comma list, e.g. blue,green.',
|
|
781
|
+
)
|
|
704
782
|
.option('--git-clean', 'Runs git clean on volume mount paths before copying.')
|
|
705
783
|
.option('--deploy-id <deploy-id>', 'Sets deploy id context for the runner execution.')
|
|
706
784
|
.option('--user <user>', 'Sets user context for the runner execution.')
|
|
@@ -714,6 +792,21 @@ program
|
|
|
714
792
|
.option('--timeout-idle <duration>', 'Sets HTTPProxy per-route idle timeout (e.g., "10s", "infinity").')
|
|
715
793
|
.option('--retry-count <count>', 'Sets HTTPProxy per-route retry count (e.g., 3).')
|
|
716
794
|
.option('--retry-per-try-timeout <duration>', 'Sets HTTPProxy retry per-try timeout (e.g., "150ms").')
|
|
795
|
+
.option(
|
|
796
|
+
'--gateway-api',
|
|
797
|
+
'Routes through the Gateway API stack (Gateway + HTTPRoute) instead of the Contour HTTPProxy. ' +
|
|
798
|
+
'Both manifest sets are always generated; this selects which one is applied.',
|
|
799
|
+
)
|
|
800
|
+
.option(
|
|
801
|
+
'--disable-gateway-api',
|
|
802
|
+
'Falls back to the Contour HTTPProxy stack in runners where the Gateway API is the default (cluster).',
|
|
803
|
+
)
|
|
804
|
+
.option('--gateway-class <name>', 'GatewayClass name for generated Gateway manifests (default "eg").')
|
|
805
|
+
.option(
|
|
806
|
+
'--disable-http3',
|
|
807
|
+
'Omits the QUIC/HTTP3 listener config and the Alt-Svc advertisement from Gateway API manifests.',
|
|
808
|
+
)
|
|
809
|
+
.option('--quic-port <port>', 'UDP port advertised for QUIC/HTTP3 in generated Gateway API manifests (default 443).')
|
|
717
810
|
.option('--disable-private-conf-update', 'Disables updates to private configuration during execution.')
|
|
718
811
|
.option('--logs', 'Streams logs during the runner execution.')
|
|
719
812
|
.option('--monitor-status <status>', 'Sets the status to monitor for pod/resource (default: "Running").')
|
|
@@ -758,6 +851,7 @@ program
|
|
|
758
851
|
'--test',
|
|
759
852
|
'Enables test/generic-purpose mode for the runner (e.g. use self-signed TLS instead of cert-manager).',
|
|
760
853
|
)
|
|
854
|
+
.option('--branch <branch>', 'Sets the branch for git operations (default: current branch).')
|
|
761
855
|
.description('Runs specified scripts using various runners.')
|
|
762
856
|
.action(Underpost.run.callback);
|
|
763
857
|
|
package/src/cli/ipfs.js
CHANGED
|
@@ -6,11 +6,14 @@
|
|
|
6
6
|
|
|
7
7
|
import { loggerFactory } from '../server/logger.js';
|
|
8
8
|
import { shellExec } from '../server/process.js';
|
|
9
|
+
import { resolveReplicaCount } from '../server/conf.js';
|
|
9
10
|
import fs from 'fs-extra';
|
|
10
11
|
import Underpost from '../index.js';
|
|
11
12
|
|
|
12
13
|
const logger = loggerFactory(import.meta);
|
|
13
14
|
|
|
15
|
+
const IPFS_DEFAULT_REPLICA_COUNT = 3;
|
|
16
|
+
|
|
14
17
|
/**
|
|
15
18
|
* @class UnderpostIPFS
|
|
16
19
|
* @description Manages deployment of an ipfs-cluster StatefulSet on Kubernetes.
|
|
@@ -25,8 +28,12 @@ class UnderpostIPFS {
|
|
|
25
28
|
* @description Resolves the IPFS cluster credentials from engine-private/ if they
|
|
26
29
|
* already exist, otherwise generates new ones (hex cluster secret + peer identity
|
|
27
30
|
* via ipfs-cluster-service init) and persists them with mode 0o600.
|
|
31
|
+
* Idempotent and self-healing: an existing pair is reused untouched, and a missing or
|
|
32
|
+
* unreadable one is regenerated. Both files are always rewritten together — the peer id and
|
|
33
|
+
* the private key are two halves of one identity, so reusing one with a freshly minted other
|
|
34
|
+
* would advertise a peer id that does not match the key.
|
|
28
35
|
* @param {string} privateDir - Absolute path to the engine-private directory.
|
|
29
|
-
* @returns {{ CLUSTER_SECRET: string, IDENTITY_JSON: { id: string, private_key: string } }}
|
|
36
|
+
* @returns {{ CLUSTER_SECRET: string, IDENTITY_JSON: { id: string, private_key: string }, generated: boolean }}
|
|
30
37
|
* @memberof UnderpostIPFS
|
|
31
38
|
*/
|
|
32
39
|
resolveCredentials(privateDir) {
|
|
@@ -34,11 +41,17 @@ class UnderpostIPFS {
|
|
|
34
41
|
const identityPath = `${privateDir}/ipfs-cluster-identity.json`;
|
|
35
42
|
|
|
36
43
|
if (fs.existsSync(secretPath) && fs.existsSync(identityPath)) {
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
44
|
+
try {
|
|
45
|
+
const CLUSTER_SECRET = fs.readFileSync(secretPath, 'utf8').trim();
|
|
46
|
+
const IDENTITY_JSON = JSON.parse(fs.readFileSync(identityPath, 'utf8'));
|
|
47
|
+
if (CLUSTER_SECRET && IDENTITY_JSON?.id && IDENTITY_JSON?.private_key) {
|
|
48
|
+
logger.info('Reusing existing IPFS cluster credentials from engine-private/');
|
|
49
|
+
return { CLUSTER_SECRET, IDENTITY_JSON, generated: false };
|
|
50
|
+
}
|
|
51
|
+
logger.warn('Existing IPFS cluster credentials are incomplete; regenerating');
|
|
52
|
+
} catch (error) {
|
|
53
|
+
logger.warn(`Existing IPFS cluster credentials are unreadable (${error.message}); regenerating`);
|
|
54
|
+
}
|
|
42
55
|
}
|
|
43
56
|
|
|
44
57
|
logger.info('Generating new IPFS cluster credentials and persisting to engine-private/');
|
|
@@ -61,7 +74,52 @@ class UnderpostIPFS {
|
|
|
61
74
|
|
|
62
75
|
logger.info(`IPFS cluster credentials saved (peer ID: ${IDENTITY_JSON.id})`);
|
|
63
76
|
|
|
64
|
-
return { CLUSTER_SECRET, IDENTITY_JSON };
|
|
77
|
+
return { CLUSTER_SECRET, IDENTITY_JSON, generated: true };
|
|
78
|
+
},
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* @method storeCredentials
|
|
82
|
+
* @description Encrypts the current IPFS credentials into the SOPS store, replacing whatever
|
|
83
|
+
* is there. Called after a regeneration so the encrypted Secret cannot drift from the peer id
|
|
84
|
+
* the `env-config` ConfigMap advertises — the id lives only in the local identity file, so a
|
|
85
|
+
* stale manifest would pair someone else's private key with the new id and the cluster would
|
|
86
|
+
* never form.
|
|
87
|
+
* @param {{ CLUSTER_SECRET: string, IDENTITY_JSON: { private_key: string } }} credentials
|
|
88
|
+
* @param {object} options
|
|
89
|
+
* @param {string} options.namespace - Kubernetes namespace.
|
|
90
|
+
* @memberof UnderpostIPFS
|
|
91
|
+
*/
|
|
92
|
+
storeCredentials({ CLUSTER_SECRET, IDENTITY_JSON }, options) {
|
|
93
|
+
const stageDir = '/dev/shm/underpost-secrets';
|
|
94
|
+
const stagePath = `${stageDir}/ipfs-cluster-secret.yaml`;
|
|
95
|
+
fs.ensureDirSync(stageDir);
|
|
96
|
+
fs.chmodSync(stageDir, 0o700);
|
|
97
|
+
try {
|
|
98
|
+
fs.outputFileSync(
|
|
99
|
+
stagePath,
|
|
100
|
+
[
|
|
101
|
+
'apiVersion: v1',
|
|
102
|
+
'kind: Secret',
|
|
103
|
+
'metadata:',
|
|
104
|
+
' name: ipfs-cluster-secret',
|
|
105
|
+
` namespace: ${options.namespace}`,
|
|
106
|
+
' labels:',
|
|
107
|
+
' app.kubernetes.io/managed-by: underpost',
|
|
108
|
+
'type: Opaque',
|
|
109
|
+
'stringData:',
|
|
110
|
+
` cluster-secret: '${CLUSTER_SECRET.replace(/'/g, "''")}'`,
|
|
111
|
+
` bootstrap-peer-priv-key: '${IDENTITY_JSON.private_key.replace(/'/g, "''")}'`,
|
|
112
|
+
'',
|
|
113
|
+
].join('\n'),
|
|
114
|
+
'utf8',
|
|
115
|
+
);
|
|
116
|
+
fs.chmodSync(stagePath, 0o600);
|
|
117
|
+
// encrypt() stages, validates, moves into place, and shreds the plaintext source.
|
|
118
|
+
Underpost.secret.sops.encrypt(stagePath, options.namespace, { force: true });
|
|
119
|
+
logger.info('Re-encrypted regenerated IPFS credentials into the SOPS store');
|
|
120
|
+
} finally {
|
|
121
|
+
fs.removeSync(stageDir);
|
|
122
|
+
}
|
|
65
123
|
},
|
|
66
124
|
|
|
67
125
|
/**
|
|
@@ -100,12 +158,18 @@ class UnderpostIPFS {
|
|
|
100
158
|
applySecrets({ CLUSTER_SECRET, IDENTITY_JSON }, options) {
|
|
101
159
|
logger.info('Applying IPFS cluster Kubernetes Secret and env ConfigMap');
|
|
102
160
|
|
|
103
|
-
|
|
104
|
-
|
|
161
|
+
// Encrypted store first, origin generate/read logic only when no manifest is stored.
|
|
162
|
+
// `--from-literal` places the cluster secret and the peer private key in the command
|
|
163
|
+
// string, so the seed path is kept out of the command log; the encrypted path never
|
|
164
|
+
// exposes them at all.
|
|
165
|
+
if (!Underpost.secret.sops.applyIfPresent('ipfs-cluster-secret', options.namespace))
|
|
166
|
+
shellExec(
|
|
167
|
+
`kubectl create secret generic ipfs-cluster-secret \
|
|
105
168
|
--from-literal=cluster-secret=${CLUSTER_SECRET} \
|
|
106
169
|
--from-literal=bootstrap-peer-priv-key=${IDENTITY_JSON.private_key} \
|
|
107
170
|
--dry-run=client -o yaml | kubectl apply -f - -n ${options.namespace}`,
|
|
108
|
-
|
|
171
|
+
{ disableLog: true },
|
|
172
|
+
);
|
|
109
173
|
|
|
110
174
|
shellExec(
|
|
111
175
|
`kubectl create configmap env-config \
|
|
@@ -165,7 +229,14 @@ sudo sysctl -w net.core.wmem_max=7500000`,
|
|
|
165
229
|
|
|
166
230
|
const credentials = Underpost.ipfs.resolveCredentials(`${underpostRoot}/engine-private`);
|
|
167
231
|
|
|
168
|
-
|
|
232
|
+
// `env-config` advertises `bootstrap-peer-id` from the local identity file — the peer id is
|
|
233
|
+
// not carried in the Secret. So a regeneration invalidates any stored manifest: it would
|
|
234
|
+
// pair the previous private key with the new id and the cluster would never form. Re-encrypt
|
|
235
|
+
// the fresh pair so the store and the ConfigMap stay one identity.
|
|
236
|
+
if (credentials.generated && Underpost.secret.sops.has('ipfs-cluster-secret', options.namespace))
|
|
237
|
+
Underpost.ipfs.storeCredentials(credentials, options);
|
|
238
|
+
|
|
239
|
+
const ipfsReplicas = resolveReplicaCount(options.replicas, IPFS_DEFAULT_REPLICA_COUNT);
|
|
169
240
|
|
|
170
241
|
Underpost.ipfs.teardown(options, ipfsReplicas);
|
|
171
242
|
Underpost.ipfs.applySecrets(credentials, options);
|
package/src/cli/monitor.js
CHANGED
|
@@ -123,7 +123,7 @@ class UnderpostMonitor {
|
|
|
123
123
|
|
|
124
124
|
let errorPayloads = [];
|
|
125
125
|
if (options.sync === true) {
|
|
126
|
-
const currentTraffic = Underpost.deploy.getCurrentTraffic(deployId, { namespace: options.namespace });
|
|
126
|
+
const currentTraffic = Underpost.deploy.getCurrentTraffic(deployId, { namespace: options.namespace, env });
|
|
127
127
|
if (currentTraffic) Underpost.env.set(`${deployId}-${env}-traffic`, currentTraffic);
|
|
128
128
|
}
|
|
129
129
|
let traffic = Underpost.env.get(`${deployId}-${env}-traffic`) ?? 'blue';
|
package/src/cli/release.js
CHANGED
|
@@ -135,6 +135,10 @@ const buildVersionBumpTargets = () => [
|
|
|
135
135
|
file: 'src/runtime/engine-cyberia/docker-compose.yml',
|
|
136
136
|
patterns: /(_TAG:-v)\d+\.\d+\.\d+/g,
|
|
137
137
|
},
|
|
138
|
+
{
|
|
139
|
+
file: 'engine-private/conf/dd-cyberia/docker-compose/cyberia/docker-compose.yml',
|
|
140
|
+
patterns: /(_TAG:-v)\d+\.\d+\.\d+/g,
|
|
141
|
+
},
|
|
138
142
|
|
|
139
143
|
// ── Cyberia CLI dev image tar/name defaults (bin/cyberia.js). ──
|
|
140
144
|
{
|