underpost 3.2.28 → 3.2.70
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 +87 -0
- package/.github/workflows/npmpkg.ci.yml +9 -6
- package/.github/workflows/pwa-microservices-template-page.cd.yml +0 -7
- package/CHANGELOG.md +161 -1
- package/CLI-HELP.md +17 -6
- package/README.md +2 -2
- package/bin/build.js +30 -0
- package/bin/deploy.js +2 -2
- package/bump.config.js +1 -0
- package/docker-compose.yml +26 -26
- 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 +10 -10
- package/scripts/disk-clean.sh +85 -60
- package/scripts/test-monitor.sh +3 -5
- package/src/cli/cluster.js +77 -4
- package/src/cli/deploy.js +121 -9
- package/src/cli/docker-compose.js +58 -1
- package/src/cli/env.js +11 -2
- package/src/cli/fs.js +45 -24
- package/src/cli/image.js +129 -51
- package/src/cli/index.js +35 -6
- package/src/cli/release.js +46 -4
- package/src/cli/repository.js +166 -22
- package/src/cli/run.js +273 -118
- package/src/cli/secrets.js +82 -48
- package/src/cli/ssh.js +44 -0
- package/src/client.build.js +0 -2
- package/src/db/mongo/MongoBootstrap.js +12 -12
- package/src/index.js +1 -1
- package/src/server/catalog.js +2 -0
- package/src/server/conf.js +0 -3
- package/src/server/runtime-status.js +18 -1
- package/src/server/start.js +12 -2
- package/src/server.js +5 -1
- package/test/deploy-monitor.test.js +26 -10
package/src/cli/deploy.js
CHANGED
|
@@ -383,6 +383,10 @@ spec:
|
|
|
383
383
|
* @param {string} [options.imagePullPolicy] - Container imagePullPolicy override (`Always`, `IfNotPresent`, `Never`); forwarded to deploymentYamlPartsFactory. Defaults to `Never` for `localhost/` images and `IfNotPresent` otherwise.
|
|
384
384
|
* @param {boolean} [options.disableRuntimeProbes] - Omit internal-status HTTP probes from generated manifests. When true no readiness/liveness/startup probes are emitted.
|
|
385
385
|
* @param {boolean} [options.tcpProbes] - Emit legacy TCP socket probes instead of HTTP internal-status probes (migration path).
|
|
386
|
+
* @param {string} [options.node] - Explicit target node for hostPath PV nodeAffinity pinning; resolved through {@link UnderpostDeploy.resolveDeployNode} together with the cluster flags.
|
|
387
|
+
* @param {boolean} [options.kind] - Kind cluster context; affects the cluster-type node default when no explicit node is set.
|
|
388
|
+
* @param {boolean} [options.kubeadm] - Kubeadm cluster context; affects the cluster-type node default when no explicit node is set.
|
|
389
|
+
* @param {boolean} [options.k3s] - K3s cluster context; affects the cluster-type node default when no explicit node is set.
|
|
386
390
|
* @returns {Promise<void>} - Promise that resolves when the manifest is built.
|
|
387
391
|
* @memberof UnderpostDeploy
|
|
388
392
|
*/
|
|
@@ -455,6 +459,15 @@ ${Underpost.deploy
|
|
|
455
459
|
? JSON.parse(fs.readFileSync(`./engine-private/conf/${deployId}/conf.volume.json`, 'utf8'))
|
|
456
460
|
: [];
|
|
457
461
|
if (confVolume.length > 0) {
|
|
462
|
+
// Mirror deployVolume's data-node resolution so the generated manifest
|
|
463
|
+
// pins the PV to the same node that physically receives the volume data.
|
|
464
|
+
const pvDataNode = Underpost.deploy.resolveDeployNode({
|
|
465
|
+
node: options.node,
|
|
466
|
+
kind: options.kind,
|
|
467
|
+
kubeadm: options.kubeadm,
|
|
468
|
+
k3s: options.k3s,
|
|
469
|
+
env,
|
|
470
|
+
});
|
|
458
471
|
let volumeYaml = '';
|
|
459
472
|
for (const deploymentVersion of deploymentVersions) {
|
|
460
473
|
for (const volume of confVolume) {
|
|
@@ -466,6 +479,7 @@ ${Underpost.deploy
|
|
|
466
479
|
pvcId,
|
|
467
480
|
namespace: options.namespace,
|
|
468
481
|
hostPath,
|
|
482
|
+
nodeName: pvDataNode,
|
|
469
483
|
})}\n`;
|
|
470
484
|
}
|
|
471
485
|
}
|
|
@@ -548,7 +562,7 @@ ${Underpost.deploy
|
|
|
548
562
|
const yamlPath = `./engine-private/conf/${deployId}/build/${env}/secret.yaml`;
|
|
549
563
|
fs.writeFileSync(yamlPath, secretYaml, 'utf8');
|
|
550
564
|
} else {
|
|
551
|
-
const deploymentsFiles = ['Dockerfile', 'proxy.yaml', 'deployment.yaml', 'pv-pvc.yaml'];
|
|
565
|
+
const deploymentsFiles = ['Dockerfile', 'proxy.yaml', 'deployment.yaml', 'pv-pvc.yaml', 'grpc-service.yaml'];
|
|
552
566
|
for (const file of deploymentsFiles) {
|
|
553
567
|
if (fs.existsSync(`./engine-private/conf/${deployId}/build/${env}/${file}`)) {
|
|
554
568
|
fs.copyFileSync(
|
|
@@ -720,7 +734,8 @@ spec:
|
|
|
720
734
|
* @param {string} options.image - Docker image for the deployment.
|
|
721
735
|
* @param {string} options.traffic - Traffic status for the deployment.
|
|
722
736
|
* @param {string} options.replicas - Number of replicas for the deployment.
|
|
723
|
-
* @param {string} options.node -
|
|
737
|
+
* @param {string} options.node - Explicit target node (highest precedence in the node chain). When empty, {@link UnderpostDeploy.resolveDeployNode} falls back to the cluster-type default (`kind-worker` for kind, host for kubeadm/k3s). Used for both volume placement and hostPath PV nodeAffinity.
|
|
738
|
+
* @param {string} [options.sshKeyPath] - Private key path for node SSH operations, forwarded to deployVolume when shipping a hostPath volume to a remote target node over SSH. Defaults to engine-private/deploy/id_rsa.
|
|
724
739
|
* @param {boolean} options.disableUpdateDeployment - Whether to disable deployment updates.
|
|
725
740
|
* @param {boolean} options.disableUpdateProxy - Whether to disable proxy updates.
|
|
726
741
|
* @param {boolean} options.disableDeploymentProxy - Whether to disable deployment proxy.
|
|
@@ -938,9 +953,16 @@ EOF`);
|
|
|
938
953
|
env,
|
|
939
954
|
version,
|
|
940
955
|
namespace,
|
|
941
|
-
nodeName:
|
|
956
|
+
nodeName: Underpost.deploy.resolveDeployNode({
|
|
957
|
+
node: options.node,
|
|
958
|
+
kind: options.kind,
|
|
959
|
+
kubeadm: options.kubeadm,
|
|
960
|
+
k3s: options.k3s,
|
|
961
|
+
env,
|
|
962
|
+
}),
|
|
942
963
|
clusterContext: options.k3s ? 'k3s' : options.kubeadm ? 'kubeadm' : 'kind',
|
|
943
964
|
gitClean: options.gitClean || false,
|
|
965
|
+
sshKeyPath: options.sshKeyPath || '',
|
|
944
966
|
});
|
|
945
967
|
}
|
|
946
968
|
|
|
@@ -987,10 +1009,17 @@ EOF`);
|
|
|
987
1009
|
*/
|
|
988
1010
|
configMap(env, namespace = 'default') {
|
|
989
1011
|
const cronDeployId = cronDeployIdResolve() || 'dd-cron';
|
|
1012
|
+
const envFilePath = `/home/dd/engine/engine-private/conf/${cronDeployId}/.env.${env}`;
|
|
1013
|
+
// `--from-env-file` turns every KEY=VALUE into a secret key that the Deployment injects via
|
|
1014
|
+
// `envFrom`. Strip shell/runtime-critical keys (notably PATH) first — an injected PATH
|
|
1015
|
+
// overrides the image's own and breaks coreutils/sudo resolution inside the pod.
|
|
1016
|
+
const sanitizedEnvPath = `${envFilePath}.secret`;
|
|
1017
|
+
fs.writeFileSync(sanitizedEnvPath, Underpost.secret.sanitizeSecretEnvFile(fs.readFileSync(envFilePath, 'utf8')));
|
|
990
1018
|
shellExec(`kubectl delete secret underpost-config -n ${namespace} --ignore-not-found`);
|
|
991
1019
|
shellExec(
|
|
992
|
-
`kubectl create secret generic underpost-config --from-env-file
|
|
1020
|
+
`kubectl create secret generic underpost-config --from-env-file=${sanitizedEnvPath} --dry-run=client -o yaml | kubectl apply -f - -n ${namespace}`,
|
|
993
1021
|
);
|
|
1022
|
+
fs.removeSync(sanitizedEnvPath);
|
|
994
1023
|
},
|
|
995
1024
|
/**
|
|
996
1025
|
* Switches the traffic for a deployment.
|
|
@@ -1036,6 +1065,37 @@ EOF`);
|
|
|
1036
1065
|
Underpost.env.set(`${deployId}-${env}-traffic`, targetTraffic);
|
|
1037
1066
|
},
|
|
1038
1067
|
|
|
1068
|
+
/**
|
|
1069
|
+
* Resolves the effective target node for a deployment, applying a single
|
|
1070
|
+
* precedence chain shared by every deploy workflow — the default `deploy`
|
|
1071
|
+
* callback, `run sync`, and custom `run instance` — so node customization
|
|
1072
|
+
* behaves identically everywhere:
|
|
1073
|
+
*
|
|
1074
|
+
* 1. **Explicit node** — `node` (the resolved `--node` value). Upstream
|
|
1075
|
+
* runners derive it from the comma-path field or `--node-name`
|
|
1076
|
+
* (`run sync`: `path.split(',')[4]` > `--node-name` > default) and from
|
|
1077
|
+
* `--node-name` directly (`run instance`).
|
|
1078
|
+
* 2. **Cluster-type default** — when no explicit node is given: `kind-worker`
|
|
1079
|
+
* for a kind cluster (the node that hosts kind hostPath volumes),
|
|
1080
|
+
* otherwise the control-plane / current host (`os.hostname()`) for
|
|
1081
|
+
* kubeadm / k3s. With no explicit cluster flag, `development` is treated
|
|
1082
|
+
* as kind and `production` as the host, preserving legacy behaviour.
|
|
1083
|
+
*
|
|
1084
|
+
* @param {object} params
|
|
1085
|
+
* @param {string} [params.node=''] - Explicit node (`--node`); highest precedence.
|
|
1086
|
+
* @param {boolean} [params.kind=false] - Kind cluster context.
|
|
1087
|
+
* @param {boolean} [params.kubeadm=false] - Kubeadm cluster context.
|
|
1088
|
+
* @param {boolean} [params.k3s=false] - K3s cluster context.
|
|
1089
|
+
* @param {string} [params.env=''] - Deployment environment; tie-breaker when no cluster flag is set.
|
|
1090
|
+
* @returns {string} The effective node name.
|
|
1091
|
+
* @memberof UnderpostDeploy
|
|
1092
|
+
*/
|
|
1093
|
+
resolveDeployNode({ node = '', kind = false, kubeadm = false, k3s = false, env = '' } = {}) {
|
|
1094
|
+
if (node) return node;
|
|
1095
|
+
const isKind = kind || (!kubeadm && !k3s && env !== 'production');
|
|
1096
|
+
return isKind ? 'kind-worker' : os.hostname();
|
|
1097
|
+
},
|
|
1098
|
+
|
|
1039
1099
|
/**
|
|
1040
1100
|
* Deploys a volume for a deployment.
|
|
1041
1101
|
* @param {object} volume - Volume configuration.
|
|
@@ -1047,9 +1107,10 @@ EOF`);
|
|
|
1047
1107
|
* @param {string} options.env - Environment for the deployment.
|
|
1048
1108
|
* @param {string} options.version - Version of the deployment.
|
|
1049
1109
|
* @param {string} options.namespace - Kubernetes namespace for the deployment.
|
|
1050
|
-
* @param {string} options.nodeName -
|
|
1110
|
+
* @param {string} options.nodeName - Effective target node (already resolved via {@link UnderpostDeploy.resolveDeployNode}). The volume data is written/shipped here and the PV is pinned to it; an empty value falls back to the cluster-type default inside this method.
|
|
1051
1111
|
* @param {string} [options.clusterContext='kind'] - Cluster context type ('kind', 'kubeadm', or 'k3s').
|
|
1052
1112
|
* @param {boolean} [options.gitClean=false] - Whether to run git clean on volumeMountPath before copying.
|
|
1113
|
+
* @param {string} [options.sshKeyPath=''] - Private key path used when the target node is remote and the volume is shipped over SSH. Empty falls back to copyDirToNode's default (engine-private/deploy/id_rsa).
|
|
1053
1114
|
* @memberof UnderpostDeploy
|
|
1054
1115
|
*/
|
|
1055
1116
|
deployVolume(
|
|
@@ -1062,6 +1123,7 @@ EOF`);
|
|
|
1062
1123
|
nodeName: '',
|
|
1063
1124
|
clusterContext: 'kind',
|
|
1064
1125
|
gitClean: false,
|
|
1126
|
+
sshKeyPath: '',
|
|
1065
1127
|
},
|
|
1066
1128
|
) {
|
|
1067
1129
|
if (!volume.claimName) {
|
|
@@ -1076,16 +1138,48 @@ EOF`);
|
|
|
1076
1138
|
if (options.gitClean && volume.volumeMountPath) {
|
|
1077
1139
|
Underpost.repo.clean({ paths: [volume.volumeMountPath] });
|
|
1078
1140
|
}
|
|
1141
|
+
// The node that physically receives the volume data. hostPath volumes are
|
|
1142
|
+
// node-local, so the data must land on the node where the pod will run, and
|
|
1143
|
+
// the PV is pinned there (nodeAffinity) so the scheduler co-locates the pod
|
|
1144
|
+
// with its volume — never mounting an empty DirectoryOrCreate on another node.
|
|
1145
|
+
let dataNode;
|
|
1079
1146
|
if (clusterContext === 'kind') {
|
|
1080
1147
|
const kindNode = options.nodeName || 'kind-worker';
|
|
1148
|
+
dataNode = kindNode;
|
|
1081
1149
|
shellExec(`docker exec -i ${kindNode} bash -c "mkdir -p ${rootVolumeHostPath}"`);
|
|
1082
1150
|
shellExec(`tar -C ${volume.volumeMountPath} -c . | docker cp - ${kindNode}:${rootVolumeHostPath}`);
|
|
1083
1151
|
shellExec(
|
|
1084
1152
|
`docker exec -i ${kindNode} bash -c "chown -R 1000:1000 ${rootVolumeHostPath}; chmod -R 755 ${rootVolumeHostPath}"`,
|
|
1085
1153
|
);
|
|
1086
1154
|
} else {
|
|
1087
|
-
|
|
1088
|
-
|
|
1155
|
+
const localHost = os.hostname();
|
|
1156
|
+
dataNode = options.nodeName || localHost;
|
|
1157
|
+
if (dataNode === localHost) {
|
|
1158
|
+
// Target node is the control plane / current host: write directly.
|
|
1159
|
+
if (!fs.existsSync(rootVolumeHostPath)) fs.mkdirSync(rootVolumeHostPath, { recursive: true });
|
|
1160
|
+
fs.copySync(volume.volumeMountPath, rootVolumeHostPath);
|
|
1161
|
+
} else {
|
|
1162
|
+
// Target node is remote: fs.copySync would only write the control-plane
|
|
1163
|
+
// filesystem, leaving the real node's hostPath empty. Ship the folder to
|
|
1164
|
+
// the node over SSH so the data exists where the pod is pinned.
|
|
1165
|
+
const nodeHost =
|
|
1166
|
+
shellExec(
|
|
1167
|
+
`kubectl get node ${dataNode} -o jsonpath='{.status.addresses[?(@.type=="InternalIP")].address}'`,
|
|
1168
|
+
{ stdout: true, silent: true, silentOnError: true },
|
|
1169
|
+
).trim() || dataNode;
|
|
1170
|
+
logger.info('Shipping volume to remote node over SSH', {
|
|
1171
|
+
node: dataNode,
|
|
1172
|
+
host: nodeHost,
|
|
1173
|
+
src: volume.volumeMountPath,
|
|
1174
|
+
dest: rootVolumeHostPath,
|
|
1175
|
+
});
|
|
1176
|
+
Underpost.ssh.copyDirToNode({
|
|
1177
|
+
host: nodeHost,
|
|
1178
|
+
localDir: volume.volumeMountPath,
|
|
1179
|
+
remoteDir: rootVolumeHostPath,
|
|
1180
|
+
...(options.sshKeyPath ? { keyPath: options.sshKeyPath } : {}),
|
|
1181
|
+
});
|
|
1182
|
+
}
|
|
1089
1183
|
}
|
|
1090
1184
|
shellExec(`kubectl delete pvc ${pvcId} -n ${namespace} --ignore-not-found`);
|
|
1091
1185
|
shellExec(`kubectl delete pv ${pvId} --ignore-not-found`);
|
|
@@ -1094,6 +1188,7 @@ ${Underpost.deploy.persistentVolumeFactory({
|
|
|
1094
1188
|
hostPath: rootVolumeHostPath,
|
|
1095
1189
|
pvcId,
|
|
1096
1190
|
namespace,
|
|
1191
|
+
nodeName: dataNode,
|
|
1097
1192
|
})}
|
|
1098
1193
|
EOF
|
|
1099
1194
|
`);
|
|
@@ -1182,11 +1277,28 @@ ${secret ? ` readOnly: true\n` : ''}`;
|
|
|
1182
1277
|
* @param {string} options.hostPath - Host path for the persistent volume.
|
|
1183
1278
|
* @param {string} options.pvcId - Persistent volume claim ID.
|
|
1184
1279
|
* @param {string} [options.namespace='default'] - Kubernetes namespace for the PVC claimRef.
|
|
1280
|
+
* @param {string} [options.nodeName=''] - Node name to which the persistent volume is pinned (optional).
|
|
1185
1281
|
* @returns {string} - YAML configuration for the persistent volume and claim.
|
|
1186
1282
|
* @memberof UnderpostDeploy
|
|
1187
1283
|
*/
|
|
1188
|
-
persistentVolumeFactory({ hostPath, pvcId, namespace = 'default' }) {
|
|
1284
|
+
persistentVolumeFactory({ hostPath, pvcId, namespace = 'default', nodeName = '' }) {
|
|
1189
1285
|
const pvId = pvcId.replace(/^pvc-/, 'pv-');
|
|
1286
|
+
// hostPath volumes are node-local: deployVolume writes the content to the
|
|
1287
|
+
// filesystem of a single node. Without nodeAffinity the scheduler can place
|
|
1288
|
+
// the pod on a different node and mount an empty DirectoryOrCreate hostPath
|
|
1289
|
+
// (missing the materialized assets). Pin the PV to the node that holds the
|
|
1290
|
+
// data so the pod is always co-located with its volume.
|
|
1291
|
+
const nodeAffinity = nodeName
|
|
1292
|
+
? `
|
|
1293
|
+
nodeAffinity:
|
|
1294
|
+
required:
|
|
1295
|
+
nodeSelectorTerms:
|
|
1296
|
+
- matchExpressions:
|
|
1297
|
+
- key: kubernetes.io/hostname
|
|
1298
|
+
operator: In
|
|
1299
|
+
values:
|
|
1300
|
+
- ${nodeName}`
|
|
1301
|
+
: '';
|
|
1190
1302
|
return `apiVersion: v1
|
|
1191
1303
|
kind: PersistentVolume
|
|
1192
1304
|
metadata:
|
|
@@ -1197,7 +1309,7 @@ spec:
|
|
|
1197
1309
|
accessModes:
|
|
1198
1310
|
- ReadWriteOnce
|
|
1199
1311
|
persistentVolumeReclaimPolicy: Retain
|
|
1200
|
-
storageClassName: manual
|
|
1312
|
+
storageClassName: manual${nodeAffinity}
|
|
1201
1313
|
claimRef:
|
|
1202
1314
|
apiVersion: v1
|
|
1203
1315
|
kind: PersistentVolumeClaim
|
|
@@ -77,14 +77,42 @@ class UnderpostDockerCompose {
|
|
|
77
77
|
return nodePath.isAbsolute(relPath) ? relPath : nodePath.join(getRootDirectory(), relPath);
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
+
/**
|
|
81
|
+
* Resolves the canonical directory for a custom docker-compose workflow,
|
|
82
|
+
* keyed by `--deploy-id` + `--docker-compose-id`:
|
|
83
|
+
* `engine-private/conf/<deploy-id>/docker-compose/<docker-compose-id>`.
|
|
84
|
+
* This directory ships its own `docker-compose.yml`, `compose.env`, and
|
|
85
|
+
* `nginx.conf` (used as-is, never generated). Returns null when
|
|
86
|
+
* `--docker-compose-id` is not set (the default, self-generating workflow).
|
|
87
|
+
* @param {object} options - CLI options.
|
|
88
|
+
* @returns {string|null} Repo-root-relative canonical dir, or null.
|
|
89
|
+
* @memberof UnderpostDockerCompose
|
|
90
|
+
*/
|
|
91
|
+
static composeIdBase(options = {}) {
|
|
92
|
+
if (!options.dockerComposeId) return null;
|
|
93
|
+
const deployId = options.deployId || DEFAULT_DEPLOY_ID;
|
|
94
|
+
return `engine-private/conf/${deployId}/docker-compose/${options.dockerComposeId}`;
|
|
95
|
+
}
|
|
96
|
+
|
|
80
97
|
/**
|
|
81
98
|
* Builds the base `docker compose` invocation with explicit file and env-file,
|
|
82
99
|
* so behavior is independent of the caller's working directory.
|
|
100
|
+
*
|
|
101
|
+
* Custom workflow (`--docker-compose-id`): the compose file, env-file, and
|
|
102
|
+
* bind-mounted config (nginx.conf, mongodb/) all live in the canonical dir, so
|
|
103
|
+
* compose runs with `--project-directory` pinned there and no app-override.
|
|
83
104
|
* @param {object} options - CLI options.
|
|
84
105
|
* @returns {string} The base command string (without a subcommand).
|
|
85
106
|
* @memberof UnderpostDockerCompose
|
|
86
107
|
*/
|
|
87
108
|
static baseCmd(options = {}) {
|
|
109
|
+
const base = UnderpostDockerCompose.composeIdBase(options);
|
|
110
|
+
if (base) {
|
|
111
|
+
const projectDir = UnderpostDockerCompose.resolve(base);
|
|
112
|
+
const composeFile = UnderpostDockerCompose.resolve(options.composeFile || `${base}/docker-compose.yml`);
|
|
113
|
+
const envFile = UnderpostDockerCompose.resolve(options.envFile || `${base}/compose.env`);
|
|
114
|
+
return `docker compose --project-directory ${projectDir} --env-file ${envFile} -f ${composeFile}`;
|
|
115
|
+
}
|
|
88
116
|
const composeFile = UnderpostDockerCompose.resolve(options.composeFile || 'docker-compose.yml');
|
|
89
117
|
const envFile = UnderpostDockerCompose.resolve(options.envFile || 'docker/compose.env');
|
|
90
118
|
const overrideFile = UnderpostDockerCompose.resolve(options.appOverride || 'docker/compose.app.yml');
|
|
@@ -163,7 +191,7 @@ services:
|
|
|
163
191
|
MONGO_IMAGE=mongo:latest
|
|
164
192
|
VALKEY_IMAGE=valkey/valkey:latest
|
|
165
193
|
APP_IMAGE=underpost/underpost-engine
|
|
166
|
-
APP_TAG=v3.2.
|
|
194
|
+
APP_TAG=v3.2.70
|
|
167
195
|
PROXY_IMAGE=nginx:stable-alpine
|
|
168
196
|
PROMETHEUS_IMAGE=prom/prometheus:latest
|
|
169
197
|
GRAFANA_IMAGE=grafana/grafana:latest
|
|
@@ -343,6 +371,20 @@ datasources:
|
|
|
343
371
|
* @memberof UnderpostDockerCompose
|
|
344
372
|
*/
|
|
345
373
|
static generate(options = {}) {
|
|
374
|
+
// Custom workflow: docker-compose.yml, compose.env, and nginx.conf are
|
|
375
|
+
// pre-authored in the canonical dir and used as-is — do NOT generate them.
|
|
376
|
+
// Only (re)write the required MongoDB entrypoint (replica-set bootstrap),
|
|
377
|
+
// the single generated infra artifact, into <canonical>/mongodb so the
|
|
378
|
+
// stack stays self-contained under `--project-directory`.
|
|
379
|
+
const composeIdBase = UnderpostDockerCompose.composeIdBase(options);
|
|
380
|
+
if (composeIdBase) {
|
|
381
|
+
const mongoEntrypointPath = UnderpostDockerCompose.resolve(`${composeIdBase}/mongodb/entrypoint.sh`);
|
|
382
|
+
fs.mkdirpSync(nodePath.dirname(mongoEntrypointPath));
|
|
383
|
+
fs.writeFileSync(mongoEntrypointPath, UnderpostDockerCompose.mongoEntrypointContent(), { mode: 0o755 });
|
|
384
|
+
logger.info('mongodb entrypoint written (custom workflow)', { path: mongoEntrypointPath });
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
|
|
346
388
|
Nginx.removeRouter();
|
|
347
389
|
for (const { host, routes } of PROXY_HOSTS) Nginx.createApp({ host, routes });
|
|
348
390
|
Nginx.createDefaultServer(DEFAULT_UPSTREAM);
|
|
@@ -449,6 +491,20 @@ datasources:
|
|
|
449
491
|
silentOnError: true,
|
|
450
492
|
});
|
|
451
493
|
|
|
494
|
+
// Custom workflow: the compose file, compose.env, and nginx.conf are
|
|
495
|
+
// hand-authored source — never prune them. Only drop the one generated
|
|
496
|
+
// artifact (the mongo entrypoint), regenerated on the next --up/--generate.
|
|
497
|
+
const composeIdBase = UnderpostDockerCompose.composeIdBase(options);
|
|
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
|
+
logger.info('Docker Compose reset complete. Run `--up` to recreate the stack.');
|
|
505
|
+
return;
|
|
506
|
+
}
|
|
507
|
+
|
|
452
508
|
// Phase 2: prune generated artifacts (regenerated on the next --generate/--up).
|
|
453
509
|
const artifacts = options.force
|
|
454
510
|
? [...GENERATED_ARTIFACTS, options.envFile || 'docker/compose.env']
|
|
@@ -489,6 +545,7 @@ datasources:
|
|
|
489
545
|
* @param {boolean} [options.shell] - Open an interactive shell in `target` (default: app).
|
|
490
546
|
* @param {string} [options.exec] - General-purpose passthrough docker compose subcommand.
|
|
491
547
|
* @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 + nginx.conf, used as-is), skipping nginx/env/app-override generation. Used by the Cyberia MMO ecosystem (`--deploy-id dd-cyberia --docker-compose-id cyberia`).
|
|
492
549
|
* @param {string} [options.env] - Deployment environment for non-default deploy ids (default: development).
|
|
493
550
|
* @param {string} [options.composeFile] - Override compose file path.
|
|
494
551
|
* @param {string} [options.envFile] - Override env-file path.
|
package/src/cli/env.js
CHANGED
|
@@ -143,12 +143,21 @@ class UnderpostRootEnv {
|
|
|
143
143
|
/**
|
|
144
144
|
* @method clean
|
|
145
145
|
* @description Cleans the underpost root environment by removing the environment file.
|
|
146
|
+
* @param {object} options - Options for cleaning the environment.
|
|
147
|
+
* @param {Array<string>} [options.keepKeys=[]] - List of keys to keep in the environment file. If provided, only these keys will be retained.
|
|
146
148
|
* @memberof UnderpostEnv
|
|
147
149
|
*/
|
|
148
|
-
clean() {
|
|
150
|
+
clean(options = { keepKeys: [] }) {
|
|
151
|
+
const { keepKeys } = options;
|
|
149
152
|
const exeRootPath = `${getNpmRootPath()}/underpost`;
|
|
150
153
|
const envPath = `${exeRootPath}/.env`;
|
|
151
|
-
fs.
|
|
154
|
+
if (keepKeys && keepKeys.length > 0 && fs.existsSync(envPath)) {
|
|
155
|
+
const env = dotenv.parse(fs.readFileSync(envPath, 'utf8'));
|
|
156
|
+
const filteredEnv = Object.fromEntries(Object.entries(env).filter(([key]) => keepKeys.includes(key)));
|
|
157
|
+
writeEnv(envPath, filteredEnv);
|
|
158
|
+
} else {
|
|
159
|
+
fs.removeSync(envPath);
|
|
160
|
+
}
|
|
152
161
|
},
|
|
153
162
|
/**
|
|
154
163
|
* @method isInsideContainer
|
package/src/cli/fs.js
CHANGED
|
@@ -64,6 +64,31 @@ class UnderpostFileStorage {
|
|
|
64
64
|
writeStorageConf(storage, storageConf) {
|
|
65
65
|
if (storage) fs.writeFileSync(storageConf, JSON.stringify(storage, null, 4), 'utf8');
|
|
66
66
|
},
|
|
67
|
+
/**
|
|
68
|
+
* @method gitTrack
|
|
69
|
+
* @description Optional, non-fatal Git tracking layer. Any Git error is logged and swallowed
|
|
70
|
+
* so it can never interrupt or roll back the canonical `storage.*.json` workflow.
|
|
71
|
+
* @param {string} gitPath - The working directory to stage/commit.
|
|
72
|
+
* @param {object} [options] - Tracking options.
|
|
73
|
+
* @param {boolean} [options.init=false] - If true, initialize a local repo before staging.
|
|
74
|
+
* @param {string} [options.message=''] - Explicit commit message; when omitted, `underpost cmt` is used.
|
|
75
|
+
* @memberof UnderpostFileStorage
|
|
76
|
+
*/
|
|
77
|
+
gitTrack(gitPath, options = { init: false, message: '' }) {
|
|
78
|
+
try {
|
|
79
|
+
if (options.init === true) Underpost.repo.initLocalRepo({ path: gitPath });
|
|
80
|
+
shellExec(`cd ${gitPath} && git add .`, { silentOnError: true, silent: true, disableLog: true });
|
|
81
|
+
if (options.message)
|
|
82
|
+
shellExec(`cd ${gitPath} && git commit -m "${options.message}"`, {
|
|
83
|
+
silentOnError: true,
|
|
84
|
+
silent: true,
|
|
85
|
+
disableLog: true,
|
|
86
|
+
});
|
|
87
|
+
else shellExec(`underpost cmt ${gitPath} feat`, { silentOnError: true, silent: true, disableLog: true });
|
|
88
|
+
} catch (error) {
|
|
89
|
+
logger.warn('git tracking skipped (non-fatal)', { gitPath, error: error?.message });
|
|
90
|
+
}
|
|
91
|
+
},
|
|
67
92
|
/**
|
|
68
93
|
* @method recursiveCallback
|
|
69
94
|
* @description Recursively processes files and directories based on the provided options.
|
|
@@ -125,6 +150,7 @@ class UnderpostFileStorage {
|
|
|
125
150
|
|
|
126
151
|
if (hasPathFilter && options.force === true && fs.existsSync(basePath)) fs.removeSync(basePath);
|
|
127
152
|
|
|
153
|
+
// Storage is canonical: persist the removal before any (optional) git tracking runs.
|
|
128
154
|
Underpost.fs.writeStorageConf(storage, storageConf);
|
|
129
155
|
|
|
130
156
|
if (associatedPaths.length === 0)
|
|
@@ -136,13 +162,8 @@ class UnderpostFileStorage {
|
|
|
136
162
|
});
|
|
137
163
|
|
|
138
164
|
if (options.git === true) {
|
|
139
|
-
const gitPath = hasPathFilter ?
|
|
140
|
-
|
|
141
|
-
shellExec(`underpost cmt ${gitPath} feat`, {
|
|
142
|
-
silentOnError: true,
|
|
143
|
-
silent: true,
|
|
144
|
-
disableLog: true,
|
|
145
|
-
});
|
|
165
|
+
const gitPath = !hasPathFilter ? '.' : isSingleFile ? parentDir : basePath;
|
|
166
|
+
Underpost.fs.gitTrack(gitPath);
|
|
146
167
|
}
|
|
147
168
|
|
|
148
169
|
return;
|
|
@@ -151,7 +172,16 @@ class UnderpostFileStorage {
|
|
|
151
172
|
// For single files, run getDeleteFiles against the parent directory to avoid
|
|
152
173
|
// trying to `cd` into a file.
|
|
153
174
|
const gitContextPath = isSingleFile ? parentDir : path;
|
|
154
|
-
|
|
175
|
+
// Detecting locally-deleted files is a best-effort enhancement backed by git; if the path is
|
|
176
|
+
// not a repo (or git is unavailable) it must not block the canonical storage workflow.
|
|
177
|
+
let deleteFiles = [];
|
|
178
|
+
if (options.pull !== true) {
|
|
179
|
+
try {
|
|
180
|
+
deleteFiles = Underpost.repo.getDeleteFiles(gitContextPath);
|
|
181
|
+
} catch (error) {
|
|
182
|
+
logger.warn('delete detection skipped (git unavailable)', { path: gitContextPath, error: error?.message });
|
|
183
|
+
}
|
|
184
|
+
}
|
|
155
185
|
|
|
156
186
|
// When processing a single file, only consider it for deletion
|
|
157
187
|
for (const relativePath of deleteFiles) {
|
|
@@ -172,12 +202,7 @@ class UnderpostFileStorage {
|
|
|
172
202
|
if (pullSkipCount > 0) logger.warn(`Pull skipped ${pullSkipCount} files that already exist`);
|
|
173
203
|
// Only run git init/commit when the caller explicitly requests git tracking (--git flag).
|
|
174
204
|
// For bundle pulls into ./build the git step is unwanted and would error on a non-repo path.
|
|
175
|
-
if (options.git === true) {
|
|
176
|
-
Underpost.repo.initLocalRepo({ path: gitContextPath });
|
|
177
|
-
shellExec(`cd ${gitContextPath} && git add . && git commit -m "Base pull state"`, {
|
|
178
|
-
silentOnError: true,
|
|
179
|
-
});
|
|
180
|
-
}
|
|
205
|
+
if (options.git === true) Underpost.fs.gitTrack(gitContextPath, { init: true, message: 'Base pull state' });
|
|
181
206
|
} else {
|
|
182
207
|
let files;
|
|
183
208
|
if (isSingleFile) {
|
|
@@ -200,15 +225,9 @@ class UnderpostFileStorage {
|
|
|
200
225
|
} else logger.warn('File already exists', _path);
|
|
201
226
|
}
|
|
202
227
|
}
|
|
228
|
+
// Storage is canonical and always persisted; git is an optional layer on top.
|
|
203
229
|
Underpost.fs.writeStorageConf(storage, storageConf);
|
|
204
|
-
if (options.git === true)
|
|
205
|
-
shellExec(`cd ${gitContextPath} && git add .`);
|
|
206
|
-
shellExec(`underpost cmt ${gitContextPath} feat`, {
|
|
207
|
-
silentOnError: true,
|
|
208
|
-
silent: true,
|
|
209
|
-
disableLog: true,
|
|
210
|
-
});
|
|
211
|
-
}
|
|
230
|
+
if (options.git === true) Underpost.fs.gitTrack(gitContextPath);
|
|
212
231
|
},
|
|
213
232
|
/**
|
|
214
233
|
* @method callback
|
|
@@ -230,10 +249,12 @@ class UnderpostFileStorage {
|
|
|
230
249
|
path,
|
|
231
250
|
options = { rm: false, recursive: false, deployId: '', force: false, pull: false, git: false, omitUnzip: false },
|
|
232
251
|
) {
|
|
233
|
-
|
|
252
|
+
// rm always routes through recursiveCallback so storage.*.json is updated regardless of
|
|
253
|
+
// --recursive/--git. The bare `delete` primitive only removes the remote asset and would
|
|
254
|
+
// otherwise leave the tracked storage key orphaned.
|
|
255
|
+
if (options.recursive === true || options.git === true || options.rm === true)
|
|
234
256
|
return await Underpost.fs.recursiveCallback(path, options);
|
|
235
257
|
if (options.pull === true) return await Underpost.fs.pull(path, options);
|
|
236
|
-
if (options.rm === true) return await Underpost.fs.delete(path, options);
|
|
237
258
|
return await Underpost.fs.upload(path, options);
|
|
238
259
|
},
|
|
239
260
|
/**
|