underpost 3.2.21 → 3.2.28
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 +1 -1
- package/.github/workflows/gitlab.ci.yml +1 -1
- package/.github/workflows/npmpkg.ci.yml +1 -1
- package/.github/workflows/publish.ci.yml +2 -2
- package/.github/workflows/pwa-microservices-template-page.cd.yml +1 -1
- package/.github/workflows/pwa-microservices-template-test.ci.yml +1 -1
- package/CHANGELOG.md +178 -1
- package/CLI-HELP.md +58 -2
- package/README.md +3 -2
- package/baremetal/commission-workflows.json +1 -0
- package/bin/build.js +13 -1
- package/docker-compose.yml +224 -0
- package/manifests/cronjobs/dd-cron/dd-cron-backup.yaml +1 -1
- package/manifests/cronjobs/dd-cron/dd-cron-dns.yaml +1 -1
- package/manifests/deployment/dd-default-development/deployment.yaml +2 -2
- package/package.json +27 -15
- package/scripts/kubeadm-node-setup.sh +317 -0
- package/scripts/rocky-kickstart.sh +877 -185
- package/scripts/rpmfusion-ffmpeg-setup.sh +26 -50
- package/scripts/test-monitor.sh +242 -78
- package/src/cli/baremetal.js +717 -16
- package/src/cli/cluster.js +3 -1
- package/src/cli/deploy.js +11 -10
- package/src/cli/docker-compose.js +591 -0
- package/src/cli/fs.js +35 -11
- package/src/cli/index.js +83 -0
- package/src/cli/kickstart.js +142 -20
- package/src/cli/monitor.js +114 -29
- package/src/cli/repository.js +75 -11
- package/src/cli/run.js +275 -6
- package/src/cli/ssh.js +190 -0
- package/src/cli/static.js +2 -2
- package/src/client/components/core/PanelForm.js +44 -44
- package/src/{server → client-builder}/client-build-docs.js +15 -5
- package/src/{server → client-builder}/client-build-live.js +3 -3
- package/src/{server → client-builder}/client-build.js +25 -22
- package/src/{server → client-builder}/client-dev-server.js +3 -3
- package/src/{server → client-builder}/client-icons.js +2 -2
- package/src/{server → client-builder}/ssr.js +5 -5
- package/src/client.build.js +1 -1
- package/src/client.dev.js +1 -1
- package/src/index.js +12 -1
- package/src/mailer/EmailRender.js +1 -1
- package/src/{server → projects/underpost}/catalog-underpost.js +1 -2
- package/src/runtime/express/Express.js +2 -2
- package/src/runtime/nginx/Nginx.js +250 -0
- package/src/server/catalog.js +7 -14
- package/src/server/conf.js +61 -12
- package/src/server/start.js +17 -5
- package/src/server.js +1 -1
- package/test/deploy-monitor.test.js +33 -5
- package/typedoc.json +3 -1
- package/src/server/ipfs-client.js +0 -597
- /package/src/client/ssr/{Render.js → RootDocument.js} +0 -0
- /package/src/{server → client-builder}/client-formatted.js +0 -0
package/src/cli/cluster.js
CHANGED
|
@@ -691,7 +691,9 @@ EOF
|
|
|
691
691
|
// shellExec(
|
|
692
692
|
// `sudo sed -i '/SystemdCgroup = true/a selinux_disabled = true' /etc/containerd/config.toml`,
|
|
693
693
|
// );
|
|
694
|
-
|
|
694
|
+
// Restart docker after containerd config changes. Rocky 9 uses systemctl,
|
|
695
|
+
// not the legacy service command.
|
|
696
|
+
shellExec(`sudo systemctl restart docker || sudo service docker restart || true`);
|
|
695
697
|
shellExec(`sudo systemctl enable --now containerd.service`);
|
|
696
698
|
shellExec(`sudo systemctl restart containerd`); // Restart containerd to apply changes
|
|
697
699
|
|
package/src/cli/deploy.js
CHANGED
|
@@ -969,8 +969,12 @@ EOF`);
|
|
|
969
969
|
if (
|
|
970
970
|
Underpost.deploy.isValidTLSContext({ host: Object.keys(confServer)[0], env, options }) &&
|
|
971
971
|
!options.selfSigned
|
|
972
|
-
)
|
|
973
|
-
|
|
972
|
+
) {
|
|
973
|
+
const secretPath = `./${manifestsPath}/secret.yaml`;
|
|
974
|
+
if (fs.existsSync(secretPath) && fs.readFileSync(secretPath, 'utf8').trim()) {
|
|
975
|
+
shellExec(`sudo kubectl apply -f ${secretPath} -n ${namespace}`);
|
|
976
|
+
} else logger.info('Skipping secret.yaml apply (no objects yet; applied by the --cert step)');
|
|
977
|
+
}
|
|
974
978
|
}
|
|
975
979
|
}
|
|
976
980
|
},
|
|
@@ -1072,15 +1076,12 @@ EOF`);
|
|
|
1072
1076
|
if (options.gitClean && volume.volumeMountPath) {
|
|
1073
1077
|
Underpost.repo.clean({ paths: [volume.volumeMountPath] });
|
|
1074
1078
|
}
|
|
1075
|
-
if (
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
shellExec(`docker exec -i kind-worker bash -c "mkdir -p ${rootVolumeHostPath}"`);
|
|
1080
|
-
// shellExec(`docker cp ${volume.volumeMountPath} kind-worker:${rootVolumeHostPath}`);
|
|
1081
|
-
shellExec(`tar -C ${volume.volumeMountPath} -c . | docker cp - kind-worker:${rootVolumeHostPath}`);
|
|
1079
|
+
if (clusterContext === 'kind') {
|
|
1080
|
+
const kindNode = options.nodeName || 'kind-worker';
|
|
1081
|
+
shellExec(`docker exec -i ${kindNode} bash -c "mkdir -p ${rootVolumeHostPath}"`);
|
|
1082
|
+
shellExec(`tar -C ${volume.volumeMountPath} -c . | docker cp - ${kindNode}:${rootVolumeHostPath}`);
|
|
1082
1083
|
shellExec(
|
|
1083
|
-
`docker exec -i
|
|
1084
|
+
`docker exec -i ${kindNode} bash -c "chown -R 1000:1000 ${rootVolumeHostPath}; chmod -R 755 ${rootVolumeHostPath}"`,
|
|
1084
1085
|
);
|
|
1085
1086
|
} else {
|
|
1086
1087
|
if (!fs.existsSync(rootVolumeHostPath)) fs.mkdirSync(rootVolumeHostPath, { recursive: true });
|
|
@@ -0,0 +1,591 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @description Docker Compose pipeline CLI for the local development stack that
|
|
3
|
+
* mirrors the Kubernetes manifests under `manifests/`. Manages dynamic
|
|
4
|
+
* generation of supporting config (nginx router, env-file) and the compose
|
|
5
|
+
* lifecycle (up/down/logs/...). General-purpose: any compose subcommand can be
|
|
6
|
+
* forwarded via `--exec`.
|
|
7
|
+
* @module src/cli/docker-compose.js
|
|
8
|
+
* @namespace UnderpostDockerCompose
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import fs from 'fs-extra';
|
|
12
|
+
import nodePath from 'path';
|
|
13
|
+
import { getRootDirectory, shellExec } from '../server/process.js';
|
|
14
|
+
import { loggerFactory } from '../server/logger.js';
|
|
15
|
+
import Nginx from '../runtime/nginx/Nginx.js';
|
|
16
|
+
|
|
17
|
+
const logger = loggerFactory(import.meta);
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Reverse-proxy route table derived from
|
|
21
|
+
* manifests/deployment/dd-default-development/proxy.yaml (Contour HTTPProxy).
|
|
22
|
+
* Longer prefixes precede '/' so nginx matches them first.
|
|
23
|
+
* @constant PROXY_HOSTS
|
|
24
|
+
* @memberof UnderpostDockerCompose
|
|
25
|
+
*/
|
|
26
|
+
const PROXY_HOSTS = [
|
|
27
|
+
{
|
|
28
|
+
host: 'default.net',
|
|
29
|
+
routes: [
|
|
30
|
+
{ location: '/peer', service: 'app', port: 4002 },
|
|
31
|
+
{ location: '/', service: 'app', port: 4001 },
|
|
32
|
+
],
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
host: 'www.default.net',
|
|
36
|
+
routes: [{ location: '/', service: 'app', port: 4003 }],
|
|
37
|
+
},
|
|
38
|
+
];
|
|
39
|
+
|
|
40
|
+
/** Fallback upstream for unmatched Host headers and the proxy healthcheck. */
|
|
41
|
+
const DEFAULT_UPSTREAM = { service: 'app', port: 4001 };
|
|
42
|
+
|
|
43
|
+
/** Prometheus scrape target: the app's /metrics endpoint on its base port. */
|
|
44
|
+
const METRICS_TARGET = { service: 'app', port: 4001 };
|
|
45
|
+
|
|
46
|
+
/** Default deployment identifier (the self-bootstrapping engine deploy). */
|
|
47
|
+
const DEFAULT_DEPLOY_ID = 'dd-default';
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Generated artifact paths (relative to repo root) that `--generate` produces
|
|
51
|
+
* and `--reset` prunes. The working env-file (docker/compose.env) is excluded
|
|
52
|
+
* so a reset never destroys configured credentials.
|
|
53
|
+
*/
|
|
54
|
+
const GENERATED_ARTIFACTS = [
|
|
55
|
+
'docker/mongodb/entrypoint.sh',
|
|
56
|
+
'docker/nginx/default.conf',
|
|
57
|
+
'docker/prometheus/prometheus.yml',
|
|
58
|
+
'docker/grafana/provisioning',
|
|
59
|
+
'docker/compose.app.yml',
|
|
60
|
+
'docker/compose.env.example',
|
|
61
|
+
];
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* @class UnderpostDockerCompose
|
|
65
|
+
* @description Docker Compose development pipeline. A single {@link UnderpostDockerCompose.API.callback}
|
|
66
|
+
* dispatches actions based on flag options, mirroring the cluster CLI pattern.
|
|
67
|
+
* @memberof UnderpostDockerCompose
|
|
68
|
+
*/
|
|
69
|
+
class UnderpostDockerCompose {
|
|
70
|
+
/**
|
|
71
|
+
* Resolves a repo-root-relative path to an absolute path.
|
|
72
|
+
* @param {string} relPath - Path relative to the engine root.
|
|
73
|
+
* @returns {string} Absolute path.
|
|
74
|
+
* @memberof UnderpostDockerCompose
|
|
75
|
+
*/
|
|
76
|
+
static resolve(relPath) {
|
|
77
|
+
return nodePath.isAbsolute(relPath) ? relPath : nodePath.join(getRootDirectory(), relPath);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Builds the base `docker compose` invocation with explicit file and env-file,
|
|
82
|
+
* so behavior is independent of the caller's working directory.
|
|
83
|
+
* @param {object} options - CLI options.
|
|
84
|
+
* @returns {string} The base command string (without a subcommand).
|
|
85
|
+
* @memberof UnderpostDockerCompose
|
|
86
|
+
*/
|
|
87
|
+
static baseCmd(options = {}) {
|
|
88
|
+
const composeFile = UnderpostDockerCompose.resolve(options.composeFile || 'docker-compose.yml');
|
|
89
|
+
const envFile = UnderpostDockerCompose.resolve(options.envFile || 'docker/compose.env');
|
|
90
|
+
const overrideFile = UnderpostDockerCompose.resolve(options.appOverride || 'docker/compose.app.yml');
|
|
91
|
+
const overrideFlag = fs.existsSync(overrideFile) ? ` -f ${overrideFile}` : '';
|
|
92
|
+
return `docker compose --env-file ${envFile} -f ${composeFile}${overrideFlag}`;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Resolves the app container start command for a deployment, mirroring
|
|
97
|
+
* src/cli/deploy.js. The `dd-default` deploy self-bootstraps a fresh engine
|
|
98
|
+
* (matching manifests/deployment/dd-default-development); any other deploy-id
|
|
99
|
+
* follows the standard deploy command (matching
|
|
100
|
+
* manifests/deployment/dd-test-development and
|
|
101
|
+
* UnderpostDeploy.deploymentYamlPartsFactory's default cmd).
|
|
102
|
+
* @param {string} deployId - Deployment identifier (conf id, e.g. `dd-test`).
|
|
103
|
+
* @param {string} env - Deployment environment (e.g. `development`).
|
|
104
|
+
* @returns {string[]} Ordered shell steps joined with `&&` at render time.
|
|
105
|
+
* @memberof UnderpostDockerCompose
|
|
106
|
+
*/
|
|
107
|
+
static appCommand(deployId = DEFAULT_DEPLOY_ID, env = 'development') {
|
|
108
|
+
if (deployId === DEFAULT_DEPLOY_ID)
|
|
109
|
+
return [
|
|
110
|
+
// `$$` escapes to a literal `$` so /bin/sh (not Compose) performs the
|
|
111
|
+
// command/variable substitution at container runtime.
|
|
112
|
+
'cd $$(underpost root)/underpost',
|
|
113
|
+
'node bin new --default-conf --conf-workflow-id template',
|
|
114
|
+
// Point the template .env.example at the Docker service-discovery hosts
|
|
115
|
+
// before `new engine` copies it. The generated dd-engine/.env.development
|
|
116
|
+
// is seeded from this file and is applied over the process env by
|
|
117
|
+
// loadConf (src/server/conf.js), so without this the engine would fall
|
|
118
|
+
// back to the .env.example localhost defaults for DB_HOST/VALKEY_HOST.
|
|
119
|
+
'sed -i "s#^DB_HOST=.*#DB_HOST=$${DB_HOST}#" .env.example',
|
|
120
|
+
'sed -i "s#^VALKEY_HOST=.*#VALKEY_HOST=$${VALKEY_HOST}#" .env.example',
|
|
121
|
+
'mkdir -p /home/dd',
|
|
122
|
+
'cd /home/dd',
|
|
123
|
+
'underpost new engine',
|
|
124
|
+
];
|
|
125
|
+
return ['underpost secret underpost --create-from-env', `underpost start --build --run ${deployId} ${env}`];
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Renders a Compose override file that sets only the `app` service command
|
|
130
|
+
* for the selected deployment. Keeping the command in an override file makes
|
|
131
|
+
* the generator the single source of truth and leaves docker-compose.yml
|
|
132
|
+
* deployment-agnostic.
|
|
133
|
+
* @param {string} deployId - Deployment identifier.
|
|
134
|
+
* @param {string} env - Deployment environment.
|
|
135
|
+
* @returns {string} The override YAML document.
|
|
136
|
+
* @memberof UnderpostDockerCompose
|
|
137
|
+
*/
|
|
138
|
+
static appOverrideContent(deployId = DEFAULT_DEPLOY_ID, env = 'development') {
|
|
139
|
+
const steps = UnderpostDockerCompose.appCommand(deployId, env).join(' &&\n ');
|
|
140
|
+
return `# Generated by 'underpost docker-compose --generate --deploy-id ${deployId}' — do not hand-edit.
|
|
141
|
+
# Overrides the app service start command for deploy '${deployId}' (${env}).
|
|
142
|
+
services:
|
|
143
|
+
app:
|
|
144
|
+
command:
|
|
145
|
+
- /bin/sh
|
|
146
|
+
- -c
|
|
147
|
+
- >
|
|
148
|
+
${steps}
|
|
149
|
+
`;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Renders the dynamic env-file example content for the compose stack.
|
|
154
|
+
* @returns {string} The env-file template.
|
|
155
|
+
* @memberof UnderpostDockerCompose
|
|
156
|
+
*/
|
|
157
|
+
static envExampleContent() {
|
|
158
|
+
return `# Generated by 'underpost docker-compose --generate' — copy to docker/compose.env.
|
|
159
|
+
# Loaded explicitly via --env-file so container service-discovery values are not
|
|
160
|
+
# overridden by the application's host-local (127.0.0.1) root .env.
|
|
161
|
+
|
|
162
|
+
# --- Container images ----------------------------------------------------
|
|
163
|
+
MONGO_IMAGE=mongo:latest
|
|
164
|
+
VALKEY_IMAGE=valkey/valkey:latest
|
|
165
|
+
APP_IMAGE=underpost/underpost-engine
|
|
166
|
+
APP_TAG=v3.2.22
|
|
167
|
+
PROXY_IMAGE=nginx:stable-alpine
|
|
168
|
+
PROMETHEUS_IMAGE=prom/prometheus:latest
|
|
169
|
+
GRAFANA_IMAGE=grafana/grafana:latest
|
|
170
|
+
|
|
171
|
+
# --- MongoDB (translation of Secret mongodb-secret) ----------------------
|
|
172
|
+
MONGO_INITDB_ROOT_USERNAME=admin
|
|
173
|
+
MONGO_INITDB_ROOT_PASSWORD=changeme
|
|
174
|
+
|
|
175
|
+
# --- Application DB connection (Docker service discovery, not localhost) --
|
|
176
|
+
NODE_ENV=development
|
|
177
|
+
DB_PROVIDER=mongoose
|
|
178
|
+
DB_HOST=mongodb://mongodb:27017
|
|
179
|
+
DB_NAME=default
|
|
180
|
+
DB_REPLICA_SET=rs0
|
|
181
|
+
DB_AUTH_SOURCE=admin
|
|
182
|
+
|
|
183
|
+
# --- Valkey (Docker service discovery) -----------------------------------
|
|
184
|
+
VALKEY_HOST=valkey-service
|
|
185
|
+
VALKEY_PORT=6379
|
|
186
|
+
|
|
187
|
+
# --- Monitoring ----------------------------------------------------------
|
|
188
|
+
GRAFANA_ADMIN_USER=admin
|
|
189
|
+
GRAFANA_ADMIN_PASSWORD=admin
|
|
190
|
+
|
|
191
|
+
# --- Host-published ports ------------------------------------------------
|
|
192
|
+
PROXY_HTTP_PORT=80
|
|
193
|
+
MONGO_HOST_PORT=27017
|
|
194
|
+
VALKEY_NODEPORT=32079
|
|
195
|
+
PROMETHEUS_PORT=9090
|
|
196
|
+
GRAFANA_PORT=3000
|
|
197
|
+
APP_PORT_4001=4001
|
|
198
|
+
APP_PORT_4002=4002
|
|
199
|
+
APP_PORT_4003=4003
|
|
200
|
+
APP_PORT_4004=4004
|
|
201
|
+
`;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Renders the MongoDB container entrypoint. Mirrors src/db/mongo/MongoBootstrap.js:
|
|
206
|
+
* generates the cluster-auth keyfile, launches mongod (replSet + keyFile + auth),
|
|
207
|
+
* and bootstraps the replica set + root user over the loopback localhost
|
|
208
|
+
* exception (single-node member is the `mongodb` service name so app clients
|
|
209
|
+
* connecting with replicaSet=rs0 resolve a reachable host). Idempotent across
|
|
210
|
+
* restarts: re-runs are no-ops once auth is enforced.
|
|
211
|
+
* @returns {string} entrypoint.sh content.
|
|
212
|
+
* @memberof UnderpostDockerCompose
|
|
213
|
+
*/
|
|
214
|
+
static mongoEntrypointContent() {
|
|
215
|
+
return `#!/usr/bin/env bash
|
|
216
|
+
# Generated by 'underpost docker-compose --generate' — do not hand-edit.
|
|
217
|
+
set -e
|
|
218
|
+
|
|
219
|
+
KEYFILE=/opt/keyfile/mongodb-keyfile
|
|
220
|
+
RS="\${DB_REPLICA_SET:-rs0}"
|
|
221
|
+
MEMBER_HOST="mongodb:27017"
|
|
222
|
+
|
|
223
|
+
if [ ! -s "$KEYFILE" ]; then
|
|
224
|
+
openssl rand -base64 756 > "$KEYFILE"
|
|
225
|
+
fi
|
|
226
|
+
chmod 400 "$KEYFILE"
|
|
227
|
+
chown 999:999 "$KEYFILE"
|
|
228
|
+
mkdir -p /data/db
|
|
229
|
+
chown -R 999:999 /data/db
|
|
230
|
+
|
|
231
|
+
# One-time replica-set + root-user bootstrap via the localhost exception,
|
|
232
|
+
# performed in the background once mongod accepts loopback connections.
|
|
233
|
+
(
|
|
234
|
+
for i in $(seq 1 60); do
|
|
235
|
+
if mongosh --quiet --host 127.0.0.1 --eval 'db.adminCommand({ ping: 1 })' >/dev/null 2>&1; then
|
|
236
|
+
break
|
|
237
|
+
fi
|
|
238
|
+
sleep 1
|
|
239
|
+
done
|
|
240
|
+
|
|
241
|
+
cat > /tmp/mongo-bootstrap.js <<JS
|
|
242
|
+
var RS = "$RS";
|
|
243
|
+
var HOST = "$MEMBER_HOST";
|
|
244
|
+
var U = "$MONGO_INITDB_ROOT_USERNAME";
|
|
245
|
+
var P = "$MONGO_INITDB_ROOT_PASSWORD";
|
|
246
|
+
function waitPrimary() {
|
|
247
|
+
for (var i = 0; i < 60; i++) {
|
|
248
|
+
var h = db.hello();
|
|
249
|
+
if (h.isWritablePrimary) return true;
|
|
250
|
+
sleep(1000);
|
|
251
|
+
}
|
|
252
|
+
return false;
|
|
253
|
+
}
|
|
254
|
+
var initialized = false;
|
|
255
|
+
try {
|
|
256
|
+
var s = rs.status();
|
|
257
|
+
if (s && s.ok === 1) initialized = true;
|
|
258
|
+
} catch (e) {
|
|
259
|
+
var m = String(e);
|
|
260
|
+
if (m.indexOf("requires authentication") >= 0 || m.indexOf("not authorized") >= 0 || m.indexOf("Unauthorized") >= 0) {
|
|
261
|
+
initialized = true;
|
|
262
|
+
} else if (m.indexOf("NotYetInitialized") < 0 && m.indexOf("no replset config") < 0) {
|
|
263
|
+
throw e;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
if (!initialized) {
|
|
267
|
+
rs.initiate({ _id: RS, members: [{ _id: 0, host: HOST }] });
|
|
268
|
+
waitPrimary();
|
|
269
|
+
var admin = db.getSiblingDB("admin");
|
|
270
|
+
try {
|
|
271
|
+
admin.createUser({ user: U, pwd: P, roles: [{ role: "root", db: "admin" }] });
|
|
272
|
+
print("BOOTSTRAP_USER_CREATED");
|
|
273
|
+
} catch (e) {
|
|
274
|
+
var s2 = String(e);
|
|
275
|
+
if (s2.indexOf("already exists") < 0 && s2.indexOf("not authorized") < 0 && s2.indexOf("requires authentication") < 0) {
|
|
276
|
+
throw e;
|
|
277
|
+
}
|
|
278
|
+
print("BOOTSTRAP_USER_SKIP");
|
|
279
|
+
}
|
|
280
|
+
print("BOOTSTRAP_DONE");
|
|
281
|
+
} else {
|
|
282
|
+
print("BOOTSTRAP_ALREADY_INITIALIZED");
|
|
283
|
+
}
|
|
284
|
+
JS
|
|
285
|
+
|
|
286
|
+
mongosh --quiet --host 127.0.0.1 /tmp/mongo-bootstrap.js > /tmp/mongo-bootstrap.log 2>&1 || true
|
|
287
|
+
) &
|
|
288
|
+
|
|
289
|
+
exec gosu mongodb mongod \\
|
|
290
|
+
--replSet "$RS" --auth --clusterAuthMode keyFile --keyFile "$KEYFILE" --bind_ip_all
|
|
291
|
+
`;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Renders the Prometheus scrape config (mirrors manifests/prometheus).
|
|
296
|
+
* @returns {string} prometheus.yml content.
|
|
297
|
+
* @memberof UnderpostDockerCompose
|
|
298
|
+
*/
|
|
299
|
+
static prometheusContent() {
|
|
300
|
+
return `# Generated by 'underpost docker-compose --generate' — do not hand-edit.
|
|
301
|
+
global:
|
|
302
|
+
scrape_interval: 30s
|
|
303
|
+
evaluation_interval: 30s
|
|
304
|
+
|
|
305
|
+
scrape_configs:
|
|
306
|
+
- job_name: 'underpost-app'
|
|
307
|
+
metrics_path: /metrics
|
|
308
|
+
scheme: http
|
|
309
|
+
static_configs:
|
|
310
|
+
- targets: ['${METRICS_TARGET.service}:${METRICS_TARGET.port}']
|
|
311
|
+
- job_name: 'prometheus'
|
|
312
|
+
static_configs:
|
|
313
|
+
- targets: ['localhost:9090']
|
|
314
|
+
`;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* Renders the Grafana datasource provisioning file wiring the Prometheus
|
|
319
|
+
* service as the default datasource.
|
|
320
|
+
* @returns {string} datasource.yml content.
|
|
321
|
+
* @memberof UnderpostDockerCompose
|
|
322
|
+
*/
|
|
323
|
+
static grafanaDatasourceContent() {
|
|
324
|
+
return `# Generated by 'underpost docker-compose --generate' — do not hand-edit.
|
|
325
|
+
apiVersion: 1
|
|
326
|
+
datasources:
|
|
327
|
+
- name: Prometheus
|
|
328
|
+
type: prometheus
|
|
329
|
+
access: proxy
|
|
330
|
+
url: http://prometheus:9090
|
|
331
|
+
isDefault: true
|
|
332
|
+
editable: true
|
|
333
|
+
`;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* Renders all dynamic supporting files: the nginx reverse-proxy config (from
|
|
338
|
+
* PROXY_HOSTS), the monitoring configs (Prometheus + Grafana datasource), and
|
|
339
|
+
* the env-file example. Creates a working env-file from the example only when
|
|
340
|
+
* one does not already exist (never overwrites credentials).
|
|
341
|
+
* @param {object} options - CLI options.
|
|
342
|
+
* @returns {void}
|
|
343
|
+
* @memberof UnderpostDockerCompose
|
|
344
|
+
*/
|
|
345
|
+
static generate(options = {}) {
|
|
346
|
+
Nginx.removeRouter();
|
|
347
|
+
for (const { host, routes } of PROXY_HOSTS) Nginx.createApp({ host, routes });
|
|
348
|
+
Nginx.createDefaultServer(DEFAULT_UPSTREAM);
|
|
349
|
+
Nginx.writeConf(UnderpostDockerCompose.resolve(options.nginxConf || 'docker/nginx/default.conf'));
|
|
350
|
+
|
|
351
|
+
const mongoEntrypointPath = UnderpostDockerCompose.resolve('docker/mongodb/entrypoint.sh');
|
|
352
|
+
fs.mkdirpSync(nodePath.dirname(mongoEntrypointPath));
|
|
353
|
+
fs.writeFileSync(mongoEntrypointPath, UnderpostDockerCompose.mongoEntrypointContent(), { mode: 0o755 });
|
|
354
|
+
logger.info('mongodb entrypoint written', { path: mongoEntrypointPath });
|
|
355
|
+
|
|
356
|
+
const promPath = UnderpostDockerCompose.resolve('docker/prometheus/prometheus.yml');
|
|
357
|
+
fs.mkdirpSync(nodePath.dirname(promPath));
|
|
358
|
+
fs.writeFileSync(promPath, UnderpostDockerCompose.prometheusContent(), 'utf8');
|
|
359
|
+
const grafanaDsPath = UnderpostDockerCompose.resolve('docker/grafana/provisioning/datasources/datasource.yml');
|
|
360
|
+
fs.mkdirpSync(nodePath.dirname(grafanaDsPath));
|
|
361
|
+
fs.writeFileSync(grafanaDsPath, UnderpostDockerCompose.grafanaDatasourceContent(), 'utf8');
|
|
362
|
+
logger.info('monitoring config written', { prometheus: promPath, grafanaDatasource: grafanaDsPath });
|
|
363
|
+
|
|
364
|
+
const examplePath = UnderpostDockerCompose.resolve('docker/compose.env.example');
|
|
365
|
+
fs.mkdirpSync(nodePath.dirname(examplePath));
|
|
366
|
+
fs.writeFileSync(examplePath, UnderpostDockerCompose.envExampleContent(), 'utf8');
|
|
367
|
+
logger.info('env example written', { path: examplePath });
|
|
368
|
+
|
|
369
|
+
const envPath = UnderpostDockerCompose.resolve(options.envFile || 'docker/compose.env');
|
|
370
|
+
if (!fs.existsSync(envPath)) {
|
|
371
|
+
fs.copySync(examplePath, envPath);
|
|
372
|
+
logger.warn('created working env-file from example — set real credentials', { path: envPath });
|
|
373
|
+
} else logger.info('working env-file already present (left untouched)', { path: envPath });
|
|
374
|
+
|
|
375
|
+
const deployId = options.deployId || DEFAULT_DEPLOY_ID;
|
|
376
|
+
const env = options.env || 'development';
|
|
377
|
+
const overridePath = UnderpostDockerCompose.resolve(options.appOverride || 'docker/compose.app.yml');
|
|
378
|
+
fs.writeFileSync(overridePath, UnderpostDockerCompose.appOverrideContent(deployId, env), 'utf8');
|
|
379
|
+
logger.info('app command override written', { path: overridePath, deployId, env });
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
/**
|
|
383
|
+
* Installs Docker Engine and the Compose v2 plugin on RHEL-compatible hosts
|
|
384
|
+
* (Rocky Linux) from the official Docker CE repository. Idempotent and
|
|
385
|
+
* host-safe: skips installation when `docker compose` already works (unless
|
|
386
|
+
* `--force`), validates the platform, enables the service, and adds the
|
|
387
|
+
* invoking user to the `docker` group.
|
|
388
|
+
* @param {object} options - CLI options.
|
|
389
|
+
* @param {boolean} [options.force] - Reinstall even if Compose is already present.
|
|
390
|
+
* @returns {void}
|
|
391
|
+
* @memberof UnderpostDockerCompose
|
|
392
|
+
*/
|
|
393
|
+
static install(options = {}) {
|
|
394
|
+
if (process.platform !== 'linux') {
|
|
395
|
+
logger.warn('docker-compose --install only supports Linux (RHEL/Rocky); skipping', {
|
|
396
|
+
platform: process.platform,
|
|
397
|
+
});
|
|
398
|
+
return;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
if (!fs.existsSync('/etc/redhat-release')) {
|
|
402
|
+
logger.warn('Host does not look RHEL-compatible (/etc/redhat-release missing); proceeding with dnf anyway');
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
if (!options.force) {
|
|
406
|
+
const probe = shellExec('docker compose version', { silent: true, silentOnError: true });
|
|
407
|
+
if (probe && probe.code === 0) {
|
|
408
|
+
logger.info('Docker Compose already installed; skipping (use --force to reinstall)', {
|
|
409
|
+
version: (probe.stdout || '').trim(),
|
|
410
|
+
});
|
|
411
|
+
return;
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
shellExec(`sudo dnf -y install dnf-plugins-core`);
|
|
416
|
+
shellExec(`sudo dnf config-manager --add-repo https://download.docker.com/linux/rhel/docker-ce.repo`);
|
|
417
|
+
shellExec(`sudo dnf -y install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin`);
|
|
418
|
+
|
|
419
|
+
shellExec(`sudo systemctl enable --now docker`);
|
|
420
|
+
|
|
421
|
+
const user = options.user || process.env.SUDO_USER || process.env.USER || '';
|
|
422
|
+
if (user && user !== 'root') {
|
|
423
|
+
shellExec(`sudo groupadd docker 2>/dev/null || true`);
|
|
424
|
+
shellExec(`sudo usermod -aG docker ${user}`);
|
|
425
|
+
logger.info(`Added '${user}' to the docker group — log out/in (or run 'newgrp docker') to use docker rootless`);
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
const verify = shellExec('docker compose version', { silent: true, silentOnError: true, stdout: true });
|
|
429
|
+
logger.info('Docker Compose installation complete', { version: `${verify || ''}`.trim() });
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
/**
|
|
433
|
+
* Comprehensive reset of the Docker Compose stack — the compose equivalent of
|
|
434
|
+
* `cluster --reset` in src/cli/cluster.js. Tears down all stack containers,
|
|
435
|
+
* the project network, and the named volumes (destroying persisted MongoDB /
|
|
436
|
+
* Valkey / Prometheus / Grafana data), removes orphans, and prunes generated
|
|
437
|
+
* artifacts so the next `--up` is a clean slate. The working env-file
|
|
438
|
+
* (credentials) is preserved unless `--force` is passed.
|
|
439
|
+
* @param {object} options - CLI options.
|
|
440
|
+
* @param {boolean} [options.force] - Also remove the working env-file (docker/compose.env).
|
|
441
|
+
* @returns {void}
|
|
442
|
+
* @memberof UnderpostDockerCompose
|
|
443
|
+
*/
|
|
444
|
+
static reset(options = {}) {
|
|
445
|
+
logger.info('=== DOCKER COMPOSE RESET (destroys containers, network, and volume data) ===');
|
|
446
|
+
|
|
447
|
+
// Phase 1: tear down containers, orphans, named volumes, and compose-built images.
|
|
448
|
+
shellExec(`${UnderpostDockerCompose.baseCmd(options)} down --remove-orphans --volumes --rmi local`, {
|
|
449
|
+
silentOnError: true,
|
|
450
|
+
});
|
|
451
|
+
|
|
452
|
+
// Phase 2: prune generated artifacts (regenerated on the next --generate/--up).
|
|
453
|
+
const artifacts = options.force
|
|
454
|
+
? [...GENERATED_ARTIFACTS, options.envFile || 'docker/compose.env']
|
|
455
|
+
: GENERATED_ARTIFACTS;
|
|
456
|
+
for (const rel of artifacts) {
|
|
457
|
+
const target = UnderpostDockerCompose.resolve(rel);
|
|
458
|
+
if (fs.existsSync(target)) {
|
|
459
|
+
fs.removeSync(target);
|
|
460
|
+
logger.info('removed generated artifact', { path: target });
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
logger.info('Docker Compose reset complete. Run `--up` to recreate the stack.');
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
static API = {
|
|
468
|
+
/**
|
|
469
|
+
* @method callback
|
|
470
|
+
* @description Single CLI entrypoint for the docker-compose pipeline. The
|
|
471
|
+
* action is selected by flag options; multiple lifecycle flags may be
|
|
472
|
+
* combined (e.g. `--generate --up`). When no action flag is supplied it
|
|
473
|
+
* defaults to `--up`.
|
|
474
|
+
* @param {string} [target] - Optional service name (logs/shell/restart) or
|
|
475
|
+
* passthrough subcommand context.
|
|
476
|
+
* @param {object} [options] - CLI options.
|
|
477
|
+
* @param {boolean} [options.install] - Install Docker + Compose on RHEL/Rocky hosts.
|
|
478
|
+
* @param {boolean} [options.reset] - Comprehensive teardown of the whole stack (containers, network, volumes) + prune generated artifacts.
|
|
479
|
+
* @param {boolean} [options.force] - Force reinstall (--install), remove volumes (--down), or also drop the env-file (--reset).
|
|
480
|
+
* @param {boolean} [options.generate] - Render nginx config + env-file.
|
|
481
|
+
* @param {boolean} [options.up] - Start the full stack detached (implies generate).
|
|
482
|
+
* @param {boolean} [options.down] - Stop and remove containers.
|
|
483
|
+
* @param {boolean} [options.volumes] - With --down, also remove named volumes (destroys data).
|
|
484
|
+
* @param {boolean} [options.restart] - Restart services (optionally a single `target`).
|
|
485
|
+
* @param {boolean} [options.build] - With --up rebuild images; alone, `build --no-cache`.
|
|
486
|
+
* @param {boolean} [options.pull] - Pull upstream images.
|
|
487
|
+
* @param {boolean} [options.logs] - Follow logs (optionally a single `target`).
|
|
488
|
+
* @param {boolean} [options.status] - Show a formatted status table.
|
|
489
|
+
* @param {boolean} [options.shell] - Open an interactive shell in `target` (default: app).
|
|
490
|
+
* @param {string} [options.exec] - General-purpose passthrough docker compose subcommand.
|
|
491
|
+
* @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.
|
|
492
|
+
* @param {string} [options.env] - Deployment environment for non-default deploy ids (default: development).
|
|
493
|
+
* @param {string} [options.composeFile] - Override compose file path.
|
|
494
|
+
* @param {string} [options.envFile] - Override env-file path.
|
|
495
|
+
* @param {string} [options.nginxConf] - Override generated nginx config path.
|
|
496
|
+
* @param {string} [options.appOverride] - Override generated app-command override path.
|
|
497
|
+
* @returns {Promise<void>}
|
|
498
|
+
* @memberof UnderpostDockerCompose
|
|
499
|
+
*/
|
|
500
|
+
async callback(target = '', options = {}) {
|
|
501
|
+
try {
|
|
502
|
+
if (options.install) {
|
|
503
|
+
UnderpostDockerCompose.install(options);
|
|
504
|
+
const onlyInstall = !options.up && !options.down && !options.generate && !options.exec;
|
|
505
|
+
if (onlyInstall) return;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
if (options.reset) {
|
|
509
|
+
UnderpostDockerCompose.reset(options);
|
|
510
|
+
const onlyReset = !options.up && !options.generate;
|
|
511
|
+
if (onlyReset) return;
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
// "Bring up" is the explicit --up or the no-action default.
|
|
515
|
+
const isUp =
|
|
516
|
+
options.up ||
|
|
517
|
+
!(
|
|
518
|
+
options.exec ||
|
|
519
|
+
options.down ||
|
|
520
|
+
options.restart ||
|
|
521
|
+
options.build ||
|
|
522
|
+
options.pull ||
|
|
523
|
+
options.logs ||
|
|
524
|
+
options.status ||
|
|
525
|
+
options.shell ||
|
|
526
|
+
options.generate
|
|
527
|
+
);
|
|
528
|
+
|
|
529
|
+
// Generate dynamic config BEFORE composing the invocation: baseCmd()
|
|
530
|
+
// conditionally includes `-f compose.app.yml`, so the override must
|
|
531
|
+
// exist first (notably after --reset, which prunes it).
|
|
532
|
+
if (isUp || options.generate) UnderpostDockerCompose.generate(options);
|
|
533
|
+
|
|
534
|
+
const base = UnderpostDockerCompose.baseCmd(options);
|
|
535
|
+
|
|
536
|
+
if (options.exec) {
|
|
537
|
+
shellExec(`${base} ${options.exec}`);
|
|
538
|
+
return;
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
if (isUp) {
|
|
542
|
+
shellExec(`${base} up -d${options.build ? ' --build' : ''}`);
|
|
543
|
+
return;
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
if (options.down) {
|
|
547
|
+
shellExec(`${base} down --remove-orphans${options.volumes ? ' --volumes' : ''}`);
|
|
548
|
+
return;
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
if (options.restart) {
|
|
552
|
+
shellExec(`${base} restart${target ? ` ${target}` : ''}`);
|
|
553
|
+
return;
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
if (options.build) {
|
|
557
|
+
shellExec(`${base} build --no-cache${target ? ` ${target}` : ''}`);
|
|
558
|
+
return;
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
if (options.pull) {
|
|
562
|
+
shellExec(`${base} pull`);
|
|
563
|
+
return;
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
if (options.logs) {
|
|
567
|
+
shellExec(`${base} logs -f --tail=200${target ? ` ${target}` : ''}`);
|
|
568
|
+
return;
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
if (options.status) {
|
|
572
|
+
shellExec(`${base} ps --format 'table {{.Name}}\\t{{.Service}}\\t{{.Status}}\\t{{.Ports}}'`);
|
|
573
|
+
return;
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
if (options.shell) {
|
|
577
|
+
const service = target || 'app';
|
|
578
|
+
shellExec(`${base} exec ${service} ${service === 'app' ? '/bin/bash' : '/bin/sh'}`);
|
|
579
|
+
return;
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
// Remaining case: --generate alone (config already written above).
|
|
583
|
+
} catch (error) {
|
|
584
|
+
logger.error(error);
|
|
585
|
+
process.exit(1);
|
|
586
|
+
}
|
|
587
|
+
},
|
|
588
|
+
};
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
export default UnderpostDockerCompose;
|