underpost 3.2.70 → 3.2.80
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 +1177 -1038
- package/CLI-HELP.md +3 -1
- package/README.md +3 -3
- package/bin/build.js +10 -4
- 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/package.json +10 -10
- package/scripts/test-monitor.sh +1 -1
- 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 +31 -11
- package/src/cli/db.js +4 -2
- package/src/cli/deploy.js +51 -9
- package/src/cli/docker-compose.js +163 -9
- package/src/cli/fs.js +0 -1
- package/src/cli/image.js +25 -7
- package/src/cli/index.js +5 -0
- package/src/cli/release.js +4 -0
- package/src/cli/repository.js +13 -2
- package/src/cli/run.js +151 -78
- package/src/cli/ssh.js +30 -11
- package/src/client/components/core/Modal.js +38 -4
- package/src/index.js +1 -1
- package/src/server/conf.js +163 -0
- package/src/server/downloader.js +3 -3
- package/src/server/middlewares.js +152 -0
package/src/cli/run.js
CHANGED
|
@@ -14,7 +14,9 @@ import {
|
|
|
14
14
|
etcHostFactory,
|
|
15
15
|
getNpmRootPath,
|
|
16
16
|
isDeployRunnerContext,
|
|
17
|
+
loadConfInstances,
|
|
17
18
|
loadConfServerJson,
|
|
19
|
+
selectConfInstances,
|
|
18
20
|
loadReplicas,
|
|
19
21
|
writeEnv,
|
|
20
22
|
} from '../server/conf.js';
|
|
@@ -49,6 +51,32 @@ const waitForPort = (port, host = '127.0.0.1', { maxAttempts = 30, interval = 20
|
|
|
49
51
|
|
|
50
52
|
const logger = loggerFactory(import.meta);
|
|
51
53
|
|
|
54
|
+
/**
|
|
55
|
+
* @method instanceProxyRoutesFactory
|
|
56
|
+
* @description Renders the HTTPProxy route block for every instance sharing a host.
|
|
57
|
+
* Routes are emitted longest-prefix first so a specific instance path (`/FOREST`)
|
|
58
|
+
* is never shadowed by the default instance's catch-all (`/`).
|
|
59
|
+
* @param {string} deployId - Parent deployment identifier.
|
|
60
|
+
* @param {Array<object>} instances - Expanded instance entries bound to one host.
|
|
61
|
+
* @param {string} env - `development` | `production`.
|
|
62
|
+
* @param {Object<string,string>} trafficById - Instance id → traffic colour.
|
|
63
|
+
* @returns {string} Concatenated route YAML.
|
|
64
|
+
*/
|
|
65
|
+
const instanceProxyRoutesFactory = ({ deployId, instances, env, trafficById }) =>
|
|
66
|
+
[...instances]
|
|
67
|
+
.sort((a, b) => (b.path || '/').length - (a.path || '/').length)
|
|
68
|
+
.map((instance) =>
|
|
69
|
+
Underpost.deploy.deploymentYamlServiceFactory({
|
|
70
|
+
path: instance.path,
|
|
71
|
+
port: env === 'development' && instance.fromDebugPort ? instance.fromDebugPort : instance.fromPort,
|
|
72
|
+
deployId: `${deployId}-${instance.id}`,
|
|
73
|
+
env,
|
|
74
|
+
deploymentVersions: [trafficById[instance.id] || 'blue'],
|
|
75
|
+
pathRewritePolicy: instance.pathRewritePolicy,
|
|
76
|
+
}),
|
|
77
|
+
)
|
|
78
|
+
.join('');
|
|
79
|
+
|
|
52
80
|
/**
|
|
53
81
|
* @constant DEFAULT_OPTION
|
|
54
82
|
* @description Default options for the UnderpostRun class.
|
|
@@ -123,6 +151,7 @@ const logger = loggerFactory(import.meta);
|
|
|
123
151
|
* @property {boolean} pullBundle - Whether to pull the bundle before running. Use together with --skip-full-build to skip the local build entirely (supported by: sync, template-deploy).
|
|
124
152
|
* @property {boolean} remove - Whether to remove/teardown resources instead of creating them (e.g. delete-expose for k3s proxy devices in dev-cluster).
|
|
125
153
|
* @property {boolean} test - Whether to enable test/generic-purpose mode (e.g. use self-signed TLS instead of cert-manager).
|
|
154
|
+
* @property {string} branch - The Git branch to use for operations (e.g., for template-deploy, ssh-deploy).
|
|
126
155
|
* @memberof UnderpostRun
|
|
127
156
|
*/
|
|
128
157
|
const DEFAULT_OPTION = {
|
|
@@ -193,6 +222,7 @@ const DEFAULT_OPTION = {
|
|
|
193
222
|
pullBundle: false,
|
|
194
223
|
remove: false,
|
|
195
224
|
test: false,
|
|
225
|
+
branch: '',
|
|
196
226
|
};
|
|
197
227
|
|
|
198
228
|
/**
|
|
@@ -714,10 +744,11 @@ class UnderpostRun {
|
|
|
714
744
|
if (deployConfId) inputs.deploy_conf_id = deployConfId;
|
|
715
745
|
if (deployType) inputs.deploy_type = deployType;
|
|
716
746
|
|
|
747
|
+
// Omit `ref` so dispatchWorkflow auto-detects the repo's default branch
|
|
748
|
+
// (a fork may default to `main` rather than the monorepo's `master`).
|
|
717
749
|
Underpost.repo.dispatchWorkflow({
|
|
718
750
|
repo,
|
|
719
751
|
workflowFile: 'npmpkg.ci.yml',
|
|
720
|
-
ref: 'master',
|
|
721
752
|
inputs,
|
|
722
753
|
});
|
|
723
754
|
},
|
|
@@ -761,7 +792,7 @@ class UnderpostRun {
|
|
|
761
792
|
* @memberof UnderpostRun
|
|
762
793
|
*/
|
|
763
794
|
'docker-image': (path, options = DEFAULT_OPTION) => {
|
|
764
|
-
const repo = Underpost.repo.resolveInstanceRepo(path, options.
|
|
795
|
+
const repo = Underpost.repo.resolveInstanceRepo(path, !options.test);
|
|
765
796
|
Underpost.repo.dispatchWorkflow({
|
|
766
797
|
repo,
|
|
767
798
|
workflowFile: `docker-image${path ? `.${path}` : ''}${options.dev ? '.dev' : ''}.ci.yml`,
|
|
@@ -841,11 +872,13 @@ class UnderpostRun {
|
|
|
841
872
|
job = 'init';
|
|
842
873
|
confId = path.replace(/^init-/, '');
|
|
843
874
|
}
|
|
844
|
-
const repo = Underpost.repo.resolveInstanceRepo(confId, options.
|
|
875
|
+
const repo = Underpost.repo.resolveInstanceRepo(confId, !options.test);
|
|
876
|
+
// Omit `ref` so dispatchWorkflow auto-detects the target repo's default
|
|
877
|
+
// branch (getDefaultBranch): the monorepo is `master` but instance repos
|
|
878
|
+
// like engine-cyberia default to `main` — hardcoding either 422s.
|
|
845
879
|
Underpost.repo.dispatchWorkflow({
|
|
846
880
|
repo,
|
|
847
881
|
workflowFile: `${confId}.cd.yml`,
|
|
848
|
-
ref: 'master',
|
|
849
882
|
inputs: { job },
|
|
850
883
|
});
|
|
851
884
|
},
|
|
@@ -1182,66 +1215,54 @@ echo -e "[code]\nname=Visual Studio Code\nbaseurl=https://packages.microsoft.com
|
|
|
1182
1215
|
|
|
1183
1216
|
'instance-promote': async (path, options = DEFAULT_OPTION) => {
|
|
1184
1217
|
const env = options.dev ? 'development' : 'production';
|
|
1185
|
-
const baseCommand = options.dev ? 'node bin' : 'underpost';
|
|
1186
|
-
const baseClusterCommand = options.dev ? ' --dev' : '';
|
|
1187
1218
|
let [deployId, id] = path.split(',');
|
|
1188
|
-
const confInstances =
|
|
1189
|
-
|
|
1190
|
-
);
|
|
1219
|
+
const confInstances = loadConfInstances(deployId);
|
|
1220
|
+
const promoted = selectConfInstances(confInstances, id);
|
|
1191
1221
|
let promotedTraffic = '';
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
metadata: _metadata,
|
|
1205
|
-
} = instance;
|
|
1206
|
-
if (id !== _id) continue;
|
|
1207
|
-
const _deployId = `${deployId}-${_id}`;
|
|
1208
|
-
// Use debug ports in development when defined, fall back to production ports.
|
|
1209
|
-
if (env === 'development' && _fromDebugPort) _fromPort = _fromDebugPort;
|
|
1210
|
-
if (env === 'development' && _toDebugPort) _toPort = _toDebugPort;
|
|
1211
|
-
const currentTraffic = Underpost.deploy.getCurrentTraffic(_deployId, {
|
|
1212
|
-
hostTest: _host,
|
|
1222
|
+
|
|
1223
|
+
// A Contour HTTPProxy is named after its host, so every instance sharing a
|
|
1224
|
+
// host shares one object. Rebuilding it from the promoted instance alone
|
|
1225
|
+
// would drop its siblings' routes, so each host is rendered from the full
|
|
1226
|
+
// set: promoted instances flip colour, the rest keep their live colour.
|
|
1227
|
+
const promotedIds = new Set(promoted.map((instance) => instance.id));
|
|
1228
|
+
const hosts = [...new Set(promoted.map((instance) => instance.host))];
|
|
1229
|
+
const affected = confInstances.filter((instance) => hosts.includes(instance.host));
|
|
1230
|
+
const trafficById = {};
|
|
1231
|
+
for (const instance of affected) {
|
|
1232
|
+
const currentTraffic = Underpost.deploy.getCurrentTraffic(`${deployId}-${instance.id}`, {
|
|
1233
|
+
hostTest: instance.host,
|
|
1213
1234
|
namespace: options.namespace,
|
|
1235
|
+
env,
|
|
1214
1236
|
});
|
|
1215
|
-
|
|
1216
|
-
|
|
1237
|
+
if (!promotedIds.has(instance.id)) {
|
|
1238
|
+
trafficById[instance.id] = currentTraffic || 'blue';
|
|
1239
|
+
continue;
|
|
1240
|
+
}
|
|
1241
|
+
trafficById[instance.id] = currentTraffic ? (currentTraffic === 'blue' ? 'green' : 'blue') : 'blue';
|
|
1242
|
+
promotedTraffic = trafficById[instance.id];
|
|
1243
|
+
}
|
|
1244
|
+
|
|
1245
|
+
for (const host of hosts) {
|
|
1246
|
+
const hostInstances = affected.filter((instance) => instance.host === host);
|
|
1217
1247
|
let proxyYaml =
|
|
1218
|
-
Underpost.deploy.baseProxyYamlFactory({ host
|
|
1219
|
-
|
|
1220
|
-
path: _path,
|
|
1221
|
-
port: _fromPort,
|
|
1222
|
-
// serviceId: deployId,
|
|
1223
|
-
deployId: _deployId,
|
|
1224
|
-
env,
|
|
1225
|
-
deploymentVersions: [targetTraffic],
|
|
1226
|
-
// pathRewritePolicy,
|
|
1227
|
-
});
|
|
1248
|
+
Underpost.deploy.baseProxyYamlFactory({ host, env: options.tls ? 'production' : env, options }) +
|
|
1249
|
+
instanceProxyRoutesFactory({ deployId, instances: hostInstances, env, trafficById });
|
|
1228
1250
|
if (options.tls) {
|
|
1229
1251
|
if (options.test) {
|
|
1230
|
-
const sslDir = `./engine-private/ssl/${
|
|
1231
|
-
const nameSafe =
|
|
1252
|
+
const sslDir = `./engine-private/ssl/${host}`;
|
|
1253
|
+
const nameSafe = host.replace(/[^a-zA-Z0-9_.-]/g, '_');
|
|
1232
1254
|
fs.mkdirpSync(sslDir);
|
|
1233
|
-
shellExec(`bash ./scripts/ssl.sh "${sslDir}" "${
|
|
1234
|
-
shellExec(`kubectl delete secret ${
|
|
1255
|
+
shellExec(`bash ./scripts/ssl.sh "${sslDir}" "${host}"`);
|
|
1256
|
+
shellExec(`kubectl delete secret ${host} -n ${options.namespace} --ignore-not-found`);
|
|
1235
1257
|
shellExec(
|
|
1236
|
-
`kubectl create secret tls ${
|
|
1258
|
+
`kubectl create secret tls ${host} --cert="${sslDir}/${nameSafe}.pem" --key="${sslDir}/${nameSafe}-key.pem" -n ${options.namespace}`,
|
|
1237
1259
|
);
|
|
1238
1260
|
} else {
|
|
1239
|
-
shellExec(`sudo kubectl delete Certificate ${
|
|
1240
|
-
proxyYaml += Underpost.deploy.buildCertManagerCertificate({ ...options, host
|
|
1261
|
+
shellExec(`sudo kubectl delete Certificate ${host} -n ${options.namespace} --ignore-not-found`);
|
|
1262
|
+
proxyYaml += Underpost.deploy.buildCertManagerCertificate({ ...options, host });
|
|
1241
1263
|
}
|
|
1242
1264
|
}
|
|
1243
|
-
|
|
1244
|
-
shellExec(`kubectl delete HTTPProxy ${_host} --namespace ${options.namespace} --ignore-not-found`);
|
|
1265
|
+
shellExec(`kubectl delete HTTPProxy ${host} --namespace ${options.namespace} --ignore-not-found`);
|
|
1245
1266
|
shellExec(
|
|
1246
1267
|
`kubectl apply -f - -n ${options.namespace} <<EOF
|
|
1247
1268
|
${proxyYaml}
|
|
@@ -1276,9 +1297,7 @@ EOF
|
|
|
1276
1297
|
const baseClusterCommand = options.dev ? ' --dev' : '';
|
|
1277
1298
|
let [deployId, id, replicas] = path.split(',');
|
|
1278
1299
|
if (!replicas) replicas = options.replicas;
|
|
1279
|
-
const confInstances =
|
|
1280
|
-
fs.readFileSync(`./engine-private/conf/${deployId}/conf.instances.json`, 'utf8'),
|
|
1281
|
-
);
|
|
1300
|
+
const confInstances = selectConfInstances(loadConfInstances(deployId), id);
|
|
1282
1301
|
const etcHosts = [];
|
|
1283
1302
|
for (const instance of confInstances) {
|
|
1284
1303
|
let {
|
|
@@ -1297,7 +1316,6 @@ EOF
|
|
|
1297
1316
|
readinessProbe: _readinessProbe,
|
|
1298
1317
|
livenessProbe: _livenessProbe,
|
|
1299
1318
|
} = instance;
|
|
1300
|
-
if (id !== _id) continue;
|
|
1301
1319
|
const _deployId = `${deployId}-${_id}`;
|
|
1302
1320
|
// Use debug ports in development when defined, fall back to production ports.
|
|
1303
1321
|
if (env === 'development' && _fromDebugPort) _fromPort = _fromDebugPort;
|
|
@@ -1321,6 +1339,7 @@ EOF
|
|
|
1321
1339
|
const currentTraffic = Underpost.deploy.getCurrentTraffic(_deployId, {
|
|
1322
1340
|
hostTest: _host,
|
|
1323
1341
|
namespace: options.namespace,
|
|
1342
|
+
env,
|
|
1324
1343
|
});
|
|
1325
1344
|
|
|
1326
1345
|
const targetTraffic = currentTraffic ? (currentTraffic === 'blue' ? 'green' : 'blue') : 'blue';
|
|
@@ -1426,13 +1445,15 @@ EOF
|
|
|
1426
1445
|
logger.error(`Deployment ${deployId} did not become ready in time.`);
|
|
1427
1446
|
return;
|
|
1428
1447
|
}
|
|
1429
|
-
shellExec(
|
|
1430
|
-
`${baseCommand} run${baseClusterCommand} --namespace ${options.namespace}` +
|
|
1431
|
-
`${options.nodeName ? ` --node-name ${options.nodeName}` : ''}` +
|
|
1432
|
-
`${options.tls ? ` --tls ${options.test ? '--test' : ''}` : ''}` +
|
|
1433
|
-
` instance-promote '${path}'`,
|
|
1434
|
-
);
|
|
1435
1448
|
}
|
|
1449
|
+
// Promote (switch the HTTPProxy to the new colour) ONLY after EVERY
|
|
1450
|
+
// instance in the family is deployed and ready — never per-instance inside
|
|
1451
|
+
// the loop. A per-variant switch would flip server.cyberiaonline.com to the
|
|
1452
|
+
// default's new deployment while -forest/-test are still being created,
|
|
1453
|
+
// pointing routes at services that don't exist yet. instance-promote
|
|
1454
|
+
// rebuilds the whole host proxy (all variant routes) in one shot, so a
|
|
1455
|
+
// single call with the family id promotes the family atomically.
|
|
1456
|
+
if (!options.expose) await UnderpostRun.RUNNERS['instance-promote'](`${deployId},${id}`, options);
|
|
1436
1457
|
if (options.etcHosts) {
|
|
1437
1458
|
const hostListenResult = etcHostFactory(etcHosts);
|
|
1438
1459
|
logger.info(hostListenResult.renderHosts);
|
|
@@ -1489,26 +1510,42 @@ EOF
|
|
|
1489
1510
|
const env = options.dev ? 'development' : 'production';
|
|
1490
1511
|
let [deployId, id, projectPath] = path.split(',');
|
|
1491
1512
|
const rootPath = projectPath ? projectPath : '.';
|
|
1513
|
+
|
|
1514
|
+
const confInstances = loadConfInstances(deployId);
|
|
1515
|
+
// Targeting a template id builds every world in the family. The fan-out
|
|
1516
|
+
// re-enters this runner with `instanceOnly` because the default world
|
|
1517
|
+
// keeps the template id verbatim — without it, selecting `mmo-server`
|
|
1518
|
+
// would match the family again and recurse forever. Only that default
|
|
1519
|
+
// world publishes to the project root, so the repo keeps exactly one
|
|
1520
|
+
// canonical Dockerfile/deployment.yaml pair.
|
|
1521
|
+
const selected = options.instanceOnly
|
|
1522
|
+
? confInstances.filter((instance) => instance.id === id)
|
|
1523
|
+
: selectConfInstances(confInstances, id);
|
|
1524
|
+
if (selected.length === 0) {
|
|
1525
|
+
logger.error(`Instance with id '${id}' not found in conf.instances.json for deployId '${deployId}'`);
|
|
1526
|
+
return;
|
|
1527
|
+
}
|
|
1528
|
+
if (!options.instanceOnly && (selected.length > 1 || selected[0].id !== id)) {
|
|
1529
|
+
for (const instance of selected)
|
|
1530
|
+
UnderpostRun.RUNNERS['instance-build-manifest'](
|
|
1531
|
+
[deployId, instance.id, projectPath].filter((v) => v !== undefined).join(','),
|
|
1532
|
+
{ ...options, instanceOnly: true },
|
|
1533
|
+
);
|
|
1534
|
+
return;
|
|
1535
|
+
}
|
|
1536
|
+
|
|
1492
1537
|
const envManifestPath = `${rootPath}/manifests/deployments/${id}-${env}`;
|
|
1493
1538
|
const outputPath = `${envManifestPath}/deployment.yaml`;
|
|
1494
1539
|
const dockerfileManifestPath = `${envManifestPath}/Dockerfile`;
|
|
1495
1540
|
|
|
1496
1541
|
fs.mkdirpSync(envManifestPath);
|
|
1497
1542
|
|
|
1498
|
-
const
|
|
1499
|
-
|
|
1500
|
-
);
|
|
1501
|
-
|
|
1502
|
-
const instance = confInstances.find((i) => i.id === id);
|
|
1503
|
-
if (!instance) {
|
|
1504
|
-
logger.error(`Instance with id '${id}' not found in conf.instances.json for deployId '${deployId}'`);
|
|
1505
|
-
return;
|
|
1506
|
-
}
|
|
1543
|
+
const instance = selected[0];
|
|
1544
|
+
const isDefaultInstance = instance.id === instance.templateId || !instance.templateId;
|
|
1507
1545
|
|
|
1508
1546
|
let {
|
|
1509
1547
|
id: _id,
|
|
1510
1548
|
host: _host,
|
|
1511
|
-
path: _path,
|
|
1512
1549
|
image: _image,
|
|
1513
1550
|
fromPort: _fromPort,
|
|
1514
1551
|
toPort: _toPort,
|
|
@@ -1651,15 +1688,19 @@ EOF
|
|
|
1651
1688
|
})}\n`;
|
|
1652
1689
|
}
|
|
1653
1690
|
|
|
1654
|
-
// proxy.yaml —
|
|
1691
|
+
// proxy.yaml — this instance's OWN route only (its sub-path → its own
|
|
1692
|
+
// service), so each variant's build dir carries a distinct, instance-scoped
|
|
1693
|
+
// fragment rather than an identical copy of the whole host proxy. The
|
|
1694
|
+
// complete host HTTPProxy — every variant's route aggregated onto the one
|
|
1695
|
+
// fqdn, each pointing at its live colour — is assembled and applied by
|
|
1696
|
+
// `instance-promote` at deploy time, and only once EVERY variant is ready.
|
|
1655
1697
|
const proxyYaml =
|
|
1656
1698
|
Underpost.deploy.baseProxyYamlFactory({ host: _host, env, options }) +
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
deployId: _deployId,
|
|
1699
|
+
instanceProxyRoutesFactory({
|
|
1700
|
+
deployId,
|
|
1701
|
+
instances: [instance],
|
|
1661
1702
|
env,
|
|
1662
|
-
|
|
1703
|
+
trafficById: { [instance.id]: targetTraffic },
|
|
1663
1704
|
});
|
|
1664
1705
|
|
|
1665
1706
|
// grpc-service.yaml — the parent deploy's gRPC ClusterIP (shared; the
|
|
@@ -1693,7 +1734,39 @@ EOF
|
|
|
1693
1734
|
grpcService: !!grpcServiceYaml,
|
|
1694
1735
|
});
|
|
1695
1736
|
|
|
1696
|
-
|
|
1737
|
+
// --- Per-instance env files -----------------------------------------
|
|
1738
|
+
// Each env file is seeded from the template instance's env file for the
|
|
1739
|
+
// same mode so operator-owned keys (API keys, memory limits, service URLs)
|
|
1740
|
+
// are inherited verbatim; only the keys the instance declares under
|
|
1741
|
+
// `multiInstance.env` — plus the derived container id — are (re)written.
|
|
1742
|
+
//
|
|
1743
|
+
// A derived instance's env dir is generated in full: both development.env
|
|
1744
|
+
// and production.env are written on every build, so a deploy in either
|
|
1745
|
+
// environment always finds the env file its `cmd` sources, no matter which
|
|
1746
|
+
// mode this build ran. The default/template instance owns the committed
|
|
1747
|
+
// source files, so only its current-mode file is idempotently refreshed.
|
|
1748
|
+
if (instance.templateId) {
|
|
1749
|
+
const instanceEnvDir = `./engine-private/conf/${deployId}/instances/${_id}/env`;
|
|
1750
|
+
fs.mkdirpSync(instanceEnvDir);
|
|
1751
|
+
const envsToWrite = isDefaultInstance ? [env] : ['development', 'production'];
|
|
1752
|
+
for (const targetEnv of envsToWrite) {
|
|
1753
|
+
const templateEnvPath = `./engine-private/conf/${deployId}/instances/${instance.templateId}/env/${targetEnv}.env`;
|
|
1754
|
+
const baseEnv = fs.existsSync(templateEnvPath) ? dotenv.parse(fs.readFileSync(templateEnvPath, 'utf8')) : {};
|
|
1755
|
+
writeEnv(`${instanceEnvDir}/${targetEnv}.env`, {
|
|
1756
|
+
...baseEnv,
|
|
1757
|
+
...(instance.instanceEnv?.[targetEnv] || {}),
|
|
1758
|
+
CONTAINER_DEPLOY_ID: `${_deployId}-${targetEnv}`,
|
|
1759
|
+
});
|
|
1760
|
+
}
|
|
1761
|
+
logger.info('[instance-build-manifest] Instance env written', {
|
|
1762
|
+
dir: instanceEnvDir,
|
|
1763
|
+
instanceCode: instance.instanceCode,
|
|
1764
|
+
envs: envsToWrite,
|
|
1765
|
+
keys: Object.keys(instance.instanceEnv?.[env] || {}),
|
|
1766
|
+
});
|
|
1767
|
+
}
|
|
1768
|
+
|
|
1769
|
+
if (env === 'production' && isDefaultInstance) {
|
|
1697
1770
|
if (fs.existsSync(dockerfileManifestPath)) {
|
|
1698
1771
|
fs.copyFileSync(dockerfileManifestPath, `${rootPath}/Dockerfile`);
|
|
1699
1772
|
}
|
package/src/cli/ssh.js
CHANGED
|
@@ -532,12 +532,28 @@ EOF`);
|
|
|
532
532
|
if (!host) throw new Error('copyDirToNode requires a host');
|
|
533
533
|
if (!localDir || !fs.existsSync(localDir)) throw new Error(`copyDirToNode: local dir not found: ${localDir}`);
|
|
534
534
|
if (!remoteDir) throw new Error('copyDirToNode requires a remoteDir');
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
535
|
+
try {
|
|
536
|
+
shellExec(`chmod 600 ${keyPath}`, { silent: true, silentOnError: true, disableLog: true });
|
|
537
|
+
const sshOpts = `-i ${keyPath} -o BatchMode=yes -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -p ${port}`;
|
|
538
|
+
shellExec(`ssh ${sshOpts} ${user}@${host} 'mkdir -p ${remoteDir}'`, {
|
|
539
|
+
silent: true,
|
|
540
|
+
disableLog: true,
|
|
541
|
+
});
|
|
542
|
+
shellExec(`tar -C ${localDir} -c . | ssh ${sshOpts} ${user}@${host} 'tar -C ${remoteDir} -x'`, {
|
|
543
|
+
silent: true,
|
|
544
|
+
disableLog: true,
|
|
545
|
+
});
|
|
546
|
+
const fixups =
|
|
547
|
+
`${owner ? `chown -R ${owner} ${remoteDir}; ` : ''}${mode ? `chmod -R ${mode} ${remoteDir}` : ''}`.trim();
|
|
548
|
+
if (fixups)
|
|
549
|
+
shellExec(`ssh ${sshOpts} ${user}@${host} '${fixups}'`, {
|
|
550
|
+
silent: true,
|
|
551
|
+
disableLog: true,
|
|
552
|
+
});
|
|
553
|
+
} catch (err) {
|
|
554
|
+
logger.error(`copyDirToNode failed`);
|
|
555
|
+
process.exit(1);
|
|
556
|
+
}
|
|
541
557
|
},
|
|
542
558
|
|
|
543
559
|
/**
|
|
@@ -703,11 +719,14 @@ EOF
|
|
|
703
719
|
|
|
704
720
|
let last = { ok: false, code: 255, stdout: '', stderr: '', attempts: 0 };
|
|
705
721
|
for (let attempt = 1; attempt <= retries; attempt++) {
|
|
706
|
-
const result = shellExec(
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
722
|
+
const result = shellExec(
|
|
723
|
+
`ssh ${sshOpts} ${user}@${host} bash -s <<'UNDERPOST_SSH_BATCH_EOF'\n${command}\nUNDERPOST_SSH_BATCH_EOF`,
|
|
724
|
+
{
|
|
725
|
+
stdout: false,
|
|
726
|
+
silentOnError: true,
|
|
727
|
+
disableLog: true,
|
|
728
|
+
},
|
|
729
|
+
);
|
|
711
730
|
last = {
|
|
712
731
|
ok: result.code === 0,
|
|
713
732
|
code: result.code,
|
|
@@ -425,8 +425,8 @@ class Modal {
|
|
|
425
425
|
s(`.main-body-btn-ui-menu-menu`).classList.add('hide');
|
|
426
426
|
s(`.main-body-btn-ui-menu-close`).classList.remove('hide');
|
|
427
427
|
if (s(`.btn-bar-center-icon-menu`)) {
|
|
428
|
-
|
|
429
|
-
|
|
428
|
+
sa(`.btn-bar-center-icon-close`).forEach((el) => el.classList.remove('hide'));
|
|
429
|
+
sa(`.btn-bar-center-icon-menu`).forEach((el) => el.classList.add('hide'));
|
|
430
430
|
}
|
|
431
431
|
|
|
432
432
|
s(`.main-body-btn-container`).style[
|
|
@@ -449,8 +449,8 @@ class Modal {
|
|
|
449
449
|
s(`.main-body-btn-ui-menu-close`).classList.add('hide');
|
|
450
450
|
s(`.main-body-btn-ui-menu-menu`).classList.remove('hide');
|
|
451
451
|
if (s(`.btn-bar-center-icon-menu`)) {
|
|
452
|
-
|
|
453
|
-
|
|
452
|
+
sa(`.btn-bar-center-icon-menu`).forEach((el) => el.classList.remove('hide'));
|
|
453
|
+
sa(`.btn-bar-center-icon-close`).forEach((el) => el.classList.add('hide'));
|
|
454
454
|
}
|
|
455
455
|
s(`.main-body-btn-container`).style[
|
|
456
456
|
true || (options.mode && options.mode.match('right')) ? 'right' : 'left'
|
|
@@ -683,6 +683,34 @@ class Modal {
|
|
|
683
683
|
>
|
|
684
684
|
</div>`
|
|
685
685
|
: ''}
|
|
686
|
+
${idModal === 'modal-menu' && options.mode !== 'slide-menu-right'
|
|
687
|
+
? html`<div
|
|
688
|
+
class="abs main-btn-menu-top-container"
|
|
689
|
+
style="bottom: 0px; left: 0px; z-index: 10; height: ${originHeightTopBar}px; width: ${originHeightTopBar}px"
|
|
690
|
+
>
|
|
691
|
+
${await BtnIcon.instance({
|
|
692
|
+
style: `height: 100%`,
|
|
693
|
+
class: `in fll main-btn-menu-top action-bar-box action-btn-center-top`,
|
|
694
|
+
label: html`<div class="abs center">
|
|
695
|
+
<i class="far fa-square btn-bar-center-icon-square hide"></i>
|
|
696
|
+
<span class="btn-bar-center-icon-close hide">${barConfig.buttons.close.label}</span>
|
|
697
|
+
<span class="btn-bar-center-icon-menu">${barConfig.buttons.menu.label}</span>
|
|
698
|
+
</div>`,
|
|
699
|
+
})}
|
|
700
|
+
</div>
|
|
701
|
+
|
|
702
|
+
<style>
|
|
703
|
+
.a-link-top-banner {
|
|
704
|
+
padding-left: 35px;
|
|
705
|
+
}
|
|
706
|
+
.main-body-btn-bar-custom {
|
|
707
|
+
top: 50px !important;
|
|
708
|
+
}
|
|
709
|
+
.main-body-btn-menu {
|
|
710
|
+
display: none;
|
|
711
|
+
}
|
|
712
|
+
</style>`
|
|
713
|
+
: ''}
|
|
686
714
|
</div>`,
|
|
687
715
|
);
|
|
688
716
|
EventsUI.onClick(`.action-btn-profile-log-in`, () => {
|
|
@@ -697,6 +725,12 @@ class Modal {
|
|
|
697
725
|
}
|
|
698
726
|
s(`.main-btn-sign-up`).click();
|
|
699
727
|
});
|
|
728
|
+
if (idModal === 'modal-menu' && options.mode !== 'slide-menu-right') {
|
|
729
|
+
EventsUI.onClick(`.action-btn-center-top`, (e) => {
|
|
730
|
+
e.preventDefault();
|
|
731
|
+
Modal.actionBtnCenter();
|
|
732
|
+
});
|
|
733
|
+
}
|
|
700
734
|
s(`.input-info-${inputSearchBoxId}`).style.textAlign = 'left';
|
|
701
735
|
htmls(`.input-info-${inputSearchBoxId}`, '');
|
|
702
736
|
const inputInfoNode = s(`.input-info-${inputSearchBoxId}`).cloneNode(true);
|